@mohasinac/appkit 4.62.1 → 4.63.1

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 (263) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +8 -29
  3. package/dist/_internal/client/features/layout/filterNavItems.d.ts +28 -0
  4. package/dist/_internal/client/features/layout/filterNavItems.js +35 -0
  5. package/dist/_internal/server/features/ads/booking.d.ts +118 -0
  6. package/dist/_internal/server/features/ads/booking.js +326 -0
  7. package/dist/_internal/server/features/checkout/actions.d.ts +51 -11
  8. package/dist/_internal/server/features/checkout/actions.js +335 -80
  9. package/dist/_internal/server/features/checkout/expand-order-items.d.ts +1 -2
  10. package/dist/_internal/server/features/checkout/expand-order-items.js +32 -0
  11. package/dist/_internal/server/features/checkout/freeze-parts.d.ts +50 -0
  12. package/dist/_internal/server/features/checkout/freeze-parts.js +94 -0
  13. package/dist/_internal/server/features/checkout/value-otp-gate.d.ts +55 -0
  14. package/dist/_internal/server/features/checkout/value-otp-gate.js +60 -0
  15. package/dist/_internal/server/features/dashboard/attention.d.ts +128 -0
  16. package/dist/_internal/server/features/dashboard/attention.js +183 -0
  17. package/dist/_internal/server/features/dashboard/data.d.ts +47 -0
  18. package/dist/_internal/server/features/dashboard/data.js +82 -0
  19. package/dist/_internal/server/features/inventory/reservations.d.ts +99 -0
  20. package/dist/_internal/server/features/inventory/reservations.js +273 -0
  21. package/dist/_internal/server/features/offers/destinations.d.ts +40 -0
  22. package/dist/_internal/server/features/offers/destinations.js +65 -0
  23. package/dist/_internal/server/features/orders/adapters.js +21 -0
  24. package/dist/_internal/server/features/orders/amend-delivery.d.ts +56 -0
  25. package/dist/_internal/server/features/orders/amend-delivery.js +124 -0
  26. package/dist/_internal/server/features/orders/balance.d.ts +89 -0
  27. package/dist/_internal/server/features/orders/balance.js +256 -0
  28. package/dist/_internal/server/features/pricing/actions.d.ts +58 -0
  29. package/dist/_internal/server/features/pricing/actions.js +144 -0
  30. package/dist/_internal/server/features/shipping/book-split.d.ts +86 -0
  31. package/dist/_internal/server/features/shipping/book-split.js +130 -0
  32. package/dist/_internal/server/features/shipping/book.d.ts +61 -0
  33. package/dist/_internal/server/features/shipping/book.js +176 -0
  34. package/dist/_internal/server/features/shipping/courier-block.js +26 -1
  35. package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
  36. package/dist/_internal/server/features/site-settings/adapters.js +14 -0
  37. package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
  38. package/dist/_internal/server/features/stores/adapters.js +14 -0
  39. package/dist/_internal/server/functions/scheduled.d.ts +2 -1
  40. package/dist/_internal/server/functions/scheduled.js +57 -1
  41. package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +2 -0
  42. package/dist/_internal/server/jobs/core/jobRunners.js +45 -0
  43. package/dist/_internal/server/jobs/core/onOrderStatusChange.js +4 -1
  44. package/dist/_internal/server/jobs/core/reservationExpiry.d.ts +17 -0
  45. package/dist/_internal/server/jobs/core/reservationExpiry.js +40 -0
  46. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +15 -0
  47. package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
  48. package/dist/_internal/server/jobs/handlers/index.js +1 -0
  49. package/dist/_internal/server/jobs/handlers/reservationExpiry.d.ts +2 -0
  50. package/dist/_internal/server/jobs/handlers/reservationExpiry.js +2 -0
  51. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  52. package/dist/_internal/shared/actions/action-registry.js +268 -0
  53. package/dist/_internal/shared/checkout/order-math.d.ts +31 -2
  54. package/dist/_internal/shared/checkout/order-math.js +22 -36
  55. package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
  56. package/dist/_internal/shared/checkout/rules/index.js +1 -1
  57. package/dist/_internal/shared/checkout/rules/payment-gate.d.ts +18 -0
  58. package/dist/_internal/shared/checkout/rules/payment-gate.js +25 -0
  59. package/dist/_internal/shared/checkout/split.d.ts +0 -12
  60. package/dist/_internal/shared/checkout/split.js +22 -2
  61. package/dist/_internal/shared/content/disclaimers.js +7 -7
  62. package/dist/_internal/shared/content/published-values.d.ts +6 -1
  63. package/dist/_internal/shared/content/published-values.js +8 -0
  64. package/dist/_internal/shared/features/admin/integrations.d.ts +32 -2
  65. package/dist/_internal/shared/features/admin/integrations.js +19 -9
  66. package/dist/_internal/shared/features/dashboard/hub-sections.d.ts +72 -0
  67. package/dist/_internal/shared/features/dashboard/hub-sections.js +75 -0
  68. package/dist/_internal/shared/features/emi/choice.d.ts +98 -0
  69. package/dist/_internal/shared/features/emi/choice.js +69 -0
  70. package/dist/_internal/shared/features/emi/group.d.ts +84 -0
  71. package/dist/_internal/shared/features/emi/group.js +126 -0
  72. package/dist/_internal/shared/features/ledger/posting.d.ts +107 -0
  73. package/dist/_internal/shared/features/ledger/posting.js +165 -0
  74. package/dist/_internal/shared/features/ledger/reports.d.ts +76 -0
  75. package/dist/_internal/shared/features/ledger/reports.js +127 -0
  76. package/dist/_internal/shared/features/ledger/rules.d.ts +170 -0
  77. package/dist/_internal/shared/features/ledger/rules.js +221 -0
  78. package/dist/_internal/shared/features/offers/coordination.d.ts +122 -0
  79. package/dist/_internal/shared/features/offers/coordination.js +80 -0
  80. package/dist/_internal/shared/features/orders/delivery-snapshot.d.ts +114 -0
  81. package/dist/_internal/shared/features/orders/delivery-snapshot.js +116 -0
  82. package/dist/_internal/shared/features/payouts/pay-period.d.ts +91 -0
  83. package/dist/_internal/shared/features/payouts/pay-period.js +104 -0
  84. package/dist/_internal/shared/features/points/accepts.d.ts +1 -8
  85. package/dist/_internal/shared/features/points/accepts.js +12 -6
  86. package/dist/_internal/shared/features/pricing/rate-registry.d.ts +150 -0
  87. package/dist/_internal/shared/features/pricing/rate-registry.js +246 -0
  88. package/dist/_internal/shared/features/shipping/parcel.d.ts +123 -0
  89. package/dist/_internal/shared/features/shipping/parcel.js +139 -0
  90. package/dist/_internal/shared/features/site-settings/fees.d.ts +16 -8
  91. package/dist/_internal/shared/features/site-settings/fees.js +12 -8
  92. package/dist/_internal/shared/fees/explain.d.ts +81 -0
  93. package/dist/_internal/shared/fees/explain.js +101 -0
  94. package/dist/_internal/shared/history/index.d.ts +1 -1
  95. package/dist/_internal/shared/history/index.js +1 -1
  96. package/dist/_internal/shared/history/types.d.ts +7 -1
  97. package/dist/_internal/shared/history/types.js +25 -0
  98. package/dist/client.d.ts +11 -1
  99. package/dist/client.js +13 -0
  100. package/dist/constants/api-endpoints.d.ts +72 -0
  101. package/dist/constants/api-endpoints.js +41 -0
  102. package/dist/constants/field-names.d.ts +41 -0
  103. package/dist/constants/field-names.js +47 -0
  104. package/dist/features/account/components/UserOrdersView.js +28 -2
  105. package/dist/features/admin/components/AdminIntegrationsView.d.ts +55 -0
  106. package/dist/features/admin/components/AdminIntegrationsView.js +74 -0
  107. package/dist/features/admin/components/AdminOrderEditorView.d.ts +16 -1
  108. package/dist/features/admin/components/AdminOrderEditorView.js +72 -3
  109. package/dist/features/admin/components/AdminOrdersView.js +20 -1
  110. package/dist/features/admin/schemas/firestore.d.ts +32 -0
  111. package/dist/features/ads/schemas/booking-write.d.ts +157 -0
  112. package/dist/features/ads/schemas/booking-write.js +88 -0
  113. package/dist/features/ads/schemas/creative-form.d.ts +18 -0
  114. package/dist/features/ads/schemas/creative-form.js +51 -0
  115. package/dist/features/ads/schemas/firestore.d.ts +140 -0
  116. package/dist/features/ads/schemas/firestore.js +109 -0
  117. package/dist/features/auctions/repository/bid.repository.js +3 -1
  118. package/dist/features/audit-log/schemas/firestore.d.ts +110 -0
  119. package/dist/features/audit-log/schemas/firestore.js +74 -0
  120. package/dist/features/auth/actions/profile-actions.d.ts +69 -1
  121. package/dist/features/auth/actions/profile-actions.js +62 -1
  122. package/dist/features/auth/schemas/firestore.d.ts +16 -2
  123. package/dist/features/auth/schemas/firestore.js +2 -1
  124. package/dist/features/blog/actions/blog-actions.d.ts +6 -0
  125. package/dist/features/blog/actions/blog-actions.js +17 -2
  126. package/dist/features/blog/api/route.js +4 -1
  127. package/dist/features/blog/columns/index.js +5 -0
  128. package/dist/features/blog/components/BlogIndexPageView.js +2 -1
  129. package/dist/features/blog/repository/blog.repository.d.ts +4 -0
  130. package/dist/features/blog/repository/blog.repository.js +5 -0
  131. package/dist/features/blog/schemas/blog-form.d.ts +3 -0
  132. package/dist/features/blog/schemas/blog-form.js +1 -0
  133. package/dist/features/blog/schemas/firestore.d.ts +38 -1
  134. package/dist/features/blog/schemas/firestore.js +23 -0
  135. package/dist/features/blog/schemas/index.d.ts +6 -0
  136. package/dist/features/blog/schemas/index.js +2 -0
  137. package/dist/features/blog/types/index.d.ts +2 -0
  138. package/dist/features/cart/components/CartPriceBreakdown.d.ts +12 -1
  139. package/dist/features/cart/components/CartPriceBreakdown.js +24 -5
  140. package/dist/features/cart/repository/cart.repository.d.ts +14 -0
  141. package/dist/features/cart/repository/cart.repository.js +20 -0
  142. package/dist/features/cart/schemas/firestore.d.ts +24 -0
  143. package/dist/features/dashboard/components/DashboardHubView.d.ts +83 -0
  144. package/dist/features/dashboard/components/DashboardHubView.js +56 -0
  145. package/dist/features/dashboard/components/DashboardSectionsClient.d.ts +23 -0
  146. package/dist/features/dashboard/components/DashboardSectionsClient.js +66 -0
  147. package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.d.ts +55 -0
  148. package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.js +175 -0
  149. package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +24 -0
  150. package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +28 -0
  151. package/dist/features/dispute-reasons/schemas/firestore.d.ts +3 -5
  152. package/dist/features/events/components/EventSaleBanner.js +1 -1
  153. package/dist/features/faq/components/FAQPageContent.js +2 -2
  154. package/dist/features/gateway-events/schemas/index.d.ts +2 -2
  155. package/dist/features/homepage/schemas/index.d.ts +8 -8
  156. package/dist/features/inventory/schemas/stock-reservation.d.ts +114 -0
  157. package/dist/features/inventory/schemas/stock-reservation.js +103 -0
  158. package/dist/features/layout/AppLayoutShell.d.ts +9 -1
  159. package/dist/features/layout/AppLayoutShell.js +12 -4
  160. package/dist/features/layout/BottomActions.js +2 -1
  161. package/dist/features/layout/BottomNavbar.d.ts +10 -2
  162. package/dist/features/layout/SideActionDock.js +52 -2
  163. package/dist/features/layout/TitleBarLayout.js +13 -4
  164. package/dist/features/layout/TitleBarLayout.style.css +59 -0
  165. package/dist/features/layout/useCtaReveal.d.ts +16 -1
  166. package/dist/features/layout/useCtaReveal.js +17 -26
  167. package/dist/features/ledger/repository/journal.repository.d.ts +110 -0
  168. package/dist/features/ledger/repository/journal.repository.js +312 -0
  169. package/dist/features/ledger/schemas/expenses.d.ts +87 -0
  170. package/dist/features/ledger/schemas/expenses.js +27 -0
  171. package/dist/features/ledger/schemas/firestore.d.ts +183 -0
  172. package/dist/features/ledger/schemas/firestore.js +69 -0
  173. package/dist/features/orders/components/BalancePaymentPanel.d.ts +12 -0
  174. package/dist/features/orders/components/BalancePaymentPanel.js +94 -0
  175. package/dist/features/orders/components/RaiseDisputeForm.js +19 -11
  176. package/dist/features/orders/repository/orders.repository.d.ts +28 -1
  177. package/dist/features/orders/repository/orders.repository.js +60 -6
  178. package/dist/features/orders/schemas/balance.d.ts +40 -0
  179. package/dist/features/orders/schemas/balance.js +47 -0
  180. package/dist/features/orders/schemas/firestore.d.ts +202 -0
  181. package/dist/features/orders/schemas/firestore.js +43 -0
  182. package/dist/features/orders/schemas/index.d.ts +2 -2
  183. package/dist/features/orders/types/index.d.ts +21 -0
  184. package/dist/features/payments/repository/payout.repository.js +3 -2
  185. package/dist/features/payments/schemas/firestore.d.ts +92 -1
  186. package/dist/features/payments/schemas/firestore.js +23 -0
  187. package/dist/features/pricing/components/AdminPricingView.d.ts +13 -0
  188. package/dist/features/pricing/components/AdminPricingView.js +157 -0
  189. package/dist/features/products/schemas/firestore.d.ts +26 -1
  190. package/dist/features/products/schemas/firestore.js +2 -0
  191. package/dist/features/products/schemas/index.d.ts +2 -2
  192. package/dist/features/reviews/repository/reviews.repository.d.ts +14 -0
  193. package/dist/features/reviews/repository/reviews.repository.js +16 -0
  194. package/dist/features/scams/repository/scammer.repository.js +2 -1
  195. package/dist/features/scams/schemas/firestore.d.ts +2 -1
  196. package/dist/features/scams/schemas/firestore.js +2 -1
  197. package/dist/features/seller/actions/offer-actions.js +56 -0
  198. package/dist/features/seller/actions/seller-actions.d.ts +1 -1
  199. package/dist/features/seller/actions/seller-actions.js +4 -0
  200. package/dist/features/seller/components/DispatchForm.d.ts +11 -0
  201. package/dist/features/seller/components/DispatchForm.js +190 -0
  202. package/dist/features/seller/components/SellerOrdersView.js +94 -2
  203. package/dist/features/seller/components/index.d.ts +1 -0
  204. package/dist/features/seller/components/index.js +1 -0
  205. package/dist/features/seller/repository/offer.repository.js +4 -3
  206. package/dist/features/seller/schemas/firestore.d.ts +23 -2
  207. package/dist/features/seller/schemas/firestore.js +3 -2
  208. package/dist/features/seller/schemas/index.d.ts +4 -4
  209. package/dist/features/seller/schemas/order-forms.d.ts +61 -4
  210. package/dist/features/seller/schemas/order-forms.js +41 -8
  211. package/dist/features/shipments/schemas/firestore.d.ts +25 -0
  212. package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +39 -0
  213. package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +55 -0
  214. package/dist/features/shipping-boxes/schemas/firestore.d.ts +2 -4
  215. package/dist/features/stores/repository/store.repository.d.ts +18 -0
  216. package/dist/features/stores/repository/store.repository.js +27 -1
  217. package/dist/features/stores/schemas/firestore.d.ts +41 -0
  218. package/dist/features/stores/schemas/firestore.js +20 -0
  219. package/dist/features/stores/schemas/index.d.ts +6 -6
  220. package/dist/features/support/repository/support.repository.js +72 -6
  221. package/dist/features/support/schemas/firestore.d.ts +17 -0
  222. package/dist/features/support/schemas/index.d.ts +2 -2
  223. package/dist/index.d.ts +5 -0
  224. package/dist/index.js +19 -0
  225. package/dist/next/routing/route-map.d.ts +92 -0
  226. package/dist/next/routing/route-map.js +53 -0
  227. package/dist/react/contexts/SessionContext.d.ts +17 -1
  228. package/dist/schemas/registry.d.ts +22 -16
  229. package/dist/security/pii-schemas.d.ts +1 -1
  230. package/dist/security/pii-schemas.js +45 -0
  231. package/dist/server-entry.d.ts +1 -1
  232. package/dist/server-entry.js +10 -1
  233. package/dist/server.d.ts +29 -1
  234. package/dist/server.js +158 -0
  235. package/dist/styles.css +349 -1
  236. package/dist/tailwind-utilities.css +1 -1
  237. package/dist/ui/components/Button.d.ts +10 -1
  238. package/dist/ui/components/Button.js +25 -3
  239. package/dist/ui/components/Chart.d.ts +63 -0
  240. package/dist/ui/components/Chart.js +169 -0
  241. package/dist/ui/components/Chart.style.css +214 -0
  242. package/dist/ui/components/Disclaimer.js +10 -1
  243. package/dist/ui/components/OverflowRow.js +54 -2
  244. package/dist/ui/components/OverflowRow.style.css +38 -0
  245. package/dist/ui/components/RichTextEditor.js +99 -8
  246. package/dist/ui/components/chart-spec.d.ts +68 -0
  247. package/dist/ui/components/chart-spec.js +140 -0
  248. package/dist/ui/components/index.style.css +1 -0
  249. package/dist/ui/components/surface-tokens.d.ts +1 -1
  250. package/dist/ui/components/surface-tokens.js +9 -0
  251. package/dist/ui/components/toolbar-collapse.d.ts +13 -0
  252. package/dist/ui/components/toolbar-collapse.js +10 -1
  253. package/dist/ui/index.d.ts +6 -0
  254. package/dist/ui/index.js +3 -0
  255. package/dist/ui/rich-text/RichText.d.ts +10 -1
  256. package/dist/ui/rich-text/RichText.js +33 -2
  257. package/dist/ui/rich-text/RichText.style.css +35 -0
  258. package/dist/ui/rich-text/RichTextRenderer.d.ts +30 -1
  259. package/dist/ui/rich-text/RichTextRenderer.js +34 -2
  260. package/dist/ui/rich-text/chart-blocks.d.ts +62 -0
  261. package/dist/ui/rich-text/chart-blocks.js +87 -0
  262. package/package.json +1 -1
  263. package/scripts/seed-cli.mjs +3 -0
@@ -233,3 +233,41 @@
233
233
  font-size: var(--appkit-text-sm);
234
234
  color: var(--appkit-color-text-muted);
235
235
  }
236
+
237
+ /* ── Docked: the collapsed control lives in the bottom bar on mobile ────────
238
+ *
239
+ * The shell stays in flow as a zero-height measuring box — `useRowCollapse`
240
+ * compares the row's cached natural width against THIS element, so it must
241
+ * keep the original container's width. Portalling it instead would make it
242
+ * measure the viewport, read as "there is room", expand, and oscillate.
243
+ */
244
+ .appkit-overflow-row--docked {
245
+ height: 0;
246
+ /* Not `display:none`: a hidden box has no width, and width is the entire
247
+ input to the collapse decision. */
248
+ overflow: hidden;
249
+ }
250
+
251
+ .appkit-overflow-row__dock {
252
+ position: relative;
253
+ display: flex;
254
+ width: 100%;
255
+ pointer-events: auto;
256
+ padding: var(--appkit-space-2) var(--appkit-space-4);
257
+ background: var(--appkit-color-surface-elevated);
258
+ border-top: 1px solid var(--appkit-color-border);
259
+ }
260
+
261
+ .appkit-overflow-row__dock > .appkit-overflow-row__trigger {
262
+ width: 100%;
263
+ }
264
+
265
+ /* 🛑 Opens UPWARD when docked. The default popover is `top: 100%`, which from
266
+ a bar at the bottom edge renders off-screen — the control would look dead.
267
+ `BottomChrome` deliberately sets no `overflow`, so the panel may escape the
268
+ tier; that is the documented contract its own header relies on. */
269
+ .appkit-overflow-row__dock .appkit-overflow-row__popover {
270
+ top: auto;
271
+ bottom: calc(100% + var(--appkit-space-1));
272
+ width: 100%;
273
+ }
@@ -1,6 +1,44 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useEffect, useMemo, useRef } from "react";
2
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
3
+ import { useEffect, useId, useMemo, useRef, useState } from "react";
4
+ import { parseChartAttr, CHART_KINDS } from "./chart-spec.js";
5
+ import { chartFigureHtml } from "../rich-text/chart-blocks.js";
6
+ /**
7
+ * What the Chart button drops into the editor to be edited.
8
+ *
9
+ * A filled, working example rather than an empty skeleton: an author editing
10
+ * real numbers into a chart that already renders can see what each field does,
11
+ * where one facing `{"kind":"","categories":[],"series":[]}` has to guess the
12
+ * shape before anything appears at all.
13
+ */
14
+ const STARTER_CHART = JSON.stringify({
15
+ kind: "groupedBar",
16
+ title: "Untitled chart",
17
+ categories: ["First", "Second", "Third"],
18
+ series: [
19
+ { label: "Series A", values: [3, 5, 4] },
20
+ { label: "Series B", values: [4, 2, 6] },
21
+ ],
22
+ unit: "",
23
+ caption: "",
24
+ }, null, 2);
25
+ /** The chart JSON editor shown under the toolbar, with live validation. */
26
+ function ChartDraftPanel({ draft, onDraftChange, onInsert, onCancel, }) {
27
+ const result = useMemo(() => parseChartAttr(draft), [draft]);
28
+ /*
29
+ * 🛑 Generated, not a literal. Two RichTextEditors on one page — a category
30
+ * form with a description and an SEO blurb, say — would otherwise emit the
31
+ * SAME hardcoded id twice, so the second panel's `htmlFor` and
32
+ * `aria-describedby` resolve to the FIRST one's textarea and error. A screen
33
+ * reader then reads the wrong field's error and clicking the label focuses
34
+ * the wrong box, which is exactly the class of bug the `ariaLabelledBy` note
35
+ * at the top of this file exists about.
36
+ */
37
+ const panelId = useId();
38
+ const fieldId = `${panelId}-draft`;
39
+ const errorId = `${panelId}-error`;
40
+ return (_jsxs("div", { className: "appkit-rich-text-editor__chart border-b p-2 border-[var(--appkit-color-border)]", children: [_jsxs("label", { className: "mb-1 block text-xs font-medium text-[var(--appkit-color-text-muted)]", htmlFor: fieldId, children: ["Chart data \u2014 kinds: ", CHART_KINDS.join(", "), ". Each series needs one value per category."] }), _jsx("textarea", { id: fieldId, value: draft, onChange: (e) => onDraftChange(e.target.value), rows: 12, spellCheck: false, "aria-invalid": !result.ok, "aria-describedby": result.ok ? undefined : errorId, className: "w-full rounded border p-2 font-mono text-xs border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-input)] text-[var(--appkit-color-text)]" }), result.ok ? null : (_jsx("p", { id: errorId, role: "alert", className: "mt-1 text-xs text-[var(--appkit-color-error)]", children: result.reason })), _jsxs("div", { className: "mt-2 flex gap-2", children: [_jsx("button", { type: "button", onClick: onInsert, disabled: !result.ok, className: "rounded px-2 py-1 text-xs font-medium transition disabled:cursor-not-allowed disabled:opacity-50 bg-[var(--appkit-color-primary)] text-[var(--appkit-color-text-on-primary)]", children: "Insert chart" }), _jsx("button", { type: "button", onClick: onCancel, className: "rounded px-2 py-1 text-xs font-medium transition hover:bg-surface-hover text-[var(--appkit-color-text-muted)]", children: "Cancel" })] })] }));
41
+ }
4
42
  // Matches exactly what this editor's own toolbar (bold/italic/underline/
5
43
  // lists/link/clear-formatting) and typing can legitimately produce.
6
44
  // Anything else — <script>, <img onerror=...>, <iframe>, <svg>, <style>,
@@ -10,6 +48,9 @@ import { useEffect, useMemo, useRef } from "react";
10
48
  const ALLOWED_TAGS = new Set([
11
49
  "B", "STRONG", "I", "EM", "U", "S", "STRIKE",
12
50
  "UL", "OL", "LI", "A", "BR", "DIV", "SPAN", "P",
51
+ // FIGURE is here only because the toolbar's "Chart" button produces it.
52
+ // It carries no text content — see sanitizeAttribute for the data-chart rule.
53
+ "FIGURE",
13
54
  ]);
14
55
  function isSafeHref(href) {
15
56
  const trimmed = href.trim();
@@ -18,14 +59,33 @@ function isSafeHref(href) {
18
59
  // relative), or "#" — reject everything else including javascript:.
19
60
  return /^(https?:\/\/|mailto:|\/(?!\/)|#)/i.test(trimmed);
20
61
  }
62
+ /**
63
+ * 🛑 This is the SAVE-time sanitiser, and it is a different, stricter one from
64
+ * `RichText.tsx`'s render-time `sanitiseHtml`. Both had to learn about
65
+ * `data-chart` — teaching only the renderer, as a first pass here did, means a
66
+ * chart inserted in the editor is silently deleted the moment it is saved,
67
+ * with the toolbar button appearing to work perfectly.
68
+ *
69
+ * The rule is stricter than the renderer's on purpose: the value is not merely
70
+ * allowed through, it must PARSE as a valid ChartSpec. Invalid JSON or a spec
71
+ * that fails validation loses the attribute, and the empty `<figure>` that
72
+ * remains is then dropped by `sanitizeElement`. So malformed chart data cannot
73
+ * be persisted at all, which is the right place to stop it — once stored, it
74
+ * is every future reader's problem.
75
+ */
21
76
  function sanitizeAttribute(el, attr) {
22
- const isSafeAnchorHref = el.tagName === "A" && attr.name.toLowerCase() === "href";
23
- if (!isSafeAnchorHref) {
24
- el.removeAttribute(attr.name);
77
+ const name = attr.name.toLowerCase();
78
+ if (el.tagName === "A" && name === "href") {
79
+ if (!isSafeHref(attr.value))
80
+ el.setAttribute("href", "#");
81
+ return;
82
+ }
83
+ if (el.tagName === "FIGURE" && name === "data-chart") {
84
+ if (!parseChartAttr(attr.value).ok)
85
+ el.removeAttribute(attr.name);
25
86
  return;
26
87
  }
27
- if (!isSafeHref(attr.value))
28
- el.setAttribute("href", "#");
88
+ el.removeAttribute(attr.name);
29
89
  }
30
90
  function sanitizeElement(root, el) {
31
91
  if (!ALLOWED_TAGS.has(el.tagName)) {
@@ -33,6 +93,14 @@ function sanitizeElement(root, el) {
33
93
  return;
34
94
  }
35
95
  Array.from(el.attributes).forEach((attr) => sanitizeAttribute(el, attr));
96
+ // A <figure> is only legal here as a chart carrier. If the attribute did not
97
+ // survive validation above, what is left is an empty element with no text and
98
+ // no meaning — drop it rather than persist a blank block the author cannot
99
+ // see, select, or delete.
100
+ if (el.tagName === "FIGURE" && !el.hasAttribute("data-chart")) {
101
+ root.removeChild(el);
102
+ return;
103
+ }
36
104
  sanitizeChildren(el);
37
105
  }
38
106
  function sanitizeChildren(root) {
@@ -55,6 +123,7 @@ function sanitizeRichTextHtml(html) {
55
123
  }
56
124
  export function RichTextEditor({ value, onChange, disabled = false, className = "", minHeightClassName = "min-h-[180px]", placeholder = "Write formatted content...", ariaLabelledBy, ariaLabel, }) {
57
125
  const editorRef = useRef(null);
126
+ const [chartDraft, setChartDraft] = useState(null);
58
127
  const emitChange = () => {
59
128
  if (!editorRef.current)
60
129
  return;
@@ -102,12 +171,34 @@ export function RichTextEditor({ value, onChange, disabled = false, className =
102
171
  exec("createLink", href);
103
172
  },
104
173
  },
174
+ {
175
+ label: "Chart",
176
+ title: "Insert chart",
177
+ run: () => setChartDraft(STARTER_CHART),
178
+ },
105
179
  {
106
180
  label: "Clear",
107
181
  title: "Clear formatting",
108
182
  run: () => exec("removeFormat"),
109
183
  },
110
184
  ], []);
185
+ /*
186
+ * `insertHTML` rather than building a node and splicing it in: the editor is
187
+ * a contenteditable and execCommand is already how every other toolbar action
188
+ * writes, so this keeps the caret and the undo stack behaving the same way.
189
+ * The inserted markup then goes straight through `emitChange` →
190
+ * `sanitizeRichTextHtml`, so the chart is validated on the way out exactly as
191
+ * a pasted one would be — the button gets no privileged path.
192
+ */
193
+ const insertChart = () => {
194
+ if (chartDraft === null)
195
+ return;
196
+ const parsed = parseChartAttr(chartDraft);
197
+ if (!parsed.ok)
198
+ return;
199
+ exec("insertHTML", chartFigureHtml(parsed.spec));
200
+ setChartDraft(null);
201
+ };
111
202
  useEffect(() => {
112
203
  const editor = editorRef.current;
113
204
  if (!editor)
@@ -120,7 +211,7 @@ export function RichTextEditor({ value, onChange, disabled = false, className =
120
211
  editor.innerHTML = safeValue;
121
212
  }
122
213
  }, [value]);
123
- return (_jsxs("div", { className: `appkit-rich-text-editor rounded-lg border border-zinc-200 bg-[var(--appkit-color-surface)] border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] ${className}`, "data-section": "richtexteditor-div-582", children: [_jsx("div", { className: "appkit-rich-text-editor__toolbar flex flex-wrap gap-1 border-b p-2 border-[var(--appkit-color-border)]", "data-section": "richtexteditor-div-583", children: toolbarActions.map((action) => (_jsx("button", { type: "button", title: action.title, onClick: action.run, disabled: disabled, className: "appkit-rich-text-editor__btn rounded px-2 py-1 text-xs font-medium transition hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-50 text-[var(--appkit-color-text-muted)]", children: action.label }, action.title))) }), _jsx("div", { ref: editorRef,
214
+ return (_jsxs("div", { className: `appkit-rich-text-editor rounded-lg border border-zinc-200 bg-[var(--appkit-color-surface)] border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] ${className}`, "data-section": "richtexteditor-div-582", children: [_jsx("div", { className: "appkit-rich-text-editor__toolbar flex flex-wrap gap-1 border-b p-2 border-[var(--appkit-color-border)]", "data-section": "richtexteditor-div-583", children: toolbarActions.map((action) => (_jsx("button", { type: "button", title: action.title, onClick: action.run, disabled: disabled, className: "appkit-rich-text-editor__btn rounded px-2 py-1 text-xs font-medium transition hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-50 text-[var(--appkit-color-text-muted)]", children: action.label }, action.title))) }), chartDraft !== null ? (_jsx(ChartDraftPanel, { draft: chartDraft, onDraftChange: setChartDraft, onInsert: insertChart, onCancel: () => setChartDraft(null) })) : null, _jsx("div", { ref: editorRef,
124
215
  /*
125
216
  * 🛑 `role` and `aria-label` USED TO BE DECLARED HERE TOO, hardcoded
126
217
  * to "Rich text editor" — and when the labelling props were added
@@ -0,0 +1,68 @@
1
+ export declare const CHART_KINDS: readonly ["bar", "groupedBar", "line", "area"];
2
+ export type ChartKind = (typeof CHART_KINDS)[number];
3
+ /**
4
+ * 🛑 Eight, and a ninth is never a generated hue.
5
+ *
6
+ * The categorical palette is a fixed, validated ORDER of eight steps, checked
7
+ * for colourblind separation between adjacent pairs. A 9th colour generated at
8
+ * runtime cannot be validated, so a 9-series chart is REFUSED and the author is
9
+ * told to fold the tail into "Other" or split into small multiples.
10
+ */
11
+ export declare const CHART_MAX_SERIES = 8;
12
+ export interface ChartSeries {
13
+ label: string;
14
+ values: number[];
15
+ }
16
+ export interface ChartSpec {
17
+ kind: ChartKind;
18
+ /** X-axis categories. `series[].values` must be exactly this long. */
19
+ categories: string[];
20
+ series: ChartSeries[];
21
+ /** Appended to every value in tooltips and the table view. */
22
+ unit?: string;
23
+ /** Rendered under the plot. */
24
+ caption?: string;
25
+ /** Accessible name. Falls back to the caption. */
26
+ title?: string;
27
+ }
28
+ export type ChartSpecResult = {
29
+ ok: true;
30
+ spec: ChartSpec;
31
+ } | {
32
+ ok: false;
33
+ reason: string;
34
+ };
35
+ /**
36
+ * Validate an untrusted object into a `ChartSpec`.
37
+ *
38
+ * 🛑 Returns a REASON rather than throwing or silently returning null. This
39
+ * parses author-supplied JSON out of a `data-chart` attribute, and an author
40
+ * who mistyped needs to be told which rule they broke — a chart that quietly
41
+ * renders nothing is the failure mode this codebase keeps paying for.
42
+ *
43
+ * 🛑 There is deliberately no second y-axis in the shape. A dual-axis chart
44
+ * invites comparing two incommensurable scales on one plot; two measures mean
45
+ * two charts, small multiples, or indexing to a common base.
46
+ */
47
+ export declare function parseChartSpec(input: unknown): ChartSpecResult;
48
+ /**
49
+ * Parse the JSON string carried by a `data-chart` attribute.
50
+ *
51
+ * 🛑 The normalised error is deliberately NOT surfaced to the caller. The
52
+ * thrown value is a `SyntaxError` from `JSON.parse` carrying nothing an author
53
+ * can act on beyond "it is not JSON" — which the returned reason already says,
54
+ * in their language, at the place they will read it. Returning the engine's
55
+ * message instead is how server text reaches a user (Root Cause #86). Malformed
56
+ * authored content is an expected input here, not an incident to report.
57
+ *
58
+ * `normalizeError` is still called, because that is what `audit-catch-normalize`
59
+ * actually accepts: its header also lists "return immediately" as sufficient,
60
+ * but the implementation only tests for `normalizeError(var)` or `throw var`
61
+ * (audit-catch-normalize.mjs:210-213), so a bare early return is reported.
62
+ */
63
+ export declare function parseChartAttr(json: string): ChartSpecResult;
64
+ /**
65
+ * How many series the palette can colour. Exposed so a caller can decide to
66
+ * fold a tail into "Other" BEFORE validation rejects the whole chart.
67
+ */
68
+ export declare function exceedsSeriesLimit(count: number): boolean;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * The chart DATA contract — pure, and deliberately separate from `<Chart>`.
3
+ *
4
+ * ## 🛑 Why this is not in Chart.tsx
5
+ *
6
+ * `Chart.tsx` is `"use client"` and statically reaches recharts. Anything that
7
+ * imports it inherits both. But the validator needs to be callable from places
8
+ * that are neither:
9
+ *
10
+ * - `chart-blocks.ts`, which must stay SSR-safe because `RichTextRenderer`
11
+ * is a Server Component;
12
+ * - a route handler or a seed script validating authored content before it
13
+ * is stored, where pulling in recharts would be absurd.
14
+ *
15
+ * Keeping the types and the parser here means a server-side caller gets the
16
+ * contract without the renderer — the same split as Root Cause #24's lesson
17
+ * about a file that mixed Node-only crypto with client-safe display helpers.
18
+ *
19
+ * No React, no DOM, no recharts.
20
+ */
21
+ import { normalizeError } from "../../errors/normalize.js";
22
+ export const CHART_KINDS = ["bar", "groupedBar", "line", "area"];
23
+ /**
24
+ * 🛑 Eight, and a ninth is never a generated hue.
25
+ *
26
+ * The categorical palette is a fixed, validated ORDER of eight steps, checked
27
+ * for colourblind separation between adjacent pairs. A 9th colour generated at
28
+ * runtime cannot be validated, so a 9-series chart is REFUSED and the author is
29
+ * told to fold the tail into "Other" or split into small multiples.
30
+ */
31
+ export const CHART_MAX_SERIES = 8;
32
+ /**
33
+ * Validate an untrusted object into a `ChartSpec`.
34
+ *
35
+ * 🛑 Returns a REASON rather than throwing or silently returning null. This
36
+ * parses author-supplied JSON out of a `data-chart` attribute, and an author
37
+ * who mistyped needs to be told which rule they broke — a chart that quietly
38
+ * renders nothing is the failure mode this codebase keeps paying for.
39
+ *
40
+ * 🛑 There is deliberately no second y-axis in the shape. A dual-axis chart
41
+ * invites comparing two incommensurable scales on one plot; two measures mean
42
+ * two charts, small multiples, or indexing to a common base.
43
+ */
44
+ export function parseChartSpec(input) {
45
+ if (typeof input !== "object" || input === null) {
46
+ return { ok: false, reason: "Chart data must be an object." };
47
+ }
48
+ const o = input;
49
+ const kind = o.kind;
50
+ if (typeof kind !== "string" || !CHART_KINDS.includes(kind)) {
51
+ return {
52
+ ok: false,
53
+ reason: `Chart "kind" must be one of ${CHART_KINDS.join(", ")}.`,
54
+ };
55
+ }
56
+ if (!Array.isArray(o.categories) ||
57
+ o.categories.length === 0 ||
58
+ !o.categories.every((c) => typeof c === "string" && c.length > 0)) {
59
+ return { ok: false, reason: 'Chart "categories" must be a non-empty array of labels.' };
60
+ }
61
+ const categories = o.categories;
62
+ if (!Array.isArray(o.series) || o.series.length === 0) {
63
+ return { ok: false, reason: 'Chart "series" must be a non-empty array.' };
64
+ }
65
+ if (o.series.length > CHART_MAX_SERIES) {
66
+ return {
67
+ ok: false,
68
+ reason: `A chart carries at most ${CHART_MAX_SERIES} series (got ${o.series.length}). ` +
69
+ `Fold the smallest into an "Other" series, or split into separate charts — ` +
70
+ `a 9th colour cannot be checked for colourblind separation.`,
71
+ };
72
+ }
73
+ const series = [];
74
+ for (const [i, raw] of o.series.entries()) {
75
+ if (typeof raw !== "object" || raw === null) {
76
+ return { ok: false, reason: `Series ${i + 1} must be an object.` };
77
+ }
78
+ const s = raw;
79
+ if (typeof s.label !== "string" || s.label.length === 0) {
80
+ return { ok: false, reason: `Series ${i + 1} needs a non-empty "label".` };
81
+ }
82
+ if (!Array.isArray(s.values) ||
83
+ !s.values.every((v) => typeof v === "number" && Number.isFinite(v))) {
84
+ return {
85
+ ok: false,
86
+ reason: `Series "${s.label}" — "values" must be an array of finite numbers.`,
87
+ };
88
+ }
89
+ if (s.values.length !== categories.length) {
90
+ return {
91
+ ok: false,
92
+ reason: `Series "${s.label}" has ${s.values.length} values but there are ` +
93
+ `${categories.length} categories. They must match one-to-one.`,
94
+ };
95
+ }
96
+ series.push({ label: s.label, values: s.values });
97
+ }
98
+ return {
99
+ ok: true,
100
+ spec: {
101
+ kind: kind,
102
+ categories,
103
+ series,
104
+ ...(typeof o.unit === "string" ? { unit: o.unit } : {}),
105
+ ...(typeof o.caption === "string" ? { caption: o.caption } : {}),
106
+ ...(typeof o.title === "string" ? { title: o.title } : {}),
107
+ },
108
+ };
109
+ }
110
+ /**
111
+ * Parse the JSON string carried by a `data-chart` attribute.
112
+ *
113
+ * 🛑 The normalised error is deliberately NOT surfaced to the caller. The
114
+ * thrown value is a `SyntaxError` from `JSON.parse` carrying nothing an author
115
+ * can act on beyond "it is not JSON" — which the returned reason already says,
116
+ * in their language, at the place they will read it. Returning the engine's
117
+ * message instead is how server text reaches a user (Root Cause #86). Malformed
118
+ * authored content is an expected input here, not an incident to report.
119
+ *
120
+ * `normalizeError` is still called, because that is what `audit-catch-normalize`
121
+ * actually accepts: its header also lists "return immediately" as sufficient,
122
+ * but the implementation only tests for `normalizeError(var)` or `throw var`
123
+ * (audit-catch-normalize.mjs:210-213), so a bare early return is reported.
124
+ */
125
+ export function parseChartAttr(json) {
126
+ try {
127
+ return parseChartSpec(JSON.parse(json));
128
+ }
129
+ catch (err) {
130
+ void normalizeError(err);
131
+ return { ok: false, reason: "Chart data is not valid JSON." };
132
+ }
133
+ }
134
+ /**
135
+ * How many series the palette can colour. Exposed so a caller can decide to
136
+ * fold a tail into "Other" BEFORE validation rejects the whole chart.
137
+ */
138
+ export function exceedsSeriesLimit(count) {
139
+ return count > CHART_MAX_SERIES;
140
+ }
@@ -51,6 +51,7 @@
51
51
  @import "./Toggle.style.css";
52
52
  @import "./QuantityStepper.style.css";
53
53
  @import "./Checkbox.style.css";
54
+ @import "./Chart.style.css";
54
55
  @import "./EmptyState.style.css";
55
56
  @import "./SkipToMain.style.css";
56
57
  @import "./Toast.style.css";
@@ -226,7 +226,7 @@ export declare const PADDING_PRESETS: {
226
226
  readonly "content-banner": "py-[var(--appkit-space-10)] md:py-[var(--appkit-space-12)] lg:py-[var(--appkit-space-16)]";
227
227
  };
228
228
  export type PaddingPresetKey = PaddingKey;
229
- export type XPaddingKey = "none" | "x-xs" | "x-sm" | "x-md" | "x-5" | "x-lg" | "x-xl" | "x-sm-md" | "x-sm-lg-md" | "x-md-lg" | "x-md-xl" | "x-md-2xl" | "x-page" | "l-xl";
229
+ export type XPaddingKey = "none" | "x-xs" | "x-sm" | "x-md" | "x-5" | "x-lg" | "x-xl" | "x-sm-md" | "x-sm-lg-md" | "x-md-lg" | "x-md-xl" | "x-md-2xl" | "x-page" | "l-md" | "l-xl";
230
230
  type YPaddingKey = "none" | "y-2xs" | "y-2xs-tall" | "y-xs" | "y-xs-tall" | "y-sm" | "y-sm-tall" | "y-md" | "y-md-lg" | "y-lg" | "y-xl" | "y-2xl" | "y-3xl" | "y-4xl" | "y-5xl" | "y-6xl" | "y-2-5xl" | "t-2xs" | "b-2xs" | "t-xs" | "b-xs" | "t-sm" | "b-sm" | "t-md" | "b-md" | "b-md-lg" | "b-lg" | "t-md-b-lg";
231
231
  export declare const X_ONLY_MAP: Record<XPaddingKey, string>;
232
232
  export type OverflowKey = "auto" | "hidden" | "scroll" | "visible" | "x-auto" | "x-hidden" | "y-auto" | "y-hidden";
@@ -232,6 +232,15 @@ export const X_ONLY_MAP = {
232
232
  // selection-checkbox gutter).
233
233
  /** Asymmetric: `pl-8` — reserves a left gutter (e.g. for an overlaid selection checkbox). */
234
234
  "l-xl": "pl-[var(--appkit-space-8)]",
235
+ /**
236
+ * Asymmetric: `pl-4` — indents a nested block under its own heading.
237
+ *
238
+ * The grouped order-item rows (admin and seller) are the motivating pair:
239
+ * a group header followed by its member lines, where the indent is the only
240
+ * thing expressing the nesting. `x-md` would pad the right edge too, which
241
+ * is not what an indent means.
242
+ */
243
+ "l-md": "pl-[var(--appkit-space-4)]",
235
244
  };
236
245
  const Y_ONLY_MAP = {
237
246
  none: "",
@@ -1,3 +1,16 @@
1
+ /** Below this the toolbar defaults to collapsed. Matches the `lg` boundary
2
+ * that both toolbars already use to switch between their mobile and desktop
3
+ * rows, so the default flips at the same width the layout does. */
4
+ /**
5
+ * Below `lg` — the complement of `DESKTOP_QUERY` in `features/layout`.
6
+ *
7
+ * Exported so `OverflowRow` can dock its collapsed control into the bottom bar
8
+ * at exactly the width the tab bar exists at. It is NOT imported from
9
+ * `features/layout`: this is `ui/`, and a `ui → features` edge inverts the
10
+ * layering and is how `audit-import-cycles` regressions start. One literal in
11
+ * the lower layer, shared, beats two that can drift apart.
12
+ */
13
+ export declare const MOBILE_QUERY = "(max-width: 1023px)";
1
14
  /**
2
15
  * Tri-state on purpose: `null` means "the user has never chosen", which is
3
16
  * what lets the viewport supply the default. A plain boolean cannot express
@@ -19,7 +19,16 @@ const STORAGE_PREFIX = "appkit:sticky-toolbar-collapsed:";
19
19
  /** Below this the toolbar defaults to collapsed. Matches the `lg` boundary
20
20
  * that both toolbars already use to switch between their mobile and desktop
21
21
  * rows, so the default flips at the same width the layout does. */
22
- const MOBILE_QUERY = "(max-width: 1023px)";
22
+ /**
23
+ * Below `lg` — the complement of `DESKTOP_QUERY` in `features/layout`.
24
+ *
25
+ * Exported so `OverflowRow` can dock its collapsed control into the bottom bar
26
+ * at exactly the width the tab bar exists at. It is NOT imported from
27
+ * `features/layout`: this is `ui/`, and a `ui → features` edge inverts the
28
+ * layering and is how `audit-import-cycles` regressions start. One literal in
29
+ * the lower layer, shared, beats two that can drift apart.
30
+ */
31
+ export const MOBILE_QUERY = "(max-width: 1023px)";
23
32
  /**
24
33
  * Tri-state on purpose: `null` means "the user has never chosen", which is
25
34
  * what lets the viewport supply the default. A plain boolean cannot express
@@ -95,6 +95,10 @@ export type { SliderProps } from "./components/Slider";
95
95
  export { Slider } from "./components/Slider";
96
96
  export type { CheckboxProps } from "./components/Checkbox";
97
97
  export { Checkbox } from "./components/Checkbox";
98
+ export type { ChartProps } from "./components/Chart";
99
+ export { Chart, ChartFromJson } from "./components/Chart";
100
+ export type { ChartKind, ChartSeries, ChartSpec, ChartSpecResult, } from "./components/chart-spec";
101
+ export { CHART_KINDS, CHART_MAX_SERIES, exceedsSeriesLimit, parseChartAttr, parseChartSpec, } from "./components/chart-spec";
98
102
  export type { RadioGroupProps, RadioOption, RadioItemProps } from "./components/Radio";
99
103
  export { RadioGroup, RadioItem } from "./components/Radio";
100
104
  export type { ToggleProps } from "./components/Toggle";
@@ -211,6 +215,8 @@ export type { RichTextProps } from "./rich-text/RichText";
211
215
  export { RichText } from "./rich-text/RichText";
212
216
  export type { RichTextRendererProps } from "./rich-text/RichTextRenderer";
213
217
  export { RichTextRenderer } from "./rich-text/RichTextRenderer";
218
+ export type { RichTextSegment } from "./rich-text/chart-blocks";
219
+ export { chartFigureHtml, hasChartBlock, splitChartBlocks, } from "./rich-text/chart-blocks";
214
220
  export type { RichTextEditorProps } from "./components/RichTextEditor";
215
221
  export { RichTextEditor } from "./components/RichTextEditor";
216
222
  export type { ListingLayoutProps, ListingLayoutLabels, } from "./components/ListingLayout";
package/dist/ui/index.js CHANGED
@@ -82,6 +82,8 @@ export { DateInput, DateRangeInput } from "./components/DateInput.js";
82
82
  export { Textarea } from "./components/Textarea.js";
83
83
  export { Slider } from "./components/Slider.js";
84
84
  export { Checkbox } from "./components/Checkbox.js";
85
+ export { Chart, ChartFromJson } from "./components/Chart.js";
86
+ export { CHART_KINDS, CHART_MAX_SERIES, exceedsSeriesLimit, parseChartAttr, parseChartSpec, } from "./components/chart-spec.js";
85
87
  export { RadioGroup, RadioItem } from "./components/Radio.js";
86
88
  export { Toggle } from "./components/Toggle.js";
87
89
  export { QuantityStepper } from "./components/QuantityStepper.js";
@@ -148,6 +150,7 @@ export { classNames, mergeTailwindClasses } from "./style.helper.js";
148
150
  export { SideModal } from "./components/SideModal.js";
149
151
  export { RichText } from "./rich-text/RichText.js";
150
152
  export { RichTextRenderer } from "./rich-text/RichTextRenderer.js";
153
+ export { chartFigureHtml, hasChartBlock, splitChartBlocks, } from "./rich-text/chart-blocks.js";
151
154
  export { RichTextEditor } from "./components/RichTextEditor.js";
152
155
  export { ListingLayout } from "./components/ListingLayout.js";
153
156
  export { SlottedListingView } from "./components/SlottedListingView.js";
@@ -29,6 +29,15 @@ export interface RichTextProps {
29
29
  * ```
30
30
  */
31
31
  highlightCode?: (code: string, lang: string) => string;
32
+ /**
33
+ * Render `<figure data-chart>` placeholders as live charts. Default true.
34
+ *
35
+ * Set `false` for untrusted user-generated content where an author should not
36
+ * be able to mount a component at all — the placeholder then renders as the
37
+ * empty `<figure>` it is. The JSON is still validated either way; this is
38
+ * about whether a chart is an affordance on that surface.
39
+ */
40
+ charts?: boolean;
32
41
  }
33
42
  /**
34
43
  * RichText renders sanitised HTML from a CMS / ProseMirror output.
@@ -42,4 +51,4 @@ export interface RichTextProps {
42
51
  * <RichText html={desc} copyableCode />
43
52
  * ```
44
53
  */
45
- export declare function RichText({ html, className, proseClass, copyableCode, highlightCode, }: RichTextProps): import("react").JSX.Element;
54
+ export declare function RichText({ html, className, proseClass, copyableCode, highlightCode, charts, }: RichTextProps): import("react").JSX.Element;
@@ -3,6 +3,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useMemo } from "react";
4
4
  import { Div } from "../components/Div.js";
5
5
  import { normalizeRichTextHtml } from "../../utils/string.formatter.js";
6
+ import { splitChartBlocks } from "./chart-blocks.js";
7
+ import { ChartFromJson } from "../components/Chart.js";
6
8
  // --- Allowed HTML tags (safe subset — no script/object; iframe allowed for YouTube only) -------
7
9
  const ALLOWED_TAGS = new Set([
8
10
  "p",
@@ -52,6 +54,19 @@ const ALLOWED_ATTRS = {
52
54
  "*": ["class", "id", "lang", "dir", "aria-label", "role", "tabindex"],
53
55
  a: ["href", "title", "target", "rel"],
54
56
  img: ["src", "alt", "width", "height", "loading", "data-align"],
57
+ /*
58
+ * `data-chart` carries a JSON ChartSpec that the renderer hydrates into
59
+ * <Chart>. Safe to allow because it is DATA, never markup: it is read with
60
+ * `getAttribute`, run through `JSON.parse`, and then validated field-by-field
61
+ * by `parseChartSpec`, which accepts only a fixed `kind` enum, string labels
62
+ * and finite numbers. Nothing in it reaches `innerHTML`, so a script payload
63
+ * smuggled into the string fails validation and renders as an error message.
64
+ *
65
+ * 🛑 It must stay on `figure` only. Allowing it via "*" would let it ride on
66
+ * an <a> or <iframe>, where the surrounding element's own attack surface is
67
+ * the problem rather than the attribute.
68
+ */
69
+ figure: ["data-chart"],
55
70
  td: ["colspan", "rowspan"],
56
71
  th: ["colspan", "rowspan", "scope"],
57
72
  ol: ["type", "start"],
@@ -167,7 +182,7 @@ function attachCopyButtons(container) {
167
182
  * <RichText html={desc} copyableCode />
168
183
  * ```
169
184
  */
170
- export function RichText({ html, className = "", proseClass = "prose max-w-none dark:prose-invert", copyableCode = false, highlightCode, }) {
185
+ export function RichText({ html, className = "", proseClass = "prose max-w-none dark:prose-invert", copyableCode = false, highlightCode, charts = true, }) {
171
186
  const safe = useMemo(() => {
172
187
  const sanitized = sanitiseHtml(normalizeRichTextHtml(html));
173
188
  if (!highlightCode || typeof document === "undefined")
@@ -186,5 +201,21 @@ export function RichText({ html, className = "", proseClass = "prose max-w-none
186
201
  attachCopyButtons(el);
187
202
  }
188
203
  };
189
- return (_jsx(Div, { ref: ref, className: `appkit-rich-text ${proseClass} ${className}`, dangerouslySetInnerHTML: { __html: safe } }));
204
+ /*
205
+ * Split AFTER sanitising, never before.
206
+ *
207
+ * The sanitiser round-trips through `DOMParser` and re-serialises, which
208
+ * rewrites the single-quoted `data-chart='{"k":1}'` we emit into
209
+ * double-quoted `data-chart="{&quot;k&quot;:1}"`. `splitChartBlocks` handles
210
+ * both spellings precisely because of this step — splitting first would
211
+ * leave chart segments that the sanitiser never got to inspect.
212
+ */
213
+ const segments = useMemo(() => (charts ? splitChartBlocks(safe) : [{ type: "html", html: safe }]), [safe, charts]);
214
+ // Fast path: no charts. Byte-identical to the pre-chart render.
215
+ if (segments.length === 1 && segments[0].type === "html") {
216
+ return (_jsx(Div, { ref: ref, className: `appkit-rich-text ${proseClass} ${className}`, dangerouslySetInnerHTML: { __html: segments[0].html } }));
217
+ }
218
+ return (_jsx(Div, { ref: ref, className: `appkit-rich-text ${proseClass} ${className}`, children: segments.map((seg, i) => seg.type === "html" ? (_jsx("div", { className: "appkit-rich-text__seg", dangerouslySetInnerHTML: { __html: seg.html } }, `h${i}`)) : (
219
+ // eslint-disable-next-line react/no-array-index-key
220
+ _jsx(ChartFromJson, { json: seg.json }, `c${i}`))) }));
190
221
  }
@@ -25,6 +25,24 @@
25
25
  border-bottom-color: var(--appkit-color-border);
26
26
  }
27
27
 
28
+ /*
29
+ * The chart-draft panel the toolbar's "Chart" button opens.
30
+ *
31
+ * It sits between the toolbar and the editable area, so it needs the same
32
+ * recessive surface the toolbar has — without it the panel reads as part of the
33
+ * document being edited rather than as editor chrome, which is misleading when
34
+ * the thing inside it is a block of JSON.
35
+ */
36
+ .appkit-rich-text-editor__chart {
37
+ background-color: var(--appkit-color-surface-raised);
38
+ border-bottom: 1px solid var(--appkit-color-border);
39
+ }
40
+
41
+ .dark .appkit-rich-text-editor__chart {
42
+ background-color: var(--appkit-color-slate-900);
43
+ border-bottom-color: var(--appkit-color-border);
44
+ }
45
+
28
46
  .appkit-rich-text-editor__btn {
29
47
  border: 1px solid var(--appkit-color-border);
30
48
  color: var(--appkit-color-zinc-700);
@@ -331,3 +349,20 @@
331
349
  .dark .appkit-rich-text del {
332
350
  color: var(--appkit-color-zinc-400);
333
351
  }
352
+
353
+ /*
354
+ * Chart-block segment wrapper.
355
+ *
356
+ * `RichTextRenderer` has to split the HTML around `<figure data-chart>` and
357
+ * each resulting run needs an element to carry `dangerouslySetInnerHTML` —
358
+ * there is no way to inject HTML without one. `display: contents` removes that
359
+ * element from the box tree so the runs lay out exactly as they did when they
360
+ * were one `innerHTML` dump.
361
+ *
362
+ * 🛑 Without this, `prose`'s adjacency and first/last-child margin rules
363
+ * restart at every chart, so paragraphs either side of a chart space
364
+ * differently from paragraphs anywhere else on the page.
365
+ */
366
+ .appkit-rich-text__seg {
367
+ display: contents;
368
+ }