@eifi1/ui-kit 0.9.0 → 0.11.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 (243) hide show
  1. package/README.md +37 -23
  2. package/dist/components/alert-banner.d.ts +57 -5
  3. package/dist/components/alert-banner.js +63 -17
  4. package/dist/components/alert-banner.js.map +1 -1
  5. package/dist/components/amount-input.d.ts +6 -0
  6. package/dist/components/breadcrumbs.d.ts +60 -0
  7. package/dist/components/breadcrumbs.js +88 -0
  8. package/dist/components/breadcrumbs.js.map +1 -0
  9. package/dist/components/bulk-action-bar.d.ts +112 -0
  10. package/dist/components/bulk-action-bar.js +196 -0
  11. package/dist/components/bulk-action-bar.js.map +1 -0
  12. package/dist/components/button-group.d.ts +58 -3
  13. package/dist/components/button-group.js +56 -5
  14. package/dist/components/button-group.js.map +1 -1
  15. package/dist/components/calculator.d.ts +6 -0
  16. package/dist/components/calendar-heatmap.d.ts +124 -0
  17. package/dist/components/calendar-heatmap.js +295 -0
  18. package/dist/components/calendar-heatmap.js.map +1 -0
  19. package/dist/components/chip.d.ts +31 -5
  20. package/dist/components/chip.js +85 -5
  21. package/dist/components/chip.js.map +1 -1
  22. package/dist/components/choice-card.d.ts +34 -2
  23. package/dist/components/choice-card.js +49 -0
  24. package/dist/components/choice-card.js.map +1 -1
  25. package/dist/components/copy-button.d.ts +14 -4
  26. package/dist/components/copy-button.js +17 -4
  27. package/dist/components/copy-button.js.map +1 -1
  28. package/dist/components/data-table.js +2 -0
  29. package/dist/components/data-table.js.map +1 -1
  30. package/dist/components/date-picker.d.ts +22 -2
  31. package/dist/components/date-picker.js +11 -3
  32. package/dist/components/date-picker.js.map +1 -1
  33. package/dist/components/description-list.d.ts +43 -6
  34. package/dist/components/description-list.js +91 -11
  35. package/dist/components/description-list.js.map +1 -1
  36. package/dist/components/dialog-frame.d.ts +7 -0
  37. package/dist/components/dialog-frame.js.map +1 -1
  38. package/dist/components/disclosure.d.ts +57 -2
  39. package/dist/components/disclosure.js +24 -12
  40. package/dist/components/disclosure.js.map +1 -1
  41. package/dist/components/field.d.ts +43 -0
  42. package/dist/components/field.js +52 -0
  43. package/dist/components/field.js.map +1 -0
  44. package/dist/components/file-button.d.ts +1 -0
  45. package/dist/components/file-dropzone.d.ts +4 -4
  46. package/dist/components/file-dropzone.js +2 -1
  47. package/dist/components/file-dropzone.js.map +1 -1
  48. package/dist/components/floating-panel.d.ts +147 -4
  49. package/dist/components/floating-panel.js +188 -28
  50. package/dist/components/floating-panel.js.map +1 -1
  51. package/dist/components/full-bleed-dialog.d.ts +23 -4
  52. package/dist/components/full-bleed-dialog.js +11 -2
  53. package/dist/components/full-bleed-dialog.js.map +1 -1
  54. package/dist/components/list.d.ts +187 -0
  55. package/dist/components/list.js +221 -0
  56. package/dist/components/list.js.map +1 -0
  57. package/dist/components/menu-item.d.ts +109 -0
  58. package/dist/components/menu-item.js +88 -0
  59. package/dist/components/menu-item.js.map +1 -0
  60. package/dist/components/mini-calendar.d.ts +70 -5
  61. package/dist/components/mini-calendar.js +166 -62
  62. package/dist/components/mini-calendar.js.map +1 -1
  63. package/dist/components/modal.d.ts +23 -1
  64. package/dist/components/modal.js +36 -10
  65. package/dist/components/modal.js.map +1 -1
  66. package/dist/components/nav-pills.d.ts +66 -0
  67. package/dist/components/nav-pills.js +65 -0
  68. package/dist/components/nav-pills.js.map +1 -0
  69. package/dist/components/number-field.d.ts +6 -0
  70. package/dist/components/number-input.d.ts +6 -0
  71. package/dist/components/numpad-sheet.d.ts +6 -0
  72. package/dist/components/page-contents.js +1 -2
  73. package/dist/components/page-contents.js.map +1 -1
  74. package/dist/components/page-header.d.ts +37 -0
  75. package/dist/components/page-header.js +36 -0
  76. package/dist/components/page-header.js.map +1 -0
  77. package/dist/components/progress-bar.d.ts +31 -2
  78. package/dist/components/progress-bar.js +73 -7
  79. package/dist/components/progress-bar.js.map +1 -1
  80. package/dist/components/series-chart-labels.d.ts +3 -0
  81. package/dist/components/series-chart-labels.js +2 -1
  82. package/dist/components/series-chart-labels.js.map +1 -1
  83. package/dist/components/series-chart.d.ts +32 -0
  84. package/dist/components/series-chart.js +141 -4
  85. package/dist/components/series-chart.js.map +1 -1
  86. package/dist/components/settings-fields.d.ts +1 -0
  87. package/dist/components/status-dot.d.ts +53 -0
  88. package/dist/components/status-dot.js +57 -0
  89. package/dist/components/status-dot.js.map +1 -0
  90. package/dist/components/table.d.ts +78 -13
  91. package/dist/components/table.js +84 -11
  92. package/dist/components/table.js.map +1 -1
  93. package/dist/components/text.d.ts +70 -0
  94. package/dist/components/text.js +24 -0
  95. package/dist/components/text.js.map +1 -0
  96. package/dist/components/time-input.d.ts +1 -0
  97. package/dist/components/toast.d.ts +167 -0
  98. package/dist/components/toast.js +226 -0
  99. package/dist/components/toast.js.map +1 -0
  100. package/dist/components/toggle-group.d.ts +44 -1
  101. package/dist/components/toggle-group.js +73 -4
  102. package/dist/components/toggle-group.js.map +1 -1
  103. package/dist/components/tooltip.d.ts +46 -15
  104. package/dist/components/tooltip.js +87 -47
  105. package/dist/components/tooltip.js.map +1 -1
  106. package/dist/components/ui.d.ts +158 -7
  107. package/dist/components/ui.js +191 -10
  108. package/dist/components/ui.js.map +1 -1
  109. package/dist/components/user-avatar.d.ts +25 -3
  110. package/dist/components/user-avatar.js +32 -3
  111. package/dist/components/user-avatar.js.map +1 -1
  112. package/dist/hooks/use-close-transition.d.ts +7 -1
  113. package/dist/hooks/use-close-transition.js +5 -2
  114. package/dist/hooks/use-close-transition.js.map +1 -1
  115. package/dist/hooks/use-copy-to-clipboard.js +1 -1
  116. package/dist/hooks/use-copy-to-clipboard.js.map +1 -1
  117. package/dist/hooks/use-file-drop.d.ts +1 -0
  118. package/dist/i18n/defaults.d.ts +6 -0
  119. package/dist/i18n/defaults.js +11 -1
  120. package/dist/i18n/defaults.js.map +1 -1
  121. package/dist/i18n/kit-labels.d.ts +11 -0
  122. package/dist/i18n/kit-labels.js.map +1 -1
  123. package/dist/i18n/locales/de-CH-informal.d.ts +6 -0
  124. package/dist/i18n/locales/de-CH.d.ts +6 -0
  125. package/dist/i18n/locales/de-informal.d.ts +6 -0
  126. package/dist/i18n/locales/de.d.ts +6 -0
  127. package/dist/i18n/locales/de.js +30 -2
  128. package/dist/i18n/locales/de.js.map +1 -1
  129. package/dist/i18n/locales/es.d.ts +6 -0
  130. package/dist/i18n/locales/es.js +30 -2
  131. package/dist/i18n/locales/es.js.map +1 -1
  132. package/dist/i18n/locales/fr.d.ts +6 -0
  133. package/dist/i18n/locales/fr.js +30 -2
  134. package/dist/i18n/locales/fr.js.map +1 -1
  135. package/dist/i18n/locales/hu.d.ts +6 -0
  136. package/dist/i18n/locales/hu.js +30 -2
  137. package/dist/i18n/locales/hu.js.map +1 -1
  138. package/dist/i18n/locales/it.d.ts +6 -0
  139. package/dist/i18n/locales/it.js +30 -2
  140. package/dist/i18n/locales/it.js.map +1 -1
  141. package/dist/i18n/locales/zh.d.ts +6 -0
  142. package/dist/i18n/locales/zh.js +30 -2
  143. package/dist/i18n/locales/zh.js.map +1 -1
  144. package/dist/index.d.ts +27 -15
  145. package/dist/index.js +15 -0
  146. package/dist/index.js.map +1 -1
  147. package/dist/lib/clipping.d.ts +25 -0
  148. package/dist/lib/clipping.js +17 -0
  149. package/dist/lib/clipping.js.map +1 -0
  150. package/dist/lib/dates.d.ts +17 -1
  151. package/dist/lib/dates.js +19 -0
  152. package/dist/lib/dates.js.map +1 -1
  153. package/dist/rhf/form.d.ts +1 -0
  154. package/dist/rhf/use-rhf-wizard-step.d.ts +22 -0
  155. package/dist/rhf/use-rhf-wizard-step.js +38 -0
  156. package/dist/rhf/use-rhf-wizard-step.js.map +1 -0
  157. package/dist/rhf.d.ts +2 -0
  158. package/dist/rhf.js +1 -0
  159. package/dist/rhf.js.map +1 -1
  160. package/dist/search/command-palette.d.ts +19 -2
  161. package/dist/search/command-palette.js +16 -5
  162. package/dist/search/command-palette.js.map +1 -1
  163. package/dist/search/global-search.d.ts +26 -1
  164. package/dist/search/global-search.js +29 -13
  165. package/dist/search/global-search.js.map +1 -1
  166. package/dist/search.d.ts +1 -1
  167. package/dist/shell/topbar-action-menu.d.ts +73 -3
  168. package/dist/shell/topbar-action-menu.js +100 -27
  169. package/dist/shell/topbar-action-menu.js.map +1 -1
  170. package/dist/shell.d.ts +2 -1
  171. package/dist/wizard/stepper-nav.d.ts +1 -0
  172. package/dist/wizard/stepper-nav.js +1 -1
  173. package/dist/wizard/stepper-nav.js.map +1 -1
  174. package/dist/wizard/types.d.ts +2 -2
  175. package/dist/wizard/types.js.map +1 -1
  176. package/dist/wizard/use-wizard.js +1 -1
  177. package/dist/wizard/use-wizard.js.map +1 -1
  178. package/dist/wizard/wizard-context.d.ts +7 -1
  179. package/dist/wizard/wizard-context.js +4 -0
  180. package/dist/wizard/wizard-context.js.map +1 -1
  181. package/dist/wizard/wizard-summary.js +3 -3
  182. package/dist/wizard/wizard-summary.js.map +1 -1
  183. package/dist/wizard.d.ts +2 -1
  184. package/package.json +16 -12
  185. package/src/components/alert-banner.tsx +149 -21
  186. package/src/components/breadcrumbs.tsx +168 -0
  187. package/src/components/bulk-action-bar.tsx +362 -0
  188. package/src/components/button-group.tsx +124 -4
  189. package/src/components/calendar-heatmap.tsx +504 -0
  190. package/src/components/chip.tsx +128 -7
  191. package/src/components/choice-card.tsx +97 -1
  192. package/src/components/copy-button.tsx +29 -7
  193. package/src/components/data-table.tsx +7 -0
  194. package/src/components/date-picker.tsx +49 -1
  195. package/src/components/description-list.tsx +168 -15
  196. package/src/components/dialog-frame.tsx +7 -0
  197. package/src/components/disclosure.tsx +93 -21
  198. package/src/components/field.tsx +137 -0
  199. package/src/components/file-dropzone.tsx +9 -10
  200. package/src/components/floating-panel.tsx +366 -15
  201. package/src/components/full-bleed-dialog.tsx +42 -5
  202. package/src/components/list.tsx +443 -0
  203. package/src/components/menu-item.tsx +235 -0
  204. package/src/components/mini-calendar.tsx +245 -54
  205. package/src/components/modal.tsx +80 -17
  206. package/src/components/nav-pills.tsx +145 -0
  207. package/src/components/page-contents.tsx +4 -4
  208. package/src/components/page-header.tsx +68 -0
  209. package/src/components/progress-bar.tsx +129 -11
  210. package/src/components/series-chart-labels.ts +4 -0
  211. package/src/components/series-chart.tsx +274 -3
  212. package/src/components/status-dot.tsx +107 -0
  213. package/src/components/table.tsx +176 -17
  214. package/src/components/text.tsx +97 -0
  215. package/src/components/toast.tsx +441 -0
  216. package/src/components/toggle-group.tsx +128 -4
  217. package/src/components/tooltip.tsx +200 -100
  218. package/src/components/ui.tsx +381 -14
  219. package/src/components/user-avatar.tsx +57 -3
  220. package/src/hooks/use-close-transition.ts +14 -5
  221. package/src/hooks/use-copy-to-clipboard.ts +1 -1
  222. package/src/i18n/defaults.ts +10 -0
  223. package/src/i18n/kit-labels.tsx +10 -0
  224. package/src/i18n/locales/de.ts +29 -0
  225. package/src/i18n/locales/es.ts +29 -0
  226. package/src/i18n/locales/fr.ts +29 -0
  227. package/src/i18n/locales/hu.ts +29 -0
  228. package/src/i18n/locales/it.ts +29 -0
  229. package/src/i18n/locales/zh.ts +29 -0
  230. package/src/index.ts +31 -0
  231. package/src/lib/clipping.ts +34 -0
  232. package/src/lib/dates.ts +37 -0
  233. package/src/rhf/use-rhf-wizard-step.ts +113 -0
  234. package/src/rhf.ts +2 -0
  235. package/src/search/command-palette.tsx +42 -5
  236. package/src/search/global-search.tsx +59 -18
  237. package/src/shell/topbar-action-menu.tsx +239 -49
  238. package/src/wizard/stepper-nav.tsx +2 -1
  239. package/src/wizard/types.ts +2 -2
  240. package/src/wizard/use-wizard.ts +3 -3
  241. package/src/wizard/wizard-context.tsx +9 -0
  242. package/src/wizard/wizard-summary.tsx +14 -10
  243. package/tokens.css +97 -0
@@ -1,10 +1,28 @@
1
1
  import * as react from 'react';
2
- import { ButtonHTMLAttributes, Ref, SelectHTMLAttributes, ReactNode, InputHTMLAttributes, ComponentPropsWithoutRef, TextareaHTMLAttributes } from 'react';
2
+ import { ButtonHTMLAttributes, Ref, ReactNode, SelectHTMLAttributes, InputHTMLAttributes, ComponentPropsWithoutRef, TextareaHTMLAttributes } from 'react';
3
3
  import { TooltipSide } from './tooltip.js';
4
4
  import '../hooks/use-anchored-rect.js';
5
+ import '../lib/clipping.js';
5
6
 
6
7
  type ButtonVariant = "primary" | "secondary" | "ghost" | "danger" | "brand" | "link";
7
8
  type ButtonSize = "sm" | "md";
9
+ /**
10
+ * The text colour of a `link` or `ghost` button, over the variant's own.
11
+ *
12
+ * `muted` is the QUIET link keksdose hand-rolls in six places (tours-page:321's "Mark
13
+ * undone", transaction-fields:176/215's "Add line" / "Fill total",
14
+ * invoice-lines-table:376, invoice-review:464): secondary text that darkens to the
15
+ * body colour under the pointer, for an action that must be findable but not compete
16
+ * with the brand-coloured one beside it. `danger` is the same quiet look turning
17
+ * `--danger` on hover — transaction-editor:401's "Remove split", which is a text
18
+ * action that destroys something.
19
+ *
20
+ * A `tone` rather than a `link-muted` variant, because it is the axis IconButton
21
+ * already has (`tone="muted"`, `tone="danger"`, same quiet-until-hover meaning), and
22
+ * because the same two looks are wanted on `ghost`. The filled and bordered variants
23
+ * carry their meaning in the box, not the text, so a tone on them does nothing.
24
+ */
25
+ type ButtonTone = "default" | "muted" | "danger";
8
26
  /** The second argument of {@link buttonClasses} in its options form. */
9
27
  interface ButtonClassesOptions {
10
28
  /** See {@link ButtonProps.size}. */
@@ -44,10 +62,43 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
44
62
  * ordinary prop, so it rides `...rest` onto the element with no `forwardRef` —
45
63
  * declared here only because `ButtonHTMLAttributes` does not carry it. */
46
64
  ref?: Ref<HTMLButtonElement>;
65
+ /** Text colour for `link` and `ghost`: `muted` (quiet, body colour on hover) or
66
+ * `danger` (quiet, `--danger` on hover). Ignored by the other variants. See
67
+ * {@link ButtonTone}. */
68
+ tone?: ButtonTone;
69
+ /**
70
+ * Make it a toggle button, as {@link IconButton}'s `pressed` does. `true` sets
71
+ * `aria-pressed="true"` and draws the "on" look — on a `link`, the brand colour and a
72
+ * medium weight; on the boxed variants, the quiet brand fill IconButton uses — and
73
+ * `false` sets `aria-pressed="false"` with the ordinary look. Left out, no
74
+ * `aria-pressed` (or the caller's own). For lenkbank's "All speeds" text toggle
75
+ * (gear/hysteresis-charts.tsx:482), which is a hand-rolled `<button aria-pressed>`
76
+ * swapping two class strings; with `variant="link" tone="muted"` it is this.
77
+ */
78
+ pressed?: boolean;
79
+ /**
80
+ * Why the action is not available — the button's half of DangerConfirm's
81
+ * `lockedReason` (kastlan handover-detail-page.tsx:197 locks a signed handover).
82
+ *
83
+ * A disabled button that cannot say why is a dead end: the native `disabled` takes it
84
+ * out of the tab order, so a keyboard user never lands on it, and a pointer gets a
85
+ * `not-allowed` cursor and nothing else. With a reason the button is `aria-disabled`
86
+ * instead — still focusable, still hoverable — clicks (and the Enter/Space and form
87
+ * submission they stand for) do nothing, and the reason is shown in the kit
88
+ * {@link Tooltip} and attached with `aria-describedby`, so a screen reader hears it
89
+ * on focus. It wins over `disabled`: passing both keeps the button reachable, which is
90
+ * the point of giving a reason.
91
+ *
92
+ * The description is a `hidden` copy of the reason rather than the bubble itself: the
93
+ * bubble exists only while hovered or focused once it is portalled, and a description
94
+ * that comes and goes is read inconsistently. The bubble is kept visual, the way
95
+ * DangerConfirm keeps its own.
96
+ */
97
+ disabledReason?: ReactNode;
47
98
  }
48
- declare function Button({ variant, size, stretch, className, ...rest }: ButtonProps): react.JSX.Element;
49
- type IconButtonSize = "lg" | "md" | "sm" | "xs" | "2xs";
50
- type IconButtonTone = "default" | "muted" | "danger" | "warning" | "info";
99
+ declare function Button({ variant, size, stretch, tone, pressed, disabledReason, className, onClick, ...rest }: ButtonProps): react.JSX.Element;
100
+ type IconButtonSize = "xl" | "lg" | "md" | "sm" | "xs" | "2xs";
101
+ type IconButtonTone = "default" | "muted" | "danger" | "warning" | "info" | "success" | "custom";
51
102
  interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
52
103
  /** Any {@link ButtonVariant}, or `overlay` — a round translucent disc for use over
53
104
  * an image (see `ICON_BUTTON_OVERLAY`). */
@@ -57,11 +108,31 @@ interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
57
108
  * xs = 28px with a 16px icon (an action in a list row), 2xs = 24px with a 14px
58
109
  * icon (an action on a chip or a tab). */
59
110
  size?: IconButtonSize;
111
+ /**
112
+ * Fill the height of the flex row it stands in, keeping its width — the delete at
113
+ * the end of keksdose's split line (transaction-fields:162), level with the labelled
114
+ * fields beside it rather than floating at half their height. {@link Button}'s
115
+ * `stretch`; no effect outside a flex row. With a `label`, the tooltip's wrapper is
116
+ * what stretches.
117
+ */
118
+ stretch?: boolean;
119
+ /** `round`: a circle instead of the rounded square — a compact status chip in a top
120
+ * bar (keksdose's sync indicator). `overlay` is always round. */
121
+ shape?: "square" | "round";
122
+ /**
123
+ * The colour of `tone="custom"`, as any CSS colour — a token (`var(--warning)`),
124
+ * ideally. Sets `--icon-button-tone` on the button, so passing a different one per
125
+ * state re-colours the glyph, its hover fill and its focus ring together. Passing it
126
+ * implies `tone="custom"`. Or leave it out and set the variable yourself from a class
127
+ * (`className="[--icon-button-tone:var(--success)]"`).
128
+ */
129
+ toneColor?: string;
60
130
  /** Glyph colour over the variant. `muted`: placeholder grey, full text colour on
61
131
  * hover. `danger`: the same grey at rest, `--danger` on hover and focus — for a
62
132
  * remove/delete that repeats down a list. `warning`: amber at rest — a flag that
63
133
  * wants attention. `info`: sky at rest — a notice-worthy but harmless action
64
- * (keksdose's reconcile). Default: the variant's own colours. See `quiet` for
134
+ * (keksdose's reconcile). `success`: green at rest — a good state worth seeing.
135
+ * `custom`: the colour of `toneColor` (see there). Default: the variant's own colours. See `quiet` for
65
136
  * turning a coloured tone's resting look the other way. */
66
137
  tone?: IconButtonTone;
67
138
  /**
@@ -116,6 +187,53 @@ interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
116
187
  * The pair look exactly like the nested version, click exactly like it, and
117
188
  * are two tab stops a screen reader can tell apart. */
118
189
  stopPropagation?: boolean;
190
+ /**
191
+ * The button's name, said once: it becomes the `aria-label` AND the text of a kit
192
+ * {@link Tooltip} round the button.
193
+ *
194
+ * Every app writes the pair by hand. lenkbank's nine icon actions all carry
195
+ * `aria-label={t(x)} title={t(x)}` (shared/lib/table-columns.tsx:103,
196
+ * projects-page.tsx:83, setpoint/segment-list.tsx:198/207, profile-bar.tsx:128/142,
197
+ * shortcut-dialog.tsx:815/857/1007) — and `title` is the browser's own tooltip,
198
+ * which shows late, never on focus and never on touch, and looks like no other
199
+ * label in the kit. kastlan's RowAction (shared/components/data-table/row-action.tsx)
200
+ * and keksdose's icon wrappers each exist to put a Tooltip round an IconButton.
201
+ *
202
+ * A caller's own `aria-label` still wins, for the rare name that should be longer
203
+ * than the bubble. The bubble is visual only: it would otherwise describe the button
204
+ * with its own name, and a screen reader would read the same word twice. Tooltip's
205
+ * default placement applies — in place, or portalled inside a scroll container —
206
+ * unless `tooltipPortal` says otherwise.
207
+ */
208
+ label?: string;
209
+ /** Show `label` as a tooltip. Default `true`; `false` keeps `label` as the
210
+ * accessible name only — for a button whose glyph is universally read (a close ✕ in
211
+ * a dialog header) or that already sits under a tooltip of its own. */
212
+ tooltip?: boolean;
213
+ /** Where the `label` tooltip opens. See {@link Tooltip}'s `side`. */
214
+ tooltipSide?: TooltipSide;
215
+ /** Passed to the `label` tooltip's `portal`. Left out, Tooltip decides (see there). */
216
+ tooltipPortal?: boolean;
217
+ /**
218
+ * Why the action is not available — {@link Button}'s `disabledReason`, on the icon
219
+ * button: keksdose's accounts page cannot hide an account with a balance or delete
220
+ * one with bookings (accounts-page:879/913 — "hide requires zero", "delete blocked"),
221
+ * nor its budgets page delete the only budget (budgets-page:330), and each wrapped a
222
+ * natively disabled IconButton in a Tooltip that swaps its label for the reason — a
223
+ * bubble a keyboard never opens, since `disabled` leaves the tab order, and a reason
224
+ * a screen reader never hears.
225
+ *
226
+ * The same contract as Button's: `aria-disabled` instead of `disabled`, so it stays
227
+ * focusable and hoverable; clicks (and the Enter/Space they stand for) are swallowed
228
+ * — `stopPropagation` still applies, so a locked action in a clickable row does not
229
+ * open the row either; the reason shows in the kit {@link Tooltip} and is attached
230
+ * through `aria-describedby` as a `hidden` copy. The NAME stays `aria-label` /
231
+ * `label` — "Delete", described by "The active budget cannot be deleted" — and the
232
+ * bubble shows the reason in place of the label, since the glyph already says what
233
+ * the button is and the reason is the news. Shown even with `tooltip={false}`: a
234
+ * reason nobody can see is not one. Wins over `disabled`, as on Button.
235
+ */
236
+ disabledReason?: ReactNode;
119
237
  }
120
238
  declare const IconButton: react.ForwardRefExoticComponent<IconButtonProps & react.RefAttributes<HTMLButtonElement>>;
121
239
  declare const FIELD_BASE: string;
@@ -441,8 +559,41 @@ interface EmptyStateProps extends Omit<ComponentPropsWithoutRef<"div">, "childre
441
559
  * wrap and centre as one row), kastlan's "Create a rule". Buttons or links the
442
560
  * caller renders; the box only places them. */
443
561
  action?: ReactNode;
562
+ /**
563
+ * `box` (default): the dashed, padded card that stands in for a whole section.
564
+ * `inline`: one quiet line with no box — for the empty state INSIDE something that
565
+ * already has its own frame, where a dashed card would be a box in a box. keksdose
566
+ * writes that line by hand five times as a muted `<p className="py-6 text-center">`:
567
+ * the notification inbox (notification-inbox:140), a support thread
568
+ * (support-thread:144), the assistant (assistant-page:321), the admin support panel
569
+ * (support-panel:265) and the funding dialog (funding-dialog:89). The icon shrinks
570
+ * to the text's size and sits before the title, the hint follows on the same line,
571
+ * and the row wraps only when it must. Centred like the box; `className="justify-start"`
572
+ * for a list that reads from the start edge (funding-dialog).
573
+ */
574
+ variant?: "box" | "inline";
575
+ /**
576
+ * Colour the icon and the title: `danger` for an error standing where the content
577
+ * should be — kastlan's ErrorState (shared/components/feedback/error-state.tsx) and
578
+ * its error boundary's fallback (error-boundary.tsx:16), both a red triangle over a
579
+ * message — and `success` for an empty state that is GOOD news, kastlan's "No defects
580
+ * recorded for this room" (handover defects-step.tsx:142, a green check). Left out,
581
+ * both stay muted. The hint stays muted either way: it is the explanation, not the
582
+ * verdict.
583
+ */
584
+ tone?: "danger" | "success";
585
+ /**
586
+ * `inline` only (0.11.0). `sm`: 12px, start-aligned and tight (`py-1`) — the empty
587
+ * line INSIDE a detail panel, under a panel heading and among `text-xs` rows, where
588
+ * the default's 14px centred line with `py-4` read as a section of its own.
589
+ * keksdose's holdings panel (holdings-panel:563), asset-loan panel
590
+ * (asset-loan-panel:108) and loan-payment panel (loan-payment-panel:191) each wrote
591
+ * `className="justify-start py-1"` over it and still got body-size text. Default `md`,
592
+ * the look it had. Ignored by the box.
593
+ */
594
+ size?: "sm" | "md";
444
595
  }
445
- declare function EmptyState({ title, hint, icon, action, headingAs, className, ...rest }: EmptyStateProps): react.JSX.Element;
596
+ declare function EmptyState({ title, hint, icon, action, headingAs, variant, tone, size, className, ...rest }: EmptyStateProps): react.JSX.Element;
446
597
  /** One tab of a {@link Tabs} strip. */
447
598
  interface TabItem<T extends string> {
448
599
  id: T;
@@ -627,4 +778,4 @@ interface TabsProps<T extends string> extends Omit<ComponentPropsWithoutRef<"div
627
778
  }
628
779
  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;
629
780
 
630
- 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, type IconButtonTone, 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 };
781
+ export { Button, type ButtonClassesOptions, type ButtonProps, type ButtonSize, type ButtonTone, 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, type IconButtonTone, 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 };
@@ -27,15 +27,48 @@ const buttonVariantClasses = {
27
27
  // ring hugging the word instead of drawing a pill round it.
28
28
  link: "rounded-sm p-0 bg-transparent text-[var(--brand)] underline-offset-4 hover:underline focus:ring-[var(--brand)]"
29
29
  };
30
+ const BUTTON_TONES = {
31
+ muted: "text-[var(--text-secondary)] hover:text-[var(--text-primary)] disabled:hover:text-[var(--text-secondary)]",
32
+ danger: "text-[var(--text-secondary)] hover:text-[var(--danger)] focus:ring-[var(--danger-border)] disabled:hover:text-[var(--text-secondary)]"
33
+ };
34
+ const TONED_VARIANTS = /* @__PURE__ */ new Set(["link", "ghost"]);
35
+ const BUTTON_LINK_PRESSED = "font-medium text-[var(--brand)] hover:text-[var(--brand)] disabled:hover:text-[var(--brand)]";
30
36
  function buttonClasses(variant = "primary", classNameOrOptions) {
31
37
  const { size = "md", className } = typeof classNameOrOptions === "object" ? classNameOrOptions : { className: classNameOrOptions };
32
38
  return cn(BUTTON_BASE, BUTTON_SIZES[size], buttonVariantClasses[variant], className);
33
39
  }
34
- function Button({ variant = "primary", size = "md", stretch, className, ...rest }) {
35
- return /* @__PURE__ */ jsx(
40
+ function hasContent(node) {
41
+ return node !== void 0 && node !== null && node !== false && node !== "";
42
+ }
43
+ function Button({
44
+ variant = "primary",
45
+ size = "md",
46
+ stretch,
47
+ tone = "default",
48
+ pressed,
49
+ disabledReason,
50
+ className,
51
+ onClick,
52
+ ...rest
53
+ }) {
54
+ const reasonId = useId();
55
+ const locked = hasContent(disabledReason);
56
+ const ownDescribedBy = rest["aria-describedby"];
57
+ const button = /* @__PURE__ */ jsx(
36
58
  "button",
37
59
  {
38
60
  ...rest,
61
+ disabled: locked ? void 0 : rest.disabled,
62
+ "aria-disabled": locked || rest["aria-disabled"],
63
+ "aria-describedby": locked ? ownDescribedBy ? `${ownDescribedBy} ${reasonId}` : reasonId : ownDescribedBy,
64
+ "aria-pressed": pressed ?? rest["aria-pressed"],
65
+ onClick: (e) => {
66
+ if (locked) {
67
+ e.preventDefault();
68
+ return;
69
+ }
70
+ onClick?.(e);
71
+ },
39
72
  className: cn(
40
73
  BUTTON_BASE,
41
74
  BUTTON_SIZES[size],
@@ -44,13 +77,31 @@ function Button({ variant = "primary", size = "md", stretch, className, ...rest
44
77
  // align-items). No effect outside a flex row / when it's already the tallest.
45
78
  stretch && "self-stretch",
46
79
  buttonVariantClasses[variant],
80
+ tone !== "default" && TONED_VARIANTS.has(variant) && BUTTON_TONES[tone],
81
+ pressed && (variant === "link" ? BUTTON_LINK_PRESSED : ICON_BUTTON_PRESSED),
82
+ // The disabled look for the focusable kind of disabled.
83
+ locked && "cursor-not-allowed opacity-50",
47
84
  className
48
85
  )
49
86
  }
50
87
  );
88
+ if (!locked) return button;
89
+ return (
90
+ // The button in a FRAGMENT so Tooltip does not clone its bubble into the
91
+ // description as well — the hidden copy below already describes it, and the same
92
+ // sentence twice would be read on every focus. `stretch` moves to the wrapper,
93
+ // which is now the flex item.
94
+ /* @__PURE__ */ jsx(Tooltip, { label: disabledReason, className: stretch ? "self-stretch" : void 0, children: /* @__PURE__ */ jsxs(Fragment, { children: [
95
+ button,
96
+ /* @__PURE__ */ jsx("span", { id: reasonId, hidden: true, children: disabledReason })
97
+ ] }) })
98
+ );
51
99
  }
52
100
  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";
53
101
  const ICON_BUTTON_SIZES = {
102
+ // 48px with a 24px glyph: the one primary action of a phone screen or sheet, a step
103
+ // past the 44px minimum rather than on it.
104
+ xl: "size-12 [&_svg]:size-6",
54
105
  // The 44px touch target (WCAG 2.5.5's size) with the same 20px glyph — keksdose's
55
106
  // bulk-action bars write `size-11` by hand over an `md` button to get it on phones.
56
107
  lg: "size-11 [&_svg]:size-5",
@@ -59,6 +110,14 @@ const ICON_BUTTON_SIZES = {
59
110
  xs: "size-7 rounded [&_svg]:size-4",
60
111
  "2xs": "size-6 rounded [&_svg]:size-3.5"
61
112
  };
113
+ const ICON_BUTTON_STRETCH_SIZES = {
114
+ xl: "w-12 min-h-12 self-stretch [&_svg]:size-6",
115
+ lg: "w-11 min-h-11 self-stretch [&_svg]:size-5",
116
+ md: "w-9 min-h-9 self-stretch [&_svg]:size-5",
117
+ sm: "w-8 min-h-8 self-stretch [&_svg]:size-5",
118
+ xs: "w-7 min-h-7 self-stretch rounded [&_svg]:size-4",
119
+ "2xs": "w-6 min-h-6 self-stretch rounded [&_svg]:size-3.5"
120
+ };
62
121
  const ICON_BUTTON_TONES = {
63
122
  danger: {
64
123
  quiet: "text-[var(--text-placeholder)] hover:bg-[var(--danger-bg)] hover:text-[var(--danger)] focus:ring-[var(--danger-border)]",
@@ -71,10 +130,27 @@ const ICON_BUTTON_TONES = {
71
130
  info: {
72
131
  quiet: "text-[var(--text-placeholder)] hover:bg-[var(--info-bg)] hover:text-[var(--info)] focus:ring-[var(--info-border)]",
73
132
  toned: "text-[var(--info)] hover:bg-[var(--info-bg)] focus:ring-[var(--info-border)]"
133
+ },
134
+ success: {
135
+ quiet: "text-[var(--text-placeholder)] hover:bg-[var(--success-bg)] hover:text-[var(--success)] focus:ring-[var(--success-border)]",
136
+ toned: "text-[var(--success)] hover:bg-[var(--success-bg)] focus:ring-[var(--success-border)]"
137
+ },
138
+ // The hover fill and the ring are MIXED from the one colour, since a custom tone
139
+ // brings no `-bg` / `-border` pair of its own. The fallback is the body colour, so a
140
+ // `custom` tone with no colour set is the default look rather than invisible.
141
+ custom: {
142
+ quiet: "text-[var(--text-placeholder)] hover:bg-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_12%,transparent)] hover:text-[var(--icon-button-tone,var(--text-primary))] focus:ring-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_40%,transparent)]",
143
+ toned: "text-[var(--icon-button-tone,var(--text-primary))] hover:bg-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_12%,transparent)] focus:ring-[color-mix(in_srgb,var(--icon-button-tone,var(--text-primary))_40%,transparent)]"
74
144
  }
75
145
  };
76
146
  const ICON_BUTTON_MUTED = "text-[var(--text-placeholder)] hover:bg-[var(--bg-surface-2)] hover:text-[var(--text-primary)]";
77
- const QUIET_BY_DEFAULT = { danger: true, warning: false, info: false };
147
+ const QUIET_BY_DEFAULT = {
148
+ danger: true,
149
+ warning: false,
150
+ info: false,
151
+ success: false,
152
+ custom: false
153
+ };
78
154
  function iconButtonToneClass(tone, quiet) {
79
155
  if (tone === "default") return "";
80
156
  if (tone === "muted") return cn(ICON_BUTTON_MUTED, ICON_BUTTON_QUIET_DISABLED_REST);
@@ -94,15 +170,48 @@ const ICON_BUTTON_DISABLED_REST = {
94
170
  const ICON_BUTTON_QUIET_DISABLED_REST = "disabled:hover:text-[var(--text-placeholder)]";
95
171
  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)]";
96
172
  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)]";
97
- const IconButton = forwardRef(function IconButton2({ variant = "ghost", size = "md", tone = "default", quiet, pressed, stopPropagation, className, onClick, onKeyDown, ...rest }, ref) {
98
- return /* @__PURE__ */ jsx(
173
+ const IconButton = forwardRef(function IconButton2({
174
+ variant = "ghost",
175
+ size = "md",
176
+ tone: toneProp,
177
+ quiet,
178
+ pressed,
179
+ stopPropagation,
180
+ stretch,
181
+ shape = "square",
182
+ toneColor,
183
+ label,
184
+ tooltip = true,
185
+ tooltipSide,
186
+ tooltipPortal,
187
+ disabledReason,
188
+ className,
189
+ style,
190
+ onClick,
191
+ onKeyDown,
192
+ ...rest
193
+ }, ref) {
194
+ const tone = toneProp ?? (toneColor !== void 0 ? "custom" : "default");
195
+ const reasonId = useId();
196
+ const locked = hasContent(disabledReason);
197
+ const ownDescribedBy = rest["aria-describedby"];
198
+ const button = /* @__PURE__ */ jsx(
99
199
  "button",
100
200
  {
101
201
  ref,
102
202
  ...rest,
203
+ disabled: locked ? void 0 : rest.disabled,
204
+ "aria-disabled": locked || rest["aria-disabled"],
205
+ "aria-describedby": locked ? ownDescribedBy ? `${ownDescribedBy} ${reasonId}` : reasonId : ownDescribedBy,
206
+ "aria-label": rest["aria-label"] ?? label,
103
207
  "aria-pressed": pressed ?? rest["aria-pressed"],
208
+ style: toneColor !== void 0 ? { ...style, ["--icon-button-tone"]: toneColor } : style,
104
209
  onClick: (e) => {
105
210
  if (stopPropagation) e.stopPropagation();
211
+ if (locked) {
212
+ e.preventDefault();
213
+ return;
214
+ }
106
215
  onClick?.(e);
107
216
  },
108
217
  onKeyDown: (e) => {
@@ -111,16 +220,44 @@ const IconButton = forwardRef(function IconButton2({ variant = "ghost", size = "
111
220
  },
112
221
  className: cn(
113
222
  ICON_BUTTON_BASE,
114
- ICON_BUTTON_SIZES[size],
223
+ stretch ? ICON_BUTTON_STRETCH_SIZES[size] : ICON_BUTTON_SIZES[size],
115
224
  // After the size, so the overlay's `rounded-full` beats the small sizes' `rounded`.
116
225
  variant === "overlay" ? ICON_BUTTON_OVERLAY : buttonVariantClasses[variant],
226
+ shape === "round" && "rounded-full",
117
227
  ICON_BUTTON_DISABLED_REST[variant],
118
228
  iconButtonToneClass(tone, quiet),
119
229
  pressed && ICON_BUTTON_PRESSED,
230
+ // The disabled look for the focusable kind of disabled, as on Button.
231
+ locked && "cursor-not-allowed opacity-50",
120
232
  className
121
233
  )
122
234
  }
123
235
  );
236
+ if (locked) {
237
+ return (
238
+ // Button's shape: the button and the hidden reason in a FRAGMENT, so Tooltip does
239
+ // not add its bubble to the description as well.
240
+ /* @__PURE__ */ jsx(
241
+ Tooltip,
242
+ {
243
+ label: disabledReason,
244
+ side: tooltipSide,
245
+ portal: tooltipPortal,
246
+ className: stretch ? "self-stretch" : void 0,
247
+ children: /* @__PURE__ */ jsxs(Fragment, { children: [
248
+ button,
249
+ /* @__PURE__ */ jsx("span", { id: reasonId, hidden: true, children: disabledReason })
250
+ ] })
251
+ }
252
+ )
253
+ );
254
+ }
255
+ if (!tooltip || label === void 0 || label === "") return button;
256
+ return (
257
+ // A fragment, so Tooltip leaves the button's description alone: the bubble says
258
+ // exactly what `aria-label` already does.
259
+ /* @__PURE__ */ jsx(Tooltip, { label, side: tooltipSide, portal: tooltipPortal, className: stretch ? "self-stretch" : void 0, children: /* @__PURE__ */ jsx(Fragment, { children: button }) })
260
+ );
124
261
  });
125
262
  IconButton.displayName = "IconButton";
126
263
  const FIELD_BASE = "block w-full rounded-md border border-[var(--border)] bg-[var(--bg-surface)] px-3 py-2 text-sm text-[var(--text-primary)] shadow-sm placeholder:text-[var(--text-placeholder)] focus:border-[var(--brand)] focus:ring-[var(--brand)] disabled:cursor-default disabled:bg-[var(--bg-surface-2)] disabled:text-[var(--text-muted)] [&[readonly]]:bg-[var(--bg-surface-2)] [&[readonly]]:text-[var(--text-muted)]";
@@ -581,8 +718,52 @@ function Spinner({ className, label, ...rest }) {
581
718
  " "
582
719
  ] }) });
583
720
  }
584
- function EmptyState({ title, hint, icon, action, headingAs, className, ...rest }) {
721
+ const EMPTY_STATE_TONE = {
722
+ danger: "text-[var(--danger)]",
723
+ success: "text-[var(--success)]"
724
+ };
725
+ function EmptyState({
726
+ title,
727
+ hint,
728
+ icon,
729
+ action,
730
+ headingAs,
731
+ variant = "box",
732
+ tone,
733
+ size = "md",
734
+ className,
735
+ ...rest
736
+ }) {
585
737
  const Title = headingAs ?? "div";
738
+ const toneClass = tone ? EMPTY_STATE_TONE[tone] : void 0;
739
+ const hasHint = hint != null && hint !== false && hint !== "";
740
+ if (variant === "inline") {
741
+ const sm = size === "sm";
742
+ return /* @__PURE__ */ jsxs(
743
+ "div",
744
+ {
745
+ ...rest,
746
+ className: cn(
747
+ "flex flex-wrap items-center text-[var(--text-muted)]",
748
+ sm ? "justify-start gap-x-1.5 gap-y-0.5 py-1 text-start text-xs" : "justify-center gap-x-2 gap-y-1 py-4 text-center text-sm",
749
+ className
750
+ ),
751
+ children: [
752
+ icon != null && /* @__PURE__ */ jsx(
753
+ "span",
754
+ {
755
+ "aria-hidden": true,
756
+ className: cn("flex text-[var(--text-placeholder)]", sm ? "[&_svg]:size-3.5" : "[&_svg]:size-4", toneClass),
757
+ children: icon
758
+ }
759
+ ),
760
+ /* @__PURE__ */ jsx(Title, { className: cn(sm ? "text-xs" : "text-sm", toneClass), children: title }),
761
+ hasHint && /* @__PURE__ */ jsx("span", { className: "text-xs", children: hint }),
762
+ action != null && /* @__PURE__ */ jsx("span", { className: "flex flex-wrap items-center gap-2", children: action })
763
+ ]
764
+ }
765
+ );
766
+ }
586
767
  return /* @__PURE__ */ jsxs(
587
768
  "div",
588
769
  {
@@ -592,9 +773,9 @@ function EmptyState({ title, hint, icon, action, headingAs, className, ...rest }
592
773
  className
593
774
  ),
594
775
  children: [
595
- icon != null && /* @__PURE__ */ jsx("div", { "aria-hidden": true, className: "mb-3 text-[var(--text-placeholder)] [&_svg]:size-8", children: icon }),
596
- /* @__PURE__ */ jsx(Title, { className: "text-sm font-medium text-[var(--text-secondary)]", children: title }),
597
- hint != null && hint !== false && hint !== "" && /* @__PURE__ */ jsx("div", { className: "mt-1 text-xs", children: hint }),
776
+ icon != null && /* @__PURE__ */ jsx("div", { "aria-hidden": true, className: cn("mb-3 text-[var(--text-placeholder)] [&_svg]:size-8", toneClass), children: icon }),
777
+ /* @__PURE__ */ jsx(Title, { className: cn("text-sm font-medium text-[var(--text-secondary)]", toneClass), children: title }),
778
+ hasHint && /* @__PURE__ */ jsx("div", { className: "mt-1 text-xs", children: hint }),
598
779
  action != null && /* @__PURE__ */ jsx("div", { className: "mt-4 flex flex-wrap items-center justify-center gap-2", children: action })
599
780
  ]
600
781
  }