@stapel/listings-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 (225) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/MODULE.md +191 -0
  3. package/README.md +169 -0
  4. package/dist/api/generated/schema.d.ts +1253 -0
  5. package/dist/api/generated/schema.d.ts.map +1 -0
  6. package/dist/api/generated/schema.js +2 -0
  7. package/dist/api/generated/schema.js.map +1 -0
  8. package/dist/api/listingsApi.d.ts +145 -0
  9. package/dist/api/listingsApi.d.ts.map +1 -0
  10. package/dist/api/listingsApi.js +38 -0
  11. package/dist/api/listingsApi.js.map +1 -0
  12. package/dist/api/types.d.ts +156 -0
  13. package/dist/api/types.d.ts.map +1 -0
  14. package/dist/api/types.js +37 -0
  15. package/dist/api/types.js.map +1 -0
  16. package/dist/default/ErrorAlert.d.ts +9 -0
  17. package/dist/default/ErrorAlert.d.ts.map +1 -0
  18. package/dist/default/ErrorAlert.js +27 -0
  19. package/dist/default/ErrorAlert.js.map +1 -0
  20. package/dist/default/FavoritesPane.d.ts +18 -0
  21. package/dist/default/FavoritesPane.d.ts.map +1 -0
  22. package/dist/default/FavoritesPane.js +28 -0
  23. package/dist/default/FavoritesPane.js.map +1 -0
  24. package/dist/default/ListingCard.d.ts +42 -0
  25. package/dist/default/ListingCard.d.ts.map +1 -0
  26. package/dist/default/ListingCard.js +37 -0
  27. package/dist/default/ListingCard.js.map +1 -0
  28. package/dist/default/ListingComposerPage.d.ts +46 -0
  29. package/dist/default/ListingComposerPage.d.ts.map +1 -0
  30. package/dist/default/ListingComposerPage.js +99 -0
  31. package/dist/default/ListingComposerPage.js.map +1 -0
  32. package/dist/default/ListingDetailPane.d.ts +33 -0
  33. package/dist/default/ListingDetailPane.d.ts.map +1 -0
  34. package/dist/default/ListingDetailPane.js +48 -0
  35. package/dist/default/ListingDetailPane.js.map +1 -0
  36. package/dist/default/ListingPhoto.d.ts +25 -0
  37. package/dist/default/ListingPhoto.d.ts.map +1 -0
  38. package/dist/default/ListingPhoto.js +20 -0
  39. package/dist/default/ListingPhoto.js.map +1 -0
  40. package/dist/default/MyListingsPane.d.ts +25 -0
  41. package/dist/default/MyListingsPane.d.ts.map +1 -0
  42. package/dist/default/MyListingsPane.js +84 -0
  43. package/dist/default/MyListingsPane.js.map +1 -0
  44. package/dist/default/StatusTags.d.ts +31 -0
  45. package/dist/default/StatusTags.d.ts.map +1 -0
  46. package/dist/default/StatusTags.js +40 -0
  47. package/dist/default/StatusTags.js.map +1 -0
  48. package/dist/default/icons.d.ts +16 -0
  49. package/dist/default/icons.d.ts.map +1 -0
  50. package/dist/default/icons.js +5 -0
  51. package/dist/default/icons.js.map +1 -0
  52. package/dist/default/index.d.ts +35 -0
  53. package/dist/default/index.d.ts.map +1 -0
  54. package/dist/default/index.js +26 -0
  55. package/dist/default/index.js.map +1 -0
  56. package/dist/default/theme.d.ts +25 -0
  57. package/dist/default/theme.d.ts.map +1 -0
  58. package/dist/default/theme.js +40 -0
  59. package/dist/default/theme.js.map +1 -0
  60. package/dist/default/types.d.ts +12 -0
  61. package/dist/default/types.d.ts.map +1 -0
  62. package/dist/default/types.js +2 -0
  63. package/dist/default/types.js.map +1 -0
  64. package/dist/flows/registry.d.ts +35 -0
  65. package/dist/flows/registry.d.ts.map +1 -0
  66. package/dist/flows/registry.js +33 -0
  67. package/dist/flows/registry.js.map +1 -0
  68. package/dist/headless/Favorites.d.ts +52 -0
  69. package/dist/headless/Favorites.d.ts.map +1 -0
  70. package/dist/headless/Favorites.js +78 -0
  71. package/dist/headless/Favorites.js.map +1 -0
  72. package/dist/headless/ListingActions.d.ts +30 -0
  73. package/dist/headless/ListingActions.d.ts.map +1 -0
  74. package/dist/headless/ListingActions.js +54 -0
  75. package/dist/headless/ListingActions.js.map +1 -0
  76. package/dist/headless/ListingComposer.d.ts +133 -0
  77. package/dist/headless/ListingComposer.d.ts.map +1 -0
  78. package/dist/headless/ListingComposer.js +246 -0
  79. package/dist/headless/ListingComposer.js.map +1 -0
  80. package/dist/headless/ListingDetail.d.ts +81 -0
  81. package/dist/headless/ListingDetail.d.ts.map +1 -0
  82. package/dist/headless/ListingDetail.js +101 -0
  83. package/dist/headless/ListingDetail.js.map +1 -0
  84. package/dist/headless/ListingsProvider.d.ts +17 -0
  85. package/dist/headless/ListingsProvider.d.ts.map +1 -0
  86. package/dist/headless/ListingsProvider.js +16 -0
  87. package/dist/headless/ListingsProvider.js.map +1 -0
  88. package/dist/headless/MyListings.d.ts +44 -0
  89. package/dist/headless/MyListings.d.ts.map +1 -0
  90. package/dist/headless/MyListings.js +82 -0
  91. package/dist/headless/MyListings.js.map +1 -0
  92. package/dist/headless/useMandateGate.d.ts +28 -0
  93. package/dist/headless/useMandateGate.d.ts.map +1 -0
  94. package/dist/headless/useMandateGate.js +39 -0
  95. package/dist/headless/useMandateGate.js.map +1 -0
  96. package/dist/i18n/errorsMap.d.ts +34 -0
  97. package/dist/i18n/errorsMap.d.ts.map +1 -0
  98. package/dist/i18n/errorsMap.js +45 -0
  99. package/dist/i18n/errorsMap.js.map +1 -0
  100. package/dist/i18n/es.d.ts +21 -0
  101. package/dist/i18n/es.d.ts.map +1 -0
  102. package/dist/i18n/es.js +146 -0
  103. package/dist/i18n/es.js.map +1 -0
  104. package/dist/i18n/generated/errors.es.gen.d.ts +16 -0
  105. package/dist/i18n/generated/errors.es.gen.d.ts.map +1 -0
  106. package/dist/i18n/generated/errors.es.gen.js +58 -0
  107. package/dist/i18n/generated/errors.es.gen.js.map +1 -0
  108. package/dist/i18n/generated/errors.gen.d.ts +413 -0
  109. package/dist/i18n/generated/errors.gen.d.ts.map +1 -0
  110. package/dist/i18n/generated/errors.gen.js +210 -0
  111. package/dist/i18n/generated/errors.gen.js.map +1 -0
  112. package/dist/i18n/generated/errors.ru.gen.d.ts +16 -0
  113. package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -0
  114. package/dist/i18n/generated/errors.ru.gen.js +58 -0
  115. package/dist/i18n/generated/errors.ru.gen.js.map +1 -0
  116. package/dist/i18n/keys.d.ts +170 -0
  117. package/dist/i18n/keys.d.ts.map +1 -0
  118. package/dist/i18n/keys.js +295 -0
  119. package/dist/i18n/keys.js.map +1 -0
  120. package/dist/i18n/ru.d.ts +37 -0
  121. package/dist/i18n/ru.d.ts.map +1 -0
  122. package/dist/i18n/ru.js +162 -0
  123. package/dist/i18n/ru.js.map +1 -0
  124. package/dist/index.d.ts +103 -0
  125. package/dist/index.d.ts.map +1 -0
  126. package/dist/index.js +98 -0
  127. package/dist/index.js.map +1 -0
  128. package/dist/model/context.d.ts +11 -0
  129. package/dist/model/context.d.ts.map +1 -0
  130. package/dist/model/context.js +16 -0
  131. package/dist/model/context.js.map +1 -0
  132. package/dist/model/draft.d.ts +138 -0
  133. package/dist/model/draft.d.ts.map +1 -0
  134. package/dist/model/draft.js +192 -0
  135. package/dist/model/draft.js.map +1 -0
  136. package/dist/model/features.d.ts +69 -0
  137. package/dist/model/features.d.ts.map +1 -0
  138. package/dist/model/features.js +73 -0
  139. package/dist/model/features.js.map +1 -0
  140. package/dist/model/mineSource.d.ts +47 -0
  141. package/dist/model/mineSource.d.ts.map +1 -0
  142. package/dist/model/mineSource.js +45 -0
  143. package/dist/model/mineSource.js.map +1 -0
  144. package/dist/model/mutations.d.ts +60 -0
  145. package/dist/model/mutations.d.ts.map +1 -0
  146. package/dist/model/mutations.js +165 -0
  147. package/dist/model/mutations.js.map +1 -0
  148. package/dist/model/queries.d.ts +75 -0
  149. package/dist/model/queries.d.ts.map +1 -0
  150. package/dist/model/queries.js +117 -0
  151. package/dist/model/queries.js.map +1 -0
  152. package/dist/model/queryKeys.d.ts +50 -0
  153. package/dist/model/queryKeys.d.ts.map +1 -0
  154. package/dist/model/queryKeys.js +24 -0
  155. package/dist/model/queryKeys.js.map +1 -0
  156. package/dist/model/runtime.d.ts +69 -0
  157. package/dist/model/runtime.d.ts.map +1 -0
  158. package/dist/model/runtime.js +14 -0
  159. package/dist/model/runtime.js.map +1 -0
  160. package/dist/model/status.d.ts +117 -0
  161. package/dist/model/status.d.ts.map +1 -0
  162. package/dist/model/status.js +164 -0
  163. package/dist/model/status.js.map +1 -0
  164. package/dist/model/transitions.d.ts +41 -0
  165. package/dist/model/transitions.d.ts.map +1 -0
  166. package/dist/model/transitions.js +25 -0
  167. package/dist/model/transitions.js.map +1 -0
  168. package/dist/model/validation.d.ts +135 -0
  169. package/dist/model/validation.d.ts.map +1 -0
  170. package/dist/model/validation.js +191 -0
  171. package/dist/model/validation.js.map +1 -0
  172. package/dist/nav/manifest.d.ts +46 -0
  173. package/dist/nav/manifest.d.ts.map +1 -0
  174. package/dist/nav/manifest.js +56 -0
  175. package/dist/nav/manifest.js.map +1 -0
  176. package/llms.txt +118 -0
  177. package/manifest.json +1044 -0
  178. package/nav-manifest.json +84 -0
  179. package/package.json +121 -0
  180. package/src/analytics/generated/events.json +7 -0
  181. package/src/api/generated/schema.ts +1255 -0
  182. package/src/api/listingsApi.ts +237 -0
  183. package/src/api/types.ts +193 -0
  184. package/src/default/ErrorAlert.tsx +46 -0
  185. package/src/default/FavoritesPane.tsx +123 -0
  186. package/src/default/ListingCard.tsx +178 -0
  187. package/src/default/ListingComposerPage.tsx +466 -0
  188. package/src/default/ListingDetailPane.tsx +271 -0
  189. package/src/default/ListingPhoto.tsx +62 -0
  190. package/src/default/MyListingsPane.tsx +333 -0
  191. package/src/default/StatusTags.tsx +86 -0
  192. package/src/default/icons.tsx +31 -0
  193. package/src/default/index.ts +35 -0
  194. package/src/default/theme.tsx +67 -0
  195. package/src/default/types.ts +12 -0
  196. package/src/flows/registry.ts +45 -0
  197. package/src/headless/Favorites.tsx +137 -0
  198. package/src/headless/ListingActions.tsx +101 -0
  199. package/src/headless/ListingComposer.tsx +476 -0
  200. package/src/headless/ListingDetail.tsx +231 -0
  201. package/src/headless/ListingsProvider.tsx +20 -0
  202. package/src/headless/MyListings.tsx +148 -0
  203. package/src/headless/useMandateGate.ts +40 -0
  204. package/src/i18n/errorsMap.ts +62 -0
  205. package/src/i18n/es.ts +198 -0
  206. package/src/i18n/generated/errors.es.gen.ts +64 -0
  207. package/src/i18n/generated/errors.gen.ts +241 -0
  208. package/src/i18n/generated/errors.json +499 -0
  209. package/src/i18n/generated/errors.ru.gen.ts +64 -0
  210. package/src/i18n/keys.ts +356 -0
  211. package/src/i18n/ru.ts +215 -0
  212. package/src/index.ts +260 -0
  213. package/src/model/context.tsx +28 -0
  214. package/src/model/draft.ts +307 -0
  215. package/src/model/features.ts +122 -0
  216. package/src/model/mineSource.ts +55 -0
  217. package/src/model/mutations.ts +224 -0
  218. package/src/model/queries.ts +150 -0
  219. package/src/model/queryKeys.ts +76 -0
  220. package/src/model/runtime.ts +87 -0
  221. package/src/model/status.ts +279 -0
  222. package/src/model/transitions.ts +62 -0
  223. package/src/model/validation.ts +271 -0
  224. package/src/nav/manifest.ts +97 -0
  225. package/tsconfig.json +26 -0
@@ -0,0 +1,117 @@
1
+ /**
2
+ * TWO AXES, BOTH ON SCREEN, NEITHER STANDING IN FOR THE OTHER.
3
+ *
4
+ * `status` (nine states) is the lifecycle and the ONLY thing that decides
5
+ * whether a listing is public: `Listing.objects.published()` filters on it
6
+ * alone, `INDEXED_STATUSES` is `{published}`, and the model says so out loud
7
+ * ("no visibility-reads-moderation_status coupling", `models.py`).
8
+ * `moderation_status` (four states) is the content verdict and decides
9
+ * NOTHING about visibility.
10
+ *
11
+ * Since stapel-listings 0.5.0 the two genuinely diverge, and that divergence
12
+ * is the reason this file exists:
13
+ *
14
+ * first publication status draft → PENDING, moderation → pending
15
+ * (nothing public until a verdict arrives)
16
+ * editing a LIVE listing status published (UNCHANGED), moderation → pending
17
+ * (the edit is visible immediately; a rejecting
18
+ * verdict later lands as PUBLISHED → BLOCKED)
19
+ *
20
+ * A dashboard that computed "under re-review" from `status` would show
21
+ * nothing at all for the second row — the listing looks exactly as it did —
22
+ * and the owner would never learn their edit is being screened. A dashboard
23
+ * that computed "visible" from `moderation_status` would tell them their live
24
+ * listing is offline while strangers are reading it. Both mistakes are one
25
+ * `if` away, which is why the sentence a person reads is produced HERE, from
26
+ * BOTH fields, once.
27
+ *
28
+ * Everything below is pure: no React, no antd, no fetch. `test/status.test.ts`
29
+ * asserts the whole 9 × 4 table, so a state added upstream is a red test and
30
+ * not a bare enum value on a page.
31
+ */
32
+ import type { ListingLifecycleStatus, ListingModerationStatus } from "../api/types.js";
33
+ /**
34
+ * How a status should READ, independent of the words: neutral (nothing is
35
+ * happening), waiting (something is in progress and needs no action), good
36
+ * (live), warning (the owner has something to do), stopped (the listing is
37
+ * over, by choice or by verdict).
38
+ *
39
+ * A tone rather than a colour: `/default` maps it to antd tags, another skin
40
+ * maps it to whatever it maps it to, and the token layer stays the only place
41
+ * a hex exists.
42
+ */
43
+ export type ListingStatusTone = "neutral" | "waiting" | "good" | "warning" | "stopped";
44
+ /** The lifecycle half of what a person is told. */
45
+ export interface LifecycleCaption {
46
+ readonly status: ListingLifecycleStatus;
47
+ readonly labelKey: string;
48
+ readonly tone: ListingStatusTone;
49
+ /** Is the listing readable by a stranger right now? `published` and
50
+ * nothing else — the same predicate the queryset uses. */
51
+ readonly publiclyVisible: boolean;
52
+ }
53
+ /** The lifecycle caption for one status. */
54
+ export declare function lifecycleCaption(status: ListingLifecycleStatus): LifecycleCaption;
55
+ /**
56
+ * Is this listing readable by someone who is not its owner?
57
+ *
58
+ * One predicate, one field, matching the server's. Exported because the
59
+ * DETAIL read needs it and cannot get it from a 404: `GET /listings/{pk}/`
60
+ * has no `published()` filter on its queryset, so a draft answers 200 to a
61
+ * stranger who knows the id (upstream note in MODULE.md). The pair reports
62
+ * the fact instead of rendering a draft as if it were a shop window.
63
+ */
64
+ export declare function isPubliclyVisible(status: ListingLifecycleStatus): boolean;
65
+ /** The owner-facing note about the moderation axis, when there is one. */
66
+ export interface ModerationNotice {
67
+ readonly moderationStatus: ListingModerationStatus;
68
+ readonly messageKey: string;
69
+ readonly tone: ListingStatusTone;
70
+ /**
71
+ * Is the listing live WHILE this verdict is pending? True only for the
72
+ * re-moderation row — a published listing whose edit is being screened.
73
+ * The sentence differs from a first submission's precisely here.
74
+ */
75
+ readonly liveDuringReview: boolean;
76
+ }
77
+ /**
78
+ * What to say about the moderation axis, given BOTH fields.
79
+ *
80
+ * `undefined` means "nothing to add": an approved listing needs no note, and
81
+ * neither does a draft nobody has submitted. Every other combination has a
82
+ * sentence, including the ones that look contradictory — those are the ones
83
+ * worth naming.
84
+ */
85
+ export declare function moderationNotice(status: ListingLifecycleStatus, moderationStatus: ListingModerationStatus): ModerationNotice | undefined;
86
+ /** Both halves at once — what a dashboard row and a detail header render. */
87
+ export interface ListingStatusView {
88
+ readonly lifecycle: LifecycleCaption;
89
+ readonly moderation: ModerationNotice | undefined;
90
+ /** True when the two axes disagree in the way 0.5.0 made possible: live to
91
+ * the public, under review for its owner. The one combination a host is
92
+ * most likely to get wrong, so it is a named boolean and not an inference
93
+ * a caller repeats. */
94
+ readonly liveUnderReview: boolean;
95
+ /** What the owner can do next, in lifecycle terms — see
96
+ * {@link LISTING_TRANSITIONS}. */
97
+ readonly allowedTransitions: readonly ListingLifecycleStatus[];
98
+ }
99
+ export declare function listingStatusView(status: ListingLifecycleStatus, moderationStatus: ListingModerationStatus): ListingStatusView;
100
+ /**
101
+ * The dashboard tabs, and the statuses each one folds together.
102
+ *
103
+ * The grouping is the SERVER's (`views.my_counters`), copied here so a tab's
104
+ * caption and its count cannot describe different sets: `active` includes
105
+ * PENDING beside PUBLISHED, and `drafts` includes REJECTED beside DRAFT —
106
+ * both because a listing in those states is something the owner is still
107
+ * working on. A client that grouped them its own way would show "3 active"
108
+ * over two rows.
109
+ */
110
+ export declare const MY_LISTINGS_TABS: readonly ["active", "drafts", "archived"];
111
+ export type MyListingsTab = (typeof MY_LISTINGS_TABS)[number];
112
+ export declare const MY_LISTINGS_TAB_STATUSES: Readonly<Record<MyListingsTab, readonly ListingLifecycleStatus[]>>;
113
+ /** Which tab a listing belongs to, or `undefined` for BLOCKED — the one
114
+ * status `my/counters` counts in no tab at all. A dashboard that silently
115
+ * dropped it would hide exactly the listing whose owner most needs to know. */
116
+ export declare function tabOf(status: ListingLifecycleStatus): MyListingsTab | undefined;
117
+ //# sourceMappingURL=status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/model/status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,KAAK,EACV,sBAAsB,EACtB,uBAAuB,EACxB,MAAM,iBAAiB,CAAC;AAIzB;;;;;;;;;GASG;AACH,MAAM,MAAM,iBAAiB,GACzB,SAAS,GACT,SAAS,GACT,MAAM,GACN,SAAS,GACT,SAAS,CAAC;AAEd,mDAAmD;AACnD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC;8DAC0D;IAC1D,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;CACnC;AAoDD,4CAA4C;AAC5C,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,sBAAsB,GAC7B,gBAAgB,CAElB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAEzE;AAED,0EAA0E;AAC1E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,gBAAgB,EAAE,uBAAuB,CAAC;IACnD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACpC;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,sBAAsB,EAC9B,gBAAgB,EAAE,uBAAuB,GACxC,gBAAgB,GAAG,SAAS,CA0D9B;AAED,6EAA6E;AAC7E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,gBAAgB,CAAC;IACrC,QAAQ,CAAC,UAAU,EAAE,gBAAgB,GAAG,SAAS,CAAC;IAClD;;;2BAGuB;IACvB,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAClC;sCACkC;IAClC,QAAQ,CAAC,kBAAkB,EAAE,SAAS,sBAAsB,EAAE,CAAC;CAChE;AAED,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,sBAAsB,EAC9B,gBAAgB,EAAE,uBAAuB,GACxC,iBAAiB,CAQnB;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,2CAA4C,CAAC;AAE1E,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9D,eAAO,MAAM,wBAAwB,EAAE,QAAQ,CAC7C,MAAM,CAAC,aAAa,EAAE,SAAS,sBAAsB,EAAE,CAAC,CAKzD,CAAC;AAEF;;+EAE+E;AAC/E,wBAAgB,KAAK,CACnB,MAAM,EAAE,sBAAsB,GAC7B,aAAa,GAAG,SAAS,CAK3B"}
@@ -0,0 +1,164 @@
1
+ import { LISTING_TRANSITIONS } from "./transitions.js";
2
+ import { LISTINGS_I18N_KEYS } from "../i18n/keys.js";
3
+ const LIFECYCLE = {
4
+ draft: {
5
+ labelKey: LISTINGS_I18N_KEYS.statusDraft,
6
+ tone: "neutral",
7
+ publiclyVisible: false,
8
+ },
9
+ pending: {
10
+ labelKey: LISTINGS_I18N_KEYS.statusPending,
11
+ tone: "waiting",
12
+ publiclyVisible: false,
13
+ },
14
+ published: {
15
+ labelKey: LISTINGS_I18N_KEYS.statusPublished,
16
+ tone: "good",
17
+ publiclyVisible: true,
18
+ },
19
+ paused: {
20
+ labelKey: LISTINGS_I18N_KEYS.statusPaused,
21
+ tone: "neutral",
22
+ publiclyVisible: false,
23
+ },
24
+ expired: {
25
+ labelKey: LISTINGS_I18N_KEYS.statusExpired,
26
+ tone: "warning",
27
+ publiclyVisible: false,
28
+ },
29
+ sold: {
30
+ labelKey: LISTINGS_I18N_KEYS.statusSold,
31
+ tone: "stopped",
32
+ publiclyVisible: false,
33
+ },
34
+ rejected: {
35
+ labelKey: LISTINGS_I18N_KEYS.statusRejected,
36
+ tone: "warning",
37
+ publiclyVisible: false,
38
+ },
39
+ blocked: {
40
+ labelKey: LISTINGS_I18N_KEYS.statusBlocked,
41
+ tone: "stopped",
42
+ publiclyVisible: false,
43
+ },
44
+ archived: {
45
+ labelKey: LISTINGS_I18N_KEYS.statusArchived,
46
+ tone: "neutral",
47
+ publiclyVisible: false,
48
+ },
49
+ };
50
+ /** The lifecycle caption for one status. */
51
+ export function lifecycleCaption(status) {
52
+ return { status, ...LIFECYCLE[status] };
53
+ }
54
+ /**
55
+ * Is this listing readable by someone who is not its owner?
56
+ *
57
+ * One predicate, one field, matching the server's. Exported because the
58
+ * DETAIL read needs it and cannot get it from a 404: `GET /listings/{pk}/`
59
+ * has no `published()` filter on its queryset, so a draft answers 200 to a
60
+ * stranger who knows the id (upstream note in MODULE.md). The pair reports
61
+ * the fact instead of rendering a draft as if it were a shop window.
62
+ */
63
+ export function isPubliclyVisible(status) {
64
+ return LIFECYCLE[status].publiclyVisible;
65
+ }
66
+ /**
67
+ * What to say about the moderation axis, given BOTH fields.
68
+ *
69
+ * `undefined` means "nothing to add": an approved listing needs no note, and
70
+ * neither does a draft nobody has submitted. Every other combination has a
71
+ * sentence, including the ones that look contradictory — those are the ones
72
+ * worth naming.
73
+ */
74
+ export function moderationNotice(status, moderationStatus) {
75
+ const live = status === "published";
76
+ if (moderationStatus === "approved") {
77
+ // A takedown keeps `approved` on the moderation axis in some flows (the
78
+ // lifecycle moved, the verdict did not), so an approved BLOCKED listing
79
+ // still owes its owner an explanation — and it is the lifecycle's.
80
+ return undefined;
81
+ }
82
+ if (moderationStatus === "pending") {
83
+ if (live) {
84
+ return {
85
+ moderationStatus,
86
+ messageKey: LISTINGS_I18N_KEYS.moderationLiveEditPending,
87
+ tone: "waiting",
88
+ liveDuringReview: true,
89
+ };
90
+ }
91
+ if (status === "pending") {
92
+ return {
93
+ moderationStatus,
94
+ messageKey: LISTINGS_I18N_KEYS.moderationFirstReview,
95
+ tone: "waiting",
96
+ liveDuringReview: false,
97
+ };
98
+ }
99
+ // A draft/archived/sold row carrying a pending verdict: the submission
100
+ // was overtaken by a lifecycle move. Say that, rather than promising a
101
+ // review that will not put it back on the shelf.
102
+ return {
103
+ moderationStatus,
104
+ messageKey: LISTINGS_I18N_KEYS.moderationPendingOffline,
105
+ tone: "neutral",
106
+ liveDuringReview: false,
107
+ };
108
+ }
109
+ if (moderationStatus === "needs_review") {
110
+ return {
111
+ moderationStatus,
112
+ messageKey: live
113
+ ? LISTINGS_I18N_KEYS.moderationLiveNeedsReview
114
+ : LISTINGS_I18N_KEYS.moderationNeedsReview,
115
+ tone: "waiting",
116
+ liveDuringReview: live,
117
+ };
118
+ }
119
+ // rejected — and it matters whether the lifecycle followed the verdict.
120
+ return {
121
+ moderationStatus,
122
+ messageKey: live
123
+ ? LISTINGS_I18N_KEYS.moderationRejectedStillLive
124
+ : LISTINGS_I18N_KEYS.moderationRejected,
125
+ tone: "warning",
126
+ liveDuringReview: live,
127
+ };
128
+ }
129
+ export function listingStatusView(status, moderationStatus) {
130
+ const moderation = moderationNotice(status, moderationStatus);
131
+ return {
132
+ lifecycle: lifecycleCaption(status),
133
+ moderation,
134
+ liveUnderReview: moderation?.liveDuringReview === true,
135
+ allowedTransitions: LISTING_TRANSITIONS[status],
136
+ };
137
+ }
138
+ /**
139
+ * The dashboard tabs, and the statuses each one folds together.
140
+ *
141
+ * The grouping is the SERVER's (`views.my_counters`), copied here so a tab's
142
+ * caption and its count cannot describe different sets: `active` includes
143
+ * PENDING beside PUBLISHED, and `drafts` includes REJECTED beside DRAFT —
144
+ * both because a listing in those states is something the owner is still
145
+ * working on. A client that grouped them its own way would show "3 active"
146
+ * over two rows.
147
+ */
148
+ export const MY_LISTINGS_TABS = ["active", "drafts", "archived"];
149
+ export const MY_LISTINGS_TAB_STATUSES = {
150
+ active: ["published", "pending"],
151
+ drafts: ["draft", "rejected"],
152
+ archived: ["archived", "paused", "expired", "sold"],
153
+ };
154
+ /** Which tab a listing belongs to, or `undefined` for BLOCKED — the one
155
+ * status `my/counters` counts in no tab at all. A dashboard that silently
156
+ * dropped it would hide exactly the listing whose owner most needs to know. */
157
+ export function tabOf(status) {
158
+ for (const tab of MY_LISTINGS_TABS) {
159
+ if (MY_LISTINGS_TAB_STATUSES[tab].includes(status))
160
+ return tab;
161
+ }
162
+ return undefined;
163
+ }
164
+ //# sourceMappingURL=status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/model/status.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The lifecycle whitelist, mirrored from `stapel_listings.models
3
+ * .LISTING_TRANSITIONS`.
4
+ *
5
+ * ── A mirror, not a verdict ────────────────────────────────────────────────
6
+ *
7
+ * The server decides. `transition_to` raises `TransitionError` and the view
8
+ * answers **409 `error.409.invalid_listing_transition`** with
9
+ * `params.from_status`, and that refusal is rendered as the named sentence it
10
+ * is. What the mirror buys is the difference between a button that is there
11
+ * and does nothing and a button that is switched off WITH its reason: "you
12
+ * cannot mark a draft sold — publish it first" beats a 409 toast after the
13
+ * click, and beats a hidden control (which teaches nothing) by the whole
14
+ * private-space canon §6.3.
15
+ *
16
+ * The mirror is deliberately CONSERVATIVE in one direction only: it may never
17
+ * block something the server would allow. That is why it is a copy of the
18
+ * table rather than a hand-written summary of it, and why `test/status.test
19
+ * .ts` walks every source state.
20
+ *
21
+ * Two entries look odd and are correct:
22
+ * - `published → blocked` exists, but the owner API has no route to it: only
23
+ * `apply_moderation("rejected")` walks it. It is in the table because the
24
+ * table is the model's, and leaving it out would make the mirror a
25
+ * paraphrase.
26
+ * - `blocked → published` is reinstatement after a successful appeal, also
27
+ * moderation-driven. `archive` and `complete` are the only two transitions
28
+ * this pair's API can actually request.
29
+ */
30
+ import type { ListingLifecycleStatus } from "../api/types.js";
31
+ export declare const LISTING_TRANSITIONS: Readonly<Record<ListingLifecycleStatus, readonly ListingLifecycleStatus[]>>;
32
+ /** Would the server accept this move? A same-status move is a no-op the
33
+ * server returns early on, so it counts as allowed. */
34
+ export declare function canTransition(from: ListingLifecycleStatus, to: ListingLifecycleStatus): boolean;
35
+ /**
36
+ * Deleting is not a transition and has its own rule
37
+ * (`views.destroy`): a PUBLISHED or PENDING listing is refused with
38
+ * `error.409.listing_cannot_delete_active` — archive it first.
39
+ */
40
+ export declare function canDelete(status: ListingLifecycleStatus): boolean;
41
+ //# sourceMappingURL=transitions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transitions.d.ts","sourceRoot":"","sources":["../../src/model/transitions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAE9D,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CACxC,MAAM,CAAC,sBAAsB,EAAE,SAAS,sBAAsB,EAAE,CAAC,CAWlE,CAAC;AAEF;uDACuD;AACvD,wBAAgB,aAAa,CAC3B,IAAI,EAAE,sBAAsB,EAC5B,EAAE,EAAE,sBAAsB,GACzB,OAAO,CAET;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAEjE"}
@@ -0,0 +1,25 @@
1
+ export const LISTING_TRANSITIONS = {
2
+ draft: ["pending", "archived"],
3
+ pending: ["published", "rejected", "draft", "archived"],
4
+ published: ["paused", "expired", "sold", "blocked", "archived"],
5
+ blocked: ["published", "draft", "archived"],
6
+ paused: ["published", "archived", "expired"],
7
+ expired: ["pending", "published", "archived"],
8
+ sold: ["archived", "published"],
9
+ rejected: ["draft", "archived"],
10
+ archived: ["draft"],
11
+ };
12
+ /** Would the server accept this move? A same-status move is a no-op the
13
+ * server returns early on, so it counts as allowed. */
14
+ export function canTransition(from, to) {
15
+ return from === to || LISTING_TRANSITIONS[from].includes(to);
16
+ }
17
+ /**
18
+ * Deleting is not a transition and has its own rule
19
+ * (`views.destroy`): a PUBLISHED or PENDING listing is refused with
20
+ * `error.409.listing_cannot_delete_active` — archive it first.
21
+ */
22
+ export function canDelete(status) {
23
+ return status !== "published" && status !== "pending";
24
+ }
25
+ //# sourceMappingURL=transitions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transitions.js","sourceRoot":"","sources":["../../src/model/transitions.ts"],"names":[],"mappings":"AA+BA,MAAM,CAAC,MAAM,mBAAmB,GAE5B;IACF,KAAK,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC;IAC9B,OAAO,EAAE,CAAC,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,CAAC;IACvD,SAAS,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,CAAC;IAC/D,OAAO,EAAE,CAAC,WAAW,EAAE,OAAO,EAAE,UAAU,CAAC;IAC3C,MAAM,EAAE,CAAC,WAAW,EAAE,UAAU,EAAE,SAAS,CAAC;IAC5C,OAAO,EAAE,CAAC,SAAS,EAAE,WAAW,EAAE,UAAU,CAAC;IAC7C,IAAI,EAAE,CAAC,UAAU,EAAE,WAAW,CAAC;IAC/B,QAAQ,EAAE,CAAC,OAAO,EAAE,UAAU,CAAC;IAC/B,QAAQ,EAAE,CAAC,OAAO,CAAC;CACpB,CAAC;AAEF;uDACuD;AACvD,MAAM,UAAU,aAAa,CAC3B,IAA4B,EAC5B,EAA0B;IAE1B,OAAO,IAAI,KAAK,EAAE,IAAI,mBAAmB,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AAC/D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,MAA8B;IACtD,OAAO,MAAM,KAAK,WAAW,IAAI,MAAM,KAAK,SAAS,CAAC;AACxD,CAAC"}
@@ -0,0 +1,135 @@
1
+ /**
2
+ * What the composer knows before it asks, and what it does with the answer.
3
+ *
4
+ * Three jobs, and the boundaries between them are the point:
5
+ *
6
+ * 1. **The mirror.** Feature values are mirrored by
7
+ * `@stapel/attributes-react`'s `mirrorValidate` — the engine's own rules,
8
+ * the engine's own error keys, one implementation. This module adds only
9
+ * the checks that belong to LISTINGS rather than to a value type: the
10
+ * description length bounds and the price shape. Nothing here is a
11
+ * verdict; the server re-runs all of it.
12
+ *
13
+ * 2. **The split of the publish 400.** `POST /{pk}/publish/` answers a bad
14
+ * draft with a BARE `ValidationBatchResult` — no `localizable_error`, no
15
+ * envelope — so core wraps it as `stapel.http.400` with the batch sitting
16
+ * on `StapelApiError.body`. A promotion that fails afterwards answers the
17
+ * ordinary envelope (`error.400.publish_validation_failed`, or
18
+ * `error.400.image_required` where the deployment requires a photo). Two
19
+ * different 400s that mean different things; `publishRefusal` is the one
20
+ * place that tells them apart.
21
+ *
22
+ * 3. **Routing a refusal onto the control that caused it.**
23
+ * `featureErrorsBySlug` (attributes-react) adds the `field` param the
24
+ * fleet's `useFieldError` convention routes on — the engine sends
25
+ * `{feature, slug}` and never `field`. Listing-level rows (the
26
+ * description, which the batch carries under the slug `"description"`)
27
+ * are lifted out to their own control here.
28
+ *
29
+ * ── The mirror's ceilings are CONFIGURABLE, and that is not a nicety ───────
30
+ *
31
+ * `DESCRIPTION_MIN_LENGTH` / `DESCRIPTION_MAX_LENGTH` are `STAPEL_LISTINGS`
32
+ * settings a deployment moves (`conf.py`). A hardcoded 4/500 would refuse a
33
+ * perfectly valid submission on a host that widened them — and the server
34
+ * would never even hear about the refusal, so there would be nothing to
35
+ * appeal to. Same rule the cdn pair wrote down: **the mirror may not refuse
36
+ * what the server would accept.**
37
+ */
38
+ import { StapelApiError } from "@stapel/core";
39
+ import type { FlowError } from "@stapel/core";
40
+ import type { FeatureDef, FeaturesDto, FeatureValidationResult, ValidationBatchResult } from "@stapel/attributes-react";
41
+ import type { ListingDraftValues } from "./draft.js";
42
+ /**
43
+ * The deployment's own ceilings, mirrored client-side.
44
+ *
45
+ * Defaults are the LIBRARY defaults from `stapel_listings/conf.py`; a host
46
+ * whose settings differ passes its own through `createListingsRuntime`.
47
+ */
48
+ export interface ListingDraftLimits {
49
+ /** `STAPEL_LISTINGS["DESCRIPTION_MIN_LENGTH"]`. */
50
+ readonly descriptionMin: number;
51
+ /** `STAPEL_LISTINGS["DESCRIPTION_MAX_LENGTH"]`. */
52
+ readonly descriptionMax: number;
53
+ /** `Listing.title` is `max_length=255` — a MODEL constraint, so it moves
54
+ * only with a migration, but mirrored from the schema all the same. */
55
+ readonly titleMax: number;
56
+ /** How many photos a gallery may carry. stapel-cdn has no opinion and
57
+ * stapel-listings stores an unbounded list; the storefront's ceiling is 10
58
+ * (spec §4.1), which makes it the PAIR's rule — hence a setting and not a
59
+ * constant, and hence a refusal in this pair's own namespace. */
60
+ readonly maxImages: number;
61
+ /** `STAPEL_LISTINGS["REQUIRE_IMAGE_ON_PUBLISH"]` — library default `True`.
62
+ * The server raises a bare `ValidationError` for it, which the view turns
63
+ * into `error.400.publish_validation_failed`; mirroring it lets the
64
+ * composer say WHICH thing is missing before the round trip. */
65
+ readonly requireImageOnPublish: boolean;
66
+ }
67
+ export declare const DEFAULT_DRAFT_LIMITS: ListingDraftLimits;
68
+ /** The slug the description's refusal is filed under — the engine files it
69
+ * the same way (`validate_description` returns a `FeatureValidationResult`
70
+ * whose slug is not a feature's), so the composer has one routing table. */
71
+ export declare const DESCRIPTION_FIELD = "description";
72
+ export declare const TITLE_FIELD = "title";
73
+ export declare const PRICE_FIELD = "price";
74
+ export declare const IMAGES_FIELD = "images";
75
+ export declare const CATEGORY_FIELD = "category_id";
76
+ /**
77
+ * The listing-level half of the mirror: everything that is not a feature
78
+ * value. Returns refusals keyed by control, empty when nothing is wrong.
79
+ *
80
+ * Note what is NOT judged: a missing title, a missing price and a missing
81
+ * location. The server accepts a published listing without any of them
82
+ * (`publish_listing` promotes `title_draft or title` and leaves the rest),
83
+ * so refusing them here would block a submission the backend would take. A
84
+ * skin may still *encourage* them; that is copy, not a gate.
85
+ */
86
+ export declare function mirrorListingFields(values: ListingDraftValues, limits: ListingDraftLimits): Readonly<Record<string, FlowError>>;
87
+ /**
88
+ * The whole mirror: listing fields + feature values, in one routing table.
89
+ *
90
+ * Feature refusals go through `featureErrorsBySlug`, so a mirrored "too long"
91
+ * and a server "too long" render the SAME sentence from the SAME key — they
92
+ * are the same key, because the mirror speaks the engine's vocabulary.
93
+ */
94
+ export declare function mirrorDraft(values: ListingDraftValues, features: readonly FeatureDef[], featuresDto: FeaturesDto, limits: ListingDraftLimits): Readonly<Record<string, FlowError>>;
95
+ /** Is this batch clean? `valid` is the server's own summary; a caller that
96
+ * recomputed it from `results` could disagree with the field the API sends. */
97
+ export declare function isBatchValid(batch: ValidationBatchResult): boolean;
98
+ /** What a failed publish actually was. */
99
+ export type PublishRefusal = {
100
+ /** The draft did not validate: per-control refusals, already routed. */
101
+ readonly kind: "invalid_draft";
102
+ readonly batch: ValidationBatchResult;
103
+ readonly fieldErrors: Readonly<Record<string, FlowError>>;
104
+ } | {
105
+ /** Anything else the server said, in the ordinary error dialect — a
106
+ * missing photo, a 403 on somebody else's listing, a 503. */
107
+ readonly kind: "error";
108
+ readonly error: StapelApiError;
109
+ };
110
+ /**
111
+ * Split a thrown publish failure into its two meanings.
112
+ *
113
+ * The check is on the BODY, not on the status: a 400 whose body is a batch is
114
+ * a per-field verdict, and a 400 whose body is an envelope is a sentence. A
115
+ * caller that branched on `status === 400` alone would put
116
+ * `error.400.image_required` under a feature control.
117
+ */
118
+ export declare function publishRefusal(thrown: unknown): PublishRefusal;
119
+ /**
120
+ * A server batch → refusals keyed by CONTROL.
121
+ *
122
+ * One call, because the server already keys everything the way the composer
123
+ * does. `services.publish.validate_draft` inserts `validate_description`'s
124
+ * row at the FRONT of the same list, and that row's slug is literally
125
+ * `"description"` (`stapel_attributes/validation.py:726`) — the same key
126
+ * {@link DESCRIPTION_FIELD} names and the same key the mirror files its own
127
+ * length refusal under. So the description's control receives the server's
128
+ * verdict and the mirror's through one lookup, and neither can land in a
129
+ * page-level banner because nothing routed it.
130
+ */
131
+ export declare function listingFieldErrors(batch: ValidationBatchResult): Readonly<Record<string, FlowError>>;
132
+ /** The rows of a batch that failed, in the order the server sent them —
133
+ * for a summary line above the form ("3 details need attention"). */
134
+ export declare function failedResults(batch: ValidationBatchResult): readonly FeatureValidationResult[];
135
+ //# sourceMappingURL=validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/model/validation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,OAAO,EAAE,cAAc,EAAoB,MAAM,cAAc,CAAC;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EACV,UAAU,EACV,WAAW,EACX,uBAAuB,EACvB,qBAAqB,EACtB,MAAM,0BAA0B,CAAC;AAOlC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,mDAAmD;IACnD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,mDAAmD;IACnD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC;2EACuE;IACvE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;qEAGiE;IACjE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;oEAGgE;IAChE,QAAQ,CAAC,qBAAqB,EAAE,OAAO,CAAC;CACzC;AAED,eAAO,MAAM,oBAAoB,EAAE,kBAMlC,CAAC;AAEF;;4EAE4E;AAC5E,eAAO,MAAM,iBAAiB,gBAAgB,CAAC;AAC/C,eAAO,MAAM,WAAW,UAAU,CAAC;AACnC,eAAO,MAAM,WAAW,UAAU,CAAC;AACnC,eAAO,MAAM,YAAY,WAAW,CAAC;AACrC,eAAO,MAAM,cAAc,gBAAgB,CAAC;AAU5C;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,kBAAkB,EAC1B,MAAM,EAAE,kBAAkB,GACzB,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAmDrC;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,SAAS,UAAU,EAAE,EAC/B,WAAW,EAAE,WAAW,EACxB,MAAM,EAAE,kBAAkB,GACzB,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAKrC;AAED;+EAC+E;AAC/E,wBAAgB,YAAY,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAElE;AAQD,0CAA0C;AAC1C,MAAM,MAAM,cAAc,GACtB;IACE,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,qBAAqB,CAAC;IACtC,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;CAC3D,GACD;IACE;iEAC6D;IAC7D,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;CAChC,CAAC;AAEN;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,cAAc,CAoB9D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,qBAAqB,GAC3B,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAErC;AAED;qEACqE;AACrE,wBAAgB,aAAa,CAC3B,KAAK,EAAE,qBAAqB,GAC3B,SAAS,uBAAuB,EAAE,CAEpC"}