@eifi1/ui-kit 0.48.2 → 0.49.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 (292) hide show
  1. package/README.md +26 -12
  2. package/dist/admin/admin-action-confirm.js +1 -1
  3. package/dist/admin/admin-action-confirm.js.map +1 -1
  4. package/dist/admin/invitations-panel.js +5 -1
  5. package/dist/admin/invitations-panel.js.map +1 -1
  6. package/dist/admin/user-roster.d.ts +11 -5
  7. package/dist/admin/user-roster.js +15 -4
  8. package/dist/admin/user-roster.js.map +1 -1
  9. package/dist/auth/accept-invitation.js +2 -2
  10. package/dist/auth/accept-invitation.js.map +1 -1
  11. package/dist/auth/not-found-page.js +1 -1
  12. package/dist/auth/not-found-page.js.map +1 -1
  13. package/dist/auth/reset-password-form.js +2 -2
  14. package/dist/auth/reset-password-form.js.map +1 -1
  15. package/dist/auth/verify-email.js +2 -2
  16. package/dist/auth/verify-email.js.map +1 -1
  17. package/dist/billing/plan-card.js +2 -2
  18. package/dist/billing/plan-card.js.map +1 -1
  19. package/dist/components/breadcrumbs.d.ts +19 -1
  20. package/dist/components/breadcrumbs.js +11 -4
  21. package/dist/components/breadcrumbs.js.map +1 -1
  22. package/dist/components/button-look.d.ts +37 -7
  23. package/dist/components/button-look.js +33 -6
  24. package/dist/components/button-look.js.map +1 -1
  25. package/dist/components/button.d.ts +4 -1
  26. package/dist/components/button.js +2 -2
  27. package/dist/components/button.js.map +1 -1
  28. package/dist/components/calendar-heatmap.js +2 -1
  29. package/dist/components/calendar-heatmap.js.map +1 -1
  30. package/dist/components/chart-edge.d.ts +10 -0
  31. package/dist/components/chart-edge.js +57 -0
  32. package/dist/components/chart-edge.js.map +1 -0
  33. package/dist/components/chart.js +2 -1
  34. package/dist/components/chart.js.map +1 -1
  35. package/dist/components/chip.d.ts +8 -1
  36. package/dist/components/chip.js +4 -1
  37. package/dist/components/chip.js.map +1 -1
  38. package/dist/components/confirm-context.d.ts +5 -0
  39. package/dist/components/confirm-context.js +7 -0
  40. package/dist/components/confirm-context.js.map +1 -0
  41. package/dist/components/confirm-dialog.js +3 -3
  42. package/dist/components/confirm-dialog.js.map +1 -1
  43. package/dist/components/danger-confirm.d.ts +2 -2
  44. package/dist/components/danger-confirm.js +1 -1
  45. package/dist/components/danger-confirm.js.map +1 -1
  46. package/dist/components/data-table-fit.js +21 -3
  47. package/dist/components/data-table-fit.js.map +1 -1
  48. package/dist/components/data-table-mobile.d.ts +48 -2
  49. package/dist/components/data-table-mobile.js +87 -10
  50. package/dist/components/data-table-mobile.js.map +1 -1
  51. package/dist/components/data-table-row-actions.d.ts +2 -1
  52. package/dist/components/data-table-row-actions.js +7 -0
  53. package/dist/components/data-table-row-actions.js.map +1 -1
  54. package/dist/components/data-table-row.d.ts +1 -1
  55. package/dist/components/data-table-row.js.map +1 -1
  56. package/dist/components/data-table.d.ts +34 -1
  57. package/dist/components/data-table.js +34 -9
  58. package/dist/components/data-table.js.map +1 -1
  59. package/dist/components/error-boundary.js +2 -2
  60. package/dist/components/error-boundary.js.map +1 -1
  61. package/dist/components/floating-panel.d.ts +36 -13
  62. package/dist/components/floating-panel.js +27 -7
  63. package/dist/components/floating-panel.js.map +1 -1
  64. package/dist/components/form-actions.d.ts +1 -1
  65. package/dist/components/form-actions.js +1 -1
  66. package/dist/components/form-actions.js.map +1 -1
  67. package/dist/components/icon-button.d.ts +11 -0
  68. package/dist/components/icon-button.js +7 -5
  69. package/dist/components/icon-button.js.map +1 -1
  70. package/dist/components/nav-pills.d.ts +8 -1
  71. package/dist/components/nav-pills.js +61 -38
  72. package/dist/components/nav-pills.js.map +1 -1
  73. package/dist/components/page-header-context.d.ts +8 -0
  74. package/dist/components/page-header-context.js +7 -0
  75. package/dist/components/page-header-context.js.map +1 -0
  76. package/dist/components/page-header.d.ts +14 -2
  77. package/dist/components/page-header.js +10 -1
  78. package/dist/components/page-header.js.map +1 -1
  79. package/dist/components/pie-chart.js +2 -1
  80. package/dist/components/pie-chart.js.map +1 -1
  81. package/dist/components/progress-bar.js +5 -4
  82. package/dist/components/progress-bar.js.map +1 -1
  83. package/dist/components/reauth-dialog.js +1 -1
  84. package/dist/components/reauth-dialog.js.map +1 -1
  85. package/dist/components/row-actions-labels.js +3 -1
  86. package/dist/components/row-actions-labels.js.map +1 -1
  87. package/dist/components/row-actions.d.ts +43 -9
  88. package/dist/components/row-actions.js +78 -15
  89. package/dist/components/row-actions.js.map +1 -1
  90. package/dist/components/section-nav.d.ts +37 -0
  91. package/dist/components/section-nav.js +33 -0
  92. package/dist/components/section-nav.js.map +1 -0
  93. package/dist/components/series-chart.js +8 -3
  94. package/dist/components/series-chart.js.map +1 -1
  95. package/dist/components/share-card.js +1 -1
  96. package/dist/components/share-card.js.map +1 -1
  97. package/dist/components/signature-pad.js +1 -1
  98. package/dist/components/signature-pad.js.map +1 -1
  99. package/dist/components/sparkline.js +2 -1
  100. package/dist/components/sparkline.js.map +1 -1
  101. package/dist/components/stat-tile.js +14 -7
  102. package/dist/components/stat-tile.js.map +1 -1
  103. package/dist/components/status-dot.d.ts +5 -1
  104. package/dist/components/status-dot.js +4 -1
  105. package/dist/components/status-dot.js.map +1 -1
  106. package/dist/components/tooltip.js +1 -1
  107. package/dist/components/tooltip.js.map +1 -1
  108. package/dist/components/translation-review-editor.js +2 -2
  109. package/dist/components/translation-review-editor.js.map +1 -1
  110. package/dist/components/translation-review.js +2 -2
  111. package/dist/components/translation-review.js.map +1 -1
  112. package/dist/components/treemap.d.ts +5 -2
  113. package/dist/components/treemap.js +26 -6
  114. package/dist/components/treemap.js.map +1 -1
  115. package/dist/components/use-table-state.js +37 -6
  116. package/dist/components/use-table-state.js.map +1 -1
  117. package/dist/data-table.js.map +1 -1
  118. package/dist/demo/demo-banner.js +1 -1
  119. package/dist/demo/demo-banner.js.map +1 -1
  120. package/dist/demo/demo-ended.js +1 -1
  121. package/dist/demo/demo-ended.js.map +1 -1
  122. package/dist/demo/demo-start.js +1 -1
  123. package/dist/demo/demo-start.js.map +1 -1
  124. package/dist/eslint/index.js +1 -1
  125. package/dist/feedback/feedback-inbox.js +1 -1
  126. package/dist/feedback/feedback-inbox.js.map +1 -1
  127. package/dist/hooks/use-dismiss.d.ts +9 -1
  128. package/dist/hooks/use-dismiss.js +25 -5
  129. package/dist/hooks/use-dismiss.js.map +1 -1
  130. package/dist/hooks/use-document-title.d.ts +14 -0
  131. package/dist/hooks/use-document-title.js +22 -0
  132. package/dist/hooks/use-document-title.js.map +1 -0
  133. package/dist/i18n/german.js +6 -2
  134. package/dist/i18n/german.js.map +1 -1
  135. package/dist/i18n/kit-labels.d.ts +31 -1
  136. package/dist/i18n/kit-labels.js +43 -2
  137. package/dist/i18n/kit-labels.js.map +1 -1
  138. package/dist/i18n/locales/en.js +6 -2
  139. package/dist/i18n/locales/en.js.map +2 -2
  140. package/dist/i18n/locales/es.js +6 -2
  141. package/dist/i18n/locales/es.js.map +1 -1
  142. package/dist/i18n/locales/fr.js +6 -2
  143. package/dist/i18n/locales/fr.js.map +1 -1
  144. package/dist/i18n/locales/hu.js +6 -2
  145. package/dist/i18n/locales/hu.js.map +1 -1
  146. package/dist/i18n/locales/it.js +6 -2
  147. package/dist/i18n/locales/it.js.map +1 -1
  148. package/dist/i18n/locales/zh.js +6 -2
  149. package/dist/i18n/locales/zh.js.map +1 -1
  150. package/dist/i18n/review.js +2 -0
  151. package/dist/i18n/review.js.map +1 -1
  152. package/dist/i18n/shared-labels.js +1 -0
  153. package/dist/i18n/shared-labels.js.map +1 -1
  154. package/dist/index.d.ts +8 -1
  155. package/dist/index.js +11 -0
  156. package/dist/index.js.map +1 -1
  157. package/dist/landing/landing-actions.js +2 -2
  158. package/dist/landing/landing-actions.js.map +1 -1
  159. package/dist/landing/public-header.js +2 -2
  160. package/dist/landing/public-header.js.map +1 -1
  161. package/dist/lib/clipboard-minus.d.ts +27 -0
  162. package/dist/lib/clipboard-minus.js +63 -0
  163. package/dist/lib/clipboard-minus.js.map +1 -0
  164. package/dist/lib/document-title.d.ts +17 -0
  165. package/dist/lib/document-title.js +29 -0
  166. package/dist/lib/document-title.js.map +1 -0
  167. package/dist/lib/float-clearance.js +8 -2
  168. package/dist/lib/float-clearance.js.map +1 -1
  169. package/dist/lib/format.d.ts +35 -10
  170. package/dist/lib/format.js +55 -11
  171. package/dist/lib/format.js.map +1 -1
  172. package/dist/lib/minus.d.ts +22 -0
  173. package/dist/lib/minus.js +14 -0
  174. package/dist/lib/minus.js.map +1 -0
  175. package/dist/lib/status-tone.d.ts +30 -0
  176. package/dist/lib/status-tone.js +17 -0
  177. package/dist/lib/status-tone.js.map +1 -0
  178. package/dist/settings/settings-layout.js +3 -2
  179. package/dist/settings/settings-layout.js.map +1 -1
  180. package/dist/shell/account-menu-language.d.ts +1 -1
  181. package/dist/shell/account-menu-language.js +9 -1
  182. package/dist/shell/account-menu-language.js.map +1 -1
  183. package/dist/shell/app-shell-labels.js +2 -1
  184. package/dist/shell/app-shell-labels.js.map +1 -1
  185. package/dist/shell/app-shell.d.ts +58 -16
  186. package/dist/shell/app-shell.js +151 -43
  187. package/dist/shell/app-shell.js.map +1 -1
  188. package/dist/shell/language-code.d.ts +3 -0
  189. package/dist/shell/language-code.js +8 -0
  190. package/dist/shell/language-code.js.map +1 -0
  191. package/dist/shell/topbar-controls.d.ts +14 -6
  192. package/dist/shell/topbar-controls.js +14 -17
  193. package/dist/shell/topbar-controls.js.map +1 -1
  194. package/dist/testing/css-variables.generated.d.ts +1 -1
  195. package/dist/testing/css-variables.generated.js +1 -1
  196. package/dist/testing/css-variables.generated.js.map +1 -1
  197. package/dist/theme/chart-palette.d.ts +82 -15
  198. package/dist/theme/chart-palette.js +51 -24
  199. package/dist/theme/chart-palette.js.map +1 -1
  200. package/dist/theme/palette-presets.d.ts +9 -0
  201. package/dist/theme/palette-presets.js +77 -62
  202. package/dist/theme/palette-presets.js.map +1 -1
  203. package/dist/tour/tour.js +1 -1
  204. package/dist/tour/tour.js.map +1 -1
  205. package/dist/wizard/stepper-nav.d.ts +1 -1
  206. package/dist/wizard/stepper-nav.js +3 -3
  207. package/dist/wizard/stepper-nav.js.map +1 -1
  208. package/package.json +1 -1
  209. package/src/admin/admin-action-confirm.tsx +1 -1
  210. package/src/admin/invitations-panel.tsx +7 -3
  211. package/src/admin/user-roster.tsx +23 -6
  212. package/src/auth/accept-invitation.tsx +3 -3
  213. package/src/auth/not-found-page.tsx +1 -1
  214. package/src/auth/reset-password-form.tsx +3 -3
  215. package/src/auth/verify-email.tsx +3 -3
  216. package/src/billing/plan-card.tsx +2 -2
  217. package/src/components/breadcrumbs.tsx +32 -4
  218. package/src/components/button-look.ts +86 -15
  219. package/src/components/button.tsx +6 -3
  220. package/src/components/calendar-heatmap.tsx +2 -1
  221. package/src/components/chart-edge.ts +73 -0
  222. package/src/components/chart.tsx +3 -1
  223. package/src/components/chip.tsx +10 -1
  224. package/src/components/confirm-context.ts +7 -0
  225. package/src/components/confirm-dialog.tsx +3 -3
  226. package/src/components/danger-confirm.tsx +3 -3
  227. package/src/components/data-table-fit.ts +41 -6
  228. package/src/components/data-table-mobile.tsx +162 -11
  229. package/src/components/data-table-row-actions.tsx +9 -1
  230. package/src/components/data-table-row.tsx +2 -0
  231. package/src/components/data-table.tsx +82 -18
  232. package/src/components/error-boundary.tsx +2 -2
  233. package/src/components/floating-panel.tsx +67 -16
  234. package/src/components/form-actions.tsx +2 -2
  235. package/src/components/icon-button.tsx +21 -8
  236. package/src/components/nav-pills.tsx +29 -2
  237. package/src/components/page-header-context.ts +8 -0
  238. package/src/components/page-header.tsx +34 -2
  239. package/src/components/pie-chart.tsx +2 -1
  240. package/src/components/progress-bar.tsx +6 -3
  241. package/src/components/reauth-dialog.tsx +1 -1
  242. package/src/components/row-actions-labels.ts +2 -0
  243. package/src/components/row-actions.tsx +154 -33
  244. package/src/components/section-nav.tsx +65 -0
  245. package/src/components/series-chart.tsx +12 -3
  246. package/src/components/share-card.tsx +1 -1
  247. package/src/components/signature-pad.tsx +1 -1
  248. package/src/components/sparkline.tsx +2 -1
  249. package/src/components/stat-tile.tsx +19 -7
  250. package/src/components/status-dot.tsx +7 -1
  251. package/src/components/tooltip.tsx +3 -1
  252. package/src/components/translation-review-editor.tsx +2 -2
  253. package/src/components/translation-review.tsx +2 -2
  254. package/src/components/treemap.tsx +34 -8
  255. package/src/components/use-table-state.ts +63 -8
  256. package/src/data-table.ts +3 -3
  257. package/src/demo/demo-banner.tsx +1 -1
  258. package/src/demo/demo-ended.tsx +1 -1
  259. package/src/demo/demo-start.tsx +1 -1
  260. package/src/feedback/feedback-inbox.tsx +1 -1
  261. package/src/hooks/use-dismiss.ts +43 -4
  262. package/src/hooks/use-document-title.ts +31 -0
  263. package/src/i18n/german.ts +4 -0
  264. package/src/i18n/kit-labels.tsx +76 -1
  265. package/src/i18n/locales/es.ts +4 -0
  266. package/src/i18n/locales/fr.ts +4 -0
  267. package/src/i18n/locales/hu.ts +4 -0
  268. package/src/i18n/locales/it.ts +4 -0
  269. package/src/i18n/locales/zh.ts +4 -0
  270. package/src/i18n/review.ts +2 -0
  271. package/src/i18n/shared-labels.ts +1 -0
  272. package/src/index.ts +13 -0
  273. package/src/landing/landing-actions.tsx +2 -2
  274. package/src/landing/public-header.tsx +2 -2
  275. package/src/lib/clipboard-minus.ts +94 -0
  276. package/src/lib/document-title.ts +41 -0
  277. package/src/lib/float-clearance.ts +19 -3
  278. package/src/lib/format.ts +115 -30
  279. package/src/lib/minus.ts +40 -0
  280. package/src/lib/status-tone.ts +43 -0
  281. package/src/settings/settings-layout.tsx +3 -2
  282. package/src/shell/account-menu-language.tsx +14 -2
  283. package/src/shell/app-shell-labels.ts +1 -0
  284. package/src/shell/app-shell.tsx +291 -68
  285. package/src/shell/language-code.ts +5 -0
  286. package/src/shell/topbar-controls.tsx +35 -15
  287. package/src/testing/css-variables.generated.ts +6 -6
  288. package/src/theme/chart-palette.ts +102 -20
  289. package/src/theme/palette-presets.ts +121 -84
  290. package/src/tour/tour.tsx +1 -1
  291. package/src/wizard/stepper-nav.tsx +4 -4
  292. package/tokens.css +28 -18
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/treemap.tsx"],"sourcesContent":["// A tile chart (\"treemap\"): one rectangle per node, its AREA the node's share of what\n// is drawn. Lifted out of keksdose's reports, where five cards drew it (spending by\n// group and by category, net worth by account and by type, allocation, payees, food\n// groups) — every domain word is gone and every string it could print is a prop.\n//\n// It uses the kit's own chart shell (`ChartContainer`, `ChartTooltipContent`) and the\n// kit's own ink rule (`textOn`), so it re-skins with the palette like every other\n// chart. The one thing it cannot take from a CSS var is the tile colour: the label ink\n// is MEASURED against the fill, and a `var(--chart-3)` string measures as nothing — see\n// `useChartTokenHex` below for how the concrete hex is found instead.\nimport { useLayoutEffect, useRef, useState, useSyncExternalStore } from \"react\";\nimport type { ComponentProps, ComponentPropsWithoutRef, KeyboardEvent, ReactNode } from \"react\";\nimport { Treemap as RechartsTreemap } from \"recharts\";\nimport { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from \"./chart\";\nimport { PALETTE_HEX, textOn } from \"../theme/chart-palette\";\nimport { parseHex } from \"../theme/color\";\nimport { dirOf, type Direction } from \"../lib/direction\";\nimport { cn } from \"../lib/cn\";\nimport { useTextSize } from \"../theme/text-size\";\nimport { chartHeightProps, type ChartHeight } from \"./chart-height\";\n\n/**\n * One tile.\n *\n * `value`, not keksdose's `total`: a treemap's areas are not always money (keksdose\n * already sizes one by transaction COUNT), and `value` is the word every other chart\n * in recharts uses for the number a mark is sized by. Migrating is a rename at the\n * mapping step — `{ id, name, value: row.total }`.\n */\nexport interface TreemapNode {\n /** Stable key, handed back by {@link TreemapProps.onNodeClick} and\n * {@link TreemapProps.nodeNote}. Names can collide; ids must not. */\n id: string;\n name: string;\n /** The tile's area. A node whose value is not a positive finite number is NOT\n * drawn — an area cannot be negative, and recharts lays a negative size out as\n * nonsense rather than refusing it. Saying so is the caller's job: a group that\n * vanishes silently between two views of the same data is a bug report. */\n value: number;\n /** Overrides the by-index palette colour for this tile — e.g. to paint every child\n * in its parent group's colour, so the groups stay legible in one picture. A\n * concrete `#rrggbb` or a bare `var(--chart-N)` keeps the label ink measured;\n * anything else (another var, a `color-mix()`) gets `currentColor` ink, because\n * there is nothing to measure — set `labelColor` then. */\n fill?: string;\n /** The label's ink, for a `fill` the kit cannot measure (a `var()` or `color-mix()`):\n * keksdose's recency plot fills with `color-mix(in srgb, var(--chart-1) N%,\n * transparent)`, where `currentColor` put dark page text on full-strength indigo\n * (~1.5:1). Wins over the measured ink when given. */\n labelColor?: string;\n}\n\n/**\n * Everything a `<div>` takes reaches the chart's root (`ChartContainer`), so a test id\n * or a `data-tour` anchor can find it. `children` is the chart's own.\n */\nexport interface TreemapProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n data: TreemapNode[];\n /**\n * Formats a node's value in the tooltip. Handed the node's SHARE of what is drawn\n * (0–1) as a second argument, because a tile is read for how big it is relative to\n * the rest, and \"€412.30\" alone makes the reader estimate that ratio off the areas.\n * The share is `undefined` when the drawn total is not positive.\n *\n * Default: the bare number through `Intl.NumberFormat` in the runtime's locale. A\n * host that has a currency or a locale passes this — the kit knows neither.\n */\n valueFormatter?: (value: number, share: number | undefined) => ReactNode;\n /** Chart height: a CSS length (`\"20rem\"`, `\"min(20rem, 50dvh)\"`), a Tailwind class,\n * or px. Default `\"20rem\"` — 320 px at Normal, growing with the text size like the\n * labels in the tiles (0.32, docs/text-size-harmonization.md §10.10). */\n height?: ChartHeight;\n /** A tile was activated — by click, or by Enter/Space on a focused tile. Setting\n * this makes every tile a focusable `role=\"button\"` named by its node name. */\n onNodeClick?: (id: string, name: string) => void;\n /**\n * The node names are the user's own data (payees, not categories): the tile labels\n * and the whole tooltip bubble get `data-private`, the same attribute the kit's\n * `Tooltip redact` and `ChartTooltipContent`'s value cell carry, so the host's one\n * privacy rule reaches them. Off by default — a category name is a label of the app.\n */\n redactNames?: boolean;\n /**\n * A second, smaller line under a tile's label — one extra figure per node that the\n * name cannot carry (a year-on-year `+12%`, `new`). Return `undefined` for a node\n * with nothing to add. Never tagged `data-private`: a ratio reveals no amount.\n *\n * ON THE TILE rather than in the tooltip, for two reasons. The tooltip identifies a\n * node by name or by raw value, so a per-node note would be keyed on an amount (two\n * nodes of equal size would swap notes). And a tooltip is a pointer affordance: a\n * phone never hovers, so a figure that only exists on hover does not exist there.\n */\n nodeNote?: (id: string) => string | undefined;\n /**\n * Draw at most this many tiles (the first N drawable nodes, in the order given —\n * sort first). Unbounded by default. A tile under ~44x20px has no room for a label,\n * and a map made of unlabelled rectangles answers nothing; around 24 is where the\n * smallest tile of a skewed series reaches that floor on a phone-width card. The\n * caller owns the \"N more not shown\" line, since it knows how many it passed.\n */\n maxTiles?: number;\n /**\n * The categorical ramp tiles take by index, as concrete hex. Default: the page's\n * live `--chart-1…9` tokens (see `useChartTokenHex`). Pass one when the host\n * already holds the active palette — e.g. `createPaletteStore`'s `useChartHex()` —\n * so a caller colouring a legend beside the map is guaranteed the same list.\n */\n colors?: readonly string[];\n}\n\n// ── The categorical ramp, as concrete hex ──────────────────────────────────\n//\n// The label ink is measured against the tile's fill, so the fill has to be a colour\n// and not a reference to one. keksdose read it from its palette STORE, which is an\n// app object the kit cannot import. What the kit does own is the token layer: whether\n// a palette arrives through `applyTokenSet` (inline properties on <html>) or through\n// tokens.css (`:root` / `.dark`), the resolved `--chart-N` on <html> is the colour\n// every other chart on the page is being painted with. So that is what is read, and\n// re-read whenever <html>'s class or style changes — which is exactly when a theme\n// toggle or a palette switch lands.\n\nconst CHART_TOKENS = 9;\n\n/** The resolved ramp, joined into ONE string so `useSyncExternalStore` compares a\n * primitive: a fresh array per read would never be `Object.is`-equal to the last and\n * would re-render forever. */\nfunction readChartTokens(): string {\n const root = document.documentElement;\n const fallback = root.classList.contains(\"dark\") ? PALETTE_HEX.dark : PALETTE_HEX.light;\n const style = getComputedStyle(root);\n return Array.from({ length: CHART_TOKENS }, (_, i) => {\n const v = style.getPropertyValue(`--chart-${i + 1}`).trim();\n // An unresolved token (a host that never loaded tokens.css, a test DOM) falls\n // back to the built-in ramp for the current theme rather than to no colour.\n return parseHex(v) ? v : fallback[i];\n }).join(\",\");\n}\n\nfunction subscribeChartTokens(onChange: () => void): () => void {\n const observer = new MutationObserver(onChange);\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: [\"class\", \"style\", \"data-theme\"],\n });\n return () => observer.disconnect();\n}\n\nconst SERVER_TOKENS = PALETTE_HEX.light.join(\",\");\n\nfunction useChartTokenHex(): string[] {\n const joined = useSyncExternalStore(subscribeChartTokens, readChartTokens, () => SERVER_TOKENS);\n return joined.split(\",\");\n}\n\nconst CHART_TOKEN_REF = /^var\\(\\s*--chart-(\\d+)\\s*\\)$/;\n\n/**\n * The hex a tile fill measures as: the fill itself when it is hex, the live token when\n * it is a bare `var(--chart-N)` (what `paletteFor` returns), otherwise null. Without\n * the token case a hand-built recharts map coloured with the kit's own `paletteFor`\n * got `currentColor` ink — dark page text on dark indigo in the light theme.\n */\nfunction measurableFill(fill: string, tokens: readonly string[]): string | null {\n if (parseHex(fill)) return fill;\n const m = CHART_TOKEN_REF.exec(fill.trim());\n if (!m) return null;\n const hex = tokens[Number(m[1]) - 1];\n return hex && parseHex(hex) ? hex : null;\n}\n\n// ── Tile ──────────────────────────────────────────────────────────────────\n\n/** The name's size in px at Normal; a tile draws it × the text size's scale (§10.10), and\n * every distance below that is measured in lines grows with it. */\nconst LABEL_FONT_SIZE = 12;\n/** The optional second line, a step down from the name so the tile reads name-first. */\nconst NOTE_FONT_SIZE = 11;\n\n/**\n * Fit a label to its tile: the text itself when it fits, an ellipsised head when it\n * does not, `null` when not even three characters would. Without this a long name ran\n * on across its neighbours and collided with THEIR labels. 0.58em is the average\n * advance of a UI sans at this weight; it only needs to be close, since the tile keeps\n * 6px of padding either side. The full name stays in the tooltip.\n */\nexport function fitLabel(name: string, boxWidth: number, fontSize: number): string | null {\n const max = Math.floor((boxWidth - 12) / (fontSize * 0.58));\n if (max < 3) return null;\n return name.length <= max ? name : `${name.slice(0, max - 1).trimEnd()}…`;\n}\n\n/** What recharts hands a `content` element for each node, plus the three options the\n * chart forwards. Everything optional because recharts clones the element it was\n * given and fills the geometry in — the element as written carries only the options. */\nexport interface TreemapCellProps {\n depth?: number;\n x?: number;\n y?: number;\n width?: number;\n height?: number;\n name?: string;\n fill?: string;\n /** See {@link TreemapNode.labelColor}; recharts hands each node's fields to its cell. */\n labelColor?: string;\n id?: string;\n onNodeClick?: (id: string, name: string) => void;\n redactNames?: boolean;\n nodeNote?: (id: string) => string | undefined;\n /** The page's reading direction, which `Treemap` reads off its own root. In `rtl`\n * the label stands in the tile's top RIGHT corner and is shaped right-to-left, so a\n * clipped name ends in its ellipsis on the reading end. Default `ltr`. */\n dir?: Direction;\n}\n\n/**\n * One tile. Exported so a caller drawing its own recharts `<Treemap>` gets the same\n * label rules.\n *\n * recharts' default cell inherits the chart-level `stroke`, an SVG presentation\n * attribute that lands on the `<svg>` root and cascades into the label — so the glyphs\n * were painted with a 1px outline ON TOP of themselves, which reads as blur. This cell\n * draws its own label with a halo in the TILE colour painted BEHIND the fill\n * (`paint-order: stroke`), which defeats any inherited stroke and keeps the text solid\n * where it overlaps a lighter or darker part of its own tile.\n */\nexport function TreemapCell({\n depth,\n x = 0,\n y = 0,\n width = 0,\n height = 0,\n name,\n fill,\n labelColor,\n id,\n onNodeClick,\n redactNames,\n nodeNote,\n dir = \"ltr\",\n}: TreemapCellProps) {\n // Before the early return: a hook has to run on every render of the cell.\n const tokenHex = useChartTokenHex();\n const { scale } = useTextSize();\n // Depth 0 is recharts' synthetic root, which spans the whole chart.\n if (!depth) return null;\n // Whole pixels: a rect on a half pixel renders its edge across two half-lit\n // columns, and the text baseline inherits that blur.\n const px = Math.round(x);\n const py = Math.round(y);\n const pw = Math.round(width);\n const ph = Math.round(height);\n const tileFill = fill ?? \"var(--chart-1)\";\n const measured = measurableFill(tileFill, tokenHex);\n const ink = labelColor ?? (measured ? textOn(measured) : \"currentColor\");\n // The halo only on an OPAQUE fill we could measure. A translucent fill (a\n // `color-mix(… transparent)`) composited twice — once as the tile, once as the halo\n // over it — draws a darker ring round every glyph, which read as doubled text on the\n // faded tiles. `none` still overrides the inherited chart `stroke`.\n const halo = measured ? tileFill : \"none\";\n // Below ~44x20 there is no room for a readable word, so no label at all rather than\n // a one-letter stub bleeding over the tile edge.\n const labelSize = LABEL_FONT_SIZE * scale;\n const noteSize = NOTE_FONT_SIZE * scale;\n const label = pw >= 44 * scale && ph >= 20 * scale ? fitLabel(name ?? \"\", pw, labelSize) : null;\n // Only under a label that is itself drawn — a stray \"+12%\" on an unnamed rectangle\n // names nothing — and only where a second baseline fits: the note's baseline sits\n // 29px down and an 11px glyph hangs ~2px below it, so 36 leaves the same ~5px under\n // the note that the label keeps under itself at the 20px floor.\n const rawNote = id != null && nodeNote ? nodeNote(id) : undefined;\n const note =\n label != null && rawNote != null && ph >= 36 * scale ? fitLabel(rawNote, pw, noteSize) : null;\n // The label's START edge, 6px in from the tile's. The tiles themselves are laid out\n // physically (the plot is not mirrored, see `ChartContainer`) and the chart's SVG is\n // pinned `ltr`, so the direction is set on each <text> explicitly: `start` is then\n // the right end in RTL, and the label grows leftwards from the tile's right edge.\n const textX = dir === \"rtl\" ? px + pw - 6 : px + 6;\n const activate = id != null && onNodeClick ? () => onNodeClick(id, name ?? \"\") : undefined;\n const onKeyDown = activate\n ? (e: KeyboardEvent<SVGGElement>) => {\n if (e.key !== \"Enter\" && e.key !== \" \") return;\n e.preventDefault();\n activate();\n }\n : undefined;\n return (\n <g\n onClick={activate}\n onKeyDown={onKeyDown}\n // A clickable tile is a button to a keyboard and a screen reader too; the\n // drilldown it opens was mouse-only in the app this came from. Named by the node\n // name, which is also why the group carries `data-private` under `redactNames`.\n role={activate ? \"button\" : undefined}\n tabIndex={activate ? 0 : undefined}\n aria-label={activate ? name : undefined}\n data-private={activate && redactNames ? \"\" : undefined}\n cursor={activate ? \"pointer\" : undefined}\n // CSS beats a presentation attribute, so this recolours the rect's own stroke\n // for as long as the tile has keyboard focus.\n className={activate ? \"outline-none [&:focus-visible>rect]:stroke-[var(--brand)]\" : undefined}\n >\n {/* The 2px gutter is the card showing through, not a white grid — on a dark\n surface white lines glare harder than the tiles. */}\n <rect\n x={px}\n y={py}\n width={pw}\n height={ph}\n fill={tileFill}\n stroke=\"var(--bg-surface)\"\n strokeWidth={2}\n rx={3}\n />\n {label != null && (\n <text\n // A plain attribute, so the host's `[data-private]` rule reaches an SVG\n // <text> exactly as it reaches a table cell.\n data-private={redactNames ? \"\" : undefined}\n x={textX}\n y={py + 15 * scale}\n direction={dir}\n fill={ink}\n stroke={halo}\n strokeWidth={2.5}\n strokeLinejoin=\"round\"\n paintOrder=\"stroke\"\n fontSize={labelSize}\n fontWeight={600}\n style={{ pointerEvents: \"none\" }}\n >\n {label}\n </text>\n )}\n {note != null && (\n <text\n x={textX}\n y={py + 29 * scale}\n direction={dir}\n fill={ink}\n fillOpacity={0.85}\n stroke={halo}\n strokeWidth={2.5}\n strokeLinejoin=\"round\"\n paintOrder=\"stroke\"\n fontSize={noteSize}\n fontWeight={500}\n style={{ pointerEvents: \"none\" }}\n >\n {note}\n </text>\n )}\n </g>\n );\n}\n\n// ── Chart ─────────────────────────────────────────────────────────────────\n\n// Nodes carry their own `fill`, and `ChartTooltipContent` falls back to it for the\n// swatch, so no series config is needed.\nconst TREEMAP_CONFIG: ChartConfig = {};\n\n/**\n * The shared tooltip with the whole bubble tagged `data-private`, for names that are\n * the user's own data.\n *\n * The bubble, not just the name: `ChartTooltipContent` prints the name either from\n * `item.name` or from a config `label`, and only the config route could be tagged — but\n * a config entry keyed by a payee name is not a CSS identifier, and `ChartStyle` would\n * (rightly) refuse it. The bubble holds the name and the value, and the value cell is\n * already `data-private`, so tagging the wrapper hides nothing extra.\n *\n * recharts CLONES the `content` element to inject `active`/`payload`, so this has to BE\n * that element — a `<div>` wrapped around `<ChartTooltipContent />` in the JSX would\n * take those props itself and hand the tooltip nothing.\n */\nfunction RedactedTooltipContent(props: ComponentProps<typeof ChartTooltipContent>) {\n return (\n <div data-private>\n <ChartTooltipContent {...props} />\n </div>\n );\n}\n\nconst DEFAULT_NUMBER = new Intl.NumberFormat();\n\n/**\n * Tile chart: one rectangle per {@link TreemapNode}, sized by `value`, coloured by\n * index from the chart ramp (or by the node's own `fill`), labelled in whichever of the\n * kit's two inks measures better against it. Hover shows the name and the formatted\n * value; `onNodeClick` turns the tiles into buttons.\n *\n * Renders nothing when no node has a positive value — an empty box is a hole in the\n * card, and the caller's own sentence is the better answer there.\n *\n * Needs `recharts`, like everything behind `@eifi1/ui-kit/chart`.\n */\nexport function Treemap({\n data,\n valueFormatter,\n height = \"20rem\",\n onNodeClick,\n redactNames = false,\n nodeNote,\n maxTiles,\n colors,\n style,\n ...rest\n}: TreemapProps) {\n const tokenHex = useChartTokenHex();\n // Read off the DOM after mount: the `dir` a treemap sits under is an ancestor's\n // attribute, and SVG text anchoring cannot follow it by CSS (see TreemapCell). Again\n // on new data, because a map that had nothing to draw rendered no root to read.\n const rootRef = useRef<HTMLDivElement>(null);\n const [dir, setDir] = useState<Direction>(\"ltr\");\n useLayoutEffect(() => {\n setDir(dirOf(rootRef.current));\n }, [data]);\n const ramp = colors && colors.length > 0 ? colors : tokenHex;\n // Coloured by index into `data` AS GIVEN, before anything is dropped: a caller\n // painting a legend or a second map from the same list must land on the same hue\n // for the same row, and filtering first would shift every colour after a gap.\n const nodes = data\n .map((d, idx) => ({\n id: d.id,\n name: d.name,\n size: d.value,\n fill: d.fill ?? ramp[idx % ramp.length],\n // recharts hands each node's fields to its cell; a field left out of this map\n // never reaches `TreemapCell` (0.8.0 shipped `labelColor` without it — keksdose).\n labelColor: d.labelColor,\n }))\n .filter((n) => Number.isFinite(n.size) && n.size > 0)\n .slice(0, maxTiles ?? undefined);\n if (nodes.length === 0) return null;\n const box = chartHeightProps(height);\n\n // The share is of what is DRAWN, so it always reads as \"of this picture\".\n const total = nodes.reduce((acc, n) => acc + n.size, 0);\n const format = (v: number): ReactNode => {\n const share = total > 0 ? v / total : undefined;\n return valueFormatter ? valueFormatter(v, share) : DEFAULT_NUMBER.format(v);\n };\n const Content = redactNames ? RedactedTooltipContent : ChartTooltipContent;\n return (\n <ChartContainer\n {...rest}\n ref={rootRef}\n config={TREEMAP_CONFIG}\n className={cn(box.className, rest.className)}\n style={{ ...style, ...box.style }}\n >\n {/* No chart-level `stroke`: recharts spreads it onto the <svg> root, from where\n it inherits into every label (see TreemapCell). Each cell strokes its own rect. */}\n <RechartsTreemap\n data={nodes}\n dataKey=\"size\"\n fill=\"var(--chart-1)\"\n isAnimationActive={false}\n content={\n <TreemapCell onNodeClick={onNodeClick} redactNames={redactNames} nodeNote={nodeNote} dir={dir} />\n }\n >\n <ChartTooltip content={<Content hideLabel valueFormatter={format} />} />\n </RechartsTreemap>\n </ChartContainer>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/treemap.tsx"],"sourcesContent":["// A tile chart (\"treemap\"): one rectangle per node, its AREA the node's share of what\n// is drawn. Lifted out of keksdose's reports, where five cards drew it (spending by\n// group and by category, net worth by account and by type, allocation, payees, food\n// groups) — every domain word is gone and every string it could print is a prop.\n//\n// It uses the kit's own chart shell (`ChartContainer`, `ChartTooltipContent`) and the\n// kit's own ink rule (`textOn`), so it re-skins with the palette like every other\n// chart. The one thing it cannot take from a CSS var is the tile colour: the label ink\n// is MEASURED against the fill, and a `var(--chart-3)` string measures as nothing — see\n// `useChartTokenHex` below for how the concrete hex is found instead.\nimport { useLayoutEffect, useRef, useState, useSyncExternalStore } from \"react\";\nimport type { ComponentProps, ComponentPropsWithoutRef, KeyboardEvent, ReactNode } from \"react\";\nimport { Treemap as RechartsTreemap } from \"recharts\";\nimport { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from \"./chart\";\nimport { PALETTE_HEX, PALETTE_SIZE, textOn } from \"../theme/chart-palette\";\nimport { resolveChartColor, useChartEdge } from \"./chart-edge\";\nimport { parseHex } from \"../theme/color\";\nimport { dirOf, type Direction } from \"../lib/direction\";\nimport { cn } from \"../lib/cn\";\nimport { useTextSize } from \"../theme/text-size\";\nimport { chartHeightProps, type ChartHeight } from \"./chart-height\";\nimport { formatWithMinus } from \"../lib/minus\";\n\n/**\n * One tile.\n *\n * `value`, not keksdose's `total`: a treemap's areas are not always money (keksdose\n * already sizes one by transaction COUNT), and `value` is the word every other chart\n * in recharts uses for the number a mark is sized by. Migrating is a rename at the\n * mapping step — `{ id, name, value: row.total }`.\n */\nexport interface TreemapNode {\n /** Stable key, handed back by {@link TreemapProps.onNodeClick} and\n * {@link TreemapProps.nodeNote}. Names can collide; ids must not. */\n id: string;\n name: string;\n /** The tile's area. A node whose value is not a positive finite number is NOT\n * drawn — an area cannot be negative, and recharts lays a negative size out as\n * nonsense rather than refusing it. Saying so is the caller's job: a group that\n * vanishes silently between two views of the same data is a bug report. */\n value: number;\n /** Overrides the by-index palette colour for this tile — e.g. to paint every child\n * in its parent group's colour, so the groups stay legible in one picture. A\n * concrete `#rrggbb` or a bare `var(--chart-N)` keeps the label ink measured — so\n * does `CHART_ROLES.other` (`var(--chart-muted)`), the colour for a \"rest\" bucket,\n * which should not wrap round to a hue the reader matches to a real node;\n * anything else (another var, a `color-mix()`) gets `currentColor` ink, because\n * there is nothing to measure — set `labelColor` then. */\n fill?: string;\n /** The label's ink, for a `fill` the kit cannot measure (a `var()` or `color-mix()`):\n * keksdose's recency plot fills with `color-mix(in srgb, var(--chart-1) N%,\n * transparent)`, where `currentColor` put dark page text on full-strength indigo\n * (~1.5:1). Wins over the measured ink when given. */\n labelColor?: string;\n}\n\n/**\n * Everything a `<div>` takes reaches the chart's root (`ChartContainer`), so a test id\n * or a `data-tour` anchor can find it. `children` is the chart's own.\n */\nexport interface TreemapProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n data: TreemapNode[];\n /**\n * Formats a node's value in the tooltip. Handed the node's SHARE of what is drawn\n * (0–1) as a second argument, because a tile is read for how big it is relative to\n * the rest, and \"€412.30\" alone makes the reader estimate that ratio off the areas.\n * The share is `undefined` when the drawn total is not positive.\n *\n * Default: the bare number through `Intl.NumberFormat` in the runtime's locale. A\n * host that has a currency or a locale passes this — the kit knows neither.\n */\n valueFormatter?: (value: number, share: number | undefined) => ReactNode;\n /** Chart height: a CSS length (`\"20rem\"`, `\"min(20rem, 50dvh)\"`), a Tailwind class,\n * or px. Default `\"20rem\"` — 320 px at Normal, growing with the text size like the\n * labels in the tiles (0.32, docs/text-size-harmonization.md §10.10). */\n height?: ChartHeight;\n /** A tile was activated — by click, or by Enter/Space on a focused tile. Setting\n * this makes every tile a focusable `role=\"button\"` named by its node name. */\n onNodeClick?: (id: string, name: string) => void;\n /**\n * The node names are the user's own data (payees, not categories): the tile labels\n * and the whole tooltip bubble get `data-private`, the same attribute the kit's\n * `Tooltip redact` and `ChartTooltipContent`'s value cell carry, so the host's one\n * privacy rule reaches them. Off by default — a category name is a label of the app.\n */\n redactNames?: boolean;\n /**\n * A second, smaller line under a tile's label — one extra figure per node that the\n * name cannot carry (a year-on-year `+12%`, `new`). Return `undefined` for a node\n * with nothing to add. Never tagged `data-private`: a ratio reveals no amount.\n *\n * ON THE TILE rather than in the tooltip, for two reasons. The tooltip identifies a\n * node by name or by raw value, so a per-node note would be keyed on an amount (two\n * nodes of equal size would swap notes). And a tooltip is a pointer affordance: a\n * phone never hovers, so a figure that only exists on hover does not exist there.\n */\n nodeNote?: (id: string) => string | undefined;\n /**\n * Draw at most this many tiles (the first N drawable nodes, in the order given —\n * sort first). Unbounded by default. A tile under ~44x20px has no room for a label,\n * and a map made of unlabelled rectangles answers nothing; around 24 is where the\n * smallest tile of a skewed series reaches that floor on a phone-width card. The\n * caller owns the \"N more not shown\" line, since it knows how many it passed.\n */\n maxTiles?: number;\n /**\n * The categorical ramp tiles take by index, as concrete hex. Default: the page's\n * live `--chart-1…8` tokens (see `useChartTokenHex`), cycled after eight like\n * `paletteFor` (0.49). Pass one when the host\n * already holds the active palette — e.g. `createPaletteStore`'s `useChartHex()` —\n * so a caller colouring a legend beside the map is guaranteed the same list.\n */\n colors?: readonly string[];\n}\n\n// ── The categorical ramp, as concrete hex ──────────────────────────────────\n//\n// The label ink is measured against the tile's fill, so the fill has to be a colour\n// and not a reference to one. keksdose read it from its palette STORE, which is an\n// app object the kit cannot import. What the kit does own is the token layer: whether\n// a palette arrives through `applyTokenSet` (inline properties on <html>) or through\n// tokens.css (`:root` / `.dark`), the resolved `--chart-N` on <html> is the colour\n// every other chart on the page is being painted with. So that is what is read, and\n// re-read whenever <html>'s class or style changes — which is exactly when a theme\n// toggle or a palette switch lands.\n\nconst CHART_TOKENS = PALETTE_SIZE;\n\n/** The resolved ramp, joined into ONE string so `useSyncExternalStore` compares a\n * primitive: a fresh array per read would never be `Object.is`-equal to the last and\n * would re-render forever. */\nfunction readChartTokens(): string {\n const root = document.documentElement;\n const fallback = root.classList.contains(\"dark\") ? PALETTE_HEX.dark : PALETTE_HEX.light;\n const style = getComputedStyle(root);\n return Array.from({ length: CHART_TOKENS }, (_, i) => {\n const v = style.getPropertyValue(`--chart-${i + 1}`).trim();\n // An unresolved token (a host that never loaded tokens.css, a test DOM) falls\n // back to the built-in ramp for the current theme rather than to no colour.\n return parseHex(v) ? v : fallback[i];\n }).join(\",\");\n}\n\nfunction subscribeChartTokens(onChange: () => void): () => void {\n const observer = new MutationObserver(onChange);\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: [\"class\", \"style\", \"data-theme\"],\n });\n return () => observer.disconnect();\n}\n\nconst SERVER_TOKENS = PALETTE_HEX.light.join(\",\");\n\nfunction useChartTokenHex(): string[] {\n const joined = useSyncExternalStore(subscribeChartTokens, readChartTokens, () => SERVER_TOKENS);\n return joined.split(\",\");\n}\n\nconst CHART_TOKEN_REF = /^var\\(\\s*--chart-(\\d+)\\s*\\)$/;\n\n/**\n * The hex a tile fill measures as: the fill itself when it is hex, the live token when\n * it is a bare `var(--chart-N)` (what `paletteFor` returns), otherwise null. Without\n * the token case a hand-built recharts map coloured with the kit's own `paletteFor`\n * got `currentColor` ink — dark page text on dark indigo in the light theme.\n */\nfunction measurableFill(fill: string, tokens: readonly string[]): string | null {\n if (parseHex(fill)) return fill;\n const m = CHART_TOKEN_REF.exec(fill.trim());\n const hex = m ? tokens[Number(m[1]) - 1] : undefined;\n if (hex && parseHex(hex)) return hex;\n // Any other bare token the page declares — `var(--chart-muted)` for a rest bucket,\n // `var(--chart-9)`, a money colour — read off <html> as it resolves.\n if (typeof document === \"undefined\") return null;\n return resolveChartColor(fill, getComputedStyle(document.documentElement));\n}\n\n// ── Tile ──────────────────────────────────────────────────────────────────\n\n/** The name's size in px at Normal; a tile draws it × the text size's scale (§10.10), and\n * every distance below that is measured in lines grows with it. */\nconst LABEL_FONT_SIZE = 12;\n/** The optional second line, a step down from the name so the tile reads name-first. */\nconst NOTE_FONT_SIZE = 11;\n\n/**\n * Fit a label to its tile: the text itself when it fits, an ellipsised head when it\n * does not, `null` when not even three characters would. Without this a long name ran\n * on across its neighbours and collided with THEIR labels. 0.58em is the average\n * advance of a UI sans at this weight; it only needs to be close, since the tile keeps\n * 6px of padding either side. The full name stays in the tooltip.\n */\nexport function fitLabel(name: string, boxWidth: number, fontSize: number): string | null {\n const max = Math.floor((boxWidth - 12) / (fontSize * 0.58));\n if (max < 3) return null;\n return name.length <= max ? name : `${name.slice(0, max - 1).trimEnd()}…`;\n}\n\n/** What recharts hands a `content` element for each node, plus the three options the\n * chart forwards. Everything optional because recharts clones the element it was\n * given and fills the geometry in — the element as written carries only the options. */\nexport interface TreemapCellProps {\n depth?: number;\n x?: number;\n y?: number;\n width?: number;\n height?: number;\n name?: string;\n fill?: string;\n /** See {@link TreemapNode.labelColor}; recharts hands each node's fields to its cell. */\n labelColor?: string;\n id?: string;\n onNodeClick?: (id: string, name: string) => void;\n redactNames?: boolean;\n nodeNote?: (id: string) => string | undefined;\n /** The page's reading direction, which `Treemap` reads off its own root. In `rtl`\n * the label stands in the tile's top RIGHT corner and is shaped right-to-left, so a\n * clipped name ends in its ellipsis on the reading end. Default `ltr`. */\n dir?: Direction;\n}\n\n/**\n * One tile. Exported so a caller drawing its own recharts `<Treemap>` gets the same\n * label rules.\n *\n * recharts' default cell inherits the chart-level `stroke`, an SVG presentation\n * attribute that lands on the `<svg>` root and cascades into the label — so the glyphs\n * were painted with a 1px outline ON TOP of themselves, which reads as blur. This cell\n * draws its own label with a halo in the TILE colour painted BEHIND the fill\n * (`paint-order: stroke`), which defeats any inherited stroke and keeps the text solid\n * where it overlaps a lighter or darker part of its own tile.\n */\nexport function TreemapCell({\n depth,\n x = 0,\n y = 0,\n width = 0,\n height = 0,\n name,\n fill,\n labelColor,\n id,\n onNodeClick,\n redactNames,\n nodeNote,\n dir = \"ltr\",\n}: TreemapCellProps) {\n // Before the early return: a hook has to run on every render of the cell.\n const tokenHex = useChartTokenHex();\n const { scale } = useTextSize();\n const edgeFor = useChartEdge();\n // Depth 0 is recharts' synthetic root, which spans the whole chart.\n if (!depth) return null;\n // Whole pixels: a rect on a half pixel renders its edge across two half-lit\n // columns, and the text baseline inherits that blur.\n const px = Math.round(x);\n const py = Math.round(y);\n const pw = Math.round(width);\n const ph = Math.round(height);\n const tileFill = fill ?? \"var(--chart-1)\";\n const measured = measurableFill(tileFill, tokenHex);\n const ink = labelColor ?? (measured ? textOn(measured) : \"currentColor\");\n // A tile in a hue under 3:1 against the card gets a 1px ink edge inside the gutter\n // (WCAG 1.4.11, anyplot's rule); a hue that clears it stays bare.\n const edge = measured ? edgeFor(measured) : undefined;\n // The halo only on an OPAQUE fill we could measure. A translucent fill (a\n // `color-mix(… transparent)`) composited twice — once as the tile, once as the halo\n // over it — draws a darker ring round every glyph, which read as doubled text on the\n // faded tiles. `none` still overrides the inherited chart `stroke`.\n const halo = measured ? tileFill : \"none\";\n // Below ~44x20 there is no room for a readable word, so no label at all rather than\n // a one-letter stub bleeding over the tile edge.\n const labelSize = LABEL_FONT_SIZE * scale;\n const noteSize = NOTE_FONT_SIZE * scale;\n const label = pw >= 44 * scale && ph >= 20 * scale ? fitLabel(name ?? \"\", pw, labelSize) : null;\n // Only under a label that is itself drawn — a stray \"+12%\" on an unnamed rectangle\n // names nothing — and only where a second baseline fits: the note's baseline sits\n // 29px down and an 11px glyph hangs ~2px below it, so 36 leaves the same ~5px under\n // the note that the label keeps under itself at the 20px floor.\n const rawNote = id != null && nodeNote ? nodeNote(id) : undefined;\n const note =\n label != null && rawNote != null && ph >= 36 * scale ? fitLabel(rawNote, pw, noteSize) : null;\n // The label's START edge, 6px in from the tile's. The tiles themselves are laid out\n // physically (the plot is not mirrored, see `ChartContainer`) and the chart's SVG is\n // pinned `ltr`, so the direction is set on each <text> explicitly: `start` is then\n // the right end in RTL, and the label grows leftwards from the tile's right edge.\n const textX = dir === \"rtl\" ? px + pw - 6 : px + 6;\n const activate = id != null && onNodeClick ? () => onNodeClick(id, name ?? \"\") : undefined;\n const onKeyDown = activate\n ? (e: KeyboardEvent<SVGGElement>) => {\n if (e.key !== \"Enter\" && e.key !== \" \") return;\n e.preventDefault();\n activate();\n }\n : undefined;\n return (\n <g\n onClick={activate}\n onKeyDown={onKeyDown}\n // A clickable tile is a button to a keyboard and a screen reader too; the\n // drilldown it opens was mouse-only in the app this came from. Named by the node\n // name, which is also why the group carries `data-private` under `redactNames`.\n role={activate ? \"button\" : undefined}\n tabIndex={activate ? 0 : undefined}\n aria-label={activate ? name : undefined}\n data-private={activate && redactNames ? \"\" : undefined}\n cursor={activate ? \"pointer\" : undefined}\n // CSS beats a presentation attribute, so this recolours the rect's own stroke\n // for as long as the tile has keyboard focus.\n className={activate ? \"outline-none [&:focus-visible>rect]:stroke-[var(--brand)]\" : undefined}\n >\n {/* The 2px gutter is the card showing through, not a white grid — on a dark\n surface white lines glare harder than the tiles. */}\n <rect\n x={px}\n y={py}\n width={pw}\n height={ph}\n fill={tileFill}\n stroke=\"var(--bg-surface)\"\n strokeWidth={2}\n rx={3}\n />\n {edge && pw > 3 && ph > 3 && (\n <rect\n data-edge=\"\"\n x={px + 1.5}\n y={py + 1.5}\n width={pw - 3}\n height={ph - 3}\n fill=\"none\"\n stroke={edge}\n strokeWidth={1}\n rx={2}\n style={{ pointerEvents: \"none\" }}\n />\n )}\n {label != null && (\n <text\n // A plain attribute, so the host's `[data-private]` rule reaches an SVG\n // <text> exactly as it reaches a table cell.\n data-private={redactNames ? \"\" : undefined}\n x={textX}\n y={py + 15 * scale}\n direction={dir}\n fill={ink}\n stroke={halo}\n strokeWidth={2.5}\n strokeLinejoin=\"round\"\n paintOrder=\"stroke\"\n fontSize={labelSize}\n fontWeight={600}\n style={{ pointerEvents: \"none\" }}\n >\n {label}\n </text>\n )}\n {note != null && (\n <text\n x={textX}\n y={py + 29 * scale}\n direction={dir}\n fill={ink}\n fillOpacity={0.85}\n stroke={halo}\n strokeWidth={2.5}\n strokeLinejoin=\"round\"\n paintOrder=\"stroke\"\n fontSize={noteSize}\n fontWeight={500}\n style={{ pointerEvents: \"none\" }}\n >\n {note}\n </text>\n )}\n </g>\n );\n}\n\n// ── Chart ─────────────────────────────────────────────────────────────────\n\n// Nodes carry their own `fill`, and `ChartTooltipContent` falls back to it for the\n// swatch, so no series config is needed.\nconst TREEMAP_CONFIG: ChartConfig = {};\n\n/**\n * The shared tooltip with the whole bubble tagged `data-private`, for names that are\n * the user's own data.\n *\n * The bubble, not just the name: `ChartTooltipContent` prints the name either from\n * `item.name` or from a config `label`, and only the config route could be tagged — but\n * a config entry keyed by a payee name is not a CSS identifier, and `ChartStyle` would\n * (rightly) refuse it. The bubble holds the name and the value, and the value cell is\n * already `data-private`, so tagging the wrapper hides nothing extra.\n *\n * recharts CLONES the `content` element to inject `active`/`payload`, so this has to BE\n * that element — a `<div>` wrapped around `<ChartTooltipContent />` in the JSX would\n * take those props itself and hand the tooltip nothing.\n */\nfunction RedactedTooltipContent(props: ComponentProps<typeof ChartTooltipContent>) {\n return (\n <div data-private>\n <ChartTooltipContent {...props} />\n </div>\n );\n}\n\nconst DEFAULT_NUMBER = new Intl.NumberFormat();\n\n/**\n * Tile chart: one rectangle per {@link TreemapNode}, sized by `value`, coloured by\n * index from the chart ramp (or by the node's own `fill`), labelled in whichever of the\n * kit's two inks measures better against it. Hover shows the name and the formatted\n * value; `onNodeClick` turns the tiles into buttons.\n *\n * Renders nothing when no node has a positive value — an empty box is a hole in the\n * card, and the caller's own sentence is the better answer there.\n *\n * Needs `recharts`, like everything behind `@eifi1/ui-kit/chart`.\n */\nexport function Treemap({\n data,\n valueFormatter,\n height = \"20rem\",\n onNodeClick,\n redactNames = false,\n nodeNote,\n maxTiles,\n colors,\n style,\n ...rest\n}: TreemapProps) {\n const tokenHex = useChartTokenHex();\n // Read off the DOM after mount: the `dir` a treemap sits under is an ancestor's\n // attribute, and SVG text anchoring cannot follow it by CSS (see TreemapCell). Again\n // on new data, because a map that had nothing to draw rendered no root to read.\n const rootRef = useRef<HTMLDivElement>(null);\n const [dir, setDir] = useState<Direction>(\"ltr\");\n useLayoutEffect(() => {\n setDir(dirOf(rootRef.current));\n }, [data]);\n const ramp = colors && colors.length > 0 ? colors : tokenHex;\n // Coloured by index into `data` AS GIVEN, before anything is dropped: a caller\n // painting a legend or a second map from the same list must land on the same hue\n // for the same row, and filtering first would shift every colour after a gap.\n const nodes = data\n .map((d, idx) => ({\n id: d.id,\n name: d.name,\n size: d.value,\n fill: d.fill ?? ramp[idx % ramp.length],\n // recharts hands each node's fields to its cell; a field left out of this map\n // never reaches `TreemapCell` (0.8.0 shipped `labelColor` without it — keksdose).\n labelColor: d.labelColor,\n }))\n .filter((n) => Number.isFinite(n.size) && n.size > 0)\n .slice(0, maxTiles ?? undefined);\n if (nodes.length === 0) return null;\n const box = chartHeightProps(height);\n\n // The share is of what is DRAWN, so it always reads as \"of this picture\".\n const total = nodes.reduce((acc, n) => acc + n.size, 0);\n const format = (v: number): ReactNode => {\n const share = total > 0 ? v / total : undefined;\n return valueFormatter ? valueFormatter(v, share) : formatWithMinus(DEFAULT_NUMBER, v);\n };\n const Content = redactNames ? RedactedTooltipContent : ChartTooltipContent;\n return (\n <ChartContainer\n {...rest}\n ref={rootRef}\n config={TREEMAP_CONFIG}\n className={cn(box.className, rest.className)}\n style={{ ...style, ...box.style }}\n >\n {/* No chart-level `stroke`: recharts spreads it onto the <svg> root, from where\n it inherits into every label (see TreemapCell). Each cell strokes its own rect. */}\n <RechartsTreemap\n data={nodes}\n dataKey=\"size\"\n fill=\"var(--chart-1)\"\n isAnimationActive={false}\n content={\n <TreemapCell onNodeClick={onNodeClick} redactNames={redactNames} nodeNote={nodeNote} dir={dir} />\n }\n >\n <ChartTooltip content={<Content hideLabel valueFormatter={format} />} />\n </RechartsTreemap>\n </ChartContainer>\n );\n}\n"],"mappings":";AAySI,SAiBE,KAjBF;AA/RJ,SAAS,iBAAiB,QAAQ,UAAU,4BAA4B;AAExE,SAAS,WAAW,uBAAuB;AAC3C,SAAS,gBAAgB,cAAc,2BAA6C;AACpF,SAAS,aAAa,cAAc,cAAc;AAClD,SAAS,mBAAmB,oBAAoB;AAChD,SAAS,gBAAgB;AACzB,SAAS,aAA6B;AACtC,SAAS,UAAU;AACnB,SAAS,mBAAmB;AAC5B,SAAS,wBAA0C;AACnD,SAAS,uBAAuB;AAyGhC,MAAM,eAAe;AAKrB,SAAS,kBAA0B;AACjC,QAAM,OAAO,SAAS;AACtB,QAAM,WAAW,KAAK,UAAU,SAAS,MAAM,IAAI,YAAY,OAAO,YAAY;AAClF,QAAM,QAAQ,iBAAiB,IAAI;AACnC,SAAO,MAAM,KAAK,EAAE,QAAQ,aAAa,GAAG,CAAC,GAAG,MAAM;AACpD,UAAM,IAAI,MAAM,iBAAiB,WAAW,IAAI,CAAC,EAAE,EAAE,KAAK;AAG1D,WAAO,SAAS,CAAC,IAAI,IAAI,SAAS,CAAC;AAAA,EACrC,CAAC,EAAE,KAAK,GAAG;AACb;AAEA,SAAS,qBAAqB,UAAkC;AAC9D,QAAM,WAAW,IAAI,iBAAiB,QAAQ;AAC9C,WAAS,QAAQ,SAAS,iBAAiB;AAAA,IACzC,YAAY;AAAA,IACZ,iBAAiB,CAAC,SAAS,SAAS,YAAY;AAAA,EAClD,CAAC;AACD,SAAO,MAAM,SAAS,WAAW;AACnC;AAEA,MAAM,gBAAgB,YAAY,MAAM,KAAK,GAAG;AAEhD,SAAS,mBAA6B;AACpC,QAAM,SAAS,qBAAqB,sBAAsB,iBAAiB,MAAM,aAAa;AAC9F,SAAO,OAAO,MAAM,GAAG;AACzB;AAEA,MAAM,kBAAkB;AAQxB,SAAS,eAAe,MAAc,QAA0C;AAC9E,MAAI,SAAS,IAAI,EAAG,QAAO;AAC3B,QAAM,IAAI,gBAAgB,KAAK,KAAK,KAAK,CAAC;AAC1C,QAAM,MAAM,IAAI,OAAO,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI;AAC3C,MAAI,OAAO,SAAS,GAAG,EAAG,QAAO;AAGjC,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,SAAO,kBAAkB,MAAM,iBAAiB,SAAS,eAAe,CAAC;AAC3E;AAMA,MAAM,kBAAkB;AAExB,MAAM,iBAAiB;AAShB,SAAS,SAAS,MAAc,UAAkB,UAAiC;AACxF,QAAM,MAAM,KAAK,OAAO,WAAW,OAAO,WAAW,KAAK;AAC1D,MAAI,MAAM,EAAG,QAAO;AACpB,SAAO,KAAK,UAAU,MAAM,OAAO,GAAG,KAAK,MAAM,GAAG,MAAM,CAAC,EAAE,QAAQ,CAAC;AACxE;AAoCO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,QAAQ;AAAA,EACR,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,MAAM;AACR,GAAqB;AAEnB,QAAM,WAAW,iBAAiB;AAClC,QAAM,EAAE,MAAM,IAAI,YAAY;AAC9B,QAAM,UAAU,aAAa;AAE7B,MAAI,CAAC,MAAO,QAAO;AAGnB,QAAM,KAAK,KAAK,MAAM,CAAC;AACvB,QAAM,KAAK,KAAK,MAAM,CAAC;AACvB,QAAM,KAAK,KAAK,MAAM,KAAK;AAC3B,QAAM,KAAK,KAAK,MAAM,MAAM;AAC5B,QAAM,WAAW,QAAQ;AACzB,QAAM,WAAW,eAAe,UAAU,QAAQ;AAClD,QAAM,MAAM,eAAe,WAAW,OAAO,QAAQ,IAAI;AAGzD,QAAM,OAAO,WAAW,QAAQ,QAAQ,IAAI;AAK5C,QAAM,OAAO,WAAW,WAAW;AAGnC,QAAM,YAAY,kBAAkB;AACpC,QAAM,WAAW,iBAAiB;AAClC,QAAM,QAAQ,MAAM,KAAK,SAAS,MAAM,KAAK,QAAQ,SAAS,QAAQ,IAAI,IAAI,SAAS,IAAI;AAK3F,QAAM,UAAU,MAAM,QAAQ,WAAW,SAAS,EAAE,IAAI;AACxD,QAAM,OACJ,SAAS,QAAQ,WAAW,QAAQ,MAAM,KAAK,QAAQ,SAAS,SAAS,IAAI,QAAQ,IAAI;AAK3F,QAAM,QAAQ,QAAQ,QAAQ,KAAK,KAAK,IAAI,KAAK;AACjD,QAAM,WAAW,MAAM,QAAQ,cAAc,MAAM,YAAY,IAAI,QAAQ,EAAE,IAAI;AACjF,QAAM,YAAY,WACd,CAAC,MAAkC;AACjC,QAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,IAAK;AACxC,MAAE,eAAe;AACjB,aAAS;AAAA,EACX,IACA;AACJ,SACE;AAAA,IAAC;AAAA;AAAA,MACC,SAAS;AAAA,MACT;AAAA,MAIA,MAAM,WAAW,WAAW;AAAA,MAC5B,UAAU,WAAW,IAAI;AAAA,MACzB,cAAY,WAAW,OAAO;AAAA,MAC9B,gBAAc,YAAY,cAAc,KAAK;AAAA,MAC7C,QAAQ,WAAW,YAAY;AAAA,MAG/B,WAAW,WAAW,8DAA8D;AAAA,MAIpF;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,GAAG;AAAA,YACH,GAAG;AAAA,YACH,OAAO;AAAA,YACP,QAAQ;AAAA,YACR,MAAM;AAAA,YACN,QAAO;AAAA,YACP,aAAa;AAAA,YACb,IAAI;AAAA;AAAA,QACN;AAAA,QACC,QAAQ,KAAK,KAAK,KAAK,KACtB;AAAA,UAAC;AAAA;AAAA,YACC,aAAU;AAAA,YACV,GAAG,KAAK;AAAA,YACR,GAAG,KAAK;AAAA,YACR,OAAO,KAAK;AAAA,YACZ,QAAQ,KAAK;AAAA,YACb,MAAK;AAAA,YACL,QAAQ;AAAA,YACR,aAAa;AAAA,YACb,IAAI;AAAA,YACJ,OAAO,EAAE,eAAe,OAAO;AAAA;AAAA,QACjC;AAAA,QAED,SAAS,QACR;AAAA,UAAC;AAAA;AAAA,YAGC,gBAAc,cAAc,KAAK;AAAA,YACjC,GAAG;AAAA,YACH,GAAG,KAAK,KAAK;AAAA,YACb,WAAW;AAAA,YACX,MAAM;AAAA,YACN,QAAQ;AAAA,YACR,aAAa;AAAA,YACb,gBAAe;AAAA,YACf,YAAW;AAAA,YACX,UAAU;AAAA,YACV,YAAY;AAAA,YACZ,OAAO,EAAE,eAAe,OAAO;AAAA,YAE9B;AAAA;AAAA,QACH;AAAA,QAED,QAAQ,QACP;AAAA,UAAC;AAAA;AAAA,YACC,GAAG;AAAA,YACH,GAAG,KAAK,KAAK;AAAA,YACb,WAAW;AAAA,YACX,MAAM;AAAA,YACN,aAAa;AAAA,YACb,QAAQ;AAAA,YACR,aAAa;AAAA,YACb,gBAAe;AAAA,YACf,YAAW;AAAA,YACX,UAAU;AAAA,YACV,YAAY;AAAA,YACZ,OAAO,EAAE,eAAe,OAAO;AAAA,YAE9B;AAAA;AAAA,QACH;AAAA;AAAA;AAAA,EAEJ;AAEJ;AAMA,MAAM,iBAA8B,CAAC;AAgBrC,SAAS,uBAAuB,OAAmD;AACjF,SACE,oBAAC,SAAI,gBAAY,MACf,8BAAC,uBAAqB,GAAG,OAAO,GAClC;AAEJ;AAEA,MAAM,iBAAiB,IAAI,KAAK,aAAa;AAatC,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAiB;AACf,QAAM,WAAW,iBAAiB;AAIlC,QAAM,UAAU,OAAuB,IAAI;AAC3C,QAAM,CAAC,KAAK,MAAM,IAAI,SAAoB,KAAK;AAC/C,kBAAgB,MAAM;AACpB,WAAO,MAAM,QAAQ,OAAO,CAAC;AAAA,EAC/B,GAAG,CAAC,IAAI,CAAC;AACT,QAAM,OAAO,UAAU,OAAO,SAAS,IAAI,SAAS;AAIpD,QAAM,QAAQ,KACX,IAAI,CAAC,GAAG,SAAS;AAAA,IAChB,IAAI,EAAE;AAAA,IACN,MAAM,EAAE;AAAA,IACR,MAAM,EAAE;AAAA,IACR,MAAM,EAAE,QAAQ,KAAK,MAAM,KAAK,MAAM;AAAA;AAAA;AAAA,IAGtC,YAAY,EAAE;AAAA,EAChB,EAAE,EACD,OAAO,CAAC,MAAM,OAAO,SAAS,EAAE,IAAI,KAAK,EAAE,OAAO,CAAC,EACnD,MAAM,GAAG,YAAY,MAAS;AACjC,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,QAAM,MAAM,iBAAiB,MAAM;AAGnC,QAAM,QAAQ,MAAM,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,MAAM,CAAC;AACtD,QAAM,SAAS,CAAC,MAAyB;AACvC,UAAM,QAAQ,QAAQ,IAAI,IAAI,QAAQ;AACtC,WAAO,iBAAiB,eAAe,GAAG,KAAK,IAAI,gBAAgB,gBAAgB,CAAC;AAAA,EACtF;AACA,QAAM,UAAU,cAAc,yBAAyB;AACvD,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,KAAK;AAAA,MACL,QAAQ;AAAA,MACR,WAAW,GAAG,IAAI,WAAW,KAAK,SAAS;AAAA,MAC3C,OAAO,EAAE,GAAG,OAAO,GAAG,IAAI,MAAM;AAAA,MAIhC;AAAA,QAAC;AAAA;AAAA,UACC,MAAM;AAAA,UACN,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,mBAAmB;AAAA,UACnB,SACE,oBAAC,eAAY,aAA0B,aAA0B,UAAoB,KAAU;AAAA,UAGjG,8BAAC,gBAAa,SAAS,oBAAC,WAAQ,WAAS,MAAC,gBAAgB,QAAQ,GAAI;AAAA;AAAA,MACxE;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import * as ReactRouter from "react-router";
3
4
  import { useLocation, useSearchParams } from "react-router";
4
5
  import { useKitTableStorageKeyPrefix } from "../i18n/kit-labels.js";
5
6
  import { readStored, writeStored } from "../lib/safe-storage.js";
@@ -48,6 +49,38 @@ function urlKeys(prefix = "") {
48
49
  search: prefix + URL_SEARCH_KEY
49
50
  };
50
51
  }
52
+ function useRouterSearch() {
53
+ const [searchParams, setSearchParams] = useSearchParams();
54
+ const location = useLocation();
55
+ return { searchParams, setSearchParams, locationState: location.state };
56
+ }
57
+ const NO_ROUTER_SEARCH = {
58
+ searchParams: new URLSearchParams(),
59
+ setSearchParams: () => {
60
+ },
61
+ locationState: null
62
+ };
63
+ function useNoRouterSearch() {
64
+ return NO_ROUTER_SEARCH;
65
+ }
66
+ function readInRouterContext() {
67
+ try {
68
+ return ReactRouter.useInRouterContext ?? (() => true);
69
+ } catch {
70
+ return () => true;
71
+ }
72
+ }
73
+ const useInRouterContext = readInRouterContext();
74
+ function useTableSearch(urlSync, owner) {
75
+ const inRouter = useInRouterContext();
76
+ if (!inRouter && urlSync && import.meta.env?.DEV) {
77
+ throw new Error(
78
+ `${owner} urlSync requires a Router: render it inside a react-router Router (BrowserRouter, a data router or a MemoryRouter), or leave urlSync off.`
79
+ );
80
+ }
81
+ const useSearch = inRouter ? useRouterSearch : useNoRouterSearch;
82
+ return useSearch();
83
+ }
51
84
  function resolveTableUrlSync(urlSync) {
52
85
  if (!urlSync) return { enabled: false, prefix: "" };
53
86
  return { enabled: true, prefix: urlSync === true ? "" : urlSync.prefix ?? "" };
@@ -131,8 +164,7 @@ function useTableState({
131
164
  }) {
132
165
  const { enabled: urlSync, prefix: urlPrefix } = resolveTableUrlSync(urlSyncOption);
133
166
  const keys = useMemo(() => urlKeys(urlPrefix), [urlPrefix]);
134
- const [searchParams, setSearchParams] = useSearchParams();
135
- const location = useLocation();
167
+ const { searchParams, setSearchParams, locationState } = useTableSearch(urlSync, "DataTable");
136
168
  const [urlInitial] = useState(
137
169
  () => urlSync ? readTableUrlState(searchParams, columns, urlPrefix) : {}
138
170
  );
@@ -215,7 +247,7 @@ function useTableState({
215
247
  if (!urlSync) return;
216
248
  if (lastUrl.current === urlSig) return;
217
249
  lastUrl.current = urlSig;
218
- writeUrlState(setSearchParams, columns, sorts, filters, pageSize, page, defaultPageSize, void 0, location.state, keys);
250
+ writeUrlState(setSearchParams, columns, sorts, filters, pageSize, page, defaultPageSize, void 0, locationState, keys);
219
251
  }, [urlSync, urlSig]);
220
252
  return {
221
253
  sorts,
@@ -285,8 +317,7 @@ function useTableUrlState({
285
317
  const urlSync = resolved.enabled;
286
318
  const urlPrefix = urlPrefixOption ?? resolved.prefix;
287
319
  const keys = useMemo(() => urlKeys(urlPrefix), [urlPrefix]);
288
- const [searchParams, setSearchParams] = useSearchParams();
289
- const location = useLocation();
320
+ const { searchParams, setSearchParams, locationState } = useTableSearch(urlSync, "useTableUrlState");
290
321
  const [state, setState] = useState(() => {
291
322
  const url = urlSync ? readTableUrlState(searchParams, columns, urlPrefix) : {};
292
323
  return {
@@ -312,7 +343,7 @@ function useTableUrlState({
312
343
  state.page,
313
344
  defaultPageSize,
314
345
  manageSearch ? state.search : void 0,
315
- location.state,
346
+ locationState,
316
347
  keys
317
348
  );
318
349
  }, [urlSync, urlSig]);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/use-table-state.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { SetStateAction } from \"react\";\nimport { useLocation, useSearchParams } from \"react-router\";\nimport { useKitTableStorageKeyPrefix } from \"../i18n/kit-labels\";\nimport { readStored, writeStored } from \"../lib/safe-storage\";\nimport {\n decodeFilterValue,\n encodeFilterValue,\n isFilterActive,\n resolveFilter,\n} from \"./data-table-filters\";\nimport { decodeSorts, encodeSorts, normalizeSorts, type SortState } from \"./data-table-sort\";\nimport type { DataTableColumn, FilterState, ServerPagination } from \"./data-table\";\n\n// ---------- Persisted + URL-encoded view state ----------\n//\n// Moved here with `useTableState` when `DataTable` was split: these are what the hook\n// reads and writes, and nothing else in the table ever called them.\n\ninterface PersistedState {\n // Multi-sort array; blobs from before the upgrade hold a single {key, dir}\n // object (or null) — read back through normalizeSorts.\n sort: SortState[] | { key: string; dir: string } | null;\n filters: FilterState;\n pageSize: number | \"all\";\n widths?: Record<string, number>;\n hidden?: string[];\n /** 0.48: the `defaultHidden` columns the user ticked back on — an entry that beats the\n * default. Written only when non-empty, so a table without such columns stores what\n * it always did. */\n shown?: string[];\n}\n\n/** The hidden set as drawn: the stored one plus every `defaultHidden` column the view\n * holds no entry for (neither hidden nor `shown`). */\nfunction withDefaultHidden(view: { hidden: Set<string>; shown: Set<string> }, defaults: string[]): Set<string> {\n const extra = defaults.filter((k) => !view.hidden.has(k) && !view.shown.has(k));\n return extra.length === 0 ? view.hidden : new Set([...view.hidden, ...extra]);\n}\n\nexport const DEFAULT_PERSIST_PREFIX = \"hbui-table:\";\n\nfunction loadPersisted(storageKey: string | undefined, prefix: string): Partial<PersistedState> {\n if (!storageKey) return {};\n const raw = readStored(prefix + storageKey);\n if (!raw) return {};\n try {\n const parsed = JSON.parse(raw) as Partial<PersistedState>;\n return parsed && typeof parsed === \"object\" ? parsed : {};\n } catch {\n return {}; // corrupt JSON — the storage access itself is guarded by readStored\n }\n}\n\n/** The blob that would be stored — also the value the effect below compares by, so\n * the comparison and the write can never disagree about what \"unchanged\" means. */\nfunction serializePersisted(state: PersistedState): string {\n return JSON.stringify(state);\n}\n\n/** A stored page size as a table holds it — `\"all\"` is `Infinity` — or `fallback`. */\nfunction storedPageSize(stored: Partial<PersistedState>, fallback: number): number {\n if (stored.pageSize === \"all\") return Infinity;\n if (typeof stored.pageSize === \"number\" && stored.pageSize > 0) return stored.pageSize;\n return fallback;\n}\n\n// ---------- URL sync helpers ----------\n\nconst URL_FILTER_PREFIX = \"f.\";\nconst URL_SORT_KEY = \"sort\";\nconst URL_PAGE_KEY = \"p\";\nconst URL_PAGE_SIZE_KEY = \"ps\";\n/** The free-text search term — managed only by {@link useTableUrlState}, and only when\n * it is asked to (`search: true`); a plain `urlSync` table leaves a `q` alone. */\nconst URL_SEARCH_KEY = \"q\";\n\n/**\n * How a table is mirrored into the URL: `true`, or `{ prefix }` for a table that shares\n * its page with another URL-synced one. The prefix is put verbatim in front of EVERY key\n * the table writes and reads — `{ prefix: \"inv.\" }` gives `inv.f.<column>`, `inv.sort`,\n * `inv.p`, `inv.ps` (and `inv.q`) — so two tables on one page stop sharing a sort and a\n * page. Without one the keys are unprefixed, as they always were.\n */\nexport type TableUrlSync = boolean | { prefix?: string };\n\n/** The query keys of one table, with its prefix applied. */\ninterface UrlKeys {\n filter: string;\n sort: string;\n page: string;\n pageSize: string;\n search: string;\n}\n\nfunction urlKeys(prefix = \"\"): UrlKeys {\n return {\n filter: prefix + URL_FILTER_PREFIX,\n sort: prefix + URL_SORT_KEY,\n page: prefix + URL_PAGE_KEY,\n pageSize: prefix + URL_PAGE_SIZE_KEY,\n search: prefix + URL_SEARCH_KEY,\n };\n}\n\n/** `urlSync` split into \"is it on\" and \"under which prefix\". */\nfunction resolveTableUrlSync(urlSync: TableUrlSync | undefined): { enabled: boolean; prefix: string } {\n if (!urlSync) return { enabled: false, prefix: \"\" };\n return { enabled: true, prefix: urlSync === true ? \"\" : (urlSync.prefix ?? \"\") };\n}\n\n/** What {@link readTableUrlState} found in a query string. */\nexport interface TableUrlState {\n sorts?: SortState[];\n filters?: FilterState;\n /** `Infinity` for `ps=all`. */\n pageSize?: number;\n /** 0-based (the URL's `p` is 1-based). */\n page?: number;\n}\n\n/**\n * What a table's URL says, read out of a query string: the column filters (`f.<key>`),\n * the sort, the page size and the 0-based page. Absent keys are absent here too, so a\n * caller can tell \"not in the link\" from \"the default\". The same reader `urlSync` uses\n * on mount, exported for an owner that keeps the state itself — see\n * {@link useTableUrlState}.\n *\n * Pass the ROUTER's query string — the one `setSearchParams` writes — and the table's\n * `prefix` when it has one (see {@link TableUrlSync}).\n *\n * This read `window.location.search` until 0.7.0, which is the same string only under a\n * BrowserRouter. Under a HashRouter the router's query lives after the `#`\n * (`/#/tx?f.payee=Rewe`) and `window.location.search` is empty: a shared link opened\n * unfiltered, and the table's first write then replaced the link's filter with its own\n * empty state. Reading and writing through `useSearchParams` is one source for both\n * directions under every router, MemoryRouter included.\n */\nexport function readTableUrlState<T>(\n sp: URLSearchParams,\n columns: DataTableColumn<T>[],\n prefix?: string,\n): TableUrlState {\n const keys = urlKeys(prefix);\n const out: TableUrlState = {};\n\n const filters: FilterState = {};\n for (const col of columns) {\n const filter = resolveFilter(col);\n if (!filter) continue;\n const raw = sp.get(keys.filter + col.key);\n if (raw == null) continue;\n filters[col.key] = decodeFilterValue(filter, raw);\n }\n if (Object.keys(filters).length) out.filters = filters;\n\n const sortRaw = sp.get(keys.sort);\n if (sortRaw) {\n const decoded = decodeSorts(sortRaw, new Set(columns.map((c) => c.key)));\n if (decoded.length) out.sorts = decoded;\n }\n\n const psRaw = sp.get(keys.pageSize);\n if (psRaw === \"all\") out.pageSize = Infinity;\n else if (psRaw && Number(psRaw) > 0) out.pageSize = Number(psRaw);\n\n const pRaw = sp.get(keys.page);\n if (pRaw && Number(pRaw) >= 1) out.page = Number(pRaw) - 1;\n\n return out;\n}\n\n/**\n * The managed part of the query string, as a string, for comparing by VALUE.\n *\n * Everything the table puts in the address and nothing else, in a fixed order.\n * `columns` is walked for its keys only, which is why it can be left out of the\n * effect's dependencies without lying: a new column ARRAY carrying the same keys\n * produces the same signature, and a genuinely different set of filterable columns\n * produces a different one.\n */\nfunction urlSignature<T>(\n columns: DataTableColumn<T>[],\n sorts: SortState[],\n filters: FilterState,\n pageSize: number,\n page: number,\n defaultPageSize: number,\n keys: UrlKeys,\n): string {\n const parts: string[] = [];\n for (const col of columns) {\n const state = filters[col.key];\n if (!state || !isFilterActive(state)) continue;\n const enc = encodeFilterValue(state);\n if (enc != null) parts.push(`${keys.filter}${col.key}=${enc}`);\n }\n parts.push(`${keys.sort}=${encodeSorts(sorts)}`);\n parts.push(\n `${keys.pageSize}=${\n pageSize === Infinity ? \"all\" : pageSize === defaultPageSize ? \"\" : String(pageSize)\n }`,\n );\n parts.push(`${keys.page}=${page > 0 ? String(page + 1) : \"\"}`);\n return parts.join(\"&\");\n}\n\nfunction writeUrlState<T>(\n setSearchParams: ReturnType<typeof useSearchParams>[1],\n columns: DataTableColumn<T>[],\n sorts: SortState[],\n filters: FilterState,\n pageSize: number,\n page: number,\n defaultPageSize: number,\n // `undefined`: the search term is not ours to manage, and a `q` is left as it is.\n search: string | undefined,\n // The current entry's route state, carried over the replace. A query rewrite is not\n // a navigation, and dropping the state would, for one, lose the mark a URL-bound\n // dialog's opener leaves there (see useDialogParam).\n state: unknown,\n keys: UrlKeys,\n): void {\n setSearchParams(\n (prev) => {\n const next = new URLSearchParams(prev);\n // clear managed keys\n for (const col of columns) next.delete(keys.filter + col.key);\n next.delete(keys.sort);\n next.delete(keys.page);\n next.delete(keys.pageSize);\n if (search !== undefined) {\n next.delete(keys.search);\n if (search.trim()) next.set(keys.search, search);\n }\n // write filters\n for (const col of columns) {\n const state = filters[col.key];\n if (!state || !isFilterActive(state)) continue;\n const enc = encodeFilterValue(state);\n if (enc != null) next.set(keys.filter + col.key, enc);\n }\n // write sort\n const sortEnc = encodeSorts(sorts);\n if (sortEnc) next.set(keys.sort, sortEnc);\n // write page size if non-default\n if (pageSize === Infinity) next.set(keys.pageSize, \"all\");\n else if (pageSize !== defaultPageSize) next.set(keys.pageSize, String(pageSize));\n // write page if not first\n if (page > 0) next.set(keys.page, String(page + 1));\n return next;\n },\n { replace: true, state },\n );\n}\n\n/**\n * Where a table's view lives: what it is sorted and filtered by, which page, how wide\n * the columns are and which are hidden — plus the two places that outlives a render.\n *\n * Lifted out of `DataTable`, which was 984 lines and twenty hooks in one function. This\n * is the part that is genuinely about STATE rather than about drawing, and the three\n * rules it encodes are easy to lose among the markup:\n *\n * * **The URL is read once, on mount.** A shared or deep link populates the initial view;\n * after that the state is local, or the browser's own history would fight the user's\n * next click.\n * * **Sorting and filtering are optionally CONTROLLED.** A server-driven table passes\n * state and a callback and owns them; everything else is self-owned. That is the\n * `sortsProp ?? internalSorts` pair, and it is why the setters are exposed separately.\n * * **Persistence writes on every change, the URL only when asked.** `storageKey` is the\n * per-table identity; `urlSync` is opt-in because only some tables want their view in\n * a shareable address.\n * * **A controlled filter, sort or page size that changes sends the table back to page\n * 1.** See the note at the reset below.\n */\nexport function useTableState<T>({\n columns,\n storageKey,\n storageKeyPrefix,\n urlSync: urlSyncOption,\n defaultPageSize,\n sortsProp,\n filtersProp,\n pageSizeProp,\n}: {\n columns: DataTableColumn<T>[];\n storageKey?: string;\n storageKeyPrefix: string;\n urlSync?: TableUrlSync;\n defaultPageSize: number;\n sortsProp?: SortState[];\n filtersProp?: FilterState;\n pageSizeProp?: number;\n}) {\n const { enabled: urlSync, prefix: urlPrefix } = resolveTableUrlSync(urlSyncOption);\n const keys = useMemo(() => urlKeys(urlPrefix), [urlPrefix]);\n // Called unconditionally (a Router is required either way — see src/data-table.ts),\n // and the SAME pair is used to read and to write; see `readTableUrlState`.\n const [searchParams, setSearchParams] = useSearchParams();\n const location = useLocation();\n // Read URL once on mount so URL-encoded views (e.g. shared links, deep links from\n // other pages) populate initial state. After mount, state is local.\n const [urlInitial] = useState<TableUrlState>(() =>\n urlSync ? readTableUrlState(searchParams, columns, urlPrefix) : {},\n );\n const initial = useMemo(\n () => loadPersisted(storageKey, storageKeyPrefix),\n [storageKey, storageKeyPrefix],\n );\n const [internalSorts, setInternalSorts] = useState<SortState[]>(() =>\n urlInitial.sorts !== undefined ? urlInitial.sorts : normalizeSorts(initial.sort),\n );\n const [internalFilters, setInternalFilters] = useState<FilterState>(\n () => urlInitial.filters ?? initial.filters ?? {},\n );\n // Controlled when the owner passes state + callback (server-driven tables);\n // self-owned otherwise.\n const sorts = sortsProp ?? internalSorts;\n const filters = filtersProp ?? internalFilters;\n const [page, setPage] = useState(() => urlInitial.page ?? 0);\n\n // ---- Controlled filters/sorts reset the page ----\n //\n // A self-owned table resets to page 1 in the filter handlers. A CONTROLLED one could\n // not: the handlers hand the change to the owner and return, and `page` lives here,\n // where the owner has no way to reach it — so narrowing a filter from outside (a\n // saved view, a chip dismissed above the table) left a client-side table on page 4\n // of what was now two pages, clamped to the last one instead of the first.\n //\n // So the table does it: whenever the controlled VALUE changes, back to the first\n // page. Compared by value, because a controlled prop is a fresh object on every\n // parent render. Adjusted during render rather than in an effect, so the stale page\n // is never painted and never written to the URL. Not on mount: a deep link's `p=3`\n // must survive the owner's first render. (In `serverPagination` mode this state is\n // unused — the owner's `page` is the page, and resetting it stays the owner's job.)\n // A controlled page size counts too: the groups of `TranslationReviewPanel` share one,\n // and a group paged deep into its rows starts over when another group's pager changes it.\n const controlledSig =\n sortsProp !== undefined || filtersProp !== undefined || pageSizeProp !== undefined\n ? JSON.stringify([sortsProp ?? null, filtersProp ?? null, String(pageSizeProp)])\n : null;\n const [seenControlledSig, setSeenControlledSig] = useState(controlledSig);\n if (seenControlledSig !== controlledSig) {\n setSeenControlledSig(controlledSig);\n setPage(0);\n }\n const [internalPageSize, setPageSize] = useState<number>(() =>\n urlInitial.pageSize !== undefined ? urlInitial.pageSize : storedPageSize(initial, defaultPageSize),\n );\n const pageSize = pageSizeProp ?? internalPageSize;\n const [widths, setWidths] = useState<Record<string, number>>(() => initial.widths ?? {});\n // Hidden columns (0.48, keksdose #617): what the user chose — `hidden`, and `shown`\n // for a `defaultHidden` column ticked back on — kept apart from the defaults, which\n // are applied on read and never stored. Keyed on the default keys as a string: every\n // call site builds `columns` inline, so the array is new on every render.\n const [view, setView] = useState(() => ({\n hidden: new Set<string>(initial.hidden ?? []),\n shown: new Set<string>(initial.shown ?? []),\n }));\n const defaultHiddenSig = columns\n .filter((c) => c.defaultHidden)\n .map((c) => c.key)\n .join(\"\\u0000\");\n const hiddenCols = useMemo(\n () => withDefaultHidden(view, defaultHiddenSig ? defaultHiddenSig.split(\"\\u0000\") : []),\n [view, defaultHiddenSig],\n );\n // The panel hands over the next DRAWN set; what changed is recorded as a choice.\n const setHiddenCols = useCallback(\n (action: SetStateAction<Set<string>>) =>\n setView((v) => {\n const defaults = defaultHiddenSig ? defaultHiddenSig.split(\"\\u0000\") : [];\n const prev = withDefaultHidden(v, defaults);\n const next = typeof action === \"function\" ? action(prev) : action;\n const hidden = new Set(v.hidden);\n const shown = new Set(v.shown);\n for (const k of next) {\n if (prev.has(k)) continue;\n hidden.add(k);\n shown.delete(k);\n }\n for (const k of prev) {\n if (next.has(k)) continue;\n hidden.delete(k);\n if (defaults.includes(k)) shown.add(k);\n }\n return { hidden, shown };\n }),\n [defaultHiddenSig],\n );\n const [showSettings, setShowSettings] = useState(false);\n const headRefs = useMemo(() => new Map<string, HTMLTableCellElement>(), []);\n\n // ---- Writing out: both of these compare by VALUE ----\n //\n // A CONTROLLED table (`sorts`/`filters` props plus callbacks — every server-driven\n // one) receives a freshly-built object on every render of the page around it, and\n // `{}` is never `===` the `{}` before it. Keyed on identity, both effects below\n // therefore fired on every unrelated parent render: persistence wrote the same JSON\n // into `localStorage` each time — a synchronous main-thread write, on a page whose\n // other half may be a form being typed into — and the URL one handed react-router a\n // `replace` it answers with a render, which is the loop the audit records. A `ref`\n // holding what was last written is what makes \"changed\" mean changed.\n const lastPersisted = useRef<string | null>(null);\n const persisted = !storageKey\n ? \"\"\n : serializePersisted({\n sort: sorts,\n filters,\n pageSize: pageSize === Infinity ? \"all\" : pageSize,\n widths,\n hidden: Array.from(view.hidden),\n ...(view.shown.size > 0 ? { shown: Array.from(view.shown) } : {}),\n });\n useEffect(() => {\n if (!storageKey) return;\n // The destination is part of the signature: two tables can hold identical state,\n // and a table that swaps `storageKey` has to write under the new one even though\n // nothing about the view changed.\n const target = storageKeyPrefix + storageKey;\n const sig = `${target}\\u0000${persisted}`;\n if (lastPersisted.current === sig) return;\n lastPersisted.current = sig;\n writeStored(target, persisted);\n }, [storageKey, storageKeyPrefix, persisted]);\n\n const lastUrl = useRef<string | null>(null);\n const urlSig = urlSync\n ? urlSignature(columns, sorts, filters, pageSize, page, defaultPageSize, keys)\n : \"\";\n useEffect(() => {\n if (!urlSync) return;\n if (lastUrl.current === urlSig) return;\n lastUrl.current = urlSig;\n // `columns` is deliberately not a dependency and does not need to be: the\n // signature above is computed from it, so a column set that would produce a\n // different address produces a different signature first.\n writeUrlState(setSearchParams, columns, sorts, filters, pageSize, page, defaultPageSize, undefined, location.state, keys);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [urlSync, urlSig]);\n\n return {\n sorts,\n filters,\n setInternalSorts,\n setInternalFilters,\n page,\n setPage,\n pageSize,\n setPageSize,\n widths,\n setWidths,\n hiddenCols,\n setHiddenCols,\n showSettings,\n setShowSettings,\n headRefs,\n };\n}\n\n// ---------- One view over several tables ----------\n\n/** What {@link useSharedTableView} keeps. */\nexport interface SharedTableView {\n sorts: SortState[];\n setSorts: (next: SortState[]) => void;\n /** `Infinity` for \"all\", as a table holds it. */\n pageSize: number;\n setPageSize: (next: number) => void;\n}\n\n/**\n * One table view — the sort and the page size — for an owner that draws it as SEVERAL\n * tables over one `storageKey`: `TranslationReviewPanel`'s groups, which sort and page\n * alike (0.35, Kurvenschmiede: its grouped review stored no view at all). Pass them to\n * each table as `sorts`/`onSortsChange` and `pageSize`/`onPageSizeChange`.\n *\n * Kept in the very blob a `DataTable` with that `storageKey` keeps, not in a format of\n * its own, so the single table and the groups read each other's view. What the blob\n * holds besides (filters, column widths, hidden columns) is carried over as stored, for\n * the single table to find as it left it. Read when `active` turns on — on mount, or\n * when the owner switches to this view — and written on a change, never on a read. The\n * prefix resolves as DataTable's does: prop > provider > the kit's.\n */\nexport function useSharedTableView({\n active,\n storageKey,\n storageKeyPrefix,\n defaultPageSize,\n}: {\n active: boolean;\n storageKey?: string;\n storageKeyPrefix?: string;\n defaultPageSize: number;\n}): SharedTableView {\n const providerPrefix = useKitTableStorageKeyPrefix();\n const prefix = storageKeyPrefix ?? providerPrefix ?? DEFAULT_PERSIST_PREFIX;\n const read = () => {\n const stored = active ? loadPersisted(storageKey, prefix) : {};\n return { sorts: normalizeSorts(stored.sort), pageSize: storedPageSize(stored, defaultPageSize) };\n };\n const [view, setView] = useState(read);\n // Adjusted during render when `active` turns on, so the first render of the groups\n // already has the view the single table left, not a frame of the default.\n const [wasActive, setWasActive] = useState(active);\n if (active !== wasActive) {\n setWasActive(active);\n if (active) setView(read());\n }\n const change = (patch: Partial<typeof view>) => {\n const next = { ...view, ...patch };\n setView(next);\n if (!active || !storageKey) return;\n const stored = loadPersisted(storageKey, prefix);\n const blob: PersistedState = {\n sort: next.sorts,\n filters: stored.filters ?? {},\n pageSize: next.pageSize === Infinity ? \"all\" : next.pageSize,\n widths: stored.widths,\n hidden: stored.hidden,\n };\n writeStored(prefix + storageKey, serializePersisted(blob));\n };\n return {\n sorts: view.sorts,\n setSorts: (sorts) => change({ sorts }),\n pageSize: view.pageSize,\n setPageSize: (pageSize) => change({ pageSize }),\n };\n}\n\n// ---------- Owner-held table state, mirrored to the URL ----------\n\nexport interface UseTableUrlStateOptions<T> {\n /** The table's columns — read for their keys and filter types only. */\n columns: DataTableColumn<T>[];\n /** Default 25. A page size equal to it is left out of the URL. */\n defaultPageSize?: number;\n /** Mirror the state into the URL and seed it from there on mount. Default true.\n * `{ prefix }` puts a prefix in front of every key, for a page with two URL-synced\n * tables — see {@link TableUrlSync}. */\n urlSync?: TableUrlSync;\n /** Shorthand for `urlSync: { prefix }`; wins over a prefix given there. */\n urlPrefix?: string;\n /** Also hold a free-text search term, under `q`. Off by default, so a `q` that\n * belongs to someone else on the page is never touched. */\n search?: boolean;\n}\n\n/** The part of {@link DataTableProps} a server-driven table takes from its owner. */\nexport interface TableUrlStateProps {\n serverPagination: ServerPagination;\n filters: FilterState;\n onFiltersChange: (next: FilterState) => void;\n sorts: SortState[];\n onSortsChange: (next: SortState[]) => void;\n}\n\nexport interface UseTableUrlStateReturn {\n /** 0-based. */\n page: number;\n setPage: (page: number) => void;\n pageSize: number;\n /** Also back to the first page. */\n setPageSize: (pageSize: number) => void;\n filters: FilterState;\n /** Also back to the first page. */\n setFilters: (next: FilterState) => void;\n sorts: SortState[];\n /** Also back to the first page. */\n setSorts: (next: SortState[]) => void;\n /** The search term (`\"\"` unless `search: true`). */\n search: string;\n /** Also back to the first page. */\n setSearch: (next: string) => void;\n /**\n * Everything the table needs, to spread onto it:\n * `<DataTable {...table.tableProps(data?.total ?? 0, { isLoading })} rows={…} … />`.\n */\n tableProps: (total: number, options?: { isLoading?: boolean }) => TableUrlStateProps;\n}\n\ninterface OwnedState {\n sorts: SortState[];\n filters: FilterState;\n pageSize: number;\n page: number;\n search: string;\n}\n\n/**\n * The controlled state of a `serverPagination` table — filters, sorts, page, page size\n * and optionally a search term — kept by the OWNER and mirrored into the URL in the\n * table's own scheme (`f.<key>`, `sort`, `p`, `ps`, and `q`).\n *\n * A server-driven table owns none of this: the owner has to hold it to turn it into\n * the query that fetches `rows`, and until now it also had to restate the table's URL\n * codec to keep the view bookmarkable (kastlan's use-server-table.ts copied\n * `readUrlState` / `writeUrlState` to do it). Same rules as `urlSync`:\n *\n * * **The URL is read once, on mount** — a shared or deep link (see {@link filterHref})\n * seeds the view; after that the state is local and written out with `replace`.\n * * **Any change but the page itself goes back to page 1** — a narrower filter must not\n * leave the user on page 7 of 2.\n * * **Written by value**: an unchanged view writes nothing, however often the owner\n * renders.\n *\n * Mapping the state onto the backend's query parameters stays the owner's: that is the\n * part the kit cannot know.\n */\nexport function useTableUrlState<T>({\n columns,\n defaultPageSize = 25,\n urlSync: urlSyncOption = true,\n urlPrefix: urlPrefixOption,\n search: manageSearch = false,\n}: UseTableUrlStateOptions<T>): UseTableUrlStateReturn {\n const resolved = resolveTableUrlSync(urlSyncOption);\n const urlSync = resolved.enabled;\n const urlPrefix = urlPrefixOption ?? resolved.prefix;\n const keys = useMemo(() => urlKeys(urlPrefix), [urlPrefix]);\n const [searchParams, setSearchParams] = useSearchParams();\n const location = useLocation();\n const [state, setState] = useState<OwnedState>(() => {\n const url = urlSync ? readTableUrlState(searchParams, columns, urlPrefix) : {};\n return {\n sorts: url.sorts ?? [],\n filters: url.filters ?? {},\n pageSize: url.pageSize ?? defaultPageSize,\n page: url.page ?? 0,\n search: urlSync && manageSearch ? (searchParams.get(keys.search) ?? \"\") : \"\",\n };\n });\n\n const lastUrl = useRef<string | null>(null);\n const urlSig = urlSync\n ? urlSignature(columns, state.sorts, state.filters, state.pageSize, state.page, defaultPageSize, keys) +\n (manageSearch ? `&${keys.search}=${state.search.trim() ? state.search : \"\"}` : \"\")\n : \"\";\n useEffect(() => {\n if (!urlSync) return;\n // First run included: a link that arrives with `p=1` or a default `ps` is\n // normalised to the table's own spelling, as `urlSync` does.\n if (lastUrl.current === urlSig) return;\n lastUrl.current = urlSig;\n writeUrlState(\n setSearchParams,\n columns,\n state.sorts,\n state.filters,\n state.pageSize,\n state.page,\n defaultPageSize,\n manageSearch ? state.search : undefined,\n location.state,\n keys,\n );\n // Keyed on the signature, which is computed from everything the write reads — see\n // the same effect in useTableState.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [urlSync, urlSig]);\n\n const setPage = (page: number) => setState((s) => ({ ...s, page }));\n const setPageSize = (pageSize: number) => setState((s) => ({ ...s, pageSize, page: 0 }));\n const setFilters = (filters: FilterState) => setState((s) => ({ ...s, filters, page: 0 }));\n const setSorts = (sorts: SortState[]) => setState((s) => ({ ...s, sorts, page: 0 }));\n const setSearch = (search: string) => setState((s) => ({ ...s, search, page: 0 }));\n\n return {\n page: state.page,\n setPage,\n pageSize: state.pageSize,\n setPageSize,\n filters: state.filters,\n setFilters,\n sorts: state.sorts,\n setSorts,\n search: state.search,\n setSearch,\n tableProps: (total, options) => ({\n serverPagination: {\n page: state.page,\n pageSize: state.pageSize,\n total,\n onPageChange: setPage,\n onPageSizeChange: setPageSize,\n isLoading: options?.isLoading,\n },\n filters: state.filters,\n onFiltersChange: setFilters,\n sorts: state.sorts,\n onSortsChange: setSorts,\n }),\n };\n}\n"],"mappings":"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","names":["state"]}
1
+ {"version":3,"sources":["../../src/components/use-table-state.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { SetStateAction } from \"react\";\nimport * as ReactRouter from \"react-router\";\nimport { useLocation, useSearchParams } from \"react-router\";\nimport { useKitTableStorageKeyPrefix } from \"../i18n/kit-labels\";\nimport { readStored, writeStored } from \"../lib/safe-storage\";\nimport {\n decodeFilterValue,\n encodeFilterValue,\n isFilterActive,\n resolveFilter,\n} from \"./data-table-filters\";\nimport { decodeSorts, encodeSorts, normalizeSorts, type SortState } from \"./data-table-sort\";\nimport type { DataTableColumn, FilterState, ServerPagination } from \"./data-table\";\n\n// ---------- Persisted + URL-encoded view state ----------\n//\n// Moved here with `useTableState` when `DataTable` was split: these are what the hook\n// reads and writes, and nothing else in the table ever called them.\n\ninterface PersistedState {\n // Multi-sort array; blobs from before the upgrade hold a single {key, dir}\n // object (or null) — read back through normalizeSorts.\n sort: SortState[] | { key: string; dir: string } | null;\n filters: FilterState;\n pageSize: number | \"all\";\n widths?: Record<string, number>;\n hidden?: string[];\n /** 0.48: the `defaultHidden` columns the user ticked back on — an entry that beats the\n * default. Written only when non-empty, so a table without such columns stores what\n * it always did. */\n shown?: string[];\n}\n\n/** The hidden set as drawn: the stored one plus every `defaultHidden` column the view\n * holds no entry for (neither hidden nor `shown`). */\nfunction withDefaultHidden(view: { hidden: Set<string>; shown: Set<string> }, defaults: string[]): Set<string> {\n const extra = defaults.filter((k) => !view.hidden.has(k) && !view.shown.has(k));\n return extra.length === 0 ? view.hidden : new Set([...view.hidden, ...extra]);\n}\n\nexport const DEFAULT_PERSIST_PREFIX = \"hbui-table:\";\n\nfunction loadPersisted(storageKey: string | undefined, prefix: string): Partial<PersistedState> {\n if (!storageKey) return {};\n const raw = readStored(prefix + storageKey);\n if (!raw) return {};\n try {\n const parsed = JSON.parse(raw) as Partial<PersistedState>;\n return parsed && typeof parsed === \"object\" ? parsed : {};\n } catch {\n return {}; // corrupt JSON — the storage access itself is guarded by readStored\n }\n}\n\n/** The blob that would be stored — also the value the effect below compares by, so\n * the comparison and the write can never disagree about what \"unchanged\" means. */\nfunction serializePersisted(state: PersistedState): string {\n return JSON.stringify(state);\n}\n\n/** A stored page size as a table holds it — `\"all\"` is `Infinity` — or `fallback`. */\nfunction storedPageSize(stored: Partial<PersistedState>, fallback: number): number {\n if (stored.pageSize === \"all\") return Infinity;\n if (typeof stored.pageSize === \"number\" && stored.pageSize > 0) return stored.pageSize;\n return fallback;\n}\n\n// ---------- URL sync helpers ----------\n\nconst URL_FILTER_PREFIX = \"f.\";\nconst URL_SORT_KEY = \"sort\";\nconst URL_PAGE_KEY = \"p\";\nconst URL_PAGE_SIZE_KEY = \"ps\";\n/** The free-text search term — managed only by {@link useTableUrlState}, and only when\n * it is asked to (`search: true`); a plain `urlSync` table leaves a `q` alone. */\nconst URL_SEARCH_KEY = \"q\";\n\n/**\n * How a table is mirrored into the URL: `true`, or `{ prefix }` for a table that shares\n * its page with another URL-synced one. The prefix is put verbatim in front of EVERY key\n * the table writes and reads — `{ prefix: \"inv.\" }` gives `inv.f.<column>`, `inv.sort`,\n * `inv.p`, `inv.ps` (and `inv.q`) — so two tables on one page stop sharing a sort and a\n * page. Without one the keys are unprefixed, as they always were.\n */\nexport type TableUrlSync = boolean | { prefix?: string };\n\n/** The query keys of one table, with its prefix applied. */\ninterface UrlKeys {\n filter: string;\n sort: string;\n page: string;\n pageSize: string;\n search: string;\n}\n\nfunction urlKeys(prefix = \"\"): UrlKeys {\n return {\n filter: prefix + URL_FILTER_PREFIX,\n sort: prefix + URL_SORT_KEY,\n page: prefix + URL_PAGE_KEY,\n pageSize: prefix + URL_PAGE_SIZE_KEY,\n search: prefix + URL_SEARCH_KEY,\n };\n}\n\n/** What a table reads and writes the URL through: react-router's, or none. */\ninterface TableSearch {\n searchParams: URLSearchParams;\n setSearchParams: ReturnType<typeof useSearchParams>[1];\n locationState: unknown;\n}\n\nfunction useRouterSearch(): TableSearch {\n const [searchParams, setSearchParams] = useSearchParams();\n const location = useLocation();\n return { searchParams, setSearchParams, locationState: location.state };\n}\n\nconst NO_ROUTER_SEARCH: TableSearch = {\n searchParams: new URLSearchParams(),\n setSearchParams: () => {},\n locationState: null,\n};\nfunction useNoRouterSearch(): TableSearch {\n return NO_ROUTER_SEARCH;\n}\n\n/**\n * `useInRouterContext`, read off the namespace inside a `try` like\n * use-search-param-state's probe: a test's `vi.mock(\"react-router\", () => ({ … }))`\n * without it throws on the read. Missing, a table assumes a Router, as before 0.49.\n */\nfunction readInRouterContext(): () => boolean {\n try {\n return (ReactRouter as unknown as { useInRouterContext?: () => boolean }).useInRouterContext ?? (() => true);\n } catch {\n return () => true;\n }\n}\nconst useInRouterContext = readInRouterContext();\n\n/**\n * The URL access a table needs (0.49, KIT-3). Without `urlSync` a table needs no Router:\n * before 0.49 it called `useSearchParams()` unconditionally and threw outside one even\n * with URL sync off. With `urlSync` and no Router, a development build throws a message\n * that names the fix; a production build keeps the view in local state rather than\n * crash the page. Whether a Router is around does not change during a table's life, so\n * which hook runs is stable from render to render.\n */\nfunction useTableSearch(urlSync: boolean, owner: string): TableSearch {\n const inRouter = useInRouterContext();\n if (!inRouter && urlSync && import.meta.env?.DEV) {\n throw new Error(\n `${owner} urlSync requires a Router: render it inside a react-router Router ` +\n \"(BrowserRouter, a data router or a MemoryRouter), or leave urlSync off.\",\n );\n }\n const useSearch = inRouter ? useRouterSearch : useNoRouterSearch;\n return useSearch();\n}\n\n/** `urlSync` split into \"is it on\" and \"under which prefix\". */\nfunction resolveTableUrlSync(urlSync: TableUrlSync | undefined): { enabled: boolean; prefix: string } {\n if (!urlSync) return { enabled: false, prefix: \"\" };\n return { enabled: true, prefix: urlSync === true ? \"\" : (urlSync.prefix ?? \"\") };\n}\n\n/** What {@link readTableUrlState} found in a query string. */\nexport interface TableUrlState {\n sorts?: SortState[];\n filters?: FilterState;\n /** `Infinity` for `ps=all`. */\n pageSize?: number;\n /** 0-based (the URL's `p` is 1-based). */\n page?: number;\n}\n\n/**\n * What a table's URL says, read out of a query string: the column filters (`f.<key>`),\n * the sort, the page size and the 0-based page. Absent keys are absent here too, so a\n * caller can tell \"not in the link\" from \"the default\". The same reader `urlSync` uses\n * on mount, exported for an owner that keeps the state itself — see\n * {@link useTableUrlState}.\n *\n * Pass the ROUTER's query string — the one `setSearchParams` writes — and the table's\n * `prefix` when it has one (see {@link TableUrlSync}).\n *\n * This read `window.location.search` until 0.7.0, which is the same string only under a\n * BrowserRouter. Under a HashRouter the router's query lives after the `#`\n * (`/#/tx?f.payee=Rewe`) and `window.location.search` is empty: a shared link opened\n * unfiltered, and the table's first write then replaced the link's filter with its own\n * empty state. Reading and writing through `useSearchParams` is one source for both\n * directions under every router, MemoryRouter included.\n */\nexport function readTableUrlState<T>(\n sp: URLSearchParams,\n columns: DataTableColumn<T>[],\n prefix?: string,\n): TableUrlState {\n const keys = urlKeys(prefix);\n const out: TableUrlState = {};\n\n const filters: FilterState = {};\n for (const col of columns) {\n const filter = resolveFilter(col);\n if (!filter) continue;\n const raw = sp.get(keys.filter + col.key);\n if (raw == null) continue;\n filters[col.key] = decodeFilterValue(filter, raw);\n }\n if (Object.keys(filters).length) out.filters = filters;\n\n const sortRaw = sp.get(keys.sort);\n if (sortRaw) {\n const decoded = decodeSorts(sortRaw, new Set(columns.map((c) => c.key)));\n if (decoded.length) out.sorts = decoded;\n }\n\n const psRaw = sp.get(keys.pageSize);\n if (psRaw === \"all\") out.pageSize = Infinity;\n else if (psRaw && Number(psRaw) > 0) out.pageSize = Number(psRaw);\n\n const pRaw = sp.get(keys.page);\n if (pRaw && Number(pRaw) >= 1) out.page = Number(pRaw) - 1;\n\n return out;\n}\n\n/**\n * The managed part of the query string, as a string, for comparing by VALUE.\n *\n * Everything the table puts in the address and nothing else, in a fixed order.\n * `columns` is walked for its keys only, which is why it can be left out of the\n * effect's dependencies without lying: a new column ARRAY carrying the same keys\n * produces the same signature, and a genuinely different set of filterable columns\n * produces a different one.\n */\nfunction urlSignature<T>(\n columns: DataTableColumn<T>[],\n sorts: SortState[],\n filters: FilterState,\n pageSize: number,\n page: number,\n defaultPageSize: number,\n keys: UrlKeys,\n): string {\n const parts: string[] = [];\n for (const col of columns) {\n const state = filters[col.key];\n if (!state || !isFilterActive(state)) continue;\n const enc = encodeFilterValue(state);\n if (enc != null) parts.push(`${keys.filter}${col.key}=${enc}`);\n }\n parts.push(`${keys.sort}=${encodeSorts(sorts)}`);\n parts.push(\n `${keys.pageSize}=${\n pageSize === Infinity ? \"all\" : pageSize === defaultPageSize ? \"\" : String(pageSize)\n }`,\n );\n parts.push(`${keys.page}=${page > 0 ? String(page + 1) : \"\"}`);\n return parts.join(\"&\");\n}\n\nfunction writeUrlState<T>(\n setSearchParams: ReturnType<typeof useSearchParams>[1],\n columns: DataTableColumn<T>[],\n sorts: SortState[],\n filters: FilterState,\n pageSize: number,\n page: number,\n defaultPageSize: number,\n // `undefined`: the search term is not ours to manage, and a `q` is left as it is.\n search: string | undefined,\n // The current entry's route state, carried over the replace. A query rewrite is not\n // a navigation, and dropping the state would, for one, lose the mark a URL-bound\n // dialog's opener leaves there (see useDialogParam).\n state: unknown,\n keys: UrlKeys,\n): void {\n setSearchParams(\n (prev) => {\n const next = new URLSearchParams(prev);\n // clear managed keys\n for (const col of columns) next.delete(keys.filter + col.key);\n next.delete(keys.sort);\n next.delete(keys.page);\n next.delete(keys.pageSize);\n if (search !== undefined) {\n next.delete(keys.search);\n if (search.trim()) next.set(keys.search, search);\n }\n // write filters\n for (const col of columns) {\n const state = filters[col.key];\n if (!state || !isFilterActive(state)) continue;\n const enc = encodeFilterValue(state);\n if (enc != null) next.set(keys.filter + col.key, enc);\n }\n // write sort\n const sortEnc = encodeSorts(sorts);\n if (sortEnc) next.set(keys.sort, sortEnc);\n // write page size if non-default\n if (pageSize === Infinity) next.set(keys.pageSize, \"all\");\n else if (pageSize !== defaultPageSize) next.set(keys.pageSize, String(pageSize));\n // write page if not first\n if (page > 0) next.set(keys.page, String(page + 1));\n return next;\n },\n { replace: true, state },\n );\n}\n\n/**\n * Where a table's view lives: what it is sorted and filtered by, which page, how wide\n * the columns are and which are hidden — plus the two places that outlives a render.\n *\n * Lifted out of `DataTable`, which was 984 lines and twenty hooks in one function. This\n * is the part that is genuinely about STATE rather than about drawing, and the three\n * rules it encodes are easy to lose among the markup:\n *\n * * **The URL is read once, on mount.** A shared or deep link populates the initial view;\n * after that the state is local, or the browser's own history would fight the user's\n * next click.\n * * **Sorting and filtering are optionally CONTROLLED.** A server-driven table passes\n * state and a callback and owns them; everything else is self-owned. That is the\n * `sortsProp ?? internalSorts` pair, and it is why the setters are exposed separately.\n * * **Persistence writes on every change, the URL only when asked.** `storageKey` is the\n * per-table identity; `urlSync` is opt-in because only some tables want their view in\n * a shareable address.\n * * **A controlled filter, sort or page size that changes sends the table back to page\n * 1.** See the note at the reset below.\n */\nexport function useTableState<T>({\n columns,\n storageKey,\n storageKeyPrefix,\n urlSync: urlSyncOption,\n defaultPageSize,\n sortsProp,\n filtersProp,\n pageSizeProp,\n}: {\n columns: DataTableColumn<T>[];\n storageKey?: string;\n storageKeyPrefix: string;\n urlSync?: TableUrlSync;\n defaultPageSize: number;\n sortsProp?: SortState[];\n filtersProp?: FilterState;\n pageSizeProp?: number;\n}) {\n const { enabled: urlSync, prefix: urlPrefix } = resolveTableUrlSync(urlSyncOption);\n const keys = useMemo(() => urlKeys(urlPrefix), [urlPrefix]);\n // The SAME pair is used to read and to write; see `readTableUrlState`. No Router is\n // needed without `urlSync` (0.49, KIT-3; see `useTableSearch`).\n const { searchParams, setSearchParams, locationState } = useTableSearch(urlSync, \"DataTable\");\n // Read URL once on mount so URL-encoded views (e.g. shared links, deep links from\n // other pages) populate initial state. After mount, state is local.\n const [urlInitial] = useState<TableUrlState>(() =>\n urlSync ? readTableUrlState(searchParams, columns, urlPrefix) : {},\n );\n const initial = useMemo(\n () => loadPersisted(storageKey, storageKeyPrefix),\n [storageKey, storageKeyPrefix],\n );\n const [internalSorts, setInternalSorts] = useState<SortState[]>(() =>\n urlInitial.sorts !== undefined ? urlInitial.sorts : normalizeSorts(initial.sort),\n );\n const [internalFilters, setInternalFilters] = useState<FilterState>(\n () => urlInitial.filters ?? initial.filters ?? {},\n );\n // Controlled when the owner passes state + callback (server-driven tables);\n // self-owned otherwise.\n const sorts = sortsProp ?? internalSorts;\n const filters = filtersProp ?? internalFilters;\n const [page, setPage] = useState(() => urlInitial.page ?? 0);\n\n // ---- Controlled filters/sorts reset the page ----\n //\n // A self-owned table resets to page 1 in the filter handlers. A CONTROLLED one could\n // not: the handlers hand the change to the owner and return, and `page` lives here,\n // where the owner has no way to reach it — so narrowing a filter from outside (a\n // saved view, a chip dismissed above the table) left a client-side table on page 4\n // of what was now two pages, clamped to the last one instead of the first.\n //\n // So the table does it: whenever the controlled VALUE changes, back to the first\n // page. Compared by value, because a controlled prop is a fresh object on every\n // parent render. Adjusted during render rather than in an effect, so the stale page\n // is never painted and never written to the URL. Not on mount: a deep link's `p=3`\n // must survive the owner's first render. (In `serverPagination` mode this state is\n // unused — the owner's `page` is the page, and resetting it stays the owner's job.)\n // A controlled page size counts too: the groups of `TranslationReviewPanel` share one,\n // and a group paged deep into its rows starts over when another group's pager changes it.\n const controlledSig =\n sortsProp !== undefined || filtersProp !== undefined || pageSizeProp !== undefined\n ? JSON.stringify([sortsProp ?? null, filtersProp ?? null, String(pageSizeProp)])\n : null;\n const [seenControlledSig, setSeenControlledSig] = useState(controlledSig);\n if (seenControlledSig !== controlledSig) {\n setSeenControlledSig(controlledSig);\n setPage(0);\n }\n const [internalPageSize, setPageSize] = useState<number>(() =>\n urlInitial.pageSize !== undefined ? urlInitial.pageSize : storedPageSize(initial, defaultPageSize),\n );\n const pageSize = pageSizeProp ?? internalPageSize;\n const [widths, setWidths] = useState<Record<string, number>>(() => initial.widths ?? {});\n // Hidden columns (0.48, keksdose #617): what the user chose — `hidden`, and `shown`\n // for a `defaultHidden` column ticked back on — kept apart from the defaults, which\n // are applied on read and never stored. Keyed on the default keys as a string: every\n // call site builds `columns` inline, so the array is new on every render.\n const [view, setView] = useState(() => ({\n hidden: new Set<string>(initial.hidden ?? []),\n shown: new Set<string>(initial.shown ?? []),\n }));\n const defaultHiddenSig = columns\n .filter((c) => c.defaultHidden)\n .map((c) => c.key)\n .join(\"\\u0000\");\n const hiddenCols = useMemo(\n () => withDefaultHidden(view, defaultHiddenSig ? defaultHiddenSig.split(\"\\u0000\") : []),\n [view, defaultHiddenSig],\n );\n // The panel hands over the next DRAWN set; what changed is recorded as a choice.\n const setHiddenCols = useCallback(\n (action: SetStateAction<Set<string>>) =>\n setView((v) => {\n const defaults = defaultHiddenSig ? defaultHiddenSig.split(\"\\u0000\") : [];\n const prev = withDefaultHidden(v, defaults);\n const next = typeof action === \"function\" ? action(prev) : action;\n const hidden = new Set(v.hidden);\n const shown = new Set(v.shown);\n for (const k of next) {\n if (prev.has(k)) continue;\n hidden.add(k);\n shown.delete(k);\n }\n for (const k of prev) {\n if (next.has(k)) continue;\n hidden.delete(k);\n if (defaults.includes(k)) shown.add(k);\n }\n return { hidden, shown };\n }),\n [defaultHiddenSig],\n );\n const [showSettings, setShowSettings] = useState(false);\n const headRefs = useMemo(() => new Map<string, HTMLTableCellElement>(), []);\n\n // ---- Writing out: both of these compare by VALUE ----\n //\n // A CONTROLLED table (`sorts`/`filters` props plus callbacks — every server-driven\n // one) receives a freshly-built object on every render of the page around it, and\n // `{}` is never `===` the `{}` before it. Keyed on identity, both effects below\n // therefore fired on every unrelated parent render: persistence wrote the same JSON\n // into `localStorage` each time — a synchronous main-thread write, on a page whose\n // other half may be a form being typed into — and the URL one handed react-router a\n // `replace` it answers with a render, which is the loop the audit records. A `ref`\n // holding what was last written is what makes \"changed\" mean changed.\n const lastPersisted = useRef<string | null>(null);\n const persisted = !storageKey\n ? \"\"\n : serializePersisted({\n sort: sorts,\n filters,\n pageSize: pageSize === Infinity ? \"all\" : pageSize,\n widths,\n hidden: Array.from(view.hidden),\n ...(view.shown.size > 0 ? { shown: Array.from(view.shown) } : {}),\n });\n useEffect(() => {\n if (!storageKey) return;\n // The destination is part of the signature: two tables can hold identical state,\n // and a table that swaps `storageKey` has to write under the new one even though\n // nothing about the view changed.\n const target = storageKeyPrefix + storageKey;\n const sig = `${target}\\u0000${persisted}`;\n if (lastPersisted.current === sig) return;\n lastPersisted.current = sig;\n writeStored(target, persisted);\n }, [storageKey, storageKeyPrefix, persisted]);\n\n const lastUrl = useRef<string | null>(null);\n const urlSig = urlSync\n ? urlSignature(columns, sorts, filters, pageSize, page, defaultPageSize, keys)\n : \"\";\n useEffect(() => {\n if (!urlSync) return;\n if (lastUrl.current === urlSig) return;\n lastUrl.current = urlSig;\n // `columns` is deliberately not a dependency and does not need to be: the\n // signature above is computed from it, so a column set that would produce a\n // different address produces a different signature first.\n writeUrlState(setSearchParams, columns, sorts, filters, pageSize, page, defaultPageSize, undefined, locationState, keys);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [urlSync, urlSig]);\n\n return {\n sorts,\n filters,\n setInternalSorts,\n setInternalFilters,\n page,\n setPage,\n pageSize,\n setPageSize,\n widths,\n setWidths,\n hiddenCols,\n setHiddenCols,\n showSettings,\n setShowSettings,\n headRefs,\n };\n}\n\n// ---------- One view over several tables ----------\n\n/** What {@link useSharedTableView} keeps. */\nexport interface SharedTableView {\n sorts: SortState[];\n setSorts: (next: SortState[]) => void;\n /** `Infinity` for \"all\", as a table holds it. */\n pageSize: number;\n setPageSize: (next: number) => void;\n}\n\n/**\n * One table view — the sort and the page size — for an owner that draws it as SEVERAL\n * tables over one `storageKey`: `TranslationReviewPanel`'s groups, which sort and page\n * alike (0.35, Kurvenschmiede: its grouped review stored no view at all). Pass them to\n * each table as `sorts`/`onSortsChange` and `pageSize`/`onPageSizeChange`.\n *\n * Kept in the very blob a `DataTable` with that `storageKey` keeps, not in a format of\n * its own, so the single table and the groups read each other's view. What the blob\n * holds besides (filters, column widths, hidden columns) is carried over as stored, for\n * the single table to find as it left it. Read when `active` turns on — on mount, or\n * when the owner switches to this view — and written on a change, never on a read. The\n * prefix resolves as DataTable's does: prop > provider > the kit's.\n */\nexport function useSharedTableView({\n active,\n storageKey,\n storageKeyPrefix,\n defaultPageSize,\n}: {\n active: boolean;\n storageKey?: string;\n storageKeyPrefix?: string;\n defaultPageSize: number;\n}): SharedTableView {\n const providerPrefix = useKitTableStorageKeyPrefix();\n const prefix = storageKeyPrefix ?? providerPrefix ?? DEFAULT_PERSIST_PREFIX;\n const read = () => {\n const stored = active ? loadPersisted(storageKey, prefix) : {};\n return { sorts: normalizeSorts(stored.sort), pageSize: storedPageSize(stored, defaultPageSize) };\n };\n const [view, setView] = useState(read);\n // Adjusted during render when `active` turns on, so the first render of the groups\n // already has the view the single table left, not a frame of the default.\n const [wasActive, setWasActive] = useState(active);\n if (active !== wasActive) {\n setWasActive(active);\n if (active) setView(read());\n }\n const change = (patch: Partial<typeof view>) => {\n const next = { ...view, ...patch };\n setView(next);\n if (!active || !storageKey) return;\n const stored = loadPersisted(storageKey, prefix);\n const blob: PersistedState = {\n sort: next.sorts,\n filters: stored.filters ?? {},\n pageSize: next.pageSize === Infinity ? \"all\" : next.pageSize,\n widths: stored.widths,\n hidden: stored.hidden,\n };\n writeStored(prefix + storageKey, serializePersisted(blob));\n };\n return {\n sorts: view.sorts,\n setSorts: (sorts) => change({ sorts }),\n pageSize: view.pageSize,\n setPageSize: (pageSize) => change({ pageSize }),\n };\n}\n\n// ---------- Owner-held table state, mirrored to the URL ----------\n\nexport interface UseTableUrlStateOptions<T> {\n /** The table's columns — read for their keys and filter types only. */\n columns: DataTableColumn<T>[];\n /** Default 25. A page size equal to it is left out of the URL. */\n defaultPageSize?: number;\n /** Mirror the state into the URL and seed it from there on mount. Default true.\n * `{ prefix }` puts a prefix in front of every key, for a page with two URL-synced\n * tables — see {@link TableUrlSync}. */\n urlSync?: TableUrlSync;\n /** Shorthand for `urlSync: { prefix }`; wins over a prefix given there. */\n urlPrefix?: string;\n /** Also hold a free-text search term, under `q`. Off by default, so a `q` that\n * belongs to someone else on the page is never touched. */\n search?: boolean;\n}\n\n/** The part of {@link DataTableProps} a server-driven table takes from its owner. */\nexport interface TableUrlStateProps {\n serverPagination: ServerPagination;\n filters: FilterState;\n onFiltersChange: (next: FilterState) => void;\n sorts: SortState[];\n onSortsChange: (next: SortState[]) => void;\n}\n\nexport interface UseTableUrlStateReturn {\n /** 0-based. */\n page: number;\n setPage: (page: number) => void;\n pageSize: number;\n /** Also back to the first page. */\n setPageSize: (pageSize: number) => void;\n filters: FilterState;\n /** Also back to the first page. */\n setFilters: (next: FilterState) => void;\n sorts: SortState[];\n /** Also back to the first page. */\n setSorts: (next: SortState[]) => void;\n /** The search term (`\"\"` unless `search: true`). */\n search: string;\n /** Also back to the first page. */\n setSearch: (next: string) => void;\n /**\n * Everything the table needs, to spread onto it:\n * `<DataTable {...table.tableProps(data?.total ?? 0, { isLoading })} rows={…} … />`.\n */\n tableProps: (total: number, options?: { isLoading?: boolean }) => TableUrlStateProps;\n}\n\ninterface OwnedState {\n sorts: SortState[];\n filters: FilterState;\n pageSize: number;\n page: number;\n search: string;\n}\n\n/**\n * The controlled state of a `serverPagination` table — filters, sorts, page, page size\n * and optionally a search term — kept by the OWNER and mirrored into the URL in the\n * table's own scheme (`f.<key>`, `sort`, `p`, `ps`, and `q`).\n *\n * A server-driven table owns none of this: the owner has to hold it to turn it into\n * the query that fetches `rows`, and until now it also had to restate the table's URL\n * codec to keep the view bookmarkable (kastlan's use-server-table.ts copied\n * `readUrlState` / `writeUrlState` to do it). Same rules as `urlSync`:\n *\n * * **The URL is read once, on mount** — a shared or deep link (see {@link filterHref})\n * seeds the view; after that the state is local and written out with `replace`.\n * * **Any change but the page itself goes back to page 1** — a narrower filter must not\n * leave the user on page 7 of 2.\n * * **Written by value**: an unchanged view writes nothing, however often the owner\n * renders.\n *\n * Mapping the state onto the backend's query parameters stays the owner's: that is the\n * part the kit cannot know.\n */\nexport function useTableUrlState<T>({\n columns,\n defaultPageSize = 25,\n urlSync: urlSyncOption = true,\n urlPrefix: urlPrefixOption,\n search: manageSearch = false,\n}: UseTableUrlStateOptions<T>): UseTableUrlStateReturn {\n const resolved = resolveTableUrlSync(urlSyncOption);\n const urlSync = resolved.enabled;\n const urlPrefix = urlPrefixOption ?? resolved.prefix;\n const keys = useMemo(() => urlKeys(urlPrefix), [urlPrefix]);\n const { searchParams, setSearchParams, locationState } = useTableSearch(urlSync, \"useTableUrlState\");\n const [state, setState] = useState<OwnedState>(() => {\n const url = urlSync ? readTableUrlState(searchParams, columns, urlPrefix) : {};\n return {\n sorts: url.sorts ?? [],\n filters: url.filters ?? {},\n pageSize: url.pageSize ?? defaultPageSize,\n page: url.page ?? 0,\n search: urlSync && manageSearch ? (searchParams.get(keys.search) ?? \"\") : \"\",\n };\n });\n\n const lastUrl = useRef<string | null>(null);\n const urlSig = urlSync\n ? urlSignature(columns, state.sorts, state.filters, state.pageSize, state.page, defaultPageSize, keys) +\n (manageSearch ? `&${keys.search}=${state.search.trim() ? state.search : \"\"}` : \"\")\n : \"\";\n useEffect(() => {\n if (!urlSync) return;\n // First run included: a link that arrives with `p=1` or a default `ps` is\n // normalised to the table's own spelling, as `urlSync` does.\n if (lastUrl.current === urlSig) return;\n lastUrl.current = urlSig;\n writeUrlState(\n setSearchParams,\n columns,\n state.sorts,\n state.filters,\n state.pageSize,\n state.page,\n defaultPageSize,\n manageSearch ? state.search : undefined,\n locationState,\n keys,\n );\n // Keyed on the signature, which is computed from everything the write reads — see\n // the same effect in useTableState.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [urlSync, urlSig]);\n\n const setPage = (page: number) => setState((s) => ({ ...s, page }));\n const setPageSize = (pageSize: number) => setState((s) => ({ ...s, pageSize, page: 0 }));\n const setFilters = (filters: FilterState) => setState((s) => ({ ...s, filters, page: 0 }));\n const setSorts = (sorts: SortState[]) => setState((s) => ({ ...s, sorts, page: 0 }));\n const setSearch = (search: string) => setState((s) => ({ ...s, search, page: 0 }));\n\n return {\n page: state.page,\n setPage,\n pageSize: state.pageSize,\n setPageSize,\n filters: state.filters,\n setFilters,\n sorts: state.sorts,\n setSorts,\n search: state.search,\n setSearch,\n tableProps: (total, options) => ({\n serverPagination: {\n page: state.page,\n pageSize: state.pageSize,\n total,\n onPageChange: setPage,\n onPageSizeChange: setPageSize,\n isLoading: options?.isLoading,\n },\n filters: state.filters,\n onFiltersChange: setFilters,\n sorts: state.sorts,\n onSortsChange: setSorts,\n }),\n };\n}\n"],"mappings":"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","names":["state"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/data-table.ts"],"sourcesContent":["// `@eifi1/ui-kit/data-table` — the table suite.\n//\n// A re-slicing of the main barrel, not a new API. This entry point requires\n// `react-router`: DataTable calls useSearchParams unconditionally, above any `urlSync`\n// branch, so a Router is needed even with URL sync switched off.\nexport * from \"./components/data-table-labels\";\nexport * from \"./components/data-table-sort\";\nexport * from \"./components/data-table-filters\";\nexport * from \"./components/data-table-pagination\";\nexport { FilterPopover } from \"./components/data-table-filter-popover\";\n// `SortState` comes from data-table-sort above; re-exporting the module wholesale\n// would collide with it, which is why the main barrel names these explicitly too.\nexport { DataTable, DEFAULT_DATA_TABLE_SORT_LABELS } from \"./components/data-table\";\nexport type {\n DataTableSortLabels,\n DataTableColumn,\n // The barrel had these two and this entry did not (refactor audit 2026-10, MOD-5):\n // the types of a column's `cellProps` and `headProps`.\n DataTableCellProps,\n DataTableHeadProps,\n DataTableProps,\n ServerPagination,\n FilterState,\n MobileSwipeActions,\n DataTableDensity,\n DataTableChrome,\n DataTableRowAction,\n DataTableDesktopSwipeActions,\n} from \"./components/data-table\";\n// 0.46.1 (keksdose N3): a swipe plan as `RowActions`' actions — the desktop column's own\n// derivation, for an app's hand-built actions column.\nexport { swipePlanRowActions } from \"./components/data-table-row-actions\";\n// 0.48 (keksdose dev #605): a `groupBy` level (also `mobileGroupBy`'s) and the `loadMore` footer.\nexport type { DataTableGroupBy, DataTableLoadMore } from \"./components/data-table-groups\";\n// 0.48.2 (keksdose dev #632): `virtualize`'s options.\nexport type { DataTableVirtualize } from \"./components/data-table-virtual\";\nexport * from \"./components/data-table-cells\";\nexport { useTableUrlState, readTableUrlState } from \"./components/use-table-state\";\nexport type {\n TableUrlState,\n TableUrlSync,\n TableUrlStateProps,\n UseTableUrlStateOptions,\n UseTableUrlStateReturn,\n} from \"./components/use-table-state\";\n"],"mappings":";AAKA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,qBAAqB;AAG9B,SAAS,WAAW,sCAAsC;AAmB1D,SAAS,2BAA2B;AAKpC,cAAc;AACd,SAAS,kBAAkB,yBAAyB;","names":[]}
1
+ {"version":3,"sources":["../src/data-table.ts"],"sourcesContent":["// `@eifi1/ui-kit/data-table` — the table suite.\n//\n// A re-slicing of the main barrel, not a new API. This entry point imports\n// `react-router` (a peer dependency), but since 0.49 (KIT-3) a Router is needed only for\n// a table with `urlSync`: without it DataTable keeps its view in local state alone.\nexport * from \"./components/data-table-labels\";\nexport * from \"./components/data-table-sort\";\nexport * from \"./components/data-table-filters\";\nexport * from \"./components/data-table-pagination\";\nexport { FilterPopover } from \"./components/data-table-filter-popover\";\n// `SortState` comes from data-table-sort above; re-exporting the module wholesale\n// would collide with it, which is why the main barrel names these explicitly too.\nexport { DataTable, DEFAULT_DATA_TABLE_SORT_LABELS } from \"./components/data-table\";\nexport type {\n DataTableSortLabels,\n DataTableColumn,\n // The barrel had these two and this entry did not (refactor audit 2026-10, MOD-5):\n // the types of a column's `cellProps` and `headProps`.\n DataTableCellProps,\n DataTableHeadProps,\n DataTableProps,\n ServerPagination,\n FilterState,\n MobileSwipeActions,\n DataTableDensity,\n DataTableChrome,\n DataTableRowAction,\n DataTableDesktopSwipeActions,\n} from \"./components/data-table\";\n// 0.46.1 (keksdose N3): a swipe plan as `RowActions`' actions — the desktop column's own\n// derivation, for an app's hand-built actions column.\nexport { swipePlanRowActions } from \"./components/data-table-row-actions\";\n// 0.48 (keksdose dev #605): a `groupBy` level (also `mobileGroupBy`'s) and the `loadMore` footer.\nexport type { DataTableGroupBy, DataTableLoadMore } from \"./components/data-table-groups\";\n// 0.48.2 (keksdose dev #632): `virtualize`'s options.\nexport type { DataTableVirtualize } from \"./components/data-table-virtual\";\nexport * from \"./components/data-table-cells\";\nexport { useTableUrlState, readTableUrlState } from \"./components/use-table-state\";\nexport type {\n TableUrlState,\n TableUrlSync,\n TableUrlStateProps,\n UseTableUrlStateOptions,\n UseTableUrlStateReturn,\n} from \"./components/use-table-state\";\n"],"mappings":";AAKA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,qBAAqB;AAG9B,SAAS,WAAW,sCAAsC;AAmB1D,SAAS,2BAA2B;AAKpC,cAAc;AACd,SAAS,kBAAkB,yBAAyB;","names":[]}
@@ -52,7 +52,7 @@ function DemoBanner({
52
52
  className: cn("sm:text-sm", className),
53
53
  action: /* @__PURE__ */ jsxs(Fragment, { children: [
54
54
  !collapsed && /* @__PURE__ */ jsxs(Fragment, { children: [
55
- /* @__PURE__ */ jsx(Button, { href: accessLink.href, variant: "brand", size: "sm", children: accessLink.label }),
55
+ /* @__PURE__ */ jsx(Button, { href: accessLink.href, variant: "accent", size: "sm", children: accessLink.label }),
56
56
  /* @__PURE__ */ jsx(Button, { href: signInHref, variant: "secondary", size: "sm", children: landing.signIn })
57
57
  ] }),
58
58
  /* @__PURE__ */ jsx(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/demo/demo-banner.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ComponentPropsWithRef } from \"react\";\nimport { ChevronDown, ChevronUp, Timer } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { readSessionStored, writeSessionStored } from \"../lib/safe-storage\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { Button } from \"../components/button\";\nimport { IconButton } from \"../components/icon-button\";\nimport { useAccessAction } from \"../landing/access\";\nimport type { AccessChoice } from \"../landing/access\";\nimport { useLandingLabels } from \"../landing/landing-labels\";\nimport type { LandingLabels } from \"../landing/landing-labels\";\nimport { useDemoCountdown } from \"./demo-countdown\";\nimport type { DemoCountdown, DemoExpiry } from \"./demo-countdown\";\nimport { useDemoLabels } from \"./demo-labels\";\nimport type { DemoLabels } from \"./demo-labels\";\nimport type { DemoModel } from \"./demo-session\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\n\n/** The default `sessionStorage` key of the collapsed state. */\nconst COLLAPSE_KEY = \"kit.demoBanner.collapsed\";\n\n/** \"Demo · 23 h 12 min left\", \"Demo · 50 min left\", the end, or plain \"Demo\". */\nfunction timeText(countdown: DemoCountdown, labels: DemoLabels): string {\n if (countdown.minutesLeft === null) return labels.badge;\n if (countdown.ended) return labels.endedTitle;\n return countdown.lastHour\n ? labels.minutesLeft(countdown.minutes)\n : labels.hoursLeft(countdown.hours, countdown.minutes);\n}\n\n/**\n * A `div`'s props, `ref` included since 0.35 (round 0.35 R4, the follow-up to API-2). They\n * go on to the {@link AlertBanner} strip, so the ref is the strip's own `<div>` — never a\n * link or a button: the banner takes no `href` or `onClick` of its own.\n */\nexport interface DemoBannerProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"onClick\"> {\n /** The demo's end: `expires_at` from the `TokenResponse`, or `demo_expires_at` from\n * `/auth/me` (§5.3). */\n expiresAt: DemoExpiry | null | undefined;\n /** Which line the banner says about the data (§2.3). */\n model: DemoModel;\n /** \"Request access\". */\n access: AccessChoice;\n /** \"Sign in\" — let through for a demo session by `RedirectIfAuthed allowDemo`. Default\n * `/login`. */\n signInHref?: string;\n /** The countdown reached zero: clear the session and replace to `/demo/ended` (§5.5). */\n onEnded?: () => void;\n /** Where the collapsed state is kept for the tab's session. Default\n * `kit.demoBanner.collapsed`. */\n collapseKey?: string;\n /** The clock, for tests and previews. Default `Date.now`. Keep it stable. */\n now?: () => number;\n labels?: LabelOverride<DemoLabels>;\n /** The access words (`landing` namespace) for this instance. */\n landingLabels?: Partial<LandingLabels>;\n}\n\n/**\n * The demo's strip under the top bar, on every page of a demo session\n * (docs/landing-demo-harmonization.md §5.4) — a kit `AlertBanner` strip:\n *\n * - the time left, \"Demo · 23 h 12 min left\" — minutes only in the last hour, and the\n * warning tone in the last ten minutes (`useDemoCountdown`);\n * - one line on the model: R \"You're looking at sample data. Changes aren't possible.\",\n * S \"Your own work is deleted when the demo ends.\";\n * - \"Request access\" (the mail) and \"Sign in\".\n *\n * It collapses to the countdown alone, for the tab's session (`sessionStorage`) — never\n * hidden for good: it is a live state, like keksdose's preview banner, which a demo user\n * gets instead of. At zero it calls `onEnded`.\n *\n * The countdown is no live region: a sentence read out every minute would drown the\n * page. A screen-reader user meets it at the top of the page, and the end page says\n * the rest.\n */\nexport function DemoBanner({\n expiresAt,\n model,\n access,\n signInHref = \"/login\",\n onEnded,\n collapseKey = COLLAPSE_KEY,\n now,\n labels: labelsProp,\n landingLabels,\n className,\n ...rest\n}: DemoBannerProps) {\n const labels = useDemoLabels(labelsProp);\n const landing = useLandingLabels(landingLabels);\n const accessLink = useAccessAction(access, landingLabels);\n const countdown = useDemoCountdown(expiresAt, { now, onEnded });\n // `sessionStorage`, guarded (lib/safe-storage): blocked storage reads as \"not set\",\n // and the banner just opens again next time.\n const [collapsed, setCollapsed] = useState(() => readSessionStored(collapseKey) === \"1\");\n\n const toggle = () => {\n const next = !collapsed;\n setCollapsed(next);\n writeSessionStored(collapseKey, next ? \"1\" : null);\n };\n\n return (\n <AlertBanner\n {...rest}\n variant=\"strip\"\n size=\"sm\"\n tone={countdown.warning || countdown.ended ? \"warning\" : \"info\"}\n icon={<Timer />}\n data-demo-model={model}\n className={cn(\"sm:text-sm\", className)}\n action={\n <>\n {!collapsed && (\n <>\n <Button href={accessLink.href} variant=\"brand\" size=\"sm\">\n {accessLink.label}\n </Button>\n <Button href={signInHref} variant=\"secondary\" size=\"sm\">\n {landing.signIn}\n </Button>\n </>\n )}\n <IconButton\n size=\"2xs\"\n variant=\"ghost\"\n label={labels.details}\n // The icon alone at every text size (docs/text-size-harmonization.md §10.8):\n // a strip's one disclosure chevron beside its two links, whose state the\n // chevron and `aria-expanded` say; \"Details\" as text at Large would push the\n // links onto another line of a strip that is meant to stay one.\n labelVisible={false}\n aria-expanded={!collapsed}\n onClick={toggle}\n >\n {collapsed ? <ChevronDown /> : <ChevronUp />}\n </IconButton>\n </>\n }\n >\n <span className=\"font-semibold\">{timeText(countdown, labels)}</span>\n {!collapsed && (\n <>\n {\" \"}\n <span>{model === \"read-only\" ? labels.readOnly : labels.sandbox}</span>\n </>\n )}\n </AlertBanner>\n );\n}\n"],"mappings":";AA+GY,SAMA,UANA,KAMA,YANA;AA/GZ,SAAS,gBAAgB;AAEzB,SAAS,aAAa,WAAW,aAAa;AAE9C,SAAS,UAAU;AACnB,SAAS,mBAAmB,0BAA0B;AACtD,SAAS,mBAAmB;AAC5B,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,uBAAuB;AAEhC,SAAS,wBAAwB;AAEjC,SAAS,wBAAwB;AAEjC,SAAS,qBAAqB;AAM9B,MAAM,eAAe;AAGrB,SAAS,SAAS,WAA0B,QAA4B;AACtE,MAAI,UAAU,gBAAgB,KAAM,QAAO,OAAO;AAClD,MAAI,UAAU,MAAO,QAAO,OAAO;AACnC,SAAO,UAAU,WACb,OAAO,YAAY,UAAU,OAAO,IACpC,OAAO,UAAU,UAAU,OAAO,UAAU,OAAO;AACzD;AAgDO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,QAAM,SAAS,cAAc,UAAU;AACvC,QAAM,UAAU,iBAAiB,aAAa;AAC9C,QAAM,aAAa,gBAAgB,QAAQ,aAAa;AACxD,QAAM,YAAY,iBAAiB,WAAW,EAAE,KAAK,QAAQ,CAAC;AAG9D,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,MAAM,kBAAkB,WAAW,MAAM,GAAG;AAEvF,QAAM,SAAS,MAAM;AACnB,UAAM,OAAO,CAAC;AACd,iBAAa,IAAI;AACjB,uBAAmB,aAAa,OAAO,MAAM,IAAI;AAAA,EACnD;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,MAAM,UAAU,WAAW,UAAU,QAAQ,YAAY;AAAA,MACzD,MAAM,oBAAC,SAAM;AAAA,MACb,mBAAiB;AAAA,MACjB,WAAW,GAAG,cAAc,SAAS;AAAA,MACrC,QACE,iCACG;AAAA,SAAC,aACA,iCACE;AAAA,8BAAC,UAAO,MAAM,WAAW,MAAM,SAAQ,SAAQ,MAAK,MACjD,qBAAW,OACd;AAAA,UACA,oBAAC,UAAO,MAAM,YAAY,SAAQ,aAAY,MAAK,MAChD,kBAAQ,QACX;AAAA,WACF;AAAA,QAEF;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,OAAO,OAAO;AAAA,YAKd,cAAc;AAAA,YACd,iBAAe,CAAC;AAAA,YAChB,SAAS;AAAA,YAER,sBAAY,oBAAC,eAAY,IAAK,oBAAC,aAAU;AAAA;AAAA,QAC5C;AAAA,SACF;AAAA,MAGF;AAAA,4BAAC,UAAK,WAAU,iBAAiB,mBAAS,WAAW,MAAM,GAAE;AAAA,QAC5D,CAAC,aACA,iCACG;AAAA;AAAA,UACD,oBAAC,UAAM,oBAAU,cAAc,OAAO,WAAW,OAAO,SAAQ;AAAA,WAClE;AAAA;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/demo/demo-banner.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ComponentPropsWithRef } from \"react\";\nimport { ChevronDown, ChevronUp, Timer } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { readSessionStored, writeSessionStored } from \"../lib/safe-storage\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { Button } from \"../components/button\";\nimport { IconButton } from \"../components/icon-button\";\nimport { useAccessAction } from \"../landing/access\";\nimport type { AccessChoice } from \"../landing/access\";\nimport { useLandingLabels } from \"../landing/landing-labels\";\nimport type { LandingLabels } from \"../landing/landing-labels\";\nimport { useDemoCountdown } from \"./demo-countdown\";\nimport type { DemoCountdown, DemoExpiry } from \"./demo-countdown\";\nimport { useDemoLabels } from \"./demo-labels\";\nimport type { DemoLabels } from \"./demo-labels\";\nimport type { DemoModel } from \"./demo-session\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\n\n/** The default `sessionStorage` key of the collapsed state. */\nconst COLLAPSE_KEY = \"kit.demoBanner.collapsed\";\n\n/** \"Demo · 23 h 12 min left\", \"Demo · 50 min left\", the end, or plain \"Demo\". */\nfunction timeText(countdown: DemoCountdown, labels: DemoLabels): string {\n if (countdown.minutesLeft === null) return labels.badge;\n if (countdown.ended) return labels.endedTitle;\n return countdown.lastHour\n ? labels.minutesLeft(countdown.minutes)\n : labels.hoursLeft(countdown.hours, countdown.minutes);\n}\n\n/**\n * A `div`'s props, `ref` included since 0.35 (round 0.35 R4, the follow-up to API-2). They\n * go on to the {@link AlertBanner} strip, so the ref is the strip's own `<div>` — never a\n * link or a button: the banner takes no `href` or `onClick` of its own.\n */\nexport interface DemoBannerProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"onClick\"> {\n /** The demo's end: `expires_at` from the `TokenResponse`, or `demo_expires_at` from\n * `/auth/me` (§5.3). */\n expiresAt: DemoExpiry | null | undefined;\n /** Which line the banner says about the data (§2.3). */\n model: DemoModel;\n /** \"Request access\". */\n access: AccessChoice;\n /** \"Sign in\" — let through for a demo session by `RedirectIfAuthed allowDemo`. Default\n * `/login`. */\n signInHref?: string;\n /** The countdown reached zero: clear the session and replace to `/demo/ended` (§5.5). */\n onEnded?: () => void;\n /** Where the collapsed state is kept for the tab's session. Default\n * `kit.demoBanner.collapsed`. */\n collapseKey?: string;\n /** The clock, for tests and previews. Default `Date.now`. Keep it stable. */\n now?: () => number;\n labels?: LabelOverride<DemoLabels>;\n /** The access words (`landing` namespace) for this instance. */\n landingLabels?: Partial<LandingLabels>;\n}\n\n/**\n * The demo's strip under the top bar, on every page of a demo session\n * (docs/landing-demo-harmonization.md §5.4) — a kit `AlertBanner` strip:\n *\n * - the time left, \"Demo · 23 h 12 min left\" — minutes only in the last hour, and the\n * warning tone in the last ten minutes (`useDemoCountdown`);\n * - one line on the model: R \"You're looking at sample data. Changes aren't possible.\",\n * S \"Your own work is deleted when the demo ends.\";\n * - \"Request access\" (the mail) and \"Sign in\".\n *\n * It collapses to the countdown alone, for the tab's session (`sessionStorage`) — never\n * hidden for good: it is a live state, like keksdose's preview banner, which a demo user\n * gets instead of. At zero it calls `onEnded`.\n *\n * The countdown is no live region: a sentence read out every minute would drown the\n * page. A screen-reader user meets it at the top of the page, and the end page says\n * the rest.\n */\nexport function DemoBanner({\n expiresAt,\n model,\n access,\n signInHref = \"/login\",\n onEnded,\n collapseKey = COLLAPSE_KEY,\n now,\n labels: labelsProp,\n landingLabels,\n className,\n ...rest\n}: DemoBannerProps) {\n const labels = useDemoLabels(labelsProp);\n const landing = useLandingLabels(landingLabels);\n const accessLink = useAccessAction(access, landingLabels);\n const countdown = useDemoCountdown(expiresAt, { now, onEnded });\n // `sessionStorage`, guarded (lib/safe-storage): blocked storage reads as \"not set\",\n // and the banner just opens again next time.\n const [collapsed, setCollapsed] = useState(() => readSessionStored(collapseKey) === \"1\");\n\n const toggle = () => {\n const next = !collapsed;\n setCollapsed(next);\n writeSessionStored(collapseKey, next ? \"1\" : null);\n };\n\n return (\n <AlertBanner\n {...rest}\n variant=\"strip\"\n size=\"sm\"\n tone={countdown.warning || countdown.ended ? \"warning\" : \"info\"}\n icon={<Timer />}\n data-demo-model={model}\n className={cn(\"sm:text-sm\", className)}\n action={\n <>\n {!collapsed && (\n <>\n <Button href={accessLink.href} variant=\"accent\" size=\"sm\">\n {accessLink.label}\n </Button>\n <Button href={signInHref} variant=\"secondary\" size=\"sm\">\n {landing.signIn}\n </Button>\n </>\n )}\n <IconButton\n size=\"2xs\"\n variant=\"ghost\"\n label={labels.details}\n // The icon alone at every text size (docs/text-size-harmonization.md §10.8):\n // a strip's one disclosure chevron beside its two links, whose state the\n // chevron and `aria-expanded` say; \"Details\" as text at Large would push the\n // links onto another line of a strip that is meant to stay one.\n labelVisible={false}\n aria-expanded={!collapsed}\n onClick={toggle}\n >\n {collapsed ? <ChevronDown /> : <ChevronUp />}\n </IconButton>\n </>\n }\n >\n <span className=\"font-semibold\">{timeText(countdown, labels)}</span>\n {!collapsed && (\n <>\n {\" \"}\n <span>{model === \"read-only\" ? labels.readOnly : labels.sandbox}</span>\n </>\n )}\n </AlertBanner>\n );\n}\n"],"mappings":";AA+GY,SAMA,UANA,KAMA,YANA;AA/GZ,SAAS,gBAAgB;AAEzB,SAAS,aAAa,WAAW,aAAa;AAE9C,SAAS,UAAU;AACnB,SAAS,mBAAmB,0BAA0B;AACtD,SAAS,mBAAmB;AAC5B,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,uBAAuB;AAEhC,SAAS,wBAAwB;AAEjC,SAAS,wBAAwB;AAEjC,SAAS,qBAAqB;AAM9B,MAAM,eAAe;AAGrB,SAAS,SAAS,WAA0B,QAA4B;AACtE,MAAI,UAAU,gBAAgB,KAAM,QAAO,OAAO;AAClD,MAAI,UAAU,MAAO,QAAO,OAAO;AACnC,SAAO,UAAU,WACb,OAAO,YAAY,UAAU,OAAO,IACpC,OAAO,UAAU,UAAU,OAAO,UAAU,OAAO;AACzD;AAgDO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,QAAM,SAAS,cAAc,UAAU;AACvC,QAAM,UAAU,iBAAiB,aAAa;AAC9C,QAAM,aAAa,gBAAgB,QAAQ,aAAa;AACxD,QAAM,YAAY,iBAAiB,WAAW,EAAE,KAAK,QAAQ,CAAC;AAG9D,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,MAAM,kBAAkB,WAAW,MAAM,GAAG;AAEvF,QAAM,SAAS,MAAM;AACnB,UAAM,OAAO,CAAC;AACd,iBAAa,IAAI;AACjB,uBAAmB,aAAa,OAAO,MAAM,IAAI;AAAA,EACnD;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,MAAM,UAAU,WAAW,UAAU,QAAQ,YAAY;AAAA,MACzD,MAAM,oBAAC,SAAM;AAAA,MACb,mBAAiB;AAAA,MACjB,WAAW,GAAG,cAAc,SAAS;AAAA,MACrC,QACE,iCACG;AAAA,SAAC,aACA,iCACE;AAAA,8BAAC,UAAO,MAAM,WAAW,MAAM,SAAQ,UAAS,MAAK,MAClD,qBAAW,OACd;AAAA,UACA,oBAAC,UAAO,MAAM,YAAY,SAAQ,aAAY,MAAK,MAChD,kBAAQ,QACX;AAAA,WACF;AAAA,QAEF;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,OAAO,OAAO;AAAA,YAKd,cAAc;AAAA,YACd,iBAAe,CAAC;AAAA,YAChB,SAAS;AAAA,YAER,sBAAY,oBAAC,eAAY,IAAK,oBAAC,aAAU;AAAA;AAAA,QAC5C;AAAA,SACF;AAAA,MAGF;AAAA,4BAAC,UAAK,WAAU,iBAAiB,mBAAS,WAAW,MAAM,GAAE;AAAA,QAC5D,CAAC,aACA,iCACG;AAAA;AAAA,UACD,oBAAC,UAAM,oBAAU,cAAc,OAAO,WAAW,OAAO,SAAQ;AAAA,WAClE;AAAA;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
@@ -30,7 +30,7 @@ function DemoEnded({
30
30
  /* @__PURE__ */ jsx("p", { className: "max-w-prose text-sm text-secondary", children: model === "sandbox" ? labels.endedSandbox : labels.endedReadOnly }),
31
31
  children,
32
32
  /* @__PURE__ */ jsxs("div", { className: "mt-4 flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:flex-wrap sm:justify-center", children: [
33
- /* @__PURE__ */ jsx(Button, { href: restartHref, variant: "brand", children: labels.restart }),
33
+ /* @__PURE__ */ jsx(Button, { href: restartHref, variant: "accent", children: labels.restart }),
34
34
  /* @__PURE__ */ jsx(Button, { href: accessLink.href, variant: "secondary", children: accessLink.label }),
35
35
  /* @__PURE__ */ jsx(Button, { href: signInHref, variant: "secondary", children: landing.signIn })
36
36
  ] })
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/demo/demo-ended.tsx"],"sourcesContent":["import type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { Hourglass } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { Button } from \"../components/button\";\nimport { useNoIndex } from \"../hooks/use-noindex\";\nimport type { AuthHeadingLevel } from \"../auth/status-parts\";\nimport { useAccessAction } from \"../landing/access\";\nimport type { AccessChoice } from \"../landing/access\";\nimport { useLandingLabels } from \"../landing/landing-labels\";\nimport type { LandingLabels } from \"../landing/landing-labels\";\nimport { useDemoLabels } from \"./demo-labels\";\nimport type { DemoLabels } from \"./demo-labels\";\nimport type { DemoModel } from \"./demo-session\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\n\nexport interface DemoEndedProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"title\"> {\n /** Which line the page says about the data (§2.3). */\n model: DemoModel;\n /** \"Request access\". */\n access: AccessChoice;\n /** \"Start a new demo\". Default `/demo` — a new demo may start after one ends, within\n * the per-IP window (§2.7). */\n restartHref?: string;\n /** \"Sign in\". Default `/login`. */\n signInHref?: string;\n /** App content under the line. */\n children?: ReactNode;\n /** `false` adds no `noindex` — for a preview. Default `true` (§4.3). */\n noIndex?: boolean;\n /** Default `h2`, under `AuthLayout`'s `h1`. */\n headingAs?: AuthHeadingLevel;\n labels?: LabelOverride<DemoLabels>;\n /** The access words (`landing` namespace) for this instance. */\n landingLabels?: Partial<LandingLabels>;\n}\n\n/**\n * The `/demo/ended` page's content (docs/landing-demo-harmonization.md §5.5): \"The demo\n * has ended\", one line on what became of the data, and three ways on — \"Start a new\n * demo\", \"Request access\", \"Sign in\".\n *\n * Where a demo lands at `expires_at`, or on ANY 401 while the session is a demo: the\n * client clears the session and replaces to `/demo/ended`, never to `/login`. keksdose's\n * expired demo met an unexplained sign-in form. The server deletes the user at the next\n * reap; nothing is kept.\n *\n * Content, like `NotFoundPage`: put it in the app's `AuthLayout` (its logo, language\n * menu and legal footer). It marks the page `noindex`; robots.txt must NOT disallow it,\n * or a crawler could never read that (§10.8).\n */\nexport function DemoEnded({\n model,\n access,\n restartHref = \"/demo\",\n signInHref = \"/login\",\n children,\n noIndex = true,\n headingAs: Heading = \"h2\",\n labels: labelsProp,\n landingLabels,\n className,\n ...rest\n}: DemoEndedProps) {\n const labels = useDemoLabels(labelsProp);\n const landing = useLandingLabels(landingLabels);\n const accessLink = useAccessAction(access, landingLabels);\n useNoIndex(noIndex);\n return (\n <div {...rest} className={cn(\"flex flex-col items-center gap-2 py-4 text-center\", className)}>\n <Hourglass aria-hidden className=\"size-10 text-placeholder\" />\n <Heading className=\"mt-2 text-xl font-semibold text-primary\">{labels.endedTitle}</Heading>\n <p className=\"max-w-prose text-sm text-secondary\">\n {model === \"sandbox\" ? labels.endedSandbox : labels.endedReadOnly}\n </p>\n {children}\n <div className=\"mt-4 flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:flex-wrap sm:justify-center\">\n <Button href={restartHref} variant=\"brand\">\n {labels.restart}\n </Button>\n <Button href={accessLink.href} variant=\"secondary\">\n {accessLink.label}\n </Button>\n <Button href={signInHref} variant=\"secondary\">\n {landing.signIn}\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":";AAsEM,cAMA,YANA;AArEN,SAAS,iBAAiB;AAE1B,SAAS,UAAU;AACnB,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAE3B,SAAS,uBAAuB;AAEhC,SAAS,wBAAwB;AAEjC,SAAS,qBAAqB;AAwCvB,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,aAAa;AAAA,EACb;AAAA,EACA,UAAU;AAAA,EACV,WAAW,UAAU;AAAA,EACrB,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAmB;AACjB,QAAM,SAAS,cAAc,UAAU;AACvC,QAAM,UAAU,iBAAiB,aAAa;AAC9C,QAAM,aAAa,gBAAgB,QAAQ,aAAa;AACxD,aAAW,OAAO;AAClB,SACE,qBAAC,SAAK,GAAG,MAAM,WAAW,GAAG,qDAAqD,SAAS,GACzF;AAAA,wBAAC,aAAU,eAAW,MAAC,WAAU,4BAA2B;AAAA,IAC5D,oBAAC,WAAQ,WAAU,2CAA2C,iBAAO,YAAW;AAAA,IAChF,oBAAC,OAAE,WAAU,sCACV,oBAAU,YAAY,OAAO,eAAe,OAAO,eACtD;AAAA,IACC;AAAA,IACD,qBAAC,SAAI,WAAU,wFACb;AAAA,0BAAC,UAAO,MAAM,aAAa,SAAQ,SAChC,iBAAO,SACV;AAAA,MACA,oBAAC,UAAO,MAAM,WAAW,MAAM,SAAQ,aACpC,qBAAW,OACd;AAAA,MACA,oBAAC,UAAO,MAAM,YAAY,SAAQ,aAC/B,kBAAQ,QACX;AAAA,OACF;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/demo/demo-ended.tsx"],"sourcesContent":["import type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { Hourglass } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { Button } from \"../components/button\";\nimport { useNoIndex } from \"../hooks/use-noindex\";\nimport type { AuthHeadingLevel } from \"../auth/status-parts\";\nimport { useAccessAction } from \"../landing/access\";\nimport type { AccessChoice } from \"../landing/access\";\nimport { useLandingLabels } from \"../landing/landing-labels\";\nimport type { LandingLabels } from \"../landing/landing-labels\";\nimport { useDemoLabels } from \"./demo-labels\";\nimport type { DemoLabels } from \"./demo-labels\";\nimport type { DemoModel } from \"./demo-session\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\n\nexport interface DemoEndedProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"title\"> {\n /** Which line the page says about the data (§2.3). */\n model: DemoModel;\n /** \"Request access\". */\n access: AccessChoice;\n /** \"Start a new demo\". Default `/demo` — a new demo may start after one ends, within\n * the per-IP window (§2.7). */\n restartHref?: string;\n /** \"Sign in\". Default `/login`. */\n signInHref?: string;\n /** App content under the line. */\n children?: ReactNode;\n /** `false` adds no `noindex` — for a preview. Default `true` (§4.3). */\n noIndex?: boolean;\n /** Default `h2`, under `AuthLayout`'s `h1`. */\n headingAs?: AuthHeadingLevel;\n labels?: LabelOverride<DemoLabels>;\n /** The access words (`landing` namespace) for this instance. */\n landingLabels?: Partial<LandingLabels>;\n}\n\n/**\n * The `/demo/ended` page's content (docs/landing-demo-harmonization.md §5.5): \"The demo\n * has ended\", one line on what became of the data, and three ways on — \"Start a new\n * demo\", \"Request access\", \"Sign in\".\n *\n * Where a demo lands at `expires_at`, or on ANY 401 while the session is a demo: the\n * client clears the session and replaces to `/demo/ended`, never to `/login`. keksdose's\n * expired demo met an unexplained sign-in form. The server deletes the user at the next\n * reap; nothing is kept.\n *\n * Content, like `NotFoundPage`: put it in the app's `AuthLayout` (its logo, language\n * menu and legal footer). It marks the page `noindex`; robots.txt must NOT disallow it,\n * or a crawler could never read that (§10.8).\n */\nexport function DemoEnded({\n model,\n access,\n restartHref = \"/demo\",\n signInHref = \"/login\",\n children,\n noIndex = true,\n headingAs: Heading = \"h2\",\n labels: labelsProp,\n landingLabels,\n className,\n ...rest\n}: DemoEndedProps) {\n const labels = useDemoLabels(labelsProp);\n const landing = useLandingLabels(landingLabels);\n const accessLink = useAccessAction(access, landingLabels);\n useNoIndex(noIndex);\n return (\n <div {...rest} className={cn(\"flex flex-col items-center gap-2 py-4 text-center\", className)}>\n <Hourglass aria-hidden className=\"size-10 text-placeholder\" />\n <Heading className=\"mt-2 text-xl font-semibold text-primary\">{labels.endedTitle}</Heading>\n <p className=\"max-w-prose text-sm text-secondary\">\n {model === \"sandbox\" ? labels.endedSandbox : labels.endedReadOnly}\n </p>\n {children}\n <div className=\"mt-4 flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:flex-wrap sm:justify-center\">\n <Button href={restartHref} variant=\"accent\">\n {labels.restart}\n </Button>\n <Button href={accessLink.href} variant=\"secondary\">\n {accessLink.label}\n </Button>\n <Button href={signInHref} variant=\"secondary\">\n {landing.signIn}\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":";AAsEM,cAMA,YANA;AArEN,SAAS,iBAAiB;AAE1B,SAAS,UAAU;AACnB,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAE3B,SAAS,uBAAuB;AAEhC,SAAS,wBAAwB;AAEjC,SAAS,qBAAqB;AAwCvB,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,aAAa;AAAA,EACb;AAAA,EACA,UAAU;AAAA,EACV,WAAW,UAAU;AAAA,EACrB,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAmB;AACjB,QAAM,SAAS,cAAc,UAAU;AACvC,QAAM,UAAU,iBAAiB,aAAa;AAC9C,QAAM,aAAa,gBAAgB,QAAQ,aAAa;AACxD,aAAW,OAAO;AAClB,SACE,qBAAC,SAAK,GAAG,MAAM,WAAW,GAAG,qDAAqD,SAAS,GACzF;AAAA,wBAAC,aAAU,eAAW,MAAC,WAAU,4BAA2B;AAAA,IAC5D,oBAAC,WAAQ,WAAU,2CAA2C,iBAAO,YAAW;AAAA,IAChF,oBAAC,OAAE,WAAU,sCACV,oBAAU,YAAY,OAAO,eAAe,OAAO,eACtD;AAAA,IACC;AAAA,IACD,qBAAC,SAAI,WAAU,wFACb;AAAA,0BAAC,UAAO,MAAM,aAAa,SAAQ,UAChC,iBAAO,SACV;AAAA,MACA,oBAAC,UAAO,MAAM,WAAW,MAAM,SAAQ,aACpC,qBAAW,OACd;AAAA,MACA,oBAAC,UAAO,MAAM,YAAY,SAAQ,aAC/B,kBAAQ,QACX;AAAA,OACF;AAAA,KACF;AAEJ;","names":[]}
@@ -58,7 +58,7 @@ function DemoStart({
58
58
  labels.starting
59
59
  ] }) : /* @__PURE__ */ jsx(AlertBanner, { tone: refusal.reason === "failed" ? "danger" : "warning", className: "text-start", children: messageOf(refusal, labels) }) }),
60
60
  refusal !== null && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap justify-center gap-3", children: [
61
- /* @__PURE__ */ jsx(Button, { href: accessLink.href, variant: "brand", children: accessLink.label }),
61
+ /* @__PURE__ */ jsx(Button, { href: accessLink.href, variant: "accent", children: accessLink.label }),
62
62
  /* @__PURE__ */ jsx(Button, { href: backHref, variant: "secondary", children: labels.backToStart })
63
63
  ] })
64
64
  ] });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/demo/demo-start.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ComponentPropsWithRef } from \"react\";\n\nimport { cn } from \"../lib/cn\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { Button } from \"../components/button\";\nimport { Spinner } from \"../components/spinner\";\nimport { useNoIndex } from \"../hooks/use-noindex\";\nimport { authErrorCode, isRateLimited, retryAfterSeconds } from \"../auth/auth-errors\";\nimport { useOncePerKey } from \"../auth/status-parts\";\nimport { useAccessAction } from \"../landing/access\";\nimport type { AccessChoice } from \"../landing/access\";\nimport type { LandingLabels } from \"../landing/landing-labels\";\nimport { useDemoLabels } from \"./demo-labels\";\nimport type { DemoLabels } from \"./demo-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\n\n/** Why a start was refused, in the page's terms (§5.2). */\ntype Refusal =\n | { reason: \"rateLimited\"; minutes?: number }\n | { reason: \"capacity\" }\n | { reason: \"unavailable\" }\n | { reason: \"failed\" };\n\n/**\n * The server's answer to `POST /auth/demo-session`, read as a refusal (§5.1, §6.2):\n * `demo_rate_limited` (429, with `Retry-After`), `demo_capacity` (429, without),\n * `demo_disabled` (404) and `demo_not_ready` (503) by their codes; a bare 429 as the\n * per-IP window too; anything else is a plain failure.\n */\nfunction refusalOf(error: unknown): Refusal {\n const code = authErrorCode(error);\n if (code === \"demo_capacity\") return { reason: \"capacity\" };\n if (code === \"demo_disabled\" || code === \"demo_not_ready\") return { reason: \"unavailable\" };\n if (code === \"demo_rate_limited\" || (code === undefined && isRateLimited(error))) {\n const seconds = retryAfterSeconds(error);\n return { reason: \"rateLimited\", minutes: seconds ? Math.ceil(seconds / 60) : undefined };\n }\n return { reason: \"failed\" };\n}\n\nfunction messageOf(refusal: Refusal, labels: DemoLabels): string {\n switch (refusal.reason) {\n case \"rateLimited\":\n return labels.rateLimited(refusal.minutes);\n case \"capacity\":\n return labels.capacity;\n case \"unavailable\":\n return labels.unavailable;\n case \"failed\":\n return labels.failed;\n }\n}\n\n/** The one start a mount makes. */\nconst ONCE = \"demo\";\n\nexport interface DemoStartProps<T> extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n /**\n * Create the demo: the app's `POST /auth/demo-session {locale}`. Resolve with what it\n * answered (the `TokenResponse`, `refresh_token: null`, `expires_at`); reject with the\n * client's error — the codes are read out of it. Called once per mount.\n */\n start: () => Promise<T>;\n /** The demo exists: store the session (and, for kastlan, `kastlan.demoSession`) and\n * replace to the app's home. The page keeps saying \"Starting the demo…\" meanwhile. */\n onStarted: (session: T) => void;\n /** \"Request access\" beside a refusal. */\n access: AccessChoice;\n /** \"Back to the start page\". Default `/`. */\n backHref?: string;\n /** `false` adds no `noindex` — for a preview. Default `true`: `/demo` is never indexed\n * (§4.3). */\n noIndex?: boolean;\n labels?: LabelOverride<DemoLabels>;\n /** The access words (`landing` namespace) for this instance. */\n landingLabels?: Partial<LandingLabels>;\n}\n\n/**\n * The `/demo` page's content (docs/landing-demo-harmonization.md §5.2): it starts the\n * demo ONCE — StrictMode's double mount included, keksdose's ref guard — and says\n * \"Starting the demo…\" until the app has the session and has moved on.\n *\n * A refusal says why, in the visitor's words, and offers \"Request access\" and \"Back to\n * the start page\":\n *\n * | code | says |\n * |---|---|\n * | `demo_rate_limited` (429) | \"Too many demos from this network. Try again in N min.\" — N from `Retry-After` |\n * | `demo_capacity` (429) | \"The demo is full right now. Try again later.\" |\n * | `demo_disabled` (404), `demo_not_ready` (503) | \"The demo isn't available right now.\" |\n * | anything else | \"The demo could not be started. Please try again.\" |\n *\n * keksdose fell back to `/register` with a toast; registration is invitation-only now,\n * so the way on is the access mail.\n *\n * Put it in the app's `AuthLayout`. A live demo session never reaches it: the `/demo`\n * route sits in `RedirectIfAuthed` WITHOUT `allowDemo`, which continues the live demo at\n * its resume target instead of starting a second one. A cold start is the kit's\n * `ServerWakeNotice`'s, once the app's watcher counts this POST\n * (`watchReadsAnd(/\\/auth\\/demo-session\\b/)`).\n */\nexport function DemoStart<T>({\n start,\n onStarted,\n access,\n backHref = \"/\",\n noIndex = true,\n labels: labelsProp,\n landingLabels,\n className,\n ...rest\n}: DemoStartProps<T>) {\n const labels = useDemoLabels(labelsProp);\n const accessLink = useAccessAction(access, landingLabels);\n const [refusal, setRefusal] = useState<Refusal | null>(null);\n useNoIndex(noIndex);\n useOncePerKey<T>(ONCE, () => start(), (_key, result) => {\n if (result.ok) onStarted(result.value);\n else setRefusal(refusalOf(result.error));\n });\n\n return (\n <div {...rest} className={cn(\"flex flex-col items-center gap-4 py-4 text-center\", className)}>\n {/* One live region, there from the start, so the refusal that replaces the\n progress line is announced (a region inserted with its text often is not). */}\n <div role=\"status\" aria-live=\"polite\" className=\"w-full\">\n {refusal === null ? (\n <p className=\"flex items-center justify-center gap-2 text-sm text-secondary\">\n <Spinner label={null} className=\"size-5\" />\n {labels.starting}\n </p>\n ) : (\n <AlertBanner tone={refusal.reason === \"failed\" ? \"danger\" : \"warning\"} className=\"text-start\">\n {messageOf(refusal, labels)}\n </AlertBanner>\n )}\n </div>\n {refusal !== null && (\n <div className=\"flex flex-wrap justify-center gap-3\">\n <Button href={accessLink.href} variant=\"brand\">\n {accessLink.label}\n </Button>\n <Button href={backHref} variant=\"secondary\">\n {labels.backToStart}\n </Button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";AAiIU,SACE,KADF;AAjIV,SAAS,gBAAgB;AAGzB,SAAS,UAAU;AACnB,SAAS,mBAAmB;AAC5B,SAAS,cAAc;AACvB,SAAS,eAAe;AACxB,SAAS,kBAAkB;AAC3B,SAAS,eAAe,eAAe,yBAAyB;AAChE,SAAS,qBAAqB;AAC9B,SAAS,uBAAuB;AAGhC,SAAS,qBAAqB;AAiB9B,SAAS,UAAU,OAAyB;AAC1C,QAAM,OAAO,cAAc,KAAK;AAChC,MAAI,SAAS,gBAAiB,QAAO,EAAE,QAAQ,WAAW;AAC1D,MAAI,SAAS,mBAAmB,SAAS,iBAAkB,QAAO,EAAE,QAAQ,cAAc;AAC1F,MAAI,SAAS,uBAAwB,SAAS,UAAa,cAAc,KAAK,GAAI;AAChF,UAAM,UAAU,kBAAkB,KAAK;AACvC,WAAO,EAAE,QAAQ,eAAe,SAAS,UAAU,KAAK,KAAK,UAAU,EAAE,IAAI,OAAU;AAAA,EACzF;AACA,SAAO,EAAE,QAAQ,SAAS;AAC5B;AAEA,SAAS,UAAU,SAAkB,QAA4B;AAC/D,UAAQ,QAAQ,QAAQ;AAAA,IACtB,KAAK;AACH,aAAO,OAAO,YAAY,QAAQ,OAAO;AAAA,IAC3C,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,OAAO;AAAA,EAClB;AACF;AAGA,MAAM,OAAO;AAgDN,SAAS,UAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,UAAU;AAAA,EACV,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAsB;AACpB,QAAM,SAAS,cAAc,UAAU;AACvC,QAAM,aAAa,gBAAgB,QAAQ,aAAa;AACxD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAyB,IAAI;AAC3D,aAAW,OAAO;AAClB,gBAAiB,MAAM,MAAM,MAAM,GAAG,CAAC,MAAM,WAAW;AACtD,QAAI,OAAO,GAAI,WAAU,OAAO,KAAK;AAAA,QAChC,YAAW,UAAU,OAAO,KAAK,CAAC;AAAA,EACzC,CAAC;AAED,SACE,qBAAC,SAAK,GAAG,MAAM,WAAW,GAAG,qDAAqD,SAAS,GAGzF;AAAA,wBAAC,SAAI,MAAK,UAAS,aAAU,UAAS,WAAU,UAC7C,sBAAY,OACX,qBAAC,OAAE,WAAU,iEACX;AAAA,0BAAC,WAAQ,OAAO,MAAM,WAAU,UAAS;AAAA,MACxC,OAAO;AAAA,OACV,IAEA,oBAAC,eAAY,MAAM,QAAQ,WAAW,WAAW,WAAW,WAAW,WAAU,cAC9E,oBAAU,SAAS,MAAM,GAC5B,GAEJ;AAAA,IACC,YAAY,QACX,qBAAC,SAAI,WAAU,uCACb;AAAA,0BAAC,UAAO,MAAM,WAAW,MAAM,SAAQ,SACpC,qBAAW,OACd;AAAA,MACA,oBAAC,UAAO,MAAM,UAAU,SAAQ,aAC7B,iBAAO,aACV;AAAA,OACF;AAAA,KAEJ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/demo/demo-start.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ComponentPropsWithRef } from \"react\";\n\nimport { cn } from \"../lib/cn\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { Button } from \"../components/button\";\nimport { Spinner } from \"../components/spinner\";\nimport { useNoIndex } from \"../hooks/use-noindex\";\nimport { authErrorCode, isRateLimited, retryAfterSeconds } from \"../auth/auth-errors\";\nimport { useOncePerKey } from \"../auth/status-parts\";\nimport { useAccessAction } from \"../landing/access\";\nimport type { AccessChoice } from \"../landing/access\";\nimport type { LandingLabels } from \"../landing/landing-labels\";\nimport { useDemoLabels } from \"./demo-labels\";\nimport type { DemoLabels } from \"./demo-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\n\n/** Why a start was refused, in the page's terms (§5.2). */\ntype Refusal =\n | { reason: \"rateLimited\"; minutes?: number }\n | { reason: \"capacity\" }\n | { reason: \"unavailable\" }\n | { reason: \"failed\" };\n\n/**\n * The server's answer to `POST /auth/demo-session`, read as a refusal (§5.1, §6.2):\n * `demo_rate_limited` (429, with `Retry-After`), `demo_capacity` (429, without),\n * `demo_disabled` (404) and `demo_not_ready` (503) by their codes; a bare 429 as the\n * per-IP window too; anything else is a plain failure.\n */\nfunction refusalOf(error: unknown): Refusal {\n const code = authErrorCode(error);\n if (code === \"demo_capacity\") return { reason: \"capacity\" };\n if (code === \"demo_disabled\" || code === \"demo_not_ready\") return { reason: \"unavailable\" };\n if (code === \"demo_rate_limited\" || (code === undefined && isRateLimited(error))) {\n const seconds = retryAfterSeconds(error);\n return { reason: \"rateLimited\", minutes: seconds ? Math.ceil(seconds / 60) : undefined };\n }\n return { reason: \"failed\" };\n}\n\nfunction messageOf(refusal: Refusal, labels: DemoLabels): string {\n switch (refusal.reason) {\n case \"rateLimited\":\n return labels.rateLimited(refusal.minutes);\n case \"capacity\":\n return labels.capacity;\n case \"unavailable\":\n return labels.unavailable;\n case \"failed\":\n return labels.failed;\n }\n}\n\n/** The one start a mount makes. */\nconst ONCE = \"demo\";\n\nexport interface DemoStartProps<T> extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n /**\n * Create the demo: the app's `POST /auth/demo-session {locale}`. Resolve with what it\n * answered (the `TokenResponse`, `refresh_token: null`, `expires_at`); reject with the\n * client's error — the codes are read out of it. Called once per mount.\n */\n start: () => Promise<T>;\n /** The demo exists: store the session (and, for kastlan, `kastlan.demoSession`) and\n * replace to the app's home. The page keeps saying \"Starting the demo…\" meanwhile. */\n onStarted: (session: T) => void;\n /** \"Request access\" beside a refusal. */\n access: AccessChoice;\n /** \"Back to the start page\". Default `/`. */\n backHref?: string;\n /** `false` adds no `noindex` — for a preview. Default `true`: `/demo` is never indexed\n * (§4.3). */\n noIndex?: boolean;\n labels?: LabelOverride<DemoLabels>;\n /** The access words (`landing` namespace) for this instance. */\n landingLabels?: Partial<LandingLabels>;\n}\n\n/**\n * The `/demo` page's content (docs/landing-demo-harmonization.md §5.2): it starts the\n * demo ONCE — StrictMode's double mount included, keksdose's ref guard — and says\n * \"Starting the demo…\" until the app has the session and has moved on.\n *\n * A refusal says why, in the visitor's words, and offers \"Request access\" and \"Back to\n * the start page\":\n *\n * | code | says |\n * |---|---|\n * | `demo_rate_limited` (429) | \"Too many demos from this network. Try again in N min.\" — N from `Retry-After` |\n * | `demo_capacity` (429) | \"The demo is full right now. Try again later.\" |\n * | `demo_disabled` (404), `demo_not_ready` (503) | \"The demo isn't available right now.\" |\n * | anything else | \"The demo could not be started. Please try again.\" |\n *\n * keksdose fell back to `/register` with a toast; registration is invitation-only now,\n * so the way on is the access mail.\n *\n * Put it in the app's `AuthLayout`. A live demo session never reaches it: the `/demo`\n * route sits in `RedirectIfAuthed` WITHOUT `allowDemo`, which continues the live demo at\n * its resume target instead of starting a second one. A cold start is the kit's\n * `ServerWakeNotice`'s, once the app's watcher counts this POST\n * (`watchReadsAnd(/\\/auth\\/demo-session\\b/)`).\n */\nexport function DemoStart<T>({\n start,\n onStarted,\n access,\n backHref = \"/\",\n noIndex = true,\n labels: labelsProp,\n landingLabels,\n className,\n ...rest\n}: DemoStartProps<T>) {\n const labels = useDemoLabels(labelsProp);\n const accessLink = useAccessAction(access, landingLabels);\n const [refusal, setRefusal] = useState<Refusal | null>(null);\n useNoIndex(noIndex);\n useOncePerKey<T>(ONCE, () => start(), (_key, result) => {\n if (result.ok) onStarted(result.value);\n else setRefusal(refusalOf(result.error));\n });\n\n return (\n <div {...rest} className={cn(\"flex flex-col items-center gap-4 py-4 text-center\", className)}>\n {/* One live region, there from the start, so the refusal that replaces the\n progress line is announced (a region inserted with its text often is not). */}\n <div role=\"status\" aria-live=\"polite\" className=\"w-full\">\n {refusal === null ? (\n <p className=\"flex items-center justify-center gap-2 text-sm text-secondary\">\n <Spinner label={null} className=\"size-5\" />\n {labels.starting}\n </p>\n ) : (\n <AlertBanner tone={refusal.reason === \"failed\" ? \"danger\" : \"warning\"} className=\"text-start\">\n {messageOf(refusal, labels)}\n </AlertBanner>\n )}\n </div>\n {refusal !== null && (\n <div className=\"flex flex-wrap justify-center gap-3\">\n <Button href={accessLink.href} variant=\"accent\">\n {accessLink.label}\n </Button>\n <Button href={backHref} variant=\"secondary\">\n {labels.backToStart}\n </Button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";AAiIU,SACE,KADF;AAjIV,SAAS,gBAAgB;AAGzB,SAAS,UAAU;AACnB,SAAS,mBAAmB;AAC5B,SAAS,cAAc;AACvB,SAAS,eAAe;AACxB,SAAS,kBAAkB;AAC3B,SAAS,eAAe,eAAe,yBAAyB;AAChE,SAAS,qBAAqB;AAC9B,SAAS,uBAAuB;AAGhC,SAAS,qBAAqB;AAiB9B,SAAS,UAAU,OAAyB;AAC1C,QAAM,OAAO,cAAc,KAAK;AAChC,MAAI,SAAS,gBAAiB,QAAO,EAAE,QAAQ,WAAW;AAC1D,MAAI,SAAS,mBAAmB,SAAS,iBAAkB,QAAO,EAAE,QAAQ,cAAc;AAC1F,MAAI,SAAS,uBAAwB,SAAS,UAAa,cAAc,KAAK,GAAI;AAChF,UAAM,UAAU,kBAAkB,KAAK;AACvC,WAAO,EAAE,QAAQ,eAAe,SAAS,UAAU,KAAK,KAAK,UAAU,EAAE,IAAI,OAAU;AAAA,EACzF;AACA,SAAO,EAAE,QAAQ,SAAS;AAC5B;AAEA,SAAS,UAAU,SAAkB,QAA4B;AAC/D,UAAQ,QAAQ,QAAQ;AAAA,IACtB,KAAK;AACH,aAAO,OAAO,YAAY,QAAQ,OAAO;AAAA,IAC3C,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,OAAO;AAAA,EAClB;AACF;AAGA,MAAM,OAAO;AAgDN,SAAS,UAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,UAAU;AAAA,EACV,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAsB;AACpB,QAAM,SAAS,cAAc,UAAU;AACvC,QAAM,aAAa,gBAAgB,QAAQ,aAAa;AACxD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAyB,IAAI;AAC3D,aAAW,OAAO;AAClB,gBAAiB,MAAM,MAAM,MAAM,GAAG,CAAC,MAAM,WAAW;AACtD,QAAI,OAAO,GAAI,WAAU,OAAO,KAAK;AAAA,QAChC,YAAW,UAAU,OAAO,KAAK,CAAC;AAAA,EACzC,CAAC;AAED,SACE,qBAAC,SAAK,GAAG,MAAM,WAAW,GAAG,qDAAqD,SAAS,GAGzF;AAAA,wBAAC,SAAI,MAAK,UAAS,aAAU,UAAS,WAAU,UAC7C,sBAAY,OACX,qBAAC,OAAE,WAAU,iEACX;AAAA,0BAAC,WAAQ,OAAO,MAAM,WAAU,UAAS;AAAA,MACxC,OAAO;AAAA,OACV,IAEA,oBAAC,eAAY,MAAM,QAAQ,WAAW,WAAW,WAAW,WAAW,WAAU,cAC9E,oBAAU,SAAS,MAAM,GAC5B,GAEJ;AAAA,IACC,YAAY,QACX,qBAAC,SAAI,WAAU,uCACb;AAAA,0BAAC,UAAO,MAAM,WAAW,MAAM,SAAQ,UACpC,qBAAW,OACd;AAAA,MACA,oBAAC,UAAO,MAAM,UAAU,SAAQ,aAC7B,iBAAO,aACV;AAAA,OACF;AAAA,KAEJ;AAEJ;","names":[]}
@@ -47,7 +47,7 @@ const roleUtilities = {
47
47
  };
48
48
  }
49
49
  };
50
- const VERSION = true ? "0.48.2" : "0.0.0-source";
50
+ const VERSION = true ? "0.49.0" : "0.0.0-source";
51
51
  const recommended = {
52
52
  name: "@eifi1/ui-kit/recommended",
53
53
  // Not the tests, as check:tokens skips the kit's: a test names the old class to assert it