-
\ No newline at end of file
diff --git a/addon/components/modals/create-gateway.hbs b/addon/components/modals/create-gateway.hbs
index 3f58401b..e035cc94 100644
--- a/addon/components/modals/create-gateway.hbs
+++ b/addon/components/modals/create-gateway.hbs
@@ -5,7 +5,7 @@
diff --git a/addon/components/schedule-manager.js b/addon/components/schedule-manager.js
index 90d3d54f..85750755 100644
--- a/addon/components/schedule-manager.js
+++ b/addon/components/schedule-manager.js
@@ -14,21 +14,40 @@ export default class ScheduleManagerComponent extends Component {
const { subject } = this.args;
const { hours } = subject;
- for (let i = 0; i < week.length; i++) {
- const day = week.objectAt(i);
-
+ for (const day of week) {
schedule[day] = [];
}
- for (let i = 0; i < hours.length; i++) {
- const hour = hours.objectAt(i);
+ const list = hours?.toArray?.() ?? Array.from(hours ?? []);
+
+ // Hour rows arrive capitalised ("Monday") from the console and lowercase ("monday")
+ // from the API and seeds; bucket them by name regardless of case.
+ for (const hour of list) {
+ const day = week.find((name) => name.toLowerCase() === String(hour.day_of_week ?? '').toLowerCase());
- schedule[hour.day_of_week].pushObject(hour);
+ if (day) {
+ schedule[day].push(hour);
+ }
}
return schedule;
}
+ /** The week in order, each day with its label in the console's language and its slots. */
+ get days() {
+ const schedule = this.schedule;
+ // 5 Jan 1970 was a Monday; adding the index gives each weekday's name in the current locale.
+ return Object.keys(schedule).map((name, index) => ({
+ name,
+ label: this.intl.formatDate(new Date(Date.UTC(1970, 0, 5 + index, 12)), { weekday: 'long', timeZone: 'UTC' }),
+ hours: schedule[name],
+ }));
+ }
+
+ get hasHours() {
+ return this.days.some((day) => day.hours.length > 0);
+ }
+
@computed('args.hourModelType') get hourModelType() {
const { hourModelType } = this.args;
diff --git a/addon/components/select-option/catalog.hbs b/addon/components/select-option/catalog.hbs
new file mode 100644
index 00000000..c43ea905
--- /dev/null
+++ b/addon/components/select-option/catalog.hbs
@@ -0,0 +1,12 @@
+
diff --git a/addon/components/select-option/food-truck.hbs b/addon/components/select-option/food-truck.hbs
new file mode 100644
index 00000000..dd2a6b30
--- /dev/null
+++ b/addon/components/select-option/food-truck.hbs
@@ -0,0 +1,12 @@
+
diff --git a/addon/components/select-option/network.hbs b/addon/components/select-option/network.hbs
new file mode 100644
index 00000000..b40f9e28
--- /dev/null
+++ b/addon/components/select-option/network.hbs
@@ -0,0 +1,12 @@
+
diff --git a/addon/components/select-option/product.hbs b/addon/components/select-option/product.hbs
new file mode 100644
index 00000000..0a098cae
--- /dev/null
+++ b/addon/components/select-option/product.hbs
@@ -0,0 +1,12 @@
+
diff --git a/addon/components/select-option/promotion.hbs b/addon/components/select-option/promotion.hbs
new file mode 100644
index 00000000..bbdcef21
--- /dev/null
+++ b/addon/components/select-option/promotion.hbs
@@ -0,0 +1,12 @@
+
diff --git a/addon/components/select-option/store.hbs b/addon/components/select-option/store.hbs
new file mode 100644
index 00000000..20fe3866
--- /dev/null
+++ b/addon/components/select-option/store.hbs
@@ -0,0 +1,12 @@
+
diff --git a/addon/components/settings-container.hbs b/addon/components/settings-container.hbs
deleted file mode 100644
index e9c481ba..00000000
--- a/addon/components/settings-container.hbs
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
- {{yield}}
-
-
-
-
\ No newline at end of file
diff --git a/addon/components/store-selector.hbs b/addon/components/store-selector.hbs
index 4b349eac..85d5f9f2 100644
--- a/addon/components/store-selector.hbs
+++ b/addon/components/store-selector.hbs
@@ -1,17 +1,23 @@
-{{#if @activeStore}}
-
+ {{#if @row.isGroup}}
+ {{! A checkout group acts through its orders; the chevron in the reference cell opens them. }}
+ {{else}}
+ {{#if this.primary}}
+
+ {{/if}}
+
+
diff --git a/addon/components/storefront/network/orders/cell/actions.js b/addon/components/storefront/network/orders/cell/actions.js
new file mode 100644
index 00000000..bd788cc1
--- /dev/null
+++ b/addon/components/storefront/network/orders/cell/actions.js
@@ -0,0 +1,45 @@
+import Component from '@glimmer/component';
+import { inject as service } from '@ember/service';
+import { action } from '@ember/object';
+
+/**
+ * The next action for an order promoted to a button, the rest behind an ellipsis.
+ * A checkout group has no action of its own; its orders do.
+ */
+export default class StorefrontNetworkOrdersCellActionsComponent extends Component {
+ @service storefrontOrderActions;
+ @service intl;
+
+ get items() {
+ const { row, column } = this.args;
+
+ if (row?.isGroup) {
+ return [];
+ }
+
+ return this.storefrontOrderActions.actionItemsFor(row, column?.onChange);
+ }
+
+ get primary() {
+ return this.items.find((item) => item.id === 'perform-workflow-action') ?? null;
+ }
+
+ get secondary() {
+ const primary = this.primary;
+ const rest = this.items.filter((item) => item !== primary);
+ const { row, column } = this.args;
+
+ return [
+ { text: this.intl.t('storefront.orders.index.view-order'), icon: 'eye', fn: () => column?.onView?.(row) },
+ ...rest,
+ { separator: true },
+ { text: this.intl.t('storefront.orders.index.delete-order'), icon: 'trash', class: 'text-danger', fn: () => column?.onDelete?.(row) },
+ ];
+ }
+
+ @action run(item) {
+ if (typeof item?.fn === 'function') {
+ item.fn();
+ }
+ }
+}
diff --git a/addon/components/storefront/network/orders/cell/driver.hbs b/addon/components/storefront/network/orders/cell/driver.hbs
new file mode 100644
index 00000000..f10ce121
--- /dev/null
+++ b/addon/components/storefront/network/orders/cell/driver.hbs
@@ -0,0 +1,19 @@
+
+ {{#if this.driver}}
+ {{! The identity cell: photo and name on one line; a shared driver says so on hover. }}
+
+ {{else if this.isPickup}}
+ {{t "storefront.networks.orders.no-driver-needed"}}
+ {{else if (gt this.driverCount 1)}}
+ {{t "storefront.networks.orders.drivers-count" count=this.driverCount}}
+ {{else if this.canAssign}}
+
+ {{else}}
+ {{t "storefront.networks.orders.unassigned"}}
+ {{/if}}
+
diff --git a/addon/components/storefront/network/orders/cell/driver.js b/addon/components/storefront/network/orders/cell/driver.js
new file mode 100644
index 00000000..c0420634
--- /dev/null
+++ b/addon/components/storefront/network/orders/cell/driver.js
@@ -0,0 +1,34 @@
+import Component from '@glimmer/component';
+import { get } from '@ember/object';
+import { relationValue } from '@fleetbase/ember-ui/utils/resource-registry';
+import { rankOf } from '../../../../../utils/order-groups';
+
+export default class StorefrontNetworkOrdersCellDriverComponent extends Component {
+ get row() {
+ return this.args.row;
+ }
+
+ get driver() {
+ if (this.row?.isGroup) {
+ return this.row.driver;
+ }
+
+ return relationValue(this.row, 'driver_assigned') ?? null;
+ }
+
+ get isPickup() {
+ return Boolean(this.row?.isGroup ? this.row.is_pickup : get(this.row, 'meta.is_pickup'));
+ }
+
+ get driverCount() {
+ return this.row?.isGroup ? this.row.driverCount : this.driver ? 1 : 0;
+ }
+
+ get isTerminal() {
+ return this.row?.isGroup ? this.row.allTerminal : rankOf(this.row?.status) >= 4;
+ }
+
+ get canAssign() {
+ return !this.row?.isGroup && !this.isPickup && !this.driver && !this.isTerminal && typeof this.args.column?.onAssign === 'function';
+ }
+}
diff --git a/addon/components/storefront/network/orders/cell/fulfillment.hbs b/addon/components/storefront/network/orders/cell/fulfillment.hbs
new file mode 100644
index 00000000..c03dd9ac
--- /dev/null
+++ b/addon/components/storefront/network/orders/cell/fulfillment.hbs
@@ -0,0 +1,3 @@
+{{#let (if @row.isGroup @row.is_pickup @row.meta.is_pickup) as |isPickup|}}
+
+{{/let}}
diff --git a/addon/components/storefront/network/orders/cell/placed.hbs b/addon/components/storefront/network/orders/cell/placed.hbs
new file mode 100644
index 00000000..cfcd22f6
--- /dev/null
+++ b/addon/components/storefront/network/orders/cell/placed.hbs
@@ -0,0 +1 @@
+{{if @row.isGroup @row.createdAtShort @row.createdAtShort}}
diff --git a/addon/components/storefront/network/orders/cell/reference.hbs b/addon/components/storefront/network/orders/cell/reference.hbs
new file mode 100644
index 00000000..09993c7b
--- /dev/null
+++ b/addon/components/storefront/network/orders/cell/reference.hbs
@@ -0,0 +1,17 @@
+{{#let (if @column.isChild (@column.isChild @row) false) as |isChild|}}
+
+ {{#if @row.isGroup}}
+
+
+ {{@row.id}}
+ {{t "storefront.networks.orders.group-summary" orders=@row.orderCount items=@row.itemCount}}
+
+ {{else}}
+ {{#if isChild}}{{/if}}
+ {{! One line: the tracking number, the order id on hover. }}
+ {{or @row.tracking @row.public_id}}
+ {{/if}}
+
+{{/let}}
diff --git a/addon/components/storefront/network/orders/cell/status.hbs b/addon/components/storefront/network/orders/cell/status.hbs
new file mode 100644
index 00000000..79def0bd
--- /dev/null
+++ b/addon/components/storefront/network/orders/cell/status.hbs
@@ -0,0 +1,7 @@
+
+ {{#if @row.isGroup}}
+
+ {{else}}
+
+ {{/if}}
+
diff --git a/addon/components/storefront/network/orders/cell/stores.hbs b/addon/components/storefront/network/orders/cell/stores.hbs
new file mode 100644
index 00000000..dec2546a
--- /dev/null
+++ b/addon/components/storefront/network/orders/cell/stores.hbs
@@ -0,0 +1,24 @@
+
+ {{#if this.isStack}}
+ {{! A checkout across stores: one round logo per store, its summary on hover. }}
+ {{#each this.visible as |entry|}}
+
+ {{#if entry.store}}
+
+
+ {{else}}
+ {{entry.initial}}
+ {{/if}}
+
+ {{/each}}
+ {{#if this.overflow}}
+ +{{this.overflow}}
+ {{/if}}
+ {{else if this.single.store}}
+
+ {{else if this.single}}
+ {{this.single.name}}
+ {{else}}
+ {{t "storefront.networks.orders.no-store"}}
+ {{/if}}
+
diff --git a/addon/components/storefront/network/orders/cell/stores.js b/addon/components/storefront/network/orders/cell/stores.js
new file mode 100644
index 00000000..53837c5c
--- /dev/null
+++ b/addon/components/storefront/network/orders/cell/stores.js
@@ -0,0 +1,59 @@
+import Component from '@glimmer/component';
+import { orderStoreId, orderStoreName } from '../../../../../utils/order-groups';
+
+const MAX_VISIBLE = 4;
+
+/**
+ * The stores behind a row: one pill for an order, up to four round logos plus an overflow count for a checkout group.
+ */
+export default class StorefrontNetworkOrdersCellStoresComponent extends Component {
+ get entries() {
+ const { row, column } = this.args;
+ const storesById = column?.storesById ?? {};
+ const ids = row?.isGroup ? (row.storeIds ?? []) : [orderStoreId(row)].filter(Boolean);
+ const names = row?.isGroup ? (row.storeNames ?? []) : [orderStoreName(row)].filter(Boolean);
+
+ return ids.map((id, index) => {
+ const name = storesById[id]?.name ?? names[index] ?? names[0] ?? id;
+
+ const store = storesById[id] ?? null;
+
+ return {
+ id,
+ store,
+ name,
+ // An order placed on the network itself carries the network's id.
+ type: String(store?.public_id ?? id).startsWith('network_') ? 'network' : 'store',
+ logo: store?.logo_url ?? null,
+ initial: String(name ?? '?')
+ .charAt(0)
+ .toUpperCase(),
+ };
+ });
+ }
+
+ /** More than one store: round logos only, so the row keeps a single line. */
+ get isStack() {
+ return this.entries.length > 1;
+ }
+
+ /** The one store behind a single order. */
+ get single() {
+ return this.isStack ? null : (this.entries[0] ?? null);
+ }
+
+ get visible() {
+ return this.entries.slice(0, MAX_VISIBLE);
+ }
+
+ get overflow() {
+ return Math.max(0, this.entries.length - MAX_VISIBLE);
+ }
+
+ get overflowTitle() {
+ return this.entries
+ .slice(MAX_VISIBLE)
+ .map((entry) => entry.name)
+ .join(', ');
+ }
+}
diff --git a/addon/components/storefront/network/orders/cell/total.hbs b/addon/components/storefront/network/orders/cell/total.hbs
new file mode 100644
index 00000000..e74fef97
--- /dev/null
+++ b/addon/components/storefront/network/orders/cell/total.hbs
@@ -0,0 +1,7 @@
+{{! One line for an order (its tip on hover); a checkout adds one line on how its tip splits. }}
+
diff --git a/addon/components/storefront/network/sidebar-group.js b/addon/components/storefront/network/sidebar-group.js
new file mode 100644
index 00000000..826777e4
--- /dev/null
+++ b/addon/components/storefront/network/sidebar-group.js
@@ -0,0 +1,8 @@
+import Component from '@glimmer/component';
+
+/**
+ * A small cross-link group under the sidebar navigation: "Member of" in store
+ * context (the store's networks) and "Your stores in this network" in network
+ * context. Items are `{ id, name, icon?, meta? }`; selecting one switches context.
+ */
+export default class StorefrontNetworkSidebarGroupComponent extends Component {}
diff --git a/addon/components/storefront/order/actions.hbs b/addon/components/storefront/order/actions.hbs
index b6204853..f968155e 100644
--- a/addon/components/storefront/order/actions.hbs
+++ b/addon/components/storefront/order/actions.hbs
@@ -1,13 +1,30 @@
-
+{{/if}}
diff --git a/addon/components/storefront/order/details/activity.hbs b/addon/components/storefront/order/details/activity.hbs
index 7270505e..d716be6d 100644
--- a/addon/components/storefront/order/details/activity.hbs
+++ b/addon/components/storefront/order/details/activity.hbs
@@ -1,5 +1,5 @@
{
this.loadActivity.perform();
diff --git a/addon/components/storefront/order/details/checkout-siblings.hbs b/addon/components/storefront/order/details/checkout-siblings.hbs
new file mode 100644
index 00000000..0e5e3abe
--- /dev/null
+++ b/addon/components/storefront/order/details/checkout-siblings.hbs
@@ -0,0 +1,21 @@
+{{! Only a checkout with more than one order has anything to show here. }}
+{{#if (or this.load.isRunning this.siblings.length)}}
+
+ {{#if this.siblings.length}}
+
-
\ No newline at end of file
+
diff --git a/addon/components/storefront/order/details/store.js b/addon/components/storefront/order/details/store.js
index ff10bcc1..c45ae9d3 100644
--- a/addon/components/storefront/order/details/store.js
+++ b/addon/components/storefront/order/details/store.js
@@ -1,5 +1,5 @@
import Component from '@glimmer/component';
-import { action } from '@ember/object';
+import { action, get } from '@ember/object';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
@@ -37,6 +37,20 @@ export default class StorefrontOrderDetailsStoreComponent extends Component {
return this.storefrontDisplay.logo_url;
}
+ /** The second line of the pill: the store's network category when the order carries it, else its currency. */
+ get storeSubtitle() {
+ const meta = this.args.resource?.meta ?? {};
+
+ return meta.storefront_category ?? meta.store_category ?? this.storefrontDisplay.currency ?? null;
+ }
+
+ /** The pickup place as one line, read through the proxy the payload hands back. */
+ get pickupAddress() {
+ const pickup = get(this.args.resource ?? {}, 'payload.pickup');
+
+ return pickup ? (get(pickup, 'address') ?? get(pickup, 'name') ?? null) : null;
+ }
+
get storePhone() {
return this.storefrontDisplay.phone;
}
diff --git a/addon/components/storefront/order/details/tabs/activity.hbs b/addon/components/storefront/order/details/tabs/activity.hbs
new file mode 100644
index 00000000..22295a1a
--- /dev/null
+++ b/addon/components/storefront/order/details/tabs/activity.hbs
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/addon/components/storefront/order/details/tabs/customer.hbs b/addon/components/storefront/order/details/tabs/customer.hbs
new file mode 100644
index 00000000..58f0f523
--- /dev/null
+++ b/addon/components/storefront/order/details/tabs/customer.hbs
@@ -0,0 +1,3 @@
+
+
+
diff --git a/addon/components/storefront/order/details/tabs/data.hbs b/addon/components/storefront/order/details/tabs/data.hbs
new file mode 100644
index 00000000..20b758cd
--- /dev/null
+++ b/addon/components/storefront/order/details/tabs/data.hbs
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/addon/components/storefront/order/details/tabs/items.hbs b/addon/components/storefront/order/details/tabs/items.hbs
new file mode 100644
index 00000000..3c59304d
--- /dev/null
+++ b/addon/components/storefront/order/details/tabs/items.hbs
@@ -0,0 +1,7 @@
+
diff --git a/addon/components/storefront/page-header.hbs b/addon/components/storefront/page-header.hbs
new file mode 100644
index 00000000..137e2d12
--- /dev/null
+++ b/addon/components/storefront/page-header.hbs
@@ -0,0 +1,29 @@
+{{! A page header on the console's section-header metrics (57px, same gutters) with room for an
+ identity (logo or icon), badges and a meta line, which Layout::Section::Header cannot show. }}
+
-
diff --git a/addon/components/storefront/product/form/translations.hbs b/addon/components/storefront/product/form/translations.hbs
index df32cf30..10d25738 100644
--- a/addon/components/storefront/product/form/translations.hbs
+++ b/addon/components/storefront/product/form/translations.hbs
@@ -1,8 +1,88 @@
-
-
-
+{{! The product in other languages: a chip per language, then that language's name and description
+ beside the original, then any custom fields. Empty fields fall back to the original in the app. }}
+
diff --git a/addon/components/storefront/products-loading.hbs b/addon/components/storefront/products-loading.hbs
new file mode 100644
index 00000000..d7c130dc
--- /dev/null
+++ b/addon/components/storefront/products-loading.hbs
@@ -0,0 +1,62 @@
+{{#if @hideHeader}}
+ {{! Under the products toolbar: the list in the view the user chose. }}
+
+ {{#if this.isList}}
+
+ {{else}}
+
+ {{/if}}
+
+{{else}}
+ {{! The whole products page: header, the category rail, the tabs and sort, then the list. }}
+
+
+
+
+
+
+
+
+
+ {{#each (array 0 1 2 3 4)}}
+
+
+
+
+
+
+
+
+ {{/each}}
+
+ {{#each (array 0 1 2 3 4)}}
+
+
+
+
+
+ {{/each}}
+
+
+
+
+
+
+
+ {{#if this.isList}}
+
+ {{else}}
+
+ {{/if}}
+
+
+
+{{/if}}
diff --git a/addon/components/storefront/products-loading.js b/addon/components/storefront/products-loading.js
new file mode 100644
index 00000000..23f8f0d3
--- /dev/null
+++ b/addon/components/storefront/products-loading.js
@@ -0,0 +1,12 @@
+import Component from '@glimmer/component';
+import { getOwner } from '@ember/application';
+
+/**
+ * The products page while its list loads, drawn in whichever view the user chose: a cards
+ * grid for the grid view, table rows for the list view.
+ */
+export default class StorefrontProductsLoadingComponent extends Component {
+ get isList() {
+ return getOwner(this).lookup('controller:products/index')?.viewMode === 'list';
+ }
+}
diff --git a/addon/components/storefront/promotion/builder.hbs b/addon/components/storefront/promotion/builder.hbs
new file mode 100644
index 00000000..866545dd
--- /dev/null
+++ b/addon/components/storefront/promotion/builder.hbs
@@ -0,0 +1,186 @@
+
+
+ <:actions>
+ {{t "storefront.promotions.builder.step-of" step=(add this.stepIndex 1) total=this.steps.length}}
+
+
+
+
+ {{#each this.steps as |step|}}
+
+
+
diff --git a/addon/components/modals/customer-segment-form.js b/addon/components/storefront/promotion/segment-panel.js
similarity index 69%
rename from addon/components/modals/customer-segment-form.js
rename to addon/components/storefront/promotion/segment-panel.js
index bb471532..3ec369cf 100644
--- a/addon/components/modals/customer-segment-form.js
+++ b/addon/components/storefront/promotion/segment-panel.js
@@ -3,12 +3,17 @@ import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { cancel, debounce } from '@ember/runloop';
+import { task } from 'ember-concurrency';
/**
- * Create / edit a customer segment with a live count of matching customers.
+ * Create or edit a customer segment in the shared right-hand panel, with a live count of
+ * the customers its rules match. Replaces the segment modal.
*/
-export default class ModalsCustomerSegmentFormComponent extends Component {
+export default class StorefrontPromotionSegmentPanelComponent extends Component {
@service fetch;
+ @service intl;
+ @service notifications;
+
@tracked matching = null;
@tracked isLoadingCount = false;
@tracked countUnavailable = false;
@@ -34,7 +39,15 @@ export default class ModalsCustomerSegmentFormComponent extends Component {
}
get segment() {
- return this.args.options.segment;
+ return this.args.segment;
+ }
+
+ get title() {
+ return this.intl.t(this.segment.isNew ? 'storefront.promotions.segments.new-segment' : 'storefront.promotions.segments.edit-segment');
+ }
+
+ get isDirty() {
+ return Boolean(this.segment.isNew || this.segment.hasDirtyAttributes);
}
get hasMatching() {
@@ -96,4 +109,27 @@ export default class ModalsCustomerSegmentFormComponent extends Component {
}
}
}
+
+ @task *save() {
+ if (!this.segment.name?.trim()) {
+ return this.notifications.warning(this.intl.t('storefront.promotions.segments.required-name'));
+ }
+
+ try {
+ yield this.segment.save();
+ this.notifications.success(this.intl.t('storefront.promotions.segments.saved-success'));
+ this.args.onSaved?.(this.segment);
+ this.args.onClose?.();
+ } catch (error) {
+ this.notifications.serverError(error);
+ }
+ }
+
+ @action close() {
+ if (typeof this.segment.rollbackAttributes === 'function') {
+ this.segment.rollbackAttributes();
+ }
+
+ this.args.onClose?.();
+ }
}
diff --git a/addon/components/storefront/promotions/hub-tabs.hbs b/addon/components/storefront/promotions/hub-tabs.hbs
new file mode 100644
index 00000000..ea534182
--- /dev/null
+++ b/addon/components/storefront/promotions/hub-tabs.hbs
@@ -0,0 +1,8 @@
+
+ {{#each this.tabs as |tab|}}
+
+ {{/each}}
+
diff --git a/addon/components/storefront/promotions/hub-tabs.js b/addon/components/storefront/promotions/hub-tabs.js
new file mode 100644
index 00000000..d041dda9
--- /dev/null
+++ b/addon/components/storefront/promotions/hub-tabs.js
@@ -0,0 +1,22 @@
+import Component from '@glimmer/component';
+import { getOwner } from '@ember/application';
+import { action } from '@ember/object';
+
+/**
+ * The promotions hub tab strip (promotions, campaigns, segments, pushes, redemptions).
+ * Every hub page renders it directly under its own section header so the header
+ * stays the console's section header and the tabs read as part of the page body.
+ */
+export default class StorefrontPromotionsHubTabsComponent extends Component {
+ get hub() {
+ return getOwner(this).lookup('controller:promotions');
+ }
+
+ get tabs() {
+ return this.hub?.tabs ?? [];
+ }
+
+ @action openTab(tab) {
+ return this.hub?.openTab(tab);
+ }
+}
diff --git a/addon/components/storefront/section-card.hbs b/addon/components/storefront/section-card.hbs
new file mode 100644
index 00000000..93feee80
--- /dev/null
+++ b/addon/components/storefront/section-card.hbs
@@ -0,0 +1,15 @@
+{{! A titled section: a heading row (title, muted subtitle, actions on the right) over the body.
+ Boxed by default where the boards draw a card; `@plain` keeps the heading row and lays the body
+ straight on the page, as the boards do for card grids. Never a collapsible panel. }}
+
+ {{#if (or @title (has-block "actions"))}}
+
+
{{@title}}
+ {{#if @subtitle}}{{@subtitle}}{{/if}}
+
{{yield to="actions"}}
+
+ {{/if}}
+
+ {{yield}}
+
+
diff --git a/addon/components/storefront/section-card.js b/addon/components/storefront/section-card.js
new file mode 100644
index 00000000..3226cd5f
--- /dev/null
+++ b/addon/components/storefront/section-card.js
@@ -0,0 +1,3 @@
+import Component from '@glimmer/component';
+
+export default class StorefrontSectionCardComponent extends Component {}
diff --git a/addon/components/storefront/setting-rule.hbs b/addon/components/storefront/setting-rule.hbs
new file mode 100644
index 00000000..43381bee
--- /dev/null
+++ b/addon/components/storefront/setting-rule.hbs
@@ -0,0 +1,19 @@
+
+
+
+
+
+
{{@label}}
+ {{#if @description}}
+
{{@description}}
+ {{/if}}
+ {{#if (and @value (has-block))}}
+
+ {{yield}}
+
+ {{/if}}
+
+
+
+
+
diff --git a/addon/components/storefront/settings-rail.hbs b/addon/components/storefront/settings-rail.hbs
new file mode 100644
index 00000000..786eea18
--- /dev/null
+++ b/addon/components/storefront/settings-rail.hbs
@@ -0,0 +1,9 @@
+
diff --git a/addon/components/storefront/settings-rail.js b/addon/components/storefront/settings-rail.js
new file mode 100644
index 00000000..5447b53c
--- /dev/null
+++ b/addon/components/storefront/settings-rail.js
@@ -0,0 +1,7 @@
+import templateOnlyComponent from '@ember/component/template-only';
+
+/**
+ * The settings section rail: one entry per section or sub-page, shared by the store
+ * and network settings shells. Items: `{ id, label, icon, isActive, badge? }`.
+ */
+export default templateOnlyComponent();
diff --git a/addon/components/storefront/truck/card.hbs b/addon/components/storefront/truck/card.hbs
new file mode 100644
index 00000000..11ab5b4e
--- /dev/null
+++ b/addon/components/storefront/truck/card.hbs
@@ -0,0 +1,71 @@
+
+
+
+
+
+ {{! The title opens the truck; the vehicle opens from the menu (Open vehicle) or its summary. }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addon/templates/catalogs/loading.hbs b/addon/templates/catalogs/loading.hbs
new file mode 100644
index 00000000..25333cca
--- /dev/null
+++ b/addon/templates/catalogs/loading.hbs
@@ -0,0 +1,5 @@
+{{! Catalogs: a borderless header without a subtitle, status tabs, then catalog cards without media. }}
+
+
+
+
diff --git a/addon/templates/customers/index.hbs b/addon/templates/customers/index.hbs
index 6835d8eb..4bd1671e 100644
--- a/addon/templates/customers/index.hbs
+++ b/addon/templates/customers/index.hbs
@@ -1,16 +1,28 @@
-
+
+
+ {{#unless this.isCards}}
+
+
+ {{/unless}}
+
+
+
+
+ {{#if this.isCards}}
+
+
+
+ {{else}}
+
+
+
+
+
+
+ {{/if}}
+
{{outlet}}
diff --git a/addon/templates/customers/index/view.hbs b/addon/templates/customers/index/view.hbs
index 0aad4e7d..a4fd6b3f 100644
--- a/addon/templates/customers/index/view.hbs
+++ b/addon/templates/customers/index/view.hbs
@@ -1 +1,8 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/addon/templates/customers/loading.hbs b/addon/templates/customers/loading.hbs
new file mode 100644
index 00000000..5acd9b8f
--- /dev/null
+++ b/addon/templates/customers/loading.hbs
@@ -0,0 +1,21 @@
+{{! Customers in the view the page is in: cards, or a table that is not selectable. }}
+
+ {{#if isCards}}
+
+
+
+ {{else}}
+
+ {{/if}}
+
diff --git a/addon/templates/error.hbs b/addon/templates/error.hbs
new file mode 100644
index 00000000..2e47e865
--- /dev/null
+++ b/addon/templates/error.hbs
@@ -0,0 +1,12 @@
+{{! Error substate: a route's model hook rejected. The sidebar stays; the page offers to try again. }}
+
-
+
+
+
+
+
-{{outlet}}
\ No newline at end of file
+ {{outlet}}
+{{else}}
+ {{outlet}}
+{{/if}}
\ No newline at end of file
diff --git a/addon/templates/networks/index/loading.hbs b/addon/templates/networks/index/loading.hbs
new file mode 100644
index 00000000..2720f8bb
--- /dev/null
+++ b/addon/templates/networks/index/loading.hbs
@@ -0,0 +1,2 @@
+{{! A network is loading; its overview is the landing page. }}
+
diff --git a/addon/templates/networks/index/network.hbs b/addon/templates/networks/index/network.hbs
index a1511336..29213fb9 100644
--- a/addon/templates/networks/index/network.hbs
+++ b/addon/templates/networks/index/network.hbs
@@ -1,17 +1,3 @@
-
-
-
-
-
-
-
- {{@model.name}}
-
-
-
-
- {{outlet}}
-
-
-
-
\ No newline at end of file
+
+ {{outlet}}
+
diff --git a/addon/templates/networks/index/network/categories-loading.hbs b/addon/templates/networks/index/network/categories-loading.hbs
new file mode 100644
index 00000000..c0b20e7b
--- /dev/null
+++ b/addon/templates/networks/index/network/categories-loading.hbs
@@ -0,0 +1,13 @@
+{{! Network categories: no search, a new-category action, a table that is not selectable. }}
+
diff --git a/addon/templates/networks/index/network/categories.hbs b/addon/templates/networks/index/network/categories.hbs
new file mode 100644
index 00000000..84649894
--- /dev/null
+++ b/addon/templates/networks/index/network/categories.hbs
@@ -0,0 +1,21 @@
+
+
+
+
+
+
- {{#if (and @model.options.tips_enabled @model.options.multi_cart_enabled)}}
- {{! Off: a multi-store order's store tip goes to the network. On: it is split across the stores by their share of the order. }}
-
+
+
+{{#if this.isInvitePanelOpen}}
+
+{{/if}}
diff --git a/addon/templates/networks/index/network/loading.hbs b/addon/templates/networks/index/network/loading.hbs
new file mode 100644
index 00000000..e20405ad
--- /dev/null
+++ b/addon/templates/networks/index/network/loading.hbs
@@ -0,0 +1,2 @@
+{{! A network page without a skeleton of its own: a header over a table. }}
+
diff --git a/addon/templates/networks/index/network/orders-loading.hbs b/addon/templates/networks/index/network/orders-loading.hbs
new file mode 100644
index 00000000..c88c8f6c
--- /dev/null
+++ b/addon/templates/networks/index/network/orders-loading.hbs
@@ -0,0 +1,13 @@
+{{! Network orders: status tabs and the grouping toggle in the toolbar, rows that are not selectable. }}
+
diff --git a/addon/templates/networks/index/network/orders.hbs b/addon/templates/networks/index/network/orders.hbs
index d759a373..ab42d9d3 100644
--- a/addon/templates/networks/index/network/orders.hbs
+++ b/addon/templates/networks/index/network/orders.hbs
@@ -1,18 +1,59 @@
-
+
+
+
+ {{#each this.statusTabs as |tab|}}
+
+ {{/each}}
+
+
+
+
+
+
+
+
diff --git a/addon/templates/networks/index/network/settings-loading.hbs b/addon/templates/networks/index/network/settings-loading.hbs
new file mode 100644
index 00000000..524804c8
--- /dev/null
+++ b/addon/templates/networks/index/network/settings-loading.hbs
@@ -0,0 +1,23 @@
+{{! Network settings: the header with its copy action, the section rail, then sections of fields in two columns. }}
+
+
+ {{#each (array 0 1 2 3 4 5 6)}}
+
+
+
+
+ {{/each}}
+
+
+ {{#each (array 6 4) as |fields|}}
+
+
+
+
+
+
+
+
+ {{/each}}
+
+
diff --git a/addon/templates/networks/index/network/settings.hbs b/addon/templates/networks/index/network/settings.hbs
new file mode 100644
index 00000000..7c3f765b
--- /dev/null
+++ b/addon/templates/networks/index/network/settings.hbs
@@ -0,0 +1,262 @@
+
{{/if}}
+
+{{#if this.panelCampaign}}
+
+{{/if}}
diff --git a/addon/templates/promotions/index-loading.hbs b/addon/templates/promotions/index-loading.hbs
new file mode 100644
index 00000000..7f578662
--- /dev/null
+++ b/addon/templates/promotions/index-loading.hbs
@@ -0,0 +1,21 @@
+{{! Promotions in the view the page is in: cards three to a row, or the selectable table; status tabs in both. }}
+
+
+
+ {{#if isTable}}
+