@mohasinac/appkit 4.45.0 → 4.47.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.
- package/dist/.build-stamp.json +4 -4
- package/dist/_internal/server/features/categories/live-counts.d.ts +41 -0
- package/dist/_internal/server/features/categories/live-counts.js +72 -0
- package/dist/_internal/server/features/checkout/expand-order-items.js +17 -0
- package/dist/_internal/server/features/orders/adapters.js +29 -0
- package/dist/_internal/server/features/parts/assembly.d.ts +126 -0
- package/dist/_internal/server/features/parts/assembly.js +246 -0
- package/dist/_internal/server/features/parts/data.d.ts +65 -0
- package/dist/_internal/server/features/parts/data.js +170 -0
- package/dist/_internal/server/features/parts/og.d.ts +34 -0
- package/dist/_internal/server/features/parts/og.js +66 -0
- package/dist/_internal/server/features/search/search-everything.d.ts +75 -0
- package/dist/_internal/server/features/search/search-everything.js +120 -0
- package/dist/_internal/server/features/search/search-sources.d.ts +17 -0
- package/dist/_internal/server/features/search/search-sources.js +138 -0
- package/dist/_internal/server/jobs/core/categoryTaxonomyBackfill.d.ts +70 -0
- package/dist/_internal/server/jobs/core/categoryTaxonomyBackfill.js +302 -0
- package/dist/_internal/server/jobs/core/countersReconcile.js +128 -19
- package/dist/_internal/server/jobs/core/jobRunners.js +12 -0
- package/dist/_internal/server/jobs/core/onCategoryWrite.js +73 -3
- package/dist/_internal/server/jobs/core/onProductWrite.js +93 -43
- package/dist/_internal/server/jobs/core/productViewWindows.d.ts +2 -0
- package/dist/_internal/server/jobs/core/productViewWindows.js +173 -0
- package/dist/_internal/server/jobs/core/testDataCleanup.d.ts +1 -1
- package/dist/_internal/server/jobs/core/testDataCleanup.js +27 -1
- package/dist/_internal/server/jobs/handlers/pageViewRollup.d.ts +20 -0
- package/dist/_internal/server/jobs/handlers/pageViewRollup.js +25 -1
- package/dist/_internal/shared/actions/action-registry.js +36 -0
- package/dist/_internal/shared/features/promotions/schema.d.ts +2 -2
- package/dist/_internal/shared/features/shipping/protections.d.ts +93 -1
- package/dist/_internal/shared/features/shipping/protections.js +127 -0
- package/dist/_internal/shared/search/part-aliases.d.ts +44 -0
- package/dist/_internal/shared/search/part-aliases.js +104 -0
- package/dist/constants/api-endpoints.d.ts +18 -12
- package/dist/constants/api-endpoints.js +6 -8
- package/dist/constants/field-names.d.ts +0 -2
- package/dist/constants/field-names.js +0 -2
- package/dist/constants/index.d.ts +1 -1
- package/dist/constants/index.js +1 -1
- package/dist/core/index.d.ts +0 -3
- package/dist/core/index.js +0 -2
- package/dist/errors/messages.d.ts +0 -7
- package/dist/errors/messages.js +0 -8
- package/dist/features/admin/components/AdminSectionsView.js +83 -32
- package/dist/features/admin/components/AdminSiteSettingsView.js +6 -2
- package/dist/features/admin/components/sections/sectionTemplates.d.ts +117 -0
- package/dist/features/admin/components/sections/sectionTemplates.js +213 -0
- package/dist/features/admin/schemas/firestore.d.ts +15 -0
- package/dist/features/admin/types/index.d.ts +0 -1
- package/dist/features/auth/permissions/constants.d.ts +1 -1
- package/dist/features/auth/permissions/constants.js +0 -2
- package/dist/features/categories/api/[id]/description-template/route.js +40 -1
- package/dist/features/categories/components/BrandDetailPageView.js +14 -3
- package/dist/features/categories/components/BreadcrumbTrail.d.ts +24 -1
- package/dist/features/categories/components/BreadcrumbTrail.js +45 -4
- package/dist/features/categories/components/CategoryDetailPageView.js +89 -14
- package/dist/features/categories/components/CategoryTemplatePicker.js +9 -1
- package/dist/features/categories/repository/categories.repository.d.ts +65 -0
- package/dist/features/categories/repository/categories.repository.js +168 -22
- package/dist/features/categories/repository/category-counts.d.ts +143 -0
- package/dist/features/categories/repository/category-counts.js +224 -0
- package/dist/features/categories/schemas/category-counts.d.ts +155 -0
- package/dist/features/categories/schemas/category-counts.js +116 -0
- package/dist/features/categories/schemas/firestore.d.ts +0 -2
- package/dist/features/categories/schemas/firestore.js +0 -1
- package/dist/features/categories/utils/breadcrumb-chain.d.ts +194 -0
- package/dist/features/categories/utils/breadcrumb-chain.js +202 -0
- package/dist/features/corporate/schemas/index.d.ts +4 -4
- package/dist/features/orders/schemas/firestore.d.ts +13 -0
- package/dist/features/orders/types/index.d.ts +30 -0
- package/dist/features/parts/components/AssemblyDrawer.d.ts +107 -0
- package/dist/features/parts/components/AssemblyDrawer.js +250 -0
- package/dist/features/parts/components/ModelCompositionPanel.d.ts +37 -0
- package/dist/features/parts/components/ModelCompositionPanel.js +34 -0
- package/dist/features/parts/components/PartDetailPageView.d.ts +48 -0
- package/dist/features/parts/components/PartDetailPageView.js +20 -0
- package/dist/features/parts/components/PartsIndexPageView.d.ts +33 -0
- package/dist/features/parts/components/PartsIndexPageView.js +17 -0
- package/dist/features/parts/schemas/firestore.d.ts +257 -0
- package/dist/features/parts/schemas/firestore.js +181 -0
- package/dist/features/parts/utils/colourway-match.d.ts +62 -0
- package/dist/features/parts/utils/colourway-match.js +98 -0
- package/dist/features/parts/utils/part-breakdown.d.ts +53 -0
- package/dist/features/parts/utils/part-breakdown.js +77 -0
- package/dist/features/products/components/ProductDetailPageView.d.ts +14 -1
- package/dist/features/products/components/ProductDetailPageView.js +22 -3
- package/dist/features/products/components/ProductTabsShell.d.ts +3 -1
- package/dist/features/products/components/ProductTabsShell.js +4 -1
- package/dist/features/products/repository/category-assignment.d.ts +132 -0
- package/dist/features/products/repository/category-assignment.js +304 -0
- package/dist/features/products/repository/products.repository.d.ts +58 -1
- package/dist/features/products/repository/products.repository.js +255 -28
- package/dist/features/products/schemas/firestore.d.ts +147 -6
- package/dist/features/products/schemas/firestore.js +49 -0
- package/dist/features/search/components/SearchResultsPageView.d.ts +33 -0
- package/dist/features/search/components/SearchResultsPageView.js +21 -0
- package/dist/features/seller/components/CategoryInlineSelect.d.ts +17 -1
- package/dist/features/seller/components/CategoryInlineSelect.js +14 -3
- package/dist/features/seller/components/SellerProductShell.d.ts +26 -1
- package/dist/features/seller/components/SellerProductShell.js +30 -1
- package/dist/features/seller/components/SellerProductsView.js +129 -3
- package/dist/features/tester/seed-data/authored/admin__site-system.js +0 -15
- package/dist/features/tester/seed-data/authored/content-discovery__parts-database.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/content-discovery__parts-database.js +166 -0
- package/dist/features/tester/seed-data/authored/index.js +2 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +28 -1
- package/dist/index.d.ts +11 -16
- package/dist/index.js +26 -18
- package/dist/next/routing/route-map.d.ts +6 -2
- package/dist/next/routing/route-map.js +14 -1
- package/dist/providers/db-firebase/base.repository.d.ts +31 -0
- package/dist/providers/db-firebase/base.repository.js +31 -0
- package/dist/repositories/index.d.ts +1 -2
- package/dist/repositories/index.js +0 -1
- package/dist/seed/_helpers/category-content-generated.d.ts +24 -0
- package/dist/seed/_helpers/category-content-generated.js +1837 -0
- package/dist/seed/_helpers/category-content.d.ts +29 -0
- package/dist/seed/_helpers/category-content.js +98 -1
- package/dist/seed/_helpers/category-forest.js +104 -24
- package/dist/seed/_helpers/category-parts.d.ts +9 -0
- package/dist/seed/_helpers/category-parts.js +4594 -0
- package/dist/seed/carousel-slides-seed-data.js +81 -8
- package/dist/seed/categories-seed-data.js +44 -6
- package/dist/seed/homepage-sections-seed-data.js +170 -9
- package/dist/seed/index.d.ts +2 -0
- package/dist/seed/index.js +13 -0
- package/dist/seed/model-composition-seed-data.d.ts +15 -0
- package/dist/seed/model-composition-seed-data.js +8918 -0
- package/dist/seed/part-glossary-seed-data.d.ts +15 -0
- package/dist/seed/part-glossary-seed-data.js +10729 -0
- package/dist/seed/products-standard-seed-data.js +52 -0
- package/dist/seed/seed-purpose.d.ts +103 -4
- package/dist/seed/seed-purpose.js +162 -11
- package/dist/seo/json-ld.d.ts +10 -1
- package/dist/seo/json-ld.js +9 -1
- package/dist/server.d.ts +7 -0
- package/dist/server.js +11 -0
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/utils/id-generators.d.ts +30 -0
- package/dist/utils/id-generators.js +31 -1
- package/dist/utils/search-txt-builders.js +22 -0
- package/dist/utils/search-txt.d.ts +22 -0
- package/dist/utils/search-txt.js +17 -0
- package/dist/values/success-messages.d.ts +0 -4
- package/dist/values/success-messages.js +0 -5
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +292 -16
- package/dist/core/copilot-log.repository.d.ts +0 -44
- package/dist/core/copilot-log.repository.js +0 -61
- package/dist/features/copilot/components/AdminCopilotView.d.ts +0 -21
- package/dist/features/copilot/components/AdminCopilotView.js +0 -51
- package/dist/features/copilot/components/index.d.ts +0 -2
- package/dist/features/copilot/components/index.js +0 -1
- package/dist/features/copilot/hooks/useCopilotChat.d.ts +0 -25
- package/dist/features/copilot/hooks/useCopilotChat.js +0 -65
- package/dist/features/copilot/hooks/useCopilotFeedback.d.ts +0 -15
- package/dist/features/copilot/hooks/useCopilotFeedback.js +0 -15
- package/dist/features/copilot/index.d.ts +0 -3
- package/dist/features/copilot/index.js +0 -3
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D4b — ONE breadcrumb chain builder, for the category page, the brand page and
|
|
3
|
+
* the product detail pages.
|
|
4
|
+
*
|
|
5
|
+
* 🛑 A `BreadcrumbList` is the one JSON-LD node whose entire value is the ORDER,
|
|
6
|
+
* and it is now emitted over a 416-node / 5-tier / 39-root forest rather than
|
|
7
|
+
* the 47-node / 2-tier tree it was written against. Measured 2026-10-12:
|
|
8
|
+
*
|
|
9
|
+
* per tier: t0=39 t1=23 t2=65 t3=71 t4=218
|
|
10
|
+
* deepest node -> 7 crumbs:
|
|
11
|
+
* Home > Categories > Spinning Tops > Beyblade Burst > Burst Tops > GT
|
|
12
|
+
* > Ace Dragon Sting Charge Zan
|
|
13
|
+
*
|
|
14
|
+
* Three surfaces render this chain and they must agree, or a crawler and a
|
|
15
|
+
* visitor are shown different hierarchies for one page — which is worse than
|
|
16
|
+
* emitting no breadcrumb, because a mismatched `BreadcrumbList` is a
|
|
17
|
+
* structured-data error against a page that looks fine:
|
|
18
|
+
*
|
|
19
|
+
* 1. the visible `BreadcrumbTrail`
|
|
20
|
+
* 2. this `BreadcrumbList`
|
|
21
|
+
* 3. `categoryNames[]` on a product — the denormalised display cache
|
|
22
|
+
*
|
|
23
|
+
* 🛑 AND THE THIRD IS STORED LEAF-FIRST. Measured:
|
|
24
|
+
*
|
|
25
|
+
* categorySlugs: category-x-basic > category-x-tops > … > category-spinning-tops
|
|
26
|
+
* categoryNames: Basic Line > Beyblade X Tops > … > Spinning Tops
|
|
27
|
+
*
|
|
28
|
+
* So anything rendering `categoryNames` in stored order shows the trail
|
|
29
|
+
* BACKWARDS. Every current consumer takes `[0]` — the leaf — which is correct
|
|
30
|
+
* for a single chip and is exactly why the reversal has never shown up. This
|
|
31
|
+
* module reverses it once, here, so no call site has to remember.
|
|
32
|
+
*
|
|
33
|
+
* 🛑 `ancestors[]` IS THE SOURCE FOR A CATEGORY; `categoryNames`+`categorySlugs`
|
|
34
|
+
* FOR A PRODUCT. Both `ancestors[]` and `parentIds[]` exist on a category and
|
|
35
|
+
* describe the same chain — verified equal in length on all 416 live rows — so
|
|
36
|
+
* neither is wrong, but two owners of one fact is Root Cause #42 waiting. The
|
|
37
|
+
* choice is `ancestors[]` because it carries the display NAME, so the visible
|
|
38
|
+
* trail needs no extra read; `parentIds[]` is ids only and stays what the
|
|
39
|
+
* repository derives `categorySlugs` from.
|
|
40
|
+
*/
|
|
41
|
+
export interface BreadcrumbCrumb {
|
|
42
|
+
name: string;
|
|
43
|
+
/** Absent on the final crumb — the current page is not a link. */
|
|
44
|
+
url?: string;
|
|
45
|
+
}
|
|
46
|
+
/** The shape `CategoryDocument.ancestors[]` entries have. */
|
|
47
|
+
export interface AncestorLike {
|
|
48
|
+
id?: string | null;
|
|
49
|
+
name?: string | null;
|
|
50
|
+
tier?: number | null;
|
|
51
|
+
}
|
|
52
|
+
export interface CategoryChainInput {
|
|
53
|
+
/** The category's own display name. */
|
|
54
|
+
name?: string | null;
|
|
55
|
+
/** Its own URL. Omitted on the last crumb, so pass it and let the caller decide. */
|
|
56
|
+
url?: string;
|
|
57
|
+
ancestors?: readonly AncestorLike[] | null;
|
|
58
|
+
/** Builds a URL from an ancestor id. */
|
|
59
|
+
hrefFor: (id: string) => string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Root → leaf, from a category's `ancestors[]`.
|
|
63
|
+
*
|
|
64
|
+
* 🛑 SORTED BY TIER, never taken as stored. An earlier revision of the category
|
|
65
|
+
* page asserted `ancestors[]` is stored root-first and used it as given; the
|
|
66
|
+
* visible `BreadcrumbTrail` on that same page had always sorted first, so the
|
|
67
|
+
* stored order was relied on nowhere. Sorting is the only safe reading, and a
|
|
68
|
+
* reversed breadcrumb is worse than none.
|
|
69
|
+
*
|
|
70
|
+
* 🛑 A BROKEN ANCESTOR IS OMITTED, NOT RENDERED AS A GAP. An entry with no id
|
|
71
|
+
* or no name cannot be linked or labelled, and emitting `{ name: "" }` puts an
|
|
72
|
+
* empty crumb in the middle of the trail — which reads as a missing level
|
|
73
|
+
* rather than as an absent one. `deriveTaxonomy` warns and leaves a chain alone
|
|
74
|
+
* when a leaf does not resolve (Root Cause #101), so a partial chain is a state
|
|
75
|
+
* that really occurs. Measured 0 in production today, which is why the branch
|
|
76
|
+
* needs a test rather than trust.
|
|
77
|
+
*
|
|
78
|
+
* A ROOT yields just itself — 39 of the 416 live rows are tier 0 (6 category
|
|
79
|
+
* roots plus 28 brands and 5 bundles), so this is the common case, not an edge.
|
|
80
|
+
* The caller's `Home` / `Categories` prefix is what keeps it a valid list.
|
|
81
|
+
*/
|
|
82
|
+
export declare function categoryCrumbs(input: CategoryChainInput): BreadcrumbCrumb[];
|
|
83
|
+
export interface ProductChainInput {
|
|
84
|
+
/** `[leaf, …ancestors, root]` — the STORED order. */
|
|
85
|
+
categorySlugs?: readonly string[] | null;
|
|
86
|
+
/** Parallel to `categorySlugs`, same stored leaf-first order. */
|
|
87
|
+
categoryNames?: readonly string[] | null;
|
|
88
|
+
hrefFor: (slug: string) => string;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Root → leaf, from a product's stored chain.
|
|
92
|
+
*
|
|
93
|
+
* 🛑 THE TWO ARRAYS ARE PARALLEL AND MAY DISAGREE IN LENGTH. `deriveTaxonomy`
|
|
94
|
+
* writes both together, but a row written before it existed — or one whose leaf
|
|
95
|
+
* did not resolve — can carry a `categorySlugs` with no matching name. Pairing
|
|
96
|
+
* by index past the shorter array would label one category with another's name,
|
|
97
|
+
* which is a wrong hierarchy rendered confidently. So the chain stops at the
|
|
98
|
+
* shorter of the two, and a slug with no name is dropped rather than shown as
|
|
99
|
+
* its raw id.
|
|
100
|
+
*
|
|
101
|
+
* 🛑 A LOT FILES AT A NON-LEAF (G3a), so its chain is genuinely SHORTER and the
|
|
102
|
+
* last crumb is a line or generation node rather than a named model. That is
|
|
103
|
+
* correct and must not be padded: fabricating a leaf to fill the final position
|
|
104
|
+
* would claim the lot is one specific model, which is the same false claim the
|
|
105
|
+
* filing rule refuses at the write. Nothing here looks at `isLot` — the chain
|
|
106
|
+
* is simply as long as it is, which is why no special case is needed.
|
|
107
|
+
*/
|
|
108
|
+
export declare function productCrumbs(input: ProductChainInput): BreadcrumbCrumb[];
|
|
109
|
+
/**
|
|
110
|
+
* I5b — 🛑 WHY `productCrumbs` NEEDED NO CHANGE FOR MULTI-ASSIGNMENT, and why
|
|
111
|
+
* that must be asserted rather than admired.
|
|
112
|
+
*
|
|
113
|
+
* A product's `categorySlugs` is now the CLOSURE over every assignment, while
|
|
114
|
+
* `categoryNames` is the DEFAULT chain only. Pairing by index would be a bug if
|
|
115
|
+
* the closure were a set — but it is ordered default-chain-first, so
|
|
116
|
+
* `min(slugs.length, names.length)` clamps to exactly the default chain and the
|
|
117
|
+
* indices line up. Measured: a 7-entry closure with 5 names yields the 5-crumb
|
|
118
|
+
* default trail, root-first.
|
|
119
|
+
*
|
|
120
|
+
* That is correct BY CONSTRUCTION and it is also fragile-looking, which is the
|
|
121
|
+
* dangerous combination — someone will eventually "fix" the pairing or make the
|
|
122
|
+
* closure a set. `verify-breadcrumb-chain.mjs` asserts it so that change fails
|
|
123
|
+
* loudly instead of silently relabelling a hierarchy.
|
|
124
|
+
*/
|
|
125
|
+
/**
|
|
126
|
+
* Crumbs to show before the middle collapses behind a dropdown.
|
|
127
|
+
*
|
|
128
|
+
* 🛑 A real measurement, not a preference: a tier-4 part leaf is **7 crumbs**
|
|
129
|
+
* (`Home > Categories > Spinning Tops > Beyblade Metal > Metal Parts >
|
|
130
|
+
* Performance Tips > Rubber Flat`) and 390px cannot hold it. D4b measured that
|
|
131
|
+
* depth and never addressed how it wraps.
|
|
132
|
+
*
|
|
133
|
+
* 5 keeps `Home`, one collapsed group, and the last two — which is the pattern
|
|
134
|
+
* every large catalogue converges on, because the last two are the only crumbs
|
|
135
|
+
* a visitor navigates by.
|
|
136
|
+
*/
|
|
137
|
+
export declare const TRAIL_COLLAPSE_THRESHOLD = 5;
|
|
138
|
+
export interface CollapsedTrail {
|
|
139
|
+
/** Always the first crumb. Never collapsed — it is the only guaranteed exit. */
|
|
140
|
+
lead: BreadcrumbCrumb[];
|
|
141
|
+
/** The middle, to be rendered behind a dropdown. Empty when the trail is short. */
|
|
142
|
+
collapsed: BreadcrumbCrumb[];
|
|
143
|
+
/** The last two: the parent you would click, and the page you are on. */
|
|
144
|
+
tail: BreadcrumbCrumb[];
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Split a trail into lead / collapsed / tail for ONE row of crumbs.
|
|
148
|
+
*
|
|
149
|
+
* 🛑 Presentation only. The JSON-LD must still emit the FULL trail — a crawler
|
|
150
|
+
* never receives a truncated hierarchy just because a phone cannot show it.
|
|
151
|
+
*/
|
|
152
|
+
export declare function collapseTrail(crumbs: readonly BreadcrumbCrumb[], threshold?: number): CollapsedTrail;
|
|
153
|
+
export interface AlternateTrailsInput {
|
|
154
|
+
/** Every assignment, default first. */
|
|
155
|
+
categoryIds?: readonly string[] | null;
|
|
156
|
+
/** One display name per assignment, parallel to `categoryIds`. */
|
|
157
|
+
categoryIdNames?: readonly string[] | null;
|
|
158
|
+
hrefFor: (slug: string) => string;
|
|
159
|
+
}
|
|
160
|
+
export interface AlternateTrails {
|
|
161
|
+
/**
|
|
162
|
+
* `none` when there is one assignment — which is the overwhelming majority,
|
|
163
|
+
* and the case that must render EXACTLY as before with no extra crumb and no
|
|
164
|
+
* post-hydration shift.
|
|
165
|
+
*/
|
|
166
|
+
mode: "none" | "inline" | "dropdown";
|
|
167
|
+
items: BreadcrumbCrumb[];
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The assignments that are NOT the default, as one crumb's worth of links.
|
|
171
|
+
*
|
|
172
|
+
* *"if it is less than 3 we can add a single crumb to show them all, for more
|
|
173
|
+
* than that we can just add a dropdown"* — so ≤2 alternates render inline and
|
|
174
|
+
* 3 (the maximum, since `MAX_CATEGORY_ASSIGNMENTS` is 4) opens a dropdown.
|
|
175
|
+
*
|
|
176
|
+
* 🛑 ONE ROW, never stacked trails. Two rows of crumbs above a product title is
|
|
177
|
+
* a pattern no competitor in the crawl uses and it costs vertical space above
|
|
178
|
+
* the fold on a phone.
|
|
179
|
+
*
|
|
180
|
+
* 🛑 And the links must be REAL `<a>` elements in the server-rendered DOM, which
|
|
181
|
+
* is this function's whole reason for returning data rather than a component: a
|
|
182
|
+
* crawler does not open menus, so alternates injected on open would trade the
|
|
183
|
+
* SEO this exists for against tidiness.
|
|
184
|
+
*/
|
|
185
|
+
export declare function alternateAssignments(input: AlternateTrailsInput): AlternateTrails;
|
|
186
|
+
/**
|
|
187
|
+
* Prepend the two fixed crumbs every trail starts with, and drop the final
|
|
188
|
+
* crumb's URL.
|
|
189
|
+
*
|
|
190
|
+
* The last crumb must not be a link — it is the page you are on, and Google's
|
|
191
|
+
* own guidance is that the final breadcrumb identifies the current page. Doing
|
|
192
|
+
* it here rather than at three call sites is what stops one of them forgetting.
|
|
193
|
+
*/
|
|
194
|
+
export declare function withTrailHead(crumbs: readonly BreadcrumbCrumb[], head: readonly BreadcrumbCrumb[]): BreadcrumbCrumb[];
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D4b — ONE breadcrumb chain builder, for the category page, the brand page and
|
|
3
|
+
* the product detail pages.
|
|
4
|
+
*
|
|
5
|
+
* 🛑 A `BreadcrumbList` is the one JSON-LD node whose entire value is the ORDER,
|
|
6
|
+
* and it is now emitted over a 416-node / 5-tier / 39-root forest rather than
|
|
7
|
+
* the 47-node / 2-tier tree it was written against. Measured 2026-10-12:
|
|
8
|
+
*
|
|
9
|
+
* per tier: t0=39 t1=23 t2=65 t3=71 t4=218
|
|
10
|
+
* deepest node -> 7 crumbs:
|
|
11
|
+
* Home > Categories > Spinning Tops > Beyblade Burst > Burst Tops > GT
|
|
12
|
+
* > Ace Dragon Sting Charge Zan
|
|
13
|
+
*
|
|
14
|
+
* Three surfaces render this chain and they must agree, or a crawler and a
|
|
15
|
+
* visitor are shown different hierarchies for one page — which is worse than
|
|
16
|
+
* emitting no breadcrumb, because a mismatched `BreadcrumbList` is a
|
|
17
|
+
* structured-data error against a page that looks fine:
|
|
18
|
+
*
|
|
19
|
+
* 1. the visible `BreadcrumbTrail`
|
|
20
|
+
* 2. this `BreadcrumbList`
|
|
21
|
+
* 3. `categoryNames[]` on a product — the denormalised display cache
|
|
22
|
+
*
|
|
23
|
+
* 🛑 AND THE THIRD IS STORED LEAF-FIRST. Measured:
|
|
24
|
+
*
|
|
25
|
+
* categorySlugs: category-x-basic > category-x-tops > … > category-spinning-tops
|
|
26
|
+
* categoryNames: Basic Line > Beyblade X Tops > … > Spinning Tops
|
|
27
|
+
*
|
|
28
|
+
* So anything rendering `categoryNames` in stored order shows the trail
|
|
29
|
+
* BACKWARDS. Every current consumer takes `[0]` — the leaf — which is correct
|
|
30
|
+
* for a single chip and is exactly why the reversal has never shown up. This
|
|
31
|
+
* module reverses it once, here, so no call site has to remember.
|
|
32
|
+
*
|
|
33
|
+
* 🛑 `ancestors[]` IS THE SOURCE FOR A CATEGORY; `categoryNames`+`categorySlugs`
|
|
34
|
+
* FOR A PRODUCT. Both `ancestors[]` and `parentIds[]` exist on a category and
|
|
35
|
+
* describe the same chain — verified equal in length on all 416 live rows — so
|
|
36
|
+
* neither is wrong, but two owners of one fact is Root Cause #42 waiting. The
|
|
37
|
+
* choice is `ancestors[]` because it carries the display NAME, so the visible
|
|
38
|
+
* trail needs no extra read; `parentIds[]` is ids only and stays what the
|
|
39
|
+
* repository derives `categorySlugs` from.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* Root → leaf, from a category's `ancestors[]`.
|
|
43
|
+
*
|
|
44
|
+
* 🛑 SORTED BY TIER, never taken as stored. An earlier revision of the category
|
|
45
|
+
* page asserted `ancestors[]` is stored root-first and used it as given; the
|
|
46
|
+
* visible `BreadcrumbTrail` on that same page had always sorted first, so the
|
|
47
|
+
* stored order was relied on nowhere. Sorting is the only safe reading, and a
|
|
48
|
+
* reversed breadcrumb is worse than none.
|
|
49
|
+
*
|
|
50
|
+
* 🛑 A BROKEN ANCESTOR IS OMITTED, NOT RENDERED AS A GAP. An entry with no id
|
|
51
|
+
* or no name cannot be linked or labelled, and emitting `{ name: "" }` puts an
|
|
52
|
+
* empty crumb in the middle of the trail — which reads as a missing level
|
|
53
|
+
* rather than as an absent one. `deriveTaxonomy` warns and leaves a chain alone
|
|
54
|
+
* when a leaf does not resolve (Root Cause #101), so a partial chain is a state
|
|
55
|
+
* that really occurs. Measured 0 in production today, which is why the branch
|
|
56
|
+
* needs a test rather than trust.
|
|
57
|
+
*
|
|
58
|
+
* A ROOT yields just itself — 39 of the 416 live rows are tier 0 (6 category
|
|
59
|
+
* roots plus 28 brands and 5 bundles), so this is the common case, not an edge.
|
|
60
|
+
* The caller's `Home` / `Categories` prefix is what keeps it a valid list.
|
|
61
|
+
*/
|
|
62
|
+
export function categoryCrumbs(input) {
|
|
63
|
+
const ancestors = (input.ancestors ?? [])
|
|
64
|
+
.filter((a) => !!a && !!a.id && !!a.name)
|
|
65
|
+
.slice()
|
|
66
|
+
.sort((a, b) => (a.tier ?? 0) - (b.tier ?? 0));
|
|
67
|
+
const out = ancestors.map((a) => ({
|
|
68
|
+
name: String(a.name),
|
|
69
|
+
url: input.hrefFor(String(a.id)),
|
|
70
|
+
}));
|
|
71
|
+
if (input.name)
|
|
72
|
+
out.push({ name: input.name, url: input.url });
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Root → leaf, from a product's stored chain.
|
|
77
|
+
*
|
|
78
|
+
* 🛑 THE TWO ARRAYS ARE PARALLEL AND MAY DISAGREE IN LENGTH. `deriveTaxonomy`
|
|
79
|
+
* writes both together, but a row written before it existed — or one whose leaf
|
|
80
|
+
* did not resolve — can carry a `categorySlugs` with no matching name. Pairing
|
|
81
|
+
* by index past the shorter array would label one category with another's name,
|
|
82
|
+
* which is a wrong hierarchy rendered confidently. So the chain stops at the
|
|
83
|
+
* shorter of the two, and a slug with no name is dropped rather than shown as
|
|
84
|
+
* its raw id.
|
|
85
|
+
*
|
|
86
|
+
* 🛑 A LOT FILES AT A NON-LEAF (G3a), so its chain is genuinely SHORTER and the
|
|
87
|
+
* last crumb is a line or generation node rather than a named model. That is
|
|
88
|
+
* correct and must not be padded: fabricating a leaf to fill the final position
|
|
89
|
+
* would claim the lot is one specific model, which is the same false claim the
|
|
90
|
+
* filing rule refuses at the write. Nothing here looks at `isLot` — the chain
|
|
91
|
+
* is simply as long as it is, which is why no special case is needed.
|
|
92
|
+
*/
|
|
93
|
+
export function productCrumbs(input) {
|
|
94
|
+
const slugs = input.categorySlugs ?? [];
|
|
95
|
+
const names = input.categoryNames ?? [];
|
|
96
|
+
const n = Math.min(slugs.length, names.length);
|
|
97
|
+
const out = [];
|
|
98
|
+
// Stored leaf-first; reverse ONCE, here, so no call site has to know.
|
|
99
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
100
|
+
const slug = slugs[i];
|
|
101
|
+
const name = names[i];
|
|
102
|
+
if (!slug || !name)
|
|
103
|
+
continue;
|
|
104
|
+
out.push({ name: String(name), url: input.hrefFor(String(slug)) });
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* I5b — 🛑 WHY `productCrumbs` NEEDED NO CHANGE FOR MULTI-ASSIGNMENT, and why
|
|
110
|
+
* that must be asserted rather than admired.
|
|
111
|
+
*
|
|
112
|
+
* A product's `categorySlugs` is now the CLOSURE over every assignment, while
|
|
113
|
+
* `categoryNames` is the DEFAULT chain only. Pairing by index would be a bug if
|
|
114
|
+
* the closure were a set — but it is ordered default-chain-first, so
|
|
115
|
+
* `min(slugs.length, names.length)` clamps to exactly the default chain and the
|
|
116
|
+
* indices line up. Measured: a 7-entry closure with 5 names yields the 5-crumb
|
|
117
|
+
* default trail, root-first.
|
|
118
|
+
*
|
|
119
|
+
* That is correct BY CONSTRUCTION and it is also fragile-looking, which is the
|
|
120
|
+
* dangerous combination — someone will eventually "fix" the pairing or make the
|
|
121
|
+
* closure a set. `verify-breadcrumb-chain.mjs` asserts it so that change fails
|
|
122
|
+
* loudly instead of silently relabelling a hierarchy.
|
|
123
|
+
*/
|
|
124
|
+
/**
|
|
125
|
+
* Crumbs to show before the middle collapses behind a dropdown.
|
|
126
|
+
*
|
|
127
|
+
* 🛑 A real measurement, not a preference: a tier-4 part leaf is **7 crumbs**
|
|
128
|
+
* (`Home > Categories > Spinning Tops > Beyblade Metal > Metal Parts >
|
|
129
|
+
* Performance Tips > Rubber Flat`) and 390px cannot hold it. D4b measured that
|
|
130
|
+
* depth and never addressed how it wraps.
|
|
131
|
+
*
|
|
132
|
+
* 5 keeps `Home`, one collapsed group, and the last two — which is the pattern
|
|
133
|
+
* every large catalogue converges on, because the last two are the only crumbs
|
|
134
|
+
* a visitor navigates by.
|
|
135
|
+
*/
|
|
136
|
+
export const TRAIL_COLLAPSE_THRESHOLD = 5;
|
|
137
|
+
/**
|
|
138
|
+
* Split a trail into lead / collapsed / tail for ONE row of crumbs.
|
|
139
|
+
*
|
|
140
|
+
* 🛑 Presentation only. The JSON-LD must still emit the FULL trail — a crawler
|
|
141
|
+
* never receives a truncated hierarchy just because a phone cannot show it.
|
|
142
|
+
*/
|
|
143
|
+
export function collapseTrail(crumbs, threshold = TRAIL_COLLAPSE_THRESHOLD) {
|
|
144
|
+
if (crumbs.length <= threshold) {
|
|
145
|
+
return { lead: [...crumbs], collapsed: [], tail: [] };
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
lead: [crumbs[0]],
|
|
149
|
+
collapsed: crumbs.slice(1, crumbs.length - 2),
|
|
150
|
+
tail: crumbs.slice(crumbs.length - 2),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The assignments that are NOT the default, as one crumb's worth of links.
|
|
155
|
+
*
|
|
156
|
+
* *"if it is less than 3 we can add a single crumb to show them all, for more
|
|
157
|
+
* than that we can just add a dropdown"* — so ≤2 alternates render inline and
|
|
158
|
+
* 3 (the maximum, since `MAX_CATEGORY_ASSIGNMENTS` is 4) opens a dropdown.
|
|
159
|
+
*
|
|
160
|
+
* 🛑 ONE ROW, never stacked trails. Two rows of crumbs above a product title is
|
|
161
|
+
* a pattern no competitor in the crawl uses and it costs vertical space above
|
|
162
|
+
* the fold on a phone.
|
|
163
|
+
*
|
|
164
|
+
* 🛑 And the links must be REAL `<a>` elements in the server-rendered DOM, which
|
|
165
|
+
* is this function's whole reason for returning data rather than a component: a
|
|
166
|
+
* crawler does not open menus, so alternates injected on open would trade the
|
|
167
|
+
* SEO this exists for against tidiness.
|
|
168
|
+
*/
|
|
169
|
+
export function alternateAssignments(input) {
|
|
170
|
+
const ids = input.categoryIds ?? [];
|
|
171
|
+
const names = input.categoryIdNames ?? [];
|
|
172
|
+
// Index 0 is the default — it is already the visible trail's last crumb.
|
|
173
|
+
const items = [];
|
|
174
|
+
for (let i = 1; i < Math.min(ids.length, names.length); i++) {
|
|
175
|
+
const id = ids[i];
|
|
176
|
+
const name = names[i];
|
|
177
|
+
// A name-less assignment is dropped rather than shown as its raw id, the
|
|
178
|
+
// same rule `productCrumbs` applies for the same reason.
|
|
179
|
+
if (!id || !name)
|
|
180
|
+
continue;
|
|
181
|
+
items.push({ name: String(name), url: input.hrefFor(String(id)) });
|
|
182
|
+
}
|
|
183
|
+
if (items.length === 0)
|
|
184
|
+
return { mode: "none", items: [] };
|
|
185
|
+
return { mode: items.length > 2 ? "dropdown" : "inline", items };
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Prepend the two fixed crumbs every trail starts with, and drop the final
|
|
189
|
+
* crumb's URL.
|
|
190
|
+
*
|
|
191
|
+
* The last crumb must not be a link — it is the page you are on, and Google's
|
|
192
|
+
* own guidance is that the final breadcrumb identifies the current page. Doing
|
|
193
|
+
* it here rather than at three call sites is what stops one of them forgetting.
|
|
194
|
+
*/
|
|
195
|
+
export function withTrailHead(crumbs, head) {
|
|
196
|
+
const all = [...head, ...crumbs];
|
|
197
|
+
if (all.length > 0) {
|
|
198
|
+
const last = all[all.length - 1];
|
|
199
|
+
all[all.length - 1] = { name: last.name };
|
|
200
|
+
}
|
|
201
|
+
return all;
|
|
202
|
+
}
|
|
@@ -34,9 +34,9 @@ export declare const corporateInquirySchema: z.ZodObject<{
|
|
|
34
34
|
customBranding: boolean;
|
|
35
35
|
createdAt?: string | undefined;
|
|
36
36
|
phone?: string | undefined;
|
|
37
|
+
units?: number | undefined;
|
|
37
38
|
adminNote?: string | undefined;
|
|
38
39
|
designation?: string | undefined;
|
|
39
|
-
units?: number | undefined;
|
|
40
40
|
budgetPerUnit?: number | undefined;
|
|
41
41
|
totalBudget?: number | undefined;
|
|
42
42
|
deliveryDateRequired?: string | undefined;
|
|
@@ -48,9 +48,9 @@ export declare const corporateInquirySchema: z.ZodObject<{
|
|
|
48
48
|
status?: "pending" | "contacted" | "proposal_sent" | "negotiating" | "closed_won" | "closed_lost" | undefined;
|
|
49
49
|
createdAt?: string | undefined;
|
|
50
50
|
phone?: string | undefined;
|
|
51
|
+
units?: number | undefined;
|
|
51
52
|
adminNote?: string | undefined;
|
|
52
53
|
designation?: string | undefined;
|
|
53
|
-
units?: number | undefined;
|
|
54
54
|
budgetPerUnit?: number | undefined;
|
|
55
55
|
totalBudget?: number | undefined;
|
|
56
56
|
deliveryDateRequired?: string | undefined;
|
|
@@ -78,8 +78,8 @@ export declare const submitCorporateInquirySchema: z.ZodObject<{
|
|
|
78
78
|
customBranding: boolean;
|
|
79
79
|
message?: string | undefined;
|
|
80
80
|
phone?: string | undefined;
|
|
81
|
-
designation?: string | undefined;
|
|
82
81
|
units?: number | undefined;
|
|
82
|
+
designation?: string | undefined;
|
|
83
83
|
budgetPerUnit?: number | undefined;
|
|
84
84
|
totalBudget?: number | undefined;
|
|
85
85
|
deliveryDateRequired?: string | undefined;
|
|
@@ -89,8 +89,8 @@ export declare const submitCorporateInquirySchema: z.ZodObject<{
|
|
|
89
89
|
contactPerson: string;
|
|
90
90
|
message?: string | undefined;
|
|
91
91
|
phone?: string | undefined;
|
|
92
|
-
designation?: string | undefined;
|
|
93
92
|
units?: number | undefined;
|
|
93
|
+
designation?: string | undefined;
|
|
94
94
|
budgetPerUnit?: number | undefined;
|
|
95
95
|
totalBudget?: number | undefined;
|
|
96
96
|
deliveryDateRequired?: string | undefined;
|
|
@@ -212,6 +212,19 @@ export interface OrderDocumentItem {
|
|
|
212
212
|
* Absent on every order written before this, which is why the rollup keeps
|
|
213
213
|
* its products-map fallback rather than treating absence as a condition.
|
|
214
214
|
*/
|
|
215
|
+
/**
|
|
216
|
+
* I8 — the category's DISPLAY NAME at order time, beside `categorySlug`.
|
|
217
|
+
*
|
|
218
|
+
* 🛑 A slug survives a rename; a NAME does not. `categorySlug` alone lets the
|
|
219
|
+
* price index band a past sale correctly, which is what it was added for —
|
|
220
|
+
* but a receipt has to SHOW something, and re-resolving the name at render
|
|
221
|
+
* time would make a renamed or deleted category change what a past invoice
|
|
222
|
+
* says. Same reasoning as `hsnCode` / `gstRate` / `finalSale` above: a past
|
|
223
|
+
* sale's facts are fixed.
|
|
224
|
+
*/
|
|
225
|
+
categoryName?: string;
|
|
226
|
+
/** I8 — the brand display name at order time, for the same reason. */
|
|
227
|
+
brand?: string;
|
|
215
228
|
condition?: string;
|
|
216
229
|
/** SB8-F â€" per-item reveal status; flips through pending â†' open â†' revealed/closed. */
|
|
217
230
|
prizeRevealStatus?: "pending" | "open" | "closed" | "revealed";
|
|
@@ -65,6 +65,36 @@ export interface OrderItem {
|
|
|
65
65
|
*/
|
|
66
66
|
hsnCode?: string;
|
|
67
67
|
gstRate?: 0 | 5 | 12 | 18 | 28;
|
|
68
|
+
/**
|
|
69
|
+
* I8 — the TAXONOMY snapshot, surfaced for the first time.
|
|
70
|
+
*
|
|
71
|
+
* 🛑 The third instance of Root Cause #57 in this one adapter. `categorySlug`
|
|
72
|
+
* has been snapshotted onto the order document since C1, with a recorded
|
|
73
|
+
* reason — *"a product re-categorised next year must not retroactively move
|
|
74
|
+
* last year's sale into a band it was never part of"* — and
|
|
75
|
+
* `orderDocumentToOrder` forwarded 13 fields, none of them this one. So the
|
|
76
|
+
* data was right on the document and reached no client surface, which is why
|
|
77
|
+
* no order or invoice page could show a category even though the facts were
|
|
78
|
+
* there.
|
|
79
|
+
*
|
|
80
|
+
* `categoryName` and `brand` are new. A slug survives a rename; a NAME is
|
|
81
|
+
* what a receipt must show, and re-resolving it at render time would let a
|
|
82
|
+
* renamed or deleted category change what a past invoice says. Snapshotting
|
|
83
|
+
* is the whole point: *"the transaction data still keeps it."*
|
|
84
|
+
*/
|
|
85
|
+
categorySlug?: string;
|
|
86
|
+
categoryName?: string;
|
|
87
|
+
brand?: string;
|
|
88
|
+
/** Snapshotted condition — the price-index band's dominant variable. */
|
|
89
|
+
condition?: string;
|
|
90
|
+
/**
|
|
91
|
+
* The grouped-line keys. `groupOrderItemsByLine` collapses a bundle or a
|
|
92
|
+
* grouped cart line for the receipt and keys on exactly these, so without
|
|
93
|
+
* them a client renders N rows where the buyer bought one thing.
|
|
94
|
+
*/
|
|
95
|
+
groupSlug?: string;
|
|
96
|
+
groupTitle?: string;
|
|
97
|
+
groupSource?: "bundle" | "product-group" | "grouped-listing";
|
|
68
98
|
}
|
|
69
99
|
/**
|
|
70
100
|
* One entry on an order's timeline — the client-facing shape of
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D13 — build a complete bey from parts you already hold, or break one down.
|
|
3
|
+
*
|
|
4
|
+
* ── 🛑 THE SLOT SET COMES FROM THE MODEL, NOT FROM A FIXED FIELD LIST ──────
|
|
5
|
+
*
|
|
6
|
+
* The rows are whatever `modelComposition` says this model's slots are, which
|
|
7
|
+
* is why the composition had to exist first: an HMS bey has a Running Core
|
|
8
|
+
* where a Plastic-Gen bey has a separate Spin Gear and Blade Base, and a 4D bey
|
|
9
|
+
* has a combined bottom. A hard-coded five-row form could not express either.
|
|
10
|
+
*
|
|
11
|
+
* When the model has no recorded composition the drawer still works — the
|
|
12
|
+
* seller adds free rows — because 37 of 259 compositions did not join to a
|
|
13
|
+
* category and a seller holding those parts should not be blocked by our data
|
|
14
|
+
* gap.
|
|
15
|
+
*
|
|
16
|
+
* ── 🛑 IT SHOWS AVAILABLE, NOT STOCK, AND SAYS WHY ────────────────────────
|
|
17
|
+
*
|
|
18
|
+
* The server gates on `availableQuantity`: a component with 10 in stock and 3
|
|
19
|
+
* reserved has SEVEN assemblable. So the picker shows the available figure and
|
|
20
|
+
* labels it, because a seller looking at "10 in stock" and being refused at 8
|
|
21
|
+
* would read that as a bug. The client never decides — it previews what the
|
|
22
|
+
* server will say.
|
|
23
|
+
*
|
|
24
|
+
* ── 🛑 IT IS A `<SectionForm>`, NOT A HAND-ROLLED FORM ────────────────────
|
|
25
|
+
*
|
|
26
|
+
* `audit-form-sectionised` caught the first version rendering `FieldSelect` and
|
|
27
|
+
* `FieldInput` directly, and it was right: a hand-rolled form gets no
|
|
28
|
+
* `FormShellContext`, so no `aria-invalid` wiring, no error summary, no
|
|
29
|
+
* jump-to-field and no pinned mobile action bar.
|
|
30
|
+
*
|
|
31
|
+
* The slots are DATA (one row per `modelComposition` slot), so
|
|
32
|
+
* `buildSectionsFromSchema` cannot express them — but `SectionDef.render` can,
|
|
33
|
+
* and `SectionForm` takes explicit sections. So this is a real adoption rather
|
|
34
|
+
* than a marker: the dynamic rows live inside one `render` slot and the form
|
|
35
|
+
* machinery is inherited. A suppression here would have hidden the violation
|
|
36
|
+
* instead of removing it (Root Cause #22).
|
|
37
|
+
*
|
|
38
|
+
* ── 🛑 AND IT REPORTS THE ARITHMETIC AFTERWARDS ───────────────────────────
|
|
39
|
+
*
|
|
40
|
+
* The route returns `before` / `after` / `taken` per component, and this
|
|
41
|
+
* renders all three. A toast reading "built 2" is the shape of silent
|
|
42
|
+
* wrongness this codebase keeps paying for: it is indistinguishable from a
|
|
43
|
+
* build that consumed the wrong parts.
|
|
44
|
+
*/
|
|
45
|
+
import React from "react";
|
|
46
|
+
import { type ModelCompositionDocument } from "../schemas/firestore";
|
|
47
|
+
/** A part listing the seller holds, as the picker needs it. */
|
|
48
|
+
export interface AssemblyPartOption {
|
|
49
|
+
productId: string;
|
|
50
|
+
title: string;
|
|
51
|
+
/** 🛑 AVAILABLE, not stock. The server gates on this. */
|
|
52
|
+
availableQuantity: number;
|
|
53
|
+
colourway?: string;
|
|
54
|
+
/** The `partGlossary` id, so a row can be pre-matched to its slot. */
|
|
55
|
+
partGlossaryId?: string;
|
|
56
|
+
slotKey?: string;
|
|
57
|
+
}
|
|
58
|
+
export interface AssemblyRow {
|
|
59
|
+
slotKey: string;
|
|
60
|
+
slotLabel: string;
|
|
61
|
+
/** Empty until the seller picks a part listing. */
|
|
62
|
+
productId: string;
|
|
63
|
+
quantity: number;
|
|
64
|
+
/** The part name the composition expects, as a hint. */
|
|
65
|
+
expectedPartName?: string;
|
|
66
|
+
}
|
|
67
|
+
/** What the route sends when a 409 names a shortfall. */
|
|
68
|
+
export interface AssemblyErrorDetail {
|
|
69
|
+
code?: string;
|
|
70
|
+
kind?: "component" | "part-out";
|
|
71
|
+
productId?: string;
|
|
72
|
+
needs?: number;
|
|
73
|
+
has?: number;
|
|
74
|
+
}
|
|
75
|
+
export interface AssemblyOutcome {
|
|
76
|
+
unitsBuilt: number;
|
|
77
|
+
consumed: Array<{
|
|
78
|
+
productId: string;
|
|
79
|
+
before: number;
|
|
80
|
+
after: number;
|
|
81
|
+
taken: number;
|
|
82
|
+
}>;
|
|
83
|
+
}
|
|
84
|
+
export interface AssemblyDrawerProps {
|
|
85
|
+
isOpen: boolean;
|
|
86
|
+
onClose: () => void;
|
|
87
|
+
direction: "assemble" | "part-out";
|
|
88
|
+
/** The complete-bey listing being built or broken down. */
|
|
89
|
+
targetTitle: string;
|
|
90
|
+
/** Its own available figure — a part-out cannot exceed it. */
|
|
91
|
+
targetAvailable?: number;
|
|
92
|
+
/** The model's composition, when one is recorded. */
|
|
93
|
+
compositions: readonly ModelCompositionDocument[];
|
|
94
|
+
/** Part listings the seller holds. */
|
|
95
|
+
options: readonly AssemblyPartOption[];
|
|
96
|
+
/** Resolves to the route's result, or throws with a readable message. */
|
|
97
|
+
onSubmit: (input: {
|
|
98
|
+
direction: "assemble" | "part-out";
|
|
99
|
+
components: Array<{
|
|
100
|
+
slotKey: string;
|
|
101
|
+
productId: string;
|
|
102
|
+
quantity: number;
|
|
103
|
+
}>;
|
|
104
|
+
units: number;
|
|
105
|
+
}) => Promise<AssemblyOutcome>;
|
|
106
|
+
}
|
|
107
|
+
export declare function AssemblyDrawer({ isOpen, onClose, direction, targetTitle, targetAvailable, compositions, options, onSubmit, }: AssemblyDrawerProps): React.JSX.Element;
|