@eifi1/ui-kit 0.7.0 → 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 (233) 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 +5 -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 +5 -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 +19 -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 +56 -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 +5 -0
  60. package/dist/components/number-input.d.ts +5 -0
  61. package/dist/components/numpad-sheet.d.ts +5 -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/settings-fields.d.ts +2 -0
  78. package/dist/components/skeleton.d.ts +36 -0
  79. package/dist/components/skeleton.js +35 -0
  80. package/dist/components/skeleton.js.map +1 -0
  81. package/dist/components/stat-tile.d.ts +60 -6
  82. package/dist/components/stat-tile.js +60 -26
  83. package/dist/components/stat-tile.js.map +1 -1
  84. package/dist/components/table.d.ts +83 -0
  85. package/dist/components/table.js +166 -0
  86. package/dist/components/table.js.map +1 -0
  87. package/dist/components/time-input.d.ts +2 -0
  88. package/dist/components/toggle-legend.d.ts +3 -2
  89. package/dist/components/toggle-legend.js +1 -1
  90. package/dist/components/toggle-legend.js.map +1 -1
  91. package/dist/components/tooltip.d.ts +10 -4
  92. package/dist/components/tooltip.js +23 -6
  93. package/dist/components/tooltip.js.map +1 -1
  94. package/dist/components/tree-view.d.ts +129 -0
  95. package/dist/components/tree-view.js +376 -0
  96. package/dist/components/tree-view.js.map +1 -0
  97. package/dist/components/treemap.d.ts +9 -2
  98. package/dist/components/treemap.js +2 -1
  99. package/dist/components/treemap.js.map +1 -1
  100. package/dist/components/ui.d.ts +92 -14
  101. package/dist/components/ui.js +112 -30
  102. package/dist/components/ui.js.map +1 -1
  103. package/dist/components/use-table-state.d.ts +1 -1
  104. package/dist/{data-table-filters-noy0Abvi.d.ts → data-table-filters-CF1PXqjQ.d.ts} +73 -3
  105. package/dist/data-table.d.ts +2 -2
  106. package/dist/data-table.js.map +1 -1
  107. package/dist/hooks/use-copy-to-clipboard.d.ts +28 -0
  108. package/dist/hooks/use-copy-to-clipboard.js +81 -0
  109. package/dist/hooks/use-copy-to-clipboard.js.map +1 -0
  110. package/dist/hooks/use-debounce.d.ts +43 -0
  111. package/dist/hooks/use-debounce.js +56 -0
  112. package/dist/hooks/use-debounce.js.map +1 -0
  113. package/dist/hooks/use-file-drop.d.ts +72 -0
  114. package/dist/hooks/use-file-drop.js +57 -0
  115. package/dist/hooks/use-file-drop.js.map +1 -0
  116. package/dist/i18n/defaults.d.ts +5 -0
  117. package/dist/i18n/defaults.js +7 -1
  118. package/dist/i18n/defaults.js.map +1 -1
  119. package/dist/i18n/kit-labels.d.ts +17 -0
  120. package/dist/i18n/kit-labels.js +6 -2
  121. package/dist/i18n/kit-labels.js.map +1 -1
  122. package/dist/i18n/locales/de-CH-informal.d.ts +43 -0
  123. package/dist/i18n/locales/de-CH-informal.js +8 -0
  124. package/dist/i18n/locales/de-CH-informal.js.map +1 -0
  125. package/dist/i18n/locales/de-CH.d.ts +5 -0
  126. package/dist/i18n/locales/de-CH.js +1 -14
  127. package/dist/i18n/locales/de-CH.js.map +1 -1
  128. package/dist/i18n/locales/de-informal.d.ts +64 -0
  129. package/dist/i18n/locales/de-informal.js +37 -0
  130. package/dist/i18n/locales/de-informal.js.map +1 -0
  131. package/dist/i18n/locales/de.d.ts +5 -0
  132. package/dist/i18n/locales/de.js +25 -0
  133. package/dist/i18n/locales/de.js.map +1 -1
  134. package/dist/i18n/locales/es.d.ts +5 -0
  135. package/dist/i18n/locales/es.js +25 -0
  136. package/dist/i18n/locales/es.js.map +1 -1
  137. package/dist/i18n/locales/fr.d.ts +5 -0
  138. package/dist/i18n/locales/fr.js +25 -0
  139. package/dist/i18n/locales/fr.js.map +1 -1
  140. package/dist/i18n/locales/hu.d.ts +5 -0
  141. package/dist/i18n/locales/hu.js +25 -0
  142. package/dist/i18n/locales/hu.js.map +1 -1
  143. package/dist/i18n/locales/it.d.ts +5 -0
  144. package/dist/i18n/locales/it.js +25 -0
  145. package/dist/i18n/locales/it.js.map +1 -1
  146. package/dist/i18n/locales/zh.d.ts +5 -0
  147. package/dist/i18n/locales/zh.js +25 -0
  148. package/dist/i18n/locales/zh.js.map +1 -1
  149. package/dist/i18n/swiss.d.ts +13 -0
  150. package/dist/i18n/swiss.js +19 -0
  151. package/dist/i18n/swiss.js.map +1 -0
  152. package/dist/index.d.ts +26 -11
  153. package/dist/index.js +22 -0
  154. package/dist/index.js.map +1 -1
  155. package/dist/lib/dates.d.ts +37 -1
  156. package/dist/lib/dates.js +19 -0
  157. package/dist/lib/dates.js.map +1 -1
  158. package/dist/rhf/form.d.ts +2 -0
  159. package/dist/rhf.d.ts +2 -0
  160. package/dist/search/command-palette.d.ts +15 -1
  161. package/dist/search/command-palette.js +10 -2
  162. package/dist/search/command-palette.js.map +1 -1
  163. package/dist/shell/app-shell.js +2 -2
  164. package/dist/shell/app-shell.js.map +1 -1
  165. package/dist/wizard/stepper-nav.d.ts +35 -2
  166. package/dist/wizard/stepper-nav.js +54 -22
  167. package/dist/wizard/stepper-nav.js.map +1 -1
  168. package/dist/wizard/types.d.ts +107 -5
  169. package/dist/wizard/types.js +2 -1
  170. package/dist/wizard/types.js.map +1 -1
  171. package/dist/wizard/use-wizard.js +141 -21
  172. package/dist/wizard/use-wizard.js.map +1 -1
  173. package/dist/wizard/wizard-summary.d.ts +10 -2
  174. package/dist/wizard/wizard-summary.js +3 -1
  175. package/dist/wizard/wizard-summary.js.map +1 -1
  176. package/dist/wizard.d.ts +4 -1
  177. package/dist/wizard.js.map +1 -1
  178. package/package.json +1 -1
  179. package/src/components/alert-banner.tsx +211 -20
  180. package/src/components/button-group.tsx +75 -0
  181. package/src/components/chart-zoom.tsx +158 -20
  182. package/src/components/chip.tsx +160 -9
  183. package/src/components/confirm-dialog.tsx +242 -0
  184. package/src/components/copy-button.tsx +158 -0
  185. package/src/components/data-table-pagination.tsx +19 -6
  186. package/src/components/data-table-sort.ts +49 -10
  187. package/src/components/data-table.tsx +293 -24
  188. package/src/components/date-picker.tsx +262 -57
  189. package/src/components/description-list.tsx +174 -0
  190. package/src/components/disclosure.tsx +58 -5
  191. package/src/components/file-button.tsx +22 -2
  192. package/src/components/file-dropzone.tsx +198 -100
  193. package/src/components/floating-panel.tsx +341 -0
  194. package/src/components/full-bleed-dialog.tsx +48 -5
  195. package/src/components/modal.tsx +9 -2
  196. package/src/components/progress-bar.tsx +163 -0
  197. package/src/components/scroll-area.tsx +129 -0
  198. package/src/components/separator.tsx +37 -0
  199. package/src/components/series-chart-ticks.ts +135 -0
  200. package/src/components/series-chart.tsx +934 -67
  201. package/src/components/skeleton.tsx +65 -0
  202. package/src/components/stat-tile.tsx +160 -27
  203. package/src/components/table.tsx +263 -0
  204. package/src/components/toggle-legend.tsx +4 -3
  205. package/src/components/tooltip.tsx +40 -12
  206. package/src/components/tree-view.tsx +589 -0
  207. package/src/components/treemap.tsx +10 -2
  208. package/src/components/ui.tsx +268 -42
  209. package/src/data-table.ts +2 -0
  210. package/src/hooks/use-copy-to-clipboard.ts +122 -0
  211. package/src/hooks/use-debounce.ts +105 -0
  212. package/src/hooks/use-file-drop.ts +123 -0
  213. package/src/i18n/defaults.ts +6 -0
  214. package/src/i18n/kit-labels.tsx +19 -0
  215. package/src/i18n/locales/de-CH-informal.ts +10 -0
  216. package/src/i18n/locales/de-CH.ts +1 -16
  217. package/src/i18n/locales/de-informal.ts +61 -0
  218. package/src/i18n/locales/de.ts +25 -0
  219. package/src/i18n/locales/es.ts +25 -0
  220. package/src/i18n/locales/fr.ts +25 -0
  221. package/src/i18n/locales/hu.ts +25 -0
  222. package/src/i18n/locales/it.ts +25 -0
  223. package/src/i18n/locales/zh.ts +25 -0
  224. package/src/i18n/swiss.ts +25 -0
  225. package/src/index.ts +29 -0
  226. package/src/lib/dates.ts +48 -0
  227. package/src/search/command-palette.tsx +29 -3
  228. package/src/shell/app-shell.tsx +2 -2
  229. package/src/wizard/stepper-nav.tsx +102 -28
  230. package/src/wizard/types.ts +106 -4
  231. package/src/wizard/use-wizard.ts +211 -31
  232. package/src/wizard/wizard-summary.tsx +24 -12
  233. package/src/wizard.ts +3 -2
@@ -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
  }
@@ -165,7 +211,7 @@ function FloatingField({
165
211
  }
166
212
  function FieldHint({
167
213
  label,
168
- side = "left",
214
+ side = "start",
169
215
  className,
170
216
  "aria-label": ariaLabel,
171
217
  ...rest
@@ -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
  {