@eifi1/ui-kit 0.22.0 → 0.24.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 (208) hide show
  1. package/README.md +42 -25
  2. package/dist/chart.d.ts +8 -7
  3. package/dist/components/amount-input.d.ts +34 -7
  4. package/dist/components/amount-input.js +14 -10
  5. package/dist/components/amount-input.js.map +1 -1
  6. package/dist/components/autocomplete.d.ts +26 -0
  7. package/dist/components/autocomplete.js +164 -137
  8. package/dist/components/autocomplete.js.map +1 -1
  9. package/dist/components/button-group.d.ts +8 -7
  10. package/dist/components/calculator.d.ts +8 -7
  11. package/dist/components/column-mapper.d.ts +235 -0
  12. package/dist/components/column-mapper.js +411 -0
  13. package/dist/components/column-mapper.js.map +1 -0
  14. package/dist/components/combobox-core.d.ts +39 -2
  15. package/dist/components/combobox-core.js +29 -6
  16. package/dist/components/combobox-core.js.map +1 -1
  17. package/dist/components/combobox.d.ts +113 -3
  18. package/dist/components/combobox.js +456 -313
  19. package/dist/components/combobox.js.map +1 -1
  20. package/dist/components/confirm-dialog.d.ts +8 -7
  21. package/dist/components/confirm-dialog.js +7 -8
  22. package/dist/components/confirm-dialog.js.map +1 -1
  23. package/dist/components/copy-button.d.ts +5 -4
  24. package/dist/components/country-select.d.ts +50 -7
  25. package/dist/components/country-select.js +65 -9
  26. package/dist/components/country-select.js.map +1 -1
  27. package/dist/components/danger-confirm.d.ts +8 -7
  28. package/dist/components/danger-confirm.js +64 -7
  29. package/dist/components/danger-confirm.js.map +1 -1
  30. package/dist/components/date-picker.d.ts +41 -0
  31. package/dist/components/date-picker.js +269 -8
  32. package/dist/components/date-picker.js.map +1 -1
  33. package/dist/components/entity-combobox.d.ts +48 -2
  34. package/dist/components/entity-combobox.js +159 -114
  35. package/dist/components/entity-combobox.js.map +1 -1
  36. package/dist/components/facing-pair.d.ts +8 -7
  37. package/dist/components/field-parts.d.ts +116 -6
  38. package/dist/components/field-parts.js +84 -0
  39. package/dist/components/field-parts.js.map +1 -1
  40. package/dist/components/field-strip.d.ts +105 -0
  41. package/dist/components/field-strip.js +46 -0
  42. package/dist/components/field-strip.js.map +1 -0
  43. package/dist/components/file-button.d.ts +8 -7
  44. package/dist/components/file-dropzone.d.ts +8 -7
  45. package/dist/components/form-actions.d.ts +8 -7
  46. package/dist/components/form-actions.js +23 -3
  47. package/dist/components/form-actions.js.map +1 -1
  48. package/dist/components/iban-input.d.ts +8 -7
  49. package/dist/components/icon-picker.d.ts +10 -1
  50. package/dist/components/icon-picker.js +12 -4
  51. package/dist/components/icon-picker.js.map +1 -1
  52. package/dist/components/language-select.d.ts +8 -7
  53. package/dist/components/money-field.d.ts +11 -7
  54. package/dist/components/money-field.js.map +1 -1
  55. package/dist/components/month-picker.js +2 -1
  56. package/dist/components/month-picker.js.map +1 -1
  57. package/dist/components/multi-entity-combobox.d.ts +31 -2
  58. package/dist/components/multi-entity-combobox.js +152 -103
  59. package/dist/components/multi-entity-combobox.js.map +1 -1
  60. package/dist/components/number-field.d.ts +8 -7
  61. package/dist/components/number-input.d.ts +8 -7
  62. package/dist/components/numpad-sheet.d.ts +8 -7
  63. package/dist/components/phone-input.d.ts +8 -7
  64. package/dist/components/reauth-dialog.d.ts +2 -2
  65. package/dist/components/reauth-dialog.js +6 -8
  66. package/dist/components/reauth-dialog.js.map +1 -1
  67. package/dist/components/series-chart.d.ts +8 -7
  68. package/dist/components/settings-fields.d.ts +8 -7
  69. package/dist/components/share-card.d.ts +8 -7
  70. package/dist/components/swatch-picker.d.ts +17 -1
  71. package/dist/components/swatch-picker.js +10 -4
  72. package/dist/components/swatch-picker.js.map +1 -1
  73. package/dist/components/text-link.d.ts +8 -7
  74. package/dist/components/tile-radio.d.ts +20 -0
  75. package/dist/components/tile-radio.js +25 -8
  76. package/dist/components/tile-radio.js.map +1 -1
  77. package/dist/components/time-input.d.ts +8 -7
  78. package/dist/components/toggle-group.js +2 -2
  79. package/dist/components/toggle-group.js.map +1 -1
  80. package/dist/components/trigger-aria.d.ts +3 -0
  81. package/dist/components/trigger-aria.js +3 -1
  82. package/dist/components/trigger-aria.js.map +1 -1
  83. package/dist/components/ui.d.ts +5 -4
  84. package/dist/components/ui.js +60 -28
  85. package/dist/components/ui.js.map +1 -1
  86. package/dist/feedback/feedback-attachment.d.ts +66 -1
  87. package/dist/feedback/feedback-attachment.js +185 -65
  88. package/dist/feedback/feedback-attachment.js.map +1 -1
  89. package/dist/feedback/feedback-dialog.d.ts +66 -1
  90. package/dist/feedback/feedback-dialog.js.map +1 -1
  91. package/dist/feedback/feedback-inbox.d.ts +67 -252
  92. package/dist/feedback/feedback-inbox.js.map +1 -1
  93. package/dist/feedback/feedback-thread.d.ts +66 -287
  94. package/dist/feedback/feedback-thread.js +16 -3
  95. package/dist/feedback/feedback-thread.js.map +1 -1
  96. package/dist/{kit-labels-R8mIAc9N.d.ts → feedback-DOwPu-Il.d.ts} +1273 -30
  97. package/dist/feedback.d.ts +65 -3
  98. package/dist/hooks/use-file-drop.d.ts +8 -7
  99. package/dist/i18n/defaults.d.ts +8 -7
  100. package/dist/i18n/defaults.js +3 -1
  101. package/dist/i18n/defaults.js.map +1 -1
  102. package/dist/i18n/german.d.ts +8 -7
  103. package/dist/i18n/german.js +36 -2
  104. package/dist/i18n/german.js.map +1 -1
  105. package/dist/i18n/kit-labels.d.ts +5 -4
  106. package/dist/i18n/kit-labels.js.map +1 -1
  107. package/dist/i18n/languages.d.ts +8 -7
  108. package/dist/i18n/locales/de-CH.d.ts +8 -7
  109. package/dist/i18n/locales/en.d.ts +8 -7
  110. package/dist/i18n/locales/en.js +7 -0
  111. package/dist/i18n/locales/en.js.map +1 -1
  112. package/dist/i18n/locales/es.d.ts +8 -7
  113. package/dist/i18n/locales/es.js +37 -2
  114. package/dist/i18n/locales/es.js.map +1 -1
  115. package/dist/i18n/locales/fr.d.ts +8 -7
  116. package/dist/i18n/locales/fr.js +36 -2
  117. package/dist/i18n/locales/fr.js.map +1 -1
  118. package/dist/i18n/locales/hu.d.ts +8 -7
  119. package/dist/i18n/locales/hu.js +38 -2
  120. package/dist/i18n/locales/hu.js.map +1 -1
  121. package/dist/i18n/locales/it.d.ts +8 -7
  122. package/dist/i18n/locales/it.js +37 -2
  123. package/dist/i18n/locales/it.js.map +1 -1
  124. package/dist/i18n/locales/zh.d.ts +8 -7
  125. package/dist/i18n/locales/zh.js +34 -2
  126. package/dist/i18n/locales/zh.js.map +1 -1
  127. package/dist/i18n/review.d.ts +8 -7
  128. package/dist/i18n/review.js +20 -1
  129. package/dist/i18n/review.js.map +1 -1
  130. package/dist/index.d.ts +6 -4
  131. package/dist/index.js +16 -0
  132. package/dist/index.js.map +1 -1
  133. package/dist/lib/column-mapping.d.ts +81 -0
  134. package/dist/lib/column-mapping.js +108 -0
  135. package/dist/lib/column-mapping.js.map +1 -0
  136. package/dist/lib/strip-fade.d.ts +2 -2
  137. package/dist/lib/strip-fade.js.map +1 -1
  138. package/dist/lib/table-text.d.ts +109 -1
  139. package/dist/lib/table-text.js +122 -1
  140. package/dist/lib/table-text.js.map +1 -1
  141. package/dist/rhf/fields.d.ts +142 -13
  142. package/dist/rhf/fields.js +185 -3
  143. package/dist/rhf/fields.js.map +1 -1
  144. package/dist/rhf/form.d.ts +8 -7
  145. package/dist/rhf.d.ts +9 -8
  146. package/dist/rhf.js.map +1 -1
  147. package/dist/shell/app-shell.d.ts +8 -7
  148. package/dist/shell/top-bar-brand.d.ts +8 -7
  149. package/dist/shell/topbar-action-menu.d.ts +36 -3
  150. package/dist/shell/topbar-action-menu.js +74 -33
  151. package/dist/shell/topbar-action-menu.js.map +1 -1
  152. package/dist/shell.d.ts +8 -7
  153. package/dist/table-text.d.ts +1 -1
  154. package/dist/wizard/stepper-nav.d.ts +38 -9
  155. package/dist/wizard/stepper-nav.js +4 -0
  156. package/dist/wizard/stepper-nav.js.map +1 -1
  157. package/dist/wizard.d.ts +8 -7
  158. package/package.json +1 -1
  159. package/src/components/amount-input.tsx +50 -14
  160. package/src/components/autocomplete.tsx +188 -116
  161. package/src/components/column-mapper.tsx +768 -0
  162. package/src/components/combobox-core.tsx +82 -9
  163. package/src/components/combobox.tsx +655 -335
  164. package/src/components/confirm-dialog.tsx +12 -8
  165. package/src/components/country-select.tsx +135 -22
  166. package/src/components/danger-confirm.tsx +274 -24
  167. package/src/components/date-picker.tsx +423 -10
  168. package/src/components/entity-combobox.tsx +243 -129
  169. package/src/components/field-parts.tsx +224 -5
  170. package/src/components/field-strip.tsx +149 -0
  171. package/src/components/form-actions.tsx +52 -5
  172. package/src/components/icon-picker.tsx +23 -4
  173. package/src/components/money-field.tsx +3 -0
  174. package/src/components/month-picker.tsx +2 -1
  175. package/src/components/multi-entity-combobox.tsx +221 -121
  176. package/src/components/reauth-dialog.tsx +17 -18
  177. package/src/components/swatch-picker.tsx +29 -5
  178. package/src/components/tile-radio.tsx +74 -13
  179. package/src/components/toggle-group.tsx +2 -2
  180. package/src/components/trigger-aria.ts +5 -0
  181. package/src/components/ui.tsx +114 -33
  182. package/src/feedback/feedback-attachment.tsx +372 -72
  183. package/src/feedback/feedback-dialog.tsx +7 -3
  184. package/src/feedback/feedback-inbox.tsx +3 -2
  185. package/src/feedback/feedback-thread.tsx +112 -9
  186. package/src/i18n/defaults.ts +2 -0
  187. package/src/i18n/german.ts +42 -0
  188. package/src/i18n/kit-labels.tsx +4 -0
  189. package/src/i18n/locales/en.ts +27 -5
  190. package/src/i18n/locales/es.ts +41 -0
  191. package/src/i18n/locales/fr.ts +42 -0
  192. package/src/i18n/locales/hu.ts +37 -0
  193. package/src/i18n/locales/it.ts +41 -0
  194. package/src/i18n/locales/zh.ts +32 -0
  195. package/src/i18n/review.ts +19 -0
  196. package/src/index.ts +18 -0
  197. package/src/lib/column-mapping.ts +234 -0
  198. package/src/lib/strip-fade.ts +2 -2
  199. package/src/lib/table-text.ts +271 -0
  200. package/src/rhf/fields.tsx +421 -6
  201. package/src/rhf.ts +2 -1
  202. package/src/shell/topbar-action-menu.tsx +134 -38
  203. package/src/wizard/stepper-nav.tsx +35 -1
  204. package/dist/components/field-anatomy.d.ts +0 -95
  205. package/dist/components/field-anatomy.js +0 -84
  206. package/dist/components/field-anatomy.js.map +0 -1
  207. package/dist/feedback-attachment-NhUm7Zga.d.ts +0 -367
  208. package/src/components/field-anatomy.tsx +0 -190
@@ -1,4 +1,4 @@
1
- import { useLayoutEffect, useRef } from "react";
1
+ import { useId, useLayoutEffect, useRef } from "react";
2
2
  import type { KeyboardEvent as ReactKeyboardEvent, ReactNode } from "react";
3
3
  import { Link } from "react-router";
4
4
  import { cn } from "../lib/cn";
@@ -11,9 +11,9 @@ import { TOPBAR_TRIGGER_CLASS } from "./topbar-controls";
11
11
 
12
12
  /**
13
13
  * One row of a {@link TopBarActionMenu}. An `action` (default) runs `onSelect`, a
14
- * `link` navigates via react-router, and a `divider` draws a rule. Non-divider
15
- * rows close the menu automatically when chosen. `kind` may be omitted for
16
- * actions, keeping the common case terse.
14
+ * `link` navigates via react-router, a `divider` draws a rule, and a `heading` names
15
+ * the rows after it. Action and link rows close the menu automatically when chosen.
16
+ * `kind` may be omitted for actions, keeping the common case terse.
17
17
  *
18
18
  * The rows are {@link MenuItem}s, so they take its options: `tone: "danger"` for the
19
19
  * log-out row, `checked` (+ `checkable`) for a choice, `current` for the link to the
@@ -46,7 +46,67 @@ export type TopBarMenuEntry =
46
46
  current?: boolean;
47
47
  disabled?: boolean;
48
48
  }
49
- | { kind: "divider"; key: string };
49
+ | { kind: "divider"; key: string }
50
+ | {
51
+ /**
52
+ * A group heading (0.23, keksdose's guided-tours menu): the rows after it, up to
53
+ * the next heading, the next `divider` or the end of `entries`, are ONE group
54
+ * named by `label`. keksdose files its tours by topic and separated the blocks
55
+ * with bare dividers — "the kit's menu has no heading row to name them with"
56
+ * (tour-menu.tsx) — so a sighted reader had to guess what each block was and a
57
+ * screen reader was told nothing at all.
58
+ *
59
+ * Not a row: it takes no focus, the arrow keys (and Home/End) pass over it, and it
60
+ * never closes the menu. It is announced the way the kit's other grouped lists
61
+ * announce theirs (`CommandPalette`): the group's rows sit in a `role="group"`
62
+ * whose `aria-labelledby` is this heading, so a reader says "On this page, group"
63
+ * when the focus enters the first row, and nothing between rows.
64
+ *
65
+ * A divider ENDS the group, so a closing row such as "All tours…" behind a rule
66
+ * belongs to no topic. A heading with no rows under it is not drawn — an empty
67
+ * group names nothing; build the list from data and it may well be empty.
68
+ *
69
+ * The menu-wide `heading` prop is the panel's title, drawn once above everything;
70
+ * with group headings it is usually redundant (two stacked caps lines read as one),
71
+ * and the trigger's `ariaLabel` already names the menu.
72
+ */
73
+ kind: "heading";
74
+ key: string;
75
+ /** The group's name, shown above its rows and announced as the group's name. Keep
76
+ * it short: it is set in small caps and wraps on a narrow panel. */
77
+ label: ReactNode;
78
+ };
79
+
80
+ type HeadingEntry = Extract<TopBarMenuEntry, { kind: "heading" }>;
81
+ type DividerEntry = Extract<TopBarMenuEntry, { kind: "divider" }>;
82
+ type RowEntry = Exclude<TopBarMenuEntry, HeadingEntry | DividerEntry>;
83
+
84
+ /** What the panel draws from `entries`: loose rows and rules, and headed groups. */
85
+ type Block = { kind: "loose"; entry: RowEntry | DividerEntry } | { kind: "group"; heading: HeadingEntry; rows: RowEntry[] };
86
+
87
+ /**
88
+ * Cut `entries` into blocks: a heading opens a group that takes the rows after it until
89
+ * the next heading or divider; everything else stands alone, as it always has.
90
+ */
91
+ function toBlocks(entries: readonly TopBarMenuEntry[]): Block[] {
92
+ const blocks: Block[] = [];
93
+ let group: Extract<Block, { kind: "group" }> | null = null;
94
+ for (const entry of entries) {
95
+ if (entry.kind === "heading") {
96
+ group = { kind: "group", heading: entry, rows: [] };
97
+ blocks.push(group);
98
+ } else if (entry.kind === "divider") {
99
+ group = null;
100
+ blocks.push({ kind: "loose", entry });
101
+ } else if (group) {
102
+ group.rows.push(entry);
103
+ } else {
104
+ blocks.push({ kind: "loose", entry });
105
+ }
106
+ }
107
+ // A heading with nothing under it names nothing: not drawn (see the `heading` kind).
108
+ return blocks.filter((block) => block.kind === "loose" || block.rows.length > 0);
109
+ }
50
110
 
51
111
  /**
52
112
  * The non-interactive identity block at the top of an account menu — kastlan's name +
@@ -130,6 +190,11 @@ function keepTabInsidePanel(e: ReactKeyboardEvent<HTMLUListElement>) {
130
190
  * It is also the ACCOUNT menu kastlan and keksdose each hand-build on `HoverMenu`: a
131
191
  * custom `trigger` (the avatar), a `header` (name + role / email), the rows, and a
132
192
  * free-content `footer` (the legal links).
193
+ *
194
+ * A long menu is grouped by topic with `{ kind: "heading" }` entries (0.23) — see
195
+ * {@link TopBarMenuEntry}. There is no separate phone layout: the panel is the same
196
+ * hover panel at 390px, kept inside the viewport by `HoverMenu`, and a heading wraps
197
+ * rather than widening it.
133
198
  */
134
199
  export function TopBarActionMenu({
135
200
  icon,
@@ -206,6 +271,10 @@ export function TopBarActionMenu({
206
271
  align?: "start" | "end" | "left" | "right";
207
272
  panelClassName?: string;
208
273
  }) {
274
+ // Names the groups' headings for `aria-labelledby`. Positional rather than from the
275
+ // entry's `key`, which is the caller's text and may hold a space — and a space in
276
+ // `aria-labelledby` is a list separator.
277
+ const uid = useId();
209
278
  return (
210
279
  <HoverMenu
211
280
  ariaLabel={ariaLabel}
@@ -258,6 +327,40 @@ export function TopBarActionMenu({
258
327
  const headerExtra = typeof header?.extra === "function" ? header.extra(close) : header?.extra;
259
328
  const footerClass =
260
329
  "flex flex-wrap gap-x-3 gap-y-1 text-xs text-[var(--text-muted)] [&_a:hover]:text-[var(--text-primary)] [&_a]:rounded-sm [&_a:focus-visible]:outline-none [&_a:focus-visible]:ring-2 [&_a:focus-visible]:ring-[var(--brand)]";
330
+ /** One action or link row — loose, or inside a heading's group. */
331
+ const row = (entry: RowEntry) => (
332
+ <li key={entry.key}>
333
+ {entry.kind === "link" ? (
334
+ <MenuItem
335
+ href={entry.to}
336
+ renderLink={({ href, ...p }) => <Link to={href} {...p} />}
337
+ onClick={close}
338
+ leading={entry.icon}
339
+ trailing={entry.trailing}
340
+ tone={entry.tone}
341
+ current={entry.current}
342
+ disabled={entry.disabled}
343
+ >
344
+ {entry.label}
345
+ </MenuItem>
346
+ ) : (
347
+ <MenuItem
348
+ onClick={() => {
349
+ entry.onSelect();
350
+ close();
351
+ }}
352
+ leading={entry.icon}
353
+ trailing={entry.trailing}
354
+ tone={entry.tone}
355
+ checked={entry.checked}
356
+ checkable={entry.checkable}
357
+ disabled={entry.disabled}
358
+ >
359
+ {entry.label}
360
+ </MenuItem>
361
+ )}
362
+ </li>
363
+ );
261
364
  return (
262
365
  // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a Tab bubbling up from a row or footer link, see keepTabInsidePanel
263
366
  <ul className={cn("py-1", panelClassName)} onKeyDown={keepTabInsidePanel}>
@@ -275,41 +378,34 @@ export function TopBarActionMenu({
275
378
  {heading}
276
379
  </li>
277
380
  )}
278
- {entries.map((entry) => {
279
- if (entry.kind === "divider") {
280
- return <li key={entry.key} className="my-1 border-t border-[var(--border)]" />;
381
+ {toBlocks(entries).map((block, i, blocks) => {
382
+ if (block.kind === "loose") {
383
+ const entry = block.entry;
384
+ if (entry.kind === "divider") {
385
+ return <li key={entry.key} className="my-1 border-t border-[var(--border)]" />;
386
+ }
387
+ return row(entry);
281
388
  }
389
+ const headingId = `${uid}-group-${i}`;
390
+ const before = blocks[i - 1];
391
+ // Under a rule (or at the top of the rows) the rule's margin is the gap;
392
+ // straight under another group's last row the heading makes its own.
393
+ const tight = !before || (before.kind === "loose" && before.entry.kind === "divider");
282
394
  return (
283
- <li key={entry.key}>
284
- {entry.kind === "link" ? (
285
- <MenuItem
286
- href={entry.to}
287
- renderLink={({ href, ...p }) => <Link to={href} {...p} />}
288
- onClick={close}
289
- leading={entry.icon}
290
- trailing={entry.trailing}
291
- tone={entry.tone}
292
- current={entry.current}
293
- disabled={entry.disabled}
294
- >
295
- {entry.label}
296
- </MenuItem>
297
- ) : (
298
- <MenuItem
299
- onClick={() => {
300
- entry.onSelect();
301
- close();
302
- }}
303
- leading={entry.icon}
304
- trailing={entry.trailing}
305
- tone={entry.tone}
306
- checked={entry.checked}
307
- checkable={entry.checkable}
308
- disabled={entry.disabled}
309
- >
310
- {entry.label}
311
- </MenuItem>
312
- )}
395
+ <li key={block.heading.key} role="none">
396
+ <div
397
+ id={headingId}
398
+ role="presentation"
399
+ className={cn(
400
+ "px-3 pb-0.5 text-[11px] font-semibold uppercase tracking-wide text-[var(--text-muted)] [overflow-wrap:anywhere]",
401
+ tight ? "pt-1" : "pt-2.5",
402
+ )}
403
+ >
404
+ {block.heading.label}
405
+ </div>
406
+ <ul role="group" aria-labelledby={headingId}>
407
+ {block.rows.map(row)}
408
+ </ul>
313
409
  </li>
314
410
  );
315
411
  })}
@@ -31,7 +31,7 @@ import type { UseWizardReturn, WizardLabels } from "./types";
31
31
  * `isCommitStep`), so the policy options — `cancellable`, `onExit`, `commits`,
32
32
  * `onDone` — are all set on `useWizard`. What is set HERE is how the Finish button
33
33
  * looks and what wraps it, because that is presentation and usually depends on
34
- * state the app holds (a "replace everything" toggle, a write lock).
34
+ * state the app holds (a "replace everything" toggle, a write lock — `finishCommit`).
35
35
  */
36
36
  export function StepperNav<TData extends Record<string, unknown>>({
37
37
  wizard,
@@ -42,6 +42,8 @@ export function StepperNav<TData extends Record<string, unknown>>({
42
42
  labels,
43
43
  finishVariant = "brand",
44
44
  finishDisabled = false,
45
+ finishCommit,
46
+ finishDisabledReason,
45
47
  doneDisabled = false,
46
48
  renderFinish,
47
49
  }: {
@@ -57,6 +59,30 @@ export function StepperNav<TData extends Record<string, unknown>>({
57
59
  /** Disable Finish for a reason of the app's own, on top of the kit's gates
58
60
  * (`canFinish`, submitting, validating). */
59
61
  finishDisabled?: boolean;
62
+ /**
63
+ * Finish COMMITS — {@link Button}'s `commit`, handed to the Finish button. Under a
64
+ * locked {@link WriteLockProvider} it is `aria-disabled` but focusable, with the lock's
65
+ * reason in the kit Tooltip, and a click (or Enter) does not run `wizard.finish`.
66
+ * Every other button of the bar stays live: the steps before the commit write nothing,
67
+ * so the whole wizard can still be walked through on a read-only page.
68
+ *
69
+ * keksdose G1: its YNAB import runs the mapping wizard on the read-only demo (dev#497)
70
+ * and locks only the confirm step's Finish — by cloning `commit` into the element
71
+ * `renderFinish` hands it, because StepperNav took no `commit` of its own. That clone
72
+ * still works; this is the same thing without one. Named `finishCommit` beside
73
+ * `finishVariant` / `finishDisabled` rather than a bare `commit`, which on a component
74
+ * with five buttons would not say which one saves — and would read like `useWizard`'s
75
+ * `commits`, which is a step's policy, not a lock.
76
+ */
77
+ finishCommit?: boolean;
78
+ /**
79
+ * Why Finish is not available — {@link Button}'s `disabledReason` on the Finish button:
80
+ * `aria-disabled` and focusable, the reason in the kit Tooltip and on the button's
81
+ * `aria-describedby`, `wizard.finish` not run. The explained form of `finishDisabled`
82
+ * (a native `disabled` takes the button out of the tab order, so its reason never
83
+ * reaches a keyboard). Under a lock with `finishCommit`, the lock's reason wins.
84
+ */
85
+ finishDisabledReason?: ReactNode;
60
86
  /**
61
87
  * Disable Done — the button of the last step after a commit — while the step still
62
88
  * has work of its own running. keksdose's YNAB import ends on a recurring step whose
@@ -82,6 +108,10 @@ export function StepperNav<TData extends Record<string, unknown>>({
82
108
  * re-derive (the problem this component exists to end); `onBeforeFinish` can veto
83
109
  * a click but cannot explain a disabled button. The element is a plain `<Button>`,
84
110
  * so a wrapper that clones it with `disabled` works as expected.
111
+ *
112
+ * For the write lock itself there is no need to wrap any more: `finishCommit` (and
113
+ * `finishDisabledReason`) put the kit Button's own Tooltip on Finish. An element
114
+ * cloned with `commit` here keeps working the same way.
85
115
  */
86
116
  renderFinish?: (button: ReactElement) => ReactNode;
87
117
  }) {
@@ -134,6 +164,10 @@ export function StepperNav<TData extends Record<string, unknown>>({
134
164
  variant={finishVariant}
135
165
  data-tour="wizard-finish"
136
166
  onClick={wizard.finish}
167
+ // The kit Button's lock: a reason (its own or, with `commit`, the provider's)
168
+ // wins over `disabled` and keeps the button reachable, saying why.
169
+ commit={finishCommit}
170
+ disabledReason={finishDisabledReason}
137
171
  disabled={wizard.isSubmitting || pending || !canFinish || finishDisabled}
138
172
  aria-busy={wizard.isSubmitting || pending || undefined}
139
173
  >
@@ -1,95 +0,0 @@
1
- import * as react from 'react';
2
- import { ReactNode } from 'react';
3
-
4
- /** `undefined`, `null`, `false` and `""` are what a caller's `cond && "…"` evaluates to
5
- * when there is nothing to say; none of them is content. A numeric `0` is. */
6
- declare function hasContent(node: ReactNode): boolean;
7
- /**
8
- * A `hint` that is plain TEXT is a caption under the field; anything else (a
9
- * `FieldHint` "?") rides the label line — the rule `Select` set in 0.15: the label
10
- * line is 11px of strip shared with the label, and a sentence placed there was set on
11
- * top of both the label and the value.
12
- */
13
- declare function isTextHint(hint: ReactNode): boolean;
14
- interface FieldMessages {
15
- /** `invalid`, a message, or an `aria-invalid` from outside (a form library). */
16
- isInvalid: boolean;
17
- /** The caller's description, then the caption, then the error — the standing
18
- * advice before the news, as `useFieldError` in ui.tsx orders them. */
19
- describedBy: string | undefined;
20
- /** The caption and the error, for under the field; `null` when there is neither. */
21
- below: ReactNode;
22
- /** The hint for the LABEL line (a `FieldHint`), or `undefined` when the hint is a
23
- * caption or there is none. */
24
- labelHint: ReactNode;
25
- }
26
- /** `hint` / `error` / `invalid` resolved the way `Select` resolves them. */
27
- declare function useFieldMessages({ hint, error, invalid, describedBy, ariaInvalid, }: {
28
- hint?: ReactNode;
29
- error?: ReactNode;
30
- invalid?: boolean;
31
- describedBy?: string;
32
- ariaInvalid?: unknown;
33
- }): FieldMessages;
34
- /**
35
- * The field and what is under it, as one box — `FieldGroup` in ui.tsx, for the same
36
- * reasons written there. Nothing of its own while there is nothing under the field and
37
- * the caller passed no `error` key (the DOM is exactly what it was before these props);
38
- * once `error` is passed the box is kept even with no message, so the trigger is never
39
- * reparented — and remounted, losing focus — as the message comes and goes.
40
- */
41
- declare function FieldBox({ below, reserve, children, }: {
42
- below: ReactNode;
43
- reserve: boolean;
44
- children: ReactNode;
45
- }): react.JSX.Element;
46
- /**
47
- * The static label in a trigger field's top strip — a {@link FieldLabel}, or, with a
48
- * `FieldHint`, the label and its "?" laid out on ONE flex row (dev#468: two absolute
49
- * offsets guessing at the same baseline put the "?" three pixels above its word).
50
- * `FloatingField`'s static-label row, except that it stops at `end-9`: the date and
51
- * month triggers keep that column (`pe-9`) for the calendar glyph or the clear ×, and a
52
- * long label truncates before it rather than putting the "?" on top of it.
53
- */
54
- declare function FieldLabelLine({ label, hint }: {
55
- label: ReactNode;
56
- hint?: ReactNode;
57
- }): react.JSX.Element;
58
- /**
59
- * How tall the strip over a bare group is.
60
- *
61
- * - `"toggle"` — 16px. keksdose's `ClearedStatusPicker` (live #288 rework, #431,
62
- * measured): *"16px of label strip plus a 26px group is exactly a labelled Input"*,
63
- * so a status group in a row of 42px fields ends where they end. The label's line
64
- * box runs into the group's top hairline, but the ACTIVE fill — the thing feedback
65
- * #117 was about — starts at 19px, below the label's 17.75px.
66
- * - `"tiles"` — 20px, keksdose's `FlagPicker` and location field (`pt-5`). A tile has
67
- * no inset fill: its selection IS its frame, a 2px dark border-and-ring from the
68
- * tile's top edge, so the tile has to start below the whole label line.
69
- */
70
- declare const LABEL_STRIP_PAD: {
71
- readonly toggle: "pt-4";
72
- readonly tiles: "pt-5";
73
- };
74
- /**
75
- * The 11px label (and its `FieldHint`) over a group that is NOT a field — keksdose K2:
76
- * a segmented control or a row of swatches standing in a form row beside labelled
77
- * inputs, whose label must sit on the same line as theirs (`top-1`, `inset-x-3`, the
78
- * type of a field's static label) without the group growing a field's border round
79
- * it. `id` goes on the label so the group can be `aria-labelledby` it.
80
- *
81
- * `relative` is load-bearing twice over: the row is positioned against it, and a
82
- * caller's `sr-only` label cannot escape it (sr-only-containment).
83
- */
84
- declare function LabelStrip({ labelId, label, hint, disabled, pad, className, children, }: {
85
- labelId: string;
86
- label: ReactNode;
87
- hint?: ReactNode;
88
- /** Dim the label with its group, as `peer-disabled:` dims a field's own. */
89
- disabled?: boolean;
90
- pad: keyof typeof LABEL_STRIP_PAD;
91
- className?: string;
92
- children: ReactNode;
93
- }): react.JSX.Element;
94
-
95
- export { FieldBox, FieldLabelLine, type FieldMessages, LABEL_STRIP_PAD, LabelStrip, hasContent, isTextHint, useFieldMessages };
@@ -1,84 +0,0 @@
1
- "use client";
2
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { useId } from "react";
4
- import { cn } from "../lib/cn.js";
5
- import { FieldLabel } from "./ui.js";
6
- function hasContent(node) {
7
- return node !== void 0 && node !== null && node !== false && node !== "";
8
- }
9
- function isTextHint(hint) {
10
- return typeof hint === "string" && hint !== "" || typeof hint === "number";
11
- }
12
- const FIELD_CAPTION_CLASS = "mt-1 text-[11px] leading-tight text-[var(--text-muted)]";
13
- const FIELD_ERROR_CLASS = "mt-1 text-[11px] leading-tight text-[var(--danger)]";
14
- function useFieldMessages({
15
- hint,
16
- error,
17
- invalid,
18
- describedBy,
19
- ariaInvalid
20
- }) {
21
- const hintId = useId();
22
- const errorId = useId();
23
- const textHint = isTextHint(hint);
24
- const hasError = hasContent(error);
25
- return {
26
- isInvalid: Boolean(invalid) || hasError || ariaInvalid === true || ariaInvalid === "true",
27
- describedBy: [describedBy, textHint && hintId, hasError && errorId].filter(Boolean).join(" ") || void 0,
28
- below: textHint || hasError ? /* @__PURE__ */ jsxs(Fragment, { children: [
29
- textHint && /* @__PURE__ */ jsx("p", { id: hintId, className: FIELD_CAPTION_CLASS, children: hint }),
30
- hasError && /* @__PURE__ */ jsx("p", { id: errorId, className: FIELD_ERROR_CLASS, children: error })
31
- ] }) : null,
32
- labelHint: !textHint && hasContent(hint) ? hint : void 0
33
- };
34
- }
35
- function FieldBox({
36
- below,
37
- reserve,
38
- children
39
- }) {
40
- if (below === null && !reserve) return /* @__PURE__ */ jsx(Fragment, { children });
41
- return /* @__PURE__ */ jsxs("div", { children: [
42
- children,
43
- below
44
- ] });
45
- }
46
- function FieldLabelLine({ label, hint }) {
47
- if (!hasContent(hint)) return /* @__PURE__ */ jsx(FieldLabel, { children: label });
48
- return (
49
- // `z-10` as FieldLabel has it: the trigger after it is `relative` and would paint
50
- // over the row (and take the "?"'s hover) otherwise.
51
- /* @__PURE__ */ jsxs("div", { className: "pointer-events-none absolute start-3 end-9 top-1 z-10 flex items-center gap-1", children: [
52
- /* @__PURE__ */ jsx(FieldLabel, { className: "static min-w-0 max-w-full", children: label }),
53
- /* @__PURE__ */ jsx("span", { className: "pointer-events-auto flex shrink-0 items-center", children: hint })
54
- ] })
55
- );
56
- }
57
- const LABEL_STRIP_PAD = { toggle: "pt-4", tiles: "pt-5" };
58
- function LabelStrip({
59
- labelId,
60
- label,
61
- hint,
62
- disabled,
63
- pad,
64
- className,
65
- children
66
- }) {
67
- return /* @__PURE__ */ jsxs("div", { className: cn("relative min-w-0", LABEL_STRIP_PAD[pad], className), children: [
68
- /* @__PURE__ */ jsxs("div", { className: "pointer-events-none absolute inset-x-3 top-1 flex items-center gap-1", children: [
69
- /* @__PURE__ */ jsx(FieldLabel, { id: labelId, className: cn("static min-w-0 max-w-full", disabled && "opacity-50"), children: label }),
70
- hasContent(hint) && /* @__PURE__ */ jsx("span", { className: "pointer-events-auto flex shrink-0 items-center", children: hint })
71
- ] }),
72
- children
73
- ] });
74
- }
75
- export {
76
- FieldBox,
77
- FieldLabelLine,
78
- LABEL_STRIP_PAD,
79
- LabelStrip,
80
- hasContent,
81
- isTextHint,
82
- useFieldMessages
83
- };
84
- //# sourceMappingURL=field-anatomy.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/components/field-anatomy.tsx"],"sourcesContent":["// Internal — not re-exported from the barrel. The pieces of a field's anatomy (its\n// label line, the caption and the error under it, the label STRIP over a bare group)\n// for the controls that are not built on `FloatingField`: the date and month pickers,\n// whose trigger is a button in a wrapper of their own, and the groups (ToggleGroup,\n// SwatchPicker, IconPicker), which have no field box at all.\n//\n// `Input` and `Select` keep their own copies in ui.tsx; these follow them class for\n// class, so a DatePicker's caption and a Select's caption are the same 11px line in\n// the same place (keksdose K4: \"uniform field anatomy … today it is a patchwork\").\n\nimport { useId } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { FieldLabel } from \"./ui\";\n\n/** `undefined`, `null`, `false` and `\"\"` are what a caller's `cond && \"…\"` evaluates to\n * when there is nothing to say; none of them is content. A numeric `0` is. */\nexport function hasContent(node: ReactNode): boolean {\n return node !== undefined && node !== null && node !== false && node !== \"\";\n}\n\n/**\n * A `hint` that is plain TEXT is a caption under the field; anything else (a\n * `FieldHint` \"?\") rides the label line — the rule `Select` set in 0.15: the label\n * line is 11px of strip shared with the label, and a sentence placed there was set on\n * top of both the label and the value.\n */\nexport function isTextHint(hint: ReactNode): boolean {\n return (typeof hint === \"string\" && hint !== \"\") || typeof hint === \"number\";\n}\n\nconst FIELD_CAPTION_CLASS = \"mt-1 text-[11px] leading-tight text-[var(--text-muted)]\";\nconst FIELD_ERROR_CLASS = \"mt-1 text-[11px] leading-tight text-[var(--danger)]\";\n\nexport interface FieldMessages {\n /** `invalid`, a message, or an `aria-invalid` from outside (a form library). */\n isInvalid: boolean;\n /** The caller's description, then the caption, then the error — the standing\n * advice before the news, as `useFieldError` in ui.tsx orders them. */\n describedBy: string | undefined;\n /** The caption and the error, for under the field; `null` when there is neither. */\n below: ReactNode;\n /** The hint for the LABEL line (a `FieldHint`), or `undefined` when the hint is a\n * caption or there is none. */\n labelHint: ReactNode;\n}\n\n/** `hint` / `error` / `invalid` resolved the way `Select` resolves them. */\nexport function useFieldMessages({\n hint,\n error,\n invalid,\n describedBy,\n ariaInvalid,\n}: {\n hint?: ReactNode;\n error?: ReactNode;\n invalid?: boolean;\n describedBy?: string;\n ariaInvalid?: unknown;\n}): FieldMessages {\n const hintId = useId();\n const errorId = useId();\n const textHint = isTextHint(hint);\n const hasError = hasContent(error);\n return {\n isInvalid: Boolean(invalid) || hasError || ariaInvalid === true || ariaInvalid === \"true\",\n describedBy:\n [describedBy, textHint && hintId, hasError && errorId].filter(Boolean).join(\" \") || undefined,\n below:\n textHint || hasError ? (\n <>\n {textHint && (\n <p id={hintId} className={FIELD_CAPTION_CLASS}>\n {hint}\n </p>\n )}\n {hasError && (\n <p id={errorId} className={FIELD_ERROR_CLASS}>\n {error}\n </p>\n )}\n </>\n ) : null,\n labelHint: !textHint && hasContent(hint) ? hint : undefined,\n };\n}\n\n/**\n * The field and what is under it, as one box — `FieldGroup` in ui.tsx, for the same\n * reasons written there. Nothing of its own while there is nothing under the field and\n * the caller passed no `error` key (the DOM is exactly what it was before these props);\n * once `error` is passed the box is kept even with no message, so the trigger is never\n * reparented — and remounted, losing focus — as the message comes and goes.\n */\nexport function FieldBox({\n below,\n reserve,\n children,\n}: {\n below: ReactNode;\n reserve: boolean;\n children: ReactNode;\n}) {\n if (below === null && !reserve) return <>{children}</>;\n return (\n <div>\n {children}\n {below}\n </div>\n );\n}\n\n/**\n * The static label in a trigger field's top strip — a {@link FieldLabel}, or, with a\n * `FieldHint`, the label and its \"?\" laid out on ONE flex row (dev#468: two absolute\n * offsets guessing at the same baseline put the \"?\" three pixels above its word).\n * `FloatingField`'s static-label row, except that it stops at `end-9`: the date and\n * month triggers keep that column (`pe-9`) for the calendar glyph or the clear ×, and a\n * long label truncates before it rather than putting the \"?\" on top of it.\n */\nexport function FieldLabelLine({ label, hint }: { label: ReactNode; hint?: ReactNode }) {\n if (!hasContent(hint)) return <FieldLabel>{label}</FieldLabel>;\n return (\n // `z-10` as FieldLabel has it: the trigger after it is `relative` and would paint\n // over the row (and take the \"?\"'s hover) otherwise.\n <div className=\"pointer-events-none absolute start-3 end-9 top-1 z-10 flex items-center gap-1\">\n <FieldLabel className=\"static min-w-0 max-w-full\">{label}</FieldLabel>\n <span className=\"pointer-events-auto flex shrink-0 items-center\">{hint}</span>\n </div>\n );\n}\n\n/**\n * How tall the strip over a bare group is.\n *\n * - `\"toggle\"` — 16px. keksdose's `ClearedStatusPicker` (live #288 rework, #431,\n * measured): *\"16px of label strip plus a 26px group is exactly a labelled Input\"*,\n * so a status group in a row of 42px fields ends where they end. The label's line\n * box runs into the group's top hairline, but the ACTIVE fill — the thing feedback\n * #117 was about — starts at 19px, below the label's 17.75px.\n * - `\"tiles\"` — 20px, keksdose's `FlagPicker` and location field (`pt-5`). A tile has\n * no inset fill: its selection IS its frame, a 2px dark border-and-ring from the\n * tile's top edge, so the tile has to start below the whole label line.\n */\nexport const LABEL_STRIP_PAD = { toggle: \"pt-4\", tiles: \"pt-5\" } as const;\n\n/**\n * The 11px label (and its `FieldHint`) over a group that is NOT a field — keksdose K2:\n * a segmented control or a row of swatches standing in a form row beside labelled\n * inputs, whose label must sit on the same line as theirs (`top-1`, `inset-x-3`, the\n * type of a field's static label) without the group growing a field's border round\n * it. `id` goes on the label so the group can be `aria-labelledby` it.\n *\n * `relative` is load-bearing twice over: the row is positioned against it, and a\n * caller's `sr-only` label cannot escape it (sr-only-containment).\n */\nexport function LabelStrip({\n labelId,\n label,\n hint,\n disabled,\n pad,\n className,\n children,\n}: {\n labelId: string;\n label: ReactNode;\n hint?: ReactNode;\n /** Dim the label with its group, as `peer-disabled:` dims a field's own. */\n disabled?: boolean;\n pad: keyof typeof LABEL_STRIP_PAD;\n className?: string;\n children: ReactNode;\n}) {\n return (\n <div className={cn(\"relative min-w-0\", LABEL_STRIP_PAD[pad], className)}>\n <div className=\"pointer-events-none absolute inset-x-3 top-1 flex items-center gap-1\">\n {/* `static`: FieldLabel positions itself absolutely; here the ROW is\n positioned, so the label and its \"?\" are centred on one line by the flex\n layout (dev#468). */}\n <FieldLabel id={labelId} className={cn(\"static min-w-0 max-w-full\", disabled && \"opacity-50\")}>\n {label}\n </FieldLabel>\n {hasContent(hint) && <span className=\"pointer-events-auto flex shrink-0 items-center\">{hint}</span>}\n </div>\n {children}\n </div>\n );\n}\n"],"mappings":";AAuEQ,mBAEI,KAFJ;AA7DR,SAAS,aAAa;AAEtB,SAAS,UAAU;AACnB,SAAS,kBAAkB;AAIpB,SAAS,WAAW,MAA0B;AACnD,SAAO,SAAS,UAAa,SAAS,QAAQ,SAAS,SAAS,SAAS;AAC3E;AAQO,SAAS,WAAW,MAA0B;AACnD,SAAQ,OAAO,SAAS,YAAY,SAAS,MAAO,OAAO,SAAS;AACtE;AAEA,MAAM,sBAAsB;AAC5B,MAAM,oBAAoB;AAgBnB,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMkB;AAChB,QAAM,SAAS,MAAM;AACrB,QAAM,UAAU,MAAM;AACtB,QAAM,WAAW,WAAW,IAAI;AAChC,QAAM,WAAW,WAAW,KAAK;AACjC,SAAO;AAAA,IACL,WAAW,QAAQ,OAAO,KAAK,YAAY,gBAAgB,QAAQ,gBAAgB;AAAA,IACnF,aACE,CAAC,aAAa,YAAY,QAAQ,YAAY,OAAO,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG,KAAK;AAAA,IACtF,OACE,YAAY,WACV,iCACG;AAAA,kBACC,oBAAC,OAAE,IAAI,QAAQ,WAAW,qBACvB,gBACH;AAAA,MAED,YACC,oBAAC,OAAE,IAAI,SAAS,WAAW,mBACxB,iBACH;AAAA,OAEJ,IACE;AAAA,IACN,WAAW,CAAC,YAAY,WAAW,IAAI,IAAI,OAAO;AAAA,EACpD;AACF;AASO,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,MAAI,UAAU,QAAQ,CAAC,QAAS,QAAO,gCAAG,UAAS;AACnD,SACE,qBAAC,SACE;AAAA;AAAA,IACA;AAAA,KACH;AAEJ;AAUO,SAAS,eAAe,EAAE,OAAO,KAAK,GAA2C;AACtF,MAAI,CAAC,WAAW,IAAI,EAAG,QAAO,oBAAC,cAAY,iBAAM;AACjD;AAAA;AAAA;AAAA,IAGE,qBAAC,SAAI,WAAU,iFACb;AAAA,0BAAC,cAAW,WAAU,6BAA6B,iBAAM;AAAA,MACzD,oBAAC,UAAK,WAAU,kDAAkD,gBAAK;AAAA,OACzE;AAAA;AAEJ;AAcO,MAAM,kBAAkB,EAAE,QAAQ,QAAQ,OAAO,OAAO;AAYxD,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GASG;AACD,SACE,qBAAC,SAAI,WAAW,GAAG,oBAAoB,gBAAgB,GAAG,GAAG,SAAS,GACpE;AAAA,yBAAC,SAAI,WAAU,wEAIb;AAAA,0BAAC,cAAW,IAAI,SAAS,WAAW,GAAG,6BAA6B,YAAY,YAAY,GACzF,iBACH;AAAA,MACC,WAAW,IAAI,KAAK,oBAAC,UAAK,WAAU,kDAAkD,gBAAK;AAAA,OAC9F;AAAA,IACC;AAAA,KACH;AAEJ;","names":[]}