@eifi1/ui-kit 0.7.1 → 0.8.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 (222) hide show
  1. package/README.md +32 -17
  2. package/dist/chart.d.ts +3 -2
  3. package/dist/components/alert-banner.d.ts +59 -10
  4. package/dist/components/alert-banner.js +114 -8
  5. package/dist/components/alert-banner.js.map +1 -1
  6. package/dist/components/amount-input.d.ts +3 -0
  7. package/dist/components/button-group.d.ts +26 -0
  8. package/dist/components/button-group.js +49 -0
  9. package/dist/components/button-group.js.map +1 -0
  10. package/dist/components/calculator.d.ts +3 -0
  11. package/dist/components/chart-zoom.d.ts +63 -14
  12. package/dist/components/chart-zoom.js +82 -16
  13. package/dist/components/chart-zoom.js.map +1 -1
  14. package/dist/components/chip.d.ts +67 -2
  15. package/dist/components/chip.js +73 -8
  16. package/dist/components/chip.js.map +1 -1
  17. package/dist/components/confirm-dialog.d.ts +120 -0
  18. package/dist/components/confirm-dialog.js +96 -0
  19. package/dist/components/confirm-dialog.js.map +1 -0
  20. package/dist/components/copy-button.d.ts +62 -0
  21. package/dist/components/copy-button.js +95 -0
  22. package/dist/components/copy-button.js.map +1 -0
  23. package/dist/components/data-table-filter-popover.d.ts +1 -1
  24. package/dist/components/data-table-filters.d.ts +1 -1
  25. package/dist/components/data-table-pagination.d.ts +5 -1
  26. package/dist/components/data-table-pagination.js +71 -58
  27. package/dist/components/data-table-pagination.js.map +1 -1
  28. package/dist/components/data-table-sort.d.ts +28 -5
  29. package/dist/components/data-table-sort.js +11 -7
  30. package/dist/components/data-table-sort.js.map +1 -1
  31. package/dist/components/data-table.d.ts +1 -1
  32. package/dist/components/data-table.js +544 -423
  33. package/dist/components/data-table.js.map +1 -1
  34. package/dist/components/date-picker.d.ts +51 -5
  35. package/dist/components/date-picker.js +152 -40
  36. package/dist/components/date-picker.js.map +1 -1
  37. package/dist/components/description-list.d.ts +60 -0
  38. package/dist/components/description-list.js +112 -0
  39. package/dist/components/description-list.js.map +1 -0
  40. package/dist/components/disclosure.d.ts +37 -2
  41. package/dist/components/disclosure.js +14 -5
  42. package/dist/components/disclosure.js.map +1 -1
  43. package/dist/components/facing-pair.d.ts +2 -0
  44. package/dist/components/file-button.d.ts +17 -1
  45. package/dist/components/file-button.js +8 -1
  46. package/dist/components/file-button.js.map +1 -1
  47. package/dist/components/file-dropzone.d.ts +54 -9
  48. package/dist/components/file-dropzone.js +100 -70
  49. package/dist/components/file-dropzone.js.map +1 -1
  50. package/dist/components/floating-panel.d.ts +107 -0
  51. package/dist/components/floating-panel.js +206 -0
  52. package/dist/components/floating-panel.js.map +1 -0
  53. package/dist/components/full-bleed-dialog.d.ts +12 -1
  54. package/dist/components/full-bleed-dialog.js +26 -5
  55. package/dist/components/full-bleed-dialog.js.map +1 -1
  56. package/dist/components/modal.d.ts +6 -2
  57. package/dist/components/modal.js +2 -1
  58. package/dist/components/modal.js.map +1 -1
  59. package/dist/components/number-field.d.ts +3 -0
  60. package/dist/components/number-input.d.ts +3 -0
  61. package/dist/components/numpad-sheet.d.ts +3 -0
  62. package/dist/components/progress-bar.d.ts +54 -0
  63. package/dist/components/progress-bar.js +94 -0
  64. package/dist/components/progress-bar.js.map +1 -0
  65. package/dist/components/scroll-area.d.ts +53 -0
  66. package/dist/components/scroll-area.js +86 -0
  67. package/dist/components/scroll-area.js.map +1 -0
  68. package/dist/components/separator.d.ts +23 -0
  69. package/dist/components/separator.js +24 -0
  70. package/dist/components/separator.js.map +1 -0
  71. package/dist/components/series-chart-ticks.d.ts +34 -1
  72. package/dist/components/series-chart-ticks.js +83 -1
  73. package/dist/components/series-chart-ticks.js.map +1 -1
  74. package/dist/components/series-chart.d.ts +314 -19
  75. package/dist/components/series-chart.js +510 -120
  76. package/dist/components/series-chart.js.map +1 -1
  77. package/dist/components/skeleton.d.ts +36 -0
  78. package/dist/components/skeleton.js +35 -0
  79. package/dist/components/skeleton.js.map +1 -0
  80. package/dist/components/stat-tile.d.ts +60 -6
  81. package/dist/components/stat-tile.js +60 -26
  82. package/dist/components/stat-tile.js.map +1 -1
  83. package/dist/components/table.d.ts +83 -0
  84. package/dist/components/table.js +166 -0
  85. package/dist/components/table.js.map +1 -0
  86. package/dist/components/toggle-legend.d.ts +3 -2
  87. package/dist/components/toggle-legend.js +1 -1
  88. package/dist/components/toggle-legend.js.map +1 -1
  89. package/dist/components/tree-view.d.ts +129 -0
  90. package/dist/components/tree-view.js +376 -0
  91. package/dist/components/tree-view.js.map +1 -0
  92. package/dist/components/treemap.d.ts +9 -2
  93. package/dist/components/treemap.js +2 -1
  94. package/dist/components/treemap.js.map +1 -1
  95. package/dist/components/ui.d.ts +87 -13
  96. package/dist/components/ui.js +111 -29
  97. package/dist/components/ui.js.map +1 -1
  98. package/dist/components/use-table-state.d.ts +1 -1
  99. package/dist/{data-table-filters-noy0Abvi.d.ts → data-table-filters-CF1PXqjQ.d.ts} +73 -3
  100. package/dist/data-table.d.ts +2 -2
  101. package/dist/data-table.js.map +1 -1
  102. package/dist/hooks/use-copy-to-clipboard.d.ts +28 -0
  103. package/dist/hooks/use-copy-to-clipboard.js +81 -0
  104. package/dist/hooks/use-copy-to-clipboard.js.map +1 -0
  105. package/dist/hooks/use-debounce.d.ts +43 -0
  106. package/dist/hooks/use-debounce.js +56 -0
  107. package/dist/hooks/use-debounce.js.map +1 -0
  108. package/dist/hooks/use-file-drop.d.ts +72 -0
  109. package/dist/hooks/use-file-drop.js +57 -0
  110. package/dist/hooks/use-file-drop.js.map +1 -0
  111. package/dist/i18n/defaults.d.ts +3 -0
  112. package/dist/i18n/defaults.js +7 -1
  113. package/dist/i18n/defaults.js.map +1 -1
  114. package/dist/i18n/kit-labels.d.ts +15 -0
  115. package/dist/i18n/kit-labels.js +6 -2
  116. package/dist/i18n/kit-labels.js.map +1 -1
  117. package/dist/i18n/locales/de-CH-informal.d.ts +43 -0
  118. package/dist/i18n/locales/de-CH-informal.js +8 -0
  119. package/dist/i18n/locales/de-CH-informal.js.map +1 -0
  120. package/dist/i18n/locales/de-CH.d.ts +3 -0
  121. package/dist/i18n/locales/de-CH.js +1 -14
  122. package/dist/i18n/locales/de-CH.js.map +1 -1
  123. package/dist/i18n/locales/de-informal.d.ts +64 -0
  124. package/dist/i18n/locales/de-informal.js +37 -0
  125. package/dist/i18n/locales/de-informal.js.map +1 -0
  126. package/dist/i18n/locales/de.d.ts +3 -0
  127. package/dist/i18n/locales/de.js +25 -0
  128. package/dist/i18n/locales/de.js.map +1 -1
  129. package/dist/i18n/locales/es.d.ts +3 -0
  130. package/dist/i18n/locales/es.js +25 -0
  131. package/dist/i18n/locales/es.js.map +1 -1
  132. package/dist/i18n/locales/fr.d.ts +3 -0
  133. package/dist/i18n/locales/fr.js +25 -0
  134. package/dist/i18n/locales/fr.js.map +1 -1
  135. package/dist/i18n/locales/hu.d.ts +3 -0
  136. package/dist/i18n/locales/hu.js +25 -0
  137. package/dist/i18n/locales/hu.js.map +1 -1
  138. package/dist/i18n/locales/it.d.ts +3 -0
  139. package/dist/i18n/locales/it.js +25 -0
  140. package/dist/i18n/locales/it.js.map +1 -1
  141. package/dist/i18n/locales/zh.d.ts +3 -0
  142. package/dist/i18n/locales/zh.js +25 -0
  143. package/dist/i18n/locales/zh.js.map +1 -1
  144. package/dist/i18n/swiss.d.ts +13 -0
  145. package/dist/i18n/swiss.js +19 -0
  146. package/dist/i18n/swiss.js.map +1 -0
  147. package/dist/index.d.ts +25 -10
  148. package/dist/index.js +22 -0
  149. package/dist/index.js.map +1 -1
  150. package/dist/lib/dates.d.ts +37 -1
  151. package/dist/lib/dates.js +19 -0
  152. package/dist/lib/dates.js.map +1 -1
  153. package/dist/search/command-palette.d.ts +15 -1
  154. package/dist/search/command-palette.js +10 -2
  155. package/dist/search/command-palette.js.map +1 -1
  156. package/dist/wizard/stepper-nav.d.ts +35 -2
  157. package/dist/wizard/stepper-nav.js +54 -22
  158. package/dist/wizard/stepper-nav.js.map +1 -1
  159. package/dist/wizard/types.d.ts +107 -5
  160. package/dist/wizard/types.js +2 -1
  161. package/dist/wizard/types.js.map +1 -1
  162. package/dist/wizard/use-wizard.js +141 -21
  163. package/dist/wizard/use-wizard.js.map +1 -1
  164. package/dist/wizard/wizard-summary.d.ts +10 -2
  165. package/dist/wizard/wizard-summary.js +3 -1
  166. package/dist/wizard/wizard-summary.js.map +1 -1
  167. package/dist/wizard.d.ts +4 -1
  168. package/dist/wizard.js.map +1 -1
  169. package/package.json +1 -1
  170. package/src/components/alert-banner.tsx +211 -20
  171. package/src/components/button-group.tsx +75 -0
  172. package/src/components/chart-zoom.tsx +158 -20
  173. package/src/components/chip.tsx +160 -9
  174. package/src/components/confirm-dialog.tsx +242 -0
  175. package/src/components/copy-button.tsx +158 -0
  176. package/src/components/data-table-pagination.tsx +19 -6
  177. package/src/components/data-table-sort.ts +49 -10
  178. package/src/components/data-table.tsx +293 -24
  179. package/src/components/date-picker.tsx +262 -57
  180. package/src/components/description-list.tsx +174 -0
  181. package/src/components/disclosure.tsx +58 -5
  182. package/src/components/file-button.tsx +22 -2
  183. package/src/components/file-dropzone.tsx +198 -100
  184. package/src/components/floating-panel.tsx +341 -0
  185. package/src/components/full-bleed-dialog.tsx +48 -5
  186. package/src/components/modal.tsx +9 -2
  187. package/src/components/progress-bar.tsx +163 -0
  188. package/src/components/scroll-area.tsx +129 -0
  189. package/src/components/separator.tsx +37 -0
  190. package/src/components/series-chart-ticks.ts +135 -0
  191. package/src/components/series-chart.tsx +934 -67
  192. package/src/components/skeleton.tsx +65 -0
  193. package/src/components/stat-tile.tsx +160 -27
  194. package/src/components/table.tsx +263 -0
  195. package/src/components/toggle-legend.tsx +4 -3
  196. package/src/components/tree-view.tsx +589 -0
  197. package/src/components/treemap.tsx +10 -2
  198. package/src/components/ui.tsx +263 -39
  199. package/src/data-table.ts +2 -0
  200. package/src/hooks/use-copy-to-clipboard.ts +122 -0
  201. package/src/hooks/use-debounce.ts +105 -0
  202. package/src/hooks/use-file-drop.ts +123 -0
  203. package/src/i18n/defaults.ts +6 -0
  204. package/src/i18n/kit-labels.tsx +19 -0
  205. package/src/i18n/locales/de-CH-informal.ts +10 -0
  206. package/src/i18n/locales/de-CH.ts +1 -16
  207. package/src/i18n/locales/de-informal.ts +61 -0
  208. package/src/i18n/locales/de.ts +25 -0
  209. package/src/i18n/locales/es.ts +25 -0
  210. package/src/i18n/locales/fr.ts +25 -0
  211. package/src/i18n/locales/hu.ts +25 -0
  212. package/src/i18n/locales/it.ts +25 -0
  213. package/src/i18n/locales/zh.ts +25 -0
  214. package/src/i18n/swiss.ts +25 -0
  215. package/src/index.ts +29 -0
  216. package/src/lib/dates.ts +48 -0
  217. package/src/search/command-palette.tsx +29 -3
  218. package/src/wizard/stepper-nav.tsx +102 -28
  219. package/src/wizard/types.ts +106 -4
  220. package/src/wizard/use-wizard.ts +211 -31
  221. package/src/wizard/wizard-summary.tsx +24 -12
  222. package/src/wizard.ts +3 -2
@@ -3,17 +3,33 @@ import { ButtonHTMLAttributes, Ref, SelectHTMLAttributes, ReactNode, InputHTMLAt
3
3
  import { TooltipSide } from './tooltip.js';
4
4
  import '../hooks/use-anchored-rect.js';
5
5
 
6
- type ButtonVariant = "primary" | "secondary" | "ghost" | "danger" | "brand";
6
+ type ButtonVariant = "primary" | "secondary" | "ghost" | "danger" | "brand" | "link";
7
+ type ButtonSize = "sm" | "md";
8
+ /** The second argument of {@link buttonClasses} in its options form. */
9
+ interface ButtonClassesOptions {
10
+ /** See {@link ButtonProps.size}. */
11
+ size?: ButtonSize;
12
+ className?: string;
13
+ }
7
14
  /**
8
15
  * Button classes for the rare case where the styling must land on a non-`<button>`
9
16
  * element that {@link Button} can't render — e.g. a router `<Link>` or a Radix
10
17
  * AlertDialog Action/Cancel (which must stay the Radix element). Everywhere a real
11
- * button works, prefer `<Button>`. Draws from the same base + variant maps as
18
+ * button works, prefer `<Button>`. Draws from the same base, size and variant maps as
12
19
  * `<Button>`, so the two stay in lockstep.
20
+ *
21
+ * The second argument is either the extra classes (the pre-0.8.0 form) or
22
+ * `{ size, className }` — `buttonClasses("secondary", { size: "sm" })` for keksdose's
23
+ * compact toolbar links.
13
24
  */
14
- declare function buttonClasses(variant?: ButtonVariant, className?: string): string;
25
+ declare function buttonClasses(variant?: ButtonVariant, classNameOrOptions?: string | ButtonClassesOptions): string;
15
26
  interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
16
27
  variant?: ButtonVariant;
28
+ /** `md` (default) is the page's action button. `sm` is the compact one — 12px text
29
+ * and `px-2 py-1` — for the secondary actions in a toolbar, a card header or a
30
+ * table's header row (keksdose writes `px-2 py-1 text-xs` over `secondary` by hand
31
+ * there). Every variant takes either size. */
32
+ size?: ButtonSize;
17
33
  /** In a flex row next to a taller labelled field, fill the field's height so the
18
34
  * two line up. No effect outside a flex row. */
19
35
  stretch?: boolean;
@@ -22,23 +38,40 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
22
38
  * declared here only because `ButtonHTMLAttributes` does not carry it. */
23
39
  ref?: Ref<HTMLButtonElement>;
24
40
  }
25
- declare function Button({ variant, stretch, className, ...rest }: ButtonProps): react.JSX.Element;
26
- type IconButtonSize = "md" | "sm" | "xs" | "2xs";
41
+ declare function Button({ variant, size, stretch, className, ...rest }: ButtonProps): react.JSX.Element;
42
+ type IconButtonSize = "lg" | "md" | "sm" | "xs" | "2xs";
27
43
  declare const ICON_BUTTON_TONES: {
28
44
  readonly default: "";
29
45
  readonly muted: "text-[var(--text-placeholder)] hover:bg-[var(--bg-surface-2)] hover:text-[var(--text-primary)]";
30
46
  readonly danger: "text-[var(--text-placeholder)] hover:bg-[var(--danger-bg)] hover:text-[var(--danger)] focus:ring-[var(--danger-border)]";
47
+ readonly warning: "text-[var(--warning)] hover:bg-[var(--warning-bg)] focus:ring-[var(--warning-border)]";
48
+ readonly info: "text-[var(--info)] hover:bg-[var(--info-bg)] focus:ring-[var(--info-border)]";
31
49
  };
32
50
  interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
33
- variant?: ButtonVariant;
34
- /** Box size: md = 36px (matches the top bar), sm = 32px — both with a 20px icon;
51
+ /** Any {@link ButtonVariant}, or `overlay` — a round translucent disc for use over
52
+ * an image (see `ICON_BUTTON_OVERLAY`). */
53
+ variant?: ButtonVariant | "overlay";
54
+ /** Box size: lg = 44px (a phone's touch target), md = 36px (matches the top bar),
55
+ * sm = 32px — all three with a 20px icon;
35
56
  * xs = 28px with a 16px icon (an action in a list row), 2xs = 24px with a 14px
36
57
  * icon (an action on a chip or a tab). */
37
58
  size?: IconButtonSize;
38
59
  /** Glyph colour over the variant. `muted`: placeholder grey, full text colour on
39
60
  * hover. `danger`: the same grey at rest, `--danger` on hover and focus — for a
40
- * remove/delete that repeats down a list. Default: the variant's own colours. */
61
+ * remove/delete that repeats down a list. `warning`: amber at rest — a flag that
62
+ * wants attention. `info`: sky at rest — a notice-worthy but harmless action
63
+ * (keksdose's reconcile). Default: the variant's own colours. */
41
64
  tone?: keyof typeof ICON_BUTTON_TONES;
65
+ /**
66
+ * Make it a toggle button. `true` sets `aria-pressed="true"` and draws the "on" look
67
+ * (brand glyph on the quiet brand fill); `false` sets `aria-pressed="false"` with the
68
+ * normal look, so a screen reader still hears a toggle that is off. Left out, it is
69
+ * an ordinary button with no `aria-pressed` — or whatever `aria-pressed` the caller
70
+ * passes. For keksdose's budget share toggle (budgets-page:291), which paints its own
71
+ * brand colour over a ghost button today. Keep the `aria-label` the same in both
72
+ * states ("Share budget"): the pressed state already says whether it is on.
73
+ */
74
+ pressed?: boolean;
42
75
  /** Keep the click (and the Enter/Space that produces it) from reaching an
43
76
  * ancestor's handler — for an action inside a clickable table row or card.
44
77
  *
@@ -302,8 +335,18 @@ interface CardProps extends ComponentPropsWithoutRef<"div"> {
302
335
  * content cards on data pages; leave off for centered dialog/panel cards.
303
336
  */
304
337
  flush?: boolean;
338
+ /**
339
+ * `inset`: a panel INSIDE a card rather than a card on the page — the raised
340
+ * `--bg-surface-2`, a small radius, `p-3` of its own and no border or shadow, since
341
+ * it is already sitting on the card that has them. Lenkbank's results blocks and
342
+ * corner panels write this by hand (`rounded-md bg-surface-2 p-3`) under a Card of
343
+ * their own. Unlike the default card it carries its padding, because every one of
344
+ * those copies wanted the same one; a caller's `p-*` still wins. `flush` is ignored:
345
+ * an inset panel never runs edge to edge.
346
+ */
347
+ variant?: "default" | "inset";
305
348
  }
306
- declare function Card({ className, children, flush, ...rest }: CardProps): react.JSX.Element;
349
+ declare function Card({ className, children, flush, variant, ...rest }: CardProps): react.JSX.Element;
307
350
  /** The sub-parts add nothing to a `<div>`'s props — they are the SAME element with a
308
351
  * `data-slot` and a padding rhythm — so each name is an alias rather than an empty
309
352
  * interface pretending to be more. They exist so a consumer's own wrapper can say
@@ -354,11 +397,22 @@ declare function Spinner({ className, label, ...rest }: SpinnerProps): react.JSX
354
397
  interface EmptyStateProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
355
398
  /** The box renders `title` and `hint` in its own two-line rhythm, which is what makes
356
399
  * every empty state in three apps look like the same thing — so there is no
357
- * `children` slot to put arbitrary content in. */
400
+ * `children` slot to put arbitrary content in. The two slots below are the only
401
+ * other things an empty state has turned out to need, and each has a fixed place. */
358
402
  title: string;
359
403
  hint?: string;
404
+ /** A glyph ABOVE the title — kastlan's InboxEmptyState (an inbox), keksdose's
405
+ * offline card (a cloud with a slash). Sized by the box (`[&_svg]:size-8`) and
406
+ * muted, so five call sites cannot pick five sizes; hidden from assistive tech,
407
+ * because the title already says what it shows. */
408
+ icon?: ReactNode;
409
+ /** What to do about it, BELOW the hint: keksdose's query-state cards (Retry), its
410
+ * error boundary (Retry / Reload / Go home — pass all three in a fragment, they
411
+ * wrap and centre as one row), kastlan's "Create a rule". Buttons or links the
412
+ * caller renders; the box only places them. */
413
+ action?: ReactNode;
360
414
  }
361
- declare function EmptyState({ title, hint, className, ...rest }: EmptyStateProps): react.JSX.Element;
415
+ declare function EmptyState({ title, hint, icon, action, className, ...rest }: EmptyStateProps): react.JSX.Element;
362
416
  /** One tab of a {@link Tabs} strip. */
363
417
  interface TabItem<T extends string> {
364
418
  id: T;
@@ -520,7 +574,27 @@ interface TabsProps<T extends string> extends Omit<ComponentPropsWithoutRef<"div
520
574
  /** An add or a remove is in flight: the × and the add button are disabled and
521
575
  * Delete is ignored, so a double click cannot remove two tabs. Nothing moves. */
522
576
  busy?: boolean;
577
+ /**
578
+ * `"vertical"`: the same strip as a SIDE NAV — one item per row, the open one
579
+ * filled, ↑/↓ walking it (Home/End as before) and `aria-orientation="vertical"` on
580
+ * the tablist. For keksdose's admin page and settings page, which each hand-built
581
+ * the identical sidebar with none of this strip's keyboard, roving tab stop or
582
+ * routed-`href` handling. Everything else — `href` tabs, `badge`, `panelId`,
583
+ * `onRemove`, `onAdd` — means exactly what it means on the horizontal strip; the
584
+ * badge moves to the row's end.
585
+ *
586
+ * **On a phone ({@link PHONE_QUERY}) it becomes the horizontal strip**, `wrap` and
587
+ * all, rather than staying a column. A side nav only works beside its content; on
588
+ * a phone it has to go ABOVE it, and eight full-width rows there push the panel
589
+ * the user picked below the fold on every visit — the two keksdose pages both
590
+ * collapsed theirs into a scrolling row by hand for exactly that reason. The
591
+ * orientation is switched in JavaScript, not with `md:` classes, so the
592
+ * `aria-orientation` a screen reader hears and the arrow keys that work always
593
+ * match the layout on screen. The caller's own two-column layout has to stack at
594
+ * the same breakpoint (`md:grid-cols-[14rem_1fr]`).
595
+ */
596
+ orientation?: "horizontal" | "vertical";
523
597
  }
524
- declare function Tabs<T extends string>({ tabs, active, onChange, className, wrap, label, panelId, onRemove, removeOn, onAdd, addLabel, labels, busy, "aria-label": ariaLabel, ...rest }: TabsProps<T>): react.JSX.Element;
598
+ declare function Tabs<T extends string>({ tabs, active, onChange, className, wrap, label, panelId, onRemove, removeOn, onAdd, addLabel, labels, busy, orientation, "aria-label": ariaLabel, ...rest }: TabsProps<T>): react.JSX.Element;
525
599
 
526
- export { Button, type ButtonProps, type ButtonVariant, Card, CardAction, type CardActionProps, CardContent, type CardContentProps, CardDescription, type CardDescriptionProps, CardFooter, type CardFooterProps, CardHeader, type CardHeaderProps, type CardProps, CardTitle, type CardTitleProps, DEFAULT_PASSWORD_REVEAL_LABELS, DEFAULT_TABS_LABELS, EmptyState, type EmptyStateProps, FIELD_BASE, FIELD_DISPLAY, FIELD_FLOATING_PAD, FIELD_INVALID, FIELD_TRIGGER, FIELD_WRITABLE_LOOK, FLOATING_INPUT_CLASS, FLOATING_LABEL_CLASS, FLOATING_LABEL_STATIC, FieldChevron, type FieldChevronProps, FieldHint, type FieldHintProps, FieldLabel, type FieldLabelProps, FloatingField, type FloatingFieldProps, IconButton, type IconButtonProps, type IconButtonSize, Input, type InputProps, Label, type LabelProps, PHONE_QUERY, type PasswordRevealLabels, Select, type SelectProps, Spinner, type SpinnerProps, type TabItem, Tabs, type TabsLabels, type TabsProps, Textarea, type TextareaProps, buttonClasses, resolvePasswordRevealLabels };
600
+ export { Button, type ButtonClassesOptions, type ButtonProps, type ButtonSize, type ButtonVariant, Card, CardAction, type CardActionProps, CardContent, type CardContentProps, CardDescription, type CardDescriptionProps, CardFooter, type CardFooterProps, CardHeader, type CardHeaderProps, type CardProps, CardTitle, type CardTitleProps, DEFAULT_PASSWORD_REVEAL_LABELS, DEFAULT_TABS_LABELS, EmptyState, type EmptyStateProps, FIELD_BASE, FIELD_DISPLAY, FIELD_FLOATING_PAD, FIELD_INVALID, FIELD_TRIGGER, FIELD_WRITABLE_LOOK, FLOATING_INPUT_CLASS, FLOATING_LABEL_CLASS, FLOATING_LABEL_STATIC, FieldChevron, type FieldChevronProps, FieldHint, type FieldHintProps, FieldLabel, type FieldLabelProps, FloatingField, type FloatingFieldProps, IconButton, type IconButtonProps, type IconButtonSize, Input, type InputProps, Label, type LabelProps, PHONE_QUERY, type PasswordRevealLabels, Select, type SelectProps, Spinner, type SpinnerProps, type TabItem, Tabs, type TabsLabels, type TabsProps, Textarea, type TextareaProps, buttonClasses, resolvePasswordRevealLabels };
@@ -7,24 +7,38 @@ import { horizontalStep } from "../lib/direction.js";
7
7
  import { useMediaQuery } from "../hooks/use-media-query.js";
8
8
  import { Tooltip } from "./tooltip.js";
9
9
  import { DEFAULT_COMMON_LABELS, useKitLabels } from "../i18n/kit-labels.js";
10
- const BUTTON_BASE = "inline-flex items-center justify-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed";
10
+ const BUTTON_BASE = "inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed";
11
+ const BUTTON_SIZES = {
12
+ md: "gap-2 px-3 py-2 text-sm",
13
+ sm: "gap-1.5 px-2 py-1 text-xs"
14
+ };
11
15
  const buttonVariantClasses = {
12
16
  primary: "border border-[var(--border)] bg-[var(--bg-surface-2)] text-[var(--text-primary)] hover:bg-[var(--border)] focus:ring-[var(--brand)]",
13
17
  secondary: "border border-[var(--border)] bg-transparent text-[var(--text-primary)] hover:bg-[var(--bg-surface-2)] focus:ring-[var(--brand)]",
14
18
  ghost: "bg-transparent text-[var(--text-primary)] hover:bg-[var(--bg-surface-2)] focus:ring-[var(--brand)]",
15
19
  danger: "bg-[var(--danger)] text-[var(--danger-contrast)] hover:bg-[var(--danger-hover)] focus:ring-[var(--danger-border)]",
16
- brand: "bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)] focus:ring-[var(--brand)]"
20
+ brand: "bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)] focus:ring-[var(--brand)]",
21
+ // A text link that is still a `<button>` — keksdose's six hand-rolled
22
+ // `<button className="text-brand underline">` sites ("Resend code", "Show all",
23
+ // "Undo" in a toast…), which act rather than navigate and so must not be anchors.
24
+ // The box padding goes (`p-0`) so it sits in a sentence at the text's own size, but
25
+ // the base's `focus:ring-2` stays: those copies had `outline-none` and no ring, so
26
+ // a keyboard user tabbing onto them saw nothing at all. `rounded-sm` keeps that
27
+ // ring hugging the word instead of drawing a pill round it.
28
+ link: "rounded-sm p-0 bg-transparent text-[var(--brand)] underline-offset-4 hover:underline focus:ring-[var(--brand)]"
17
29
  };
18
- function buttonClasses(variant = "primary", className) {
19
- return cn(BUTTON_BASE, buttonVariantClasses[variant], className);
30
+ function buttonClasses(variant = "primary", classNameOrOptions) {
31
+ const { size = "md", className } = typeof classNameOrOptions === "object" ? classNameOrOptions : { className: classNameOrOptions };
32
+ return cn(BUTTON_BASE, BUTTON_SIZES[size], buttonVariantClasses[variant], className);
20
33
  }
21
- function Button({ variant = "primary", stretch, className, ...rest }) {
34
+ function Button({ variant = "primary", size = "md", stretch, className, ...rest }) {
22
35
  return /* @__PURE__ */ jsx(
23
36
  "button",
24
37
  {
25
38
  ...rest,
26
39
  className: cn(
27
40
  BUTTON_BASE,
41
+ BUTTON_SIZES[size],
28
42
  // In a flex row next to a taller labelled field, `stretch` makes the button
29
43
  // fill the field's height so the two line up (self-stretch overrides the row's
30
44
  // align-items). No effect outside a flex row / when it's already the tallest.
@@ -37,6 +51,9 @@ function Button({ variant = "primary", stretch, className, ...rest }) {
37
51
  }
38
52
  const ICON_BUTTON_BASE = "inline-flex items-center justify-center rounded-md transition-colors focus:outline-none focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed";
39
53
  const ICON_BUTTON_SIZES = {
54
+ // The 44px touch target (WCAG 2.5.5's size) with the same 20px glyph — keksdose's
55
+ // bulk-action bars write `size-11` by hand over an `md` button to get it on phones.
56
+ lg: "size-11 [&_svg]:size-5",
40
57
  md: "size-9 [&_svg]:size-5",
41
58
  sm: "size-8 [&_svg]:size-5",
42
59
  xs: "size-7 rounded [&_svg]:size-4",
@@ -45,14 +62,39 @@ const ICON_BUTTON_SIZES = {
45
62
  const ICON_BUTTON_TONES = {
46
63
  default: "",
47
64
  muted: "text-[var(--text-placeholder)] hover:bg-[var(--bg-surface-2)] hover:text-[var(--text-primary)]",
48
- danger: "text-[var(--text-placeholder)] hover:bg-[var(--danger-bg)] hover:text-[var(--danger)] focus:ring-[var(--danger-border)]"
65
+ danger: "text-[var(--text-placeholder)] hover:bg-[var(--danger-bg)] hover:text-[var(--danger)] focus:ring-[var(--danger-border)]",
66
+ // Amber AT REST, unlike `danger`: a warning icon button is the one on the row that
67
+ // needs attention (keksdose's "needs review" flag on a transaction, the stale-rate
68
+ // marker), not an action repeated down a list — quiet grey would hide the very
69
+ // thing it is there to point out.
70
+ warning: "text-[var(--warning)] hover:bg-[var(--warning-bg)] focus:ring-[var(--warning-border)]",
71
+ // Sky at rest, for the same reason as `warning`: keksdose's reconcile action on an
72
+ // account row (accounts-page:867) is the one on the row to notice, and it is
73
+ // informational rather than a problem, so it takes the `--info` family.
74
+ info: "text-[var(--info)] hover:bg-[var(--info-bg)] focus:ring-[var(--info-border)]"
75
+ };
76
+ const ICON_BUTTON_DISABLED_REST = {
77
+ primary: "disabled:hover:bg-[var(--bg-surface-2)]",
78
+ secondary: "disabled:hover:bg-transparent",
79
+ ghost: "disabled:hover:bg-transparent",
80
+ danger: "disabled:hover:bg-[var(--danger)]",
81
+ brand: "disabled:hover:bg-[var(--brand)]",
82
+ link: "disabled:hover:bg-transparent disabled:hover:no-underline",
83
+ overlay: "disabled:hover:bg-[color-mix(in_srgb,var(--bg-inverse)_60%,transparent)]"
49
84
  };
50
- const IconButton = forwardRef(function IconButton2({ variant = "ghost", size = "md", tone = "default", stopPropagation, className, onClick, onKeyDown, ...rest }, ref) {
85
+ const ICON_BUTTON_TONES_DISABLED_REST = {
86
+ muted: "disabled:hover:text-[var(--text-placeholder)]",
87
+ danger: "disabled:hover:text-[var(--text-placeholder)]"
88
+ };
89
+ const ICON_BUTTON_PRESSED = "bg-[var(--brand-bg)] text-[var(--brand)] hover:bg-[var(--brand-bg-hover)] hover:text-[var(--brand)] disabled:hover:bg-[var(--brand-bg)] disabled:hover:text-[var(--brand)]";
90
+ const ICON_BUTTON_OVERLAY = "rounded-full bg-[color-mix(in_srgb,var(--bg-inverse)_60%,transparent)] text-[var(--text-inverse)] backdrop-blur-sm hover:bg-[color-mix(in_srgb,var(--bg-inverse)_75%,transparent)] focus:ring-[var(--text-inverse)]";
91
+ const IconButton = forwardRef(function IconButton2({ variant = "ghost", size = "md", tone = "default", pressed, stopPropagation, className, onClick, onKeyDown, ...rest }, ref) {
51
92
  return /* @__PURE__ */ jsx(
52
93
  "button",
53
94
  {
54
95
  ref,
55
96
  ...rest,
97
+ "aria-pressed": pressed ?? rest["aria-pressed"],
56
98
  onClick: (e) => {
57
99
  if (stopPropagation) e.stopPropagation();
58
100
  onClick?.(e);
@@ -64,8 +106,12 @@ const IconButton = forwardRef(function IconButton2({ variant = "ghost", size = "
64
106
  className: cn(
65
107
  ICON_BUTTON_BASE,
66
108
  ICON_BUTTON_SIZES[size],
67
- buttonVariantClasses[variant],
109
+ // After the size, so the overlay's `rounded-full` beats the small sizes' `rounded`.
110
+ variant === "overlay" ? ICON_BUTTON_OVERLAY : buttonVariantClasses[variant],
111
+ ICON_BUTTON_DISABLED_REST[variant],
68
112
  ICON_BUTTON_TONES[tone],
113
+ ICON_BUTTON_TONES_DISABLED_REST[tone],
114
+ pressed && ICON_BUTTON_PRESSED,
69
115
  className
70
116
  )
71
117
  }
@@ -439,17 +485,19 @@ const Textarea = forwardRef(function Textarea2({ className, label, id, placehold
439
485
  ) }) });
440
486
  });
441
487
  Textarea.displayName = "Textarea";
442
- function Card({ className, children, flush, ...rest }) {
488
+ function Card({ className, children, flush, variant = "default", ...rest }) {
443
489
  return /* @__PURE__ */ jsx(
444
490
  "div",
445
491
  {
446
492
  ...rest,
447
493
  className: cn(
448
- // Surface + border are theme tokens so the palette switcher (feedback
449
- // #307) can re-skin every card; a caller's own bg-*/border-* override
450
- // still wins via tailwind-merge.
451
- "bg-[var(--bg-surface)]",
452
- flush ? "border-y border-[var(--border)] md:rounded-lg md:border md:shadow-sm" : "rounded-lg border border-[var(--border)] shadow-sm",
494
+ variant === "inset" ? "rounded-md bg-[var(--bg-surface-2)] p-3" : cn(
495
+ // Surface + border are theme tokens so the palette switcher (feedback
496
+ // #307) can re-skin every card; a caller's own bg-*/border-* override
497
+ // still wins via tailwind-merge.
498
+ "bg-[var(--bg-surface)]",
499
+ flush ? "border-y border-[var(--border)] md:rounded-lg md:border md:shadow-sm" : "rounded-lg border border-[var(--border)] shadow-sm"
500
+ ),
453
501
  className
454
502
  ),
455
503
  children
@@ -528,7 +576,7 @@ function Spinner({ className, label, ...rest }) {
528
576
  " "
529
577
  ] }) });
530
578
  }
531
- function EmptyState({ title, hint, className, ...rest }) {
579
+ function EmptyState({ title, hint, icon, action, className, ...rest }) {
532
580
  return /* @__PURE__ */ jsxs(
533
581
  "div",
534
582
  {
@@ -538,8 +586,10 @@ function EmptyState({ title, hint, className, ...rest }) {
538
586
  className
539
587
  ),
540
588
  children: [
589
+ icon != null && /* @__PURE__ */ jsx("div", { "aria-hidden": true, className: "mb-3 text-[var(--text-placeholder)] [&_svg]:size-8", children: icon }),
541
590
  /* @__PURE__ */ jsx("div", { className: "font-medium text-[var(--text-secondary)]", children: title }),
542
- hint && /* @__PURE__ */ jsx("div", { className: "mt-1 text-xs", children: hint })
591
+ hint && /* @__PURE__ */ jsx("div", { className: "mt-1 text-xs", children: hint }),
592
+ action != null && /* @__PURE__ */ jsx("div", { className: "mt-4 flex flex-wrap items-center justify-center gap-2", children: action })
543
593
  ]
544
594
  }
545
595
  );
@@ -549,6 +599,10 @@ const DEFAULT_TABS_LABELS = {
549
599
  remove: (tab) => `Remove ${tab}`
550
600
  };
551
601
  const TABLIST_CLASSES = "flex gap-1 overflow-x-auto overflow-y-hidden border-b border-[var(--border)]";
602
+ const TABLIST_VERTICAL_CLASSES = "flex flex-col gap-0.5";
603
+ const TAB_VERTICAL_CLASSES = "flex w-full items-center rounded-md px-3 py-2 text-start text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--border-strong)]";
604
+ const TAB_VERTICAL_ACTIVE_CLASSES = "bg-[var(--bg-active)] text-[var(--text-primary)]";
605
+ const TAB_VERTICAL_INACTIVE_CLASSES = "text-[var(--text-muted)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]";
552
606
  const TABLIST_WRAP_CLASSES = "flex flex-wrap gap-1.5 md:flex-nowrap md:gap-1 md:overflow-x-auto md:overflow-y-hidden md:border-b md:border-[var(--border)]";
553
607
  const TAB_CLASSES = "whitespace-nowrap px-3 py-2 text-sm font-medium transition-colors border-b-2 -mb-px focus:outline-none focus:ring-2 focus:ring-[var(--border-strong)]";
554
608
  const TAB_ACTIVE_CLASSES = "border-[var(--text-primary)] text-[var(--text-primary)]";
@@ -574,10 +628,13 @@ function Tabs({
574
628
  addLabel,
575
629
  labels,
576
630
  busy = false,
631
+ orientation = "horizontal",
577
632
  "aria-label": ariaLabel,
578
633
  ...rest
579
634
  }) {
580
635
  const text = useKitLabels("tabs", DEFAULT_TABS_LABELS, labels);
636
+ const phone = useMediaQuery(PHONE_QUERY, false);
637
+ const vertical = orientation === "vertical" && !phone;
581
638
  const stripRef = useRef(null);
582
639
  const isRemovable = (tab) => onRemove !== void 0 && tab.removable !== false;
583
640
  const deletesOnKey = (tab) => isRemovable(tab) && (removeOn === "every" || tab.id === active);
@@ -610,7 +667,7 @@ function Tabs({
610
667
  if (!busy) requestRemove(tab.id);
611
668
  return;
612
669
  }
613
- const step = horizontalStep(e.key, e.currentTarget);
670
+ const step = vertical ? e.key === "ArrowDown" ? 1 : e.key === "ArrowUp" ? -1 : 0 : horizontalStep(e.key, e.currentTarget);
614
671
  if (step === 0 && e.key !== "Home" && e.key !== "End") return;
615
672
  const strip = e.currentTarget.closest('[role="tablist"]');
616
673
  if (!strip) return;
@@ -628,8 +685,9 @@ function Tabs({
628
685
  ref: stripRef,
629
686
  role: "tablist",
630
687
  "aria-label": ariaLabel ?? label,
688
+ "aria-orientation": vertical ? "vertical" : void 0,
631
689
  className: cn(
632
- wrap ? TABLIST_WRAP_CLASSES : TABLIST_CLASSES,
690
+ vertical ? TABLIST_VERTICAL_CLASSES : wrap ? TABLIST_WRAP_CLASSES : TABLIST_CLASSES,
633
691
  onAdd ? "min-w-0" : className
634
692
  ),
635
693
  children: tabs.map((tab) => {
@@ -656,21 +714,36 @@ function Tabs({
656
714
  id: isActive && panelId ? `${panelId}-tab` : void 0,
657
715
  "aria-keyshortcuts": deletesOnKey(tab) ? "Delete" : void 0,
658
716
  onKeyDown: (e) => onTabKeyDown(e, tab),
659
- className: cn(
717
+ className: vertical ? cn(
718
+ TAB_VERTICAL_CLASSES,
719
+ isActive ? TAB_VERTICAL_ACTIVE_CLASSES : TAB_VERTICAL_INACTIVE_CLASSES,
720
+ removable && "pe-8"
721
+ ) : cn(
660
722
  wrap ? TAB_WRAP_CLASSES : TAB_CLASSES,
661
723
  wrap ? isActive ? TAB_WRAP_ACTIVE_CLASSES : TAB_WRAP_INACTIVE_CLASSES : isActive ? TAB_ACTIVE_CLASSES : TAB_INACTIVE_CLASSES,
662
724
  removable && TAB_REMOVABLE_CLASSES
663
725
  )
664
726
  };
665
- const inner = /* @__PURE__ */ jsxs("span", { className: cn("inline-flex items-center gap-1.5", tab.empty && TAB_EMPTY_CLASSES), children: [
666
- tab.icon != null && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "inline-flex shrink-0 items-center gap-0.5", children: tab.icon }),
667
- tab.detail != null ? /* @__PURE__ */ jsxs("span", { className: "flex flex-col items-start text-start", children: [
668
- /* @__PURE__ */ jsx("span", { children: tab.label }),
669
- " ",
670
- /* @__PURE__ */ jsx("span", { className: "text-[11px] font-normal leading-tight text-[var(--text-muted)]", children: tab.detail })
671
- ] }) : tab.label,
672
- tab.badge != null ? tab.badge : null
673
- ] });
727
+ const inner = /* @__PURE__ */ jsxs(
728
+ "span",
729
+ {
730
+ className: cn(
731
+ // In a side nav the row is the full width, so the label takes it and a
732
+ // badge lands at the row's end, where a column of counts lines up.
733
+ vertical ? "flex min-w-0 flex-1 items-center gap-1.5" : "inline-flex items-center gap-1.5",
734
+ tab.empty && TAB_EMPTY_CLASSES
735
+ ),
736
+ children: [
737
+ tab.icon != null && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "inline-flex shrink-0 items-center gap-0.5", children: tab.icon }),
738
+ tab.detail != null ? /* @__PURE__ */ jsxs("span", { className: "flex flex-col items-start text-start", children: [
739
+ /* @__PURE__ */ jsx("span", { children: tab.label }),
740
+ " ",
741
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] font-normal leading-tight text-[var(--text-muted)]", children: tab.detail })
742
+ ] }) : tab.label,
743
+ tab.badge != null ? vertical ? /* @__PURE__ */ jsx("span", { className: "ms-auto flex shrink-0 items-center", children: tab.badge }) : tab.badge : null
744
+ ]
745
+ }
746
+ );
674
747
  const element = tab.href ? /* @__PURE__ */ jsx(
675
748
  "a",
676
749
  {
@@ -697,7 +770,7 @@ function Tabs({
697
770
  // The × cannot go INSIDE the tab: a button in a button is invalid HTML, and
698
771
  // a tab's children are presentational, so a reader would flatten it into the
699
772
  // tab's name. It sits beside the tab, over the gutter the tab reserves.
700
- /* @__PURE__ */ jsxs("div", { className: "relative flex shrink-0", children: [
773
+ /* @__PURE__ */ jsxs("div", { className: cn("relative flex", !vertical && "shrink-0"), children: [
701
774
  element,
702
775
  isActive && /* @__PURE__ */ jsx(
703
776
  IconButton,
@@ -724,6 +797,15 @@ function Tabs({
724
797
  }
725
798
  );
726
799
  if (!onAdd) return tablist;
800
+ if (vertical) {
801
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-0.5", className), children: [
802
+ tablist,
803
+ /* @__PURE__ */ jsxs("button", { type: "button", onClick: onAdd, disabled: busy, className: TAB_ADD_CLASSES, children: [
804
+ /* @__PURE__ */ jsx(Plus, { "aria-hidden": true }),
805
+ addLabel ?? text.add
806
+ ] })
807
+ ] });
808
+ }
727
809
  return /* @__PURE__ */ jsxs(
728
810
  "div",
729
811
  {