@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,90 @@
1
+ import { categoriesErrorBundleRu } from "./generated/errors.ru.gen.js";
2
+ export { categoriesErrorBundleRu } from "./generated/errors.ru.gen.js";
3
+ /**
4
+ * Russian bundle for categories-react — the `@stapel/categories-react/i18n/ru`
5
+ * subpath (i18n-shipping.md §2) so the locale is opt-in: hosts that do not
6
+ * register it never carry these strings (the main entry does not import this
7
+ * module — gated by size-limit + the bundle-purity test).
8
+ *
9
+ * This is the storefront's DEFAULT language (darom-storefront verdict F1:
10
+ * ru-first).
11
+ *
12
+ * ── What comes from where ──────────────────────────────────────────────────
13
+ *
14
+ * `stapel-categories` ships **no `translations/` directory at all**, so 20 of
15
+ * the 62 registry codes can never come from an upstream catalogue. They split
16
+ * by owner, and the split is the whole point:
17
+ *
18
+ * - the 42 cross-cutting `stapel_core` codes are GENERATED, merged in from
19
+ * stapel-core's own catalogue by `pnpm gen:errors` (spread first below);
20
+ * - the **8 `stapel_categories` codes** are authored here — and when upstream
21
+ * ships a catalogue, eight lines are deleted and no key or text moves;
22
+ * - the **12 `stapel_attributes` codes** are NOT here. They belong to
23
+ * `@stapel/attributes-react`, which already translates them and which is a
24
+ * peer of this pair anyway. Two pairs must not give one refusal two
25
+ * sentences (§13.2 note 3). A host registers both bundles;
26
+ * `test/i18n.test.ts` proves the union covers the registry.
27
+ *
28
+ * PROVENANCE, stated rather than implied: the core catalogue ships
29
+ * `origin=seed:authored` and is UNREVIEWED; the pair-authored strings below
30
+ * are the same grade. Neither is a claim of review.
31
+ *
32
+ * And once more, because it is the fact this pair exists to be honest about:
33
+ * **no category name is translated here.** Category and feature names arrive
34
+ * as translation KEYS (`catalog/labels.ts`) and belong in the deployment's own
35
+ * bundle, not in a library's.
36
+ */
37
+ export const categoriesI18nBundleRu = {
38
+ ...categoriesErrorBundleRu,
39
+ // ── the 8 stapel_categories-owned codes, pair-authored ───────────────────
40
+ "error.400.categories_config_required": "Требуется объект config.",
41
+ "error.400.categories_database_error": "Ошибка базы данных при сохранении изменений.",
42
+ "error.400.categories_duplicate_slug": "Характеристика со слагом «{slug}» уже существует.",
43
+ "error.400.categories_expected_list": "Ожидался список объектов.",
44
+ "error.400.categories_feature_editor_invalid": "Некорректный запрос редактора характеристик: {reason}",
45
+ "error.400.categories_invalid_conversion": "Недопустимое преобразование типа (поддерживается только select ↔ string).",
46
+ "error.400.categories_not_deleted": "Категория не удалена.",
47
+ "error.409.categories_feature_editor_conflict": "Категорию изменил другой редактор (ожидалась ревизия {expected}, сейчас {actual}); перезагрузите страницу и повторите.",
48
+ "categories.error.unknown": "Что-то пошло не так с каталогом.",
49
+ "categories.catalog.title": "Каталог",
50
+ "categories.catalog.loading": "Загружаем каталог…",
51
+ "categories.catalog.load_failed": "Не удалось загрузить каталог.",
52
+ "categories.catalog.empty": "В каталоге пока нет категорий.",
53
+ "categories.catalog.retry": "Повторить",
54
+ "categories.catalog.truncated": "Каталог загружен не полностью. Категории, которой здесь нет, ещё не прочитали — её не удаляли.",
55
+ "categories.catalog.refreshing": "Проверяем изменения в каталоге…",
56
+ "categories.catalog.as_of": "Каталог на ревизию {revision}",
57
+ "categories.category.title": "Категория",
58
+ "categories.category.unknown_slug": "По этому адресу нет категории.",
59
+ "categories.category.subcategories": "Подкатегории",
60
+ "categories.category.no_subcategories": "У этой категории нет подкатегорий.",
61
+ "categories.category.open": "Открыть",
62
+ "categories.breadcrumbs.root": "Все категории",
63
+ "categories.breadcrumbs.label": "Вы здесь",
64
+ "categories.carousel.title": "Категории",
65
+ "categories.carousel.loading": "Загружаем категории…",
66
+ "categories.carousel.load_failed": "Не удалось загрузить категории.",
67
+ "categories.carousel.empty": "Сейчас ни одна категория не вынесена на витрину.",
68
+ "categories.picker.title": "Категория",
69
+ "categories.picker.search": "Поиск по категориям",
70
+ "categories.picker.loading": "Загружаем категории…",
71
+ "categories.picker.load_failed": "Не удалось загрузить категории.",
72
+ "categories.picker.no_matches": "Ни одна категория не подходит.",
73
+ "categories.picker.up": "На уровень выше",
74
+ "categories.picker.selected": "Выбрано: {category}",
75
+ "categories.picker.blocked.nothing_selected": "Сначала выберите категорию.",
76
+ "categories.picker.blocked.not_a_leaf": "Выберите категорию точнее — у этой есть подкатегории, а от них зависит, какие характеристики спросят.",
77
+ "categories.features.title": "Характеристики в этой категории",
78
+ "categories.features.loading": "Загружаем характеристики…",
79
+ "categories.features.load_failed": "Не удалось загрузить характеристики.",
80
+ "categories.features.empty": "В этой категории не нужно заполнять дополнительные характеристики.",
81
+ "categories.features.mandatory": "Обязательно",
82
+ "categories.features.type": "{type}",
83
+ "categories.features.untyped": "У этой характеристики нет типа, показать её нельзя.",
84
+ };
85
+ /** Register the `ru` bundle. Call AFTER `registerCategoriesI18n` so it
86
+ * overrides the English floor. */
87
+ export function registerCategoriesI18nRu(engine, locale = "ru") {
88
+ engine.registerBundle(locale, categoriesI18nBundleRu);
89
+ }
90
+ //# sourceMappingURL=ru.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ru.js","sourceRoot":"","sources":["../../src/i18n/ru.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAEvE,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAmB;IACpD,GAAG,uBAAuB;IAE1B,4EAA4E;IAC5E,sCAAsC,EAAE,0BAA0B;IAClE,qCAAqC,EACnC,8CAA8C;IAChD,qCAAqC,EACnC,mDAAmD;IACrD,oCAAoC,EAAE,2BAA2B;IACjE,6CAA6C,EAC3C,uDAAuD;IACzD,yCAAyC,EACvC,2EAA2E;IAC7E,kCAAkC,EAAE,uBAAuB;IAC3D,8CAA8C,EAC5C,wHAAwH;IAE1H,0BAA0B,EAAE,kCAAkC;IAE9D,0BAA0B,EAAE,SAAS;IACrC,4BAA4B,EAAE,oBAAoB;IAClD,gCAAgC,EAAE,+BAA+B;IACjE,0BAA0B,EAAE,gCAAgC;IAC5D,0BAA0B,EAAE,WAAW;IACvC,8BAA8B,EAC5B,gGAAgG;IAClG,+BAA+B,EAAE,iCAAiC;IAClE,0BAA0B,EAAE,+BAA+B;IAE3D,2BAA2B,EAAE,WAAW;IACxC,kCAAkC,EAAE,gCAAgC;IACpE,mCAAmC,EAAE,cAAc;IACnD,sCAAsC,EAAE,oCAAoC;IAC5E,0BAA0B,EAAE,SAAS;IAErC,6BAA6B,EAAE,eAAe;IAC9C,8BAA8B,EAAE,UAAU;IAE1C,2BAA2B,EAAE,WAAW;IACxC,6BAA6B,EAAE,sBAAsB;IACrD,iCAAiC,EAAE,iCAAiC;IACpE,2BAA2B,EAAE,kDAAkD;IAE/E,yBAAyB,EAAE,WAAW;IACtC,0BAA0B,EAAE,qBAAqB;IACjD,2BAA2B,EAAE,sBAAsB;IACnD,+BAA+B,EAAE,iCAAiC;IAClE,8BAA8B,EAAE,gCAAgC;IAChE,sBAAsB,EAAE,iBAAiB;IACzC,4BAA4B,EAAE,qBAAqB;IACnD,4CAA4C,EAAE,6BAA6B;IAC3E,sCAAsC,EACpC,uGAAuG;IAEzG,2BAA2B,EAAE,iCAAiC;IAC9D,6BAA6B,EAAE,2BAA2B;IAC1D,iCAAiC,EAAE,sCAAsC;IACzE,2BAA2B,EACzB,oEAAoE;IACtE,+BAA+B,EAAE,aAAa;IAC9C,0BAA0B,EAAE,QAAQ;IACpC,6BAA6B,EAC3B,qDAAqD;CACxD,CAAC;AAEF;kCACkC;AAClC,MAAM,UAAU,wBAAwB,CAAC,MAAkB,EAAE,MAAM,GAAG,IAAI;IACxE,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;AACxD,CAAC"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * `@stapel/categories-react` — the headless React pair for stapel-categories
3
+ * (frontend-standard §2). Business + state only, zero visual opinion; the antd
4
+ * skin lives behind the `./default` subpath, so a host that renders its own
5
+ * catalogue chrome never carries it.
6
+ *
7
+ * ── The one-liner ──────────────────────────────────────────────────────────
8
+ *
9
+ * ```tsx
10
+ * const runtime = createCategoriesRuntime({ baseUrl: "/categories/api/v1/" });
11
+ * <CategoriesProvider runtime={runtime}>
12
+ * <CatalogPage /> // or <CategoryPage slug={slug} renderListings={…} />
13
+ * </CategoriesProvider>
14
+ * ```
15
+ *
16
+ * No session, no workspace id, no auth client: every endpoint this pair calls
17
+ * is a SAFE method under `ReadOnlyOrStaff`, so a catalogue renders for a
18
+ * visitor who will never sign in.
19
+ *
20
+ * ── The four properties this pair exists to guarantee ──────────────────────
21
+ *
22
+ * 1. **The client builds the tree, because the server has none.**
23
+ * `GET /categories/` returns FLAT rows ordered by `revision`, with treenode
24
+ * ancestry as COMMA-JOINED pk strings (`"1,7,12"`, `""` for a root).
25
+ * `catalog/tree.ts` parses and assembles; `catalog/pks.ts` is the parser,
26
+ * and it exists because the spec described those columns as arrays.
27
+ * 2. **The catalogue is synced by delta, not refetched.** A stored snapshot
28
+ * (`createRepository`, `scope: "app"`) plus `?min_revision=` is the module's
29
+ * own documented protocol; without it every storefront page pulls the whole
30
+ * catalogue. Two things the documentation does not say are implemented
31
+ * anyway and explained where they live: `revisions.deleted_ids` is the
32
+ * complete tombstone channel (the `deleted: true` rows are paginated and a
33
+ * short walk misses them), and a multi-page walk must pin `max_revision` or
34
+ * a concurrent write shifts its page boundaries.
35
+ * 3. **A slug is resolved by the client, because the server cannot.**
36
+ * `lookup_field` is never overridden and the list endpoint has no slug
37
+ * filter, so `/c/:slug` resolves against the synced tree. That is a second,
38
+ * independent reason the tree is cached.
39
+ * 4. **Names are translation KEYS, and this pair says so.** No serializer runs
40
+ * the module's `DISPLAY_TRANSLATOR` seam, so `name` arrives as
41
+ * `category.electronics` even on a deployment with a real translator
42
+ * configured, and `translatable` / `translate` say per row whether a string
43
+ * is a key at all. `catalog/labels.ts` carries the whole answer, and the
44
+ * pair ships no catalogue of category names — those belong to the host.
45
+ *
46
+ * Layers: api → catalog (pure) → model → headless → i18n. Generated surfaces
47
+ * (the typed schema, the error map, the manifest, the nav manifest, llms.txt)
48
+ * are produced by the monorepo `gen:*` drivers from stapel-categories' own
49
+ * `docs/` artifacts and stand under drift gates.
50
+ */
51
+ export { categoryListParams, createCategoriesApi } from "./api/categoriesApi.js";
52
+ export type { CategoriesApi } from "./api/categoriesApi.js";
53
+ export type { Category, CategoryFeature, CategoryListParams, CategoryPage as CategoryListPage, CategoryRevisions, FeatureConfig, MaxRevision, Schemas, } from "./api/types.js";
54
+ export { parseTreenodePks } from "./catalog/pks.js";
55
+ export { buildCategoryTree, categoryAncestorIds, categoryBreadcrumbs, categoryChildIds, flattenCategoryNodes, resolveCategorySlug, } from "./catalog/tree.js";
56
+ export type { BuildCategoryTreeOptions, CategoryIndex, CategoryNode, } from "./catalog/tree.js";
57
+ export { EMPTY_SNAPSHOT, applyCategoryPage, firstPageRequest, isEmptySnapshot, nextPageRequest, parseSnapshot, } from "./catalog/sync.js";
58
+ export type { CategorySnapshot } from "./catalog/sync.js";
59
+ export { categoryLabel, featureLabel, featureOptionsAreKeys, renderCategoryLabel, } from "./catalog/labels.js";
60
+ export type { CategoryLabel, CategoryLabelKind } from "./catalog/labels.js";
61
+ export { createCategoriesRuntime } from "./model/runtime.js";
62
+ export type { CategoriesRuntime, CreateCategoriesRuntimeOptions, } from "./model/runtime.js";
63
+ export { CategoriesRuntimeContext, useCategoriesAnalytics, useCategoriesApi, useCategoriesRuntime, } from "./model/context.js";
64
+ export { CATALOG_KEY, CATALOG_NAMESPACE, createCatalogStore, memoryCatalogStore, } from "./model/catalogStore.js";
65
+ export type { CatalogStore, CreateCatalogStoreOptions, } from "./model/catalogStore.js";
66
+ export { MAX_SYNC_PAGES, syncCatalog } from "./model/catalogSync.js";
67
+ export type { SyncCatalogOptions, SyncCatalogResult } from "./model/catalogSync.js";
68
+ export { catalogKeyOptions, categoriesQueryKeys } from "./model/queryKeys.js";
69
+ export type { CatalogKeyOptions } from "./model/queryKeys.js";
70
+ export { useCategoriesRevision, useCategoryCarousel, useCategoryCatalog, useCategoryChildren, useCategoryFeatures, } from "./model/queries.js";
71
+ export type { CategoryCatalog, UseCategoryCatalogOptions, } from "./model/queries.js";
72
+ export { CategoriesProvider } from "./headless/CategoriesProvider.js";
73
+ export { CategoryTree } from "./headless/CategoryTree.js";
74
+ export type { CategoryTreeBag, CategoryTreeProps } from "./headless/CategoryTree.js";
75
+ export { CategoryBreadcrumbs } from "./headless/CategoryBreadcrumbs.js";
76
+ export type { CategoryBreadcrumbsBag, CategoryBreadcrumbsProps, CategoryCrumb, } from "./headless/CategoryBreadcrumbs.js";
77
+ export { CategoryCarousel } from "./headless/CategoryCarousel.js";
78
+ export type { CarouselEntry, CategoryCarouselBag, CategoryCarouselProps, } from "./headless/CategoryCarousel.js";
79
+ export { CategoryPicker } from "./headless/CategoryPicker.js";
80
+ export type { CategoryOption, CategoryPickerBag, CategoryPickerBlockedReason, CategoryPickerProps, } from "./headless/CategoryPicker.js";
81
+ export { CategoryFeatures } from "./headless/CategoryFeatures.js";
82
+ export type { CategoryFeatureEntry, CategoryFeaturesBag, CategoryFeaturesProps, } from "./headless/CategoryFeatures.js";
83
+ export { navEntries } from "./nav/manifest.js";
84
+ export { CATEGORIES_I18N_KEYS, categoriesI18nBundleEn, registerCategoriesI18n, } from "./i18n/keys.js";
85
+ export type { CategoriesI18nKey } from "./i18n/keys.js";
86
+ export { CATEGORIES_ERRORS, CATEGORIES_ERROR_CODES, CATEGORIES_FEATURE_EDITOR_CONFLICT, categoriesErrorBundleEn, explainCategoriesError, } from "./i18n/errorsMap.js";
87
+ export type { CategoriesErrorCode, CategoriesErrorSpec, Remediation, } from "./i18n/errorsMap.js";
88
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * `@stapel/categories-react` — the headless React pair for stapel-categories
3
+ * (frontend-standard §2). Business + state only, zero visual opinion; the antd
4
+ * skin lives behind the `./default` subpath, so a host that renders its own
5
+ * catalogue chrome never carries it.
6
+ *
7
+ * ── The one-liner ──────────────────────────────────────────────────────────
8
+ *
9
+ * ```tsx
10
+ * const runtime = createCategoriesRuntime({ baseUrl: "/categories/api/v1/" });
11
+ * <CategoriesProvider runtime={runtime}>
12
+ * <CatalogPage /> // or <CategoryPage slug={slug} renderListings={…} />
13
+ * </CategoriesProvider>
14
+ * ```
15
+ *
16
+ * No session, no workspace id, no auth client: every endpoint this pair calls
17
+ * is a SAFE method under `ReadOnlyOrStaff`, so a catalogue renders for a
18
+ * visitor who will never sign in.
19
+ *
20
+ * ── The four properties this pair exists to guarantee ──────────────────────
21
+ *
22
+ * 1. **The client builds the tree, because the server has none.**
23
+ * `GET /categories/` returns FLAT rows ordered by `revision`, with treenode
24
+ * ancestry as COMMA-JOINED pk strings (`"1,7,12"`, `""` for a root).
25
+ * `catalog/tree.ts` parses and assembles; `catalog/pks.ts` is the parser,
26
+ * and it exists because the spec described those columns as arrays.
27
+ * 2. **The catalogue is synced by delta, not refetched.** A stored snapshot
28
+ * (`createRepository`, `scope: "app"`) plus `?min_revision=` is the module's
29
+ * own documented protocol; without it every storefront page pulls the whole
30
+ * catalogue. Two things the documentation does not say are implemented
31
+ * anyway and explained where they live: `revisions.deleted_ids` is the
32
+ * complete tombstone channel (the `deleted: true` rows are paginated and a
33
+ * short walk misses them), and a multi-page walk must pin `max_revision` or
34
+ * a concurrent write shifts its page boundaries.
35
+ * 3. **A slug is resolved by the client, because the server cannot.**
36
+ * `lookup_field` is never overridden and the list endpoint has no slug
37
+ * filter, so `/c/:slug` resolves against the synced tree. That is a second,
38
+ * independent reason the tree is cached.
39
+ * 4. **Names are translation KEYS, and this pair says so.** No serializer runs
40
+ * the module's `DISPLAY_TRANSLATOR` seam, so `name` arrives as
41
+ * `category.electronics` even on a deployment with a real translator
42
+ * configured, and `translatable` / `translate` say per row whether a string
43
+ * is a key at all. `catalog/labels.ts` carries the whole answer, and the
44
+ * pair ships no catalogue of category names — those belong to the host.
45
+ *
46
+ * Layers: api → catalog (pure) → model → headless → i18n. Generated surfaces
47
+ * (the typed schema, the error map, the manifest, the nav manifest, llms.txt)
48
+ * are produced by the monorepo `gen:*` drivers from stapel-categories' own
49
+ * `docs/` artifacts and stand under drift gates.
50
+ */
51
+ // ── api ──────────────────────────────────────────────────────────────────────
52
+ export { categoryListParams, createCategoriesApi } from "./api/categoriesApi.js";
53
+ // ── catalog (pure: no React, no fetch, no storage) ───────────────────────────
54
+ export { parseTreenodePks } from "./catalog/pks.js";
55
+ export { buildCategoryTree, categoryAncestorIds, categoryBreadcrumbs, categoryChildIds, flattenCategoryNodes, resolveCategorySlug, } from "./catalog/tree.js";
56
+ export { EMPTY_SNAPSHOT, applyCategoryPage, firstPageRequest, isEmptySnapshot, nextPageRequest, parseSnapshot, } from "./catalog/sync.js";
57
+ export { categoryLabel, featureLabel, featureOptionsAreKeys, renderCategoryLabel, } from "./catalog/labels.js";
58
+ // ── model (runtime wiring, persistence, sync driver, hooks) ──────────────────
59
+ export { createCategoriesRuntime } from "./model/runtime.js";
60
+ export { CategoriesRuntimeContext, useCategoriesAnalytics, useCategoriesApi, useCategoriesRuntime, } from "./model/context.js";
61
+ export { CATALOG_KEY, CATALOG_NAMESPACE, createCatalogStore, memoryCatalogStore, } from "./model/catalogStore.js";
62
+ export { MAX_SYNC_PAGES, syncCatalog } from "./model/catalogSync.js";
63
+ export { catalogKeyOptions, categoriesQueryKeys } from "./model/queryKeys.js";
64
+ export { useCategoriesRevision, useCategoryCarousel, useCategoryCatalog, useCategoryChildren, useCategoryFeatures, } from "./model/queries.js";
65
+ // ── headless (renderless components) ─────────────────────────────────────────
66
+ export { CategoriesProvider } from "./headless/CategoriesProvider.js";
67
+ export { CategoryTree } from "./headless/CategoryTree.js";
68
+ export { CategoryBreadcrumbs } from "./headless/CategoryBreadcrumbs.js";
69
+ export { CategoryCarousel } from "./headless/CategoryCarousel.js";
70
+ export { CategoryPicker } from "./headless/CategoryPicker.js";
71
+ export { CategoryFeatures } from "./headless/CategoryFeatures.js";
72
+ // ── nav manifest (the pair's public surface declaration) ─────────────────────
73
+ export { navEntries } from "./nav/manifest.js";
74
+ // ── i18n ─────────────────────────────────────────────────────────────────────
75
+ export { CATEGORIES_I18N_KEYS, categoriesI18nBundleEn, registerCategoriesI18n, } from "./i18n/keys.js";
76
+ // ── errors map (code → status/params/remediation/en; generated) ──────────────
77
+ export { CATEGORIES_ERRORS, CATEGORIES_ERROR_CODES, CATEGORIES_FEATURE_EDITOR_CONFLICT, categoriesErrorBundleEn, explainCategoriesError, } from "./i18n/errorsMap.js";
78
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAEH,gFAAgF;AAChF,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAajF,gFAAgF;AAChF,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,mBAAmB,CAAC;AAO3B,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,aAAa,GACd,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,aAAa,EACb,YAAY,EACZ,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,qBAAqB,CAAC;AAG7B,gFAAgF;AAChF,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAK7D,OAAO,EACL,wBAAwB,EACxB,sBAAsB,EACtB,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AAKjC,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAErE,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAE9E,OAAO,EACL,qBAAqB,EACrB,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAM5B,gFAAgF;AAChF,OAAO,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAMxE,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAMlE,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAO9D,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAOlE,gFAAgF;AAChF,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE/C,gFAAgF;AAChF,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,sBAAsB,GACvB,MAAM,gBAAgB,CAAC;AAGxB,gFAAgF;AAChF,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,kCAAkC,EAClC,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,23 @@
1
+ import type { Repository } from "@stapel/core";
2
+ import type { CategorySnapshot } from "../catalog/sync.js";
3
+ /** The repository namespace. Changing it orphans every stored snapshot, which
4
+ * is the intended migration for a shape change too large for `version`. */
5
+ export declare const CATALOG_NAMESPACE = "categories.catalog";
6
+ /** The single key inside that namespace. */
7
+ export declare const CATALOG_KEY = "snapshot";
8
+ /** Read / write the catalogue snapshot, with every failure mode absorbed. */
9
+ export interface CatalogStore {
10
+ load(): Promise<CategorySnapshot>;
11
+ save(snapshot: CategorySnapshot): Promise<void>;
12
+ clear(): Promise<void>;
13
+ }
14
+ export interface CreateCatalogStoreOptions {
15
+ /** Inject a repository (tests, SSR, a host with its own backend). Default:
16
+ * an app-scoped repository under {@link CATALOG_NAMESPACE}. */
17
+ readonly repository?: Repository<unknown>;
18
+ }
19
+ /** A store that remembers nothing — the honest fallback when persistence is
20
+ * unavailable, and what a caller passes to opt out of caching entirely. */
21
+ export declare function memoryCatalogStore(): CatalogStore;
22
+ export declare function createCatalogStore(options?: CreateCatalogStoreOptions): CatalogStore;
23
+ //# sourceMappingURL=catalogStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalogStore.d.ts","sourceRoot":"","sources":["../../src/model/catalogStore.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D;2EAC2E;AAC3E,eAAO,MAAM,iBAAiB,uBAAuB,CAAC;AAEtD,4CAA4C;AAC5C,eAAO,MAAM,WAAW,aAAa,CAAC;AAEtC,6EAA6E;AAC7E,MAAM,WAAW,YAAY;IAC3B,IAAI,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAClC,IAAI,CAAC,QAAQ,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,MAAM,WAAW,yBAAyB;IACxC;mEAC+D;IAC/D,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC;CAC3C;AAED;2EAC2E;AAC3E,wBAAgB,kBAAkB,IAAI,YAAY,CAajD;AAED,wBAAgB,kBAAkB,CAChC,OAAO,GAAE,yBAA8B,GACtC,YAAY,CAmCd"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Where the catalogue lives between page loads.
3
+ *
4
+ * `createRepository` is the ONE sanctioned client-side persistence primitive
5
+ * (`@stapel/core` `repository.ts`; direct `localStorage` outside it is a lint
6
+ * error, `stapel/no-raw-storage`), and the spec's verdict (§4.3) puts the tree
7
+ * in it with `scope: "app"`.
8
+ *
9
+ * **`scope: "app"` is a correctness choice, not a size one.** A user-scoped
10
+ * repository is encrypted with the per-session key and wiped on logout, with
11
+ * no opt-out — both of which are wrong here twice over: the category tree is
12
+ * a deployment's PUBLIC content, identical for every visitor and for a visitor
13
+ * with no session at all, and wiping it at logout would re-download the whole
14
+ * catalogue for somebody who just signed out of a storefront they are still
15
+ * browsing. App scope also means `createRepository` never asks for a
16
+ * `SessionManager`, so the store works on a page with no auth wired at all.
17
+ *
18
+ * A repository read can fail or return junk (a private window, cleared site
19
+ * data, a snapshot written by an older shape). Every read here degrades to
20
+ * "no snapshot" — a cold sync costs one request; a cache that throws costs
21
+ * the page.
22
+ */
23
+ import { createRepository } from "@stapel/core";
24
+ import { EMPTY_SNAPSHOT, parseSnapshot } from "../catalog/sync.js";
25
+ /** The repository namespace. Changing it orphans every stored snapshot, which
26
+ * is the intended migration for a shape change too large for `version`. */
27
+ export const CATALOG_NAMESPACE = "categories.catalog";
28
+ /** The single key inside that namespace. */
29
+ export const CATALOG_KEY = "snapshot";
30
+ /** A store that remembers nothing — the honest fallback when persistence is
31
+ * unavailable, and what a caller passes to opt out of caching entirely. */
32
+ export function memoryCatalogStore() {
33
+ let held = EMPTY_SNAPSHOT;
34
+ return {
35
+ load: () => Promise.resolve(held),
36
+ save: (snapshot) => {
37
+ held = snapshot;
38
+ return Promise.resolve();
39
+ },
40
+ clear: () => {
41
+ held = EMPTY_SNAPSHOT;
42
+ return Promise.resolve();
43
+ },
44
+ };
45
+ }
46
+ export function createCatalogStore(options = {}) {
47
+ let repository;
48
+ try {
49
+ repository =
50
+ options.repository ??
51
+ createRepository(CATALOG_NAMESPACE, { scope: "app" });
52
+ }
53
+ catch {
54
+ // No storage backend at all (an exotic runtime, a locked-down origin).
55
+ return memoryCatalogStore();
56
+ }
57
+ return {
58
+ async load() {
59
+ try {
60
+ return parseSnapshot(await repository.get(CATALOG_KEY)) ?? EMPTY_SNAPSHOT;
61
+ }
62
+ catch {
63
+ return EMPTY_SNAPSHOT;
64
+ }
65
+ },
66
+ async save(snapshot) {
67
+ try {
68
+ await repository.set(CATALOG_KEY, snapshot);
69
+ }
70
+ catch {
71
+ // A full quota or a blocked origin must not fail the render that
72
+ // triggered the sync: the catalogue is already in memory and correct.
73
+ }
74
+ },
75
+ async clear() {
76
+ try {
77
+ await repository.del(CATALOG_KEY);
78
+ }
79
+ catch {
80
+ /* nothing to undo */
81
+ }
82
+ },
83
+ };
84
+ }
85
+ //# sourceMappingURL=catalogStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalogStore.js","sourceRoot":"","sources":["../../src/model/catalogStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGnE;2EAC2E;AAC3E,MAAM,CAAC,MAAM,iBAAiB,GAAG,oBAAoB,CAAC;AAEtD,4CAA4C;AAC5C,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC;AAetC;2EAC2E;AAC3E,MAAM,UAAU,kBAAkB;IAChC,IAAI,IAAI,GAAqB,cAAc,CAAC;IAC5C,OAAO;QACL,IAAI,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC;QACjC,IAAI,EAAE,CAAC,QAAQ,EAAE,EAAE;YACjB,IAAI,GAAG,QAAQ,CAAC;YAChB,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QAC3B,CAAC;QACD,KAAK,EAAE,GAAG,EAAE;YACV,IAAI,GAAG,cAAc,CAAC;YACtB,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QAC3B,CAAC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,kBAAkB,CAChC,UAAqC,EAAE;IAEvC,IAAI,UAA+B,CAAC;IACpC,IAAI,CAAC;QACH,UAAU;YACR,OAAO,CAAC,UAAU;gBAClB,gBAAgB,CAAU,iBAAiB,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IACnE,CAAC;IAAC,MAAM,CAAC;QACP,uEAAuE;QACvE,OAAO,kBAAkB,EAAE,CAAC;IAC9B,CAAC;IAED,OAAO;QACL,KAAK,CAAC,IAAI;YACR,IAAI,CAAC;gBACH,OAAO,aAAa,CAAC,MAAM,UAAU,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,IAAI,cAAc,CAAC;YAC5E,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,cAAc,CAAC;YACxB,CAAC;QACH,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,QAAQ;YACjB,IAAI,CAAC;gBACH,MAAM,UAAU,CAAC,GAAG,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;YAC9C,CAAC;YAAC,MAAM,CAAC;gBACP,iEAAiE;gBACjE,sEAAsE;YACxE,CAAC;QACH,CAAC;QACD,KAAK,CAAC,KAAK;YACT,IAAI,CAAC;gBACH,MAAM,UAAU,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;YACpC,CAAC;YAAC,MAAM,CAAC;gBACP,qBAAqB;YACvB,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The sync walk: stored snapshot → one request or several → a fresh snapshot.
3
+ *
4
+ * Pure of React, so the same routine serves the hook, an SSR prefetch and a
5
+ * test. The rules it enforces are all in `catalog/sync.ts`; this is the loop
6
+ * that applies them, plus the two guards a loop over a paginated endpoint
7
+ * needs:
8
+ *
9
+ * - a **page budget**, because `has_next` is server-reported and a bug there
10
+ * (or a catalogue growing faster than the walk reads it) is an infinite
11
+ * request loop against someone's production API, not a slow render; and
12
+ * - **publish-on-completion**, so a walk that dies on page 3 leaves the
13
+ * previous catalogue intact instead of replacing it with two thirds of one.
14
+ */
15
+ import type { CategoriesApi } from "../api/categoriesApi.js";
16
+ import type { CategorySnapshot } from "../catalog/sync.js";
17
+ /**
18
+ * Hard ceiling on pages per sync. At the server's max `page_size` of 1000 this
19
+ * is a million categories — far beyond any real catalogue, and small enough
20
+ * that a runaway loop stops in seconds instead of hammering a backend.
21
+ */
22
+ export declare const MAX_SYNC_PAGES = 1000;
23
+ export interface SyncCatalogOptions {
24
+ readonly signal?: AbortSignal;
25
+ /** Rows per request. Server default 100, maximum 1000. */
26
+ readonly pageSize?: number;
27
+ readonly maxPages?: number;
28
+ }
29
+ export interface SyncCatalogResult {
30
+ readonly snapshot: CategorySnapshot;
31
+ /** `true` when the walk started from nothing (a cold catalogue). */
32
+ readonly wasFullSync: boolean;
33
+ /** How many requests the walk actually made. `0` is impossible — the
34
+ * freshness probe is the caller's job, not this function's. */
35
+ readonly pages: number;
36
+ /** The page budget stopped the walk before `has_next` did. The snapshot is
37
+ * a truncated catalogue and the caller must NOT treat it as complete. */
38
+ readonly truncated: boolean;
39
+ }
40
+ /**
41
+ * Bring `stored` up to date.
42
+ *
43
+ * A cold start walks the whole catalogue into a fresh accumulator; a warm
44
+ * start folds the delta into the stored rows. Which one happens is decided by
45
+ * `firstPageRequest` and by what the accumulator starts as — there is no flag
46
+ * that a caller could set the wrong way (see `catalog/sync.ts`).
47
+ */
48
+ export declare function syncCatalog(api: CategoriesApi, stored: CategorySnapshot, options?: SyncCatalogOptions): Promise<SyncCatalogResult>;
49
+ //# sourceMappingURL=catalogSync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalogSync.d.ts","sourceRoot":"","sources":["../../src/model/catalogSync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAS7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D;;;;GAIG;AACH,eAAO,MAAM,cAAc,OAAO,CAAC;AAEnC,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,oEAAoE;IACpE,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;mEAC+D;IAC/D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;6EACyE;IACzE,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,gBAAgB,EACxB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,iBAAiB,CAAC,CAmD5B"}
@@ -0,0 +1,64 @@
1
+ import { EMPTY_SNAPSHOT, applyCategoryPage, firstPageRequest, isEmptySnapshot, nextPageRequest, } from "../catalog/sync.js";
2
+ /**
3
+ * Hard ceiling on pages per sync. At the server's max `page_size` of 1000 this
4
+ * is a million categories — far beyond any real catalogue, and small enough
5
+ * that a runaway loop stops in seconds instead of hammering a backend.
6
+ */
7
+ export const MAX_SYNC_PAGES = 1000;
8
+ /**
9
+ * Bring `stored` up to date.
10
+ *
11
+ * A cold start walks the whole catalogue into a fresh accumulator; a warm
12
+ * start folds the delta into the stored rows. Which one happens is decided by
13
+ * `firstPageRequest` and by what the accumulator starts as — there is no flag
14
+ * that a caller could set the wrong way (see `catalog/sync.ts`).
15
+ */
16
+ export async function syncCatalog(api, stored, options = {}) {
17
+ const wasFullSync = isEmptySnapshot(stored);
18
+ const budget = options.maxPages ?? MAX_SYNC_PAGES;
19
+ let request = {
20
+ ...firstPageRequest(stored),
21
+ ...(options.pageSize !== undefined ? { pageSize: options.pageSize } : {}),
22
+ };
23
+ // A full sync must not inherit rows the server is no longer listing, so it
24
+ // accumulates from empty. A delta folds into what is already known.
25
+ let accumulator = wasFullSync
26
+ ? EMPTY_SNAPSHOT
27
+ : { ...stored };
28
+ let pages = 0;
29
+ let truncated = false;
30
+ while (request !== undefined) {
31
+ if (pages >= budget) {
32
+ truncated = true;
33
+ break;
34
+ }
35
+ const current = request;
36
+ const page = await api.list(current, {
37
+ ...(options.signal !== undefined ? { signal: options.signal } : {}),
38
+ });
39
+ pages += 1;
40
+ accumulator = applyCategoryPage(accumulator, page, {
41
+ // The walk pinned its window with `max_revision`; the cursor may not
42
+ // claim to have read past it.
43
+ ...(current.maxRevision !== undefined
44
+ ? { cursorLimit: current.maxRevision }
45
+ : {}),
46
+ });
47
+ request = nextPageRequest(current, page);
48
+ }
49
+ if (truncated) {
50
+ // The rows gathered so far are still worth showing — a partial catalogue
51
+ // beats a blank menu — but the CURSOR must not advance, or the next sync
52
+ // asks for a delta on top of a catalogue that was never fully read and the
53
+ // gap becomes permanent. Rewinding it here means the worst case is one
54
+ // repeated full walk, not a silently incomplete tree forever.
55
+ return {
56
+ snapshot: { ...accumulator, cursor: stored.cursor },
57
+ wasFullSync,
58
+ pages,
59
+ truncated,
60
+ };
61
+ }
62
+ return { snapshot: accumulator, wasFullSync, pages, truncated };
63
+ }
64
+ //# sourceMappingURL=catalogSync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalogSync.js","sourceRoot":"","sources":["../../src/model/catalogSync.ts"],"names":[],"mappings":"AAgBA,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,eAAe,GAChB,MAAM,oBAAoB,CAAC;AAG5B;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC;AAqBnC;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,GAAkB,EAClB,MAAwB,EACxB,UAA8B,EAAE;IAEhC,MAAM,WAAW,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,IAAI,cAAc,CAAC;IAElD,IAAI,OAAO,GAAmC;QAC5C,GAAG,gBAAgB,CAAC,MAAM,CAAC;QAC3B,GAAG,CAAC,OAAO,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC1E,CAAC;IACF,2EAA2E;IAC3E,oEAAoE;IACpE,IAAI,WAAW,GAAqB,WAAW;QAC7C,CAAC,CAAC,cAAc;QAChB,CAAC,CAAC,EAAE,GAAG,MAAM,EAAE,CAAC;IAClB,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,SAAS,GAAG,KAAK,CAAC;IAEtB,OAAO,OAAO,KAAK,SAAS,EAAE,CAAC;QAC7B,IAAI,KAAK,IAAI,MAAM,EAAE,CAAC;YACpB,SAAS,GAAG,IAAI,CAAC;YACjB,MAAM;QACR,CAAC;QACD,MAAM,OAAO,GAAuB,OAAO,CAAC;QAC5C,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE;YACnC,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACpE,CAAC,CAAC;QACH,KAAK,IAAI,CAAC,CAAC;QACX,WAAW,GAAG,iBAAiB,CAAC,WAAW,EAAE,IAAI,EAAE;YACjD,qEAAqE;YACrE,8BAA8B;YAC9B,GAAG,CAAC,OAAO,CAAC,WAAW,KAAK,SAAS;gBACnC,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE;gBACtC,CAAC,CAAC,EAAE,CAAC;SACR,CAAC,CAAC;QACH,OAAO,GAAG,eAAe,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IAC3C,CAAC;IAED,IAAI,SAAS,EAAE,CAAC;QACd,yEAAyE;QACzE,yEAAyE;QACzE,2EAA2E;QAC3E,uEAAuE;QACvE,8DAA8D;QAC9D,OAAO;YACL,QAAQ,EAAE,EAAE,GAAG,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;YACnD,WAAW;YACX,KAAK;YACL,SAAS;SACV,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;AAClE,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { Context } from "react";
2
+ import type { Analytics, ModuleContextKit } from "@stapel/core";
3
+ import type { CategoriesApi } from "../api/categoriesApi.js";
4
+ import type { CategoriesRuntime } from "./runtime.js";
5
+ export declare const CategoriesRuntimeContext: Context<CategoriesRuntime | null>;
6
+ export declare const useCategoriesRuntime: () => CategoriesRuntime;
7
+ export declare const useCategoriesApi: () => CategoriesApi;
8
+ export declare const useCategoriesAnalytics: () => Analytics | null;
9
+ /** @internal Re-exported as `<CategoriesProvider>` from `headless/`. */
10
+ export declare const ModuleProvider: ModuleContextKit<CategoriesRuntime>["Provider"];
11
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/model/context.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAErC,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAYtD,eAAO,MAAM,wBAAwB,EAAE,OAAO,CAAC,iBAAiB,GAAG,IAAI,CACnD,CAAC;AAErB,eAAO,MAAM,oBAAoB,EAAE,MAAM,iBAAkC,CAAC;AAE5E,eAAO,MAAM,gBAAgB,EAAE,MAAM,aAA0B,CAAC;AAEhE,eAAO,MAAM,sBAAsB,EAAE,MAAM,SAAS,GAAG,IAAuB,CAAC;AAE/E,wEAAwE;AACxE,eAAO,MAAM,cAAc,EAAE,gBAAgB,CAAC,iBAAiB,CAAC,CAAC,UAAU,CAC7D,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { createModuleContext } from "@stapel/core";
2
+ /**
3
+ * The wired CategoriesRuntime shared through React context by
4
+ * `<CategoriesProvider>`. Hooks in `model/` and `headless/` read the
5
+ * singletons from here. One reviewed copy of this plumbing lives in
6
+ * `@stapel/core` (`createModuleContext`, slim wave §21/S2); this module binds
7
+ * it under the pair's public names.
8
+ */
9
+ const kit = createModuleContext("Categories");
10
+ export const CategoriesRuntimeContext = kit.RuntimeContext;
11
+ export const useCategoriesRuntime = kit.useRuntime;
12
+ export const useCategoriesApi = kit.useApi;
13
+ export const useCategoriesAnalytics = kit.useAnalytics;
14
+ /** @internal Re-exported as `<CategoriesProvider>` from `headless/`. */
15
+ export const ModuleProvider = kit.Provider;
16
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/model/context.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAKnD;;;;;;GAMG;AACH,MAAM,GAAG,GACP,mBAAmB,CAAoB,YAAY,CAAC,CAAC;AAEvD,MAAM,CAAC,MAAM,wBAAwB,GACnC,GAAG,CAAC,cAAc,CAAC;AAErB,MAAM,CAAC,MAAM,oBAAoB,GAA4B,GAAG,CAAC,UAAU,CAAC;AAE5E,MAAM,CAAC,MAAM,gBAAgB,GAAwB,GAAG,CAAC,MAAM,CAAC;AAEhE,MAAM,CAAC,MAAM,sBAAsB,GAA2B,GAAG,CAAC,YAAY,CAAC;AAE/E,wEAAwE;AACxE,MAAM,CAAC,MAAM,cAAc,GACzB,GAAG,CAAC,QAAQ,CAAC"}