@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
package/CHANGELOG.md ADDED
@@ -0,0 +1 @@
1
+ # @stapel/categories-react
package/MODULE.md ADDED
@@ -0,0 +1,194 @@
1
+ # @stapel/categories-react — module guide
2
+
3
+ Pairs with **stapel-categories 0.6.0** (`>=0.6 <0.7`), 23 paths under
4
+ `/categories/api/v1/`. Contract sources: the module's own
5
+ `docs/{schema,errors,flows}.json`, pinned in `contract-pins.json` and
6
+ regenerated under `pnpm gen:check`.
7
+
8
+ ## Layers
9
+
10
+ ```
11
+ src/api/ categoriesApi.ts the five public reads; the one home of path strings
12
+ types.ts wire aliases, and the one hand-mirrored field
13
+ generated/ openapi-typescript, drift-gated
14
+ src/catalog/ pks.ts treenode's comma-joined pk columns
15
+ tree.ts flat rows → tree, slug → node, breadcrumbs
16
+ sync.ts the delta protocol as pure transitions
17
+ labels.ts key-or-literal, per row
18
+ src/model/ runtime · context · queryKeys · catalogStore · catalogSync · queries
19
+ src/headless/ CategoriesProvider · CategoryTree · CategoryBreadcrumbs
20
+ CategoryCarousel · CategoryPicker · CategoryFeatures
21
+ src/default/ the antd skin, `./default` subpath
22
+ src/i18n/ keys · ru · es · errorsMap + generated/
23
+ src/nav/ manifest.ts
24
+ ```
25
+
26
+ `catalog/` is pure: no React, no fetch, no storage. That is what lets the tree
27
+ assembly and the whole delta protocol be tested without a DOM, and what lets an
28
+ SSR render call them directly.
29
+
30
+ There is **no `flows/` layer**: `stapel-categories` annotates no `@flow_step`
31
+ and its `docs/flows.json` is `[]`, so the pair is not wired into `gen:flows`
32
+ (spec §8.1 — a flows layer only where a multi-step flow exists).
33
+
34
+ ## The three server facts this package exists to absorb
35
+
36
+ ### 1. There is no tree endpoint
37
+
38
+ Three shapes could serve a catalogue, and the right one for a storefront is the
39
+ paginated list: `GET /categories/` with `RevisionPagination`, envelope
40
+ `{pagination, revisions, results}`, rows ordered by `revision`. Each row
41
+ carries `tn_parent`, `tn_priority`, `tn_ancestors_pks`, `tn_children_pks`.
42
+
43
+ The ancestry columns are django-treenode `TextField`s holding **comma-joined
44
+ pks** (`treenode/utils.py`: `PKS_SEPARATOR = ","`), typed `string` in the
45
+ schema, `""` for a root. `parseTreenodePks` exists because the storefront spec
46
+ described them as arrays; a `JSON.parse` there fails, and a naive
47
+ `Number(column)` yields `NaN`, which fails every `===` against a real id and
48
+ therefore reads as "the parent is missing".
49
+
50
+ `buildCategoryTree` filters two flags the endpoint does not:
51
+
52
+ - `deleted` — the list's `include_deleted` defaults to **true**, and the delta
53
+ protocol needs it that way.
54
+ - `active` — filtered server-side only by `/carousel/`. It is the storefront's
55
+ visibility switch, so the public tree drops inactive rows by default and
56
+ keeping them is an explicit option.
57
+
58
+ An unreachable row (a parent cycle in authored data) is promoted to a root
59
+ rather than dropped: a broken branch must be visible, not absent.
60
+
61
+ ### 2. There is no slug lookup
62
+
63
+ `lookup_field` is never overridden and the list has no slug filter. `/c/:slug`
64
+ resolves against the synced tree. `Category.slug` is `unique=True` on the
65
+ model, so a collision cannot come from the database — it can only come from a
66
+ stale row in the client's own snapshot, and first-in-display-order wins
67
+ deterministically.
68
+
69
+ ### 3. Names are translation keys
70
+
71
+ Covered in the README and in `catalog/labels.ts` at length. The short version:
72
+ `translate()` runs the `DISPLAY_TRANSLATOR` seam, its default is the identity
73
+ function, and **no serializer calls it** — the only two call sites are
74
+ `Category.__str__` and the admin's memoized feature label. `translatable`
75
+ (category) and `translate` (feature) say per row whether a string is a key.
76
+
77
+ `featureOptionsAreKeys` carries the third case, which is the one that bites
78
+ both ways: option labels are keys only under `translate: "all"` and only while
79
+ `config.translatable_options` is not false. Translating an opted-out option
80
+ shows the raw key; not translating an opted-in one shows the raw key too.
81
+
82
+ ## The delta protocol, and the two rules the docs omit
83
+
84
+ The module's viewset docstring gives four steps: full GET, store
85
+ `revisions.global_max`, `?min_revision=`, drop `deleted: true`. Implemented
86
+ literally, that protocol has two holes.
87
+
88
+ **`deleted_ids` beats scanning rows.** `revisions.deleted_ids` is computed as
89
+ `filter(deleted=True, revision__gt=min_revision)` — unpaginated, over the whole
90
+ table, and reflecting the CURRENT flag (so a category deleted and then restored
91
+ is simply absent from it). The `deleted: true` rows, by contrast, are
92
+ paginated: a walk that stops early misses one. Both channels are applied;
93
+ `deleted_ids` is the complete one. On a full sync it is `[]` by construction —
94
+ the server only computes it when `min_revision` was sent — which is why
95
+ dropping `deleted: true` rows still matters.
96
+
97
+ **A multi-page walk must pin its window.** `paginate_queryset` filters at
98
+ request time and orders by `revision`; `global_max` is `Max(revision)` over the
99
+ table at response time. A write landing between two pages shifts every later
100
+ boundary. `max_revision` exists for exactly this and the documented flow never
101
+ mentions it: `nextPageRequest` pins the window to the first page's
102
+ `global_max`, and `applyCategoryPage`'s `cursorLimit` stops the cursor
103
+ recording anything past it. Whatever was written during the walk is picked up
104
+ by the next delta.
105
+
106
+ Beyond that, two loop guards:
107
+
108
+ - a **page budget** (`MAX_SYNC_PAGES`, 1000 — a million categories at the
109
+ server's max page size), because a server-reported `has_next` bug is an
110
+ infinite request loop against production, not a slow render; and
111
+ - **publish-on-completion**: a truncated walk rewinds its cursor to the stored
112
+ one, so the worst case is one repeated full walk rather than a permanently
113
+ incomplete tree.
114
+
115
+ The FULL/DELTA distinction is expressed as *what the accumulator starts from*
116
+ (`EMPTY_SNAPSHOT` vs the stored snapshot), not as a flag on the merge. A flag
117
+ invites both silent failures: a delta treated as full empties the catalogue, a
118
+ full treated as delta resurrects rows deleted while the client was away.
119
+
120
+ ## Why the catalogue is app-scoped, not user-scoped
121
+
122
+ `createRepository` is the only sanctioned client-side persistence primitive,
123
+ and `scope` is not a size decision here. A user-scoped repository is encrypted
124
+ with the per-session key and wiped at logout with no opt-out — both wrong for a
125
+ category tree, which is public content identical for every visitor and for a
126
+ visitor with no session at all. App scope also means the store never asks for a
127
+ `SessionManager`, so it works on a page with no auth wired.
128
+
129
+ Every read and write is wrapped: a private window, cleared site data or a
130
+ snapshot written by an older shape all degrade to "no snapshot". A cold sync
131
+ costs one request; a cache that throws costs the page.
132
+
133
+ ## The bridge to @stapel/attributes-react
134
+
135
+ `GET /categories/{id}/features/` is where the two packages meet.
136
+ `CategoryFeature` **is** attributes-react's `FeatureDef` — not a parallel type
137
+ — so `bag.features` feeds `<FeatureFields>`, `unsupportedTypeGate`,
138
+ `mirrorValidate` and `formatFeatureValue` unmodified. `test/features.test.tsx`
139
+ asserts that end to end: the rows this pair fetches go through
140
+ `unsupportedTypes` and it names the one type no builtin editor covers.
141
+
142
+ This pair adds exactly three decisions on top and no more: whether the name is
143
+ a key, whether the options are keys, and which rows are `show_as_badge` /
144
+ `show_at_title` projections. Value semantics — defaults, validation, formatting
145
+ — stay with the package that owns them.
146
+
147
+ ## Notes on the contract, recorded rather than worked around
148
+
149
+ 1. **`FeatureConfig`'s discriminator is malformed, and the generated types are
150
+ wrong because of it.** The schema declares
151
+ `discriminator: {propertyName: "type", mapping: {"null": ConvertibleUnitConfig}}`
152
+ — one bogus entry instead of the ten type slugs. openapi-typescript
153
+ therefore (a) strips the discriminator from every use site, emitting
154
+ `Omit<FeatureConfig, "type">`, and (b) re-adds a synthetic one per member,
155
+ so the generated `IntConfig` declares `type: "IntConfig"` where the wire
156
+ sends `"int"`. Feature configs are consequently typed through
157
+ attributes-react's hand-mirrored `FeatureConfig` (itself pinned against the
158
+ engine's generated golden corpus). **Upstream ask**: emit the ten slugs in
159
+ `discriminator.mapping`, and both halves disappear.
160
+ 2. **`FeatureCompact` carries no `revision`**, so the `/features/` collection's
161
+ `RevisionPagination` envelope always reports `revisions.min/max: null`. Not
162
+ on this pair's surface (a storefront reads features through a category), but
163
+ it makes the parallel feature tree un-syncable by the same protocol.
164
+ 3. **`POST {id}/validate-dto/` reads like a public helper and is not one.** The
165
+ viewset is `ReadOnlyOrStaff`, which allows safe methods to anyone and
166
+ everything else to staff — so this answers 403 to a visitor. It is absent
167
+ from `CategoriesApi` for that reason, not by oversight.
168
+ 4. **`include_deleted` is honoured** (`RevisionViewSetMixin.get_queryset`),
169
+ despite being documented on the paginator, which ignores it. Its default is
170
+ `true`.
171
+ 5. **No `translations/` directory.** 20 of the 62 registry codes can never come
172
+ from an upstream catalogue. They split by owner: the 42 cross-cutting
173
+ `stapel_core` codes are generated from core's catalogue, the 8
174
+ `stapel_categories` codes are authored in this pair's `./i18n/{ru,es}`, and
175
+ the 12 `stapel_attributes` codes stay with `@stapel/attributes-react`, which
176
+ already translates them. `test/i18n.test.ts` asserts over the UNION of the
177
+ two bundles a host registers, so nobody can "fix" a red test by copying
178
+ another package's twelve keys in and giving one refusal two sentences.
179
+
180
+ ## Tests
181
+
182
+ 119 in 9 files (115 in `test`, 4 in `test:pack`).
183
+
184
+ | File | What it holds down |
185
+ |---|---|
186
+ | `tree.test.ts` | flat rows → tree; priority order and its tie-break; both filters; orphan promotion; parent cycles; slug resolution incl. collisions; breadcrumbs cross-checked against the server's own `tn_ancestors_pks` |
187
+ | `sync.test.ts` | cold vs warm request; the `max_revision` pin; `deleted_ids` applied with no tombstone row present; cursor monotonicity and the window cap; snapshot parsing rejecting junk; the page budget and its cursor rewind |
188
+ | `labels.test.ts` | key vs literal for categories, features and option labels; a missing translation showing the key |
189
+ | `catalog.test.tsx` | loading / ready-empty / failed as three different sentences; "unknown slug" only after the catalogue loaded; the second mount asking for a delta |
190
+ | `features.test.tsx` | the attributes-react bridge, including `unsupportedTypes` over the un-reshaped payload |
191
+ | `skin.test.tsx` | the four `matchList` arms on screen; both blocked reasons named; searching without a request; ru copy |
192
+ | `i18n.test.ts` | every registry code resolving in en/ru/es over the union of the two bundles; ownership of the twenty un-catalogued keys; interpolation slots preserved |
193
+ | `pair.test.ts` | query-key namespace; the API surface being exactly the five public reads; nav ids, surfaces, routes and components |
194
+ | `prodBundlePurity.test.ts` | no demo/showcase code in the tarball |
package/README.md ADDED
@@ -0,0 +1,191 @@
1
+ # @stapel/categories-react
2
+
3
+ The frontend pair for **stapel-categories**: the catalogue tree a storefront
4
+ navigates by, kept fresh with the module's own revision-delta protocol instead
5
+ of being refetched on every page.
6
+
7
+ The backend has **no tree endpoint** and **no slug lookup**, and it serves
8
+ category names as **translation keys**. This package is where each of those
9
+ three facts is handled once, in the open, rather than rediscovered by every
10
+ host.
11
+
12
+ Business + state in the main entry, zero visual opinion; the antd skin lives
13
+ behind `./default`. Built on `@stapel/core` (typed client + `StapelApiError`
14
+ envelope, `LoadState`, `createRepository`, i18n engine, TanStack Query) and
15
+ `@stapel/attributes-react` (the feature schema's value types).
16
+
17
+ ## Install
18
+
19
+ ```
20
+ pnpm add @stapel/categories-react @stapel/core @stapel/attributes-react @tanstack/react-query react
21
+ # for the default skin:
22
+ pnpm add antd @stapel/tokens-antd
23
+ ```
24
+
25
+ ## A catalogue, in six lines
26
+
27
+ **No session, no workspace id, no auth client** — every endpoint this pair
28
+ calls is a safe method under `ReadOnlyOrStaff`, so the catalogue renders for a
29
+ visitor who will never sign in:
30
+
31
+ ```tsx
32
+ import { createCategoriesRuntime, CategoriesProvider } from "@stapel/categories-react";
33
+ import { CatalogPage } from "@stapel/categories-react/default";
34
+
35
+ const runtime = createCategoriesRuntime({ baseUrl: "/categories/api/v1/" });
36
+
37
+ export function CatalogRoute() {
38
+ return (
39
+ <CategoriesProvider runtime={runtime}>
40
+ <CatalogPage />
41
+ </CategoriesProvider>
42
+ );
43
+ }
44
+ ```
45
+
46
+ ## The client builds the tree, because the server has none
47
+
48
+ `GET /categories/` returns **flat rows ordered by `revision`** — a sync feed,
49
+ not a menu. Ancestry rides along in django-treenode's columns, which are
50
+ **comma-joined primary-key strings**, not arrays:
51
+
52
+ ```jsonc
53
+ { "id": 4, "slug": "used-phones", "name": "category.used_phones",
54
+ "tn_parent": 2, "tn_ancestors_pks": "1,2", "tn_children_pks": "" }
55
+ ```
56
+
57
+ `buildCategoryTree(rows)` assembles the hierarchy, orders siblings by
58
+ `tn_priority` descending (ties by id, so a catalogue that never set priority
59
+ does not reshuffle between renders), and drops two kinds of row the endpoint
60
+ happily serves: soft-deleted ones, and **inactive** ones — `active` is filtered
61
+ server-side only by `/carousel/`.
62
+
63
+ A row whose parent is missing becomes a root rather than disappearing. Dropping
64
+ it would delete a live branch of the catalogue from the menu and give nobody a
65
+ reason.
66
+
67
+ ## The catalogue is synced by delta, not refetched
68
+
69
+ The module documents the protocol on its own viewset: full GET → store
70
+ `revisions.global_max` → later `?min_revision=<stored>` → drop what comes back
71
+ `deleted: true`. This package implements it into an **app-scoped repository**
72
+ (`createRepository(..., { scope: "app" })` — the one sanctioned persistence
73
+ primitive), so a second page costs one small delta request instead of the whole
74
+ catalogue.
75
+
76
+ Two rules the documentation does not state are implemented anyway:
77
+
78
+ - **`revisions.deleted_ids` is the complete tombstone channel.** The
79
+ `deleted: true` rows are paginated, so one can sit on a page a short walk
80
+ never reached; `deleted_ids` is computed unpaginated over the whole table.
81
+ Both are applied; the list is the one that is complete.
82
+ - **A multi-page walk pins `max_revision`.** Pages are ordered by revision and
83
+ filtered at request time, so a write landing between page 1 and page 2 shifts
84
+ every later boundary and the walk skips a row. The window is pinned to the
85
+ `global_max` the first page reported, and the cursor never records reading
86
+ past it.
87
+
88
+ A walk stopped by its page budget reports `truncated` and **rewinds its
89
+ cursor**: the rows are still shown, but the next sync redoes the walk rather
90
+ than sitting a delta on top of a catalogue that was never fully read.
91
+
92
+ ## `/c/:slug` is resolved on the client
93
+
94
+ `CategoryViewSet` never overrides `lookup_field` and the list endpoint has no
95
+ slug filter, so `GET /categories/<slug>/` is a 404 and `?slug=` is ignored. The
96
+ storefront's `/c/:slug` resolves against the synced tree —
97
+ `resolveCategorySlug(index, slug)` — which is a second, independent reason the
98
+ tree is cached.
99
+
100
+ A slug is not "unknown" until the catalogue has actually loaded. Rendering
101
+ "there is no category here" during a sync shows a 404 for a page that exists.
102
+
103
+ ## Names are translation KEYS
104
+
105
+ This is the fact most likely to be got wrong, so the package states it in the
106
+ types instead of in a comment.
107
+
108
+ `stapel-categories` stores keys and never owns a catalogue. Its
109
+ `DISPLAY_TRANSLATOR` seam is called from `Category.__str__` and the admin's
110
+ label cache — **no serializer calls it**. So a category arrives as
111
+ `{"name": "category.electronics"}` even on a deployment with a real translator
112
+ configured, there is no `?lang=`, and `GET /translation-keys/` is a
113
+ service-only extraction feed for translators, not a resolver.
114
+
115
+ Which strings are keys is declared per row, and the pair reads the flag rather
116
+ than guessing:
117
+
118
+ ```tsx
119
+ categoryLabel(category) // { kind: "key" | "literal", value } ← `translatable`
120
+ featureLabel(feature) // `translate: "none"` makes the name a literal
121
+ featureOptionsAreKeys(f) // `translate: "all"` AND config.translatable_options
122
+ renderCategoryLabel(label, t)
123
+ ```
124
+
125
+ The resolution goes through the **host's** i18n engine, and this package ships
126
+ no category names: a catalogue is a deployment's content, not a library's
127
+ chrome. When a key does not resolve, the key is shown — deliberately. A visible
128
+ `category.electronics` gets fixed; a prettified "Electronics" invented by the
129
+ library ships for a year in the wrong language.
130
+
131
+ ## The feature schema, and where attributes-react takes over
132
+
133
+ `GET /categories/{id}/features/` resolves inheritance and order server-side and
134
+ returns the polymorphic attributes config **verbatim** — not through
135
+ `get_config_with_defaults()`, so an absent key means "the type's default", and
136
+ `@stapel/attributes-react` owns those defaults. `<CategoryFeatures>` hands the
137
+ rows over unmodified:
138
+
139
+ ```tsx
140
+ <CategoryFeatures categoryId={id}>
141
+ {(bag) => (
142
+ <FeatureFields features={bag.features} values={values} onChange={set} />
143
+ )}
144
+ </CategoryFeatures>
145
+ ```
146
+
147
+ The same `bag.features` is what `@stapel/search-react`'s facet panel takes as
148
+ `categoryFeatures` to caption facet values.
149
+
150
+ ## Surface
151
+
152
+ | Export | What it is |
153
+ |---|---|
154
+ | `createCategoriesRuntime` / `CategoriesProvider` | wiring |
155
+ | `useCategoryCatalog` | the delta-synced tree, one hook, mounted once |
156
+ | `useCategoryChildren` / `useCategoryCarousel` / `useCategoryFeatures` / `useCategoriesRevision` | the four direct reads |
157
+ | `buildCategoryTree` / `resolveCategorySlug` / `categoryBreadcrumbs` / `parseTreenodePks` | pure tree assembly |
158
+ | `applyCategoryPage` / `firstPageRequest` / `nextPageRequest` / `syncCatalog` | the delta protocol, testable without React |
159
+ | `categoryLabel` / `featureLabel` / `renderCategoryLabel` | the translation-key answer |
160
+ | `<CategoryTree>` `<CategoryBreadcrumbs>` `<CategoryCarousel>` `<CategoryPicker>` `<CategoryFeatures>` | headless bags |
161
+ | `/default`: `CatalogPage` `CategoryPage` `CategoryTreePane` `CategoryBreadcrumbsBar` `CategoryCarouselStrip` `CategoryPickerField` `CategoryFeatureList` | the antd skin |
162
+
163
+ Nav entries: `categories.catalog` → `/c`, `categories.category` → `/c/:slug`,
164
+ both `surface: "public"`. `/` is **not** claimed — the storefront's landing is
165
+ categories *plus* search, and a composed route belongs to the container that
166
+ composes it.
167
+
168
+ `<CategoryPage>` takes `renderListings` for the same reason: the results half of
169
+ `/c/:slug` belongs to another pair, handed in rather than imported across the
170
+ L2 layer.
171
+
172
+ ## Not in this version
173
+
174
+ The catalogue **admin** — create/update/delete, `bulk_add`, `bulk-commands`,
175
+ `undelete`, `convert-type` and the four feature-editor operations — is not on
176
+ this pair's surface. Every one of them is `IsStaffUser`, and the storefront
177
+ wave leaves them to Django admin. `POST {id}/validate-dto/` is absent for the
178
+ same reason: DRF treats it as a write, so it answers 403 to exactly the
179
+ visitors who would want it; the compose form uses attributes-react's
180
+ client-side mirror plus the server's verdict on publish. `GET
181
+ /translation-keys/` is `IsServiceRequest`. `GET /data.json` is a second sync
182
+ protocol and one tested protocol beats two half-tested ones.
183
+
184
+ Nothing is hidden: `manifest.json` lists the whole contract.
185
+
186
+ ## More
187
+
188
+ - [`MODULE.md`](./MODULE.md) — the module guide: layers, and every contract
189
+ note recorded rather than worked around.
190
+ - [`llms.txt`](./llms.txt) — the generated surface slice for a coding harness.
191
+ - [`manifest.json`](./manifest.json) — the machine-readable self-description.
@@ -0,0 +1,121 @@
1
+ import type { StapelClient } from "@stapel/core";
2
+ import type { Category, CategoryFeature, CategoryListParams, CategoryPage, MaxRevision } from "./types.js";
3
+ /**
4
+ * The pair's typed operation surface — one method per stapel-categories
5
+ * endpoint a storefront may call, bound to the injected {@link StapelClient}
6
+ * (the per-module override seam of frontend-standard §7.2). Paths are relative
7
+ * to the runtime's `baseUrl` (`/categories/api/v1/`).
8
+ *
9
+ * ── Five reads, and the eighteen operations that are NOT here ──────────────
10
+ *
11
+ * The contract has 23 paths. Every one of them is on `ReadOnlyOrStaff`, which
12
+ * means SAFE methods are open to anonymous callers and everything else needs
13
+ * `is_staff` — so the split below is not a taste call, it is the permission
14
+ * boundary drawn on the surface:
15
+ *
16
+ * - **Public reads, and this pair's whole surface**: the category list (the
17
+ * delta-sync source), `children`, `carousel`, `{id}/features`, and
18
+ * `revision`.
19
+ * - **Staff writes**: create / update / delete, `bulk_add`,
20
+ * `bulk-commands`, `undelete`, `convert-type`, and the four
21
+ * `feature-editor*` operations. These are the catalogue admin, which the
22
+ * storefront wave leaves to Django admin (spec §4.3) — a pair that exposed
23
+ * them would invite a public screen to call something only staff may.
24
+ * - **`POST {id}/validate-dto/` is a WRITE in DRF's eyes**, so despite
25
+ * reading like a public helper it answers 403 to a visitor. The listing
26
+ * compose flow validates against the same rules with
27
+ * `@stapel/attributes-react`'s client-side mirror plus the server's verdict
28
+ * on publish; asking this endpoint would fail for exactly the people who
29
+ * need it.
30
+ * - **`GET {id}/validate-configs/`** is a catalogue-health read for an
31
+ * operator, not a storefront read.
32
+ * - **`GET translation-keys/`** is `IsServiceRequest` (service-to-service):
33
+ * it is the extraction feed for translators, and it returns KEYS with
34
+ * admin refs — never resolved labels. See `catalog/labels.ts`.
35
+ * - **`GET data.json/`** requires a `?revision=` cache-buster and hands back
36
+ * the whole table in one response. It is a legitimate alternative to the
37
+ * paged walk, but it is a *second* sync protocol, and the module documents
38
+ * the paged one. One protocol, tested, beats two, half-tested.
39
+ * - The parallel `/features/` collection (list, retrieve, create, …) is the
40
+ * feature tree in its own right — an admin axis. A storefront reads
41
+ * features THROUGH a category, which is the only place inheritance and
42
+ * order are resolved.
43
+ *
44
+ * Nothing is hidden: `manifest.json` lists the whole contract.
45
+ *
46
+ * These operations will be GENERATED from schema.json operationIds by gen-api
47
+ * v2 (task `core-typed-ops`); until then they are hand-authored here (the ONE
48
+ * legal home of path strings — `stapel/no-string-paths` §2.3 carve-out).
49
+ */
50
+ export interface CategoriesApi {
51
+ readonly client: StapelClient;
52
+ /**
53
+ * One page of the revision-paginated category list — the source the whole
54
+ * tree is assembled from, and the only endpoint that reports deletions.
55
+ *
56
+ * Rows are ordered by `revision`, NOT by tree position: this is a sync feed,
57
+ * not a menu. `revisions.global_max` is the cursor to store;
58
+ * `revisions.deleted_ids` is the authoritative tombstone list for a delta
59
+ * (see `catalog/sync.ts` for why it beats scanning `deleted: true` rows).
60
+ */
61
+ list(params?: CategoryListParams, options?: {
62
+ readonly signal?: AbortSignal;
63
+ }): Promise<CategoryPage>;
64
+ /**
65
+ * Non-deleted direct children of one category, `tn_priority` descending.
66
+ *
67
+ * Redundant with the synced tree by construction, and deliberately kept: a
68
+ * host that wants one branch without paying for the catalogue (an SSR
69
+ * category page, a lazily-expanded admin-ish picker) has an endpoint for it,
70
+ * and it is the one place the server does the `deleted` filtering.
71
+ */
72
+ children(id: number, options?: {
73
+ readonly signal?: AbortSignal;
74
+ }): Promise<readonly Category[]>;
75
+ /**
76
+ * The carousel strip: `active` AND `carousel_enabled`, `tn_priority`
77
+ * descending, cached server-side and sent with
78
+ * `Cache-Control: public, max-age` — so this one is safe to call on every
79
+ * landing render.
80
+ */
81
+ carousel(options?: {
82
+ readonly signal?: AbortSignal;
83
+ }): Promise<readonly Category[]>;
84
+ /**
85
+ * A category's resolved feature schema: its own features plus every ancestor's,
86
+ * deduplicated by SLUG (an `inherit` override is a new row sharing the
87
+ * parent's slug, and the version nearest this category wins), ordered by the
88
+ * category's own `CategoryFeature.order` first.
89
+ *
90
+ * This is what the listing compose form draws with
91
+ * `@stapel/attributes-react` and what labels a search facet. `config`
92
+ * arrives verbatim — defaults are the reader's job, and attributes-react
93
+ * owns them.
94
+ */
95
+ features(id: number, options?: {
96
+ readonly signal?: AbortSignal;
97
+ }): Promise<readonly CategoryFeature[]>;
98
+ /**
99
+ * The table's current maximum revision, without transferring any rows.
100
+ *
101
+ * The cheap freshness probe: a stored snapshot whose cursor already equals
102
+ * this needs no delta request at all.
103
+ */
104
+ revision(options?: {
105
+ readonly signal?: AbortSignal;
106
+ }): Promise<MaxRevision>;
107
+ }
108
+ /**
109
+ * The wire query object for one list request.
110
+ *
111
+ * Exported because the query KEY is built from it, so the key and the request
112
+ * are the same value by construction.
113
+ *
114
+ * `include_deleted` is sent ONLY when the caller asked for a value, and the
115
+ * server's own default is `true`. Sending it always would be harmless; leaving
116
+ * it out when unasked keeps the cache key of a plain read from depending on
117
+ * this pair's opinion about a server default.
118
+ */
119
+ export declare function categoryListParams(params?: CategoryListParams): Record<string, string | number | boolean | undefined>;
120
+ export declare function createCategoriesApi(client: StapelClient): CategoriesApi;
121
+ //# sourceMappingURL=categoriesApi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"categoriesApi.d.ts","sourceRoot":"","sources":["../../src/api/categoriesApi.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EACV,QAAQ,EACR,eAAe,EACf,kBAAkB,EAClB,YAAY,EACZ,WAAW,EACZ,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAE9B;;;;;;;;OAQG;IACH,IAAI,CACF,MAAM,CAAC,EAAE,kBAAkB,EAC3B,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAC1C,OAAO,CAAC,YAAY,CAAC,CAAC;IAEzB;;;;;;;OAOG;IACH,QAAQ,CACN,EAAE,EAAE,MAAM,EACV,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAC1C,OAAO,CAAC,SAAS,QAAQ,EAAE,CAAC,CAAC;IAEhC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE;QACjB,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;KAC/B,GAAG,OAAO,CAAC,SAAS,QAAQ,EAAE,CAAC,CAAC;IAEjC;;;;;;;;;;OAUG;IACH,QAAQ,CACN,EAAE,EAAE,MAAM,EACV,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAC1C,OAAO,CAAC,SAAS,eAAe,EAAE,CAAC,CAAC;IAEvC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE;QACjB,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;KAC/B,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CAC1B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,GAAE,kBAAuB,GAC9B,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAUvD;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,GAAG,aAAa,CA8BvE"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The wire query object for one list request.
3
+ *
4
+ * Exported because the query KEY is built from it, so the key and the request
5
+ * are the same value by construction.
6
+ *
7
+ * `include_deleted` is sent ONLY when the caller asked for a value, and the
8
+ * server's own default is `true`. Sending it always would be harmless; leaving
9
+ * it out when unasked keeps the cache key of a plain read from depending on
10
+ * this pair's opinion about a server default.
11
+ */
12
+ export function categoryListParams(params = {}) {
13
+ const query = {};
14
+ if (params.minRevision !== undefined)
15
+ query["min_revision"] = params.minRevision;
16
+ if (params.maxRevision !== undefined)
17
+ query["max_revision"] = params.maxRevision;
18
+ if (params.includeDeleted !== undefined) {
19
+ query["include_deleted"] = params.includeDeleted;
20
+ }
21
+ if (params.page !== undefined)
22
+ query["page"] = params.page;
23
+ if (params.pageSize !== undefined)
24
+ query["page_size"] = params.pageSize;
25
+ return query;
26
+ }
27
+ export function createCategoriesApi(client) {
28
+ return {
29
+ client,
30
+ list: (params, options) => client.get("/categories/", {
31
+ query: categoryListParams(params),
32
+ ...(options?.signal !== undefined ? { signal: options.signal } : {}),
33
+ }),
34
+ children: (id, options) => client.get(`/categories/${String(id)}/children/`, {
35
+ ...(options?.signal !== undefined ? { signal: options.signal } : {}),
36
+ }),
37
+ carousel: (options) => client.get("/categories/carousel/", {
38
+ ...(options?.signal !== undefined ? { signal: options.signal } : {}),
39
+ }),
40
+ features: (id, options) => client.get(`/categories/${String(id)}/features/`, {
41
+ ...(options?.signal !== undefined ? { signal: options.signal } : {}),
42
+ }),
43
+ revision: (options) => client.get("/categories/revision/", {
44
+ ...(options?.signal !== undefined ? { signal: options.signal } : {}),
45
+ }),
46
+ };
47
+ }
48
+ //# sourceMappingURL=categoriesApi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"categoriesApi.js","sourceRoot":"","sources":["../../src/api/categoriesApi.ts"],"names":[],"mappings":"AA2HA;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kBAAkB,CAChC,SAA6B,EAAE;IAE/B,MAAM,KAAK,GAA0D,EAAE,CAAC;IACxE,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS;QAAE,KAAK,CAAC,cAAc,CAAC,GAAG,MAAM,CAAC,WAAW,CAAC;IACjF,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS;QAAE,KAAK,CAAC,cAAc,CAAC,GAAG,MAAM,CAAC,WAAW,CAAC;IACjF,IAAI,MAAM,CAAC,cAAc,KAAK,SAAS,EAAE,CAAC;QACxC,KAAK,CAAC,iBAAiB,CAAC,GAAG,MAAM,CAAC,cAAc,CAAC;IACnD,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;QAAE,KAAK,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC;IAC3D,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS;QAAE,KAAK,CAAC,WAAW,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC;IACxE,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,MAAoB;IACtD,OAAO;QACL,MAAM;QAEN,IAAI,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,CACxB,MAAM,CAAC,GAAG,CAAC,cAAc,EAAE;YACzB,KAAK,EAAE,kBAAkB,CAAC,MAAM,CAAC;YACjC,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACrE,CAAC;QAEJ,QAAQ,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,CACxB,MAAM,CAAC,GAAG,CAAC,eAAe,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE;YAChD,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACrE,CAAC;QAEJ,QAAQ,EAAE,CAAC,OAAO,EAAE,EAAE,CACpB,MAAM,CAAC,GAAG,CAAC,uBAAuB,EAAE;YAClC,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACrE,CAAC;QAEJ,QAAQ,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,CACxB,MAAM,CAAC,GAAG,CAAC,eAAe,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE;YAChD,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACrE,CAAC;QAEJ,QAAQ,EAAE,CAAC,OAAO,EAAE,EAAE,CACpB,MAAM,CAAC,GAAG,CAAC,uBAAuB,EAAE;YAClC,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACrE,CAAC;KACL,CAAC;AACJ,CAAC"}