@stapel/categories-react 0.1.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/CHANGELOG.md +1 -0
- package/MODULE.md +194 -0
- package/README.md +191 -0
- package/dist/api/categoriesApi.d.ts +121 -0
- package/dist/api/categoriesApi.d.ts.map +1 -0
- package/dist/api/categoriesApi.js +48 -0
- package/dist/api/categoriesApi.js.map +1 -0
- package/dist/api/generated/schema.d.ts +2321 -0
- package/dist/api/generated/schema.d.ts.map +1 -0
- package/dist/api/generated/schema.js +2 -0
- package/dist/api/generated/schema.js.map +1 -0
- package/dist/api/types.d.ts +92 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +2 -0
- package/dist/api/types.js.map +1 -0
- package/dist/catalog/labels.d.ts +103 -0
- package/dist/catalog/labels.d.ts.map +1 -0
- package/dist/catalog/labels.js +56 -0
- package/dist/catalog/labels.js.map +1 -0
- package/dist/catalog/pks.d.ts +25 -0
- package/dist/catalog/pks.d.ts.map +1 -0
- package/dist/catalog/pks.js +38 -0
- package/dist/catalog/pks.js.map +1 -0
- package/dist/catalog/sync.d.ts +103 -0
- package/dist/catalog/sync.d.ts.map +1 -0
- package/dist/catalog/sync.js +114 -0
- package/dist/catalog/sync.js.map +1 -0
- package/dist/catalog/tree.d.ts +119 -0
- package/dist/catalog/tree.d.ts.map +1 -0
- package/dist/catalog/tree.js +166 -0
- package/dist/catalog/tree.js.map +1 -0
- package/dist/default/CatalogPage.d.ts +16 -0
- package/dist/default/CatalogPage.d.ts.map +1 -0
- package/dist/default/CatalogPage.js +13 -0
- package/dist/default/CatalogPage.js.map +1 -0
- package/dist/default/CategoryBreadcrumbsBar.d.ts +20 -0
- package/dist/default/CategoryBreadcrumbsBar.d.ts.map +1 -0
- package/dist/default/CategoryBreadcrumbsBar.js +35 -0
- package/dist/default/CategoryBreadcrumbsBar.js.map +1 -0
- package/dist/default/CategoryCarouselStrip.d.ts +22 -0
- package/dist/default/CategoryCarouselStrip.d.ts.map +1 -0
- package/dist/default/CategoryCarouselStrip.js +24 -0
- package/dist/default/CategoryCarouselStrip.js.map +1 -0
- package/dist/default/CategoryFeatureList.d.ts +23 -0
- package/dist/default/CategoryFeatureList.d.ts.map +1 -0
- package/dist/default/CategoryFeatureList.js +28 -0
- package/dist/default/CategoryFeatureList.js.map +1 -0
- package/dist/default/CategoryPage.d.ts +43 -0
- package/dist/default/CategoryPage.d.ts.map +1 -0
- package/dist/default/CategoryPage.js +21 -0
- package/dist/default/CategoryPage.js.map +1 -0
- package/dist/default/CategoryPickerField.d.ts +26 -0
- package/dist/default/CategoryPickerField.d.ts.map +1 -0
- package/dist/default/CategoryPickerField.js +40 -0
- package/dist/default/CategoryPickerField.js.map +1 -0
- package/dist/default/CategoryTreePane.d.ts +28 -0
- package/dist/default/CategoryTreePane.d.ts.map +1 -0
- package/dist/default/CategoryTreePane.js +25 -0
- package/dist/default/CategoryTreePane.js.map +1 -0
- package/dist/default/ErrorAlert.d.ts +9 -0
- package/dist/default/ErrorAlert.d.ts.map +1 -0
- package/dist/default/ErrorAlert.js +27 -0
- package/dist/default/ErrorAlert.js.map +1 -0
- package/dist/default/index.d.ts +37 -0
- package/dist/default/index.d.ts.map +1 -0
- package/dist/default/index.js +28 -0
- package/dist/default/index.js.map +1 -0
- package/dist/default/theme.d.ts +25 -0
- package/dist/default/theme.d.ts.map +1 -0
- package/dist/default/theme.js +40 -0
- package/dist/default/theme.js.map +1 -0
- package/dist/default/types.d.ts +12 -0
- package/dist/default/types.d.ts.map +1 -0
- package/dist/default/types.js +2 -0
- package/dist/default/types.js.map +1 -0
- package/dist/headless/CategoriesProvider.d.ts +19 -0
- package/dist/headless/CategoriesProvider.d.ts.map +1 -0
- package/dist/headless/CategoriesProvider.js +15 -0
- package/dist/headless/CategoriesProvider.js.map +1 -0
- package/dist/headless/CategoryBreadcrumbs.d.ts +40 -0
- package/dist/headless/CategoryBreadcrumbs.d.ts.map +1 -0
- package/dist/headless/CategoryBreadcrumbs.js +42 -0
- package/dist/headless/CategoryBreadcrumbs.js.map +1 -0
- package/dist/headless/CategoryCarousel.d.ts +48 -0
- package/dist/headless/CategoryCarousel.d.ts.map +1 -0
- package/dist/headless/CategoryCarousel.js +34 -0
- package/dist/headless/CategoryCarousel.js.map +1 -0
- package/dist/headless/CategoryFeatures.d.ts +67 -0
- package/dist/headless/CategoryFeatures.d.ts.map +1 -0
- package/dist/headless/CategoryFeatures.js +46 -0
- package/dist/headless/CategoryFeatures.js.map +1 -0
- package/dist/headless/CategoryPicker.d.ts +69 -0
- package/dist/headless/CategoryPicker.d.ts.map +1 -0
- package/dist/headless/CategoryPicker.js +86 -0
- package/dist/headless/CategoryPicker.js.map +1 -0
- package/dist/headless/CategoryTree.d.ts +55 -0
- package/dist/headless/CategoryTree.d.ts.map +1 -0
- package/dist/headless/CategoryTree.js +51 -0
- package/dist/headless/CategoryTree.js.map +1 -0
- package/dist/i18n/errorsMap.d.ts +22 -0
- package/dist/i18n/errorsMap.d.ts.map +1 -0
- package/dist/i18n/errorsMap.js +31 -0
- package/dist/i18n/errorsMap.js.map +1 -0
- package/dist/i18n/es.d.ts +25 -0
- package/dist/i18n/es.d.ts.map +1 -0
- package/dist/i18n/es.js +74 -0
- package/dist/i18n/es.js.map +1 -0
- package/dist/i18n/generated/errors.es.gen.d.ts +16 -0
- package/dist/i18n/generated/errors.es.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.es.gen.js +58 -0
- package/dist/i18n/generated/errors.es.gen.js.map +1 -0
- package/dist/i18n/generated/errors.gen.d.ts +407 -0
- package/dist/i18n/generated/errors.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.gen.js +207 -0
- package/dist/i18n/generated/errors.gen.js.map +1 -0
- package/dist/i18n/generated/errors.ru.gen.d.ts +16 -0
- package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.ru.gen.js +58 -0
- package/dist/i18n/generated/errors.ru.gen.js.map +1 -0
- package/dist/i18n/keys.d.ts +89 -0
- package/dist/i18n/keys.d.ts.map +1 -0
- package/dist/i18n/keys.js +134 -0
- package/dist/i18n/keys.js.map +1 -0
- package/dist/i18n/ru.d.ts +41 -0
- package/dist/i18n/ru.d.ts.map +1 -0
- package/dist/i18n/ru.js +90 -0
- package/dist/i18n/ru.js.map +1 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +78 -0
- package/dist/index.js.map +1 -0
- package/dist/model/catalogStore.d.ts +23 -0
- package/dist/model/catalogStore.d.ts.map +1 -0
- package/dist/model/catalogStore.js +85 -0
- package/dist/model/catalogStore.js.map +1 -0
- package/dist/model/catalogSync.d.ts +49 -0
- package/dist/model/catalogSync.d.ts.map +1 -0
- package/dist/model/catalogSync.js +64 -0
- package/dist/model/catalogSync.js.map +1 -0
- package/dist/model/context.d.ts +11 -0
- package/dist/model/context.d.ts.map +1 -0
- package/dist/model/context.js +16 -0
- package/dist/model/context.js.map +1 -0
- package/dist/model/queries.d.ts +124 -0
- package/dist/model/queries.d.ts.map +1 -0
- package/dist/model/queries.js +147 -0
- package/dist/model/queries.js.map +1 -0
- package/dist/model/queryKeys.d.ts +30 -0
- package/dist/model/queryKeys.d.ts.map +1 -0
- package/dist/model/queryKeys.js +16 -0
- package/dist/model/queryKeys.js.map +1 -0
- package/dist/model/runtime.d.ts +31 -0
- package/dist/model/runtime.d.ts.map +1 -0
- package/dist/model/runtime.js +6 -0
- package/dist/model/runtime.js.map +1 -0
- package/dist/nav/manifest.d.ts +31 -0
- package/dist/nav/manifest.d.ts.map +1 -0
- package/dist/nav/manifest.js +37 -0
- package/dist/nav/manifest.js.map +1 -0
- package/llms.txt +125 -0
- package/manifest.json +918 -0
- package/nav-manifest.json +44 -0
- package/package.json +119 -0
- package/src/analytics/generated/events.json +7 -0
- package/src/api/categoriesApi.ts +179 -0
- package/src/api/generated/schema.ts +2323 -0
- package/src/api/types.ts +99 -0
- package/src/catalog/labels.ts +129 -0
- package/src/catalog/pks.ts +35 -0
- package/src/catalog/sync.ts +169 -0
- package/src/catalog/tree.ts +242 -0
- package/src/default/CatalogPage.tsx +40 -0
- package/src/default/CategoryBreadcrumbsBar.tsx +93 -0
- package/src/default/CategoryCarouselStrip.tsx +102 -0
- package/src/default/CategoryFeatureList.tsx +125 -0
- package/src/default/CategoryPage.tsx +114 -0
- package/src/default/CategoryPickerField.tsx +173 -0
- package/src/default/CategoryTreePane.tsx +134 -0
- package/src/default/ErrorAlert.tsx +46 -0
- package/src/default/index.ts +36 -0
- package/src/default/theme.tsx +67 -0
- package/src/default/types.ts +12 -0
- package/src/headless/CategoriesProvider.tsx +20 -0
- package/src/headless/CategoryBreadcrumbs.tsx +78 -0
- package/src/headless/CategoryCarousel.tsx +80 -0
- package/src/headless/CategoryFeatures.tsx +100 -0
- package/src/headless/CategoryPicker.tsx +173 -0
- package/src/headless/CategoryTree.tsx +115 -0
- package/src/i18n/errorsMap.ts +46 -0
- package/src/i18n/es.ts +92 -0
- package/src/i18n/generated/errors.es.gen.ts +64 -0
- package/src/i18n/generated/errors.gen.ts +238 -0
- package/src/i18n/generated/errors.json +495 -0
- package/src/i18n/generated/errors.ru.gen.ts +64 -0
- package/src/i18n/keys.ts +156 -0
- package/src/i18n/ru.ts +110 -0
- package/src/index.ts +191 -0
- package/src/model/catalogStore.ts +102 -0
- package/src/model/catalogSync.ts +115 -0
- package/src/model/context.tsx +28 -0
- package/src/model/queries.ts +226 -0
- package/src/model/queryKeys.ts +50 -0
- package/src/model/runtime.ts +39 -0
- package/src/nav/manifest.ts +66 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { UseQueryResult } from "@tanstack/react-query";
|
|
2
|
+
import type { StapelApiError } from "@stapel/core";
|
|
3
|
+
import type { Category, CategoryFeature, MaxRevision } from "../api/types.js";
|
|
4
|
+
import type { BuildCategoryTreeOptions, CategoryIndex } from "../catalog/tree.js";
|
|
5
|
+
import type { CategorySnapshot } from "../catalog/sync.js";
|
|
6
|
+
import type { CatalogStore } from "./catalogStore.js";
|
|
7
|
+
/**
|
|
8
|
+
* Read hooks over the categories API (frontend-standard §2). Keys are
|
|
9
|
+
* namespaced (see `categoriesQueryKeys`).
|
|
10
|
+
*
|
|
11
|
+
* NOT SESSION-GATED, and that is the documented exception rather than an
|
|
12
|
+
* omission. Sibling pairs gate every read on `useActiveSessionReady` because
|
|
13
|
+
* their endpoints need a principal; every read here is a SAFE method under
|
|
14
|
+
* `ReadOnlyOrStaff`, which anonymous callers may make. Gating them would make
|
|
15
|
+
* a storefront's catalogue wait for a login bootstrap that a visitor who will
|
|
16
|
+
* never sign in has no stake in.
|
|
17
|
+
*/
|
|
18
|
+
/** What `useCategoryCatalog` hands back beside the tree. */
|
|
19
|
+
export interface CategoryCatalog {
|
|
20
|
+
readonly index: CategoryIndex;
|
|
21
|
+
/** The snapshot the tree was built from — cursor included, so a host can
|
|
22
|
+
* show "catalogue as of revision N" or drive its own refresh policy. */
|
|
23
|
+
readonly snapshot: CategorySnapshot;
|
|
24
|
+
/** The page budget stopped the walk. The tree is a PARTIAL catalogue: a
|
|
25
|
+
* missing branch here is not an empty branch, and a skin that can say so
|
|
26
|
+
* should. */
|
|
27
|
+
readonly truncated: boolean;
|
|
28
|
+
/** The walk started from nothing (cold cache), rather than applying a delta. */
|
|
29
|
+
readonly wasFullSync: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface UseCategoryCatalogOptions extends BuildCategoryTreeOptions {
|
|
32
|
+
readonly enabled?: boolean;
|
|
33
|
+
/** Rows per sync request (server default 100, max 1000). */
|
|
34
|
+
readonly pageSize?: number;
|
|
35
|
+
/** Persistence seam. Default: an app-scoped `createRepository` store. Pass
|
|
36
|
+
* `memoryCatalogStore()` to opt out of persistence entirely. */
|
|
37
|
+
readonly store?: CatalogStore;
|
|
38
|
+
/**
|
|
39
|
+
* How long the built catalogue is considered fresh. Default: five minutes.
|
|
40
|
+
*
|
|
41
|
+
* A catalogue changes when somebody edits it in the admin, not while a
|
|
42
|
+
* person browses, and every refetch is a delta request that usually returns
|
|
43
|
+
* zero rows — so the default is generous on purpose. `0` makes every mount
|
|
44
|
+
* re-ask.
|
|
45
|
+
*/
|
|
46
|
+
readonly staleTime?: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The whole category tree, delta-synced and cached across page loads.
|
|
50
|
+
*
|
|
51
|
+
* This is the hook a storefront mounts once, high up: `/c/:slug` resolves its
|
|
52
|
+
* slug against it, breadcrumbs walk it, the picker in the compose form reads
|
|
53
|
+
* it, and none of them costs a request. Without it every page pulls the
|
|
54
|
+
* catalogue (spec §4.3).
|
|
55
|
+
*
|
|
56
|
+
* ── What one "load" actually does ──────────────────────────────────────────
|
|
57
|
+
*
|
|
58
|
+
* read the stored snapshot (app-scoped repository; may be empty)
|
|
59
|
+
* → GET /categories/?include_deleted=false (cold)
|
|
60
|
+
* or GET /categories/?min_revision=<cursor> (warm)
|
|
61
|
+
* → follow `has_next` with the window pinned by `max_revision`
|
|
62
|
+
* → fold rows + `revisions.deleted_ids` into the snapshot
|
|
63
|
+
* → persist, then build the tree
|
|
64
|
+
*
|
|
65
|
+
* The load discipline is the fleet's: `loading` while the first answer is in
|
|
66
|
+
* flight (including while the repository read is), `failed` on a refusal — a
|
|
67
|
+
* failed sync never degrades to "the catalogue is empty". Because the stored
|
|
68
|
+
* snapshot is read INSIDE the query function, a warm start still reports
|
|
69
|
+
* `loading` on the very first render and then resolves in the same tick; it
|
|
70
|
+
* does not flash an empty tree.
|
|
71
|
+
*
|
|
72
|
+
* `retry: false`: a sync failure is worth reporting immediately, and the
|
|
73
|
+
* caller has `refetch`. Three silent retries only delay the sentence.
|
|
74
|
+
*/
|
|
75
|
+
export declare function useCategoryCatalog(options?: UseCategoryCatalogOptions): UseQueryResult<CategoryCatalog, StapelApiError>;
|
|
76
|
+
/**
|
|
77
|
+
* One category's direct children, straight from the server.
|
|
78
|
+
*
|
|
79
|
+
* The synced tree already knows them; this is for the host that does not mount
|
|
80
|
+
* the catalogue — an SSR category page, a lazily expanded branch. The server
|
|
81
|
+
* filters `deleted` here and orders by `tn_priority` descending, but it does
|
|
82
|
+
* NOT filter `active`, so a public screen still has to.
|
|
83
|
+
*/
|
|
84
|
+
export declare function useCategoryChildren(id: number | null | undefined, options?: {
|
|
85
|
+
readonly enabled?: boolean;
|
|
86
|
+
}): UseQueryResult<readonly Category[], StapelApiError>;
|
|
87
|
+
/**
|
|
88
|
+
* The carousel strip for a landing page.
|
|
89
|
+
*
|
|
90
|
+
* The one endpoint the server already filters completely (`active` AND
|
|
91
|
+
* `carousel_enabled`) and already caches, sending
|
|
92
|
+
* `Cache-Control: public, max-age`. `staleTime` mirrors that: the browser's
|
|
93
|
+
* HTTP cache and the query cache should not disagree about how fresh this is.
|
|
94
|
+
*/
|
|
95
|
+
export declare function useCategoryCarousel(options?: {
|
|
96
|
+
readonly enabled?: boolean;
|
|
97
|
+
readonly staleTime?: number;
|
|
98
|
+
}): UseQueryResult<readonly Category[], StapelApiError>;
|
|
99
|
+
/**
|
|
100
|
+
* A category's resolved feature schema — own plus inherited, deduplicated by
|
|
101
|
+
* slug, in the category's own order.
|
|
102
|
+
*
|
|
103
|
+
* This is the payload `@stapel/attributes-react` draws (the compose form) and
|
|
104
|
+
* labels facets from (the search pair's `categoryFeatures` slot). It changes
|
|
105
|
+
* only when the catalogue is edited, hence the same generous `staleTime`.
|
|
106
|
+
*/
|
|
107
|
+
export declare function useCategoryFeatures(id: number | null | undefined, options?: {
|
|
108
|
+
readonly enabled?: boolean;
|
|
109
|
+
readonly staleTime?: number;
|
|
110
|
+
}): UseQueryResult<readonly CategoryFeature[], StapelApiError>;
|
|
111
|
+
/**
|
|
112
|
+
* The catalogue's current maximum revision — the cheap freshness probe.
|
|
113
|
+
*
|
|
114
|
+
* A host that wants to poll for catalogue changes without transferring rows
|
|
115
|
+
* compares this to `catalog.snapshot.cursor` and invalidates the catalogue key
|
|
116
|
+
* when it moves. Not wired into `useCategoryCatalog` on purpose: how often to
|
|
117
|
+
* ask is a deployment's decision, and a library that picked one would be
|
|
118
|
+
* polling somebody's API on their behalf.
|
|
119
|
+
*/
|
|
120
|
+
export declare function useCategoriesRevision(options?: {
|
|
121
|
+
readonly enabled?: boolean;
|
|
122
|
+
readonly refetchInterval?: number;
|
|
123
|
+
}): UseQueryResult<MaxRevision, StapelApiError>;
|
|
124
|
+
//# sourceMappingURL=queries.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queries.d.ts","sourceRoot":"","sources":["../../src/model/queries.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9E,OAAO,KAAK,EAAE,wBAAwB,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAClF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAG3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAItD;;;;;;;;;;GAUG;AAEH,4DAA4D;AAC5D,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B;4EACwE;IACxE,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC;;iBAEa;IACb,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,gFAAgF;IAChF,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,yBAA0B,SAAQ,wBAAwB;IACzE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;oEACgE;IAChE,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC;IAC9B;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,GAAE,yBAA8B,GACtC,cAAc,CAAC,eAAe,EAAE,cAAc,CAAC,CA8CjD;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC7B,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GACvC,cAAc,CAAC,SAAS,QAAQ,EAAE,EAAE,cAAc,CAAC,CAQrD;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,CAAC,EAAE;IAC5C,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,GAAG,cAAc,CAAC,SAAS,QAAQ,EAAE,EAAE,cAAc,CAAC,CAStD;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC7B,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GACpE,cAAc,CAAC,SAAS,eAAe,EAAE,EAAE,cAAc,CAAC,CAS5D;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE;IAC9C,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CACnC,GAAG,cAAc,CAAC,WAAW,EAAE,cAAc,CAAC,CAW9C"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { buildCategoryTree } from "../catalog/tree.js";
|
|
4
|
+
import { useCategoriesApi } from "./context.js";
|
|
5
|
+
import { createCatalogStore } from "./catalogStore.js";
|
|
6
|
+
import { syncCatalog } from "./catalogSync.js";
|
|
7
|
+
import { catalogKeyOptions, categoriesQueryKeys } from "./queryKeys.js";
|
|
8
|
+
const DEFAULT_CATALOG_STALE_TIME = 5 * 60 * 1000;
|
|
9
|
+
/**
|
|
10
|
+
* The whole category tree, delta-synced and cached across page loads.
|
|
11
|
+
*
|
|
12
|
+
* This is the hook a storefront mounts once, high up: `/c/:slug` resolves its
|
|
13
|
+
* slug against it, breadcrumbs walk it, the picker in the compose form reads
|
|
14
|
+
* it, and none of them costs a request. Without it every page pulls the
|
|
15
|
+
* catalogue (spec §4.3).
|
|
16
|
+
*
|
|
17
|
+
* ── What one "load" actually does ──────────────────────────────────────────
|
|
18
|
+
*
|
|
19
|
+
* read the stored snapshot (app-scoped repository; may be empty)
|
|
20
|
+
* → GET /categories/?include_deleted=false (cold)
|
|
21
|
+
* or GET /categories/?min_revision=<cursor> (warm)
|
|
22
|
+
* → follow `has_next` with the window pinned by `max_revision`
|
|
23
|
+
* → fold rows + `revisions.deleted_ids` into the snapshot
|
|
24
|
+
* → persist, then build the tree
|
|
25
|
+
*
|
|
26
|
+
* The load discipline is the fleet's: `loading` while the first answer is in
|
|
27
|
+
* flight (including while the repository read is), `failed` on a refusal — a
|
|
28
|
+
* failed sync never degrades to "the catalogue is empty". Because the stored
|
|
29
|
+
* snapshot is read INSIDE the query function, a warm start still reports
|
|
30
|
+
* `loading` on the very first render and then resolves in the same tick; it
|
|
31
|
+
* does not flash an empty tree.
|
|
32
|
+
*
|
|
33
|
+
* `retry: false`: a sync failure is worth reporting immediately, and the
|
|
34
|
+
* caller has `refetch`. Three silent retries only delay the sentence.
|
|
35
|
+
*/
|
|
36
|
+
export function useCategoryCatalog(options = {}) {
|
|
37
|
+
const api = useCategoriesApi();
|
|
38
|
+
const treeOptions = useMemo(() => ({
|
|
39
|
+
...(options.includeDeleted !== undefined
|
|
40
|
+
? { includeDeleted: options.includeDeleted }
|
|
41
|
+
: {}),
|
|
42
|
+
...(options.includeInactive !== undefined
|
|
43
|
+
? { includeInactive: options.includeInactive }
|
|
44
|
+
: {}),
|
|
45
|
+
}), [options.includeDeleted, options.includeInactive]);
|
|
46
|
+
const keyOptions = useMemo(() => catalogKeyOptions(treeOptions), [treeOptions]);
|
|
47
|
+
const providedStore = options.store;
|
|
48
|
+
const store = useMemo(() => providedStore ?? createCatalogStore(), [providedStore]);
|
|
49
|
+
const pageSize = options.pageSize;
|
|
50
|
+
return useQuery({
|
|
51
|
+
queryKey: categoriesQueryKeys.catalog(keyOptions),
|
|
52
|
+
queryFn: async ({ signal }) => {
|
|
53
|
+
const stored = await store.load();
|
|
54
|
+
const result = await syncCatalog(api, stored, {
|
|
55
|
+
signal,
|
|
56
|
+
...(pageSize !== undefined ? { pageSize } : {}),
|
|
57
|
+
});
|
|
58
|
+
// A truncated walk keeps the previous cursor (`syncCatalog`), so
|
|
59
|
+
// persisting it cannot record progress that was not made.
|
|
60
|
+
await store.save(result.snapshot);
|
|
61
|
+
return {
|
|
62
|
+
index: buildCategoryTree(result.snapshot.rows, treeOptions),
|
|
63
|
+
snapshot: result.snapshot,
|
|
64
|
+
truncated: result.truncated,
|
|
65
|
+
wasFullSync: result.wasFullSync,
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
enabled: options.enabled ?? true,
|
|
69
|
+
staleTime: options.staleTime ?? DEFAULT_CATALOG_STALE_TIME,
|
|
70
|
+
retry: false,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* One category's direct children, straight from the server.
|
|
75
|
+
*
|
|
76
|
+
* The synced tree already knows them; this is for the host that does not mount
|
|
77
|
+
* the catalogue — an SSR category page, a lazily expanded branch. The server
|
|
78
|
+
* filters `deleted` here and orders by `tn_priority` descending, but it does
|
|
79
|
+
* NOT filter `active`, so a public screen still has to.
|
|
80
|
+
*/
|
|
81
|
+
export function useCategoryChildren(id, options) {
|
|
82
|
+
const api = useCategoriesApi();
|
|
83
|
+
return useQuery({
|
|
84
|
+
queryKey: categoriesQueryKeys.children(id ?? -1),
|
|
85
|
+
queryFn: ({ signal }) => api.children(id, { signal }),
|
|
86
|
+
enabled: (options?.enabled ?? true) && typeof id === "number",
|
|
87
|
+
retry: false,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The carousel strip for a landing page.
|
|
92
|
+
*
|
|
93
|
+
* The one endpoint the server already filters completely (`active` AND
|
|
94
|
+
* `carousel_enabled`) and already caches, sending
|
|
95
|
+
* `Cache-Control: public, max-age`. `staleTime` mirrors that: the browser's
|
|
96
|
+
* HTTP cache and the query cache should not disagree about how fresh this is.
|
|
97
|
+
*/
|
|
98
|
+
export function useCategoryCarousel(options) {
|
|
99
|
+
const api = useCategoriesApi();
|
|
100
|
+
return useQuery({
|
|
101
|
+
queryKey: categoriesQueryKeys.carousel,
|
|
102
|
+
queryFn: ({ signal }) => api.carousel({ signal }),
|
|
103
|
+
enabled: options?.enabled ?? true,
|
|
104
|
+
staleTime: options?.staleTime ?? DEFAULT_CATALOG_STALE_TIME,
|
|
105
|
+
retry: false,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* A category's resolved feature schema — own plus inherited, deduplicated by
|
|
110
|
+
* slug, in the category's own order.
|
|
111
|
+
*
|
|
112
|
+
* This is the payload `@stapel/attributes-react` draws (the compose form) and
|
|
113
|
+
* labels facets from (the search pair's `categoryFeatures` slot). It changes
|
|
114
|
+
* only when the catalogue is edited, hence the same generous `staleTime`.
|
|
115
|
+
*/
|
|
116
|
+
export function useCategoryFeatures(id, options) {
|
|
117
|
+
const api = useCategoriesApi();
|
|
118
|
+
return useQuery({
|
|
119
|
+
queryKey: categoriesQueryKeys.features(id ?? -1),
|
|
120
|
+
queryFn: ({ signal }) => api.features(id, { signal }),
|
|
121
|
+
enabled: (options?.enabled ?? true) && typeof id === "number",
|
|
122
|
+
staleTime: options?.staleTime ?? DEFAULT_CATALOG_STALE_TIME,
|
|
123
|
+
retry: false,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The catalogue's current maximum revision — the cheap freshness probe.
|
|
128
|
+
*
|
|
129
|
+
* A host that wants to poll for catalogue changes without transferring rows
|
|
130
|
+
* compares this to `catalog.snapshot.cursor` and invalidates the catalogue key
|
|
131
|
+
* when it moves. Not wired into `useCategoryCatalog` on purpose: how often to
|
|
132
|
+
* ask is a deployment's decision, and a library that picked one would be
|
|
133
|
+
* polling somebody's API on their behalf.
|
|
134
|
+
*/
|
|
135
|
+
export function useCategoriesRevision(options) {
|
|
136
|
+
const api = useCategoriesApi();
|
|
137
|
+
return useQuery({
|
|
138
|
+
queryKey: categoriesQueryKeys.revision,
|
|
139
|
+
queryFn: ({ signal }) => api.revision({ signal }),
|
|
140
|
+
enabled: options?.enabled ?? true,
|
|
141
|
+
...(options?.refetchInterval !== undefined
|
|
142
|
+
? { refetchInterval: options.refetchInterval }
|
|
143
|
+
: {}),
|
|
144
|
+
retry: false,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=queries.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queries.js","sourceRoot":"","sources":["../../src/model/queries.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Namespaced TanStack Query keys (frontend-standard §2 — namespaced keys).
|
|
3
|
+
* Everything under the `"categories"` root so a host can invalidate the whole
|
|
4
|
+
* module or match a single read. Explicit tuple return types satisfy
|
|
5
|
+
* `--isolatedDeclarations`.
|
|
6
|
+
*
|
|
7
|
+
* THE CATALOGUE IS ONE KEY, NOT A PAGE PER REQUEST. `catalog()` is keyed on
|
|
8
|
+
* nothing but the tree options, because the pages of a sync walk are an
|
|
9
|
+
* implementation detail of producing ONE value — the snapshot. Keying pages
|
|
10
|
+
* individually would cache the intermediate states of a protocol whose whole
|
|
11
|
+
* purpose is that the intermediate states are never shown, and would leave a
|
|
12
|
+
* half-walked catalogue in the cache for the next mount to find.
|
|
13
|
+
*/
|
|
14
|
+
import type { BuildCategoryTreeOptions } from "../catalog/tree.js";
|
|
15
|
+
/** The tree-shaping options a catalogue read is keyed on. Normalized to
|
|
16
|
+
* booleans so `undefined` and `false` cannot cache twice. */
|
|
17
|
+
export interface CatalogKeyOptions {
|
|
18
|
+
readonly includeDeleted: boolean;
|
|
19
|
+
readonly includeInactive: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare function catalogKeyOptions(options?: BuildCategoryTreeOptions): CatalogKeyOptions;
|
|
22
|
+
export declare const categoriesQueryKeys: {
|
|
23
|
+
readonly all: readonly ["categories"];
|
|
24
|
+
catalog(options: CatalogKeyOptions): readonly ["categories", "catalog", CatalogKeyOptions];
|
|
25
|
+
children(id: number): readonly ["categories", "children", number];
|
|
26
|
+
readonly carousel: readonly ["categories", "carousel"];
|
|
27
|
+
features(id: number): readonly ["categories", "features", number];
|
|
28
|
+
readonly revision: readonly ["categories", "revision"];
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=queryKeys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queryKeys.d.ts","sourceRoot":"","sources":["../../src/model/queryKeys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAInE;6DAC6D;AAC7D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;CACnC;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,GAAE,wBAA6B,GACrC,iBAAiB,CAKnB;AAED,eAAO,MAAM,mBAAmB,EAAE;IAChC,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAC,YAAY,CAAC,CAAC;IACtC,OAAO,CACL,OAAO,EAAE,iBAAiB,GACzB,SAAS,CAAC,YAAY,EAAE,SAAS,EAAE,iBAAiB,CAAC,CAAC;IACzD,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,CAAC,YAAY,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;IAClE,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;IACvD,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,CAAC,YAAY,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;IAClE,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;CAQxD,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const ROOT = "categories";
|
|
2
|
+
export function catalogKeyOptions(options = {}) {
|
|
3
|
+
return {
|
|
4
|
+
includeDeleted: options.includeDeleted === true,
|
|
5
|
+
includeInactive: options.includeInactive === true,
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export const categoriesQueryKeys = {
|
|
9
|
+
all: [ROOT],
|
|
10
|
+
catalog: (options) => [ROOT, "catalog", options],
|
|
11
|
+
children: (id) => [ROOT, "children", id],
|
|
12
|
+
carousel: [ROOT, "carousel"],
|
|
13
|
+
features: (id) => [ROOT, "features", id],
|
|
14
|
+
revision: [ROOT, "revision"],
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=queryKeys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queryKeys.js","sourceRoot":"","sources":["../../src/model/queryKeys.ts"],"names":[],"mappings":"AAeA,MAAM,IAAI,GAAG,YAAqB,CAAC;AASnC,MAAM,UAAU,iBAAiB,CAC/B,UAAoC,EAAE;IAEtC,OAAO;QACL,cAAc,EAAE,OAAO,CAAC,cAAc,KAAK,IAAI;QAC/C,eAAe,EAAE,OAAO,CAAC,eAAe,KAAK,IAAI;KAClD,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,mBAAmB,GAS5B;IACF,GAAG,EAAE,CAAC,IAAI,CAAC;IACX,OAAO,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC;IAChD,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,CAAC;IACxC,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,CAAC;IAC5B,QAAQ,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,CAAC;IACxC,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,CAAC;CAC7B,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { CreateModuleRuntimeOptions, ModuleRuntime } from "@stapel/core";
|
|
2
|
+
import type { CategoriesApi } from "../api/categoriesApi.js";
|
|
3
|
+
/**
|
|
4
|
+
* The wired categories runtime — core's `ModuleRuntime` bound to this pair's
|
|
5
|
+
* API (slim wave §21/S2). The returned `client` is what the host injects into
|
|
6
|
+
* core's `StapelConfigProvider` (as the default or the `"categories"` module
|
|
7
|
+
* client), preserving the client-injection fork seam (frontend-standard §7.2).
|
|
8
|
+
*
|
|
9
|
+
* ANONYMOUS BY DESIGN. Every endpoint this pair calls is a SAFE method under
|
|
10
|
+
* `ReadOnlyOrStaff`, i.e. open to a caller with no session at all:
|
|
11
|
+
*
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const runtime = createCategoriesRuntime({ baseUrl: "/categories/api/v1/" });
|
|
14
|
+
* <CategoriesProvider runtime={runtime}>…</CategoriesProvider>
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* No session, no workspace id, no auth client — which is what lets a
|
|
18
|
+
* catalogue render for a visitor who will never sign in, and why the read
|
|
19
|
+
* hooks here are deliberately not gated on `useActiveSessionReady`.
|
|
20
|
+
*
|
|
21
|
+
* The catalogue snapshot is stored in an **app-scoped** repository
|
|
22
|
+
* (`createRepository(..., { scope: "app" })`, `model/catalogStore.ts`): a
|
|
23
|
+
* category tree is a deployment's public content, identical for every visitor,
|
|
24
|
+
* so it is neither encrypted with the per-session key nor wiped at logout.
|
|
25
|
+
* Wiping it would mean re-downloading the whole catalogue every time somebody
|
|
26
|
+
* signs out.
|
|
27
|
+
*/
|
|
28
|
+
export type CategoriesRuntime = ModuleRuntime<CategoriesApi>;
|
|
29
|
+
export type CreateCategoriesRuntimeOptions = CreateModuleRuntimeOptions;
|
|
30
|
+
export declare function createCategoriesRuntime(options: CreateCategoriesRuntimeOptions): CategoriesRuntime;
|
|
31
|
+
//# sourceMappingURL=runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/model/runtime.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,0BAA0B,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE9E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE7D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,MAAM,iBAAiB,GAAG,aAAa,CAAC,aAAa,CAAC,CAAC;AAE7D,MAAM,MAAM,8BAA8B,GAAG,0BAA0B,CAAC;AAExE,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,8BAA8B,GACtC,iBAAiB,CAEnB"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createModuleRuntime } from "@stapel/core";
|
|
2
|
+
import { createCategoriesApi } from "../api/categoriesApi.js";
|
|
3
|
+
export function createCategoriesRuntime(options) {
|
|
4
|
+
return createModuleRuntime((client) => createCategoriesApi(client), options);
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../src/model/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAgC9D,MAAM,UAAU,uBAAuB,CACrC,OAAuC;IAEvC,OAAO,mBAAmB,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,mBAAmB,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC;AAC/E,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This pair's contribution to the scripted-fullstack nav contract
|
|
3
|
+
* (`@stapel/core`'s `NavEntry`/`PackageNavManifest`). `scripts/gen-nav-
|
|
4
|
+
* manifest.mjs` reads `navEntries` below, stamps `package`/`version` from THIS
|
|
5
|
+
* package's own `package.json`, and emits
|
|
6
|
+
* `packages/categories-react/nav-manifest.json` plus this package's slice of
|
|
7
|
+
* the root aggregate.
|
|
8
|
+
*
|
|
9
|
+
* ── Why `/` is NOT claimed here ────────────────────────────────────────────
|
|
10
|
+
*
|
|
11
|
+
* The spec's route table (§5.1) gives `/` to "categories + search": a carousel
|
|
12
|
+
* of categories above a feed of the newest listings. That screen is COMPOSED
|
|
13
|
+
* of two pairs, and a composed route belongs to the container that composes it
|
|
14
|
+
* (§6.2 — wiring adds, generation emits). A pair claiming `/` would be one
|
|
15
|
+
* package deciding the landing page of every host that installs it, and the
|
|
16
|
+
* second pair would have nowhere to put its half.
|
|
17
|
+
*
|
|
18
|
+
* So the catalogue's own root is `/c` — the whole tree as a browsable page,
|
|
19
|
+
* which is a real single-pair screen — and `/c/:slug` is the category page.
|
|
20
|
+
* The container's landing mounts `<CategoryCarousel>` (or the skin's
|
|
21
|
+
* `CategoryCarouselStrip`) itself, which is one line and no manifest entry.
|
|
22
|
+
*
|
|
23
|
+
* Both entries declare `surface: "public"` EXPLICITLY. The derivation
|
|
24
|
+
* `requiresAuth ? "member" : "public"` lands on the same answer today; the
|
|
25
|
+
* explicit declaration is what a public container can rely on if an entry ever
|
|
26
|
+
* gains an auth requirement for an unrelated reason (`core/src/nav.ts`,
|
|
27
|
+
* `navEntrySurface`; the trap is §1.3 — `audience: undefined` does not filter).
|
|
28
|
+
*/
|
|
29
|
+
import type { NavEntry } from "@stapel/core";
|
|
30
|
+
export declare const navEntries: readonly NavEntry[];
|
|
31
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/nav/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7C,eAAO,MAAM,UAAU,EAAE,SAAS,QAAQ,EAmCzC,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const navEntries = [
|
|
2
|
+
{
|
|
3
|
+
// The catalogue root: the tree, browsable, with the carousel above it.
|
|
4
|
+
// This one IS a menu item — "Catalogue" is the browse bar's anchor, which
|
|
5
|
+
// is why it is the single entry in this pair with
|
|
6
|
+
// `menuVisibleDefault: true`.
|
|
7
|
+
id: "categories.catalog",
|
|
8
|
+
labelKey: "categories.catalog.title",
|
|
9
|
+
icon: "AppstoreOutlined",
|
|
10
|
+
route: { path: "/c" },
|
|
11
|
+
component: { export: "CatalogPage", subpath: "default" },
|
|
12
|
+
placement: { level: "top" },
|
|
13
|
+
menuVisibleDefault: true,
|
|
14
|
+
requiresAuth: false,
|
|
15
|
+
surface: "public",
|
|
16
|
+
order: 5,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
// `/c/:slug`, not `/c/:id`. `Category.slug` is `unique=True` on the model,
|
|
20
|
+
// but the SERVER cannot look a category up by it — `lookup_field` is never
|
|
21
|
+
// overridden and the list endpoint has no slug filter — so the resolution
|
|
22
|
+
// happens client-side against the synced tree (spec §4.3). A
|
|
23
|
+
// parameterized route is a navigation TARGET, not a menu item: the same
|
|
24
|
+
// treatment `auth.login` and `search.results` get.
|
|
25
|
+
id: "categories.category",
|
|
26
|
+
labelKey: "categories.category.title",
|
|
27
|
+
icon: "FolderOpenOutlined",
|
|
28
|
+
route: { path: "/c/:slug" },
|
|
29
|
+
component: { export: "CategoryPage", subpath: "default" },
|
|
30
|
+
placement: { level: "top" },
|
|
31
|
+
menuVisibleDefault: false,
|
|
32
|
+
requiresAuth: false,
|
|
33
|
+
surface: "public",
|
|
34
|
+
order: 6,
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/nav/manifest.ts"],"names":[],"mappings":"AA8BA,MAAM,CAAC,MAAM,UAAU,GAAwB;IAC7C;QACE,uEAAuE;QACvE,0EAA0E;QAC1E,kDAAkD;QAClD,8BAA8B;QAC9B,EAAE,EAAE,oBAAoB;QACxB,QAAQ,EAAE,0BAA0B;QACpC,IAAI,EAAE,kBAAkB;QACxB,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE;QACrB,SAAS,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE;QACxD,SAAS,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE;QAC3B,kBAAkB,EAAE,IAAI;QACxB,YAAY,EAAE,KAAK;QACnB,OAAO,EAAE,QAAQ;QACjB,KAAK,EAAE,CAAC;KACT;IACD;QACE,2EAA2E;QAC3E,2EAA2E;QAC3E,0EAA0E;QAC1E,6DAA6D;QAC7D,wEAAwE;QACxE,mDAAmD;QACnD,EAAE,EAAE,qBAAqB;QACzB,QAAQ,EAAE,2BAA2B;QACrC,IAAI,EAAE,oBAAoB;QAC1B,KAAK,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE;QAC3B,SAAS,EAAE,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,SAAS,EAAE;QACzD,SAAS,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE;QAC3B,kBAAkB,EAAE,KAAK;QACzB,YAAY,EAAE,KAAK;QACnB,OAAO,EAAE,QAAQ;QACjB,KAAK,EAAE,CAAC;KACT;CACF,CAAC"}
|
package/llms.txt
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# @stapel/categories-react 0.1.0
|
|
2
|
+
|
|
3
|
+
Headless React flow pair for stapel-categories (contract >=0.6 <0.7) — business + state, zero visual opinion.
|
|
4
|
+
Built on @stapel/core: typed client + StapelApiError envelope, auth token refresh,
|
|
5
|
+
verification-403 interception, i18n engine, analytics facade, TanStack Query layer.
|
|
6
|
+
|
|
7
|
+
## The one right way (do this, the rest is a review/lint smell)
|
|
8
|
+
- No raw fetch/axios. The client is injected via <CategoriesProvider>/StapelConfigProvider;
|
|
9
|
+
every hook and flow already carries auth, refresh, and the error envelope.
|
|
10
|
+
- Render errors, never try/catch them: a flow's state carries FlowError{code,params};
|
|
11
|
+
render `t(code, params)` and branch on `explainCategoriesError(code)` remediation.
|
|
12
|
+
- Server state = the use* hooks (query layer); keys come only from categoriesQueryKeys.
|
|
13
|
+
- User strings = i18n keys (registerCategoriesI18n); never string literals.
|
|
14
|
+
- Sign-in UI = a headless flow component; copy it (shadcn-style) to restyle.
|
|
15
|
+
|
|
16
|
+
## Layers
|
|
17
|
+
api (typed client) · model (hooks, session) · flows (machines) · headless · i18n
|
|
18
|
+
|
|
19
|
+
## Documented flows (flows.json — canonical id, steps, endpoints)
|
|
20
|
+
|
|
21
|
+
## Operations (typed; use the named op, never a path string)
|
|
22
|
+
Request/response schema names are in manifest.json + the generated types.
|
|
23
|
+
Paths are relative to `/categories/api/v1/`.
|
|
24
|
+
- categories_api_v1_categories_bulk_add_create: POST /categories/bulk_add/
|
|
25
|
+
- categories_api_v1_categories_bulk_commands_create: POST /categories/bulk-commands/
|
|
26
|
+
- categories_api_v1_categories_carousel_list: GET /categories/carousel/
|
|
27
|
+
- categories_api_v1_categories_children_list: GET /categories/{id}/children/
|
|
28
|
+
- categories_api_v1_categories_create: POST /categories/
|
|
29
|
+
- categories_api_v1_categories_data.json_retrieve: GET /categories/data.json/
|
|
30
|
+
- categories_api_v1_categories_deleted_children_list: GET /categories/{id}/deleted-children/
|
|
31
|
+
- categories_api_v1_categories_destroy: DELETE /categories/{id}/
|
|
32
|
+
- categories_api_v1_categories_feature_editor_apply_create: POST /categories/{id}/feature-editor/apply/
|
|
33
|
+
- categories_api_v1_categories_feature_editor_draft_create: POST /categories/{id}/feature-editor/draft/
|
|
34
|
+
- categories_api_v1_categories_feature_editor_retrieve: GET /categories/{id}/feature-editor/
|
|
35
|
+
- categories_api_v1_categories_features_list: GET /categories/{id}/features/
|
|
36
|
+
- categories_api_v1_categories_list: GET /categories/
|
|
37
|
+
- categories_api_v1_categories_partial_update: PATCH /categories/{id}/
|
|
38
|
+
- categories_api_v1_categories_retrieve: GET /categories/{id}/
|
|
39
|
+
- categories_api_v1_categories_revision_retrieve: GET /categories/revision/
|
|
40
|
+
- categories_api_v1_categories_undelete_create: POST /categories/{id}/undelete/
|
|
41
|
+
- categories_api_v1_categories_update: PUT /categories/{id}/
|
|
42
|
+
- categories_api_v1_categories_validate_configs_retrieve: GET /categories/{id}/validate-configs/
|
|
43
|
+
- categories_api_v1_categories_validate_dto_create: POST /categories/{id}/validate-dto/
|
|
44
|
+
- categories_api_v1_features_bulk_add_create: POST /features/bulk_add/
|
|
45
|
+
- categories_api_v1_features_convert_type_create: POST /features/{id}/convert-type/
|
|
46
|
+
- categories_api_v1_features_create: POST /features/
|
|
47
|
+
- categories_api_v1_features_data.json_retrieve: GET /features/data.json/
|
|
48
|
+
- categories_api_v1_features_destroy: DELETE /features/{id}/
|
|
49
|
+
- categories_api_v1_features_list: GET /features/
|
|
50
|
+
- categories_api_v1_features_partial_update: PATCH /features/{id}/
|
|
51
|
+
- categories_api_v1_features_retrieve: GET /features/{id}/
|
|
52
|
+
- categories_api_v1_features_revision_retrieve: GET /features/revision/
|
|
53
|
+
- categories_api_v1_features_update: PUT /features/{id}/
|
|
54
|
+
- collect_translation_keys: GET /categories/translation-keys/
|
|
55
|
+
|
|
56
|
+
## Query hooks (server state; keys come only from the key factory)
|
|
57
|
+
- useCategoriesRevision (query) → revision
|
|
58
|
+
- useCategoryCarousel (query) → carousel
|
|
59
|
+
- useCategoryCatalog (query) → —
|
|
60
|
+
- useCategoryChildren (query) → children
|
|
61
|
+
- useCategoryFeatures (query) → features
|
|
62
|
+
|
|
63
|
+
## Errors (render t(code, params); UX from remediation)
|
|
64
|
+
62 keys (full catalog: manifest.json §errors). By remediation: fix_input 36 · retry 13 · verify 5 · wait_and_retry 5 · contact_support 2 · reauthenticate 1.
|
|
65
|
+
Param-bearing keys (interpolation slots matter):
|
|
66
|
+
- error.400.categories_duplicate_slug [400] → fix_input {slug}
|
|
67
|
+
- error.400.categories_feature_editor_invalid [400] → fix_input {reason}
|
|
68
|
+
- error.400.description_too_long [400] → fix_input {max_length}
|
|
69
|
+
- error.400.description_too_short [400] → fix_input {min_length}
|
|
70
|
+
- error.400.feature_above_maximum [400] → fix_input {feature}
|
|
71
|
+
- error.400.feature_below_minimum [400] → fix_input {feature}
|
|
72
|
+
- error.400.feature_invalid_config [400] → fix_input {feature}
|
|
73
|
+
- error.400.feature_invalid_format [400] → fix_input {feature}
|
|
74
|
+
- error.400.feature_invalid_type [400] → fix_input {feature}
|
|
75
|
+
- error.400.feature_mandatory_missing [400] → fix_input {feature}
|
|
76
|
+
- error.400.feature_not_allowed [400] → fix_input {feature}
|
|
77
|
+
- error.400.feature_not_in_options [400] → fix_input {feature}
|
|
78
|
+
- error.400.feature_unknown [400] → fix_input {feature}
|
|
79
|
+
- error.400.feature_unknown_type [400] → fix_input {feature}
|
|
80
|
+
- error.400.field.blank [400] → fix_input {field}
|
|
81
|
+
- error.400.field.does_not_exist [400] → fix_input {field}
|
|
82
|
+
- error.400.field.invalid [400] → fix_input {field}
|
|
83
|
+
- error.400.field.invalid_choice [400] → fix_input {field}
|
|
84
|
+
- error.400.field.max_length [400] → fix_input {field,max_length}
|
|
85
|
+
- error.400.field.max_value [400] → fix_input {field,max_value}
|
|
86
|
+
- error.400.field.min_length [400] → fix_input {field,min_length}
|
|
87
|
+
- error.400.field.min_value [400] → fix_input {field,min_value}
|
|
88
|
+
- error.400.field.null [400] → fix_input {field}
|
|
89
|
+
- error.400.field.required [400] → fix_input {field}
|
|
90
|
+
- error.400.field.unique [400] → fix_input {field}
|
|
91
|
+
- error.409.categories_feature_editor_conflict [409] → fix_input {expected,actual}
|
|
92
|
+
- error.429.rate_limit [429] → wait_and_retry {retry_after_minutes}
|
|
93
|
+
|
|
94
|
+
## Analytics events (typed; defineEvent → events.json, drift-gated)
|
|
95
|
+
- (no app defineEvent() in this pair — its analytic events are the
|
|
96
|
+
auto-instrumented flow funnels below)
|
|
97
|
+
|
|
98
|
+
## Flow funnels (auto-instrumented: flow.<id>.<step> {phase})
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
// Typed event + tracked() click (the one right way; §3.1).
|
|
102
|
+
const planSelected = defineEvent({
|
|
103
|
+
name: "pricing.plan.selected",
|
|
104
|
+
description: "User picked a plan",
|
|
105
|
+
props: { plan: prop.oneOf(["free", "pro", "team"], "Plan code") },
|
|
106
|
+
});
|
|
107
|
+
const { tracked } = useTracked();
|
|
108
|
+
<Button onClick={tracked(planSelected, { plan }, startCheckout)} />
|
|
109
|
+
// A click that STEPS a flow machine is already instrumented — mark it
|
|
110
|
+
// data-analytics="flow" instead; tracked() on top double-counts (§3.2).
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Demos (defineDemo → manifest.demos; compiled, linted, rendered examples)
|
|
114
|
+
- categories.carousel → <CategoryCarousel> [default] demo/CategoryCarousel.demo.tsx
|
|
115
|
+
- categories.features → <CategoryFeatures> [default] demo/CategoryFeatures.demo.tsx
|
|
116
|
+
- categories.picker → <CategoryPicker> [default] demo/CategoryPicker.demo.tsx
|
|
117
|
+
- categories.tree → <CategoryTree> [roots|one category] demo/CategoryTree.demo.tsx
|
|
118
|
+
Each source file is the canonical usage snippet (open the default variant).
|
|
119
|
+
|
|
120
|
+
## Snippets
|
|
121
|
+
```tsx
|
|
122
|
+
// Error rendering + remediation branch (one pattern for every pair).
|
|
123
|
+
const r = explainCategoriesError(err.code); // 'wait_and_retry' | 'verify' | ...
|
|
124
|
+
return <Alert action={r}>{t(err.code, err.params)}</Alert>;
|
|
125
|
+
```
|