@eifi1/ui-kit 0.34.0 → 0.35.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 (241) hide show
  1. package/README.md +82 -38
  2. package/dist/billing/billing-banner.d.ts +5 -2
  3. package/dist/billing/billing-banner.js.map +1 -1
  4. package/dist/billing/plan-limit-notice.d.ts +5 -2
  5. package/dist/billing/plan-limit-notice.js.map +1 -1
  6. package/dist/chart.js.map +1 -1
  7. package/dist/components/amount-input.d.ts +0 -3
  8. package/dist/components/amount-input.js +2 -2
  9. package/dist/components/amount-input.js.map +1 -1
  10. package/dist/components/calculator.d.ts +1 -4
  11. package/dist/components/calculator.js +1 -3
  12. package/dist/components/calculator.js.map +1 -1
  13. package/dist/components/chart-zoom.d.ts +0 -37
  14. package/dist/components/chart-zoom.js +1 -28
  15. package/dist/components/chart-zoom.js.map +1 -1
  16. package/dist/components/chip-input.d.ts +0 -4
  17. package/dist/components/chip-input.js +1 -5
  18. package/dist/components/chip-input.js.map +1 -1
  19. package/dist/components/combobox.d.ts +1 -1
  20. package/dist/components/combobox.js.map +1 -1
  21. package/dist/components/copy-button.d.ts +5 -1
  22. package/dist/components/copy-button.js.map +1 -1
  23. package/dist/components/currency-option-list.d.ts +3 -8
  24. package/dist/components/currency-option-list.js +1 -29
  25. package/dist/components/currency-option-list.js.map +1 -1
  26. package/dist/components/danger-confirm.d.ts +3 -3
  27. package/dist/components/danger-confirm.js.map +1 -1
  28. package/dist/components/data-table-filter-popover.d.ts +1 -1
  29. package/dist/components/data-table-filter-popover.js +1 -1
  30. package/dist/components/data-table-filter-popover.js.map +1 -1
  31. package/dist/components/data-table-labels.d.ts +1 -42
  32. package/dist/components/data-table-labels.js +2 -31
  33. package/dist/components/data-table-labels.js.map +1 -1
  34. package/dist/components/data-table-shared.d.ts +17 -1
  35. package/dist/components/data-table-shared.js +22 -0
  36. package/dist/components/data-table-shared.js.map +1 -1
  37. package/dist/components/data-table.d.ts +12 -2
  38. package/dist/components/data-table.js +18 -14
  39. package/dist/components/data-table.js.map +1 -1
  40. package/dist/components/dialog-frame.d.ts +3 -1
  41. package/dist/components/dialog-frame.js.map +1 -1
  42. package/dist/components/dropdown.d.ts +18 -15
  43. package/dist/components/dropdown.js +11 -3
  44. package/dist/components/dropdown.js.map +1 -1
  45. package/dist/components/entity-combobox.d.ts +1 -1
  46. package/dist/components/entity-combobox.js.map +1 -1
  47. package/dist/components/field-base.d.ts +0 -5
  48. package/dist/components/field-base.js +0 -5
  49. package/dist/components/field-base.js.map +1 -1
  50. package/dist/components/field-parts.d.ts +3 -11
  51. package/dist/components/field-parts.js +1 -9
  52. package/dist/components/field-parts.js.map +1 -1
  53. package/dist/components/field-strip.d.ts +8 -23
  54. package/dist/components/field-strip.js +24 -22
  55. package/dist/components/field-strip.js.map +1 -1
  56. package/dist/components/field-sync.d.ts +0 -4
  57. package/dist/components/field-sync.js +0 -4
  58. package/dist/components/field-sync.js.map +1 -1
  59. package/dist/components/field.d.ts +4 -4
  60. package/dist/components/field.js.map +1 -1
  61. package/dist/components/full-bleed-dialog.js +2 -2
  62. package/dist/components/full-bleed-dialog.js.map +1 -1
  63. package/dist/components/hover-menu.d.ts +6 -13
  64. package/dist/components/hover-menu.js +3 -3
  65. package/dist/components/hover-menu.js.map +1 -1
  66. package/dist/components/inline-entity-combobox.d.ts +1 -1
  67. package/dist/components/inline-entity-combobox.js.map +1 -1
  68. package/dist/components/modal.d.ts +9 -1
  69. package/dist/components/modal.js +7 -4
  70. package/dist/components/modal.js.map +1 -1
  71. package/dist/components/multi-select.d.ts +5 -16
  72. package/dist/components/multi-select.js +5 -10
  73. package/dist/components/multi-select.js.map +1 -1
  74. package/dist/components/number-input.d.ts +0 -3
  75. package/dist/components/number-input.js +2 -4
  76. package/dist/components/number-input.js.map +1 -1
  77. package/dist/components/popover.js +2 -2
  78. package/dist/components/popover.js.map +1 -1
  79. package/dist/components/series-chart.js.map +1 -1
  80. package/dist/components/server-wake.d.ts +0 -6
  81. package/dist/components/server-wake.js.map +1 -1
  82. package/dist/components/settings-fields.d.ts +4 -2
  83. package/dist/components/settings-fields.js.map +1 -1
  84. package/dist/components/stat-tile.d.ts +8 -2
  85. package/dist/components/stat-tile.js.map +1 -1
  86. package/dist/components/swipeable-row.d.ts +2 -17
  87. package/dist/components/swipeable-row.js +11 -16
  88. package/dist/components/swipeable-row.js.map +1 -1
  89. package/dist/components/tooltip.d.ts +9 -1
  90. package/dist/components/tooltip.js +20 -5
  91. package/dist/components/tooltip.js.map +1 -1
  92. package/dist/components/translation-review.d.ts +10 -2
  93. package/dist/components/translation-review.js +11 -3
  94. package/dist/components/translation-review.js.map +1 -1
  95. package/dist/components/use-table-state.d.ts +31 -3
  96. package/dist/components/use-table-state.js +53 -8
  97. package/dist/components/use-table-state.js.map +1 -1
  98. package/dist/components/user-avatar.d.ts +2 -2
  99. package/dist/components/user-avatar.js.map +1 -1
  100. package/dist/components/wizard-stepper.d.ts +1 -4
  101. package/dist/components/wizard-stepper.js +2 -3
  102. package/dist/components/wizard-stepper.js.map +1 -1
  103. package/dist/currencies.d.ts +23 -0
  104. package/dist/currencies.js +38 -0
  105. package/dist/currencies.js.map +1 -0
  106. package/dist/demo/demo-banner.d.ts +7 -2
  107. package/dist/demo/demo-banner.js.map +1 -1
  108. package/dist/eslint/index.d.ts +83 -0
  109. package/dist/eslint/index.js +69 -0
  110. package/dist/eslint/index.js.map +1 -0
  111. package/dist/eslint/role-utilities.d.ts +48 -0
  112. package/dist/eslint/role-utilities.generated.d.ts +2 -0
  113. package/dist/eslint/role-utilities.generated.js +143 -0
  114. package/dist/eslint/role-utilities.generated.js.map +1 -0
  115. package/dist/eslint/role-utilities.js +43 -0
  116. package/dist/eslint/role-utilities.js.map +1 -0
  117. package/dist/feedback/feedback-menu.d.ts +2 -2
  118. package/dist/feedback/feedback-menu.js.map +1 -1
  119. package/dist/feedback/feedback-thread.d.ts +3 -8
  120. package/dist/feedback/feedback-thread.js +0 -2
  121. package/dist/feedback/feedback-thread.js.map +1 -1
  122. package/dist/hooks/use-dismiss.d.ts +16 -3
  123. package/dist/hooks/use-dismiss.js +14 -2
  124. package/dist/hooks/use-dismiss.js.map +1 -1
  125. package/dist/i18n/kit-labels.d.ts +14 -4
  126. package/dist/i18n/kit-labels.js.map +1 -1
  127. package/dist/i18n/locales/en.js +1 -1
  128. package/dist/i18n/locales/en.js.map +2 -2
  129. package/dist/i18n/locales/fr.js +3 -2
  130. package/dist/i18n/locales/fr.js.map +1 -1
  131. package/dist/i18n/locales/it.js +3 -2
  132. package/dist/i18n/locales/it.js.map +1 -1
  133. package/dist/index.d.ts +2 -1
  134. package/dist/index.js +2 -2
  135. package/dist/index.js.map +1 -1
  136. package/dist/lib/refs.d.ts +13 -0
  137. package/dist/lib/refs.js +15 -0
  138. package/dist/lib/refs.js.map +1 -0
  139. package/dist/rhf/fields.d.ts +10 -10
  140. package/dist/rhf/fields.js.map +1 -1
  141. package/dist/rhf/form-core.d.ts +5 -4
  142. package/dist/rhf/form-core.js.map +1 -1
  143. package/dist/rhf/form.d.ts +7 -36
  144. package/dist/rhf/form.js +1 -16
  145. package/dist/rhf/form.js.map +1 -1
  146. package/dist/rhf/line-items.d.ts +1 -1
  147. package/dist/rhf/line-items.js.map +1 -1
  148. package/dist/rhf/use-rhf-wizard-step.js.map +1 -1
  149. package/dist/rhf.js.map +1 -1
  150. package/dist/shell/option-switcher-menu.d.ts +5 -17
  151. package/dist/shell/option-switcher-menu.js +1 -3
  152. package/dist/shell/option-switcher-menu.js.map +1 -1
  153. package/dist/shell/role-switcher.d.ts +1 -4
  154. package/dist/shell/role-switcher.js +2 -3
  155. package/dist/shell/role-switcher.js.map +1 -1
  156. package/dist/shell/topbar-action-menu.d.ts +7 -19
  157. package/dist/shell/topbar-action-menu.js +1 -3
  158. package/dist/shell/topbar-action-menu.js.map +1 -1
  159. package/dist/shell/topbar-controls.d.ts +3 -12
  160. package/dist/shell/topbar-controls.js +6 -9
  161. package/dist/shell/topbar-controls.js.map +1 -1
  162. package/dist/testing/index.d.ts +1 -1
  163. package/dist/testing/index.js +2 -0
  164. package/dist/testing/index.js.map +1 -1
  165. package/dist/testing/viewport.d.ts +25 -2
  166. package/dist/testing/viewport.js +61 -10
  167. package/dist/testing/viewport.js.map +1 -1
  168. package/dist/theme/palette-presets.d.ts +0 -7
  169. package/dist/theme/palette-presets.js +0 -279
  170. package/dist/theme/palette-presets.js.map +1 -1
  171. package/dist/wizard/types.d.ts +0 -5
  172. package/dist/wizard/types.js +1 -5
  173. package/dist/wizard/types.js.map +1 -1
  174. package/package.json +9 -1
  175. package/src/billing/billing-banner.tsx +5 -2
  176. package/src/billing/plan-limit-notice.tsx +5 -2
  177. package/src/chart.ts +5 -3
  178. package/src/components/amount-input.tsx +2 -6
  179. package/src/components/calculator.tsx +2 -7
  180. package/src/components/chart-zoom.tsx +4 -47
  181. package/src/components/chip-input.tsx +0 -7
  182. package/src/components/combobox.tsx +1 -1
  183. package/src/components/copy-button.tsx +5 -1
  184. package/src/components/currency-option-list.tsx +8 -42
  185. package/src/components/danger-confirm.tsx +3 -3
  186. package/src/components/data-table-filter-popover.tsx +2 -1
  187. package/src/components/data-table-labels.ts +2 -84
  188. package/src/components/data-table-shared.tsx +55 -2
  189. package/src/components/data-table.tsx +29 -17
  190. package/src/components/dialog-frame.tsx +3 -1
  191. package/src/components/dropdown.tsx +42 -18
  192. package/src/components/entity-combobox.tsx +1 -1
  193. package/src/components/field-base.tsx +0 -11
  194. package/src/components/field-parts.tsx +5 -19
  195. package/src/components/field-strip.tsx +9 -25
  196. package/src/components/field-sync.tsx +0 -7
  197. package/src/components/field.tsx +4 -4
  198. package/src/components/full-bleed-dialog.tsx +4 -3
  199. package/src/components/hover-menu.tsx +12 -16
  200. package/src/components/inline-entity-combobox.tsx +1 -1
  201. package/src/components/modal.tsx +20 -4
  202. package/src/components/multi-select.tsx +13 -30
  203. package/src/components/number-input.tsx +3 -8
  204. package/src/components/popover.tsx +5 -3
  205. package/src/components/series-chart.tsx +1 -1
  206. package/src/components/server-wake.tsx +0 -6
  207. package/src/components/settings-fields.tsx +4 -2
  208. package/src/components/stat-tile.tsx +8 -2
  209. package/src/components/swipeable-row.tsx +15 -37
  210. package/src/components/tooltip.tsx +37 -6
  211. package/src/components/translation-review.tsx +25 -7
  212. package/src/components/use-table-state.ts +91 -10
  213. package/src/components/user-avatar.tsx +2 -2
  214. package/src/components/wizard-stepper.tsx +2 -6
  215. package/src/currencies.ts +70 -0
  216. package/src/demo/demo-banner.tsx +7 -2
  217. package/src/eslint/index.ts +161 -0
  218. package/src/eslint/role-utilities.generated.ts +146 -0
  219. package/src/eslint/role-utilities.ts +99 -0
  220. package/src/feedback/feedback-menu.tsx +2 -2
  221. package/src/feedback/feedback-thread.tsx +3 -8
  222. package/src/hooks/use-dismiss.ts +39 -4
  223. package/src/i18n/kit-labels.tsx +15 -5
  224. package/src/i18n/locales/fr.ts +3 -2
  225. package/src/i18n/locales/it.ts +3 -2
  226. package/src/index.ts +3 -1
  227. package/src/lib/refs.ts +22 -0
  228. package/src/rhf/fields.tsx +10 -10
  229. package/src/rhf/form-core.tsx +5 -4
  230. package/src/rhf/form.tsx +7 -43
  231. package/src/rhf/line-items.tsx +1 -1
  232. package/src/rhf/use-rhf-wizard-step.ts +4 -4
  233. package/src/rhf.ts +3 -2
  234. package/src/shell/option-switcher-menu.tsx +5 -22
  235. package/src/shell/role-switcher.tsx +2 -6
  236. package/src/shell/topbar-action-menu.tsx +7 -24
  237. package/src/shell/topbar-controls.tsx +6 -18
  238. package/src/testing/index.ts +3 -1
  239. package/src/testing/viewport.ts +125 -19
  240. package/src/theme/palette-presets.ts +7 -293
  241. package/src/wizard/types.ts +0 -8
@@ -52,7 +52,8 @@ export interface ThemeSettingSelectProps
52
52
  * choice visible without opening anything, in every app (keksdose offered light and dark
53
53
  * only; its store already knew "system"). A {@link ToggleGroup}'s props pass through —
54
54
  * `labelPlacement`, `disabledReason`, `commit`, `size`, `overflow` — minus the options
55
- * and the value, which this owns.
55
+ * and the value, which this owns. `ref` too: the group, whose `focus()` lands on the
56
+ * checked option (pinned in round 0.35 R4; the select variant's `ref` is the `<select>`).
56
57
  *
57
58
  * The options wrap rather than truncate (`overflow="wrap"`), as in its two siblings
58
59
  * {@link TextSizeSetting} and {@link ContrastSetting} (0.32.1, Kurvenschmiede's 0.32
@@ -161,7 +162,8 @@ export function LanguageSetting({ options, ...rest }: LanguageSettingProps) {
161
162
  /**
162
163
  * What {@link TextSizeSetting} and {@link ContrastSetting} take: a {@link ToggleGroup}'s
163
164
  * props — `labelPlacement`, `disabledReason`, `commit`, `size`, `overflow` — minus the
164
- * options and the value, which they own, as {@link ThemeSettingToggleProps} does.
165
+ * options and the value, which they own, as {@link ThemeSettingToggleProps} does. `ref`
166
+ * is the group's, as there (round 0.35 R4).
165
167
  */
166
168
  type AppearanceToggleProps<T extends string> = Omit<ToggleGroupBaseProps<T>, "options" | "label"> & {
167
169
  value: T;
@@ -1,5 +1,5 @@
1
1
  import { createContext, isValidElement, useContext, useId, useLayoutEffect, useState } from "react";
2
- import type { ComponentPropsWithoutRef, ComponentPropsWithRef, CSSProperties, ReactElement, ReactNode } from "react";
2
+ import type { ComponentPropsWithRef, CSSProperties, ReactElement, ReactNode } from "react";
3
3
  import { ArrowDown, ArrowUp, Minus } from "lucide-react";
4
4
  import { cn } from "../lib/cn";
5
5
  import { useKitLabels, useKitLink, useKitLocale, type LabelOverride } from "../i18n/kit-labels";
@@ -115,8 +115,14 @@ export interface StatTileLinkProps {
115
115
  children: ReactNode;
116
116
  }
117
117
 
118
+ /**
119
+ * A `div`'s props, `ref` included since 0.35 (round 0.35 R4, the follow-up to API-2): they
120
+ * ride on to the {@link Card} that is the tile, so the ref is the tile's own `<div>` — the
121
+ * one to scroll into view or measure. `ComponentPropsWithoutRef` only kept it out of the
122
+ * type; at runtime `{...rest}` already carried it there.
123
+ */
118
124
  export interface StatTileProps
119
- extends Omit<ComponentPropsWithoutRef<"div">, "children" | "onClick"> {
125
+ extends Omit<ComponentPropsWithRef<"div">, "children" | "onClick"> {
120
126
  /** What is measured. Also the tile's accessible name when it is a link/button. */
121
127
  label: ReactNode;
122
128
  /**
@@ -51,9 +51,8 @@ interface SwipeActionBase {
51
51
  * dark-mode pastel.
52
52
  *
53
53
  * **`paint`** follows the same rule for an app's documented exception (see
54
- * {@link SwipePaint}). **`className` / `armedClassName`** is the old path, kept so
55
- * existing call sites compile; there the label is white unless the classes set a
56
- * colour, and the idle panel is still dimmed.
54
+ * {@link SwipePaint}). The old path, a `className` / `armedClassName` pair under a white
55
+ * label and dimmed idle, was deprecated in 0.33 and removed in 0.35 (round 0.35 R1).
57
56
  */
58
57
  export type SwipeAction = SwipeActionBase &
59
58
  (
@@ -61,27 +60,12 @@ export type SwipeAction = SwipeActionBase &
61
60
  /** The kit paints the panel: the tone's wash idle, its fill and `-contrast` armed. */
62
61
  tone: SwipeTone;
63
62
  paint?: never;
64
- className?: never;
65
- armedClassName?: never;
66
63
  }
67
64
  | {
68
65
  /** A documented app exception: idle a 14 % wash of `fill` under the body ink,
69
66
  * armed `fill` under `foreground`. */
70
67
  paint: SwipePaint;
71
68
  tone?: never;
72
- className?: never;
73
- armedClassName?: never;
74
- }
75
- | {
76
- tone?: never;
77
- paint?: never;
78
- /** @deprecated 0.33 — pass `tone`, or `paint` for an exception; removed in 0.35.
79
- * Background utility class while the action is previewed but not yet armed; the
80
- * panel is dimmed then, and its text is white unless this sets a text colour. */
81
- className: string;
82
- /** @deprecated 0.33 — pass `tone`, or `paint`; removed in 0.35. Background
83
- * utility class once the drag has passed this action's threshold. */
84
- armedClassName: string;
85
69
  }
86
70
  );
87
71
 
@@ -114,21 +98,16 @@ export const SWIPE_TONE: Record<SwipeTone, { idle: string; armed: string }> = {
114
98
  const PAINT_IDLE = "bg-[color-mix(in_oklab,var(--swipe-paint-fill)_14%,var(--bg-surface))] text-primary";
115
99
  const PAINT_ARMED = "bg-[var(--swipe-paint-fill)] text-[var(--swipe-paint-fg)]";
116
100
 
117
- /** The panel's colour classes, inline style and dimming for one action and state. */
118
- function panelLook(action: SwipeAction, armed: boolean): { className: string; style?: CSSProperties; dim: boolean } {
119
- if (action.tone) return { className: SWIPE_TONE[action.tone][armed ? "armed" : "idle"], dim: false };
120
- if (action.paint) {
121
- return {
122
- className: armed ? PAINT_ARMED : PAINT_IDLE,
123
- style: {
124
- "--swipe-paint-fill": action.paint.fill,
125
- "--swipe-paint-fg": action.paint.foreground,
126
- } as CSSProperties,
127
- dim: false,
128
- };
129
- }
130
- // The deprecated path, as it was: white text under the caller's fill, dimmed idle.
131
- return { className: cn("text-white", armed ? action.armedClassName : action.className), dim: !armed };
101
+ /** The panel's colour classes and inline style for one action and state. */
102
+ function panelLook(action: SwipeAction, armed: boolean): { className: string; style?: CSSProperties } {
103
+ if (action.tone) return { className: SWIPE_TONE[action.tone][armed ? "armed" : "idle"] };
104
+ return {
105
+ className: armed ? PAINT_ARMED : PAINT_IDLE,
106
+ style: {
107
+ "--swipe-paint-fill": action.paint.fill,
108
+ "--swipe-paint-fg": action.paint.foreground,
109
+ } as CSSProperties,
110
+ };
132
111
  }
133
112
 
134
113
  /**
@@ -296,11 +275,10 @@ export function SwipeableRow({
296
275
  className={cn(
297
276
  "pointer-events-none absolute inset-0 flex items-center gap-2 px-4",
298
277
  "text-xs font-medium transition-all",
278
+ // No dimming: a tone or a paint says "not yet armed" with the soft→solid
279
+ // step, which keeps its label readable at both ends (the 0.33 rule; the
280
+ // dimmed path went with `className` / `armedClassName` in 0.35).
299
281
  look.className,
300
- // The old path only: dimmed until the drag has passed a threshold. A tone
301
- // or a paint says it with the soft→solid step instead, which keeps its
302
- // label readable at both ends.
303
- look.dim && "opacity-60",
304
282
  // The panel is anchored to the edge the row is uncovering, so the label
305
283
  // sits where the eye already is rather than across the screen. That edge is
306
284
  // physical, while a flex row starts at the INLINE start — so in RTL the two
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  cloneElement,
3
3
  isValidElement,
4
+ useCallback,
4
5
  useEffect,
5
6
  useId,
6
7
  useLayoutEffect,
@@ -12,6 +13,7 @@ import {
12
13
  type PointerEvent,
13
14
  type ReactElement,
14
15
  type ReactNode,
16
+ type Ref,
15
17
  type RefObject,
16
18
  } from "react";
17
19
  import { createPortal } from "react-dom";
@@ -21,6 +23,7 @@ import { useAnchoredRect, type AnchorRect } from "../hooks/use-anchored-rect";
21
23
  import { viewportSize } from "../hooks/use-anchored-panel";
22
24
  import { dirOf, type Direction } from "../lib/direction";
23
25
  import { hasClippingAncestor } from "../lib/clipping";
26
+ import { assignRef } from "../lib/refs";
24
27
 
25
28
  /** The attribute that marks an element as a clipping container for {@link Tooltip}'s
26
29
  * auto-portal, whatever its computed `overflow` — `<div data-clips>` or
@@ -129,6 +132,14 @@ export interface TooltipProps extends ComponentPropsWithoutRef<"span"> {
129
132
  */
130
133
  tap?: TooltipTap;
131
134
  children: ReactNode;
135
+ /**
136
+ * The trigger WRAPPER — the `<span>` the rest of the props go to, around the caller's
137
+ * child (round 0.35 R4, as Card and EmptyState took one in 0.34) — merged with its own
138
+ * ref, which the bubble is anchored to and dismissed by. Not the bubble: a portalled
139
+ * one exists only while it is up. On the empty-label branch there is no wrapper, so
140
+ * the ref stays `null` (see the ⚠️ above).
141
+ */
142
+ ref?: Ref<HTMLSpanElement>;
132
143
  }
133
144
 
134
145
  /** What each variant below takes: the resolved `side` and `tap`, and every span attribute
@@ -630,6 +641,22 @@ function useTooltipTrigger(
630
641
  return { open, dismissed, handlers };
631
642
  }
632
643
 
644
+ /**
645
+ * The trigger wrapper's own ref, and one callback ref that hands the wrapper to the
646
+ * caller's `ref` as well (round 0.35 R4).
647
+ */
648
+ function useTriggerRef(ref: Ref<HTMLSpanElement> | undefined) {
649
+ const triggerRef = useRef<HTMLSpanElement | null>(null);
650
+ const setTrigger = useCallback(
651
+ (el: HTMLSpanElement | null) => {
652
+ triggerRef.current = el;
653
+ assignRef(ref, el);
654
+ },
655
+ [ref],
656
+ );
657
+ return { triggerRef, setTrigger };
658
+ }
659
+
633
660
  /** The variant that lives next to its trigger, and — when `detect` is on, which is the
634
661
  * default — moves its bubble to `<body>` when that turns out to be inside a clipping
635
662
  * container (see "Inside a scroll container" on {@link Tooltip}).
@@ -656,10 +683,11 @@ function InPlaceTooltip({
656
683
  lazy,
657
684
  tap,
658
685
  children,
686
+ ref,
659
687
  ...rest
660
688
  }: TooltipVariantProps & { detect: boolean; lazy: boolean }) {
661
689
  const id = useId();
662
- const triggerRef = useRef<HTMLSpanElement | null>(null);
690
+ const { triggerRef, setTrigger } = useTriggerRef(ref);
663
691
  const [clipped, setClipped] = useState(false);
664
692
  const [dir, setDir] = useState<Direction>("ltr");
665
693
  // The reading direction on every request (the clamp keeps the start edge of a bubble
@@ -677,7 +705,8 @@ function InPlaceTooltip({
677
705
  // phantom-scroll bug whether or not anyone opens it.
678
706
  useLayoutEffect(() => {
679
707
  if (detect) setClipped(hasClippingAncestor(triggerRef.current));
680
- }, [detect]);
708
+ // `triggerRef` is the same object for the component's life (useTriggerRef's useRef).
709
+ }, [detect, triggerRef]);
681
710
  return (
682
711
  // No role for this span: its handlers track whether the bubble is up and activate
683
712
  // nothing; the caller's child is the interactive element, keeps its own handlers,
@@ -687,7 +716,7 @@ function InPlaceTooltip({
687
716
  // a caller passing an `onFocus` of its own must not replace them (its pointer
688
717
  // handlers are called from them).
689
718
  {...rest}
690
- ref={triggerRef}
719
+ ref={setTrigger}
691
720
  // Clipped for the one render in which the bubble is measured (see useViewportClamp):
692
721
  // that layout must not reach the page's width.
693
722
  style={measuring ? { ...rest.style, overflow: "clip" } : rest.style}
@@ -1041,9 +1070,10 @@ function PortalTooltip({
1041
1070
  redact,
1042
1071
  tap,
1043
1072
  children,
1073
+ ref,
1044
1074
  ...rest
1045
1075
  }: TooltipVariantProps) {
1046
- const triggerRef = useRef<HTMLSpanElement | null>(null);
1076
+ const { triggerRef, setTrigger } = useTriggerRef(ref);
1047
1077
  // The trigger's reading direction, read when the bubble is asked for (an event, not a
1048
1078
  // render): it resolves `start` / `end`, and the portalled bubble — which has left the
1049
1079
  // subtree it would have inherited `dir` from — carries it too.
@@ -1062,9 +1092,10 @@ function PortalTooltip({
1062
1092
  // As in `InPlaceTooltip`: the caller's attributes first, the handlers that run
1063
1093
  // this component after them. The BUBBLE is deliberately not given them — it
1064
1094
  // is portalled to `<body>`, and an id or a tour anchor duplicated onto a node
1065
- // that only exists while hovered would match twice or match nothing.
1095
+ // that only exists while hovered would match twice or match nothing. So, too,
1096
+ // the caller's `ref` (round 0.35 R4).
1066
1097
  {...rest}
1067
- ref={triggerRef}
1098
+ ref={setTrigger}
1068
1099
  className={cn("relative inline-flex", className)}
1069
1100
  {...handlers}
1070
1101
  >
@@ -33,12 +33,13 @@ import { BulkActionBar } from "./bulk-action-bar";
33
33
  import { Checkbox } from "./checkbox";
34
34
  import { Chip } from "./chip";
35
35
  import { DataTable } from "./data-table";
36
- import type { DataTableColumn, DataTableProps, MobileSwipeActions, SortState } from "./data-table";
36
+ import type { DataTableColumn, DataTableProps, MobileSwipeActions } from "./data-table";
37
37
  import { Disclosure } from "./disclosure";
38
38
  import { SearchField } from "./search-field";
39
39
  import { toast } from "./toast";
40
40
  import type { SwipeAction } from "./swipeable-row";
41
41
  import { ToggleGroup } from "./toggle-group";
42
+ import { useSharedTableView } from "./use-table-state";
42
43
  import { TranslationReviewEditor } from "./translation-review-editor";
43
44
  import { DEFAULT_TRANSLATION_REVIEW_LABELS, reviewStatusLabel } from "./translation-review-labels";
44
45
  import type { TranslationReviewLabels } from "./translation-review-labels";
@@ -168,7 +169,8 @@ export interface TranslationReviewPanelProps {
168
169
  /** Rows per page. Default 50. */
169
170
  pageSize?: number;
170
171
  /** Persists the table's sort and page size (DataTable's `storageKey`), under the
171
- * prefix below. */
172
+ * prefix below — under {@link groupBy}, the one sort and page size every group
173
+ * shares, in the same place (0.35). */
172
174
  storageKey?: string;
173
175
  /**
174
176
  * The `localStorage` prefix in front of `storageKey` (DataTable's `storageKeyPrefix`).
@@ -257,7 +259,14 @@ export interface TranslationReviewPanelProps {
257
259
  * reference; a column sort orders the rows INSIDE every group alike. Each group pages
258
260
  * on its own, `pageSize` rows at a time — a header then always stands over its own
259
261
  * rows, and paging one area does not move another — rather than one pager cutting
260
- * through the headers. `storageKey` keeps persisting the ungrouped table only.
262
+ * through the headers. The page SIZE is one for all of them, as the sort is: a
263
+ * group's pager changes it everywhere.
264
+ *
265
+ * `storageKey` persists that one view — the sort and the page size — in the place the
266
+ * ungrouped table keeps its own (same key, same prefix, same blob), read on mount and
267
+ * written on a change, so a reviewer switching the grouping on or off keeps both
268
+ * (0.35, Kurvenschmiede; before, it persisted the ungrouped table only). What only
269
+ * the ungrouped table has — column widths, hidden columns — stays in the blob for it.
261
270
  *
262
271
  * Groups fold (each on its own, remembered while the panel lives), and a folded group's
263
272
  * rows are not rendered at all — only its header. Which groups start open is
@@ -558,8 +567,15 @@ export function TranslationReviewPanel({
558
567
  // has mounted and taken focus, the group that asked.
559
568
  const [confirming, setConfirming] = useState<string | null>(null);
560
569
  const confirmFocus = useRef<string | null>(null);
561
- // One sort for every group's table, so a header click orders all of them alike.
562
- const [groupSorts, setGroupSorts] = useState<SortState[]>([]);
570
+ // One view for every group's table — one sort, so a header click orders all of them
571
+ // alike, and one page size — kept under `storageKey` in the single table's own blob, so
572
+ // switching the grouping keeps both (0.35, Kurvenschmiede).
573
+ const groupView = useSharedTableView({
574
+ active: groupBy !== undefined,
575
+ storageKey,
576
+ storageKeyPrefix,
577
+ defaultPageSize: pageSize,
578
+ });
563
579
 
564
580
  const setFilter = (patch: Partial<TranslationReviewFilter>) => {
565
581
  const next = { ...filter, ...patch };
@@ -870,8 +886,10 @@ export function TranslationReviewPanel({
870
886
  frame: false,
871
887
  chrome: "minimal" as const,
872
888
  maxBodyHeight: "none",
873
- sorts: groupSorts,
874
- onSortsChange: setGroupSorts,
889
+ sorts: groupView.sorts,
890
+ onSortsChange: groupView.setSorts,
891
+ pageSize: groupView.pageSize,
892
+ onPageSizeChange: groupView.setPageSize,
875
893
  labels: { table: group.name },
876
894
  }
877
895
  : { storageKey, storageKeyPrefix })}
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useMemo, useRef, useState } from "react";
2
2
  import { useLocation, useSearchParams } from "react-router";
3
+ import { useKitTableStorageKeyPrefix } from "../i18n/kit-labels";
3
4
  import { readStored, writeStored } from "../lib/safe-storage";
4
5
  import {
5
6
  decodeFilterValue,
@@ -45,6 +46,13 @@ function serializePersisted(state: PersistedState): string {
45
46
  return JSON.stringify(state);
46
47
  }
47
48
 
49
+ /** A stored page size as a table holds it — `"all"` is `Infinity` — or `fallback`. */
50
+ function storedPageSize(stored: Partial<PersistedState>, fallback: number): number {
51
+ if (stored.pageSize === "all") return Infinity;
52
+ if (typeof stored.pageSize === "number" && stored.pageSize > 0) return stored.pageSize;
53
+ return fallback;
54
+ }
55
+
48
56
  // ---------- URL sync helpers ----------
49
57
 
50
58
  const URL_FILTER_PREFIX = "f.";
@@ -251,8 +259,8 @@ function writeUrlState<T>(
251
259
  * * **Persistence writes on every change, the URL only when asked.** `storageKey` is the
252
260
  * per-table identity; `urlSync` is opt-in because only some tables want their view in
253
261
  * a shareable address.
254
- * * **A controlled filter or sort that changes sends the table back to page 1.** See
255
- * the note at the reset below.
262
+ * * **A controlled filter, sort or page size that changes sends the table back to page
263
+ * 1.** See the note at the reset below.
256
264
  */
257
265
  export function useTableState<T>({
258
266
  columns,
@@ -262,6 +270,7 @@ export function useTableState<T>({
262
270
  defaultPageSize,
263
271
  sortsProp,
264
272
  filtersProp,
273
+ pageSizeProp,
265
274
  }: {
266
275
  columns: DataTableColumn<T>[];
267
276
  storageKey?: string;
@@ -270,6 +279,7 @@ export function useTableState<T>({
270
279
  defaultPageSize: number;
271
280
  sortsProp?: SortState[];
272
281
  filtersProp?: FilterState;
282
+ pageSizeProp?: number;
273
283
  }) {
274
284
  const { enabled: urlSync, prefix: urlPrefix } = resolveTableUrlSync(urlSyncOption);
275
285
  const keys = useMemo(() => urlKeys(urlPrefix), [urlPrefix]);
@@ -312,21 +322,21 @@ export function useTableState<T>({
312
322
  // is never painted and never written to the URL. Not on mount: a deep link's `p=3`
313
323
  // must survive the owner's first render. (In `serverPagination` mode this state is
314
324
  // unused — the owner's `page` is the page, and resetting it stays the owner's job.)
325
+ // A controlled page size counts too: the groups of `TranslationReviewPanel` share one,
326
+ // and a group paged deep into its rows starts over when another group's pager changes it.
315
327
  const controlledSig =
316
- sortsProp !== undefined || filtersProp !== undefined
317
- ? JSON.stringify([sortsProp ?? null, filtersProp ?? null])
328
+ sortsProp !== undefined || filtersProp !== undefined || pageSizeProp !== undefined
329
+ ? JSON.stringify([sortsProp ?? null, filtersProp ?? null, String(pageSizeProp)])
318
330
  : null;
319
331
  const [seenControlledSig, setSeenControlledSig] = useState(controlledSig);
320
332
  if (seenControlledSig !== controlledSig) {
321
333
  setSeenControlledSig(controlledSig);
322
334
  setPage(0);
323
335
  }
324
- const [pageSize, setPageSize] = useState<number>(() => {
325
- if (urlInitial.pageSize !== undefined) return urlInitial.pageSize;
326
- if (initial.pageSize === "all") return Infinity;
327
- if (typeof initial.pageSize === "number" && initial.pageSize > 0) return initial.pageSize;
328
- return defaultPageSize;
329
- });
336
+ const [internalPageSize, setPageSize] = useState<number>(() =>
337
+ urlInitial.pageSize !== undefined ? urlInitial.pageSize : storedPageSize(initial, defaultPageSize),
338
+ );
339
+ const pageSize = pageSizeProp ?? internalPageSize;
330
340
  const [widths, setWidths] = useState<Record<string, number>>(() => initial.widths ?? {});
331
341
  const [hiddenCols, setHiddenCols] = useState<Set<string>>(() => new Set(initial.hidden ?? []));
332
342
  const [showSettings, setShowSettings] = useState(false);
@@ -398,6 +408,77 @@ export function useTableState<T>({
398
408
  };
399
409
  }
400
410
 
411
+ // ---------- One view over several tables ----------
412
+
413
+ /** What {@link useSharedTableView} keeps. */
414
+ export interface SharedTableView {
415
+ sorts: SortState[];
416
+ setSorts: (next: SortState[]) => void;
417
+ /** `Infinity` for "all", as a table holds it. */
418
+ pageSize: number;
419
+ setPageSize: (next: number) => void;
420
+ }
421
+
422
+ /**
423
+ * One table view — the sort and the page size — for an owner that draws it as SEVERAL
424
+ * tables over one `storageKey`: `TranslationReviewPanel`'s groups, which sort and page
425
+ * alike (0.35, Kurvenschmiede: its grouped review stored no view at all). Pass them to
426
+ * each table as `sorts`/`onSortsChange` and `pageSize`/`onPageSizeChange`.
427
+ *
428
+ * Kept in the very blob a `DataTable` with that `storageKey` keeps, not in a format of
429
+ * its own, so the single table and the groups read each other's view. What the blob
430
+ * holds besides (filters, column widths, hidden columns) is carried over as stored, for
431
+ * the single table to find as it left it. Read when `active` turns on — on mount, or
432
+ * when the owner switches to this view — and written on a change, never on a read. The
433
+ * prefix resolves as DataTable's does: prop > provider > the kit's.
434
+ */
435
+ export function useSharedTableView({
436
+ active,
437
+ storageKey,
438
+ storageKeyPrefix,
439
+ defaultPageSize,
440
+ }: {
441
+ active: boolean;
442
+ storageKey?: string;
443
+ storageKeyPrefix?: string;
444
+ defaultPageSize: number;
445
+ }): SharedTableView {
446
+ const providerPrefix = useKitTableStorageKeyPrefix();
447
+ const prefix = storageKeyPrefix ?? providerPrefix ?? DEFAULT_PERSIST_PREFIX;
448
+ const read = () => {
449
+ const stored = active ? loadPersisted(storageKey, prefix) : {};
450
+ return { sorts: normalizeSorts(stored.sort), pageSize: storedPageSize(stored, defaultPageSize) };
451
+ };
452
+ const [view, setView] = useState(read);
453
+ // Adjusted during render when `active` turns on, so the first render of the groups
454
+ // already has the view the single table left, not a frame of the default.
455
+ const [wasActive, setWasActive] = useState(active);
456
+ if (active !== wasActive) {
457
+ setWasActive(active);
458
+ if (active) setView(read());
459
+ }
460
+ const change = (patch: Partial<typeof view>) => {
461
+ const next = { ...view, ...patch };
462
+ setView(next);
463
+ if (!active || !storageKey) return;
464
+ const stored = loadPersisted(storageKey, prefix);
465
+ const blob: PersistedState = {
466
+ sort: next.sorts,
467
+ filters: stored.filters ?? {},
468
+ pageSize: next.pageSize === Infinity ? "all" : next.pageSize,
469
+ widths: stored.widths,
470
+ hidden: stored.hidden,
471
+ };
472
+ writeStored(prefix + storageKey, serializePersisted(blob));
473
+ };
474
+ return {
475
+ sorts: view.sorts,
476
+ setSorts: (sorts) => change({ sorts }),
477
+ pageSize: view.pageSize,
478
+ setPageSize: (pageSize) => change({ pageSize }),
479
+ };
480
+ }
481
+
401
482
  // ---------- Owner-held table state, mirrored to the URL ----------
402
483
 
403
484
  export interface UseTableUrlStateOptions<T> {
@@ -69,9 +69,9 @@ export interface UserAvatarProps extends Omit<ComponentPropsWithRef<"span">, "ch
69
69
  * in the surface colour so it reads as its own mark (keksdose account-menu ~385,
70
70
  * top-bar :152). Its `label` is read as text where the dot sits — inside a button
71
71
  * named by its content it joins that name. In `TopBarActionMenu`'s `trigger` the
72
- * button is named by the menu's `ariaLabel` followed by this label: "Account menu
72
+ * button is named by the menu's `aria-label` followed by this label: "Account menu
73
73
  * 3 unread". The initials stay hidden either way, so the person's name is NOT part
74
- * of it — put it in `ariaLabel` ("Marcel Eifert, account menu") if it should be.
74
+ * of it — put it in `aria-label` ("Marcel Eifert, account menu") if it should be.
75
75
  */
76
76
  badge?: UserAvatarBadge | null;
77
77
  }
@@ -5,16 +5,12 @@ import { cn } from "../lib/cn";
5
5
  export function WizardStepper({
6
6
  steps,
7
7
  current,
8
- "aria-label": ariaLabelAttr,
9
- ariaLabel,
8
+ "aria-label": ariaLabel,
10
9
  }: {
11
10
  steps: { key: string; label: string }[];
12
11
  current: string;
13
12
  /** Names the list of steps ("Import steps"). */
14
13
  "aria-label"?: string;
15
- /** @deprecated 0.34 — use `aria-label`; removed in 0.35 (refactor audit 2026-10, API-1).
16
- * Names the steps only when `aria-label` is absent. */
17
- ariaLabel?: string;
18
14
  }) {
19
15
  const currentIndex = steps.findIndex((s) => s.key === current);
20
16
  return (
@@ -22,7 +18,7 @@ export function WizardStepper({
22
18
  // (0.32.1, the 360 px Extra-large sweep; docs/text-size-harmonization.md §4).
23
19
  // "Upload — Map columns — Confirm" ran 22 px past a 360 px phone at Extra large.
24
20
  // Where the steps fit, they stay one row.
25
- <ol className="flex flex-wrap items-center gap-1 text-xs" aria-label={ariaLabelAttr ?? ariaLabel}>
21
+ <ol className="flex flex-wrap items-center gap-1 text-xs" aria-label={ariaLabel}>
26
22
  {steps.map((step, i) => (
27
23
  <li key={step.key} className="flex items-center gap-1">
28
24
  {i > 0 && <span className="w-4 border-t border-subtle" />}
@@ -0,0 +1,70 @@
1
+ // `@eifi1/ui-kit/currencies` — the currencies the kit's pickers offer, as plain data
2
+ // (0.35, keksdose's 0.34 report).
3
+ //
4
+ // This module imports NOTHING: no React, no stylesheet, no other kit module. That is the
5
+ // entry's whole promise, because an app's BUILD reads it in Node — keksdose's Vite plugin
6
+ // builds its flag-icons subset from these country codes while Vite loads its config. Until
7
+ // 0.35 that plugin parsed the `country` literals out of dist/components/currency-select.js;
8
+ // 0.34 moved the list into the picker's option list and 91 of its test files failed at
9
+ // load. A file inside dist/ is not something the kit promises; an entry is.
10
+ // packaging-contract.test.ts pins the purity, scripts/verify-package.mjs the load in Node.
11
+ //
12
+ // CurrencySelect, AmountInput's currency chip and NumberInput's code detection read this
13
+ // same array (components/currency-option-list re-exports it, and currency-select as it
14
+ // always has), so the list the pickers show and the flags an app ships cannot drift.
15
+
16
+ export interface CurrencyOption {
17
+ code: string;
18
+ name: string;
19
+ symbol: string;
20
+ /** ISO 3166-1 alpha-2 country code (lowercase) used by flag-icons. */
21
+ country: string;
22
+ }
23
+
24
+ export const CURRENCIES: CurrencyOption[] = [
25
+ { code: "EUR", name: "Euro", symbol: "€", country: "eu" },
26
+ { code: "USD", name: "US Dollar", symbol: "$", country: "us" },
27
+ { code: "GBP", name: "British Pound", symbol: "£", country: "gb" },
28
+ { code: "CHF", name: "Swiss Franc", symbol: "CHF", country: "ch" },
29
+ { code: "JPY", name: "Japanese Yen", symbol: "¥", country: "jp" },
30
+ { code: "CAD", name: "Canadian Dollar", symbol: "CA$", country: "ca" },
31
+ { code: "AUD", name: "Australian Dollar", symbol: "A$", country: "au" },
32
+ { code: "NZD", name: "New Zealand Dollar", symbol: "NZ$", country: "nz" },
33
+ { code: "SEK", name: "Swedish Krona", symbol: "kr", country: "se" },
34
+ { code: "NOK", name: "Norwegian Krone", symbol: "kr", country: "no" },
35
+ { code: "DKK", name: "Danish Krone", symbol: "kr", country: "dk" },
36
+ { code: "PLN", name: "Polish Zloty", symbol: "zł", country: "pl" },
37
+ { code: "CZK", name: "Czech Koruna", symbol: "Kč", country: "cz" },
38
+ { code: "HUF", name: "Hungarian Forint", symbol: "Ft", country: "hu" },
39
+ { code: "RON", name: "Romanian Leu", symbol: "lei", country: "ro" },
40
+ { code: "BGN", name: "Bulgarian Lev", symbol: "лв", country: "bg" },
41
+ { code: "TRY", name: "Turkish Lira", symbol: "₺", country: "tr" },
42
+ { code: "RUB", name: "Russian Ruble", symbol: "₽", country: "ru" },
43
+ { code: "CNY", name: "Chinese Yuan", symbol: "¥", country: "cn" },
44
+ { code: "HKD", name: "Hong Kong Dollar", symbol: "HK$", country: "hk" },
45
+ { code: "SGD", name: "Singapore Dollar", symbol: "S$", country: "sg" },
46
+ { code: "INR", name: "Indian Rupee", symbol: "₹", country: "in" },
47
+ { code: "BRL", name: "Brazilian Real", symbol: "R$", country: "br" },
48
+ { code: "MXN", name: "Mexican Peso", symbol: "MX$", country: "mx" },
49
+ { code: "ZAR", name: "South African Rand", symbol: "R", country: "za" },
50
+ { code: "ILS", name: "Israeli Shekel", symbol: "₪", country: "il" },
51
+ { code: "AED", name: "UAE Dirham", symbol: "د.إ", country: "ae" },
52
+ ];
53
+
54
+ /**
55
+ * Each currency code → the flag the kit paints for it: the lowercase ISO 3166-1 alpha-2
56
+ * code flag-icons names it by (`fi-ch`, `flags/4x3/ch.svg`). `CHF` → `"ch"`, `EUR` →
57
+ * `"eu"` (flag-icons has the EU's flag under that code).
58
+ *
59
+ * Derived from {@link CURRENCIES}, so a currency the pickers gain is a flag here too. An
60
+ * app that ships only the flags it renders builds its subset from these values:
61
+ *
62
+ * ```ts
63
+ * // vite.config.ts — plain Node, no DOM
64
+ * import { CURRENCY_COUNTRIES } from "@eifi1/ui-kit/currencies";
65
+ * const flags = new Set(Object.values(CURRENCY_COUNTRIES));
66
+ * ```
67
+ */
68
+ export const CURRENCY_COUNTRIES: Record<string, string> = Object.fromEntries(
69
+ CURRENCIES.map((c) => [c.code, c.country]),
70
+ );
@@ -1,5 +1,5 @@
1
1
  import { useState } from "react";
2
- import type { ComponentPropsWithoutRef } from "react";
2
+ import type { ComponentPropsWithRef } from "react";
3
3
  import { ChevronDown, ChevronUp, Timer } from "lucide-react";
4
4
 
5
5
  import { cn } from "../lib/cn";
@@ -30,7 +30,12 @@ function timeText(countdown: DemoCountdown, labels: DemoLabels): string {
30
30
  : labels.hoursLeft(countdown.hours, countdown.minutes);
31
31
  }
32
32
 
33
- export interface DemoBannerProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "onClick"> {
33
+ /**
34
+ * A `div`'s props, `ref` included since 0.35 (round 0.35 R4, the follow-up to API-2). They
35
+ * go on to the {@link AlertBanner} strip, so the ref is the strip's own `<div>` — never a
36
+ * link or a button: the banner takes no `href` or `onClick` of its own.
37
+ */
38
+ export interface DemoBannerProps extends Omit<ComponentPropsWithRef<"div">, "children" | "onClick"> {
34
39
  /** The demo's end: `expires_at` from the `TokenResponse`, or `demo_expires_at` from
35
40
  * `/auth/me` (§5.3). */
36
41
  expiresAt: DemoExpiry | null | undefined;