@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.
Files changed (205) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/MODULE.md +194 -0
  3. package/README.md +191 -0
  4. package/dist/api/categoriesApi.d.ts +121 -0
  5. package/dist/api/categoriesApi.d.ts.map +1 -0
  6. package/dist/api/categoriesApi.js +48 -0
  7. package/dist/api/categoriesApi.js.map +1 -0
  8. package/dist/api/generated/schema.d.ts +2321 -0
  9. package/dist/api/generated/schema.d.ts.map +1 -0
  10. package/dist/api/generated/schema.js +2 -0
  11. package/dist/api/generated/schema.js.map +1 -0
  12. package/dist/api/types.d.ts +92 -0
  13. package/dist/api/types.d.ts.map +1 -0
  14. package/dist/api/types.js +2 -0
  15. package/dist/api/types.js.map +1 -0
  16. package/dist/catalog/labels.d.ts +103 -0
  17. package/dist/catalog/labels.d.ts.map +1 -0
  18. package/dist/catalog/labels.js +56 -0
  19. package/dist/catalog/labels.js.map +1 -0
  20. package/dist/catalog/pks.d.ts +25 -0
  21. package/dist/catalog/pks.d.ts.map +1 -0
  22. package/dist/catalog/pks.js +38 -0
  23. package/dist/catalog/pks.js.map +1 -0
  24. package/dist/catalog/sync.d.ts +103 -0
  25. package/dist/catalog/sync.d.ts.map +1 -0
  26. package/dist/catalog/sync.js +114 -0
  27. package/dist/catalog/sync.js.map +1 -0
  28. package/dist/catalog/tree.d.ts +119 -0
  29. package/dist/catalog/tree.d.ts.map +1 -0
  30. package/dist/catalog/tree.js +166 -0
  31. package/dist/catalog/tree.js.map +1 -0
  32. package/dist/default/CatalogPage.d.ts +16 -0
  33. package/dist/default/CatalogPage.d.ts.map +1 -0
  34. package/dist/default/CatalogPage.js +13 -0
  35. package/dist/default/CatalogPage.js.map +1 -0
  36. package/dist/default/CategoryBreadcrumbsBar.d.ts +20 -0
  37. package/dist/default/CategoryBreadcrumbsBar.d.ts.map +1 -0
  38. package/dist/default/CategoryBreadcrumbsBar.js +35 -0
  39. package/dist/default/CategoryBreadcrumbsBar.js.map +1 -0
  40. package/dist/default/CategoryCarouselStrip.d.ts +22 -0
  41. package/dist/default/CategoryCarouselStrip.d.ts.map +1 -0
  42. package/dist/default/CategoryCarouselStrip.js +24 -0
  43. package/dist/default/CategoryCarouselStrip.js.map +1 -0
  44. package/dist/default/CategoryFeatureList.d.ts +23 -0
  45. package/dist/default/CategoryFeatureList.d.ts.map +1 -0
  46. package/dist/default/CategoryFeatureList.js +28 -0
  47. package/dist/default/CategoryFeatureList.js.map +1 -0
  48. package/dist/default/CategoryPage.d.ts +43 -0
  49. package/dist/default/CategoryPage.d.ts.map +1 -0
  50. package/dist/default/CategoryPage.js +21 -0
  51. package/dist/default/CategoryPage.js.map +1 -0
  52. package/dist/default/CategoryPickerField.d.ts +26 -0
  53. package/dist/default/CategoryPickerField.d.ts.map +1 -0
  54. package/dist/default/CategoryPickerField.js +40 -0
  55. package/dist/default/CategoryPickerField.js.map +1 -0
  56. package/dist/default/CategoryTreePane.d.ts +28 -0
  57. package/dist/default/CategoryTreePane.d.ts.map +1 -0
  58. package/dist/default/CategoryTreePane.js +25 -0
  59. package/dist/default/CategoryTreePane.js.map +1 -0
  60. package/dist/default/ErrorAlert.d.ts +9 -0
  61. package/dist/default/ErrorAlert.d.ts.map +1 -0
  62. package/dist/default/ErrorAlert.js +27 -0
  63. package/dist/default/ErrorAlert.js.map +1 -0
  64. package/dist/default/index.d.ts +37 -0
  65. package/dist/default/index.d.ts.map +1 -0
  66. package/dist/default/index.js +28 -0
  67. package/dist/default/index.js.map +1 -0
  68. package/dist/default/theme.d.ts +25 -0
  69. package/dist/default/theme.d.ts.map +1 -0
  70. package/dist/default/theme.js +40 -0
  71. package/dist/default/theme.js.map +1 -0
  72. package/dist/default/types.d.ts +12 -0
  73. package/dist/default/types.d.ts.map +1 -0
  74. package/dist/default/types.js +2 -0
  75. package/dist/default/types.js.map +1 -0
  76. package/dist/headless/CategoriesProvider.d.ts +19 -0
  77. package/dist/headless/CategoriesProvider.d.ts.map +1 -0
  78. package/dist/headless/CategoriesProvider.js +15 -0
  79. package/dist/headless/CategoriesProvider.js.map +1 -0
  80. package/dist/headless/CategoryBreadcrumbs.d.ts +40 -0
  81. package/dist/headless/CategoryBreadcrumbs.d.ts.map +1 -0
  82. package/dist/headless/CategoryBreadcrumbs.js +42 -0
  83. package/dist/headless/CategoryBreadcrumbs.js.map +1 -0
  84. package/dist/headless/CategoryCarousel.d.ts +48 -0
  85. package/dist/headless/CategoryCarousel.d.ts.map +1 -0
  86. package/dist/headless/CategoryCarousel.js +34 -0
  87. package/dist/headless/CategoryCarousel.js.map +1 -0
  88. package/dist/headless/CategoryFeatures.d.ts +67 -0
  89. package/dist/headless/CategoryFeatures.d.ts.map +1 -0
  90. package/dist/headless/CategoryFeatures.js +46 -0
  91. package/dist/headless/CategoryFeatures.js.map +1 -0
  92. package/dist/headless/CategoryPicker.d.ts +69 -0
  93. package/dist/headless/CategoryPicker.d.ts.map +1 -0
  94. package/dist/headless/CategoryPicker.js +86 -0
  95. package/dist/headless/CategoryPicker.js.map +1 -0
  96. package/dist/headless/CategoryTree.d.ts +55 -0
  97. package/dist/headless/CategoryTree.d.ts.map +1 -0
  98. package/dist/headless/CategoryTree.js +51 -0
  99. package/dist/headless/CategoryTree.js.map +1 -0
  100. package/dist/i18n/errorsMap.d.ts +22 -0
  101. package/dist/i18n/errorsMap.d.ts.map +1 -0
  102. package/dist/i18n/errorsMap.js +31 -0
  103. package/dist/i18n/errorsMap.js.map +1 -0
  104. package/dist/i18n/es.d.ts +25 -0
  105. package/dist/i18n/es.d.ts.map +1 -0
  106. package/dist/i18n/es.js +74 -0
  107. package/dist/i18n/es.js.map +1 -0
  108. package/dist/i18n/generated/errors.es.gen.d.ts +16 -0
  109. package/dist/i18n/generated/errors.es.gen.d.ts.map +1 -0
  110. package/dist/i18n/generated/errors.es.gen.js +58 -0
  111. package/dist/i18n/generated/errors.es.gen.js.map +1 -0
  112. package/dist/i18n/generated/errors.gen.d.ts +407 -0
  113. package/dist/i18n/generated/errors.gen.d.ts.map +1 -0
  114. package/dist/i18n/generated/errors.gen.js +207 -0
  115. package/dist/i18n/generated/errors.gen.js.map +1 -0
  116. package/dist/i18n/generated/errors.ru.gen.d.ts +16 -0
  117. package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -0
  118. package/dist/i18n/generated/errors.ru.gen.js +58 -0
  119. package/dist/i18n/generated/errors.ru.gen.js.map +1 -0
  120. package/dist/i18n/keys.d.ts +89 -0
  121. package/dist/i18n/keys.d.ts.map +1 -0
  122. package/dist/i18n/keys.js +134 -0
  123. package/dist/i18n/keys.js.map +1 -0
  124. package/dist/i18n/ru.d.ts +41 -0
  125. package/dist/i18n/ru.d.ts.map +1 -0
  126. package/dist/i18n/ru.js +90 -0
  127. package/dist/i18n/ru.js.map +1 -0
  128. package/dist/index.d.ts +88 -0
  129. package/dist/index.d.ts.map +1 -0
  130. package/dist/index.js +78 -0
  131. package/dist/index.js.map +1 -0
  132. package/dist/model/catalogStore.d.ts +23 -0
  133. package/dist/model/catalogStore.d.ts.map +1 -0
  134. package/dist/model/catalogStore.js +85 -0
  135. package/dist/model/catalogStore.js.map +1 -0
  136. package/dist/model/catalogSync.d.ts +49 -0
  137. package/dist/model/catalogSync.d.ts.map +1 -0
  138. package/dist/model/catalogSync.js +64 -0
  139. package/dist/model/catalogSync.js.map +1 -0
  140. package/dist/model/context.d.ts +11 -0
  141. package/dist/model/context.d.ts.map +1 -0
  142. package/dist/model/context.js +16 -0
  143. package/dist/model/context.js.map +1 -0
  144. package/dist/model/queries.d.ts +124 -0
  145. package/dist/model/queries.d.ts.map +1 -0
  146. package/dist/model/queries.js +147 -0
  147. package/dist/model/queries.js.map +1 -0
  148. package/dist/model/queryKeys.d.ts +30 -0
  149. package/dist/model/queryKeys.d.ts.map +1 -0
  150. package/dist/model/queryKeys.js +16 -0
  151. package/dist/model/queryKeys.js.map +1 -0
  152. package/dist/model/runtime.d.ts +31 -0
  153. package/dist/model/runtime.d.ts.map +1 -0
  154. package/dist/model/runtime.js +6 -0
  155. package/dist/model/runtime.js.map +1 -0
  156. package/dist/nav/manifest.d.ts +31 -0
  157. package/dist/nav/manifest.d.ts.map +1 -0
  158. package/dist/nav/manifest.js +37 -0
  159. package/dist/nav/manifest.js.map +1 -0
  160. package/llms.txt +125 -0
  161. package/manifest.json +918 -0
  162. package/nav-manifest.json +44 -0
  163. package/package.json +119 -0
  164. package/src/analytics/generated/events.json +7 -0
  165. package/src/api/categoriesApi.ts +179 -0
  166. package/src/api/generated/schema.ts +2323 -0
  167. package/src/api/types.ts +99 -0
  168. package/src/catalog/labels.ts +129 -0
  169. package/src/catalog/pks.ts +35 -0
  170. package/src/catalog/sync.ts +169 -0
  171. package/src/catalog/tree.ts +242 -0
  172. package/src/default/CatalogPage.tsx +40 -0
  173. package/src/default/CategoryBreadcrumbsBar.tsx +93 -0
  174. package/src/default/CategoryCarouselStrip.tsx +102 -0
  175. package/src/default/CategoryFeatureList.tsx +125 -0
  176. package/src/default/CategoryPage.tsx +114 -0
  177. package/src/default/CategoryPickerField.tsx +173 -0
  178. package/src/default/CategoryTreePane.tsx +134 -0
  179. package/src/default/ErrorAlert.tsx +46 -0
  180. package/src/default/index.ts +36 -0
  181. package/src/default/theme.tsx +67 -0
  182. package/src/default/types.ts +12 -0
  183. package/src/headless/CategoriesProvider.tsx +20 -0
  184. package/src/headless/CategoryBreadcrumbs.tsx +78 -0
  185. package/src/headless/CategoryCarousel.tsx +80 -0
  186. package/src/headless/CategoryFeatures.tsx +100 -0
  187. package/src/headless/CategoryPicker.tsx +173 -0
  188. package/src/headless/CategoryTree.tsx +115 -0
  189. package/src/i18n/errorsMap.ts +46 -0
  190. package/src/i18n/es.ts +92 -0
  191. package/src/i18n/generated/errors.es.gen.ts +64 -0
  192. package/src/i18n/generated/errors.gen.ts +238 -0
  193. package/src/i18n/generated/errors.json +495 -0
  194. package/src/i18n/generated/errors.ru.gen.ts +64 -0
  195. package/src/i18n/keys.ts +156 -0
  196. package/src/i18n/ru.ts +110 -0
  197. package/src/index.ts +191 -0
  198. package/src/model/catalogStore.ts +102 -0
  199. package/src/model/catalogSync.ts +115 -0
  200. package/src/model/context.tsx +28 -0
  201. package/src/model/queries.ts +226 -0
  202. package/src/model/queryKeys.ts +50 -0
  203. package/src/model/runtime.ts +39 -0
  204. package/src/nav/manifest.ts +66 -0
  205. package/tsconfig.json +26 -0
@@ -0,0 +1,44 @@
1
+ {
2
+ "package": "@stapel/categories-react",
3
+ "version": "0.1.0",
4
+ "entries": [
5
+ {
6
+ "id": "categories.catalog",
7
+ "labelKey": "categories.catalog.title",
8
+ "icon": "AppstoreOutlined",
9
+ "route": {
10
+ "path": "/c"
11
+ },
12
+ "component": {
13
+ "export": "CatalogPage",
14
+ "subpath": "default"
15
+ },
16
+ "placement": {
17
+ "level": "top"
18
+ },
19
+ "menuVisibleDefault": true,
20
+ "requiresAuth": false,
21
+ "surface": "public",
22
+ "order": 5
23
+ },
24
+ {
25
+ "id": "categories.category",
26
+ "labelKey": "categories.category.title",
27
+ "icon": "FolderOpenOutlined",
28
+ "route": {
29
+ "path": "/c/:slug"
30
+ },
31
+ "component": {
32
+ "export": "CategoryPage",
33
+ "subpath": "default"
34
+ },
35
+ "placement": {
36
+ "level": "top"
37
+ },
38
+ "menuVisibleDefault": false,
39
+ "requiresAuth": false,
40
+ "surface": "public",
41
+ "order": 6
42
+ }
43
+ ]
44
+ }
package/package.json ADDED
@@ -0,0 +1,119 @@
1
+ {
2
+ "name": "@stapel/categories-react",
3
+ "version": "0.1.0",
4
+ "description": "Headless React pair for stapel-categories: a typed client, a client-assembled category tree, and the revision-delta sync the module documents. The list endpoint returns FLAT rows with treenode ancestry as comma-joined pk strings — this package parses them, builds the tree, resolves a slug to a category (the server has no slug lookup), walks breadcrumbs, and keeps the whole catalogue in an app-scoped repository refreshed by `?min_revision=` deltas instead of refetching it on every page. Category and feature names are TRANSLATION KEYS on the wire, never resolved labels, and the pair says so in its types. A category's feature schema is served typed for @stapel/attributes-react to draw and for search facets to label. Zero visual opinion in the main entry; an opt-in /default subpath ships the antd skin.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/usestapel/stapel-react.git",
9
+ "directory": "packages/categories-react"
10
+ },
11
+ "type": "module",
12
+ "sideEffects": false,
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "default": "./dist/index.js"
19
+ },
20
+ "./default": {
21
+ "types": "./dist/default/index.d.ts",
22
+ "default": "./dist/default/index.js"
23
+ },
24
+ "./i18n/ru": {
25
+ "types": "./dist/i18n/ru.d.ts",
26
+ "default": "./dist/i18n/ru.js"
27
+ },
28
+ "./i18n/es": {
29
+ "types": "./dist/i18n/es.d.ts",
30
+ "default": "./dist/i18n/es.js"
31
+ },
32
+ "./manifest": "./manifest.json",
33
+ "./manifest.json": "./manifest.json",
34
+ "./nav-manifest.json": "./nav-manifest.json",
35
+ "./llms.txt": "./llms.txt",
36
+ "./package.json": "./package.json"
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "src",
41
+ "tsconfig.json",
42
+ "README.md",
43
+ "MODULE.md",
44
+ "CHANGELOG.md",
45
+ "manifest.json",
46
+ "nav-manifest.json",
47
+ "llms.txt"
48
+ ],
49
+ "scripts": {
50
+ "build": "tsc -p tsconfig.json",
51
+ "test": "tsc -p tsconfig.test.json && vitest run --exclude test/prodBundlePurity.test.ts",
52
+ "lint": "eslint .",
53
+ "size": "size-limit",
54
+ "test:pack": "vitest run test/prodBundlePurity.test.ts"
55
+ },
56
+ "size-limit": [
57
+ {
58
+ "name": "index — the headless pair (client + tree assembly + delta sync + hooks): no antd",
59
+ "path": "dist/index.js",
60
+ "limit": "9 KB"
61
+ },
62
+ {
63
+ "name": "default — the antd skin (catalog, category page, carousel, picker, breadcrumbs) must stay out of the main bundle",
64
+ "path": "dist/default/index.js",
65
+ "limit": "13 KB"
66
+ },
67
+ {
68
+ "name": "i18n/ru — the Russian bundle is opt-in, not carried by the main entry",
69
+ "path": "dist/i18n/ru.js",
70
+ "limit": "4 KB"
71
+ },
72
+ {
73
+ "name": "i18n/es — the Spanish bundle is opt-in, not carried by the main entry",
74
+ "path": "dist/i18n/es.js",
75
+ "limit": "3.5 KB"
76
+ }
77
+ ],
78
+ "peerDependencies": {
79
+ "@stapel/attributes-react": ">=0.1.0",
80
+ "@stapel/core": ">=0.15.0 <1.0.0",
81
+ "@stapel/tokens-antd": ">=0.5.0",
82
+ "@tanstack/react-query": "^5.0.0",
83
+ "antd": ">=5.20.0 <7",
84
+ "react": ">=19"
85
+ },
86
+ "peerDependenciesMeta": {
87
+ "@stapel/tokens-antd": {
88
+ "optional": true
89
+ },
90
+ "antd": {
91
+ "optional": true
92
+ }
93
+ },
94
+ "devDependencies": {
95
+ "@size-limit/preset-small-lib": "^11.2.0",
96
+ "@stapel/attributes-react": "workspace:^",
97
+ "@stapel/core": "workspace:^",
98
+ "@stapel/showcase": "workspace:^",
99
+ "@stapel/tokens": "workspace:^",
100
+ "@stapel/tokens-antd": "workspace:^",
101
+ "@tanstack/react-query": "^5.81.0",
102
+ "@testing-library/react": "^16.3.0",
103
+ "@types/react": "^19.1.0",
104
+ "@types/react-dom": "^19.1.0",
105
+ "antd": "^6.5.0",
106
+ "jsdom": "^26.1.0",
107
+ "react": "^19.1.0",
108
+ "react-dom": "^19.1.0",
109
+ "size-limit": "^11.2.0",
110
+ "typescript": "^5.8.3",
111
+ "vitest": "^3.2.4"
112
+ },
113
+ "engines": {
114
+ "node": ">=22"
115
+ },
116
+ "publishConfig": {
117
+ "access": "public"
118
+ }
119
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "$generated": "by scripts/gen-events.mjs — do not edit; drift-gated (pnpm gen:events:check)",
3
+ "package": "@stapel/categories-react",
4
+ "version": "0.1.0",
5
+ "defined": [],
6
+ "flows": []
7
+ }
@@ -0,0 +1,179 @@
1
+ import type { StapelClient } from "@stapel/core";
2
+ import type {
3
+ Category,
4
+ CategoryFeature,
5
+ CategoryListParams,
6
+ CategoryPage,
7
+ MaxRevision,
8
+ } from "./types.js";
9
+
10
+ /**
11
+ * The pair's typed operation surface — one method per stapel-categories
12
+ * endpoint a storefront may call, bound to the injected {@link StapelClient}
13
+ * (the per-module override seam of frontend-standard §7.2). Paths are relative
14
+ * to the runtime's `baseUrl` (`/categories/api/v1/`).
15
+ *
16
+ * ── Five reads, and the eighteen operations that are NOT here ──────────────
17
+ *
18
+ * The contract has 23 paths. Every one of them is on `ReadOnlyOrStaff`, which
19
+ * means SAFE methods are open to anonymous callers and everything else needs
20
+ * `is_staff` — so the split below is not a taste call, it is the permission
21
+ * boundary drawn on the surface:
22
+ *
23
+ * - **Public reads, and this pair's whole surface**: the category list (the
24
+ * delta-sync source), `children`, `carousel`, `{id}/features`, and
25
+ * `revision`.
26
+ * - **Staff writes**: create / update / delete, `bulk_add`,
27
+ * `bulk-commands`, `undelete`, `convert-type`, and the four
28
+ * `feature-editor*` operations. These are the catalogue admin, which the
29
+ * storefront wave leaves to Django admin (spec §4.3) — a pair that exposed
30
+ * them would invite a public screen to call something only staff may.
31
+ * - **`POST {id}/validate-dto/` is a WRITE in DRF's eyes**, so despite
32
+ * reading like a public helper it answers 403 to a visitor. The listing
33
+ * compose flow validates against the same rules with
34
+ * `@stapel/attributes-react`'s client-side mirror plus the server's verdict
35
+ * on publish; asking this endpoint would fail for exactly the people who
36
+ * need it.
37
+ * - **`GET {id}/validate-configs/`** is a catalogue-health read for an
38
+ * operator, not a storefront read.
39
+ * - **`GET translation-keys/`** is `IsServiceRequest` (service-to-service):
40
+ * it is the extraction feed for translators, and it returns KEYS with
41
+ * admin refs — never resolved labels. See `catalog/labels.ts`.
42
+ * - **`GET data.json/`** requires a `?revision=` cache-buster and hands back
43
+ * the whole table in one response. It is a legitimate alternative to the
44
+ * paged walk, but it is a *second* sync protocol, and the module documents
45
+ * the paged one. One protocol, tested, beats two, half-tested.
46
+ * - The parallel `/features/` collection (list, retrieve, create, …) is the
47
+ * feature tree in its own right — an admin axis. A storefront reads
48
+ * features THROUGH a category, which is the only place inheritance and
49
+ * order are resolved.
50
+ *
51
+ * Nothing is hidden: `manifest.json` lists the whole contract.
52
+ *
53
+ * These operations will be GENERATED from schema.json operationIds by gen-api
54
+ * v2 (task `core-typed-ops`); until then they are hand-authored here (the ONE
55
+ * legal home of path strings — `stapel/no-string-paths` §2.3 carve-out).
56
+ */
57
+ export interface CategoriesApi {
58
+ readonly client: StapelClient;
59
+
60
+ /**
61
+ * One page of the revision-paginated category list — the source the whole
62
+ * tree is assembled from, and the only endpoint that reports deletions.
63
+ *
64
+ * Rows are ordered by `revision`, NOT by tree position: this is a sync feed,
65
+ * not a menu. `revisions.global_max` is the cursor to store;
66
+ * `revisions.deleted_ids` is the authoritative tombstone list for a delta
67
+ * (see `catalog/sync.ts` for why it beats scanning `deleted: true` rows).
68
+ */
69
+ list(
70
+ params?: CategoryListParams,
71
+ options?: { readonly signal?: AbortSignal }
72
+ ): Promise<CategoryPage>;
73
+
74
+ /**
75
+ * Non-deleted direct children of one category, `tn_priority` descending.
76
+ *
77
+ * Redundant with the synced tree by construction, and deliberately kept: a
78
+ * host that wants one branch without paying for the catalogue (an SSR
79
+ * category page, a lazily-expanded admin-ish picker) has an endpoint for it,
80
+ * and it is the one place the server does the `deleted` filtering.
81
+ */
82
+ children(
83
+ id: number,
84
+ options?: { readonly signal?: AbortSignal }
85
+ ): Promise<readonly Category[]>;
86
+
87
+ /**
88
+ * The carousel strip: `active` AND `carousel_enabled`, `tn_priority`
89
+ * descending, cached server-side and sent with
90
+ * `Cache-Control: public, max-age` — so this one is safe to call on every
91
+ * landing render.
92
+ */
93
+ carousel(options?: {
94
+ readonly signal?: AbortSignal;
95
+ }): Promise<readonly Category[]>;
96
+
97
+ /**
98
+ * A category's resolved feature schema: its own features plus every ancestor's,
99
+ * deduplicated by SLUG (an `inherit` override is a new row sharing the
100
+ * parent's slug, and the version nearest this category wins), ordered by the
101
+ * category's own `CategoryFeature.order` first.
102
+ *
103
+ * This is what the listing compose form draws with
104
+ * `@stapel/attributes-react` and what labels a search facet. `config`
105
+ * arrives verbatim — defaults are the reader's job, and attributes-react
106
+ * owns them.
107
+ */
108
+ features(
109
+ id: number,
110
+ options?: { readonly signal?: AbortSignal }
111
+ ): Promise<readonly CategoryFeature[]>;
112
+
113
+ /**
114
+ * The table's current maximum revision, without transferring any rows.
115
+ *
116
+ * The cheap freshness probe: a stored snapshot whose cursor already equals
117
+ * this needs no delta request at all.
118
+ */
119
+ revision(options?: {
120
+ readonly signal?: AbortSignal;
121
+ }): Promise<MaxRevision>;
122
+ }
123
+
124
+ /**
125
+ * The wire query object for one list request.
126
+ *
127
+ * Exported because the query KEY is built from it, so the key and the request
128
+ * are the same value by construction.
129
+ *
130
+ * `include_deleted` is sent ONLY when the caller asked for a value, and the
131
+ * server's own default is `true`. Sending it always would be harmless; leaving
132
+ * it out when unasked keeps the cache key of a plain read from depending on
133
+ * this pair's opinion about a server default.
134
+ */
135
+ export function categoryListParams(
136
+ params: CategoryListParams = {}
137
+ ): Record<string, string | number | boolean | undefined> {
138
+ const query: Record<string, string | number | boolean | undefined> = {};
139
+ if (params.minRevision !== undefined) query["min_revision"] = params.minRevision;
140
+ if (params.maxRevision !== undefined) query["max_revision"] = params.maxRevision;
141
+ if (params.includeDeleted !== undefined) {
142
+ query["include_deleted"] = params.includeDeleted;
143
+ }
144
+ if (params.page !== undefined) query["page"] = params.page;
145
+ if (params.pageSize !== undefined) query["page_size"] = params.pageSize;
146
+ return query;
147
+ }
148
+
149
+ export function createCategoriesApi(client: StapelClient): CategoriesApi {
150
+ return {
151
+ client,
152
+
153
+ list: (params, options) =>
154
+ client.get("/categories/", {
155
+ query: categoryListParams(params),
156
+ ...(options?.signal !== undefined ? { signal: options.signal } : {}),
157
+ }),
158
+
159
+ children: (id, options) =>
160
+ client.get(`/categories/${String(id)}/children/`, {
161
+ ...(options?.signal !== undefined ? { signal: options.signal } : {}),
162
+ }),
163
+
164
+ carousel: (options) =>
165
+ client.get("/categories/carousel/", {
166
+ ...(options?.signal !== undefined ? { signal: options.signal } : {}),
167
+ }),
168
+
169
+ features: (id, options) =>
170
+ client.get(`/categories/${String(id)}/features/`, {
171
+ ...(options?.signal !== undefined ? { signal: options.signal } : {}),
172
+ }),
173
+
174
+ revision: (options) =>
175
+ client.get("/categories/revision/", {
176
+ ...(options?.signal !== undefined ? { signal: options.signal } : {}),
177
+ }),
178
+ };
179
+ }