@mohasinac/appkit 4.45.0 → 4.46.0

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 (152) hide show
  1. package/dist/.build-stamp.json +4 -4
  2. package/dist/_internal/server/features/categories/live-counts.d.ts +41 -0
  3. package/dist/_internal/server/features/categories/live-counts.js +72 -0
  4. package/dist/_internal/server/features/checkout/expand-order-items.js +17 -0
  5. package/dist/_internal/server/features/orders/adapters.js +29 -0
  6. package/dist/_internal/server/features/parts/assembly.d.ts +126 -0
  7. package/dist/_internal/server/features/parts/assembly.js +246 -0
  8. package/dist/_internal/server/features/parts/data.d.ts +65 -0
  9. package/dist/_internal/server/features/parts/data.js +170 -0
  10. package/dist/_internal/server/features/parts/og.d.ts +34 -0
  11. package/dist/_internal/server/features/parts/og.js +66 -0
  12. package/dist/_internal/server/features/search/search-everything.d.ts +75 -0
  13. package/dist/_internal/server/features/search/search-everything.js +120 -0
  14. package/dist/_internal/server/features/search/search-sources.d.ts +17 -0
  15. package/dist/_internal/server/features/search/search-sources.js +138 -0
  16. package/dist/_internal/server/jobs/core/categoryTaxonomyBackfill.d.ts +70 -0
  17. package/dist/_internal/server/jobs/core/categoryTaxonomyBackfill.js +302 -0
  18. package/dist/_internal/server/jobs/core/countersReconcile.js +128 -19
  19. package/dist/_internal/server/jobs/core/jobRunners.js +12 -0
  20. package/dist/_internal/server/jobs/core/onCategoryWrite.js +73 -3
  21. package/dist/_internal/server/jobs/core/onProductWrite.js +93 -43
  22. package/dist/_internal/server/jobs/core/productViewWindows.d.ts +2 -0
  23. package/dist/_internal/server/jobs/core/productViewWindows.js +173 -0
  24. package/dist/_internal/server/jobs/core/testDataCleanup.d.ts +1 -1
  25. package/dist/_internal/server/jobs/core/testDataCleanup.js +27 -1
  26. package/dist/_internal/server/jobs/handlers/pageViewRollup.d.ts +20 -0
  27. package/dist/_internal/server/jobs/handlers/pageViewRollup.js +25 -1
  28. package/dist/_internal/shared/actions/action-registry.js +36 -0
  29. package/dist/_internal/shared/features/promotions/schema.d.ts +2 -2
  30. package/dist/_internal/shared/features/shipping/protections.d.ts +93 -1
  31. package/dist/_internal/shared/features/shipping/protections.js +127 -0
  32. package/dist/_internal/shared/search/part-aliases.d.ts +44 -0
  33. package/dist/_internal/shared/search/part-aliases.js +104 -0
  34. package/dist/constants/api-endpoints.d.ts +18 -12
  35. package/dist/constants/api-endpoints.js +6 -8
  36. package/dist/constants/field-names.d.ts +0 -2
  37. package/dist/constants/field-names.js +0 -2
  38. package/dist/constants/index.d.ts +1 -1
  39. package/dist/constants/index.js +1 -1
  40. package/dist/core/index.d.ts +0 -3
  41. package/dist/core/index.js +0 -2
  42. package/dist/errors/messages.d.ts +0 -7
  43. package/dist/errors/messages.js +0 -8
  44. package/dist/features/admin/components/AdminSectionsView.js +83 -32
  45. package/dist/features/admin/components/AdminSiteSettingsView.js +6 -2
  46. package/dist/features/admin/components/sections/sectionTemplates.d.ts +117 -0
  47. package/dist/features/admin/components/sections/sectionTemplates.js +213 -0
  48. package/dist/features/admin/schemas/firestore.d.ts +15 -0
  49. package/dist/features/admin/types/index.d.ts +0 -1
  50. package/dist/features/auth/permissions/constants.d.ts +1 -1
  51. package/dist/features/auth/permissions/constants.js +0 -2
  52. package/dist/features/categories/api/[id]/description-template/route.js +40 -1
  53. package/dist/features/categories/components/BrandDetailPageView.js +14 -3
  54. package/dist/features/categories/components/CategoryDetailPageView.js +89 -14
  55. package/dist/features/categories/components/CategoryTemplatePicker.js +9 -1
  56. package/dist/features/categories/repository/categories.repository.d.ts +65 -0
  57. package/dist/features/categories/repository/categories.repository.js +168 -22
  58. package/dist/features/categories/repository/category-counts.d.ts +143 -0
  59. package/dist/features/categories/repository/category-counts.js +224 -0
  60. package/dist/features/categories/schemas/category-counts.d.ts +155 -0
  61. package/dist/features/categories/schemas/category-counts.js +116 -0
  62. package/dist/features/categories/schemas/firestore.d.ts +0 -2
  63. package/dist/features/categories/schemas/firestore.js +0 -1
  64. package/dist/features/categories/utils/breadcrumb-chain.d.ts +194 -0
  65. package/dist/features/categories/utils/breadcrumb-chain.js +202 -0
  66. package/dist/features/corporate/schemas/index.d.ts +4 -4
  67. package/dist/features/orders/schemas/firestore.d.ts +13 -0
  68. package/dist/features/orders/types/index.d.ts +30 -0
  69. package/dist/features/parts/components/AssemblyDrawer.d.ts +107 -0
  70. package/dist/features/parts/components/AssemblyDrawer.js +250 -0
  71. package/dist/features/parts/components/ModelCompositionPanel.d.ts +37 -0
  72. package/dist/features/parts/components/ModelCompositionPanel.js +34 -0
  73. package/dist/features/parts/components/PartDetailPageView.d.ts +48 -0
  74. package/dist/features/parts/components/PartDetailPageView.js +20 -0
  75. package/dist/features/parts/components/PartsIndexPageView.d.ts +33 -0
  76. package/dist/features/parts/components/PartsIndexPageView.js +17 -0
  77. package/dist/features/parts/schemas/firestore.d.ts +257 -0
  78. package/dist/features/parts/schemas/firestore.js +181 -0
  79. package/dist/features/parts/utils/colourway-match.d.ts +62 -0
  80. package/dist/features/parts/utils/colourway-match.js +98 -0
  81. package/dist/features/parts/utils/part-breakdown.d.ts +53 -0
  82. package/dist/features/parts/utils/part-breakdown.js +77 -0
  83. package/dist/features/products/components/ProductDetailPageView.d.ts +14 -1
  84. package/dist/features/products/components/ProductDetailPageView.js +22 -3
  85. package/dist/features/products/components/ProductTabsShell.d.ts +3 -1
  86. package/dist/features/products/components/ProductTabsShell.js +4 -1
  87. package/dist/features/products/repository/category-assignment.d.ts +132 -0
  88. package/dist/features/products/repository/category-assignment.js +304 -0
  89. package/dist/features/products/repository/products.repository.d.ts +58 -1
  90. package/dist/features/products/repository/products.repository.js +255 -28
  91. package/dist/features/products/schemas/firestore.d.ts +147 -6
  92. package/dist/features/products/schemas/firestore.js +49 -0
  93. package/dist/features/search/components/SearchResultsPageView.d.ts +33 -0
  94. package/dist/features/search/components/SearchResultsPageView.js +21 -0
  95. package/dist/features/seller/components/SellerProductsView.js +129 -3
  96. package/dist/features/tester/seed-data/authored/admin__site-system.js +0 -15
  97. package/dist/features/tester/seed-data/authored/content-discovery__parts-database.d.ts +2 -0
  98. package/dist/features/tester/seed-data/authored/content-discovery__parts-database.js +166 -0
  99. package/dist/features/tester/seed-data/authored/index.js +2 -0
  100. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +28 -1
  101. package/dist/index.d.ts +11 -16
  102. package/dist/index.js +26 -18
  103. package/dist/next/routing/route-map.d.ts +6 -2
  104. package/dist/next/routing/route-map.js +14 -1
  105. package/dist/providers/db-firebase/base.repository.d.ts +31 -0
  106. package/dist/providers/db-firebase/base.repository.js +31 -0
  107. package/dist/repositories/index.d.ts +1 -2
  108. package/dist/repositories/index.js +0 -1
  109. package/dist/seed/_helpers/category-content.d.ts +29 -0
  110. package/dist/seed/_helpers/category-content.js +98 -1
  111. package/dist/seed/_helpers/category-forest.js +104 -24
  112. package/dist/seed/_helpers/category-parts.d.ts +9 -0
  113. package/dist/seed/_helpers/category-parts.js +4594 -0
  114. package/dist/seed/carousel-slides-seed-data.js +81 -8
  115. package/dist/seed/categories-seed-data.js +44 -6
  116. package/dist/seed/homepage-sections-seed-data.js +170 -9
  117. package/dist/seed/index.d.ts +2 -0
  118. package/dist/seed/index.js +13 -0
  119. package/dist/seed/model-composition-seed-data.d.ts +15 -0
  120. package/dist/seed/model-composition-seed-data.js +8918 -0
  121. package/dist/seed/part-glossary-seed-data.d.ts +15 -0
  122. package/dist/seed/part-glossary-seed-data.js +10729 -0
  123. package/dist/seed/products-standard-seed-data.js +52 -0
  124. package/dist/seed/seed-purpose.d.ts +103 -4
  125. package/dist/seed/seed-purpose.js +162 -11
  126. package/dist/seo/json-ld.d.ts +10 -1
  127. package/dist/seo/json-ld.js +9 -1
  128. package/dist/server.d.ts +7 -0
  129. package/dist/server.js +11 -0
  130. package/dist/styles.css +1 -1
  131. package/dist/tailwind-utilities.css +1 -1
  132. package/dist/utils/id-generators.d.ts +30 -0
  133. package/dist/utils/id-generators.js +31 -1
  134. package/dist/utils/search-txt-builders.js +22 -0
  135. package/dist/utils/search-txt.d.ts +22 -0
  136. package/dist/utils/search-txt.js +17 -0
  137. package/dist/values/success-messages.d.ts +0 -4
  138. package/dist/values/success-messages.js +0 -5
  139. package/package.json +1 -1
  140. package/scripts/seed-cli.mjs +292 -16
  141. package/dist/core/copilot-log.repository.d.ts +0 -44
  142. package/dist/core/copilot-log.repository.js +0 -61
  143. package/dist/features/copilot/components/AdminCopilotView.d.ts +0 -21
  144. package/dist/features/copilot/components/AdminCopilotView.js +0 -51
  145. package/dist/features/copilot/components/index.d.ts +0 -2
  146. package/dist/features/copilot/components/index.js +0 -1
  147. package/dist/features/copilot/hooks/useCopilotChat.d.ts +0 -25
  148. package/dist/features/copilot/hooks/useCopilotChat.js +0 -65
  149. package/dist/features/copilot/hooks/useCopilotFeedback.d.ts +0 -15
  150. package/dist/features/copilot/hooks/useCopilotFeedback.js +0 -15
  151. package/dist/features/copilot/index.d.ts +0 -3
  152. package/dist/features/copilot/index.js +0 -3
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Section, Container, Stack, Row, Div, Heading, Text, Span, TextLink } from "../../../ui/index.js";
3
+ export function SearchResultsPageView({ result, renderSearchInput, }) {
4
+ const { q, terms, groups, totalHits } = result;
5
+ const hasQuery = q.trim().length > 0;
6
+ /*
7
+ * 🛑 THREE states, not two, and the middle one is the honest part.
8
+ *
9
+ * no query — prompt, no claim about results
10
+ * query, 0 terms — the query normalised away ("!!!", "—"). Saying
11
+ * "no results for !!!" implies we looked; we could
12
+ * not. A2's bug was the inverse of this: a
13
+ * punctuation-only query returned the ENTIRE
14
+ * published catalogue as search results.
15
+ * query, 0 hits — genuinely nothing matched
16
+ */
17
+ const normalisedAway = hasQuery && terms.length === 0;
18
+ return (_jsx(Section, { padding: "page", children: _jsx(Container, { children: _jsxs(Stack, { gap: "loose", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 1, children: hasQuery ? `Search: ${q}` : "Search" }), hasQuery && !normalisedAway && (_jsx(Text, { color: "muted", size: "sm", children: totalHits === 0
19
+ ? "Nothing matched."
20
+ : `${totalHits}${groups.some((g) => g.truncated) ? "+" : ""} result${totalHits === 1 ? "" : "s"} across ${groups.length} section${groups.length === 1 ? "" : "s"}.` }))] }), renderSearchInput?.(), !hasQuery && (_jsx(Text, { color: "muted", children: "Search listings, categories, brands, stores, articles and help pages at once." })), normalisedAway && (_jsx(Text, { color: "muted", children: "That search had no searchable words in it. Try a product, part or category name." })), groups.map((group) => (_jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", wrap: true, gap: "sm", children: [_jsxs(Heading, { level: 2, size: "lg", children: [group.label, group.truncated && (_jsxs(Span, { size: "sm", color: "muted", children: [" ", "(", group.hits.length, "+)"] }))] }), _jsxs(TextLink, { href: group.moreHref, children: ["See all ", group.label.toLowerCase(), " \u2192"] })] }), _jsx(Stack, { gap: "xs", children: group.hits.map((hit) => (_jsxs(Div, { surface: "card", padding: "sm", rounded: "md", children: [_jsx(TextLink, { href: hit.href, children: hit.title }), hit.detail && (_jsx(Text, { size: "xs", color: "muted", children: hit.detail }))] }, `${group.key}-${hit.id}`))) })] }, group.key))), hasQuery && !normalisedAway && totalHits === 0 && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { color: "muted", children: ["No listings, categories, brands, stores, articles or help pages matched \u201C", q, "\u201D."] }), _jsx(TextLink, { href: "/products", children: "Browse all listings \u2192" })] }))] }) }) }));
21
+ }
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { formatPhysicalLocation } from "../../../utils/physical-location.js";
4
4
  import { normalizeError } from "../../../errors/normalize.js";
5
+ import { trackError, ErrorCategory, ErrorSeverity, } from "../../../monitoring/error-tracking.js";
5
6
  import { toUserMessage } from "../../../errors/error-display-map.js";
6
7
  import { SIEVE_OP, Stack, sieveFilter } from "@mohasinac/appkit/client";
7
8
  import { LOW_STOCK_THRESHOLD } from "../../store-extensions/constants/alert-metrics.js";
@@ -9,7 +10,7 @@ import { sortBy } from "@mohasinac/appkit/client";
9
10
  import { useState, useCallback } from "react";
10
11
  import { useEntityDelete } from "../../../react/hooks/useEntityDelete.js";
11
12
  import { useActionDispatch } from "../../../react/hooks/use-action-dispatch.js";
12
- import { Eye, EyeOff, Pencil, Trash2, Printer, MapPin, Boxes } from "lucide-react";
13
+ import { Eye, EyeOff, Pencil, Trash2, Printer, MapPin, Boxes, Wrench, Scissors } from "lucide-react";
13
14
  import { PhysicalLocationModal } from "./PhysicalLocationModal.js";
14
15
  import { BulkStockModal } from "./BulkStockModal.js";
15
16
  import { useUrlTable } from "../../../react/hooks/useUrlTable.js";
@@ -30,6 +31,7 @@ import { SellerProductsCards } from "./SellerProductsCards.js";
30
31
  import { SellerProductsFilterFields } from "./SellerProductsFilterDrawer.js";
31
32
  import { listingBadgeVariant } from "../../products/utils/listing-badge-variant.js";
32
33
  import { DataListingView } from "../../admin/components/DataListingView.js";
34
+ import { AssemblyDrawer, } from "../../parts/components/AssemblyDrawer.js";
33
35
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
34
36
  import { buildBulkAction } from "../../../_internal/shared/actions/bulk-helpers.js";
35
37
  const FILTER_KEYS = [
@@ -148,6 +150,29 @@ const PRODUCT_COLUMNS = [
148
150
  * Both stock fields have been `canFilter`/`canSort` at the repository layer
149
151
  * the whole time; only the view omitted them.
150
152
  */
153
+ /*
154
+ * D8 — views, which were collected on ~30 surfaces and joined to NOTHING.
155
+ * A seller could not see how many people looked at their listing: the metric
156
+ * every competitor platform leads with.
157
+ *
158
+ * 🛑 Three states, and the middle one is the whole reason this is a column
159
+ * rather than a number in a subtitle. `undefined` means NOT YET ROLLED UP —
160
+ * the nightly pass has not run — and renders an em-dash; `0` means measured
161
+ * and nobody looked, and renders `0`. Collapsing them with `?? 0` is exactly
162
+ * what made the admin Top Products table show "0 views" on every row as
163
+ * though it were a measurement (A5).
164
+ */
165
+ {
166
+ key: "views",
167
+ header: "7d views",
168
+ className: "w-24 text-right",
169
+ render: (row) => {
170
+ if (typeof row.views7d !== "number") {
171
+ return _jsx(Span, { size: "xs", color: "faint", children: "\u2014" });
172
+ }
173
+ return (_jsx(Span, { size: "xs", color: row.views7d > 0 ? "primary" : "muted", numeric: true, children: row.views7d.toLocaleString() }));
174
+ },
175
+ },
151
176
  {
152
177
  key: "stock",
153
178
  header: "Stock",
@@ -227,6 +252,94 @@ export function SellerProductsView({ onDeleteProduct, onCreateClick, }) {
227
252
  const [setLocationOpen, setSetLocationOpen] = useState(false);
228
253
  const [selectedIdsForLocation, setSelectedIdsForLocation] = useState([]);
229
254
  const [setStockOpen, setSetStockOpen] = useState(false);
255
+ /*
256
+ * ── D13: assembly ──────────────────────────────────────────────────────
257
+ *
258
+ * 🛑 The drawer's data is fetched when it OPENS, not with the page. 50 part
259
+ * listings and a composition per row would be a read nobody asked for on a
260
+ * view that renders 50 rows — and the drawer is used on a handful of them.
261
+ */
262
+ const [assembleFor, setAssembleFor] = useState(null);
263
+ const [assembleData, setAssembleData] = useState(null);
264
+ const [assembleLoading, setAssembleLoading] = useState(false);
265
+ const openAssembly = useCallback(async (row, direction) => {
266
+ setAssembleFor({ id: row.id, title: row.primary ?? row.id, direction });
267
+ setAssembleData(null);
268
+ setAssembleLoading(true);
269
+ try {
270
+ const res = await fetch(SELLER_ENDPOINTS.PRODUCT_ASSEMBLE(row.id));
271
+ const body = (await res.json().catch(() => null));
272
+ if (!res.ok)
273
+ throw new Error(body?.error ?? "Could not load your parts");
274
+ setAssembleData({
275
+ compositions: body?.data?.compositions ?? [],
276
+ options: body?.data?.options ?? [],
277
+ targetAvailable: body?.data?.targetAvailable ?? 0,
278
+ });
279
+ }
280
+ catch (err) {
281
+ const e = normalizeError(err);
282
+ /*
283
+ * 🛑 Closed and reported, never left open and empty. An empty picker
284
+ * with no explanation is the state in which a seller concludes they
285
+ * hold no parts — which may be false.
286
+ */
287
+ setAssembleFor(null);
288
+ /*
289
+ * 🛑 AUTHORED copy, not `e.message`. Unlike the 409 (which carries
290
+ * `{needs, has}` and composes a sentence around the real figures),
291
+ * a load failure has no number worth surfacing — so this is one honest
292
+ * line, and the raw text goes to the recorder where it belongs.
293
+ */
294
+ showToast("Could not load your parts. Please try again.", "error");
295
+ trackError(e.message, ErrorCategory.API, ErrorSeverity.MEDIUM, {
296
+ component: "SellerProductsView",
297
+ action: "openAssembly",
298
+ });
299
+ }
300
+ finally {
301
+ setAssembleLoading(false);
302
+ }
303
+ }, [showToast]);
304
+ const submitAssembly = useCallback(async (input) => {
305
+ const id = assembleFor?.id;
306
+ if (!id)
307
+ throw new Error("No listing selected");
308
+ const res = await fetch(SELLER_ENDPOINTS.PRODUCT_ASSEMBLE(id), {
309
+ method: "POST",
310
+ headers: { "Content-Type": "application/json" },
311
+ body: JSON.stringify(input),
312
+ });
313
+ const body = (await res.json().catch(() => null));
314
+ /*
315
+ * 🛑 THROWS with the server's own message, which the drawer renders. A
316
+ * 409 reads "Not enough available: needs 6, has 5" — the one sentence
317
+ * that tells the seller what to do. Swallowing it into a generic failure
318
+ * would turn "restock" into "retry".
319
+ */
320
+ if (!res.ok) {
321
+ /*
322
+ * 🛑 Carries the structured DETAIL to the drawer and never a rendered
323
+ * sentence. The drawer composes its own copy around `{needs, has}`;
324
+ * `message` here is for the recorder (Root Cause #86 —
325
+ * `audit-raw-error-text` blocks showing a thrown `.message`).
326
+ */
327
+ const err = new Error(body?.error ?? "assembly failed");
328
+ /*
329
+ * Typed rather than `unknown`: the 409's shape is OURS — the planner
330
+ * throws `{ kind, productId, needs, has }` — so declaring it is what
331
+ * lets the drawer compose a sentence around real figures instead of
332
+ * casting at the boundary (Root Cause #57/#70: a cast at a boundary is
333
+ * where this class of bug lives).
334
+ */
335
+ err.detail = { code: body?.code, ...(body?.details ?? {}) };
336
+ throw err;
337
+ }
338
+ return {
339
+ unitsBuilt: body?.data?.unitsBuilt ?? input.units,
340
+ consumed: body?.data?.consumed ?? [],
341
+ };
342
+ }, [assembleFor?.id]);
230
343
  const [selectedIdsForStock, setSelectedIdsForStock] = useState([]);
231
344
  const [stockBusy, setStockBusy] = useState(false);
232
345
  /*
@@ -472,6 +585,9 @@ export function SellerProductsView({ onDeleteProduct, onCreateClick, }) {
472
585
  price: priceRaw ? `₹${priceRaw.toLocaleString("en-IN")}` : "—",
473
586
  stock: stockLabel || "—",
474
587
  stockValue,
588
+ // Carried through ONLY when the document really has it — see the
589
+ // field's own note on undefined-vs-zero.
590
+ ...(typeof item.views7d === "number" ? { views7d: item.views7d } : {}),
475
591
  physicalLocation: item.physicalLocation &&
476
592
  typeof item.physicalLocation.zone === "string"
477
593
  ? item.physicalLocation
@@ -524,8 +640,18 @@ export function SellerProductsView({ onDeleteProduct, onCreateClick, }) {
524
640
  : `/products/${row.id}`,
525
641
  renderRowActions: (row) => {
526
642
  const isPublished = (statusOverrides.get(row.id) ?? row.status) === "published";
527
- return (_jsxs(Row, { gap: "xs", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); handleEdit(row); }, "aria-label": ACTIONS.STORE["edit-listing"].ariaLabel, children: _jsx(Pencil, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void handleTogglePublish(row); }, "aria-label": isPublished ? "Unpublish" : "Publish", title: isPublished ? "Unpublish" : "Publish", disabled: publishingId === row.id, children: isPublished ? _jsx(EyeOff, { className: "w-4 h-4" }) : _jsx(Eye, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void handleDuplicate(row); }, "aria-label": "Duplicate listing", title: "Duplicate", children: "\u29C9" }), onDeleteProduct && (_jsx(Button, { variant: "ghost", size: "sm", action: ACTIONS.STORE["delete-listing"], onClick: (e) => { e.stopPropagation(); void handleDelete(row); }, disabled: deletingId === row.id, className: "text-[var(--appkit-color-error)] hover:bg-[var(--appkit-color-border-subtle)]", children: _jsx(Trash2, { className: "w-4 h-4" }) }))] }));
643
+ return (_jsxs(Row, { gap: "xs", children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); handleEdit(row); }, "aria-label": ACTIONS.STORE["edit-listing"].ariaLabel, children: _jsx(Pencil, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void openAssembly(row, "assemble"); }, "aria-label": ACTIONS.STORE["assemble-from-parts"].ariaLabel, title: ACTIONS.STORE["assemble-from-parts"].label, disabled: assembleLoading, children: _jsx(Wrench, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void openAssembly(row, "part-out"); }, "aria-label": ACTIONS.STORE["part-out-listing"].ariaLabel, title: ACTIONS.STORE["part-out-listing"].label, disabled: assembleLoading, children: _jsx(Scissors, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void handleTogglePublish(row); }, "aria-label": isPublished ? "Unpublish" : "Publish", title: isPublished ? "Unpublish" : "Publish", disabled: publishingId === row.id, children: isPublished ? _jsx(EyeOff, { className: "w-4 h-4" }) : _jsx(Eye, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); void handleDuplicate(row); }, "aria-label": "Duplicate listing", title: "Duplicate", children: "\u29C9" }), onDeleteProduct && (_jsx(Button, { variant: "ghost", size: "sm", action: ACTIONS.STORE["delete-listing"], onClick: (e) => { e.stopPropagation(); void handleDelete(row); }, disabled: deletingId === row.id, className: "text-[var(--appkit-color-error)] hover:bg-[var(--appkit-color-border-subtle)]", children: _jsx(Trash2, { className: "w-4 h-4" }) }))] }));
528
644
  },
529
645
  };
530
- return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), setLocationOpen && (_jsx(PhysicalLocationModal, { count: selectedIdsForLocation.length, onSave: (loc) => handleSetLocation(loc, selectedIdsForLocation), onClose: () => setSetLocationOpen(false) })), setStockOpen && (_jsx(BulkStockModal, { count: selectedIdsForStock.length, busy: stockBusy, onSave: (qty) => handleSetStock(qty, selectedIdsForStock), onClose: () => setSetStockOpen(false) }))] }));
646
+ return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), setLocationOpen && (_jsx(PhysicalLocationModal, { count: selectedIdsForLocation.length, onSave: (loc) => handleSetLocation(loc, selectedIdsForLocation), onClose: () => setSetLocationOpen(false) })), setStockOpen && (_jsx(BulkStockModal, { count: selectedIdsForStock.length, busy: stockBusy, onSave: (qty) => handleSetStock(qty, selectedIdsForStock), onClose: () => setSetStockOpen(false) })), assembleFor && assembleData && (_jsx(AssemblyDrawer, { isOpen: true,
647
+ /*
648
+ * 🛑 No refetch on close, and that is a stated limitation rather than
649
+ * a silent one: `DataListingView` owns the query and exposes no
650
+ * refetch handle to a consumer. The drawer reports the real
651
+ * before/after per component, so the seller SEES what moved — but the
652
+ * row behind it still shows the pre-build figure until the next
653
+ * navigation. Wiring a refetch means giving `ListingViewConfig` a
654
+ * handle, which is a shared-surface change and its own commit.
655
+ */
656
+ onClose: () => { setAssembleFor(null); setAssembleData(null); }, direction: assembleFor.direction, targetTitle: assembleFor.title, targetAvailable: assembleData.targetAvailable, compositions: assembleData.compositions, options: assembleData.options, onSubmit: submitAssembly }))] }));
531
657
  }
@@ -424,21 +424,6 @@ export const authored = {
424
424
  expectedUiState: "The page renders logs, or a readable message naming what is unavailable. It does not show a stack trace, and it does not take the rest of the admin panel with it.",
425
425
  endResult: "Read-only; nothing persists.",
426
426
  },
427
- "checklist-admin-site-system-copilot-admin": {
428
- roles: ["admin"],
429
- startPage: "/admin",
430
- steps: [
431
- "Sign in as admin@letitrip.in / TempPass123!.",
432
- "Open /admin/copilot and read what it offers.",
433
- "Read whether it states which model or provider it uses.",
434
- "Submit a simple request and read the response.",
435
- "Read whether any error surfaces a provider key or a raw response body.",
436
- "Read whether the feature declines cleanly when unconfigured.",
437
- ],
438
- expectedBehaviour: "The feature works or declines cleanly, and never surfaces a provider credential or a raw upstream error to the browser. Every 5xx message is scrubbed before it reaches a client, with the real one kept server-side for the error recorder.",
439
- expectedUiState: "A response renders, or a readable message names what is missing. No provider key, no raw upstream body, no stack trace.",
440
- endResult: "Read-only; nothing persists.",
441
- },
442
427
  "checklist-admin-site-system-team-admin": {
443
428
  roles: ["admin"],
444
429
  startPage: "/admin/team",
@@ -0,0 +1,2 @@
1
+ import type { AuthoredCase } from "./_types";
2
+ export declare const authored: Record<string, AuthoredCase>;
@@ -0,0 +1,166 @@
1
+ /*
2
+ * WHY: Authored six-part procedures for content-discovery/parts-database.
3
+ * WHAT: 9 cases, keyed by full checklist id.
4
+ *
5
+ * 🛑 EVERY CASE HERE IS WRITTEN AS BEFORE → AFTER, because every defect this
6
+ * feature has already produced returned HTTP 200 with plausible content. A case
7
+ * reading "check the parts page works" passes against all four of them:
8
+ *
9
+ * 1. `getCompositionsForCategory` first queried a `searchTxt` field that
10
+ * `modelComposition` does not have. Zero rows, no error, an empty panel
11
+ * that reads as "no composition recorded".
12
+ * 2. The composition `name` was a FILE PATH for all 259 rows, so the
13
+ * category join matched 0 of 259 — and a model page with no panel is
14
+ * indistinguishable from a model we have no data for.
15
+ * 3. Without the paren-stripped fallback the join reached only 150 of 259,
16
+ * and HMS models in particular silently had no panel.
17
+ * 4. 🛑 The one live listing whose leaf has a composition is an ART POSTER,
18
+ * so before `LISTING_TYPE_HAS_COMPOSITION` the only Parts tab in the
19
+ * catalogue offered a poster an attack ring and a blade base — a correct
20
+ * part list for the wrong kind of object.
21
+ *
22
+ * So each case below names what the WRONG answer looked like, and several
23
+ * compare two surfaces that must agree rather than checking that something
24
+ * rendered.
25
+ *
26
+ * @tag domain:tester
27
+ * @tag layer:seed
28
+ * @tag pattern:none
29
+ * @tag access:server-only
30
+ * @tag consumers:seed-data/authored/index.ts
31
+ * @tag sideEffects:none
32
+ */
33
+ export const authored = {
34
+ "checklist-content-discovery-parts-database-index-lists-every-classification": {
35
+ roles: ["guest"],
36
+ startPage: "/parts",
37
+ steps: [
38
+ "Open /parts.",
39
+ "Count the classification tiles.",
40
+ "Confirm every tile reads as a part NAME in sentence case — 'Performance Tip', 'Fusion Wheel', 'Bit Chip' — and not as a URL slug like 'performance-tip' or 'bit-chip'.",
41
+ "Open any one tile and confirm it loads a page rather than 404ing.",
42
+ ],
43
+ expectedBehaviour: "BEFORE: there was no parts database at all, so a buyer reading '105RF' on a listing had nowhere to learn that 105 is a spin-track height and RF is a rubber flat tip. AFTER: 20 classifications, each a real page. The labels come from a compile-checked map rather than being title-cased from the slug, because 'Bit Chip' is not 'bit-chip' title-cased and 'Armour' is not 'armour'.",
44
+ expectedUiState: "Exactly 20 tiles, each labelled in sentence case, each a working link.",
45
+ expectedData: { classificationTiles: 20 },
46
+ endResult: "Nothing is changed; this case only reads.",
47
+ },
48
+ "checklist-content-discovery-parts-database-index-shows-no-fabricated-counts": {
49
+ roles: ["guest"],
50
+ startPage: "/parts",
51
+ steps: [
52
+ "Open /parts.",
53
+ "Look at each classification tile and confirm NO tile shows a number of parts.",
54
+ "Answer no if any tile shows a count, including a zero.",
55
+ ],
56
+ expectedBehaviour: "A per-classification count on this page would be 20 queries or a 753-document scan on a page that exists only to route — against a ~3-round-trip budget per render. The honest version of 'we are not going to pay for that number' is to not show a number, rather than to show a stale or a zero one.",
57
+ expectedUiState: "Tiles carry a label and a link, and no count.",
58
+ expectedData: { tileShowsCount: false },
59
+ endResult: "Nothing is changed; this case only reads.",
60
+ },
61
+ "checklist-content-discovery-parts-database-classification-page-lists-its-parts": {
62
+ roles: ["guest"],
63
+ startPage: "/parts",
64
+ steps: [
65
+ "Open /parts, then open the Performance Tip tile.",
66
+ "Read the count in the line under the heading.",
67
+ "Count the rows listed.",
68
+ "Confirm the rows are in alphabetical order by part name.",
69
+ "Confirm a part that has a recorded weight shows it as '(N g)' and a part with no recorded weight shows nothing in its place — not '0 g' and not an em-dash.",
70
+ ],
71
+ expectedBehaviour: "The count is a promise about the list directly beneath it, so the two must agree exactly. Only 98 of 753 parts carry a weight and only 23 carry a code; both are facts about our sources rather than missing data, so an absent one renders as nothing at all. A '0 g' would be a measurement we never took.",
72
+ expectedUiState: "The stated count equals the number of rows. Rows are alphabetical. Weights appear only where recorded.",
73
+ expectedData: { statedCountEqualsRowCount: true },
74
+ endResult: "Nothing is changed; this case only reads.",
75
+ },
76
+ "checklist-content-discovery-parts-database-unknown-classification-404s": {
77
+ roles: ["guest"],
78
+ startPage: "/parts",
79
+ steps: [
80
+ "Open /parts, then edit the address bar to /parts/zzzznope and load it.",
81
+ "Confirm you get a 404 page.",
82
+ "Then go back to /parts and open the Performance Tip tile as a positive control, and confirm it loads with rows.",
83
+ "Answer no if /parts/zzzznope renders an EMPTY classification page instead of 404ing.",
84
+ ],
85
+ expectedBehaviour: "BEFORE the guard existed, casting a URL segment to a slot type would have made this a perfectly successful query for a classification no document has: zero rows, HTTP 200, and a page reading 'No parts recorded in this classification yet' — which is a lie about our data rather than a statement about the URL. The positive control is what distinguishes a working 404 from a page that 404s on everything.",
86
+ expectedUiState: "/parts/zzzznope is a 404. /parts/performance-tip loads and lists parts.",
87
+ expectedData: { unknownSegment404s: true, knownSegmentLoads: true },
88
+ endResult: "Nothing is changed; this case only reads.",
89
+ },
90
+ "checklist-content-discovery-parts-database-part-page-shows-which-models-use-it": {
91
+ roles: ["guest"],
92
+ startPage: "/parts",
93
+ steps: [
94
+ "Open /parts, then open Disc, then open the part named Sting.",
95
+ "Read the 'Used by N models' heading.",
96
+ "Count the model rows beneath it.",
97
+ "Click one model row and confirm it opens that model's category page.",
98
+ "Confirm any model row that is NOT a link is still readable as plain text rather than rendering as a broken link.",
99
+ ],
100
+ expectedBehaviour: "'Which beys use this part' is the single most useful thing a part page says and the reason the glossary is a collection rather than a lookup table. 210 of 259 compositions joined to a category, so a model that did not join renders as plain text — a link to a page that does not exist is worse than no link.",
101
+ expectedUiState: "The heading count equals the number of rows. Linked models open their category page; unlinked models render as text.",
102
+ expectedData: { headingCountEqualsRowCount: true },
103
+ endResult: "Nothing is changed; this case only reads.",
104
+ },
105
+ "checklist-content-discovery-parts-database-part-page-shows-no-prices": {
106
+ roles: ["guest"],
107
+ startPage: "/parts",
108
+ steps: [
109
+ "Open /parts, then Disc, then Sting.",
110
+ "Read the Listings section.",
111
+ "Confirm NO row shows a rupee amount.",
112
+ "Confirm no row shows 'Sign in to see price' either.",
113
+ "Click 'Search the catalogue' or 'See all listings' and confirm it lands on /products with a search term.",
114
+ ],
115
+ expectedBehaviour: "Prices are withheld from signed-out visitors, so an amount here would need the price gate and would make this a client-rendered page. A row carrying a title, a condition and a link has nothing to gate and therefore nothing to leak — which is why there is no 'Sign in to see price' placeholder either. The price lives one click away on the listing, where the gate already is.",
116
+ expectedUiState: "Listing rows show a title and optionally a condition. No rupee amounts, no sign-in-to-see-price text.",
117
+ expectedData: { pricesOnPartPage: false },
118
+ endResult: "Nothing is changed; this case only reads.",
119
+ },
120
+ "checklist-content-discovery-parts-database-model-page-shows-its-composition": {
121
+ roles: ["guest"],
122
+ startPage: "/categories/category-ace-dragon-sting-charge-zan",
123
+ steps: [
124
+ "Open /categories/category-ace-dragon-sting-charge-zan — a tier-4 named-model leaf. Every category id carries the `category-` prefix and a URL without it does not resolve.",
125
+ "Find the 'What it is made of' section.",
126
+ "Confirm it lists at least one slot row, each reading as a part classification on the left and a part name on the right.",
127
+ "Click a part name and confirm it opens that part's page under /parts/.",
128
+ "Then open /categories/category-burst-gt — the generation LINE one level up — and confirm it has NO 'What it is made of' section at all.",
129
+ ],
130
+ expectedBehaviour: "BEFORE: the panel showed nothing on every model, twice over — the read queried a field the collection does not have, and separately every composition's name was a file path so none of them joined to a category. Both failures rendered as an absent section, which is indistinguishable from a model we have no data for. AFTER: the leaf shows its parts and each one links out. The LINE shows nothing deliberately — 'Burst GT' is not made of parts, the beys under it are.",
131
+ expectedUiState: "The model leaf shows a composition with linked part names. The line above it shows no composition section.",
132
+ expectedData: { leafHasPanel: true, lineHasPanel: false },
133
+ endResult: "Nothing is changed; this case only reads.",
134
+ },
135
+ "checklist-content-discovery-parts-database-art-listing-has-no-parts-tab": {
136
+ roles: ["guest"],
137
+ startPage: "/products/art-dranzer-phoenix-poster",
138
+ steps: [
139
+ "Open /products/art-dranzer-phoenix-poster — an art print filed under the Dranzer F model leaf.",
140
+ "Read the tab strip above the description.",
141
+ "Confirm there is NO 'Parts' tab.",
142
+ "Answer no if a Parts tab appears listing an attack ring, a weight disk or a blade base.",
143
+ ],
144
+ expectedBehaviour: "BEFORE: this was the ONLY listing in the catalogue that satisfied every other condition, so the one Parts tab in the whole product catalogue offered a poster an attack ring and a blade base. It rendered a correct part list for the wrong kind of object, with no error anywhere. AFTER: a print, a sticker sheet and a redemption code are excluded by type. They file under the model's leaf on purpose — a sticker sheet belongs beside the bey it is for — and none of them is made of its parts.",
145
+ expectedUiState: "No Parts tab on an art listing.",
146
+ expectedData: { partsTabOnArtListing: false },
147
+ endResult: "Nothing is changed; this case only reads.",
148
+ },
149
+ "checklist-content-discovery-parts-database-seller-description-lists-parts": {
150
+ roles: ["seller"],
151
+ startPage: "/store/products/new",
152
+ steps: [
153
+ "Open /store/products/new and enter any title.",
154
+ "Pick the category category-ace-dragon-sting-charge-zan, a Burst GT named-model leaf.",
155
+ "Set the condition to Used.",
156
+ "Wait for the template picker to fill the Description field.",
157
+ "Read the filled description and confirm it contains a 'Parts:' line naming at least one real part.",
158
+ "Now change the category to category-pokemon-set-151 — a real trading-card expansion-set leaf, which has no parts — and confirm the refilled description contains NO 'Parts:' line at all.",
159
+ "Answer no if the second description shows an EMPTY 'Parts:' line.",
160
+ ],
161
+ expectedBehaviour: "A listing filed under a named model can state what the bey is made of, from the same data the model page renders. The second half is the half that matters: an unfilled slot resolves to an empty string, so a shared template would render an empty 'Parts:' bullet on every listing in the catalogue that is not a Beyblade. The line is REMOVED rather than emptied — absent data must read as absent, not as blank.",
162
+ expectedUiState: "The Beyblade description carries a Parts line naming real parts. The trading-card description has no Parts line and no empty bullet.",
163
+ expectedData: { partsLineOnModelLeaf: true, emptyPartsLineElsewhere: false },
164
+ endResult: "Nothing is saved — the draft is abandoned without publishing, so no listing is created.",
165
+ },
166
+ };
@@ -76,6 +76,7 @@ import { authored as a_content_discovery__blog } from "./content-discovery__blog
76
76
  import { authored as a_content_discovery__coupons } from "./content-discovery__coupons.js";
77
77
  import { authored as a_content_discovery__events } from "./content-discovery__events.js";
78
78
  import { authored as a_content_discovery__faq_help } from "./content-discovery__faq-help.js";
79
+ import { authored as a_content_discovery__parts_database } from "./content-discovery__parts-database.js";
79
80
  import { authored as a_content_discovery__notifications } from "./content-discovery__notifications.js";
80
81
  import { authored as a_content_discovery__search } from "./content-discovery__search.js";
81
82
  import { authored as a_cta_layout__checkout_bottom_bar } from "./cta-layout__checkout-bottom-bar.js";
@@ -195,6 +196,7 @@ export const AUTHORED_CASES = {
195
196
  ...a_content_discovery__coupons,
196
197
  ...a_content_discovery__events,
197
198
  ...a_content_discovery__faq_help,
199
+ ...a_content_discovery__parts_database,
198
200
  ...a_content_discovery__notifications,
199
201
  ...a_content_discovery__search,
200
202
  ...a_cta_layout__checkout_bottom_bar,
@@ -3091,6 +3091,34 @@ const rawTesterChecklistItems = [
3091
3091
  { key: "brand-count-matches-its-listing", label: "A brand page's count equals what that brand page lists" },
3092
3092
  ],
3093
3093
  },
3094
+ {
3095
+ /*
3096
+ * D12 — the parts database, the composition panel, and the part line in a
3097
+ * seller's auto-filled description.
3098
+ *
3099
+ * 🛑 Four defects this feature has already produced, every one of them a
3100
+ * 200 with plausible content: a read against a field the collection does
3101
+ * not have; a composition `name` that was a FILE PATH for all 259 rows so
3102
+ * nothing joined; a join that reached 150 of 259 until a paren-stripped
3103
+ * fallback took it to 210; and a Parts tab on an ART POSTER offering it
3104
+ * an attack ring. None of them errored, and three of them rendered as an
3105
+ * ABSENT panel — indistinguishable from a model we have no data for.
3106
+ */
3107
+ pageKey: "parts-database",
3108
+ pageLabel: "Parts database, composition panel and the part line in a description",
3109
+ href: "/parts",
3110
+ cases: [
3111
+ { key: "index-lists-every-classification", label: "The parts index lists all 20 classifications, labelled as names and not as URL slugs" },
3112
+ { key: "index-shows-no-fabricated-counts", label: "No classification tile shows a part count — not even a zero" },
3113
+ { key: "classification-page-lists-its-parts", label: "A classification's stated count equals the rows it lists, and a missing weight renders as nothing rather than 0 g" },
3114
+ { key: "unknown-classification-404s", label: "An unknown classification 404s instead of rendering an empty page — with a real one as the control" },
3115
+ { key: "part-page-shows-which-models-use-it", label: "A part page names the models that use it, and an unjoined model reads as text rather than a broken link" },
3116
+ { key: "part-page-shows-no-prices", label: "A part page's listing rows carry no price and no sign-in-to-see-price placeholder" },
3117
+ { key: "model-page-shows-its-composition", label: "A named-model leaf shows what it is made of with linked parts; the generation line above it shows none" },
3118
+ { key: "art-listing-has-no-parts-tab", label: "An art print filed under a model leaf has NO Parts tab" },
3119
+ { key: "seller-description-lists-parts", label: "A listing under a model leaf gets a Parts line in its filled description, and a non-Beyblade category gets no empty one" },
3120
+ ],
3121
+ },
3094
3122
  {
3095
3123
  /*
3096
3124
  * Store reviews. The endpoint answered 200 with zero reviews for EVERY
@@ -5497,7 +5525,6 @@ const rawTesterChecklistItems = [
5497
5525
  description: "Found 2026-09-02: this is the only page in the app calling a Google Cloud API rather than Firestore, and the call was unguarded — a permission or outage error took the whole page down (and failed the production build). It now degrades to an empty list and records a DEGRADED_READ. Confirm the page renders its heading and empty state rather than an error boundary.",
5498
5526
  href: "/admin/maintenance/cloud-logs",
5499
5527
  },
5500
- { key: "copilot-admin", label: "Admin copilot page works correctly", href: "/admin/copilot" },
5501
5528
  { key: "team-admin", label: "Admin team page works correctly", href: "/admin/team" },
5502
5529
  {
5503
5530
  key: "team-permission-group-filter",
package/dist/index.d.ts CHANGED
@@ -365,10 +365,7 @@ export { FEATURE_SUBPATH_MAP } from "./cli/index";
365
365
  export { mergeFeatureMessages } from "./cli/index";
366
366
  export { withFeatures } from "./cli/index";
367
367
  export type { NextConfig } from "./cli/index";
368
- export { COPILOT_LOGS_COLLECTION } from "./core/index";
369
- export { COPILOT_LOGS_INDEXED_FIELDS } from "./core/index";
370
368
  export { CacheManager } from "./core/index";
371
- export { CopilotLogRepository } from "./core/index";
372
369
  export { EventBus } from "./core/index";
373
370
  export { Logger } from "./core/index";
374
371
  export { MUTATION_EVENTS } from "./core/index";
@@ -381,8 +378,6 @@ export { StorageManager } from "./core/index";
381
378
  export { _resetActionMiddleware } from "./core/index";
382
379
  export { cacheManager } from "./core/index";
383
380
  export { configureMarketDefaults } from "./core/index";
384
- export { copilotLogQueryHelpers } from "./core/index";
385
- export { copilotLogRepository } from "./core/index";
386
381
  export { createServerAction } from "./core/index";
387
382
  export { createSiteConfig } from "./core/index";
388
383
  export { emitMutation } from "./core/index";
@@ -414,9 +409,6 @@ export type { ActionResult } from "./_internal/shared/types/action-result";
414
409
  export { handleActionError } from "./utils/action-response";
415
410
  export type { CacheEntry } from "./core/index";
416
411
  export type { CacheOptions } from "./core/index";
417
- export type { CopilotFeedback } from "./core/index";
418
- export type { CopilotLogCreateInput } from "./core/index";
419
- export type { CopilotLogDocument } from "./core/index";
420
412
  export type { EventSubscription } from "./core/index";
421
413
  export type { LogEntry } from "./core/index";
422
414
  export type { LogLevel } from "./core/index";
@@ -1612,13 +1604,6 @@ export type { ContactFormProps } from "./features/contact/index";
1612
1604
  export type { ContactInfoItem } from "./features/contact/index";
1613
1605
  export type { ContactInfoSidebarProps } from "./features/contact/index";
1614
1606
  export type { UseContactSubmitOptions } from "./features/contact/index";
1615
- export { AdminCopilotView } from "./features/copilot/index";
1616
- export { useCopilotChat } from "./features/copilot/index";
1617
- export { useCopilotFeedback } from "./features/copilot/index";
1618
- export type { AdminCopilotViewProps } from "./features/copilot/index";
1619
- export type { CopilotChatResponse } from "./features/copilot/index";
1620
- export type { CopilotMessage } from "./features/copilot/index";
1621
- export type { UseCopilotFeedbackOptions } from "./features/copilot/index";
1622
1607
  export { CorporateInquiryForm } from "./features/corporate/index";
1623
1608
  export { buildCorporateColumns } from "./features/corporate/index";
1624
1609
  export { corporateAdminColumns } from "./features/corporate/index";
@@ -2999,7 +2984,7 @@ export { Ol } from "./ui/index";
2999
2984
  export { LoginRequiredModal } from "./ui/components/LoginRequiredModal";
3000
2985
  export type { LoginRequiredModalProps } from "./ui/components/LoginRequiredModal";
3001
2986
  export { isAuthError } from "./utils/auth-error";
3002
- export { API_ENDPOINTS, API_ROUTES, LOGS_ENDPOINTS, AUTH_ENDPOINTS, ACCOUNT_ENDPOINTS, NOTIFICATIONS_ENDPOINTS, ADMIN_ENDPOINTS, AUCTION_ENDPOINTS, BID_ENDPOINTS, CART_ENDPOINTS, CATEGORY_ENDPOINTS, CHECKOUT_ENDPOINTS, PAYMENT_ENDPOINTS, COPILOT_ENDPOINTS, CORPORATE_ENDPOINTS, EVENT_ENDPOINTS, FAQ_ENDPOINTS, HOMEPAGE_ENDPOINTS, LOYALTY_ENDPOINTS, ANALYTICS_ENDPOINTS, MEDIA_ENDPOINTS, ORDER_ENDPOINTS, PREORDER_ENDPOINTS, PRODUCT_ENDPOINTS, REVIEW_ENDPOINTS, SEARCH_ENDPOINTS, SELLER_ENDPOINTS, BLOG_ENDPOINTS, WISHLIST_ENDPOINTS, COLLECTION_CACHE_PATHS, resolveEndpoint, resolveEndpointFn, ROUTES, PUBLIC_ROUTES, PROTECTED_ROUTES, AUTH_ROUTES, WISHLIST_MAX, HISTORY_MAX, CART_MAX_ITEMS, WISHLIST_DOC_ID, HISTORY_DOC_ID, WISHLIST_COLLECTION, HISTORY_COLLECTION, } from "./constants/index";
2987
+ export { API_ENDPOINTS, API_ROUTES, LOGS_ENDPOINTS, AUTH_ENDPOINTS, ACCOUNT_ENDPOINTS, NOTIFICATIONS_ENDPOINTS, ADMIN_ENDPOINTS, AUCTION_ENDPOINTS, BID_ENDPOINTS, CART_ENDPOINTS, CATEGORY_ENDPOINTS, CHECKOUT_ENDPOINTS, FAQ_ENDPOINTS, HOMEPAGE_ENDPOINTS, LOYALTY_ENDPOINTS, ANALYTICS_ENDPOINTS, MEDIA_ENDPOINTS, ORDER_ENDPOINTS, PREORDER_ENDPOINTS, PRODUCT_ENDPOINTS, REVIEW_ENDPOINTS, SEARCH_ENDPOINTS, SELLER_ENDPOINTS, BLOG_ENDPOINTS, WISHLIST_ENDPOINTS, COLLECTION_CACHE_PATHS, resolveEndpoint, resolveEndpointFn, ROUTES, PUBLIC_ROUTES, PROTECTED_ROUTES, AUTH_ROUTES, WISHLIST_MAX, HISTORY_MAX, CART_MAX_ITEMS, WISHLIST_DOC_ID, HISTORY_DOC_ID, WISHLIST_COLLECTION, HISTORY_COLLECTION, } from "./constants/index";
3003
2988
  export { WishlistFullError } from "./features/wishlist/server";
3004
2989
  export { useHistory, useHistoryMergeOnLogin, getGuestHistory, trackGuestHistory, removeGuestHistoryItem, clearGuestHistory, getGuestHistoryCount, type GuestHistoryItem, type GuestHistoryType, type UserHistoryItem, type HistoryProductType, type HistoryItemSnapshot, type TrackArgs as TrackHistoryArgs, } from "./features/history";
3005
2990
  export { historyRepository, UserHistoryRepository, trackHistoryView, getHistoryForUser, removeHistoryItem, clearHistory, mergeGuestHistory, } from "./features/history/server";
@@ -3072,6 +3057,16 @@ export { groupedListingFormSchema, groupedListingCreateSchema, groupedListingUpd
3072
3057
  export { scammersSeedData } from "./seed/index";
3073
3058
  export { productFeaturesSeedData } from "./seed/index";
3074
3059
  export { taxCodesSeedData } from "./seed/index";
3060
+ export { PART_GLOSSARY_COLLECTION, MODEL_COMPOSITION_COLLECTION, PART_SLOT_VALUES, PART_SLOT_LABEL, ALL_PART_SLOTS, LISTING_TYPE_HAS_COMPOSITION, partSlotFromSegment, partGlossaryId, } from "./features/parts/schemas/firestore";
3061
+ export type { PartGlossaryDocument, ModelCompositionDocument, ModelSlot, ModelSlotMerge, PartSlot, } from "./features/parts/schemas/firestore";
3062
+ export { partGlossarySeedData } from "./seed/index";
3063
+ export { modelCompositionSeedData } from "./seed/index";
3064
+ export { ModelCompositionPanel } from "./features/parts/components/ModelCompositionPanel";
3065
+ export { PartsIndexPageView, PartClassificationPageView, } from "./features/parts/components/PartsIndexPageView";
3066
+ export { PartDetailPageView } from "./features/parts/components/PartDetailPageView";
3067
+ export { AssemblyDrawer } from "./features/parts/components/AssemblyDrawer";
3068
+ export type { AssemblyDrawerProps, AssemblyPartOption, AssemblyRow, AssemblyOutcome, } from "./features/parts/components/AssemblyDrawer";
3069
+ export type { PartListingRow } from "./features/parts/components/PartDetailPageView";
3075
3070
  export { SCAMMER_COLLECTION, SCAMMER_ID_PREFIX, SCAMMER_MATCH_FIELDS, SCAMMER_INDEXED_FIELDS, SCAMMER_FIELDS, SCAMMER_INCIDENTS_SUBCOLLECTION, SCAMMER_COMMENTS_SUBCOLLECTION, SCAMMER_CONTESTS_SUBCOLLECTION, MAX_COMMENT_BODY_LENGTH, MAX_PENDING_SCAMMER_REPORTS_PER_USER, ScammerStatusValues, ScamPlatformValues, SocialPlatformValues, ContestTypeValues, ContestStatusValues, SCAM_PLATFORM_LABELS, SOCIAL_PLATFORM_LABELS, SCAMMER_STATUS_LABELS, CONTEST_TYPE_LABELS, CONTEST_STATUS_LABELS, DEFAULT_SCAMMER_DATA, DEFAULT_SCAMMER_INCIDENT_DATA, DEFAULT_SCAMMER_COMMENT_DATA, DEFAULT_SCAMMER_CONTEST_DATA, } from "./features/scams/schemas/firestore";
3076
3071
  export type { ScammerStatus, ScamPlatform, SocialPlatform, ContestType, ContestStatus, ScammerMatchField, ScammerMatchStrength, ScammerSocialMedia, ScammerDocument, ScammerIncidentDocument, ScammerCommentDocument, ScammerContestDocument, ScammerSuggestion, ScammerCreateInput, ScammerAdminUpdateInput, ScammerIncidentCreateInput, ScammerCommentCreateInput, ScammerContestCreateInput, } from "./features/scams/schemas/firestore";
3077
3072
  export { SCAM_TYPES, SCAM_CATEGORIES, SCAM_TYPE_LABELS, SCAM_CATEGORY_LABELS, getScamType, getScamTypesByCategory, getCategoryForScamType, } from "./features/scams/constants/scam-types";