@eifi1/ui-kit 0.21.0 → 0.23.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.
- package/README.md +42 -25
- package/dist/chart.d.ts +19 -14
- package/dist/components/account-chips.d.ts +10 -3
- package/dist/components/account-chips.js +5 -2
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/account-settings.d.ts +33 -2
- package/dist/components/account-settings.js +43 -21
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +53 -14
- package/dist/components/amount-input.js +17 -28
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +29 -0
- package/dist/components/autocomplete.js +134 -104
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +19 -14
- package/dist/components/calculator.d.ts +19 -14
- package/dist/components/checkbox-group.d.ts +104 -0
- package/dist/components/checkbox-group.js +111 -0
- package/dist/components/checkbox-group.js.map +1 -0
- package/dist/components/checkbox.d.ts +19 -0
- package/dist/components/checkbox.js +30 -7
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/chip.d.ts +18 -0
- package/dist/components/chip.js +47 -10
- package/dist/components/chip.js.map +1 -1
- package/dist/components/column-mapper.d.ts +182 -0
- package/dist/components/column-mapper.js +375 -0
- package/dist/components/column-mapper.js.map +1 -0
- package/dist/components/combobox-core.d.ts +39 -2
- package/dist/components/combobox-core.js +29 -6
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +91 -2
- package/dist/components/combobox.js +454 -308
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +69 -161
- package/dist/components/confirm-dialog.js +7 -8
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.d.ts +16 -11
- package/dist/components/country-select.d.ts +167 -0
- package/dist/components/country-select.js +293 -0
- package/dist/components/country-select.js.map +1 -0
- package/dist/components/currency-select.d.ts +17 -1
- package/dist/components/currency-select.js +56 -23
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +69 -166
- package/dist/components/danger-confirm.js +144 -50
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +1 -2
- package/dist/components/data-table-filter-popover.d.ts +1 -2
- package/dist/components/data-table-filters.d.ts +1 -2
- package/dist/components/data-table-labels.d.ts +4 -97
- package/dist/components/data-table-labels.js +9 -1
- package/dist/components/data-table-labels.js.map +1 -1
- package/dist/components/data-table-pagination.d.ts +3 -1
- package/dist/components/data-table.d.ts +1 -2
- package/dist/components/data-table.js +98 -3
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +66 -5
- package/dist/components/date-picker.js +378 -66
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +42 -1
- package/dist/components/entity-combobox.js +154 -113
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +19 -14
- package/dist/components/field-parts.d.ts +224 -0
- package/dist/components/field-parts.js +156 -0
- package/dist/components/field-parts.js.map +1 -0
- package/dist/components/field-strip.d.ts +105 -0
- package/dist/components/field-strip.js +46 -0
- package/dist/components/field-strip.js.map +1 -0
- package/dist/components/file-button.d.ts +19 -14
- package/dist/components/file-button.js +13 -3
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +41 -15
- package/dist/components/file-dropzone.js +57 -34
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/form-actions.d.ts +18 -13
- package/dist/components/form-actions.js +69 -5
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/iban-input.d.ts +69 -0
- package/dist/components/iban-input.js +125 -0
- package/dist/components/iban-input.js.map +1 -0
- package/dist/components/icon-picker.d.ts +21 -2
- package/dist/components/icon-picker.js +19 -4
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/inline-edit-field.d.ts +167 -0
- package/dist/components/inline-edit-field.js +293 -0
- package/dist/components/inline-edit-field.js.map +1 -0
- package/dist/components/language-select.d.ts +130 -0
- package/dist/components/language-select.js +18 -0
- package/dist/components/language-select.js.map +1 -0
- package/dist/components/line-items.d.ts +25 -3
- package/dist/components/line-items.js +55 -46
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.d.ts +25 -0
- package/dist/components/list.js +16 -5
- package/dist/components/list.js.map +1 -1
- package/dist/components/money-field.d.ts +32 -14
- package/dist/components/money-field.js.map +1 -1
- package/dist/components/month-picker.d.ts +52 -4
- package/dist/components/month-picker.js +291 -86
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +26 -1
- package/dist/components/multi-entity-combobox.js +143 -102
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +8 -1
- package/dist/components/multi-select.js +9 -4
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +34 -19
- package/dist/components/number-field.js +6 -20
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +79 -19
- package/dist/components/number-input.js +90 -38
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +19 -14
- package/dist/components/one-time-code-input.d.ts +100 -0
- package/dist/components/one-time-code-input.js +137 -0
- package/dist/components/one-time-code-input.js.map +1 -0
- package/dist/components/phone-input.d.ts +69 -0
- package/dist/components/phone-input.js +171 -0
- package/dist/components/phone-input.js.map +1 -0
- package/dist/components/reauth-dialog.d.ts +2 -2
- package/dist/components/reauth-dialog.js +6 -8
- package/dist/components/reauth-dialog.js.map +1 -1
- package/dist/components/series-chart.d.ts +19 -14
- package/dist/components/settings-fields.d.ts +19 -14
- package/dist/components/share-card.d.ts +16 -11
- package/dist/components/sign-chip.d.ts +66 -0
- package/dist/components/sign-chip.js +39 -0
- package/dist/components/sign-chip.js.map +1 -0
- package/dist/components/swatch-picker.d.ts +43 -2
- package/dist/components/swatch-picker.js +80 -32
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/switch.d.ts +19 -0
- package/dist/components/switch.js +24 -6
- package/dist/components/switch.js.map +1 -1
- package/dist/components/text-link.d.ts +18 -13
- package/dist/components/tile-radio.d.ts +88 -13
- package/dist/components/tile-radio.js +28 -11
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +24 -15
- package/dist/components/time-input.js.map +1 -1
- package/dist/components/toggle-group.d.ts +47 -1
- package/dist/components/toggle-group.js +110 -69
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/trigger-aria.d.ts +3 -0
- package/dist/components/trigger-aria.js +3 -1
- package/dist/components/trigger-aria.js.map +1 -1
- package/dist/components/ui.d.ts +16 -11
- package/dist/components/ui.js +284 -88
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -2
- package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
- package/dist/data-table.d.ts +1 -2
- package/dist/data-table.js +2 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +1 -1
- package/dist/feedback/feedback-attachment.js +348 -41
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +1 -1
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +3 -2
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +68 -253
- package/dist/feedback/feedback-thread.js +46 -10
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/{kit-labels-D1J2_jyD.d.ts → feedback-BxeQVzwq.d.ts} +1206 -20
- package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-fGAzZPf0.d.ts} +178 -11
- package/dist/feedback.d.ts +66 -2
- package/dist/hooks/use-file-drop.d.ts +19 -14
- package/dist/i18n/defaults.d.ts +19 -14
- package/dist/i18n/defaults.js +19 -2
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +19 -14
- package/dist/i18n/german.js +84 -5
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +15 -10
- package/dist/i18n/kit-labels.js +8 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +19 -14
- package/dist/i18n/locales/de-CH.d.ts +19 -14
- package/dist/i18n/locales/en.d.ts +19 -14
- package/dist/i18n/locales/en.js +16 -0
- package/dist/i18n/locales/en.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +19 -14
- package/dist/i18n/locales/es.js +83 -5
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +19 -14
- package/dist/i18n/locales/fr.js +82 -5
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +19 -14
- package/dist/i18n/locales/hu.js +85 -5
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +19 -14
- package/dist/i18n/locales/it.js +84 -5
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +19 -14
- package/dist/i18n/locales/zh.js +80 -5
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +19 -14
- package/dist/i18n/review.js +27 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +21 -10
- package/dist/index.js +73 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/column-mapping.d.ts +81 -0
- package/dist/lib/column-mapping.js +108 -0
- package/dist/lib/column-mapping.js.map +1 -0
- package/dist/lib/countries.d.ts +43 -0
- package/dist/lib/countries.js +39 -0
- package/dist/lib/countries.js.map +1 -0
- package/dist/lib/decimal-marks.d.ts +46 -0
- package/dist/lib/decimal-marks.js +40 -0
- package/dist/lib/decimal-marks.js.map +1 -0
- package/dist/lib/iban.d.ts +98 -0
- package/dist/lib/iban.js +154 -0
- package/dist/lib/iban.js.map +1 -0
- package/dist/lib/isin.d.ts +30 -0
- package/dist/lib/isin.js +36 -0
- package/dist/lib/isin.js.map +1 -0
- package/dist/lib/phone.d.ts +69 -0
- package/dist/lib/phone.js +121 -0
- package/dist/lib/phone.js.map +1 -0
- package/dist/lib/table-text.d.ts +109 -1
- package/dist/lib/table-text.js +122 -1
- package/dist/lib/table-text.js.map +1 -1
- package/dist/rhf/fields.d.ts +233 -18
- package/dist/rhf/fields.js +456 -3
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +19 -14
- package/dist/rhf.d.ts +22 -15
- package/dist/rhf.js.map +1 -1
- package/dist/shell/app-shell.d.ts +18 -13
- package/dist/shell/top-bar-brand.d.ts +19 -14
- package/dist/shell/topbar-action-menu.d.ts +36 -3
- package/dist/shell/topbar-action-menu.js +74 -33
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +18 -13
- package/dist/table-text.d.ts +1 -1
- package/dist/tour/tour.js +20 -24
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +49 -16
- package/dist/wizard/stepper-nav.js +4 -0
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard.d.ts +19 -14
- package/package.json +1 -1
- package/src/components/account-chips.tsx +17 -3
- package/src/components/account-settings.tsx +92 -26
- package/src/components/amount-input.tsx +68 -56
- package/src/components/autocomplete.tsx +198 -114
- package/src/components/checkbox-group.tsx +229 -0
- package/src/components/checkbox.tsx +72 -8
- package/src/components/chip.tsx +80 -10
- package/src/components/column-mapper.tsx +666 -0
- package/src/components/combobox-core.tsx +82 -9
- package/src/components/combobox.tsx +661 -340
- package/src/components/confirm-dialog.tsx +12 -8
- package/src/components/country-select.tsx +556 -0
- package/src/components/currency-select.tsx +109 -33
- package/src/components/danger-confirm.tsx +429 -82
- package/src/components/data-table-labels.ts +16 -1
- package/src/components/data-table.tsx +175 -6
- package/src/components/date-picker.tsx +595 -71
- package/src/components/entity-combobox.tsx +235 -124
- package/src/components/field-parts.tsx +403 -0
- package/src/components/field-strip.tsx +149 -0
- package/src/components/file-button.tsx +36 -3
- package/src/components/file-dropzone.tsx +106 -38
- package/src/components/form-actions.tsx +147 -6
- package/src/components/iban-input.tsx +231 -0
- package/src/components/icon-picker.tsx +47 -6
- package/src/components/inline-edit-field.tsx +533 -0
- package/src/components/language-select.tsx +78 -0
- package/src/components/line-items.tsx +35 -2
- package/src/components/list.tsx +55 -6
- package/src/components/money-field.tsx +13 -0
- package/src/components/month-picker.tsx +400 -63
- package/src/components/multi-entity-combobox.tsx +207 -120
- package/src/components/multi-select.tsx +80 -58
- package/src/components/number-field.tsx +28 -46
- package/src/components/number-input.tsx +210 -43
- package/src/components/one-time-code-input.tsx +288 -0
- package/src/components/phone-input.tsx +299 -0
- package/src/components/reauth-dialog.tsx +17 -18
- package/src/components/sign-chip.tsx +103 -0
- package/src/components/swatch-picker.tsx +143 -31
- package/src/components/switch.tsx +58 -7
- package/src/components/tile-radio.tsx +145 -29
- package/src/components/time-input.tsx +8 -1
- package/src/components/toggle-group.tsx +206 -73
- package/src/components/trigger-aria.ts +5 -0
- package/src/components/ui.tsx +479 -138
- package/src/data-table.ts +2 -1
- package/src/feedback/feedback-attachment.tsx +597 -46
- package/src/feedback/feedback-dialog.tsx +9 -3
- package/src/feedback/feedback-inbox.tsx +3 -2
- package/src/feedback/feedback-thread.tsx +113 -9
- package/src/i18n/defaults.ts +18 -1
- package/src/i18n/german.ts +89 -0
- package/src/i18n/kit-labels.tsx +82 -2
- package/src/i18n/locales/en.ts +36 -4
- package/src/i18n/locales/es.ts +87 -0
- package/src/i18n/locales/fr.ts +87 -0
- package/src/i18n/locales/hu.ts +83 -0
- package/src/i18n/locales/it.ts +88 -0
- package/src/i18n/locales/zh.ts +76 -0
- package/src/i18n/review.ts +26 -0
- package/src/index.ts +71 -3
- package/src/lib/column-mapping.ts +234 -0
- package/src/lib/countries.ts +91 -0
- package/src/lib/decimal-marks.ts +99 -0
- package/src/lib/iban.ts +171 -0
- package/src/lib/isin.ts +62 -0
- package/src/lib/phone.ts +211 -0
- package/src/lib/table-text.ts +271 -0
- package/src/rhf/fields.tsx +860 -9
- package/src/rhf.ts +3 -1
- package/src/shell/topbar-action-menu.tsx +134 -38
- package/src/tour/tour.tsx +66 -28
- package/src/wizard/stepper-nav.tsx +35 -1
package/src/rhf.ts
CHANGED
|
@@ -10,7 +10,9 @@ export * from "./rhf/form";
|
|
|
10
10
|
// The react-hook-form bridge to the wizard's Next gate (useWizard lives in /wizard).
|
|
11
11
|
export * from "./rhf/use-rhf-wizard-step";
|
|
12
12
|
// Bound fields — one line per field (RhfTextField, RhfNumberField, RhfIntegerField, RhfMoneyField,
|
|
13
|
-
// RhfDateField, RhfTextarea, RhfSelect, RhfCheckbox, RhfCombobox, RhfTextCombobox
|
|
13
|
+
// RhfDateField, RhfTextarea, RhfSelect, RhfCheckbox, RhfCombobox, RhfTextCombobox; 0.22: RhfTimeInput,
|
|
14
|
+
// RhfDateRangePicker, RhfToggleGroup, RhfIbanInput, RhfPhoneInput; 0.23: RhfCountrySelect,
|
|
15
|
+
// RhfMonthPicker)
|
|
14
16
|
// and RhfField, the shell they are built on.
|
|
15
17
|
export * from "./rhf/fields";
|
|
16
18
|
// LineItems over useFieldArray.
|
|
@@ -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,
|
|
15
|
-
* rows close the menu automatically when chosen.
|
|
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((
|
|
279
|
-
if (
|
|
280
|
-
|
|
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={
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
})}
|
package/src/tour/tour.tsx
CHANGED
|
@@ -261,6 +261,43 @@ function sameRect(a: Rect | null, b: Rect | null): boolean {
|
|
|
261
261
|
return a.top === b.top && a.left === b.left && a.width === b.width && a.height === b.height;
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
function rectOf(el: Element): Rect {
|
|
265
|
+
const r = el.getBoundingClientRect();
|
|
266
|
+
return { top: r.top, left: r.left, width: r.width, height: r.height };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* What the finder established for ONE step: where its target is (`null` = no target,
|
|
271
|
+
* or not on the page), and the reading direction the card takes from it.
|
|
272
|
+
*
|
|
273
|
+
* It carries the step it was found for, and the overlay only believes it for that
|
|
274
|
+
* step — which is the whole fix for keksdose's tours audit 2026-10-02, §1.0 A ("stale
|
|
275
|
+
* spotlight on the step after a spotlighted one", 15 steps in 16 tours, desktop and
|
|
276
|
+
* phone, mouse and keyboard). When step N spotlit a target and step N+1 had none (or
|
|
277
|
+
* its anchor was missing), N+1 kept N's hole and parked its card beside it, so a
|
|
278
|
+
* centred "You're all set" read as pointing at the Import & Export link; Back→Next kept
|
|
279
|
+
* it for as long as anyone watched.
|
|
280
|
+
*
|
|
281
|
+
* The overlay used to hold `rect` and `ready` as separate states and reset them
|
|
282
|
+
* DURING RENDER when the step changed. React 19 does not keep a render-phase update
|
|
283
|
+
* while an earlier, lower-priority update to the same hook is still queued — it only
|
|
284
|
+
* writes the result to the base state "unless the queue is empty" — and the follow
|
|
285
|
+
* interval below queues exactly such an update every 250 ms (eagerly bailed out because
|
|
286
|
+
* the rect had not moved, but left in the queue for rebasing). So the click's render
|
|
287
|
+
* showed the reset, and the very next render (the finder saying "ready") re-based on
|
|
288
|
+
* the old rect. A deep link was fine because nothing had queued anything yet.
|
|
289
|
+
*
|
|
290
|
+
* Keying the result to its step does not depend on any of that ordering: a rect found
|
|
291
|
+
* for another step is not this step's rect, whatever the queue does with it, and a
|
|
292
|
+
* missing anchor reads as "no target" instead of as the last thing that was lit.
|
|
293
|
+
*/
|
|
294
|
+
interface Located {
|
|
295
|
+
index: number;
|
|
296
|
+
step: TourStep;
|
|
297
|
+
rect: Rect | null;
|
|
298
|
+
dir: Direction;
|
|
299
|
+
}
|
|
300
|
+
|
|
264
301
|
function TourOverlay({
|
|
265
302
|
steps,
|
|
266
303
|
index,
|
|
@@ -279,33 +316,31 @@ function TourOverlay({
|
|
|
279
316
|
const step = steps[index];
|
|
280
317
|
const isFirst = index === 0;
|
|
281
318
|
const isLast = index === steps.length - 1;
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
setLocating({ index, step });
|
|
290
|
-
setReady(false);
|
|
291
|
-
setRect(null);
|
|
292
|
-
}
|
|
319
|
+
// A new step starts unlocated: whatever was found for another step (see `Located`)
|
|
320
|
+
// is ignored, so the overlay renders nothing until the effect below has located THIS
|
|
321
|
+
// step, and never the previous step's spotlight.
|
|
322
|
+
const [located, setLocated] = useState<Located | null>(null);
|
|
323
|
+
const current = located !== null && located.index === index && located.step === step ? located : null;
|
|
324
|
+
const ready = current !== null;
|
|
325
|
+
const rect = current?.rect ?? null;
|
|
293
326
|
// The overlay is portalled to <body>, out of whatever `dir` subtree the app set, so it
|
|
294
327
|
// takes the direction of the element it spotlights (the document's for a centered
|
|
295
328
|
// step). Arrow keys and start/end placement read it.
|
|
296
|
-
const
|
|
329
|
+
const dir: Direction = current?.dir ?? "ltr";
|
|
297
330
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
298
331
|
|
|
299
332
|
// Run beforeStep, then locate the target (retrying across route/render lag).
|
|
300
333
|
useEffect(() => {
|
|
301
334
|
let cancelled = false;
|
|
302
335
|
let raf = 0;
|
|
336
|
+
// `null` = untargeted, or the finder gave up → the card centres, with no hole.
|
|
337
|
+
const settle = (el: Element | null) =>
|
|
338
|
+
setLocated({ index, step, rect: el ? rectOf(el) : null, dir: dirOf(el) });
|
|
303
339
|
void (async () => {
|
|
304
340
|
await step.beforeStep?.();
|
|
305
341
|
if (cancelled) return;
|
|
306
342
|
if (!step.target) {
|
|
307
|
-
|
|
308
|
-
setReady(true);
|
|
343
|
+
settle(null);
|
|
309
344
|
return;
|
|
310
345
|
}
|
|
311
346
|
let tries = 0;
|
|
@@ -314,17 +349,11 @@ function TourOverlay({
|
|
|
314
349
|
const el = queryVisibleTarget(step.target!);
|
|
315
350
|
if (el) {
|
|
316
351
|
el.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
317
|
-
|
|
318
|
-
setRect({ top: r.top, left: r.left, width: r.width, height: r.height });
|
|
319
|
-
setDir(dirOf(el));
|
|
320
|
-
setReady(true);
|
|
352
|
+
settle(el);
|
|
321
353
|
return;
|
|
322
354
|
}
|
|
323
355
|
if (tries++ < 45) raf = requestAnimationFrame(find);
|
|
324
|
-
else
|
|
325
|
-
setDir(dirOf(null));
|
|
326
|
-
setReady(true); // give up → render centered
|
|
327
|
-
}
|
|
356
|
+
else settle(null); // give up → render centered
|
|
328
357
|
};
|
|
329
358
|
find();
|
|
330
359
|
})();
|
|
@@ -334,15 +363,19 @@ function TourOverlay({
|
|
|
334
363
|
};
|
|
335
364
|
}, [index, step]);
|
|
336
365
|
|
|
337
|
-
// Follow the target as the page scrolls / reflows
|
|
366
|
+
// Follow the target as the page scrolls / reflows — only once THIS step is located,
|
|
367
|
+
// and only into this step's result.
|
|
338
368
|
useEffect(() => {
|
|
339
369
|
if (!step.target) return;
|
|
340
370
|
const update = () => {
|
|
341
371
|
const el = queryVisibleTarget(step.target!);
|
|
342
372
|
if (el) {
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
373
|
+
const next = rectOf(el);
|
|
374
|
+
setLocated((prev) =>
|
|
375
|
+
prev && prev.index === index && prev.step === step && !sameRect(prev.rect, next)
|
|
376
|
+
? { ...prev, rect: next }
|
|
377
|
+
: prev,
|
|
378
|
+
);
|
|
346
379
|
}
|
|
347
380
|
};
|
|
348
381
|
window.addEventListener("scroll", update, true);
|
|
@@ -358,14 +391,19 @@ function TourOverlay({
|
|
|
358
391
|
// awaitClick: advance when the *real* target is clicked. The spotlight hole is
|
|
359
392
|
// click-through (see the shields in the render below) so the element's own
|
|
360
393
|
// handler runs first; we step forward on the next macrotask.
|
|
394
|
+
//
|
|
395
|
+
// Re-armed when the target turns up after the finder gave up (the follow effect
|
|
396
|
+
// lights it late): the card swaps Next for the "click the highlighted element" hint
|
|
397
|
+
// as soon as there is a rect, so the click must work from that moment too.
|
|
398
|
+
const spotted = rect !== null;
|
|
361
399
|
useEffect(() => {
|
|
362
|
-
if (!ready || !step.awaitClick || !step.target) return;
|
|
400
|
+
if (!ready || !spotted || !step.awaitClick || !step.target) return;
|
|
363
401
|
const el = queryVisibleTarget(step.target);
|
|
364
402
|
if (!el) return;
|
|
365
403
|
const onClick = () => window.setTimeout(() => onNext(), 0);
|
|
366
404
|
el.addEventListener("click", onClick, { once: true });
|
|
367
405
|
return () => el.removeEventListener("click", onClick);
|
|
368
|
-
}, [ready, index, step, onNext]);
|
|
406
|
+
}, [ready, spotted, index, step, onNext]);
|
|
369
407
|
|
|
370
408
|
// Keyboard: Esc skips, Enter and the reading-direction arrow advance (→ in LTR,
|
|
371
409
|
// ← in RTL), the other arrow goes back. On awaitClick steps Enter is ignored (it
|
|
@@ -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
|
>
|