@eifi1/ui-kit 0.8.0 → 0.9.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 +4 -0
- package/dist/components/amount-input.d.ts +2 -0
- package/dist/components/calculator.d.ts +2 -0
- package/dist/components/chip.d.ts +27 -5
- package/dist/components/chip.js +6 -2
- package/dist/components/chip.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 +69 -3
- package/dist/components/date-picker.js +152 -65
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/disclosure.d.ts +11 -2
- package/dist/components/disclosure.js +10 -4
- package/dist/components/disclosure.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.js +3 -1
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/number-field.d.ts +2 -0
- package/dist/components/number-input.d.ts +2 -0
- package/dist/components/numpad-sheet.d.ts +2 -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/treemap.js +4 -1
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/ui.d.ts +44 -14
- package/dist/components/ui.js +28 -22
- 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-overlay-history.js +84 -4
- package/dist/hooks/use-overlay-history.js.map +1 -1
- package/dist/i18n/defaults.d.ts +2 -0
- package/dist/i18n/defaults.js +2 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +3 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +2 -0
- package/dist/i18n/locales/de-CH.d.ts +2 -0
- package/dist/i18n/locales/de-informal.d.ts +2 -0
- package/dist/i18n/locales/de.d.ts +2 -0
- package/dist/i18n/locales/de.js +10 -0
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +2 -0
- package/dist/i18n/locales/es.js +10 -0
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +2 -0
- package/dist/i18n/locales/fr.js +10 -0
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +2 -0
- package/dist/i18n/locales/hu.js +10 -0
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +2 -0
- package/dist/i18n/locales/it.js +10 -0
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +2 -0
- package/dist/i18n/locales/zh.js +10 -0
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/search/command-palette.d.ts +49 -1
- package/dist/search/command-palette.js +132 -29
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.d.ts +141 -0
- package/dist/search/global-search.js +277 -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 +2 -0
- 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 +2 -1
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/package.json +12 -3
- package/src/components/chip.tsx +36 -6
- package/src/components/data-table.tsx +81 -5
- package/src/components/date-picker.tsx +328 -117
- package/src/components/disclosure.tsx +24 -8
- package/src/components/floating-panel.tsx +46 -2
- package/src/components/full-bleed-dialog.tsx +7 -2
- package/src/components/progress-bar.tsx +27 -3
- package/src/components/treemap.tsx +3 -0
- package/src/components/ui.tsx +95 -37
- package/src/hooks/use-overlay-history.ts +181 -4
- package/src/i18n/defaults.ts +2 -0
- package/src/i18n/kit-labels.tsx +2 -0
- package/src/i18n/locales/de.ts +10 -0
- package/src/i18n/locales/es.ts +10 -0
- package/src/i18n/locales/fr.ts +10 -0
- package/src/i18n/locales/hu.ts +10 -0
- package/src/i18n/locales/it.ts +10 -0
- package/src/i18n/locales/zh.ts +10 -0
- package/src/index.ts +4 -0
- package/src/search/command-palette.tsx +200 -27
- package/src/search/global-search.tsx +484 -0
- package/src/search/search-index.ts +295 -0
- package/src/search.ts +2 -0
- package/src/wizard/stepper-nav.tsx +11 -1
|
@@ -144,8 +144,17 @@ export interface DisclosureProps extends Omit<ComponentPropsWithoutRef<"div">, "
|
|
|
144
144
|
* settings or analysis page. `bare` draws nothing: a leading chevron that turns
|
|
145
145
|
* down, for an inline "Show 3 hidden accounts" inside something that already has
|
|
146
146
|
* its own surface.
|
|
147
|
+
*
|
|
148
|
+
* `menu` is a row of a `HoverMenu`: the top bar's menu-item look
|
|
149
|
+
* (`TOPBAR_MENU_ITEM_CLASS` — full-width, `px-3 py-2`, regular weight, square, the
|
|
150
|
+
* hover wash with the text colour left alone), an INSET focus ring so the menu's
|
|
151
|
+
* clipping edge cannot cut it off, the chevron at the end (`chevronPosition`
|
|
152
|
+
* defaults to `end` here) and a body with no padding of its own, so the sub-list's
|
|
153
|
+
* rows sit flush like the rows around them. keksdose's account menu opens its
|
|
154
|
+
* language sub-list this way, and on `bare` it took four header overrides and a
|
|
155
|
+
* body one to get there (account-menu.tsx).
|
|
147
156
|
*/
|
|
148
|
-
variant?: "card" | "bare";
|
|
157
|
+
variant?: "card" | "bare" | "menu";
|
|
149
158
|
/**
|
|
150
159
|
* Where a `bare` disclosure draws its chevron. `start` (default) is the leading
|
|
151
160
|
* chevron that turns from the reading direction to down. `end` puts it at the far
|
|
@@ -153,7 +162,7 @@ export interface DisclosureProps extends Omit<ComponentPropsWithoutRef<"div">, "
|
|
|
153
162
|
* account menu, whose language sub-list reads "Language 🇩🇪 ⌄" with the chevron
|
|
154
163
|
* after the flag, like every other menu row with a sub-list. With `trailing`, the
|
|
155
164
|
* chevron follows it, as on a card. A `card` always has it at the end, so this is
|
|
156
|
-
* ignored there.
|
|
165
|
+
* ignored there. A `menu` defaults to `end`, like the menu rows around it.
|
|
157
166
|
*/
|
|
158
167
|
chevronPosition?: "start" | "end";
|
|
159
168
|
/**
|
|
@@ -230,7 +239,7 @@ export function Disclosure({
|
|
|
230
239
|
defaultOpen = false,
|
|
231
240
|
onOpenChange,
|
|
232
241
|
variant = "card",
|
|
233
|
-
chevronPosition
|
|
242
|
+
chevronPosition,
|
|
234
243
|
headingAs: Heading,
|
|
235
244
|
keepMounted,
|
|
236
245
|
disabled,
|
|
@@ -247,8 +256,10 @@ export function Disclosure({
|
|
|
247
256
|
const open = controlled ?? own;
|
|
248
257
|
const bodyId = useId();
|
|
249
258
|
const card = variant === "card";
|
|
250
|
-
|
|
251
|
-
|
|
259
|
+
const menu = variant === "menu";
|
|
260
|
+
// The card's chevron trails always; a menu row's unless asked otherwise; a bare
|
|
261
|
+
// one only when asked to.
|
|
262
|
+
const chevronAtEnd = card || (chevronPosition ?? (menu ? "end" : "start")) === "end";
|
|
252
263
|
const triggerOnly = controls !== undefined;
|
|
253
264
|
// The card's header squares its lower corners only when a body opens under it.
|
|
254
265
|
const joined = open && !triggerOnly;
|
|
@@ -280,7 +291,7 @@ export function Disclosure({
|
|
|
280
291
|
cn(
|
|
281
292
|
"min-w-0 flex-1 after:absolute after:inset-0 after:content-['']",
|
|
282
293
|
"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",
|
|
294
|
+
card ? cn("after:rounded-lg", joined && "after:rounded-b-none") : !menu && "after:rounded-sm",
|
|
284
295
|
)
|
|
285
296
|
: "focus-visible:ring-2 focus-visible:ring-[var(--brand)]",
|
|
286
297
|
card
|
|
@@ -289,7 +300,12 @@ export function Disclosure({
|
|
|
289
300
|
hasTrailing ? "pe-0" : "hover:bg-[var(--bg-hover)] focus-visible:ring-inset",
|
|
290
301
|
joined && "rounded-b-none",
|
|
291
302
|
)
|
|
292
|
-
:
|
|
303
|
+
: menu
|
|
304
|
+
? // TOPBAR_MENU_ITEM_CLASS's row, spelled out rather than imported: a
|
|
305
|
+
// component does not reach up into the shell. A test holds the two to
|
|
306
|
+
// the same classes.
|
|
307
|
+
"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"
|
|
308
|
+
: "items-center rounded-sm text-sm font-medium text-[var(--text-secondary)] hover:text-[var(--text-primary)]",
|
|
293
309
|
headerClassName,
|
|
294
310
|
)}
|
|
295
311
|
>
|
|
@@ -337,7 +353,7 @@ export function Disclosure({
|
|
|
337
353
|
)}
|
|
338
354
|
{!triggerOnly && (
|
|
339
355
|
<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>
|
|
356
|
+
<div className={cn(card ? "space-y-3 px-4 pb-4" : !menu && "space-y-2 pt-2", bodyClassName)}>{children}</div>
|
|
341
357
|
</Collapse>
|
|
342
358
|
)}
|
|
343
359
|
</div>
|
|
@@ -65,6 +65,30 @@ export interface FloatingActionButtonProps extends Omit<ButtonHTMLAttributes<HTM
|
|
|
65
65
|
* clears the transactions page's own action group this way (`calc(1rem + 4rem)`).
|
|
66
66
|
*/
|
|
67
67
|
offset?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Whether the button carries the browser's own tooltip (`title`, the `label` unless a
|
|
70
|
+
* `title` is passed). Default `true`, so an icon-only button keeps a hover hint for a
|
|
71
|
+
* mouse user with no other way to learn what it is. `false` renders no `title` at all:
|
|
72
|
+
* keksdose has ONE tooltip, the kit's `Tooltip` (dev#523), and its source scan fails a
|
|
73
|
+
* native `title=` anywhere — so a flag, not `title=""`, which the scan would still
|
|
74
|
+
* see. An empty `title` renders no attribute either, for the caller who has no scan.
|
|
75
|
+
*
|
|
76
|
+
* Not flipped to off by default: a FAB wrapped in nothing would lose the only hover
|
|
77
|
+
* hint it has, silently, in every app already on it. And the kit cannot wrap itself
|
|
78
|
+
* in its `Tooltip` — the bubble's anchor is a `relative inline-flex` span, and the
|
|
79
|
+
* button is `fixed` and portalled — so the one-tooltip caller wraps it.
|
|
80
|
+
*/
|
|
81
|
+
nativeTitle?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Make it a toggle. `true`/`false` set `aria-pressed` and swap the solid brand disc for
|
|
84
|
+
* a surface disc — the glyph in brand on the quiet brand fill when on, the secondary
|
|
85
|
+
* text colour when off — the "on" look of `IconButton`'s `pressed` and of a selected
|
|
86
|
+
* Chip. Left out, it is the ordinary action FAB. For keksdose's corner filter toggles
|
|
87
|
+
* (feedback-page's "awaiting only", the /transactions pending and upcoming toggles),
|
|
88
|
+
* which paint `text-[var(--brand)]` on a surface button by hand. Keep `label` the same
|
|
89
|
+
* in both states; `aria-pressed` already says which one it is in.
|
|
90
|
+
*/
|
|
91
|
+
pressed?: boolean;
|
|
68
92
|
ref?: Ref<HTMLButtonElement>;
|
|
69
93
|
}
|
|
70
94
|
|
|
@@ -90,12 +114,18 @@ export function FloatingActionButton({
|
|
|
90
114
|
icon,
|
|
91
115
|
corner = "bottom-end",
|
|
92
116
|
offset = "1rem",
|
|
117
|
+
nativeTitle = true,
|
|
118
|
+
pressed,
|
|
93
119
|
className,
|
|
94
120
|
style,
|
|
95
121
|
type = "button",
|
|
122
|
+
title,
|
|
96
123
|
...rest
|
|
97
124
|
}: FloatingActionButtonProps) {
|
|
98
125
|
const [marker, dir] = usePortalDir();
|
|
126
|
+
const toggle = pressed !== undefined;
|
|
127
|
+
// An empty title is "no tooltip", not an attribute with nothing in it.
|
|
128
|
+
const nativeTip = nativeTitle && title !== "" ? (title ?? label) : undefined;
|
|
99
129
|
return (
|
|
100
130
|
<>
|
|
101
131
|
<span ref={marker} hidden />
|
|
@@ -106,11 +136,20 @@ export function FloatingActionButton({
|
|
|
106
136
|
type={type}
|
|
107
137
|
dir={dir}
|
|
108
138
|
aria-label={label}
|
|
109
|
-
|
|
139
|
+
aria-pressed={pressed ?? rest["aria-pressed"]}
|
|
140
|
+
title={nativeTip}
|
|
110
141
|
style={{ ...cornerStyle(corner, offset), ...style }}
|
|
111
142
|
className={cn(
|
|
112
143
|
"fixed z-40 inline-flex size-12 items-center justify-center rounded-full",
|
|
113
|
-
"
|
|
144
|
+
"shadow-lg transition-colors",
|
|
145
|
+
!toggle && "bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)]",
|
|
146
|
+
// A toggle sits on the page's surface — a solid brand disc would read as
|
|
147
|
+
// "on" in both states — and says "on" in the brand family.
|
|
148
|
+
toggle && "border border-[var(--border)]",
|
|
149
|
+
pressed === false &&
|
|
150
|
+
"bg-[var(--bg-surface)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]",
|
|
151
|
+
pressed === true &&
|
|
152
|
+
"border-[var(--brand)] bg-[var(--brand-bg)] text-[var(--brand)] hover:bg-[var(--brand-bg-hover)]",
|
|
114
153
|
"outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-2",
|
|
115
154
|
"[&_svg]:size-6",
|
|
116
155
|
className,
|
|
@@ -141,6 +180,9 @@ export interface FloatingPanelProps {
|
|
|
141
180
|
/** The FAB's distance above the nav — see {@link FloatingActionButtonProps.offset}.
|
|
142
181
|
* From `md` up the card sits above the FAB, so it moves with it. */
|
|
143
182
|
offset?: string;
|
|
183
|
+
/** The FAB's native `title` — see {@link FloatingActionButtonProps.nativeTitle}.
|
|
184
|
+
* `false` for keksdose's assistant launcher, under its one-tooltip rule (dev#523). */
|
|
185
|
+
fabNativeTitle?: boolean;
|
|
144
186
|
/** Default: `floatingPanel.close` from the {@link UiKitProvider}, else "Close". */
|
|
145
187
|
closeLabel?: string;
|
|
146
188
|
/**
|
|
@@ -196,6 +238,7 @@ export function FloatingPanel({
|
|
|
196
238
|
onOpenChange,
|
|
197
239
|
corner = "bottom-end",
|
|
198
240
|
offset = "1rem",
|
|
241
|
+
fabNativeTitle,
|
|
199
242
|
closeLabel,
|
|
200
243
|
initialFocus,
|
|
201
244
|
className,
|
|
@@ -264,6 +307,7 @@ export function FloatingPanel({
|
|
|
264
307
|
icon={fabIcon}
|
|
265
308
|
corner={corner}
|
|
266
309
|
offset={offset}
|
|
310
|
+
nativeTitle={fabNativeTitle}
|
|
267
311
|
aria-expanded={open}
|
|
268
312
|
aria-controls={open ? panelId : undefined}
|
|
269
313
|
onClick={() => {
|
|
@@ -123,9 +123,14 @@ export function FullBleedDialog({
|
|
|
123
123
|
|
|
124
124
|
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
|
125
125
|
// The caller's handler first (keksdose's transaction-search keeps its list keys on
|
|
126
|
-
// the dialog); `preventDefault()`
|
|
126
|
+
// the dialog); a `preventDefault()` IN THAT HANDLER claims the key, Escape included.
|
|
127
|
+
// Only there: a descendant's preventDefault does not count. A SearchField in the
|
|
128
|
+
// header prevents Escape to stop Chromium clearing a type=search input, and in
|
|
129
|
+
// 0.8.0 that silently stopped Escape from closing the dialog (keksdose).
|
|
130
|
+
const preventedBefore = e.defaultPrevented;
|
|
127
131
|
onKeyDown?.(e);
|
|
128
|
-
|
|
132
|
+
const claimedByCaller = !preventedBefore && e.defaultPrevented;
|
|
133
|
+
if (claimedByCaller || e.key !== "Escape") return;
|
|
129
134
|
// On the panel, the way `Modal` does it, and NOT through `useEscapeKey`: this
|
|
130
135
|
// dialog is the thing a `PickerSheet` opens on top of, and a document-level
|
|
131
136
|
// listener cannot tell which of the two the user meant. One press would dismiss
|
|
@@ -3,8 +3,30 @@ import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from "../i18n/kit-labels";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* `income` / `expense` are the money pair (`--money-income` / `--money-expense`), the
|
|
8
|
+
* names `Chip`, `StatTile` and `Sparkline` already use for them: a share bar of what
|
|
9
|
+
* came in or went out is money, and painting it `success` / `warning` said "good" and
|
|
10
|
+
* "careful" about a figure that is neither. keksdose's report meters (food-group-card,
|
|
11
|
+
* drilldown-modal) sit beside amounts that are already in the money colours.
|
|
12
|
+
*/
|
|
13
|
+
export type ProgressBarTone =
|
|
14
|
+
| "brand"
|
|
15
|
+
| "neutral"
|
|
16
|
+
| "success"
|
|
17
|
+
| "warning"
|
|
18
|
+
| "danger"
|
|
19
|
+
| "info"
|
|
20
|
+
| "income"
|
|
21
|
+
| "expense";
|
|
22
|
+
/**
|
|
23
|
+
* Track heights: `sm` 4px, `slim` 6px, `md` 8px, `lg` 12px. `slim` is a word and not a
|
|
24
|
+
* letter because the letters are taken in order — an `xs` at 6px would be THICKER than
|
|
25
|
+
* `sm` — and renaming `sm` would move every bar already on it. 6px is the Slider's
|
|
26
|
+
* track and keksdose's hand-drawn meters (`h-1.5`, landing-visuals), for a bar under a
|
|
27
|
+
* line of text where 8px outweighs the text.
|
|
28
|
+
*/
|
|
29
|
+
export type ProgressBarSize = "sm" | "slim" | "md" | "lg";
|
|
8
30
|
|
|
9
31
|
const FILL: Record<ProgressBarTone, string> = {
|
|
10
32
|
brand: "bg-[var(--brand)]",
|
|
@@ -13,9 +35,11 @@ const FILL: Record<ProgressBarTone, string> = {
|
|
|
13
35
|
warning: "bg-[var(--warning)]",
|
|
14
36
|
danger: "bg-[var(--danger)]",
|
|
15
37
|
info: "bg-[var(--info)]",
|
|
38
|
+
income: "bg-[var(--money-income)]",
|
|
39
|
+
expense: "bg-[var(--money-expense)]",
|
|
16
40
|
};
|
|
17
41
|
|
|
18
|
-
const TRACK_HEIGHT: Record<ProgressBarSize, string> = { sm: "h-1", md: "h-2", lg: "h-3" };
|
|
42
|
+
const TRACK_HEIGHT: Record<ProgressBarSize, string> = { sm: "h-1", slim: "h-1.5", md: "h-2", lg: "h-3" };
|
|
19
43
|
|
|
20
44
|
export interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "role"> {
|
|
21
45
|
/** Where it stands. Leave it undefined for an INDETERMINATE bar — work is under way
|
|
@@ -389,6 +389,9 @@ export function Treemap({
|
|
|
389
389
|
name: d.name,
|
|
390
390
|
size: d.value,
|
|
391
391
|
fill: d.fill ?? ramp[idx % ramp.length],
|
|
392
|
+
// recharts hands each node's fields to its cell; a field left out of this map
|
|
393
|
+
// never reaches `TreemapCell` (0.8.0 shipped `labelColor` without it — keksdose).
|
|
394
|
+
labelColor: d.labelColor,
|
|
392
395
|
}))
|
|
393
396
|
.filter((n) => Number.isFinite(n.size) && n.size > 0)
|
|
394
397
|
.slice(0, maxTiles ?? undefined);
|
package/src/components/ui.tsx
CHANGED
|
@@ -84,6 +84,13 @@ export function buttonClasses(
|
|
|
84
84
|
return cn(BUTTON_BASE, BUTTON_SIZES[size], buttonVariantClasses[variant], className);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* ⚠️ No default `type`: like a native `<button>`, a `Button` inside a `<form>` SUBMITS
|
|
89
|
+
* it unless you pass `type="button"`. Kept native on purpose — a form's own submit
|
|
90
|
+
* button relies on it, and changing the default would silently stop those forms from
|
|
91
|
+
* submitting. Pass `type="button"` for every other action in a form (keksdose had six
|
|
92
|
+
* link buttons submitting their form).
|
|
93
|
+
*/
|
|
87
94
|
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
88
95
|
variant?: ButtonVariant;
|
|
89
96
|
/** `md` (default) is the page's action button. `sm` is the compact one — 12px text
|
|
@@ -147,27 +154,50 @@ const ICON_BUTTON_SIZES: Record<IconButtonSize, string> = {
|
|
|
147
154
|
};
|
|
148
155
|
|
|
149
156
|
// A tone re-colours the glyph without changing what the variant draws around it.
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
157
|
+
// Each coloured tone comes in two resting looks, picked by `quiet` (see the prop):
|
|
158
|
+
// QUIET is placeholder grey until the pointer or focus arrives, then the tone's
|
|
159
|
+
// family; TONED wears the tone's colour at rest. The defaults are the looks each tone
|
|
160
|
+
// had before `quiet` existed: `danger` is quiet — an action in every row of a list
|
|
161
|
+
// must not shout from every row, so the red arrives only on the one row you are about
|
|
162
|
+
// to act on — while `warning` and `info` are toned, because they mark the ONE thing
|
|
163
|
+
// on the row to notice (keksdose's "needs review" flag, its reconcile action at
|
|
164
|
+
// accounts-page:867), and quiet grey would hide the very thing they point out.
|
|
165
|
+
// `muted` is quiet by definition and has no toned look; `default` has no tone.
|
|
166
|
+
export type IconButtonTone = "default" | "muted" | "danger" | "warning" | "info";
|
|
167
|
+
|
|
168
|
+
type ColouredTone = "danger" | "warning" | "info";
|
|
169
|
+
|
|
170
|
+
const ICON_BUTTON_TONES: Record<ColouredTone, { quiet: string; toned: string }> = {
|
|
171
|
+
danger: {
|
|
172
|
+
quiet:
|
|
173
|
+
"text-[var(--text-placeholder)] hover:bg-[var(--danger-bg)] hover:text-[var(--danger)] focus:ring-[var(--danger-border)]",
|
|
174
|
+
toned: "text-[var(--danger)] hover:bg-[var(--danger-bg)] focus:ring-[var(--danger-border)]",
|
|
175
|
+
},
|
|
176
|
+
warning: {
|
|
177
|
+
quiet:
|
|
178
|
+
"text-[var(--text-placeholder)] hover:bg-[var(--warning-bg)] hover:text-[var(--warning)] focus:ring-[var(--warning-border)]",
|
|
179
|
+
toned: "text-[var(--warning)] hover:bg-[var(--warning-bg)] focus:ring-[var(--warning-border)]",
|
|
180
|
+
},
|
|
181
|
+
info: {
|
|
182
|
+
quiet:
|
|
183
|
+
"text-[var(--text-placeholder)] hover:bg-[var(--info-bg)] hover:text-[var(--info)] focus:ring-[var(--info-border)]",
|
|
184
|
+
toned: "text-[var(--info)] hover:bg-[var(--info-bg)] focus:ring-[var(--info-border)]",
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const ICON_BUTTON_MUTED =
|
|
189
|
+
"text-[var(--text-placeholder)] hover:bg-[var(--bg-surface-2)] hover:text-[var(--text-primary)]";
|
|
190
|
+
|
|
191
|
+
/** Which tones sit quiet at rest when `quiet` is left out. */
|
|
192
|
+
const QUIET_BY_DEFAULT: Record<ColouredTone, boolean> = { danger: true, warning: false, info: false };
|
|
193
|
+
|
|
194
|
+
function iconButtonToneClass(tone: IconButtonTone, quiet: boolean | undefined): string {
|
|
195
|
+
if (tone === "default") return "";
|
|
196
|
+
if (tone === "muted") return cn(ICON_BUTTON_MUTED, ICON_BUTTON_QUIET_DISABLED_REST);
|
|
197
|
+
const isQuiet = quiet ?? QUIET_BY_DEFAULT[tone];
|
|
198
|
+
const look = ICON_BUTTON_TONES[tone];
|
|
199
|
+
return isQuiet ? cn(look.quiet, ICON_BUTTON_QUIET_DISABLED_REST) : look.toned;
|
|
200
|
+
}
|
|
171
201
|
|
|
172
202
|
// A disabled button must not answer the pointer. The hover classes above are plain
|
|
173
203
|
// `hover:` (so a caller's `className="hover:…"` still replaces them through
|
|
@@ -187,11 +217,9 @@ const ICON_BUTTON_DISABLED_REST: Record<ButtonVariant | "overlay", string> = {
|
|
|
187
217
|
overlay: "disabled:hover:bg-[color-mix(in_srgb,var(--bg-inverse)_60%,transparent)]",
|
|
188
218
|
};
|
|
189
219
|
|
|
190
|
-
// The
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
danger: "disabled:hover:text-[var(--text-placeholder)]",
|
|
194
|
-
};
|
|
220
|
+
// The quiet looks change the glyph on hover, so they pin their resting glyph the
|
|
221
|
+
// same way. A toned look keeps its glyph on hover and needs no pin.
|
|
222
|
+
const ICON_BUTTON_QUIET_DISABLED_REST = "disabled:hover:text-[var(--text-placeholder)]";
|
|
195
223
|
|
|
196
224
|
// `pressed`: a toggle that is on. The brand glyph on the quiet brand fill — the
|
|
197
225
|
// "selected" look of a Chip or a SegmentedControl option, so an on toggle reads as on
|
|
@@ -222,8 +250,26 @@ export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>
|
|
|
222
250
|
* hover. `danger`: the same grey at rest, `--danger` on hover and focus — for a
|
|
223
251
|
* remove/delete that repeats down a list. `warning`: amber at rest — a flag that
|
|
224
252
|
* wants attention. `info`: sky at rest — a notice-worthy but harmless action
|
|
225
|
-
* (keksdose's reconcile). Default: the variant's own colours.
|
|
226
|
-
tone
|
|
253
|
+
* (keksdose's reconcile). Default: the variant's own colours. See `quiet` for
|
|
254
|
+
* turning a coloured tone's resting look the other way. */
|
|
255
|
+
tone?: IconButtonTone;
|
|
256
|
+
/**
|
|
257
|
+
* Whether a coloured tone (`danger`, `warning`, `info`) waits for the pointer:
|
|
258
|
+
* `true` is placeholder grey at rest and the tone's colour on hover and focus;
|
|
259
|
+
* `false` wears the tone's colour at rest. Left out, each tone keeps its own
|
|
260
|
+
* default — `danger` quiet, `warning` and `info` not. Ignored for `muted` (quiet by
|
|
261
|
+
* definition) and `default` (no tone).
|
|
262
|
+
*
|
|
263
|
+
* `quiet={false}` on `danger` is for a destructive action that stands ALONE, where
|
|
264
|
+
* hover-to-reveal hides it: keksdose's phone bulk bar (mobile-bulk-bar.tsx) has one
|
|
265
|
+
* "delete all" and a touch screen that never hovers, so it painted rose by hand.
|
|
266
|
+
* `quiet` on `warning`/`info` is the same switch the other way, for a flag repeated
|
|
267
|
+
* down a list. A boolean over the tone rather than a new tone (`danger-solid`) or an
|
|
268
|
+
* `emphasis` scale: there are exactly two resting looks, every coloured tone has
|
|
269
|
+
* both, and which one fits is a question about the SITE (alone or repeated, touch or
|
|
270
|
+
* pointer), not about the tone — so it is one switch the family shares.
|
|
271
|
+
*/
|
|
272
|
+
quiet?: boolean;
|
|
227
273
|
/**
|
|
228
274
|
* Make it a toggle button. `true` sets `aria-pressed="true"` and draws the "on" look
|
|
229
275
|
* (brand glyph on the quiet brand fill); `false` sets `aria-pressed="false"` with the
|
|
@@ -262,7 +308,7 @@ export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>
|
|
|
262
308
|
}
|
|
263
309
|
|
|
264
310
|
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(function IconButton(
|
|
265
|
-
{ variant = "ghost", size = "md", tone = "default", pressed, stopPropagation, className, onClick, onKeyDown, ...rest },
|
|
311
|
+
{ variant = "ghost", size = "md", tone = "default", quiet, pressed, stopPropagation, className, onClick, onKeyDown, ...rest },
|
|
266
312
|
ref,
|
|
267
313
|
) {
|
|
268
314
|
return (
|
|
@@ -286,8 +332,7 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(functio
|
|
|
286
332
|
// After the size, so the overlay's `rounded-full` beats the small sizes' `rounded`.
|
|
287
333
|
variant === "overlay" ? ICON_BUTTON_OVERLAY : buttonVariantClasses[variant],
|
|
288
334
|
ICON_BUTTON_DISABLED_REST[variant],
|
|
289
|
-
|
|
290
|
-
ICON_BUTTON_TONES_DISABLED_REST[tone],
|
|
335
|
+
iconButtonToneClass(tone, quiet),
|
|
291
336
|
pressed && ICON_BUTTON_PRESSED,
|
|
292
337
|
className,
|
|
293
338
|
)}
|
|
@@ -1327,13 +1372,24 @@ export function Spinner({ className, label, ...rest }: SpinnerProps) {
|
|
|
1327
1372
|
);
|
|
1328
1373
|
}
|
|
1329
1374
|
|
|
1330
|
-
export interface EmptyStateProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
|
1375
|
+
export interface EmptyStateProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "title"> {
|
|
1331
1376
|
/** The box renders `title` and `hint` in its own two-line rhythm, which is what makes
|
|
1332
1377
|
* every empty state in three apps look like the same thing — so there is no
|
|
1333
1378
|
* `children` slot to put arbitrary content in. The two slots below are the only
|
|
1334
1379
|
* other things an empty state has turned out to need, and each has a fixed place. */
|
|
1335
|
-
title
|
|
1336
|
-
|
|
1380
|
+
/** Nodes rather than strings, so a title can carry a code, a link or an emphasised
|
|
1381
|
+
* word — keksdose's error boundary shows the error's name in `<code>` — while the
|
|
1382
|
+
* box still sets the type. */
|
|
1383
|
+
title: ReactNode;
|
|
1384
|
+
hint?: ReactNode;
|
|
1385
|
+
/**
|
|
1386
|
+
* Render the title as a heading of this level. Left out it is a `<div>`, as before:
|
|
1387
|
+
* most empty states are a message inside a section that already has its heading.
|
|
1388
|
+
* keksdose's error boundary IS the page when it trips, and its `<h2>` went missing
|
|
1389
|
+
* when it moved onto EmptyState — so the page's heading outline lost the one line
|
|
1390
|
+
* that says what happened. Only the element changes; the look is the box's.
|
|
1391
|
+
*/
|
|
1392
|
+
headingAs?: "h2" | "h3" | "h4" | "h5" | "h6";
|
|
1337
1393
|
/** A glyph ABOVE the title — kastlan's InboxEmptyState (an inbox), keksdose's
|
|
1338
1394
|
* offline card (a cloud with a slash). Sized by the box (`[&_svg]:size-8`) and
|
|
1339
1395
|
* muted, so five call sites cannot pick five sizes; hidden from assistive tech,
|
|
@@ -1346,7 +1402,8 @@ export interface EmptyStateProps extends Omit<ComponentPropsWithoutRef<"div">, "
|
|
|
1346
1402
|
action?: ReactNode;
|
|
1347
1403
|
}
|
|
1348
1404
|
|
|
1349
|
-
export function EmptyState({ title, hint, icon, action, className, ...rest }: EmptyStateProps) {
|
|
1405
|
+
export function EmptyState({ title, hint, icon, action, headingAs, className, ...rest }: EmptyStateProps) {
|
|
1406
|
+
const Title = headingAs ?? "div";
|
|
1350
1407
|
return (
|
|
1351
1408
|
<div
|
|
1352
1409
|
{...rest}
|
|
@@ -1360,8 +1417,9 @@ export function EmptyState({ title, hint, icon, action, className, ...rest }: Em
|
|
|
1360
1417
|
{icon}
|
|
1361
1418
|
</div>
|
|
1362
1419
|
)}
|
|
1363
|
-
|
|
1364
|
-
|
|
1420
|
+
{/* `text-sm`: a heading keeps the box's size, not whatever a stylesheet gives h2. */}
|
|
1421
|
+
<Title className="text-sm font-medium text-[var(--text-secondary)]">{title}</Title>
|
|
1422
|
+
{hint != null && hint !== false && hint !== "" && <div className="mt-1 text-xs">{hint}</div>}
|
|
1365
1423
|
{action != null && (
|
|
1366
1424
|
<div className="mt-4 flex flex-wrap items-center justify-center gap-2">{action}</div>
|
|
1367
1425
|
)}
|