@tokenoftrust/storefront-runner 2.0.2 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/package.json +1 -1
  2. package/scripts/lib/tenant-dirs.mjs +80 -0
  3. package/scripts/tenant/lint-embeds.mjs +180 -0
  4. package/scripts/tenant/materialize-tenant-content.mjs +533 -0
  5. package/scripts/tenant/validate.mjs +837 -0
  6. package/apps/storefront/scripts/build-v84-clean-clone-standalone.mjs +0 -68
  7. package/apps/storefront/scripts/deny-v84-clean-clone-standalone-capabilities.mjs +0 -111
  8. package/apps/storefront/scripts/deny-v84-dogfood-capabilities.mjs +0 -2
  9. package/apps/storefront/scripts/deny-v84-lifecycle-capabilities.mjs +0 -247
  10. package/apps/storefront/scripts/dogfood-v84-nonportable-routes.ts +0 -73
  11. package/apps/storefront/scripts/fixed-v84-native-install-producer.ts +0 -79
  12. package/apps/storefront/scripts/generated/v84-clean-clone-replay.bundle.mjs +0 -10982
  13. package/apps/storefront/scripts/import-v84-through-ws2.ts +0 -38
  14. package/apps/storefront/scripts/initialize-v84-lifecycle-tooling-dependency.ts +0 -2
  15. package/apps/storefront/scripts/initialize-v84-lifecycle-tooling.ts +0 -4
  16. package/apps/storefront/scripts/install-v84-native-through-ws1.ts +0 -185
  17. package/apps/storefront/scripts/prove-clone-isolation.ts +0 -506
  18. package/apps/storefront/scripts/replay-v84-clean-clone-pass.ts +0 -148
  19. package/apps/storefront/scripts/replay-v84-clean-clone-standalone.ts +0 -35
  20. package/apps/storefront/scripts/replay-v84-clean-clone.ts +0 -214
  21. package/apps/storefront/scripts/run-fixed-v84-native-install-producer.mjs +0 -18
  22. package/apps/storefront/scripts/run-v84-clone-isolation-proof.mjs +0 -18
  23. package/apps/storefront/scripts/run-v84-clone-lifecycle-proof.mjs +0 -18
  24. package/apps/storefront/scripts/run-v84-import-through-ws2-proof.mjs +0 -17
  25. package/apps/storefront/scripts/run-v84-nonportable-route-dogfood.mjs +0 -15
  26. package/apps/storefront/scripts/sanitize-v84-proof-environment.mjs +0 -21
  27. package/apps/storefront/scripts/v84CleanCloneGraphPin.ts +0 -42
  28. package/apps/storefront/scripts/v84CleanCloneSourceGraph.ts +0 -145
  29. package/apps/storefront/scripts/validate-v84-clone-lifecycle.ts +0 -18
  30. package/apps/storefront/src/components/admin/AdminAIWorkflowsTab.astro +0 -531
  31. package/apps/storefront/src/components/admin/AdminBlogDetailRail.astro +0 -878
  32. package/apps/storefront/src/components/admin/AdminBlogTab.astro +0 -1435
  33. package/apps/storefront/src/components/admin/AdminCheckoutSettingsPanel.astro +0 -226
  34. package/apps/storefront/src/components/admin/AdminCustomersTab.astro +0 -593
  35. package/apps/storefront/src/components/admin/AdminFulfillmentTab.astro +0 -465
  36. package/apps/storefront/src/components/admin/AdminIntegrationsTab.astro +0 -105
  37. package/apps/storefront/src/components/admin/AdminProductsTab.astro +0 -901
  38. package/apps/storefront/src/components/admin/AdminPublishTab.astro +0 -2983
  39. package/apps/storefront/src/components/admin/AdminReportingTab.astro +0 -435
  40. package/apps/storefront/src/components/admin/AdminSettingsTab.astro +0 -782
  41. package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +0 -611
  42. package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +0 -1223
  43. package/apps/storefront/src/lib/dev/cockpitStore.ts +0 -57
  44. package/apps/storefront/src/lib/publish/adminPublishTab.ts +0 -4625
  45. package/apps/storefront/src/lib/publish/apex-readiness.ts +0 -360
  46. package/apps/storefront/src/lib/publish/buildStream.ts +0 -269
  47. package/apps/storefront/src/lib/publish/collapsePersistence.ts +0 -72
  48. package/apps/storefront/src/lib/publish/dispatchHealth.ts +0 -267
  49. package/apps/storefront/src/lib/publish/domainState.ts +0 -360
  50. package/apps/storefront/src/lib/publish/lifecycleState.ts +0 -369
  51. package/apps/storefront/src/lib/publish/liveReleaseSummary.ts +0 -29
  52. package/apps/storefront/src/lib/publish/mergeDoctorOnFailure.ts +0 -78
  53. package/apps/storefront/src/lib/publish/mergeHealthBanner.ts +0 -62
  54. package/apps/storefront/src/lib/publish/mergeRowFindings.ts +0 -124
  55. package/apps/storefront/src/lib/publish/mergeSupportEscalation.ts +0 -75
  56. package/apps/storefront/src/lib/publish/panelPolling.ts +0 -78
  57. package/apps/storefront/src/lib/publish/publish-guard.ts +0 -88
  58. package/apps/storefront/src/lib/publish/publishFormat.ts +0 -165
  59. package/apps/storefront/src/lib/publish/publishHero.ts +0 -177
  60. package/apps/storefront/src/lib/publish/reviewRelease/changeState.ts +0 -66
  61. package/apps/storefront/src/lib/publish/reviewRelease/copy.ts +0 -141
  62. package/apps/storefront/src/lib/publish/reviewRelease/index.ts +0 -41
  63. package/apps/storefront/src/lib/publish/reviewRelease/permissions.ts +0 -47
  64. package/apps/storefront/src/lib/publish/reviewRelease/releaseState.ts +0 -42
  65. package/apps/storefront/src/lib/publish/reviewRelease/types.ts +0 -59
  66. package/apps/storefront/src/lib/publish/reviewRelease/viewModel.ts +0 -440
  67. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/actions.ts +0 -161
  68. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/controller.ts +0 -325
  69. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/render.ts +0 -241
  70. package/apps/storefront/src/lib/publish/shipWorkspace.ts +0 -439
  71. package/apps/storefront/src/lib/publish/sseRoute.ts +0 -119
  72. package/apps/storefront/src/lib/publish/stageBundleUpload.ts +0 -215
  73. package/apps/storefront/src/lib/publish/streamLogConsole.ts +0 -142
  74. package/apps/storefront/src/lib/publish/versionNumber.ts +0 -52
  75. package/apps/storefront/src/pages/admin/ops-timeline.astro +0 -420
  76. package/apps/storefront/src/pages/admin/review-release.astro +0 -265
  77. package/apps/storefront/src/pages/admin/select.astro +0 -96
  78. package/apps/storefront/src/pages/admin.astro +0 -570
  79. package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +0 -86
  80. package/apps/storefront/src/pages/api/apps/admin/gtm-migration-install.ts +0 -174
  81. package/apps/storefront/src/pages/api/apps/admin/gtm-migration.ts +0 -96
  82. package/apps/storefront/src/pages/api/apps/admin/health.ts +0 -44
  83. package/apps/storefront/src/pages/api/apps/admin/install.ts +0 -125
  84. package/apps/storefront/src/pages/api/apps/admin/integration-lifecycle.ts +0 -118
  85. package/apps/storefront/src/pages/api/apps/admin/integrations.ts +0 -89
  86. package/apps/storefront/src/pages/api/apps/admin/list.ts +0 -26
  87. package/apps/storefront/src/pages/api/apps/admin/platform-adapters.ts +0 -85
  88. package/apps/storefront/src/pages/api/apps/admin/resume.ts +0 -79
  89. package/apps/storefront/src/pages/api/apps/admin/suspend.ts +0 -80
  90. package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +0 -98
  91. package/apps/storefront/src/pages/api/apps/admin/update.ts +0 -126
  92. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +0 -70
  93. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries.ts +0 -51
  94. package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +0 -255
  95. package/apps/storefront/src/pages/api/apps/oauth/token.ts +0 -87
  96. package/apps/storefront/src/pages/api/apps/v1/attribution.ts +0 -162
  97. package/apps/storefront/src/pages/api/apps/v1/catalog/products/[handle].ts +0 -39
  98. package/apps/storefront/src/pages/api/apps/v1/catalog/products.ts +0 -49
  99. package/apps/storefront/src/pages/api/apps/v1/health.ts +0 -32
  100. package/apps/storefront/src/pages/api/apps/v1/inventory.ts +0 -59
  101. package/apps/storefront/src/pages/api/apps/v1/orders/[id].ts +0 -48
  102. package/apps/storefront/src/pages/api/apps/v1/orders.ts +0 -73
  103. package/apps/storefront/src/pages/api/apps/v1/reports.ts +0 -21
  104. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +0 -74
  105. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries.ts +0 -47
  106. package/apps/storefront/src/pages/api/cache-purge.ts +0 -55
  107. package/apps/storefront/src/pages/api/compliance/tamper.ts +0 -88
  108. package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +0 -154
  109. package/apps/storefront/src/pages/api/dashboard/vendor-search.ts +0 -120
  110. package/apps/storefront/src/pages/api/integrations/v1/[operation].ts +0 -108
  111. package/apps/storefront/src/pages/api/publish/stage-bundle.ts +0 -70
  112. package/apps/storefront/src/pages/api/test/dev-session.ts +0 -133
  113. package/apps/storefront/src/pages/cockpit.astro +0 -1548
  114. package/apps/storefront/src/pages/dashboard/[appDomain]/apps/index.astro +0 -119
  115. package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +0 -105
  116. package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +0 -177
  117. package/apps/storefront/src/pages/dashboard/index.astro +0 -160
@@ -1,901 +0,0 @@
1
- ---
2
- /**
3
- * Products tab (baseline §Products — catalog readiness and sellability),
4
- * wired to the live products view model (lib/admin/services/mock, from
5
- * data-view-contracts) rather than static prototype data.
6
- */
7
- import { getProductsViewModel } from "@/lib/admin/services/mock";
8
- import type { ProductRow } from "@/lib/admin/viewmodels/products";
9
- import type { AdminTenantEnvelope } from "@/lib/admin/envelope";
10
-
11
- interface Props {
12
- envelope: AdminTenantEnvelope;
13
- }
14
-
15
- const { envelope } = Astro.props;
16
- const cspNonce = Astro.locals.cspNonce;
17
-
18
- const vm = getProductsViewModel(envelope);
19
-
20
- const STATUS_TONE: Record<ProductRow["status"], string> = {
21
- active: "green",
22
- draft: "amber",
23
- archived: "",
24
- };
25
-
26
- function savedViewKeysFor(row: ProductRow): string[] {
27
- const keys = ["all"];
28
- if (!row.sellable) keys.push("not_sellable");
29
- if (row.aiChannelReady) keys.push("ai_channel_ready");
30
- return keys;
31
- }
32
-
33
- const totalVariants = vm.rows.reduce((sum, row) => sum + (vm.detailsById[row.id]?.variants.length ?? 0), 0);
34
- const needsAttentionCount = vm.rows.filter((r) => r.missingExciseCount > 0).length;
35
- const aiReadyCount = vm.rows.filter((r) => r.aiChannelReady).length;
36
- const avgReadiness = vm.rows.length
37
- ? Math.round(vm.rows.reduce((sum, r) => sum + r.readinessScore, 0) / vm.rows.length)
38
- : 0;
39
- const totalExciseGaps = vm.rows.reduce((sum, r) => sum + r.missingExciseCount, 0);
40
-
41
- const defaultView = vm.savedViews.find((v) => v.key === "not_sellable") && needsAttentionCount > 0 ? "not_sellable" : "all";
42
- const defaultRow = vm.rows.find((r) => !r.sellable) ?? vm.rows[0] ?? null;
43
-
44
- ---
45
-
46
- <section id="products" class="band products-tab" aria-labelledby="admin-tab-products products-title" data-admin-panel data-agent-scope="products" role="tabpanel" data-products-target-tenant={vm.targetTenant}>
47
- <div class="section-title">
48
- <div>
49
- <h2 id="products-title">Products</h2>
50
- <p>Catalog, variants, pricing, inventory, media, and sellability.</p>
51
- </div>
52
- <div class="actions">
53
- <button class="btn" type="button"><svg><use href="#i-export"></use></svg>Export</button>
54
- <button class="btn" type="button"><svg><use href="#i-refresh"></use></svg>Import</button>
55
- <button class="btn primary" type="button"><svg><use href="#i-box"></use></svg>Add product</button>
56
- </div>
57
- </div>
58
-
59
- <div class="grid cols-4">
60
- <div class="metric"><span>Active products</span><strong>{vm.rows.length}</strong><small>In this catalog</small></div>
61
- <div class="metric"><span>Variants</span><strong>{totalVariants}</strong><small>Across all products</small></div>
62
- <div class="metric"><span>Needs attention</span><strong>{needsAttentionCount}</strong><small>Missing excise metadata</small></div>
63
- <div class="metric"><span>AI channel ready</span><strong>{aiReadyCount}</strong><small>Of {vm.rows.length} products</small></div>
64
- </div>
65
-
66
- <div class="panel product-intelligence-panel">
67
- <div class="panel-head product-panel-head">
68
- <h3><svg><use href="#i-ask"></use></svg>Catalog Intelligence</h3>
69
- <span class:list={["chip", totalExciseGaps > 0 ? "amber" : "green"]}>{totalExciseGaps} excise gap{totalExciseGaps === 1 ? "" : "s"}</span>
70
- </div>
71
- <div class="panel-body grid">
72
- <div class="product-signal-grid">
73
- <div class="product-signal">
74
- <span class:list={["chip", avgReadiness >= 90 ? "teal" : "amber"]}>Readiness score</span>
75
- <strong>{avgReadiness}%</strong>
76
- <p>Catalog quality, sellability, and excise metadata across all products.</p>
77
- </div>
78
- <div class="product-signal">
79
- <span class:list={["chip", totalExciseGaps > 0 ? "amber" : "green"]}>Excise metadata gaps</span>
80
- <strong>{totalExciseGaps} field{totalExciseGaps === 1 ? "" : "s"}</strong>
81
- <p>Missing tax class, wholesale basis, or volume/weight needed for regulated sale readiness.</p>
82
- </div>
83
- <div class="product-signal">
84
- <span class="chip violet">AI channel readiness</span>
85
- <strong>{aiReadyCount} of {vm.rows.length}</strong>
86
- <p>Products with complete, agentic-commerce-ready listing data.</p>
87
- </div>
88
- </div>
89
- <div class="actions">
90
- <button class="btn primary" type="button">Review excise gaps</button>
91
- <button class="btn" type="button">Draft catalog fixes</button>
92
- <button class="btn" type="button">Schedule readiness audit</button>
93
- </div>
94
- </div>
95
- </div>
96
-
97
- <div class="product-workspace">
98
- <div class="panel product-list-panel">
99
- <div class="panel-head product-panel-head">
100
- <h3><svg><use href="#i-box"></use></svg>Catalog</h3>
101
- <span class="chip teal">{vm.rows.length} products</span>
102
- </div>
103
- <div class="panel-body grid">
104
- <div class="tabs" role="tablist" aria-label="Product saved views" data-products-view-list>
105
- {vm.savedViews.map((view) => (
106
- <button
107
- type="button"
108
- class:list={["tab", { active: view.key === defaultView }]}
109
- data-products-view-button
110
- data-view-key={view.key}
111
- >
112
- {view.label}
113
- </button>
114
- ))}
115
- </div>
116
-
117
- <div class="product-toolbar" aria-label="Product controls">
118
- <label class="product-search">
119
- <span>Search</span>
120
- <input type="search" placeholder="Search products, SKUs, category" aria-label="Search products" data-products-search />
121
- </label>
122
- <div class="product-filters" aria-label="Product filters">
123
- <label>
124
- <span>Status</span>
125
- <select aria-label="Filter by status" data-products-status-select>
126
- <option value="">Any status</option>
127
- <option value="active">Active</option>
128
- <option value="draft">Draft</option>
129
- <option value="archived">Archived</option>
130
- </select>
131
- </label>
132
- <label>
133
- <span>Sellability</span>
134
- <select aria-label="Filter by sellability" data-products-sellability-select>
135
- <option value="">Any</option>
136
- <option value="sellable">Sellable</option>
137
- <option value="blocked">Not sellable</option>
138
- </select>
139
- </label>
140
- </div>
141
- </div>
142
-
143
- <div class="table-wrap products-table-wrap">
144
- <table class="products-table">
145
- <thead>
146
- <tr>
147
- <th>Product</th>
148
- <th>Type</th>
149
- <th>Status</th>
150
- <th>Readiness</th>
151
- <th>Sellability</th>
152
- <th>AI channel</th>
153
- <th class="action">Action</th>
154
- </tr>
155
- </thead>
156
- <tbody data-products-tbody>
157
- {vm.rows.map((row) => (
158
- <tr
159
- class:list={[{ selected: row.id === defaultRow?.id }]}
160
- data-products-row
161
- data-product-id={row.id}
162
- data-view-keys={savedViewKeysFor(row).join(" ")}
163
- data-status={row.status}
164
- data-sellability={row.sellable ? "sellable" : "blocked"}
165
- data-search-haystack={`${row.name} ${row.id} ${row.category} ${row.productType}`.toLowerCase()}
166
- >
167
- <td>
168
- <div class="product-title-cell">
169
- <div>
170
- <strong>{row.name}</strong>
171
- <span class="subtle">{row.category}</span>
172
- </div>
173
- </div>
174
- </td>
175
- <td><strong>{row.productType}</strong></td>
176
- <td><span class:list={["chip", STATUS_TONE[row.status]]}>{row.status[0]!.toUpperCase() + row.status.slice(1)}</span></td>
177
- <td><strong>{row.readinessScore}%</strong>{row.missingExciseCount > 0 && <span class="subtle">{row.missingExciseCount} gap{row.missingExciseCount === 1 ? "" : "s"}</span>}</td>
178
- <td><span class:list={["chip", row.sellable ? "green" : "amber"]}>{row.sellable ? "Sellable" : "Excise gaps"}</span></td>
179
- <td><span class:list={["chip", row.aiChannelReady ? "teal" : ""]}>{row.aiChannelReady ? "Ready" : "Not ready"}</span></td>
180
- <td class="action"><button type="button" class="btn" data-products-select-row={row.id}>Open</button></td>
181
- </tr>
182
- ))}
183
- </tbody>
184
- </table>
185
- <p class="products-empty-state" data-products-empty hidden>No products match the current filters.</p>
186
- </div>
187
- </div>
188
- </div>
189
-
190
- <aside class="panel product-detail-panel" aria-labelledby="product-detail-title" data-products-detail-root>
191
- {vm.rows.map((row) => {
192
- const detail = vm.detailsById[row.id]!;
193
- return (
194
- <div
195
- class="product-detail-record"
196
- data-products-detail-record={row.id}
197
- hidden={row.id !== defaultRow?.id}
198
- >
199
- <div class="panel-head product-panel-head">
200
- <h3 id={row.id === defaultRow?.id ? "product-detail-title" : undefined}><svg><use href="#i-file"></use></svg>{row.name}</h3>
201
- <span class:list={["chip", row.sellable ? "green" : "amber"]}>{row.sellable ? "Sellable" : "Excise gaps"}</span>
202
- </div>
203
- <div class="panel-body grid">
204
- <div class="product-detail-meta">
205
- <div>
206
- <span class="eyebrow">Product type</span>
207
- <strong>{row.productType}</strong>
208
- </div>
209
- <div>
210
- <span class="eyebrow">Category</span>
211
- <strong>{row.category}</strong>
212
- </div>
213
- <div>
214
- <span class="eyebrow">Readiness score</span>
215
- <strong>{row.readinessScore}%</strong>
216
- </div>
217
- </div>
218
-
219
- {!row.sellable && detail.complianceNotes.length > 0 && (
220
- <div class="check-row blocking">
221
- <div><b>Not sellable</b><p>{detail.complianceNotes[0]}</p></div>
222
- <strong>{row.missingExciseCount} missing</strong>
223
- <span class="chip amber">Action</span>
224
- </div>
225
- )}
226
-
227
- <details class="detail-section" open>
228
- <summary class="detail-section-head">
229
- <h4>Variants</h4>
230
- <span class="chip">{detail.variants.length}</span>
231
- </summary>
232
- <div class="mini-table" role="table" aria-label="Variants">
233
- {detail.variants.map((variant) => (
234
- <div class="mini-row" role="row">
235
- <div role="cell"><strong>{variant.name}</strong><span class="subtle">{variant.sku}</span></div>
236
- <div role="cell"><span class="eyebrow">Stock</span><strong>{variant.inventory}</strong></div>
237
- <div role="cell"><span class="eyebrow">Price</span><strong>${variant.price.toFixed(2)}</strong></div>
238
- </div>
239
- ))}
240
- </div>
241
- </details>
242
-
243
- <details class="detail-section">
244
- <summary class="detail-section-head">
245
- <h4>Media</h4>
246
- <span class="chip">{detail.media.length}</span>
247
- </summary>
248
- <div class="mini-table" role="table" aria-label="Media">
249
- {detail.media.map((asset) => (
250
- <div class="mini-row" role="row">
251
- <div role="cell"><strong>{asset.kind[0]!.toUpperCase() + asset.kind.slice(1)}</strong><span class="subtle">{asset.alt}</span></div>
252
- <div role="cell"><span class="subtle">{asset.url}</span></div>
253
- </div>
254
- ))}
255
- </div>
256
- </details>
257
-
258
- <details class="detail-section" open={!row.sellable}>
259
- <summary class="detail-section-head">
260
- <h4>Excise metadata readiness</h4>
261
- <span class:list={["chip", row.missingExciseCount > 0 ? "amber" : "green"]}>{row.missingExciseCount} missing</span>
262
- </summary>
263
- <div>
264
- {detail.exciseChecks.map((check) => (
265
- <div class:list={["check-row", { blocking: !check.present }]}>
266
- <div><b>{check.label}</b><p>{check.present ? `Set to ${check.value}.` : "Not yet set — required for regulated sale readiness."}</p></div>
267
- <strong>{check.present ? check.value : "Missing"}</strong>
268
- <span class:list={["chip", check.present ? "green" : "amber"]}>{check.present ? "OK" : "Fix"}</span>
269
- </div>
270
- ))}
271
- </div>
272
- </details>
273
-
274
- <details class="detail-section">
275
- <summary class="detail-section-head">
276
- <h4>AI channel readiness</h4>
277
- <span class:list={["chip", row.aiChannelReady ? "teal" : "amber"]}>{row.aiChannelReady ? "Ready" : "Not ready"}</span>
278
- </summary>
279
- <div class="check-row">
280
- <div><b>Agentic commerce surfaces</b><p>{row.aiChannelReady ? "Listing data is complete enough for AI shopping agents to represent this product." : "Complete excise metadata and sellability first — AI channel readiness depends on a clean listing."}</p></div>
281
- <strong>{row.aiChannelReady ? "Ready" : "Blocked"}</strong>
282
- <span class:list={["chip", row.aiChannelReady ? "teal" : "amber"]}>{row.aiChannelReady ? "OK" : "Action"}</span>
283
- </div>
284
- </details>
285
-
286
- <details class="detail-section">
287
- <summary class="detail-section-head">
288
- <h4>Activity</h4>
289
- <span class="chip">{detail.activity.length}</span>
290
- </summary>
291
- <div class="mini-table" role="table" aria-label="Activity">
292
- {detail.activity.map((event) => (
293
- <div class="mini-row" role="row">
294
- <div role="cell"><strong>{event.title}</strong><span class="subtle">{event.detail}</span></div>
295
- <div role="cell"><span class="subtle">{new Date(event.at).toLocaleString([], { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" })}</span></div>
296
- </div>
297
- ))}
298
- </div>
299
- </details>
300
- </div>
301
- </div>
302
- );
303
- })}
304
- </aside>
305
- </div>
306
- </section>
307
-
308
- <script is:inline nonce={cspNonce}>
309
- (() => {
310
- const root = document.querySelector('[data-products-target-tenant]');
311
- if (!root) return;
312
-
313
- const viewButtons = Array.from(root.querySelectorAll("[data-products-view-button]"));
314
- const statusSelect = root.querySelector("[data-products-status-select]");
315
- const sellabilitySelect = root.querySelector("[data-products-sellability-select]");
316
- const searchInput = root.querySelector("[data-products-search]");
317
- const rows = Array.from(root.querySelectorAll("[data-products-row]"));
318
- const emptyState = root.querySelector("[data-products-empty]");
319
- const detailRoot = root.querySelector("[data-products-detail-root]");
320
- const detailRecords = detailRoot ? Array.from(detailRoot.querySelectorAll("[data-products-detail-record]")) : [];
321
-
322
- let activeView = (viewButtons.find((b) => b.classList.contains("active")) || viewButtons[0])?.dataset.viewKey ?? "all";
323
-
324
- const applyFilters = () => {
325
- const status = statusSelect ? statusSelect.value : "";
326
- const sellability = sellabilitySelect ? sellabilitySelect.value : "";
327
- const term = searchInput ? searchInput.value.trim().toLowerCase() : "";
328
- let visibleCount = 0;
329
-
330
- for (const row of rows) {
331
- const viewKeys = (row.dataset.viewKeys ?? "").split(" ");
332
- const matchesView = viewKeys.includes(activeView);
333
- const matchesStatus = !status || row.dataset.status === status;
334
- const matchesSellability = !sellability || row.dataset.sellability === sellability;
335
- const matchesSearch = !term || (row.dataset.searchHaystack ?? "").includes(term);
336
- const visible = matchesView && matchesStatus && matchesSellability && matchesSearch;
337
- row.hidden = !visible;
338
- if (visible) visibleCount += 1;
339
- }
340
-
341
- if (emptyState) emptyState.hidden = visibleCount > 0;
342
- };
343
-
344
- const selectProduct = (productId) => {
345
- for (const row of rows) {
346
- row.classList.toggle("selected", row.dataset.productId === productId);
347
- }
348
- for (const record of detailRecords) {
349
- record.hidden = record.dataset.productsDetailRecord !== productId;
350
- }
351
- };
352
-
353
- viewButtons.forEach((button) => {
354
- button.addEventListener("click", () => {
355
- activeView = button.dataset.viewKey ?? "all";
356
- viewButtons.forEach((b) => b.classList.toggle("active", b === button));
357
- applyFilters();
358
- });
359
- });
360
-
361
- statusSelect?.addEventListener("change", applyFilters);
362
- sellabilitySelect?.addEventListener("change", applyFilters);
363
- searchInput?.addEventListener("input", applyFilters);
364
-
365
- root.querySelectorAll("[data-products-select-row]").forEach((button) => {
366
- button.addEventListener("click", () => {
367
- const productId = button.dataset.productsSelectRow;
368
- if (productId) selectProduct(productId);
369
- });
370
- });
371
-
372
- rows.forEach((row) => {
373
- row.addEventListener("click", (event) => {
374
- if (event.target instanceof Element && event.target.closest("[data-products-select-row]")) return;
375
- const productId = row.dataset.productId;
376
- if (productId) selectProduct(productId);
377
- });
378
- });
379
-
380
- applyFilters();
381
- })();
382
- </script>
383
-
384
- <style>
385
- .products-tab {
386
- --product-split: minmax(0, 1.18fr) minmax(360px, 0.82fr);
387
- }
388
-
389
- .products-tab svg {
390
- width: 18px;
391
- height: 18px;
392
- stroke: currentColor;
393
- stroke-width: 1.9;
394
- stroke-linecap: round;
395
- stroke-linejoin: round;
396
- fill: none;
397
- flex: 0 0 auto;
398
- }
399
-
400
- .products-tab button,
401
- .products-tab input,
402
- .products-tab select {
403
- font: inherit;
404
- }
405
-
406
- .products-tab button {
407
- cursor: pointer;
408
- }
409
-
410
- .products-tab .section-title {
411
- display: flex;
412
- align-items: center;
413
- justify-content: space-between;
414
- gap: 12px;
415
- min-width: 0;
416
- }
417
-
418
- .products-tab .section-title h2 {
419
- margin: 0;
420
- font-size: 16px;
421
- }
422
-
423
- .products-tab .section-title p {
424
- margin: 4px 0 0;
425
- color: var(--admin-muted);
426
- font-size: 13px;
427
- }
428
-
429
- .products-tab .actions {
430
- display: flex;
431
- flex-wrap: wrap;
432
- gap: 8px;
433
- min-width: 0;
434
- }
435
-
436
- .products-tab .btn {
437
- min-height: 38px;
438
- max-width: 100%;
439
- display: inline-flex;
440
- align-items: center;
441
- justify-content: center;
442
- gap: 8px;
443
- padding: 0 13px;
444
- border: 1px solid var(--admin-line-strong);
445
- border-radius: var(--admin-radius-sm);
446
- background: var(--admin-surface);
447
- color: var(--admin-ink);
448
- font-weight: 680;
449
- text-decoration: none;
450
- white-space: nowrap;
451
- }
452
-
453
- .products-tab .btn.primary {
454
- border-color: var(--admin-teal-dark);
455
- background: var(--admin-teal);
456
- color: #ffffff;
457
- }
458
-
459
- .products-tab .grid {
460
- display: grid;
461
- grid-template-columns: minmax(0, 1fr);
462
- gap: 12px;
463
- min-width: 0;
464
- }
465
-
466
- .products-tab .grid.cols-4 {
467
- grid-template-columns: repeat(4, minmax(0, 1fr));
468
- }
469
-
470
- .products-tab .panel {
471
- min-width: 0;
472
- border: 1px solid var(--admin-line);
473
- border-radius: var(--admin-radius);
474
- background: var(--admin-surface);
475
- }
476
-
477
- .products-tab .panel-head {
478
- display: flex;
479
- align-items: center;
480
- justify-content: space-between;
481
- gap: 12px;
482
- min-height: 50px;
483
- padding: 13px 14px;
484
- border-bottom: 1px solid var(--admin-line);
485
- }
486
-
487
- .products-tab .panel-head h3 {
488
- margin: 0;
489
- display: flex;
490
- align-items: center;
491
- gap: 8px;
492
- font-size: 15px;
493
- }
494
-
495
- .products-tab .panel-body {
496
- display: grid;
497
- grid-template-columns: minmax(0, 1fr);
498
- gap: 12px;
499
- padding: 14px;
500
- }
501
-
502
- .product-intelligence-panel {
503
- border-color: #b9d8d4;
504
- background: #fbfefd;
505
- }
506
-
507
- .product-signal-grid {
508
- display: grid;
509
- grid-template-columns: repeat(3, minmax(0, 1fr));
510
- gap: 10px;
511
- }
512
-
513
- .product-signal {
514
- display: grid;
515
- gap: 7px;
516
- min-width: 0;
517
- padding: 12px;
518
- border: 1px solid var(--admin-line);
519
- border-radius: var(--admin-radius-sm);
520
- background: var(--admin-surface);
521
- }
522
-
523
- .product-signal strong {
524
- font-size: 20px;
525
- }
526
-
527
- .product-signal p {
528
- margin: 0;
529
- color: var(--admin-muted);
530
- font-size: 12px;
531
- }
532
-
533
- .products-tab .metric {
534
- min-height: 84px;
535
- padding: 14px;
536
- border: 1px solid var(--admin-line);
537
- border-radius: var(--admin-radius);
538
- background: var(--admin-surface);
539
- }
540
-
541
- .products-tab .metric span,
542
- .products-tab .field label {
543
- display: block;
544
- color: var(--admin-muted);
545
- font-size: 11px;
546
- font-weight: 760;
547
- text-transform: uppercase;
548
- }
549
-
550
- .products-tab .metric strong {
551
- display: block;
552
- margin-top: 9px;
553
- font-size: 24px;
554
- }
555
-
556
- .products-tab .metric small {
557
- display: block;
558
- margin-top: 3px;
559
- color: var(--admin-muted);
560
- font-size: 12px;
561
- }
562
-
563
- .products-tab .tabs {
564
- display: flex;
565
- gap: 6px;
566
- max-width: 100%;
567
- overflow-x: auto;
568
- border-bottom: 1px solid var(--admin-line);
569
- }
570
-
571
- .products-tab .tab {
572
- flex: 0 0 auto;
573
- min-height: 38px;
574
- display: inline-flex;
575
- align-items: center;
576
- padding: 0 12px;
577
- border: 0;
578
- border-bottom: 2px solid transparent;
579
- background: transparent;
580
- color: var(--admin-muted);
581
- font-weight: 760;
582
- text-decoration: none;
583
- }
584
-
585
- .products-tab .tab.active {
586
- border-color: var(--admin-teal);
587
- color: var(--admin-ink);
588
- }
589
-
590
- .products-tab .table-wrap {
591
- min-width: 0;
592
- max-width: 100%;
593
- overflow-x: auto;
594
- border: 1px solid var(--admin-line);
595
- border-radius: var(--admin-radius);
596
- background: var(--admin-surface);
597
- }
598
-
599
- .products-tab table {
600
- width: 100%;
601
- min-width: 860px;
602
- border-collapse: collapse;
603
- font-size: 13px;
604
- }
605
-
606
- .products-tab th {
607
- height: 38px;
608
- padding: 0 12px;
609
- border-bottom: 1px solid var(--admin-line);
610
- background: var(--admin-surface-soft);
611
- color: var(--admin-muted);
612
- font-size: 11px;
613
- font-weight: 760;
614
- text-align: left;
615
- text-transform: uppercase;
616
- white-space: nowrap;
617
- }
618
-
619
- .products-tab td {
620
- height: 46px;
621
- padding: 7px 12px;
622
- border-bottom: 1px solid var(--admin-line);
623
- vertical-align: middle;
624
- }
625
-
626
- .products-tab tbody tr {
627
- cursor: pointer;
628
- }
629
-
630
- .products-tab tr.selected {
631
- background: #eef6f4;
632
- }
633
-
634
- .products-tab tr:last-child td {
635
- border-bottom: 0;
636
- }
637
-
638
- .products-tab .subtle {
639
- display: block;
640
- margin-top: 2px;
641
- color: var(--admin-muted);
642
- font-size: 12px;
643
- }
644
-
645
- .products-tab .action {
646
- text-align: right;
647
- }
648
-
649
- .products-empty-state {
650
- margin: 0;
651
- padding: 24px 14px;
652
- color: var(--admin-muted);
653
- font-size: 13px;
654
- text-align: center;
655
- }
656
-
657
- .products-tab .check-row {
658
- display: grid;
659
- grid-template-columns: minmax(120px, 0.36fr) minmax(0, 1fr) auto;
660
- gap: 10px;
661
- align-items: center;
662
- padding: 10px 0;
663
- border-bottom: 1px solid var(--admin-line);
664
- }
665
-
666
- .products-tab .check-row:last-child {
667
- border-bottom: 0;
668
- }
669
-
670
- .products-tab .check-row.blocking {
671
- margin: 4px -6px;
672
- padding: 10px 6px;
673
- border: 1px solid #efc477;
674
- border-radius: var(--admin-radius-sm);
675
- background: #fff8ec;
676
- }
677
-
678
- .products-tab .check-row b {
679
- font-size: 13px;
680
- }
681
-
682
- .products-tab .check-row p {
683
- margin: 2px 0 0;
684
- color: var(--admin-muted);
685
- font-size: 12px;
686
- }
687
-
688
- .product-workspace {
689
- display: grid;
690
- grid-template-columns: var(--product-split);
691
- gap: 14px;
692
- align-items: start;
693
- min-width: 0;
694
- }
695
-
696
- .product-list-panel,
697
- .product-detail-panel {
698
- min-width: 0;
699
- }
700
-
701
- .product-detail-panel {
702
- position: sticky;
703
- top: 86px;
704
- box-shadow: var(--admin-shadow);
705
- }
706
-
707
- .product-panel-head {
708
- min-width: 0;
709
- }
710
-
711
- .product-panel-head h3 {
712
- min-width: 0;
713
- overflow-wrap: anywhere;
714
- }
715
-
716
- .product-toolbar {
717
- display: grid;
718
- gap: 8px;
719
- align-items: end;
720
- }
721
-
722
- .product-search {
723
- min-width: 0;
724
- }
725
-
726
- .product-filters {
727
- display: grid;
728
- grid-template-columns: repeat(2, minmax(128px, 1fr));
729
- gap: 8px;
730
- min-width: 0;
731
- }
732
-
733
- .product-toolbar label,
734
- .product-filters label {
735
- display: grid;
736
- gap: 5px;
737
- min-width: 0;
738
- }
739
-
740
- .product-toolbar span,
741
- .product-filters span,
742
- .eyebrow {
743
- color: var(--admin-muted);
744
- font-size: 11px;
745
- font-weight: 760;
746
- text-transform: uppercase;
747
- }
748
-
749
- .product-toolbar input,
750
- .product-toolbar select,
751
- .product-filters select {
752
- min-width: 0;
753
- width: 100%;
754
- min-height: 38px;
755
- padding: 0 10px;
756
- overflow: hidden;
757
- border: 1px solid var(--admin-line-strong);
758
- border-radius: var(--admin-radius-sm);
759
- background: var(--admin-surface);
760
- color: var(--admin-ink);
761
- text-overflow: ellipsis;
762
- }
763
-
764
- .products-table {
765
- min-width: 900px;
766
- }
767
-
768
- .products-table td {
769
- height: 58px;
770
- }
771
-
772
- .product-title-cell {
773
- display: flex;
774
- align-items: center;
775
- gap: 8px;
776
- min-width: 0;
777
- }
778
-
779
- .product-detail-meta {
780
- display: grid;
781
- grid-template-columns: repeat(3, minmax(0, 1fr));
782
- gap: 10px;
783
- }
784
-
785
- .product-detail-meta > div {
786
- min-width: 0;
787
- padding-bottom: 10px;
788
- border-bottom: 1px solid var(--admin-line);
789
- }
790
-
791
- .product-detail-meta strong,
792
- .mini-row strong {
793
- display: block;
794
- margin-top: 3px;
795
- overflow-wrap: anywhere;
796
- }
797
-
798
- .detail-section {
799
- display: grid;
800
- gap: 10px;
801
- padding-top: 2px;
802
- }
803
-
804
- .detail-section + .detail-section {
805
- padding-top: 12px;
806
- border-top: 1px solid var(--admin-line);
807
- }
808
-
809
- .detail-section-head {
810
- display: flex;
811
- align-items: center;
812
- justify-content: space-between;
813
- gap: 10px;
814
- cursor: pointer;
815
- list-style: none;
816
- }
817
-
818
- .detail-section-head::-webkit-details-marker {
819
- display: none;
820
- }
821
-
822
- .detail-section-head h4 {
823
- margin: 0;
824
- font-size: 13px;
825
- }
826
-
827
- .mini-table {
828
- display: grid;
829
- margin-top: 10px;
830
- border: 1px solid var(--admin-line);
831
- border-radius: var(--admin-radius);
832
- overflow: hidden;
833
- }
834
-
835
- .mini-row {
836
- display: grid;
837
- grid-template-columns: minmax(130px, 1.25fr) 90px 90px;
838
- gap: 10px;
839
- align-items: center;
840
- min-height: 58px;
841
- padding: 9px 10px;
842
- border-bottom: 1px solid var(--admin-line);
843
- }
844
-
845
- .mini-row:last-child {
846
- border-bottom: 0;
847
- }
848
-
849
- @media (max-width: 1180px) {
850
- .product-workspace {
851
- grid-template-columns: minmax(0, 1fr);
852
- }
853
-
854
- .product-detail-panel {
855
- position: static;
856
- }
857
- }
858
-
859
- @media (max-width: 920px) {
860
- .products-tab .section-title,
861
- .products-tab .panel-head {
862
- align-items: flex-start;
863
- flex-direction: column;
864
- }
865
-
866
- .products-tab .grid.cols-4,
867
- .product-signal-grid {
868
- grid-template-columns: repeat(2, minmax(0, 1fr));
869
- }
870
-
871
- .product-filters {
872
- grid-template-columns: repeat(2, minmax(0, 1fr));
873
- }
874
- }
875
-
876
- @media (max-width: 640px) {
877
- .products-tab .grid.cols-4,
878
- .product-signal-grid {
879
- grid-template-columns: 1fr;
880
- }
881
-
882
- .products-tab .actions {
883
- width: 100%;
884
- }
885
-
886
- .products-tab .actions .btn {
887
- flex: 1 1 150px;
888
- }
889
-
890
- .products-tab .check-row {
891
- grid-template-columns: 1fr;
892
- }
893
-
894
- .product-toolbar,
895
- .product-filters,
896
- .product-detail-meta,
897
- .mini-row {
898
- grid-template-columns: 1fr;
899
- }
900
- }
901
- </style>