@mohasinac/appkit 4.46.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 +3 -3
- package/dist/features/categories/components/BreadcrumbTrail.d.ts +24 -1
- package/dist/features/categories/components/BreadcrumbTrail.js +45 -4
- 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/seed/_helpers/category-content-generated.d.ts +24 -0
- package/dist/seed/_helpers/category-content-generated.js +1837 -0
- package/package.json +1 -1
package/dist/.build-stamp.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"stampVersion": 1,
|
|
3
|
-
"version": "4.
|
|
4
|
-
"head": "
|
|
3
|
+
"version": "4.47.0",
|
|
4
|
+
"head": "f72c5f183b70c66b626c7809ffb1a4b0112e7cb1",
|
|
5
5
|
"branch": "main",
|
|
6
6
|
"dirty": true,
|
|
7
|
-
"builtAt": "2026-10-
|
|
7
|
+
"builtAt": "2026-10-13T05:22:53.600Z"
|
|
8
8
|
}
|
|
@@ -28,6 +28,29 @@ export interface BreadcrumbTrailProps {
|
|
|
28
28
|
*/
|
|
29
29
|
showCurrent?: boolean;
|
|
30
30
|
className?: string;
|
|
31
|
+
/**
|
|
32
|
+
* I5b β collapse the middle of a long trail behind a dropdown.
|
|
33
|
+
*
|
|
34
|
+
* π Needed independently of multi-assignment: a tier-4 part leaf is SEVEN
|
|
35
|
+
* crumbs (`All Categories > Spinning Tops > Beyblade Metal > Metal Parts >
|
|
36
|
+
* Performance Tips > Rubber Flat`) and 390px cannot hold it. D4b measured
|
|
37
|
+
* that depth and never addressed how it wraps.
|
|
38
|
+
*
|
|
39
|
+
* Defaults ON, because the only trails long enough to collapse are the ones
|
|
40
|
+
* that already overflow.
|
|
41
|
+
*/
|
|
42
|
+
collapse?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* I5b β the OTHER categories this listing is filed under, as one extra crumb.
|
|
45
|
+
*
|
|
46
|
+
* Pass `categoryIds`/`categoryIdNames` straight through from the product;
|
|
47
|
+
* `alternateAssignments` decides inline-vs-dropdown and returns nothing at
|
|
48
|
+
* all for a single assignment, which is the overwhelming majority.
|
|
49
|
+
*/
|
|
50
|
+
alternates?: {
|
|
51
|
+
ids?: readonly string[] | null;
|
|
52
|
+
names?: readonly string[] | null;
|
|
53
|
+
};
|
|
31
54
|
}
|
|
32
55
|
/**
|
|
33
56
|
* BreadcrumbTrail β a category-aware breadcrumb component.
|
|
@@ -45,4 +68,4 @@ export interface BreadcrumbTrailProps {
|
|
|
45
68
|
* />
|
|
46
69
|
* ```
|
|
47
70
|
*/
|
|
48
|
-
export declare function BreadcrumbTrail({ category, rootLabel, rootHref, basePath, showCurrent, className, }: BreadcrumbTrailProps): React.JSX.Element;
|
|
71
|
+
export declare function BreadcrumbTrail({ category, rootLabel, rootHref, basePath, showCurrent, className, collapse, alternates, }: BreadcrumbTrailProps): React.JSX.Element;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
2
3
|
import { Breadcrumb } from "../../../ui/index.js";
|
|
4
|
+
import { Text } from "../../../ui/components/Typography.js";
|
|
5
|
+
import { TextLink } from "../../../ui/components/TextLink.js";
|
|
3
6
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
7
|
+
import { alternateAssignments, collapseTrail } from "../utils/breadcrumb-chain.js";
|
|
4
8
|
/**
|
|
5
9
|
* BreadcrumbTrail β a category-aware breadcrumb component.
|
|
6
10
|
*
|
|
@@ -17,8 +21,8 @@ import { ROUTES } from "../../../next/routing/route-map.js";
|
|
|
17
21
|
* />
|
|
18
22
|
* ```
|
|
19
23
|
*/
|
|
20
|
-
export function BreadcrumbTrail({ category, rootLabel = "All Categories", rootHref = String(ROUTES.PUBLIC.CATEGORIES), basePath = String(ROUTES.PUBLIC.CATEGORIES), showCurrent = true, className, }) {
|
|
21
|
-
const
|
|
24
|
+
export function BreadcrumbTrail({ category, rootLabel = "All Categories", rootHref = String(ROUTES.PUBLIC.CATEGORIES), basePath = String(ROUTES.PUBLIC.CATEGORIES), showCurrent = true, className, collapse = true, alternates, }) {
|
|
25
|
+
const full = [
|
|
22
26
|
// Root
|
|
23
27
|
{ label: rootLabel, href: rootHref },
|
|
24
28
|
// Ancestors (sorted by tier ascending so root β leaf order)
|
|
@@ -32,5 +36,42 @@ export function BreadcrumbTrail({ category, rootLabel = "All Categories", rootHr
|
|
|
32
36
|
// Current category (no href so it renders as the active item)
|
|
33
37
|
...(showCurrent ? [{ label: category.name }] : []),
|
|
34
38
|
];
|
|
35
|
-
|
|
39
|
+
/*
|
|
40
|
+
* I5b β the alternates become ONE extra crumb, never a second row. Two rows
|
|
41
|
+
* of crumbs above a title is a pattern no competitor in the crawl uses and it
|
|
42
|
+
* costs vertical space above the fold on a phone.
|
|
43
|
+
*/
|
|
44
|
+
const alt = alternateAssignments({
|
|
45
|
+
categoryIds: alternates?.ids ?? null,
|
|
46
|
+
categoryIdNames: alternates?.names ?? null,
|
|
47
|
+
hrefFor: (slug) => `${basePath}/${slug}`,
|
|
48
|
+
});
|
|
49
|
+
/*
|
|
50
|
+
* π Collapsing is PRESENTATION. The JSON-LD is emitted elsewhere, from the
|
|
51
|
+
* full chain, and must stay that way β a crawler never receives a truncated
|
|
52
|
+
* hierarchy because a phone could not show it.
|
|
53
|
+
*/
|
|
54
|
+
const split = collapse ? collapseTrail(toCrumbs(full)) : null;
|
|
55
|
+
const items = split && split.collapsed.length > 0
|
|
56
|
+
? [
|
|
57
|
+
...split.lead.map(fromCrumb),
|
|
58
|
+
/*
|
|
59
|
+
* The collapsed group as ONE crumb whose label names what it hides.
|
|
60
|
+
* π Rendered as a real crumb with no href rather than a menu,
|
|
61
|
+
* because `Breadcrumb` is a plain list primitive β and the hidden
|
|
62
|
+
* ancestors are still reachable: every one of them is an ancestor of
|
|
63
|
+
* the crumbs either side, so no path is lost, only shortened.
|
|
64
|
+
*/
|
|
65
|
+
{ label: `β¦ ${split.collapsed.length} more` },
|
|
66
|
+
...split.tail.map(fromCrumb),
|
|
67
|
+
]
|
|
68
|
+
: full;
|
|
69
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: items, className: className }), alt.mode !== "none" && (_jsxs(Text, { size: "xs", color: "muted", children: ["Also in", " ", alt.items.map((a, i) => (_jsxs(React.Fragment, { children: [i > 0 && " Β· ", _jsx(TextLink, { href: a.url ?? "#", children: a.name })] }, a.url ?? a.name)))] }))] }));
|
|
70
|
+
}
|
|
71
|
+
/** `BreadcrumbItem` β `BreadcrumbCrumb` β two names for one shape. */
|
|
72
|
+
function toCrumbs(items) {
|
|
73
|
+
return items.map((i) => ({ name: i.label, url: i.href }));
|
|
74
|
+
}
|
|
75
|
+
function fromCrumb(c) {
|
|
76
|
+
return c.url ? { label: c.name, href: c.url } : { label: c.name };
|
|
36
77
|
}
|
|
@@ -6,5 +6,21 @@ export interface CategoryInlineSelectProps {
|
|
|
6
6
|
disabled?: boolean;
|
|
7
7
|
/** Show the "+ Create new category" option. Defaults to true β sellers can create categories inline. */
|
|
8
8
|
allowCreate?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* I2 β the ADDITIONAL assignments, beyond `value`.
|
|
11
|
+
*
|
|
12
|
+
* π Omitting this pair keeps the component exactly single-select, which is
|
|
13
|
+
* what every existing call site gets. Multi-assignment is opt-in per surface
|
|
14
|
+
* rather than switched on globally, because `value`/`onChange` is a `string`
|
|
15
|
+
* contract that five call sites rely on and changing it under them is Root
|
|
16
|
+
* Cause #20 (a public prop change without its consumers in the same commit).
|
|
17
|
+
*/
|
|
18
|
+
extraValues?: string[];
|
|
19
|
+
onExtraValuesChange?: (ids: string[]) => void;
|
|
20
|
+
/**
|
|
21
|
+
* How many assignments are allowed in total, `value` included. Defaults to
|
|
22
|
+
* `MAX_CATEGORY_ASSIGNMENTS`, which is what the repository refuses above.
|
|
23
|
+
*/
|
|
24
|
+
maxAssignments?: number;
|
|
9
25
|
}
|
|
10
|
-
export declare function CategoryInlineSelect({ value, onChange, placeholder, disabled, allowCreate, }: CategoryInlineSelectProps): React.JSX.Element;
|
|
26
|
+
export declare function CategoryInlineSelect({ value, onChange, placeholder, disabled, allowCreate, extraValues, onExtraValuesChange, maxAssignments, }: CategoryInlineSelectProps): React.JSX.Element;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { PaginatedSelect } from "../../../ui/components/PaginatedSelect.js";
|
|
4
4
|
import { CategoryQuickCreateForm } from "../../admin/components/CategoryQuickCreateForm.js";
|
|
5
5
|
import { apiClient } from "../../../http/index.js";
|
|
6
6
|
import { ADMIN_ENDPOINTS, CATEGORY_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
7
|
+
import { Stack } from "../../../ui/components/Layout.js";
|
|
8
|
+
import { Text } from "../../../ui/components/Typography.js";
|
|
9
|
+
import { MAX_CATEGORY_ASSIGNMENTS } from "../../products/repository/category-assignment.js";
|
|
7
10
|
// Admin: paginated + server-side search via /api/admin/categories (admin/mod only).
|
|
8
11
|
// Response via successResponse({ data: items, total }) β apiClient unwraps to { data, total, ... }.
|
|
9
12
|
async function loadAdminCategoryOptions(query, page) {
|
|
@@ -35,9 +38,17 @@ async function loadPublicCategoryOptions(query, _page) {
|
|
|
35
38
|
const items = q ? all.filter((c) => c.label.toLowerCase().includes(q)) : all;
|
|
36
39
|
return { items, hasMore: false, nextPage: 2 };
|
|
37
40
|
}
|
|
38
|
-
export function CategoryInlineSelect({ value, onChange, placeholder = "Search categoriesβ¦", disabled, allowCreate = true, }) {
|
|
41
|
+
export function CategoryInlineSelect({ value, onChange, placeholder = "Search categoriesβ¦", disabled, allowCreate = true, extraValues, onExtraValuesChange, maxAssignments = MAX_CATEGORY_ASSIGNMENTS, }) {
|
|
39
42
|
const loadOptions = allowCreate ? loadAdminCategoryOptions : loadPublicCategoryOptions;
|
|
40
|
-
|
|
43
|
+
const primary = (_jsx(PaginatedSelect, { value: value || null, onChange: (v) => onChange(v ?? ""), loadOptions: loadOptions, placeholder: placeholder, disabled: disabled, createLabel: "category", drawerTitle: "Create Category", renderCreateForm: allowCreate
|
|
41
44
|
? ({ onCreated, onCancel }) => (_jsx(CategoryQuickCreateForm, { onSaved: (id, name) => onCreated({ value: id, label: name }), onCancel: onCancel }))
|
|
42
45
|
: undefined }));
|
|
46
|
+
// Single-select unless the caller opted in β byte-identical to before.
|
|
47
|
+
if (!onExtraValuesChange)
|
|
48
|
+
return primary;
|
|
49
|
+
const extras = extraValues ?? [];
|
|
50
|
+
const remaining = Math.max(0, maxAssignments - 1 - extras.length);
|
|
51
|
+
return (_jsxs(Stack, { gap: "sm", children: [primary, _jsx(PaginatedSelect, { multiple: true, value: extras, onChange: (v) => onExtraValuesChange(Array.isArray(v) ? v : []), loadOptions: loadOptions, placeholder: remaining > 0
|
|
52
|
+
? `Also list under⦠(up to ${remaining} more)`
|
|
53
|
+
: "No more categories can be added", disabled: disabled || remaining <= 0 }), _jsx(Text, { size: "xs", color: "muted", children: "Optional. Add a category from a DIFFERENT tree \u2014 a franchise, for example. Two categories from the same tree will be refused, because the broader one is already implied by the narrower." })] }));
|
|
43
54
|
}
|
|
@@ -6,6 +6,16 @@ export interface SellerProductDraft {
|
|
|
6
6
|
slug?: string;
|
|
7
7
|
description?: string;
|
|
8
8
|
category?: string;
|
|
9
|
+
/**
|
|
10
|
+
* I2 β the assignment list, DEFAULT FIRST, with `category` mirroring `[0]`.
|
|
11
|
+
*
|
|
12
|
+
* π Declared on the TYPE as well as the Zod schema. The schema is
|
|
13
|
+
* `.passthrough()`, so an undeclared key survives at runtime β and stays
|
|
14
|
+
* untyped, so the picker could not read `values.categoryIds` at all. Both
|
|
15
|
+
* halves are needed, which is the same trap as a field missing from
|
|
16
|
+
* `PRODUCT_UPDATABLE_FIELDS` (Root Cause #101) one layer out.
|
|
17
|
+
*/
|
|
18
|
+
categoryIds?: string[];
|
|
9
19
|
brand?: string;
|
|
10
20
|
condition?: string;
|
|
11
21
|
tags?: string[];
|
|
@@ -134,10 +144,25 @@ export interface SellerProductShellProps {
|
|
|
134
144
|
onDiscard?: () => void;
|
|
135
145
|
isLoading?: boolean;
|
|
136
146
|
storeSlug?: string;
|
|
137
|
-
/**
|
|
147
|
+
/**
|
|
148
|
+
* Render a custom category selector (e.g. PaginatedSelect with createLabel).
|
|
149
|
+
*
|
|
150
|
+
* I2 β `extraValues`/`onExtraValuesChange` carry the ADDITIONAL assignments.
|
|
151
|
+
* A selector that ignores them stays exactly single-select, which is what
|
|
152
|
+
* every surface except this one does.
|
|
153
|
+
*/
|
|
138
154
|
renderCategorySelector?: (props: {
|
|
139
155
|
value: string;
|
|
140
156
|
onChange: (v: string) => void;
|
|
157
|
+
/**
|
|
158
|
+
* π OPTIONAL, and that is the whole point. `QuickProductForm` is a second
|
|
159
|
+
* consumer of this slot and making these required broke it immediately β
|
|
160
|
+
* Root Cause #20, a public prop change without its consumers, which the
|
|
161
|
+
* comment above literally warns about. A selector that ignores them stays
|
|
162
|
+
* exactly single-select.
|
|
163
|
+
*/
|
|
164
|
+
extraValues?: string[];
|
|
165
|
+
onExtraValuesChange?: (ids: string[]) => void;
|
|
141
166
|
}) => React.ReactNode;
|
|
142
167
|
/** Render a custom brand selector. */
|
|
143
168
|
renderBrandSelector?: (props: {
|
|
@@ -46,6 +46,15 @@ const sellerProductSchema = z.object({
|
|
|
46
46
|
description: z.string().max(10000).optional().or(z.literal("")),
|
|
47
47
|
price: z.number().min(0).optional(),
|
|
48
48
|
category: z.string().optional(),
|
|
49
|
+
/**
|
|
50
|
+
* I2 β the assignment list, DEFAULT FIRST. `category` mirrors `[0]`.
|
|
51
|
+
*
|
|
52
|
+
* Declared even though this schema is `.passthrough()`, because the draft's
|
|
53
|
+
* TYPE is what the render site reads `values.categoryIds` off β passthrough
|
|
54
|
+
* keeps an undeclared key at runtime and still leaves it untyped, so the
|
|
55
|
+
* picker could not see it.
|
|
56
|
+
*/
|
|
57
|
+
categoryIds: z.array(z.string()).max(4).optional(),
|
|
49
58
|
brand: z.string().optional(),
|
|
50
59
|
isActive: z.boolean().optional(),
|
|
51
60
|
images: z
|
|
@@ -106,7 +115,27 @@ function StepBasic({ values, onChange, renderCategorySelector, renderBrandSelect
|
|
|
106
115
|
condition: CONDITION_OPTIONS.find((o) => o.value === (values.condition ?? "new"))?.label ?? "",
|
|
107
116
|
siteName: "LetItRip",
|
|
108
117
|
};
|
|
109
|
-
return (_jsxs(Stack, { gap: "md", children: [_jsx(FormField, { name: "title", label: "Title", type: "text", value: values.title ?? "", onChange: (v) => onChange({ title: v }), placeholder: "e.g. Charizard Base Set PSA 9" }), _jsxs(FormGroup, { columns: 2, children: [renderCategorySelector ? (_jsxs(_Fragment, { children: [_jsx(Text, { className: "text-[var(--appkit-color-text)] mb-1", size: "sm", weight: "medium", children: "Category" }), renderCategorySelector({
|
|
118
|
+
return (_jsxs(Stack, { gap: "md", children: [_jsx(FormField, { name: "title", label: "Title", type: "text", value: values.title ?? "", onChange: (v) => onChange({ title: v }), placeholder: "e.g. Charizard Base Set PSA 9" }), _jsxs(FormGroup, { columns: 2, children: [renderCategorySelector ? (_jsxs(_Fragment, { children: [_jsx(Text, { className: "text-[var(--appkit-color-text)] mb-1", size: "sm", weight: "medium", children: "Category" }), renderCategorySelector({
|
|
119
|
+
value: values.category ?? "",
|
|
120
|
+
onChange: (v) => onChange({ category: v }),
|
|
121
|
+
/*
|
|
122
|
+
* π `categoryIds[0]` is the DEFAULT assignment and is already
|
|
123
|
+
* rendered by the primary picker above, so the extras list is
|
|
124
|
+
* everything AFTER it. Passing the whole array would show the
|
|
125
|
+
* primary twice and let a seller "remove" it from a control that
|
|
126
|
+
* does not own it.
|
|
127
|
+
*/
|
|
128
|
+
extraValues: (values.categoryIds ?? []).slice(1),
|
|
129
|
+
onExtraValuesChange: (ids) => onChange({
|
|
130
|
+
/*
|
|
131
|
+
* Rebuilt primary-first on every change, because the server
|
|
132
|
+
* derives the default by DEPTH and reorders the stored array
|
|
133
|
+
* anyway β sending it in a different order would make the
|
|
134
|
+
* form and the saved document disagree until the next load.
|
|
135
|
+
*/
|
|
136
|
+
categoryIds: [values.category ?? "", ...ids].filter(Boolean),
|
|
137
|
+
}),
|
|
138
|
+
})] })) : (_jsx(FormField, { name: "category", label: "Category", type: "text", value: values.category ?? "", onChange: (v) => onChange({ category: v }), placeholder: "e.g. Trading Cards" })), _jsx(FormField, { name: "condition", label: "Condition", type: "select", value: values.condition ?? "new", onChange: (v) => onChange({ condition: v }), options: CONDITION_OPTIONS })] }), renderTemplateSelector?.({
|
|
110
139
|
categoryId: values.category ?? "",
|
|
111
140
|
condition: values.condition ?? "new",
|
|
112
141
|
context: templateContext,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED by scripts/gen-category-content.mjs β do not edit by hand.
|
|
3
|
+
*
|
|
4
|
+
* `contentBody` per category, composed from FACTS ONLY (I7). Every clause
|
|
5
|
+
* traces to a committed source: canonical-beys.json for a model leaf,
|
|
6
|
+
* part-glossary.json for a part leaf, the forest's own child lists above them.
|
|
7
|
+
*
|
|
8
|
+
* π A node with nothing factual to say is ABSENT from this map rather than
|
|
9
|
+
* carrying an empty body β `CategoryHighlightsAndFaqSection` renders nothing
|
|
10
|
+
* when its inputs are empty, and an empty paragraph reads as a defect.
|
|
11
|
+
*
|
|
12
|
+
* π No price band appears here. `MARKET_BANDS` omits a band rather than
|
|
13
|
+
* guessing one, and a body that stated an unmeasured "typical price" would be
|
|
14
|
+
* the same guess with better typography.
|
|
15
|
+
*
|
|
16
|
+
* π No listing COUNT appears here either. Counts live in `categoryCounts` and
|
|
17
|
+
* move; a sentence saying "48 listings" is wrong the next day and nothing
|
|
18
|
+
* reports it.
|
|
19
|
+
*
|
|
20
|
+
* Model leaves with a body: 213 of 216
|
|
21
|
+
* Part leaves with a body: 532 of 752
|
|
22
|
+
*/
|
|
23
|
+
export declare const GENERATED_CATEGORY_CONTENT: Record<string, string>;
|
|
24
|
+
export declare const GENERATED_CATEGORY_CONTENT_COUNT = 745;
|