@eifi1/ui-kit 0.8.1 → 0.10.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 +25 -11
- package/dist/components/alert-banner.d.ts +32 -5
- package/dist/components/alert-banner.js +57 -14
- 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 +66 -0
- package/dist/components/bulk-action-bar.js +149 -0
- package/dist/components/bulk-action-bar.js.map +1 -0
- package/dist/components/calculator.d.ts +6 -0
- package/dist/components/chip.d.ts +58 -10
- package/dist/components/chip.js +91 -7
- package/dist/components/chip.js.map +1 -1
- package/dist/components/copy-button.d.ts +13 -4
- package/dist/components/copy-button.js +17 -4
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +122 -91
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +89 -3
- package/dist/components/date-picker.js +163 -68
- 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 +68 -4
- package/dist/components/disclosure.js +33 -15
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +3 -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 +29 -2
- package/dist/components/floating-panel.js +16 -2
- 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 +174 -0
- package/dist/components/list.js +215 -0
- package/dist/components/list.js.map +1 -0
- package/dist/components/menu-item.d.ts +99 -0
- package/dist/components/menu-item.js +83 -0
- package/dist/components/menu-item.js.map +1 -0
- 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/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 +16 -2
- package/dist/components/progress-bar.js +4 -2
- 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 +16 -0
- package/dist/components/series-chart.js +137 -3
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/status-dot.d.ts +49 -0
- package/dist/components/status-dot.js +52 -0
- package/dist/components/status-dot.js.map +1 -0
- package/dist/components/table.d.ts +39 -6
- package/dist/components/table.js +43 -4
- package/dist/components/table.js.map +1 -1
- package/dist/components/text.d.ts +63 -0
- package/dist/components/text.js +23 -0
- package/dist/components/text.js.map +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 +29 -1
- package/dist/components/toggle-group.js +52 -3
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/tooltip.d.ts +38 -15
- package/dist/components/tooltip.js +85 -47
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +139 -16
- package/dist/components/ui.js +136 -26
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/{data-table-filters-CF1PXqjQ.d.ts → data-table-filters-Dh9uF_S-.d.ts} +33 -2
- package/dist/data-table.d.ts +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-overlay-history.js +84 -4
- package/dist/hooks/use-overlay-history.js.map +1 -1
- 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 +31 -1
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +6 -0
- package/dist/i18n/locales/es.js +31 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +6 -0
- package/dist/i18n/locales/fr.js +31 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +6 -0
- package/dist/i18n/locales/hu.js +31 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +6 -0
- package/dist/i18n/locales/it.js +31 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +6 -0
- package/dist/i18n/locales/zh.js +31 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +18 -8
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/clipping.d.ts +9 -0
- package/dist/lib/clipping.js +14 -0
- package/dist/lib/clipping.js.map +1 -0
- package/dist/search/command-palette.d.ts +67 -2
- package/dist/search/command-palette.js +147 -33
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.d.ts +166 -0
- package/dist/search/global-search.js +293 -0
- package/dist/search/global-search.js.map +1 -0
- package/dist/search/search-index.d.ts +105 -0
- package/dist/search/search-index.js +142 -0
- package/dist/search/search-index.js.map +1 -0
- package/dist/search.d.ts +3 -1
- package/dist/search.js +2 -0
- package/dist/search.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +10 -1
- package/dist/wizard/stepper-nav.js +3 -2
- 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-summary.js +3 -3
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/package.json +25 -12
- package/src/components/alert-banner.tsx +117 -18
- package/src/components/breadcrumbs.tsx +168 -0
- package/src/components/bulk-action-bar.tsx +256 -0
- package/src/components/chip.tsx +164 -13
- package/src/components/copy-button.tsx +29 -7
- package/src/components/data-table.tsx +81 -5
- package/src/components/date-picker.tsx +377 -118
- package/src/components/description-list.tsx +168 -15
- package/src/components/dialog-frame.tsx +7 -0
- package/src/components/disclosure.tsx +116 -28
- package/src/components/file-dropzone.tsx +9 -10
- package/src/components/floating-panel.tsx +46 -2
- package/src/components/full-bleed-dialog.tsx +42 -5
- package/src/components/list.tsx +417 -0
- package/src/components/menu-item.tsx +214 -0
- package/src/components/modal.tsx +80 -17
- package/src/components/page-contents.tsx +4 -4
- package/src/components/page-header.tsx +68 -0
- package/src/components/progress-bar.tsx +27 -3
- package/src/components/series-chart-labels.ts +4 -0
- package/src/components/series-chart.tsx +253 -3
- package/src/components/status-dot.tsx +99 -0
- package/src/components/table.tsx +91 -6
- package/src/components/text.tsx +89 -0
- package/src/components/toast.tsx +441 -0
- package/src/components/toggle-group.tsx +82 -3
- package/src/components/tooltip.tsx +186 -100
- package/src/components/ui.tsx +321 -41
- package/src/hooks/use-close-transition.ts +14 -5
- package/src/hooks/use-copy-to-clipboard.ts +1 -1
- package/src/hooks/use-overlay-history.ts +181 -4
- package/src/i18n/defaults.ts +10 -0
- package/src/i18n/kit-labels.tsx +10 -0
- package/src/i18n/locales/de.ts +30 -0
- package/src/i18n/locales/es.ts +30 -0
- package/src/i18n/locales/fr.ts +30 -0
- package/src/i18n/locales/hu.ts +30 -0
- package/src/i18n/locales/it.ts +30 -0
- package/src/i18n/locales/zh.ts +30 -0
- package/src/index.ts +30 -0
- package/src/lib/clipping.ts +16 -0
- package/src/search/command-palette.tsx +241 -31
- package/src/search/global-search.tsx +525 -0
- package/src/search/search-index.ts +295 -0
- package/src/search.ts +2 -0
- package/src/wizard/stepper-nav.tsx +13 -2
- package/src/wizard/types.ts +2 -2
- package/src/wizard/use-wizard.ts +3 -3
- package/src/wizard/wizard-summary.tsx +14 -10
- package/tokens.css +97 -0
|
@@ -3,16 +3,62 @@ import type { ComponentPropsWithoutRef, CSSProperties, ReactNode } from "react";
|
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
import { FieldHint } from "./ui";
|
|
5
5
|
|
|
6
|
-
export type DescriptionListLayout = "rows" | "cards";
|
|
7
|
-
export type DescriptionListDensity = "comfortable" | "compact";
|
|
6
|
+
export type DescriptionListLayout = "rows" | "cards" | "stacked";
|
|
7
|
+
export type DescriptionListDensity = "comfortable" | "compact" | "tight";
|
|
8
|
+
/** How many columns a `stacked` or `cards` list may use. */
|
|
9
|
+
export type DescriptionListColumns = 1 | 2 | 3 | 4 | 5 | 6;
|
|
8
10
|
|
|
9
11
|
interface ListContextValue {
|
|
10
12
|
layout: DescriptionListLayout;
|
|
11
13
|
numeric: boolean;
|
|
14
|
+
prose: boolean;
|
|
12
15
|
density: DescriptionListDensity;
|
|
16
|
+
columns: DescriptionListColumns;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
const ListContext = createContext<ListContextValue>({
|
|
19
|
+
const ListContext = createContext<ListContextValue>({
|
|
20
|
+
layout: "rows",
|
|
21
|
+
numeric: false,
|
|
22
|
+
prose: false,
|
|
23
|
+
density: "comfortable",
|
|
24
|
+
columns: 2,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* `stacked` columns, by the width of the LIST (a container query on a wrapper — the
|
|
29
|
+
* `<dl>` is the grid and cannot query itself). One column on a phone-narrow pane, two
|
|
30
|
+
* from 20rem, then one more column each time every column would still get ~12rem.
|
|
31
|
+
* The same shape as kastlan's DetailGrid (1 → 2 at `sm` → n at `md`), measured on the
|
|
32
|
+
* pane rather than the viewport, so the four preview dialogs and a full-width detail
|
|
33
|
+
* page can share one list. Literal strings, because Tailwind only emits what it reads.
|
|
34
|
+
*/
|
|
35
|
+
const STACKED_COLS: Record<DescriptionListColumns, string> = {
|
|
36
|
+
1: "grid-cols-1",
|
|
37
|
+
2: "grid-cols-1 @xs:grid-cols-2",
|
|
38
|
+
3: "grid-cols-1 @xs:grid-cols-2 @xl:grid-cols-3",
|
|
39
|
+
4: "grid-cols-1 @xs:grid-cols-2 @xl:grid-cols-3 @3xl:grid-cols-4",
|
|
40
|
+
5: "grid-cols-1 @xs:grid-cols-2 @xl:grid-cols-3 @3xl:grid-cols-4 @5xl:grid-cols-5",
|
|
41
|
+
6: "grid-cols-1 @xs:grid-cols-2 @xl:grid-cols-3 @3xl:grid-cols-4 @5xl:grid-cols-5 @6xl:grid-cols-6",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** An item's span at each of those thresholds, never more columns than exist there —
|
|
45
|
+
* so a span can never open an implicit column and push the grid past its pane. */
|
|
46
|
+
const STACKED_SPAN: Record<DescriptionListColumns, string> = {
|
|
47
|
+
1: "",
|
|
48
|
+
2: "@xs:col-span-2",
|
|
49
|
+
3: "@xs:col-span-2 @xl:col-span-3",
|
|
50
|
+
4: "@xs:col-span-2 @xl:col-span-3 @3xl:col-span-4",
|
|
51
|
+
5: "@xs:col-span-2 @xl:col-span-3 @3xl:col-span-4 @5xl:col-span-5",
|
|
52
|
+
6: "@xs:col-span-2 @xl:col-span-3 @3xl:col-span-4 @5xl:col-span-5 @6xl:col-span-6",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** The cards grid's gap per density, as a class and as the length the column formula
|
|
56
|
+
* subtracts — one table so the two cannot disagree. */
|
|
57
|
+
const CARD_GAP: Record<DescriptionListDensity, { cls: string; len: string }> = {
|
|
58
|
+
comfortable: { cls: "gap-3", len: "0.75rem" },
|
|
59
|
+
compact: { cls: "gap-2", len: "0.5rem" },
|
|
60
|
+
tight: { cls: "gap-1", len: "0.25rem" },
|
|
61
|
+
};
|
|
16
62
|
|
|
17
63
|
export interface DescriptionListProps extends ComponentPropsWithoutRef<"dl"> {
|
|
18
64
|
/**
|
|
@@ -21,6 +67,10 @@ export interface DescriptionListProps extends ComponentPropsWithoutRef<"dl"> {
|
|
|
21
67
|
* beside a sidebar stacks where it is actually cramped, not where the viewport is.
|
|
22
68
|
* `cards`: a grid of small bordered cards, term above body — keksdose's overview
|
|
23
69
|
* figures, lenkbank's account facts.
|
|
70
|
+
* `stacked` (0.10.0): a borderless grid, term ABOVE detail, {@link columns} wide —
|
|
71
|
+
* kastlan's DetailField/DetailGrid and the label-over-value grids of its lease,
|
|
72
|
+
* invoice, contact and unit preview dialogs. Items can {@link DescriptionItemProps.span}
|
|
73
|
+
* several columns (a notes field across the whole row).
|
|
24
74
|
*/
|
|
25
75
|
layout?: DescriptionListLayout;
|
|
26
76
|
/** Every detail is a figure: tabular digits, end-aligned. A `numeric` list of
|
|
@@ -28,10 +78,31 @@ export interface DescriptionListProps extends ComponentPropsWithoutRef<"dl"> {
|
|
|
28
78
|
* term would double the list's height to gain no width. Per item: see
|
|
29
79
|
* {@link DescriptionItemProps.numeric}. */
|
|
30
80
|
numeric?: boolean;
|
|
31
|
-
/** `compact` tightens the rhythm for a dense side panel.
|
|
81
|
+
/** `compact` tightens the rhythm for a dense side panel. `tight` (0.10.0) is 11px
|
|
82
|
+
* type on a 2px rhythm with no rules between rows — keksdose's admin metrics
|
|
83
|
+
* breakdowns (metrics-panel's label-left, count-right lists), which are read as a
|
|
84
|
+
* block of small print, not scanned row by row. */
|
|
32
85
|
density?: DescriptionListDensity;
|
|
33
86
|
/** `cards` only: the narrowest a card may get before the grid drops a column. */
|
|
34
87
|
minCardWidth?: string;
|
|
88
|
+
/**
|
|
89
|
+
* `stacked`: how many columns the grid grows to on a wide pane (default 2), see
|
|
90
|
+
* {@link STACKED_COLS}. `cards` (0.10.0): the MOST cards per row; the grid still
|
|
91
|
+
* drops columns below {@link minCardWidth}. keksdose's category editor faked "two
|
|
92
|
+
* side by side" with `minCardWidth="calc(50% - 0.5rem)"`, which is `columns={2}
|
|
93
|
+
* minCardWidth="0"` now, without knowing the gap. Ignored by `rows`.
|
|
94
|
+
*/
|
|
95
|
+
columns?: DescriptionListColumns;
|
|
96
|
+
/**
|
|
97
|
+
* The details are sentences, not values: regular weight in the secondary ink.
|
|
98
|
+
* lenkbank's control explainer cards each carry a paragraph, and at the card's
|
|
99
|
+
* `font-medium` primary ink a grid of paragraphs reads as a wall of headings — it
|
|
100
|
+
* spelled `font-normal text-slate-700` into every item. A flag rather than a new
|
|
101
|
+
* default because every other card list in the apps holds values (dates, figures,
|
|
102
|
+
* counts) that are not flagged `numeric` and ARE meant to stand out; lightening them
|
|
103
|
+
* all would not be an improvement. Per item: {@link DescriptionItemProps.prose}.
|
|
104
|
+
*/
|
|
105
|
+
prose?: boolean;
|
|
35
106
|
}
|
|
36
107
|
|
|
37
108
|
/**
|
|
@@ -54,27 +125,66 @@ export interface DescriptionListProps extends ComponentPropsWithoutRef<"dl"> {
|
|
|
54
125
|
export function DescriptionList({
|
|
55
126
|
layout = "rows",
|
|
56
127
|
numeric = false,
|
|
128
|
+
prose = false,
|
|
57
129
|
density = "comfortable",
|
|
58
130
|
minCardWidth = "10rem",
|
|
131
|
+
columns,
|
|
59
132
|
className,
|
|
60
133
|
style,
|
|
61
134
|
...rest
|
|
62
135
|
}: DescriptionListProps) {
|
|
63
136
|
const cards = layout === "cards";
|
|
137
|
+
const stacked = layout === "stacked";
|
|
138
|
+
const gap = CARD_GAP[density];
|
|
139
|
+
// Cards: auto-fill at `minCardWidth`, and with `columns` a floor on the track of one
|
|
140
|
+
// n-th of the row (less the gaps) — so auto-fill can never fit more than n. The
|
|
141
|
+
// outer `min(100%, …)` keeps a single card from overflowing a pane narrower than
|
|
142
|
+
// its minimum.
|
|
143
|
+
const track =
|
|
144
|
+
columns !== undefined
|
|
145
|
+
? `max(${minCardWidth}, (100% - ${columns - 1} * ${gap.len}) / ${columns})`
|
|
146
|
+
: minCardWidth;
|
|
64
147
|
const grid: CSSProperties | undefined = cards
|
|
65
|
-
? { gridTemplateColumns: `repeat(auto-fill, minmax(min(100%, ${
|
|
148
|
+
? { gridTemplateColumns: `repeat(auto-fill, minmax(min(100%, ${track}), 1fr))` }
|
|
66
149
|
: undefined;
|
|
150
|
+
const ctx: ListContextValue = { layout, numeric, prose, density, columns: columns ?? 2 };
|
|
151
|
+
const text = density === "tight" ? "text-[11px] leading-4" : density === "compact" ? "text-xs" : "text-sm";
|
|
152
|
+
|
|
153
|
+
if (stacked) {
|
|
154
|
+
return (
|
|
155
|
+
<ListContext.Provider value={ctx}>
|
|
156
|
+
{/* The container the columns are measured on — see STACKED_COLS. */}
|
|
157
|
+
<div className="@container">
|
|
158
|
+
<dl
|
|
159
|
+
{...rest}
|
|
160
|
+
data-layout={layout}
|
|
161
|
+
style={style}
|
|
162
|
+
className={cn(
|
|
163
|
+
"grid",
|
|
164
|
+
STACKED_COLS[ctx.columns],
|
|
165
|
+
density === "tight" ? "gap-x-3 gap-y-1" : density === "compact" ? "gap-x-4 gap-y-2" : "gap-x-6 gap-y-4",
|
|
166
|
+
text,
|
|
167
|
+
className,
|
|
168
|
+
)}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
</ListContext.Provider>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
67
175
|
return (
|
|
68
|
-
<ListContext.Provider value={
|
|
176
|
+
<ListContext.Provider value={ctx}>
|
|
69
177
|
<dl
|
|
70
178
|
{...rest}
|
|
71
179
|
data-layout={layout}
|
|
72
180
|
style={grid ? { ...grid, ...style } : style}
|
|
73
181
|
className={cn(
|
|
74
182
|
cards
|
|
75
|
-
? cn("grid",
|
|
76
|
-
:
|
|
77
|
-
|
|
183
|
+
? cn("grid", gap.cls)
|
|
184
|
+
: // Tight rows carry no rules: at a 2px rhythm a line under every row
|
|
185
|
+
// turns a block of small print into a ruled table.
|
|
186
|
+
cn("@container", density !== "tight" && "divide-y divide-[var(--border)]"),
|
|
187
|
+
text,
|
|
78
188
|
className,
|
|
79
189
|
)}
|
|
80
190
|
/>
|
|
@@ -95,10 +205,28 @@ export interface DescriptionItemProps extends Omit<ComponentPropsWithoutRef<"div
|
|
|
95
205
|
numeric?: boolean;
|
|
96
206
|
/** Classes for the `<dd>`. */
|
|
97
207
|
detailClassName?: string;
|
|
208
|
+
/** This detail is prose (see {@link DescriptionListProps.prose}); overrides the
|
|
209
|
+
* list's setting either way. */
|
|
210
|
+
prose?: boolean;
|
|
211
|
+
/**
|
|
212
|
+
* `stacked`: how many columns this item spans, clamped to the list's `columns` and
|
|
213
|
+
* to however many the pane has room for; `"full"` is the whole row. kastlan's
|
|
214
|
+
* DetailField `span={2}` for notes and descriptions. `cards`: only `"full"` applies,
|
|
215
|
+
* because a card grid's column count is not known to CSS ahead of layout.
|
|
216
|
+
*/
|
|
217
|
+
span?: number | "full";
|
|
98
218
|
}
|
|
99
219
|
|
|
100
|
-
const ROW_PAD: Record<DescriptionListDensity, string> = {
|
|
101
|
-
|
|
220
|
+
const ROW_PAD: Record<DescriptionListDensity, string> = {
|
|
221
|
+
comfortable: "py-2.5",
|
|
222
|
+
compact: "py-1.5",
|
|
223
|
+
tight: "py-px",
|
|
224
|
+
};
|
|
225
|
+
const CARD_PAD: Record<DescriptionListDensity, string> = {
|
|
226
|
+
comfortable: "px-3 py-2.5",
|
|
227
|
+
compact: "px-2.5 py-1.5",
|
|
228
|
+
tight: "px-2 py-1",
|
|
229
|
+
};
|
|
102
230
|
|
|
103
231
|
/** One term and its detail inside a {@link DescriptionList}. */
|
|
104
232
|
export function DescriptionItem({
|
|
@@ -106,19 +234,32 @@ export function DescriptionItem({
|
|
|
106
234
|
children,
|
|
107
235
|
hint,
|
|
108
236
|
numeric: numericProp,
|
|
237
|
+
prose: proseProp,
|
|
238
|
+
span,
|
|
109
239
|
detailClassName,
|
|
110
240
|
className,
|
|
111
241
|
...rest
|
|
112
242
|
}: DescriptionItemProps) {
|
|
113
|
-
const { layout, numeric: listNumeric, density } = useContext(ListContext);
|
|
243
|
+
const { layout, numeric: listNumeric, prose: listProse, density, columns } = useContext(ListContext);
|
|
114
244
|
const numeric = numericProp ?? listNumeric;
|
|
245
|
+
const prose = proseProp ?? listProse;
|
|
115
246
|
const cards = layout === "cards";
|
|
247
|
+
const stacked = layout === "stacked";
|
|
248
|
+
const spanClass =
|
|
249
|
+
span === "full"
|
|
250
|
+
? "col-span-full"
|
|
251
|
+
: stacked && span !== undefined && span > 1
|
|
252
|
+
? STACKED_SPAN[Math.min(Math.floor(span), columns) as DescriptionListColumns]
|
|
253
|
+
: undefined;
|
|
116
254
|
|
|
117
255
|
const termNode = (
|
|
118
256
|
<dt
|
|
119
257
|
className={cn(
|
|
120
258
|
"flex min-w-0 items-center gap-1 text-[var(--text-muted)]",
|
|
121
|
-
cards && "text-xs uppercase tracking-wide",
|
|
259
|
+
cards && (density === "tight" ? "text-[10px] uppercase tracking-wide" : "text-xs uppercase tracking-wide"),
|
|
260
|
+
// Stacked terms are a step smaller than the detail, as kastlan's dialogs set
|
|
261
|
+
// them; in a list that is already small they stay the list's own size.
|
|
262
|
+
stacked && density === "comfortable" && "text-xs",
|
|
122
263
|
)}
|
|
123
264
|
>
|
|
124
265
|
<span className="min-w-0 break-words">{term}</span>
|
|
@@ -129,8 +270,10 @@ export function DescriptionItem({
|
|
|
129
270
|
<dd
|
|
130
271
|
className={cn(
|
|
131
272
|
"min-w-0 break-words text-[var(--text-primary)]",
|
|
132
|
-
|
|
133
|
-
|
|
273
|
+
// A stacked figure sits under its term, so it keeps the term's start edge.
|
|
274
|
+
numeric && (stacked ? "tabular-nums" : "tabular-nums text-end"),
|
|
275
|
+
(cards || stacked) && "mt-0.5 font-medium",
|
|
276
|
+
prose && "font-normal text-[var(--text-secondary)]",
|
|
134
277
|
detailClassName,
|
|
135
278
|
)}
|
|
136
279
|
>
|
|
@@ -145,6 +288,7 @@ export function DescriptionItem({
|
|
|
145
288
|
className={cn(
|
|
146
289
|
"min-w-0 rounded-md border border-[var(--border)] bg-[var(--bg-surface)]",
|
|
147
290
|
CARD_PAD[density],
|
|
291
|
+
spanClass,
|
|
148
292
|
className,
|
|
149
293
|
)}
|
|
150
294
|
>
|
|
@@ -154,6 +298,15 @@ export function DescriptionItem({
|
|
|
154
298
|
);
|
|
155
299
|
}
|
|
156
300
|
|
|
301
|
+
if (stacked) {
|
|
302
|
+
return (
|
|
303
|
+
<div {...rest} className={cn("min-w-0", spanClass, className)}>
|
|
304
|
+
{termNode}
|
|
305
|
+
{detail}
|
|
306
|
+
</div>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
157
310
|
return (
|
|
158
311
|
<div
|
|
159
312
|
{...rest}
|
|
@@ -103,6 +103,13 @@ export interface DialogFrameProps extends Omit<ModalProps, "labelledBy" | "child
|
|
|
103
103
|
* tailwind-merged last), which is how a full-screen phone sheet is spelt:
|
|
104
104
|
* `fullBleed className="h-[100dvh] max-w-full rounded-none md:h-auto md:rounded-lg"`.
|
|
105
105
|
*
|
|
106
|
+
* ## Kept mounted
|
|
107
|
+
*
|
|
108
|
+
* `open` reaches the `Modal` like every other prop, so `<DialogFrame open={open} …>`
|
|
109
|
+
* replaces the `{open && <DialogFrame …/>}` gate (kastlan's `FormModal` was only
|
|
110
|
+
* that gate) and gains the exit a caller-driven close otherwise skips. See
|
|
111
|
+
* {@link ModalProps.open}.
|
|
112
|
+
*
|
|
106
113
|
* ## What it is not
|
|
107
114
|
*
|
|
108
115
|
* Not `FullBleedDialog`: that is the phone's full-screen editor with its own `open`,
|
|
@@ -3,22 +3,14 @@ import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
|
3
3
|
import { ChevronDown } from "lucide-react";
|
|
4
4
|
|
|
5
5
|
import { cn } from "../lib/cn";
|
|
6
|
+
// Read at the moment of closing, as `useCloseTransition` does: the setting can change
|
|
7
|
+
// under a long-lived page.
|
|
8
|
+
import { prefersReducedMotion } from "../hooks/use-close-transition";
|
|
6
9
|
|
|
7
10
|
/** How long the fold takes, in ms. The same number as the `duration-200` below and as
|
|
8
11
|
* the unmount timer, because they are the same movement. */
|
|
9
12
|
const COLLAPSE_MS = 200;
|
|
10
13
|
|
|
11
|
-
/** Read at the moment of closing, like `useCloseTransition` does: the setting can
|
|
12
|
-
* change under a long-lived page, and jsdom/SSR have no `matchMedia` — where "no
|
|
13
|
-
* animation" is also the only correct answer, since nothing is painting. */
|
|
14
|
-
function prefersReducedMotion(): boolean {
|
|
15
|
-
return (
|
|
16
|
-
typeof window === "undefined" ||
|
|
17
|
-
typeof window.matchMedia !== "function" ||
|
|
18
|
-
window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
14
|
/**
|
|
23
15
|
* `extends` the div's props so an `id` (what a trigger's `aria-controls` points at), a
|
|
24
16
|
* `data-tour` anchor or a test id reaches the element that folds.
|
|
@@ -144,8 +136,17 @@ export interface DisclosureProps extends Omit<ComponentPropsWithoutRef<"div">, "
|
|
|
144
136
|
* settings or analysis page. `bare` draws nothing: a leading chevron that turns
|
|
145
137
|
* down, for an inline "Show 3 hidden accounts" inside something that already has
|
|
146
138
|
* its own surface.
|
|
139
|
+
*
|
|
140
|
+
* `menu` is a row of a `HoverMenu`: the top bar's menu-item look
|
|
141
|
+
* (`TOPBAR_MENU_ITEM_CLASS` — full-width, `px-3 py-2`, regular weight, square, the
|
|
142
|
+
* hover wash with the text colour left alone), an INSET focus ring so the menu's
|
|
143
|
+
* clipping edge cannot cut it off, the chevron at the end (`chevronPosition`
|
|
144
|
+
* defaults to `end` here) and a body with no padding of its own, so the sub-list's
|
|
145
|
+
* rows sit flush like the rows around them. keksdose's account menu opens its
|
|
146
|
+
* language sub-list this way, and on `bare` it took four header overrides and a
|
|
147
|
+
* body one to get there (account-menu.tsx).
|
|
147
148
|
*/
|
|
148
|
-
variant?: "card" | "bare";
|
|
149
|
+
variant?: "card" | "bare" | "menu";
|
|
149
150
|
/**
|
|
150
151
|
* Where a `bare` disclosure draws its chevron. `start` (default) is the leading
|
|
151
152
|
* chevron that turns from the reading direction to down. `end` puts it at the far
|
|
@@ -153,9 +154,15 @@ export interface DisclosureProps extends Omit<ComponentPropsWithoutRef<"div">, "
|
|
|
153
154
|
* account menu, whose language sub-list reads "Language 🇩🇪 ⌄" with the chevron
|
|
154
155
|
* after the flag, like every other menu row with a sub-list. With `trailing`, the
|
|
155
156
|
* chevron follows it, as on a card. A `card` always has it at the end, so this is
|
|
156
|
-
* ignored there.
|
|
157
|
+
* ignored there. A `menu` defaults to `end`, like the menu rows around it.
|
|
158
|
+
*
|
|
159
|
+
* `after-title` sits it right after the title's text, pointing down and turning up —
|
|
160
|
+
* "Net profit ⌄ ······ 1,234.50", Lenkbank's result rows, where the far end of the
|
|
161
|
+
* row belongs to a figure (see {@link trailingInTrigger}) and a leading chevron
|
|
162
|
+
* would push every label off the column the non-expandable rows' labels start on.
|
|
163
|
+
* Not a card's: its chevron keeps the end.
|
|
157
164
|
*/
|
|
158
|
-
chevronPosition?: "start" | "end";
|
|
165
|
+
chevronPosition?: "start" | "end" | "after-title";
|
|
159
166
|
/**
|
|
160
167
|
* Wrap the header button in a heading of this level. The WAI-ARIA disclosure pattern
|
|
161
168
|
* puts the button INSIDE the heading when the disclosure titles a section, so the
|
|
@@ -194,8 +201,26 @@ export interface DisclosureProps extends Omit<ComponentPropsWithoutRef<"div">, "
|
|
|
194
201
|
* the row under it (a stretched hit area), so clicking the empty space or the
|
|
195
202
|
* card's chevron still toggles, while whatever sits in `trailing` gets its own
|
|
196
203
|
* clicks. With `trailing`, the card's chevron moves after it, to the row's end.
|
|
204
|
+
*
|
|
205
|
+
* Unless {@link trailingInTrigger} is set, which moves it INTO the button.
|
|
197
206
|
*/
|
|
198
207
|
trailing?: ReactNode;
|
|
208
|
+
/**
|
|
209
|
+
* Render {@link trailing} inside the header button instead of beside it, so the
|
|
210
|
+
* whole row — figure included — is one click target and the figure is part of the
|
|
211
|
+
* button's accessible name: "Net profit 1,234.50, collapsed" rather than a button
|
|
212
|
+
* called "Net profit" next to an unexplained number.
|
|
213
|
+
*
|
|
214
|
+
* Lenkbank's result rows (features/steering/results-panel.tsx) are the case: label,
|
|
215
|
+
* chevron, and the value at the far end, all one `<button>`, because a result row
|
|
216
|
+
* has nothing else it could mean.
|
|
217
|
+
*
|
|
218
|
+
* ⚠️ Only for NON-interactive content — a count, a figure, a status chip. A button
|
|
219
|
+
* may not contain another interactive element; an action belongs in the default,
|
|
220
|
+
* sibling `trailing`. The content is rendered in a `<span>`, so keep it phrasing
|
|
221
|
+
* content too (no `div`s).
|
|
222
|
+
*/
|
|
223
|
+
trailingInTrigger?: boolean;
|
|
199
224
|
/**
|
|
200
225
|
* Trigger-only mode: the id of an element the CALLER renders elsewhere — the hidden
|
|
201
226
|
* rows of a table, a panel in another column — which this header shows and hides.
|
|
@@ -203,6 +228,37 @@ export interface DisclosureProps extends Omit<ComponentPropsWithoutRef<"div">, "
|
|
|
203
228
|
* its own (`children`, `bodyClassName` and `keepMounted` are ignored). Pair it with
|
|
204
229
|
* `open` / `onOpenChange`: the caller owns the state, since the caller renders what
|
|
205
230
|
* it governs.
|
|
231
|
+
*
|
|
232
|
+
* It is an IDREF LIST, as `aria-controls` is: several space-separated ids when what
|
|
233
|
+
* folds is several elements — a budget group's category rows, each its own `<li>`,
|
|
234
|
+
* siblings of this header (keksdose's budget-mobile-list joins their ids with
|
|
235
|
+
* `" "`). It may be the EMPTY string: trigger-only mode with nothing to point at yet
|
|
236
|
+
* (a group whose rows are all filtered away, rows that are not rendered while shut).
|
|
237
|
+
* The header then carries no `aria-controls` at all rather than an empty one, and
|
|
238
|
+
* still toggles.
|
|
239
|
+
*
|
|
240
|
+
* A table's leading row — keksdose's "Upcoming" toggle over the scheduled rows of
|
|
241
|
+
* its transactions table — is this mode too: the rows it folds are the TABLE's, so
|
|
242
|
+
* the disclosure is only the header, and the table renders or drops them from the
|
|
243
|
+
* same state.
|
|
244
|
+
*
|
|
245
|
+
* ```tsx
|
|
246
|
+
* <DataTable
|
|
247
|
+
* rows={showFuture ? [...scheduled, ...posted] : posted}
|
|
248
|
+
* leadingRow={
|
|
249
|
+
* <Disclosure
|
|
250
|
+
* variant="bare"
|
|
251
|
+
* title={upcomingLabel}
|
|
252
|
+
* open={showFuture}
|
|
253
|
+
* onOpenChange={setShowFuture}
|
|
254
|
+
* // The scheduled rows' ids while they are rendered; "" while they are not.
|
|
255
|
+
* controls={showFuture ? scheduled.map((tx) => `row-s${tx.id}`).join(" ") : ""}
|
|
256
|
+
* headerClassName="px-3 py-2"
|
|
257
|
+
* />
|
|
258
|
+
* }
|
|
259
|
+
* …
|
|
260
|
+
* />
|
|
261
|
+
* ```
|
|
206
262
|
*/
|
|
207
263
|
controls?: string;
|
|
208
264
|
/** The body. Not rendered in trigger-only mode ({@link DisclosureProps.controls}). */
|
|
@@ -230,7 +286,7 @@ export function Disclosure({
|
|
|
230
286
|
defaultOpen = false,
|
|
231
287
|
onOpenChange,
|
|
232
288
|
variant = "card",
|
|
233
|
-
chevronPosition
|
|
289
|
+
chevronPosition,
|
|
234
290
|
headingAs: Heading,
|
|
235
291
|
keepMounted,
|
|
236
292
|
disabled,
|
|
@@ -238,6 +294,7 @@ export function Disclosure({
|
|
|
238
294
|
triggerProps,
|
|
239
295
|
bodyClassName,
|
|
240
296
|
trailing,
|
|
297
|
+
trailingInTrigger = false,
|
|
241
298
|
controls,
|
|
242
299
|
className,
|
|
243
300
|
children,
|
|
@@ -247,12 +304,20 @@ export function Disclosure({
|
|
|
247
304
|
const open = controlled ?? own;
|
|
248
305
|
const bodyId = useId();
|
|
249
306
|
const card = variant === "card";
|
|
250
|
-
|
|
251
|
-
|
|
307
|
+
const menu = variant === "menu";
|
|
308
|
+
// The card's chevron trails always; a menu row's unless asked otherwise; a bare
|
|
309
|
+
// one only when asked to.
|
|
310
|
+
const chevronAt = card ? "end" : (chevronPosition ?? (menu ? "end" : "start"));
|
|
311
|
+
const chevronAtEnd = chevronAt === "end";
|
|
312
|
+
const chevronAfterTitle = chevronAt === "after-title";
|
|
252
313
|
const triggerOnly = controls !== undefined;
|
|
253
314
|
// The card's header squares its lower corners only when a body opens under it.
|
|
254
315
|
const joined = open && !triggerOnly;
|
|
255
316
|
const hasTrailing = trailing !== undefined && trailing !== null && trailing !== false;
|
|
317
|
+
// `trailing` beside the button, which then stretches under it; inside the button,
|
|
318
|
+
// the row is simply the button.
|
|
319
|
+
const trailingInside = hasTrailing && trailingInTrigger;
|
|
320
|
+
const trailingBeside = hasTrailing && !trailingInTrigger;
|
|
256
321
|
|
|
257
322
|
const toggle = () => {
|
|
258
323
|
const next = !open;
|
|
@@ -267,55 +332,78 @@ export function Disclosure({
|
|
|
267
332
|
{...extraTriggerProps}
|
|
268
333
|
type="button"
|
|
269
334
|
aria-expanded={open}
|
|
270
|
-
|
|
335
|
+
// `||`: an empty `controls` is trigger-only with nothing to name, not `aria-controls=""`.
|
|
336
|
+
aria-controls={triggerOnly ? controls || undefined : bodyId}
|
|
271
337
|
disabled={disabled}
|
|
272
338
|
onClick={toggle}
|
|
273
339
|
data-disclosure-trigger=""
|
|
274
340
|
className={cn(
|
|
275
341
|
"flex w-full gap-2 text-start outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
276
|
-
|
|
342
|
+
trailingBeside
|
|
277
343
|
? // Stretched over the whole header row (the row is `relative`), so the space
|
|
278
344
|
// round `trailing` and the card's chevron still toggle; the ring and the
|
|
279
345
|
// card's hover paint on the stretched area / the row, not the text box.
|
|
280
346
|
cn(
|
|
281
347
|
"min-w-0 flex-1 after:absolute after:inset-0 after:content-['']",
|
|
282
348
|
"focus-visible:after:ring-2 focus-visible:after:ring-inset focus-visible:after:ring-[var(--brand)]",
|
|
283
|
-
card ? cn("after:rounded-lg", joined && "after:rounded-b-none") : "after:rounded-sm",
|
|
349
|
+
card ? cn("after:rounded-lg", joined && "after:rounded-b-none") : !menu && "after:rounded-sm",
|
|
284
350
|
)
|
|
285
351
|
: "focus-visible:ring-2 focus-visible:ring-[var(--brand)]",
|
|
286
352
|
card
|
|
287
353
|
? cn(
|
|
288
354
|
"items-center justify-between rounded-lg p-4",
|
|
289
|
-
|
|
355
|
+
trailingBeside ? "pe-0" : "hover:bg-[var(--bg-hover)] focus-visible:ring-inset",
|
|
290
356
|
joined && "rounded-b-none",
|
|
291
357
|
)
|
|
292
|
-
:
|
|
358
|
+
: menu
|
|
359
|
+
? // TOPBAR_MENU_ITEM_CLASS's row, spelled out rather than imported: a
|
|
360
|
+
// component does not reach up into the shell. A test holds the two to
|
|
361
|
+
// the same classes.
|
|
362
|
+
"items-center justify-between gap-3 px-3 py-2 text-sm text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] focus-visible:ring-inset"
|
|
363
|
+
: "items-center rounded-sm text-sm font-medium text-[var(--text-secondary)] hover:text-[var(--text-primary)]",
|
|
293
364
|
headerClassName,
|
|
294
365
|
)}
|
|
295
366
|
>
|
|
296
|
-
{
|
|
367
|
+
{chevronAt === "start" && <Chevron open={open} leading />}
|
|
297
368
|
{/* `flex-1`: the title takes the row, so whatever a caller puts at its end sits
|
|
298
369
|
at the header's far edge. */}
|
|
299
370
|
<span className="min-w-0 flex-1">
|
|
300
|
-
|
|
371
|
+
{chevronAfterTitle ? (
|
|
372
|
+
// The chevron hugs the text, however long the translation: the text wraps
|
|
373
|
+
// before the chevron is pushed away from it.
|
|
374
|
+
<span className="flex items-center gap-1">
|
|
375
|
+
<span className="min-w-0">{title}</span>
|
|
376
|
+
<Chevron open={open} small />
|
|
377
|
+
</span>
|
|
378
|
+
) : (
|
|
379
|
+
<span className={cn("block", card && "text-sm font-semibold text-[var(--text-primary)]")}>{title}</span>
|
|
380
|
+
)}
|
|
301
381
|
{hint !== undefined && (
|
|
302
382
|
<span className="mt-0.5 block text-xs font-normal text-[var(--text-muted)]">{hint}</span>
|
|
303
383
|
)}
|
|
304
384
|
</span>
|
|
305
|
-
{
|
|
385
|
+
{/* The space is for the accessible name — "Net profit 1,234.50", not
|
|
386
|
+
"Net profit1,234.50"; between flex items it renders as nothing. */}
|
|
387
|
+
{trailingInside && " "}
|
|
388
|
+
{trailingInside && (
|
|
389
|
+
<span data-disclosure-trailing="" className="flex shrink-0 items-center gap-2">
|
|
390
|
+
{trailing}
|
|
391
|
+
</span>
|
|
392
|
+
)}
|
|
393
|
+
{chevronAtEnd && !trailingBeside && <Chevron open={open} small={!card} />}
|
|
306
394
|
</button>
|
|
307
395
|
);
|
|
308
396
|
|
|
309
397
|
// Preflight already makes h1–h6 inherit size and weight, so the heading adds
|
|
310
398
|
// structure and nothing visible.
|
|
311
|
-
const header = Heading ? <Heading className={
|
|
399
|
+
const header = Heading ? <Heading className={trailingBeside ? "min-w-0 flex-1" : undefined}>{button}</Heading> : button;
|
|
312
400
|
|
|
313
401
|
return (
|
|
314
402
|
<div
|
|
315
403
|
{...rest}
|
|
316
404
|
className={cn(card && "rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] shadow-sm", className)}
|
|
317
405
|
>
|
|
318
|
-
{
|
|
406
|
+
{trailingBeside ? (
|
|
319
407
|
<div
|
|
320
408
|
className={cn(
|
|
321
409
|
"relative flex items-center gap-2",
|
|
@@ -337,7 +425,7 @@ export function Disclosure({
|
|
|
337
425
|
)}
|
|
338
426
|
{!triggerOnly && (
|
|
339
427
|
<Collapse id={bodyId} open={open} keepMounted={keepMounted}>
|
|
340
|
-
<div className={cn(card ? "space-y-3 px-4 pb-4" : "space-y-2 pt-2", bodyClassName)}>{children}</div>
|
|
428
|
+
<div className={cn(card ? "space-y-3 px-4 pb-4" : !menu && "space-y-2 pt-2", bodyClassName)}>{children}</div>
|
|
341
429
|
</Collapse>
|
|
342
430
|
)}
|
|
343
431
|
</div>
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "./file-button";
|
|
15
15
|
import type { FilePickHandler, FilePickerLabels, FileRejection } from "./file-button";
|
|
16
16
|
import { useDragTarget } from "../hooks/use-file-drop";
|
|
17
|
+
import { toast } from "./toast";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Where a refused file's message goes.
|
|
@@ -97,10 +98,9 @@ export interface FileDropzoneProps extends Omit<ComponentPropsWithoutRef<"div">,
|
|
|
97
98
|
* carries the reason and the translated message as well.
|
|
98
99
|
*
|
|
99
100
|
* Its existence used to be what switched the toast off, and it still does (see
|
|
100
|
-
* {@link rejectionFeedback}). The toast is
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* `optional-peer-imports.test.tsx`).
|
|
101
|
+
* {@link rejectionFeedback}). The toast is the kit's `toast`, which loads `sonner`
|
|
102
|
+
* lazily: it is an OPTIONAL peer, and a static import here once broke
|
|
103
|
+
* `import { Button }` for every app without it (see `optional-peer-imports.test.tsx`).
|
|
104
104
|
*/
|
|
105
105
|
onInvalid?: (file: File) => void;
|
|
106
106
|
/** Every refused file of one pick, with its reason and message. */
|
|
@@ -246,12 +246,11 @@ export function FileDropzone({
|
|
|
246
246
|
? summariseRejections(rejected, labels)
|
|
247
247
|
: labels.rejectedPick(picked.length);
|
|
248
248
|
if (feedback === "toast") {
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
void import("sonner").then(({ toast }) => toast.error(message));
|
|
249
|
+
// The kit's `toast`, which loads sonner (an optional peer) lazily, so an app
|
|
250
|
+
// that never trips this never has to install it. Nothing downstream depends
|
|
251
|
+
// on the toast having appeared. The toaster is a live region, so this path
|
|
252
|
+
// does not announce a second time.
|
|
253
|
+
toast.error(message);
|
|
255
254
|
} else {
|
|
256
255
|
if (feedback === "inline") setError(message);
|
|
257
256
|
alert.announce(message);
|