@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.
- package/README.md +37 -23
- package/dist/components/alert-banner.d.ts +57 -5
- package/dist/components/alert-banner.js +63 -17
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +6 -0
- package/dist/components/breadcrumbs.d.ts +60 -0
- package/dist/components/breadcrumbs.js +88 -0
- package/dist/components/breadcrumbs.js.map +1 -0
- package/dist/components/bulk-action-bar.d.ts +112 -0
- package/dist/components/bulk-action-bar.js +196 -0
- package/dist/components/bulk-action-bar.js.map +1 -0
- package/dist/components/button-group.d.ts +58 -3
- package/dist/components/button-group.js +56 -5
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +6 -0
- package/dist/components/calendar-heatmap.d.ts +124 -0
- package/dist/components/calendar-heatmap.js +295 -0
- package/dist/components/calendar-heatmap.js.map +1 -0
- package/dist/components/chip.d.ts +31 -5
- package/dist/components/chip.js +85 -5
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +34 -2
- package/dist/components/choice-card.js +49 -0
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/copy-button.d.ts +14 -4
- package/dist/components/copy-button.js +17 -4
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/data-table.js +2 -0
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +22 -2
- package/dist/components/date-picker.js +11 -3
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.d.ts +43 -6
- package/dist/components/description-list.js +91 -11
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +7 -0
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.d.ts +57 -2
- package/dist/components/disclosure.js +24 -12
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/field.d.ts +43 -0
- package/dist/components/field.js +52 -0
- package/dist/components/field.js.map +1 -0
- package/dist/components/file-button.d.ts +1 -0
- package/dist/components/file-dropzone.d.ts +4 -4
- package/dist/components/file-dropzone.js +2 -1
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/floating-panel.d.ts +147 -4
- package/dist/components/floating-panel.js +188 -28
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/full-bleed-dialog.d.ts +23 -4
- package/dist/components/full-bleed-dialog.js +11 -2
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/list.d.ts +187 -0
- package/dist/components/list.js +221 -0
- package/dist/components/list.js.map +1 -0
- package/dist/components/menu-item.d.ts +109 -0
- package/dist/components/menu-item.js +88 -0
- package/dist/components/menu-item.js.map +1 -0
- package/dist/components/mini-calendar.d.ts +70 -5
- package/dist/components/mini-calendar.js +166 -62
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +23 -1
- package/dist/components/modal.js +36 -10
- package/dist/components/modal.js.map +1 -1
- package/dist/components/nav-pills.d.ts +66 -0
- package/dist/components/nav-pills.js +65 -0
- package/dist/components/nav-pills.js.map +1 -0
- package/dist/components/number-field.d.ts +6 -0
- package/dist/components/number-input.d.ts +6 -0
- package/dist/components/numpad-sheet.d.ts +6 -0
- package/dist/components/page-contents.js +1 -2
- package/dist/components/page-contents.js.map +1 -1
- package/dist/components/page-header.d.ts +37 -0
- package/dist/components/page-header.js +36 -0
- package/dist/components/page-header.js.map +1 -0
- package/dist/components/progress-bar.d.ts +31 -2
- package/dist/components/progress-bar.js +73 -7
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/series-chart-labels.d.ts +3 -0
- package/dist/components/series-chart-labels.js +2 -1
- package/dist/components/series-chart-labels.js.map +1 -1
- package/dist/components/series-chart.d.ts +32 -0
- package/dist/components/series-chart.js +141 -4
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.d.ts +1 -0
- package/dist/components/status-dot.d.ts +53 -0
- package/dist/components/status-dot.js +57 -0
- package/dist/components/status-dot.js.map +1 -0
- package/dist/components/table.d.ts +78 -13
- package/dist/components/table.js +84 -11
- package/dist/components/table.js.map +1 -1
- package/dist/components/text.d.ts +70 -0
- package/dist/components/text.js +24 -0
- package/dist/components/text.js.map +1 -0
- package/dist/components/time-input.d.ts +1 -0
- package/dist/components/toast.d.ts +167 -0
- package/dist/components/toast.js +226 -0
- package/dist/components/toast.js.map +1 -0
- package/dist/components/toggle-group.d.ts +44 -1
- package/dist/components/toggle-group.js +73 -4
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/tooltip.d.ts +46 -15
- package/dist/components/tooltip.js +87 -47
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +158 -7
- package/dist/components/ui.js +191 -10
- package/dist/components/ui.js.map +1 -1
- package/dist/components/user-avatar.d.ts +25 -3
- package/dist/components/user-avatar.js +32 -3
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/hooks/use-close-transition.d.ts +7 -1
- package/dist/hooks/use-close-transition.js +5 -2
- package/dist/hooks/use-close-transition.js.map +1 -1
- package/dist/hooks/use-copy-to-clipboard.js +1 -1
- package/dist/hooks/use-copy-to-clipboard.js.map +1 -1
- package/dist/hooks/use-file-drop.d.ts +1 -0
- package/dist/i18n/defaults.d.ts +6 -0
- package/dist/i18n/defaults.js +11 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +11 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +6 -0
- package/dist/i18n/locales/de-CH.d.ts +6 -0
- package/dist/i18n/locales/de-informal.d.ts +6 -0
- package/dist/i18n/locales/de.d.ts +6 -0
- package/dist/i18n/locales/de.js +30 -2
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +6 -0
- package/dist/i18n/locales/es.js +30 -2
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +6 -0
- package/dist/i18n/locales/fr.js +30 -2
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +6 -0
- package/dist/i18n/locales/hu.js +30 -2
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +6 -0
- package/dist/i18n/locales/it.js +30 -2
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +6 -0
- package/dist/i18n/locales/zh.js +30 -2
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +27 -15
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/clipping.d.ts +25 -0
- package/dist/lib/clipping.js +17 -0
- package/dist/lib/clipping.js.map +1 -0
- package/dist/lib/dates.d.ts +17 -1
- package/dist/lib/dates.js +19 -0
- package/dist/lib/dates.js.map +1 -1
- package/dist/rhf/form.d.ts +1 -0
- package/dist/rhf/use-rhf-wizard-step.d.ts +22 -0
- package/dist/rhf/use-rhf-wizard-step.js +38 -0
- package/dist/rhf/use-rhf-wizard-step.js.map +1 -0
- package/dist/rhf.d.ts +2 -0
- package/dist/rhf.js +1 -0
- package/dist/rhf.js.map +1 -1
- package/dist/search/command-palette.d.ts +19 -2
- package/dist/search/command-palette.js +16 -5
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.d.ts +26 -1
- package/dist/search/global-search.js +29 -13
- package/dist/search/global-search.js.map +1 -1
- package/dist/search.d.ts +1 -1
- package/dist/shell/topbar-action-menu.d.ts +73 -3
- package/dist/shell/topbar-action-menu.js +100 -27
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +2 -1
- package/dist/wizard/stepper-nav.d.ts +1 -0
- package/dist/wizard/stepper-nav.js +1 -1
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +2 -2
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +1 -1
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-context.d.ts +7 -1
- package/dist/wizard/wizard-context.js +4 -0
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-summary.js +3 -3
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/dist/wizard.d.ts +2 -1
- package/package.json +16 -12
- package/src/components/alert-banner.tsx +149 -21
- package/src/components/breadcrumbs.tsx +168 -0
- package/src/components/bulk-action-bar.tsx +362 -0
- package/src/components/button-group.tsx +124 -4
- package/src/components/calendar-heatmap.tsx +504 -0
- package/src/components/chip.tsx +128 -7
- package/src/components/choice-card.tsx +97 -1
- package/src/components/copy-button.tsx +29 -7
- package/src/components/data-table.tsx +7 -0
- package/src/components/date-picker.tsx +49 -1
- package/src/components/description-list.tsx +168 -15
- package/src/components/dialog-frame.tsx +7 -0
- package/src/components/disclosure.tsx +93 -21
- package/src/components/field.tsx +137 -0
- package/src/components/file-dropzone.tsx +9 -10
- package/src/components/floating-panel.tsx +366 -15
- package/src/components/full-bleed-dialog.tsx +42 -5
- package/src/components/list.tsx +443 -0
- package/src/components/menu-item.tsx +235 -0
- package/src/components/mini-calendar.tsx +245 -54
- package/src/components/modal.tsx +80 -17
- package/src/components/nav-pills.tsx +145 -0
- package/src/components/page-contents.tsx +4 -4
- package/src/components/page-header.tsx +68 -0
- package/src/components/progress-bar.tsx +129 -11
- package/src/components/series-chart-labels.ts +4 -0
- package/src/components/series-chart.tsx +274 -3
- package/src/components/status-dot.tsx +107 -0
- package/src/components/table.tsx +176 -17
- package/src/components/text.tsx +97 -0
- package/src/components/toast.tsx +441 -0
- package/src/components/toggle-group.tsx +128 -4
- package/src/components/tooltip.tsx +200 -100
- package/src/components/ui.tsx +381 -14
- package/src/components/user-avatar.tsx +57 -3
- package/src/hooks/use-close-transition.ts +14 -5
- package/src/hooks/use-copy-to-clipboard.ts +1 -1
- package/src/i18n/defaults.ts +10 -0
- package/src/i18n/kit-labels.tsx +10 -0
- package/src/i18n/locales/de.ts +29 -0
- package/src/i18n/locales/es.ts +29 -0
- package/src/i18n/locales/fr.ts +29 -0
- package/src/i18n/locales/hu.ts +29 -0
- package/src/i18n/locales/it.ts +29 -0
- package/src/i18n/locales/zh.ts +29 -0
- package/src/index.ts +31 -0
- package/src/lib/clipping.ts +34 -0
- package/src/lib/dates.ts +37 -0
- package/src/rhf/use-rhf-wizard-step.ts +113 -0
- package/src/rhf.ts +2 -0
- package/src/search/command-palette.tsx +42 -5
- package/src/search/global-search.tsx +59 -18
- package/src/shell/topbar-action-menu.tsx +239 -49
- package/src/wizard/stepper-nav.tsx +2 -1
- package/src/wizard/types.ts +2 -2
- package/src/wizard/use-wizard.ts +3 -3
- package/src/wizard/wizard-context.tsx +9 -0
- package/src/wizard/wizard-summary.tsx +14 -10
- package/tokens.css +97 -0
|
@@ -1,20 +1,116 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useLayoutEffect, useRef } from "react";
|
|
2
|
+
import type { KeyboardEvent as ReactKeyboardEvent, ReactNode } from "react";
|
|
2
3
|
import { Link } from "react-router";
|
|
3
4
|
import { cn } from "../lib/cn";
|
|
4
5
|
import { HoverMenu } from "../components/hover-menu";
|
|
5
|
-
import {
|
|
6
|
+
import { MenuItem } from "../components/menu-item";
|
|
7
|
+
import type { MenuItemTone } from "../components/menu-item";
|
|
8
|
+
import { TOPBAR_TRIGGER_CLASS } from "./topbar-controls";
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
* One row of a {@link TopBarActionMenu}. An `action` (default) runs `onSelect`, a
|
|
9
12
|
* `link` navigates via react-router, and a `divider` draws a rule. Non-divider
|
|
10
13
|
* rows close the menu automatically when chosen. `kind` may be omitted for
|
|
11
14
|
* actions, keeping the common case terse.
|
|
15
|
+
*
|
|
16
|
+
* The rows are {@link MenuItem}s, so they take its options: `tone: "danger"` for the
|
|
17
|
+
* log-out row, `checked` (+ `checkable`) for a choice, `current` for the link to the
|
|
18
|
+
* page already open, `disabled`.
|
|
12
19
|
*/
|
|
13
20
|
export type TopBarMenuEntry =
|
|
14
|
-
| {
|
|
15
|
-
|
|
21
|
+
| {
|
|
22
|
+
kind?: "action";
|
|
23
|
+
key: string;
|
|
24
|
+
icon?: ReactNode;
|
|
25
|
+
label: ReactNode;
|
|
26
|
+
trailing?: ReactNode;
|
|
27
|
+
onSelect: () => void;
|
|
28
|
+
tone?: MenuItemTone;
|
|
29
|
+
/** Makes the row a choice (`menuitemradio`, or `menuitemcheckbox` with
|
|
30
|
+
* `checkable: "checkbox"`) and says whether it is chosen. */
|
|
31
|
+
checked?: boolean;
|
|
32
|
+
checkable?: "radio" | "checkbox";
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
}
|
|
35
|
+
| {
|
|
36
|
+
kind: "link";
|
|
37
|
+
key: string;
|
|
38
|
+
icon?: ReactNode;
|
|
39
|
+
label: ReactNode;
|
|
40
|
+
trailing?: ReactNode;
|
|
41
|
+
to: string;
|
|
42
|
+
tone?: MenuItemTone;
|
|
43
|
+
/** The link to the page already open: `aria-current="page"` and a check mark. */
|
|
44
|
+
current?: boolean;
|
|
45
|
+
disabled?: boolean;
|
|
46
|
+
}
|
|
16
47
|
| { kind: "divider"; key: string };
|
|
17
48
|
|
|
49
|
+
/**
|
|
50
|
+
* The non-interactive identity block at the top of an account menu — kastlan's name +
|
|
51
|
+
* role, keksdose's display name + email + role chip. It is text, not a `menuitem`: the
|
|
52
|
+
* arrow keys pass over it.
|
|
53
|
+
*/
|
|
54
|
+
export interface TopBarMenuHeader {
|
|
55
|
+
/** The first line — the user's name. */
|
|
56
|
+
title: ReactNode;
|
|
57
|
+
/** The muted second line — an email, a role. */
|
|
58
|
+
subtitle?: ReactNode;
|
|
59
|
+
/** Anything below — a role chip. A link or button here stays out of the arrow keys
|
|
60
|
+
* and in the Tab order, like the footer's. */
|
|
61
|
+
extra?: ReactNode;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** What a control is, as far as `HoverMenu` is concerned, when it lives in the header
|
|
65
|
+
* or the footer: not a menu row. */
|
|
66
|
+
const SECTION_CONTROLS = "a[href]:not([role]),button:not([role])";
|
|
67
|
+
const TABBABLE = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]';
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A header or footer inside the menu panel.
|
|
71
|
+
*
|
|
72
|
+
* `HoverMenu` promotes EVERY role-less button and link in its panel to a `menuitem` out
|
|
73
|
+
* of the Tab order, and turns every list into `role="none"` — right for the rows, wrong
|
|
74
|
+
* for a footer of legal links, which would join the arrow-key cycle and drop out of Tab.
|
|
75
|
+
* So before it looks (a child's layout effect runs before its parent's), the controls
|
|
76
|
+
* in here get their own role spelled out, which `HoverMenu` leaves alone, and any list
|
|
77
|
+
* keeps its list semantics.
|
|
78
|
+
*/
|
|
79
|
+
function MenuSection({ className, children }: { className: string; children: ReactNode }) {
|
|
80
|
+
const ref = useRef<HTMLLIElement>(null);
|
|
81
|
+
useLayoutEffect(() => {
|
|
82
|
+
const el = ref.current;
|
|
83
|
+
if (!el) return;
|
|
84
|
+
for (const c of el.querySelectorAll<HTMLElement>(SECTION_CONTROLS)) {
|
|
85
|
+
c.setAttribute("role", c.tagName === "A" ? "link" : "button");
|
|
86
|
+
}
|
|
87
|
+
for (const l of el.querySelectorAll<HTMLElement>("ul:not([role]),ol:not([role])")) l.setAttribute("role", "list");
|
|
88
|
+
for (const l of el.querySelectorAll<HTMLElement>("li:not([role])")) l.setAttribute("role", "listitem");
|
|
89
|
+
});
|
|
90
|
+
return (
|
|
91
|
+
<li ref={ref} role="none" className={className}>
|
|
92
|
+
{children}
|
|
93
|
+
</li>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* `HoverMenu` closes on any Tab inside the panel — right when Tab leaves it, wrong when
|
|
99
|
+
* Tab is on its way from a row to a footer link, or from one footer link to the next:
|
|
100
|
+
* the panel would unmount under the focus. So a Tab whose next stop is still inside the
|
|
101
|
+
* panel stops here and the browser moves focus; one that leaves reaches `HoverMenu`,
|
|
102
|
+
* which closes.
|
|
103
|
+
*/
|
|
104
|
+
function keepTabInsidePanel(e: ReactKeyboardEvent<HTMLUListElement>) {
|
|
105
|
+
if (e.key !== "Tab") return;
|
|
106
|
+
const from = e.target as HTMLElement;
|
|
107
|
+
const stops = Array.from(e.currentTarget.querySelectorAll<HTMLElement>(TABBABLE)).filter(
|
|
108
|
+
(el) => el.getAttribute("tabindex") !== "-1",
|
|
109
|
+
);
|
|
110
|
+
const way = e.shiftKey ? Node.DOCUMENT_POSITION_PRECEDING : Node.DOCUMENT_POSITION_FOLLOWING;
|
|
111
|
+
if (stops.some((el) => el !== from && from.compareDocumentPosition(el) & way)) e.stopPropagation();
|
|
112
|
+
}
|
|
113
|
+
|
|
18
114
|
/**
|
|
19
115
|
* A top-bar icon button that opens a {@link HoverMenu} of actions and/or links,
|
|
20
116
|
* with an optional uppercase heading and divider rules. Generic glue over the
|
|
@@ -22,70 +118,164 @@ export type TopBarMenuEntry =
|
|
|
22
118
|
* so those call sites describe their rows as data instead of rebuilding the
|
|
23
119
|
* trigger + list markup. For a checkmarked value-switcher use
|
|
24
120
|
* {@link OptionSwitcherMenu} instead.
|
|
121
|
+
*
|
|
122
|
+
* It is also the ACCOUNT menu kastlan and keksdose each hand-build on `HoverMenu`: a
|
|
123
|
+
* custom `trigger` (the avatar), a `header` (name + role / email), the rows, and a
|
|
124
|
+
* free-content `footer` (the legal links).
|
|
25
125
|
*/
|
|
26
126
|
export function TopBarActionMenu({
|
|
27
127
|
icon,
|
|
128
|
+
trigger,
|
|
129
|
+
triggerClassName,
|
|
28
130
|
ariaLabel,
|
|
29
131
|
heading,
|
|
132
|
+
header,
|
|
30
133
|
entries,
|
|
134
|
+
children,
|
|
135
|
+
footer,
|
|
136
|
+
footerLabel,
|
|
137
|
+
align,
|
|
31
138
|
panelClassName,
|
|
32
139
|
}: {
|
|
33
|
-
icon
|
|
140
|
+
/** The trigger's face for the usual icon button. Ignored when `trigger` is given. */
|
|
141
|
+
icon?: ReactNode;
|
|
142
|
+
/**
|
|
143
|
+
* A face of your own for the trigger — the account menu's `UserAvatar`. It is drawn
|
|
144
|
+
* INSIDE the kit's button, which keeps the wiring: `aria-haspopup`, `aria-expanded`,
|
|
145
|
+
* the toggle, the keyboard. `open` is there for a chevron that turns.
|
|
146
|
+
*
|
|
147
|
+
* The button is named by `ariaLabel` followed by any readable text in the face — a
|
|
148
|
+
* `UserAvatar` `badge` label reaches a screen reader that way ("Account menu 3
|
|
149
|
+
* unread"), where an `aria-label` on the button would have hidden it. A
|
|
150
|
+
* `UserAvatar`'s initials are `aria-hidden`, so the person's name is only in the
|
|
151
|
+
* name if `ariaLabel` carries it ("Marcel Eifert, account menu").
|
|
152
|
+
*/
|
|
153
|
+
trigger?: (state: { open: boolean }) => ReactNode;
|
|
154
|
+
/** Extra classes for the trigger button — `rounded-full` round an avatar. */
|
|
155
|
+
triggerClassName?: string;
|
|
156
|
+
/** Names the trigger and the menu. */
|
|
34
157
|
ariaLabel: string;
|
|
35
158
|
heading?: string;
|
|
159
|
+
/** A non-interactive identity block above the rows. */
|
|
160
|
+
header?: TopBarMenuHeader;
|
|
36
161
|
entries: TopBarMenuEntry[];
|
|
162
|
+
/**
|
|
163
|
+
* Rows the entry data cannot describe (a `Disclosure` sub-list, an app's own
|
|
164
|
+
* section), after `entries`. Render `<li>`s holding `MenuItem`s or plain controls;
|
|
165
|
+
* `close` shuts the menu.
|
|
166
|
+
*/
|
|
167
|
+
children?: (close: () => void) => ReactNode;
|
|
168
|
+
/**
|
|
169
|
+
* Free content below the rows, behind a rule — the legal links. Its links and
|
|
170
|
+
* buttons are NOT menu rows: the arrow keys skip them and Tab reaches them. Pass a
|
|
171
|
+
* function to get `close` for the links' `onClick`.
|
|
172
|
+
*/
|
|
173
|
+
footer?: ReactNode | ((close: () => void) => ReactNode);
|
|
174
|
+
/** Wraps the footer in a `<nav>` with this name ("Legal"). Without it, a plain block. */
|
|
175
|
+
footerLabel?: string;
|
|
176
|
+
/** Which edge of the trigger the panel lines up with (see `HoverMenu`). Default `end`. */
|
|
177
|
+
align?: "start" | "end" | "left" | "right";
|
|
37
178
|
panelClassName?: string;
|
|
38
179
|
}) {
|
|
39
180
|
return (
|
|
40
181
|
<HoverMenu
|
|
41
182
|
ariaLabel={ariaLabel}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
183
|
+
align={align}
|
|
184
|
+
trigger={({ open, toggle }) =>
|
|
185
|
+
trigger ? (
|
|
186
|
+
<button type="button" onClick={toggle} className={cn(TOPBAR_TRIGGER_CLASS, "relative", triggerClassName)}>
|
|
187
|
+
<span className="sr-only">{ariaLabel}</span>
|
|
188
|
+
{/* The space keeps the name from running into the face's text ("Account
|
|
189
|
+
menu3 unread"); as a flex item it takes no room. */}
|
|
190
|
+
{" "}
|
|
191
|
+
{trigger({ open })}
|
|
192
|
+
</button>
|
|
193
|
+
) : (
|
|
194
|
+
<button
|
|
195
|
+
type="button"
|
|
196
|
+
onClick={toggle}
|
|
197
|
+
aria-label={ariaLabel}
|
|
198
|
+
className={cn(TOPBAR_TRIGGER_CLASS, triggerClassName)}
|
|
199
|
+
>
|
|
200
|
+
{icon}
|
|
201
|
+
</button>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
47
204
|
>
|
|
48
|
-
{(close) =>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
)}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
<span className="flex w-full items-center gap-2">
|
|
61
|
-
{entry.icon}
|
|
62
|
-
{entry.label}
|
|
63
|
-
{entry.trailing && <span className="ms-auto ps-2">{entry.trailing}</span>}
|
|
64
|
-
</span>
|
|
65
|
-
);
|
|
66
|
-
return (
|
|
67
|
-
<li key={entry.key}>
|
|
68
|
-
{entry.kind === "link" ? (
|
|
69
|
-
<Link to={entry.to} onClick={close} className={TOPBAR_MENU_ITEM_CLASS}>
|
|
70
|
-
{content}
|
|
71
|
-
</Link>
|
|
72
|
-
) : (
|
|
73
|
-
<button
|
|
74
|
-
type="button"
|
|
75
|
-
onClick={() => {
|
|
76
|
-
entry.onSelect();
|
|
77
|
-
close();
|
|
78
|
-
}}
|
|
79
|
-
className={TOPBAR_MENU_ITEM_CLASS}
|
|
80
|
-
>
|
|
81
|
-
{content}
|
|
82
|
-
</button>
|
|
205
|
+
{(close) => {
|
|
206
|
+
const footerBody = typeof footer === "function" ? footer(close) : footer;
|
|
207
|
+
const footerClass =
|
|
208
|
+
"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)]";
|
|
209
|
+
return (
|
|
210
|
+
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a Tab bubbling up from a row or footer link, see keepTabInsidePanel
|
|
211
|
+
<ul className={cn("py-1", panelClassName)} onKeyDown={keepTabInsidePanel}>
|
|
212
|
+
{header && (
|
|
213
|
+
<MenuSection className="mb-1 border-b border-[var(--border)] px-3 py-2">
|
|
214
|
+
<div className="truncate text-sm font-medium text-[var(--text-primary)]">{header.title}</div>
|
|
215
|
+
{header.subtitle != null && (
|
|
216
|
+
<div className="truncate text-xs text-[var(--text-muted)]">{header.subtitle}</div>
|
|
83
217
|
)}
|
|
218
|
+
{header.extra != null && <div className="mt-1">{header.extra}</div>}
|
|
219
|
+
</MenuSection>
|
|
220
|
+
)}
|
|
221
|
+
{heading && (
|
|
222
|
+
<li className="px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-[var(--text-placeholder)]">
|
|
223
|
+
{heading}
|
|
84
224
|
</li>
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
225
|
+
)}
|
|
226
|
+
{entries.map((entry) => {
|
|
227
|
+
if (entry.kind === "divider") {
|
|
228
|
+
return <li key={entry.key} className="my-1 border-t border-[var(--border)]" />;
|
|
229
|
+
}
|
|
230
|
+
return (
|
|
231
|
+
<li key={entry.key}>
|
|
232
|
+
{entry.kind === "link" ? (
|
|
233
|
+
<MenuItem
|
|
234
|
+
href={entry.to}
|
|
235
|
+
renderLink={({ href, ...p }) => <Link to={href} {...p} />}
|
|
236
|
+
onClick={close}
|
|
237
|
+
leading={entry.icon}
|
|
238
|
+
trailing={entry.trailing}
|
|
239
|
+
tone={entry.tone}
|
|
240
|
+
current={entry.current}
|
|
241
|
+
disabled={entry.disabled}
|
|
242
|
+
>
|
|
243
|
+
{entry.label}
|
|
244
|
+
</MenuItem>
|
|
245
|
+
) : (
|
|
246
|
+
<MenuItem
|
|
247
|
+
onClick={() => {
|
|
248
|
+
entry.onSelect();
|
|
249
|
+
close();
|
|
250
|
+
}}
|
|
251
|
+
leading={entry.icon}
|
|
252
|
+
trailing={entry.trailing}
|
|
253
|
+
tone={entry.tone}
|
|
254
|
+
checked={entry.checked}
|
|
255
|
+
checkable={entry.checkable}
|
|
256
|
+
disabled={entry.disabled}
|
|
257
|
+
>
|
|
258
|
+
{entry.label}
|
|
259
|
+
</MenuItem>
|
|
260
|
+
)}
|
|
261
|
+
</li>
|
|
262
|
+
);
|
|
263
|
+
})}
|
|
264
|
+
{children?.(close)}
|
|
265
|
+
{footerBody != null && footerBody !== false && (
|
|
266
|
+
<MenuSection className="mt-1 border-t border-[var(--border)] px-3 py-2">
|
|
267
|
+
{footerLabel ? (
|
|
268
|
+
<nav aria-label={footerLabel} className={footerClass}>
|
|
269
|
+
{footerBody}
|
|
270
|
+
</nav>
|
|
271
|
+
) : (
|
|
272
|
+
<div className={footerClass}>{footerBody}</div>
|
|
273
|
+
)}
|
|
274
|
+
</MenuSection>
|
|
275
|
+
)}
|
|
276
|
+
</ul>
|
|
277
|
+
);
|
|
278
|
+
}}
|
|
89
279
|
</HoverMenu>
|
|
90
280
|
);
|
|
91
281
|
}
|
|
@@ -107,7 +107,8 @@ export function StepperNav<TData extends Record<string, unknown>>({
|
|
|
107
107
|
const pending = wizard.isValidating;
|
|
108
108
|
const stepNextLabel = wizard.currentStep.nextLabel;
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
// Every branch below assigns it; no initial value to overwrite.
|
|
111
|
+
let forwardButton: ReactNode;
|
|
111
112
|
if (wizard.isCommitStep) {
|
|
112
113
|
const finishButton = (
|
|
113
114
|
<Button
|
package/src/wizard/types.ts
CHANGED
|
@@ -109,8 +109,8 @@ export interface UseWizardOptions<TData extends Record<string, unknown>> {
|
|
|
109
109
|
*/
|
|
110
110
|
missingRequiredMessage?: string;
|
|
111
111
|
/**
|
|
112
|
-
* How to surface {@link missingRequiredMessage}. Defaults to
|
|
113
|
-
* (an optional peer,
|
|
112
|
+
* How to surface {@link missingRequiredMessage}. Defaults to the kit's `toast.error`
|
|
113
|
+
* (sonner, an optional peer, is loaded only when a toast is shown). Pass your own to
|
|
114
114
|
* route it somewhere else, or to a no-op to silence it.
|
|
115
115
|
*/
|
|
116
116
|
onValidationFailed?: (message: string) => void;
|
package/src/wizard/use-wizard.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { useCallback, useEffect, useReducer, useRef, useState } from "react";
|
|
|
18
18
|
import { useSearchParams } from "react-router";
|
|
19
19
|
import { useKitLabels } from "../i18n/kit-labels";
|
|
20
20
|
import { DEFAULT_WIZARD_LABELS } from "./types";
|
|
21
|
+
import { toast } from "../components/toast";
|
|
21
22
|
import type {
|
|
22
23
|
FieldErrors,
|
|
23
24
|
StepStatus,
|
|
@@ -312,9 +313,8 @@ export function useWizard<TData extends Record<string, unknown>>(
|
|
|
312
313
|
if (onValidationFailed) {
|
|
313
314
|
onValidationFailed(message);
|
|
314
315
|
} else {
|
|
315
|
-
// sonner
|
|
316
|
-
//
|
|
317
|
-
const { toast } = await import("sonner");
|
|
316
|
+
// The kit's `toast` loads sonner (an optional peer) lazily, so an app
|
|
317
|
+
// that never trips this never has to install it.
|
|
318
318
|
toast.error(message);
|
|
319
319
|
}
|
|
320
320
|
}
|
|
@@ -27,3 +27,12 @@ export function useWizardContext(): WizardContextValue {
|
|
|
27
27
|
}
|
|
28
28
|
return ctx;
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The wizard context, or `null` outside a `<StepperNav>`. For a hook that registers
|
|
33
|
+
* with the wizard when there is one and works without it — `useRhfWizardStep` in
|
|
34
|
+
* `@eifi1/ui-kit/rhf`, whose returned `validate` a custom nav can call itself.
|
|
35
|
+
*/
|
|
36
|
+
export function useOptionalWizardContext(): WizardContextValue | null {
|
|
37
|
+
return useContext(WizardContext);
|
|
38
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Pencil } from "lucide-react";
|
|
2
2
|
import { Card, CardAction, CardContent, CardHeader, CardTitle, IconButton } from "../components/ui";
|
|
3
|
+
import { Tooltip } from "../components/tooltip";
|
|
3
4
|
import type { SummarySection, WizardLabels } from "./types";
|
|
4
5
|
import { DEFAULT_WIZARD_LABELS } from "./types";
|
|
5
6
|
import { useKitLabels } from "../i18n/kit-labels";
|
|
@@ -44,16 +45,19 @@ export function WizardSummary({
|
|
|
44
45
|
<CardTitle className="text-base">{section.label}</CardTitle>
|
|
45
46
|
{section.stepIndex !== undefined && onEditStep && (
|
|
46
47
|
<CardAction>
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
{/* The kit Tooltip, not a native `title`: it shows on keyboard focus too,
|
|
49
|
+
and an app with a one-tooltip rule (keksdose dev#523) sees one. */}
|
|
50
|
+
<Tooltip label={l.edit}>
|
|
51
|
+
<IconButton
|
|
52
|
+
size="sm"
|
|
53
|
+
variant="ghost"
|
|
54
|
+
aria-label={l.edit}
|
|
55
|
+
disabled={disabled}
|
|
56
|
+
onClick={() => onEditStep(section.stepIndex as number)}
|
|
57
|
+
>
|
|
58
|
+
<Pencil />
|
|
59
|
+
</IconButton>
|
|
60
|
+
</Tooltip>
|
|
57
61
|
</CardAction>
|
|
58
62
|
)}
|
|
59
63
|
</CardHeader>
|
package/tokens.css
CHANGED
|
@@ -233,6 +233,103 @@
|
|
|
233
233
|
--success-bg: rgba(6, 78, 59, 0.25);
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
/* ── Toasts ─────────────────────────────────────────────────────────────────
|
|
237
|
+
The kit `<Toaster>` (components/toast.tsx) paints sonner with the AlertBanner
|
|
238
|
+
tones. These are ALIASES, not new colours, and they exist for one reason: sonner
|
|
239
|
+
declares its own `--success-bg` / `--success-border` (and info, warning) on the
|
|
240
|
+
toaster element, so that element cannot read the kit's value under the same name
|
|
241
|
+
— `--success-bg: var(--success-bg)` is a cycle. Declared here, they are computed
|
|
242
|
+
on `<html>` and inherited down under a name sonner does not touch.
|
|
243
|
+
|
|
244
|
+
The background is the tone's wash layered on the surface, as AlertBanner's
|
|
245
|
+
`elevated` frame does: the dark washes are translucent, and a toast floats over
|
|
246
|
+
content. `--z-toast` is above every kit overlay (modal and sheets z-50, tour
|
|
247
|
+
z-60/61, command palette z-70) and above whatever an app stacks on top of those —
|
|
248
|
+
a toast confirming a dialog's action must not land under the dialog. */
|
|
249
|
+
:root,
|
|
250
|
+
.dark {
|
|
251
|
+
--z-toast: 2147483647;
|
|
252
|
+
--toast-success-bg: linear-gradient(var(--success-bg), var(--success-bg)) var(--bg-surface);
|
|
253
|
+
--toast-success-border: var(--success-border);
|
|
254
|
+
--toast-info-bg: linear-gradient(var(--info-bg), var(--info-bg)) var(--bg-surface);
|
|
255
|
+
--toast-info-border: var(--info-border);
|
|
256
|
+
--toast-warning-bg: linear-gradient(var(--warning-bg), var(--warning-bg)) var(--bg-surface);
|
|
257
|
+
--toast-warning-border: var(--warning-border);
|
|
258
|
+
--toast-danger-bg: linear-gradient(var(--danger-bg), var(--danger-bg)) var(--bg-surface);
|
|
259
|
+
--toast-danger-border: var(--danger-border);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* The description of an untoned toast in the secondary text colour (sonner hard-codes
|
|
263
|
+
#3f3f3f, unreadable in the dark theme); a toned toast's inherits its tone. */
|
|
264
|
+
.kit-toaster [data-sonner-toast][data-styled="true"]:not([data-type="success"], [data-type="info"], [data-type="warning"], [data-type="error"]) [data-description] {
|
|
265
|
+
color: var(--text-secondary);
|
|
266
|
+
}
|
|
267
|
+
/* The action button in the tone's own colour rather than sonner's black pill. */
|
|
268
|
+
.kit-toaster [data-sonner-toast][data-styled="true"] [data-button]:not([data-cancel]) {
|
|
269
|
+
background: var(--toast-action-bg, var(--text-primary));
|
|
270
|
+
color: var(--bg-surface);
|
|
271
|
+
}
|
|
272
|
+
.kit-toaster [data-sonner-toast][data-type="success"] { --toast-action-bg: var(--success); }
|
|
273
|
+
.kit-toaster [data-sonner-toast][data-type="info"] { --toast-action-bg: var(--info); }
|
|
274
|
+
.kit-toaster [data-sonner-toast][data-type="warning"] { --toast-action-bg: var(--warning); }
|
|
275
|
+
.kit-toaster [data-sonner-toast][data-type="error"] { --toast-action-bg: var(--danger); }
|
|
276
|
+
/* The kit's focus ring on the toast's buttons — sonner's is a black shadow that
|
|
277
|
+
vanishes on the dark theme. */
|
|
278
|
+
.kit-toaster [data-sonner-toast][data-styled="true"] :is([data-button], [data-close-button]):focus-visible {
|
|
279
|
+
outline: 2px solid var(--brand);
|
|
280
|
+
outline-offset: 2px;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/* ── Categorical hues ───────────────────────────────────────────────────────
|
|
284
|
+
Colours that MEAN nothing but "this is a different value": kastlan's status
|
|
285
|
+
enums (status-badge.tsx over enum-meta.ts), where ~15 enums map to 9 Tailwind
|
|
286
|
+
hues. Four of those are semantic and already have tokens — green = --success,
|
|
287
|
+
yellow = --warning, red = --danger, gray = the neutral chip — so only the five
|
|
288
|
+
below are new: blue (open / sent / rented), purple (assigned / accepted),
|
|
289
|
+
orange (terminated / high urgency), teal (invoiced) and indigo (a meeting
|
|
290
|
+
stage). The smallest set that covers every enum; deliberately not a ramp.
|
|
291
|
+
|
|
292
|
+
Same triple and same shades as the status family (700 text / 300 border /
|
|
293
|
+
100 surface in light, 300 text / translucent 700 border / 900 wash in dark),
|
|
294
|
+
so a categorical chip sits beside a semantic one at the same weight. Not in
|
|
295
|
+
`TokenSet`: like `--danger`, a hue is a specific colour, not a tint of the
|
|
296
|
+
palette preset, so it lives here and nowhere else. */
|
|
297
|
+
:root {
|
|
298
|
+
--hue-blue: #1d4ed8; /* blue-700 */
|
|
299
|
+
--hue-blue-border: #93c5fd; /* blue-300 */
|
|
300
|
+
--hue-blue-bg: #dbeafe; /* blue-100 */
|
|
301
|
+
--hue-indigo: #4338ca; /* indigo-700 */
|
|
302
|
+
--hue-indigo-border: #a5b4fc; /* indigo-300 */
|
|
303
|
+
--hue-indigo-bg: #e0e7ff; /* indigo-100 */
|
|
304
|
+
--hue-purple: #7e22ce; /* purple-700 */
|
|
305
|
+
--hue-purple-border: #d8b4fe; /* purple-300 */
|
|
306
|
+
--hue-purple-bg: #f3e8ff; /* purple-100 */
|
|
307
|
+
--hue-teal: #0f766e; /* teal-700 */
|
|
308
|
+
--hue-teal-border: #5eead4; /* teal-300 */
|
|
309
|
+
--hue-teal-bg: #ccfbf1; /* teal-100 */
|
|
310
|
+
--hue-orange: #c2410c; /* orange-700 */
|
|
311
|
+
--hue-orange-border: #fdba74; /* orange-300 */
|
|
312
|
+
--hue-orange-bg: #ffedd5; /* orange-100 */
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.dark {
|
|
316
|
+
--hue-blue: #93c5fd; /* blue-300 */
|
|
317
|
+
--hue-blue-border: rgba(29, 78, 216, 0.6);
|
|
318
|
+
--hue-blue-bg: rgba(30, 58, 138, 0.25);
|
|
319
|
+
--hue-indigo: #a5b4fc; /* indigo-300 */
|
|
320
|
+
--hue-indigo-border: rgba(67, 56, 202, 0.6);
|
|
321
|
+
--hue-indigo-bg: rgba(49, 46, 129, 0.25);
|
|
322
|
+
--hue-purple: #d8b4fe; /* purple-300 */
|
|
323
|
+
--hue-purple-border: rgba(126, 34, 206, 0.6);
|
|
324
|
+
--hue-purple-bg: rgba(88, 28, 135, 0.25);
|
|
325
|
+
--hue-teal: #5eead4; /* teal-300 */
|
|
326
|
+
--hue-teal-border: rgba(15, 118, 110, 0.6);
|
|
327
|
+
--hue-teal-bg: rgba(19, 78, 74, 0.25);
|
|
328
|
+
--hue-orange: #fdba74; /* orange-300 */
|
|
329
|
+
--hue-orange-border: rgba(194, 65, 12, 0.6);
|
|
330
|
+
--hue-orange-bg: rgba(124, 45, 18, 0.25);
|
|
331
|
+
}
|
|
332
|
+
|
|
236
333
|
/* Semantic colour utilities backed by the tokens above. Unlayered so they win
|
|
237
334
|
over Tailwind's layered colour utilities, and theme-aware for free (the var
|
|
238
335
|
flips under .dark) — no dark: variant needed at the call site. */
|