@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
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ComponentType, MouseEvent, ReactElement, ReactNode } from "react";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface NavPillItem<T extends string> {
|
|
5
|
+
value: T;
|
|
6
|
+
label: ReactNode;
|
|
7
|
+
/** Where the pill leads. With one it is a link (see `renderLink`); without, a button
|
|
8
|
+
* that calls `onSelect`. */
|
|
9
|
+
href?: string;
|
|
10
|
+
/** A Lucide icon (or any component taking a `className`), before the label. */
|
|
11
|
+
icon?: ComponentType<{ className?: string; "aria-hidden"?: boolean }>;
|
|
12
|
+
/** Buttons only — a link cannot be disabled. */
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** What {@link NavPillsProps.renderLink} is handed. Spread it onto your router's link
|
|
17
|
+
* — `({ href, ...p }) => <Link to={href} {...p} />`. */
|
|
18
|
+
export interface NavPillLinkProps {
|
|
19
|
+
href: string;
|
|
20
|
+
className: string;
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
"aria-current": NavPillsCurrent | undefined;
|
|
23
|
+
onClick: (e: MouseEvent<HTMLAnchorElement>) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The `aria-current` token the current pill carries. */
|
|
27
|
+
export type NavPillsCurrent = "page" | "step" | "location" | "true";
|
|
28
|
+
|
|
29
|
+
export interface NavPillsProps<T extends string> extends Omit<ComponentPropsWithoutRef<"nav">, "children" | "onSelect"> {
|
|
30
|
+
items: NavPillItem<T>[];
|
|
31
|
+
/** The pill that is current. `null` or a value not in `items`: none is. */
|
|
32
|
+
current: T | null;
|
|
33
|
+
/** Called with the pill's value on a click — of a button, and of a link too (before
|
|
34
|
+
* the router follows it), for a caller that keeps the choice in state as well. */
|
|
35
|
+
onSelect?: (value: T) => void;
|
|
36
|
+
/** Your router's link. Default `<a>`. The API `Breadcrumbs`, `Chip` and `ListItem` share. */
|
|
37
|
+
renderLink?: (props: NavPillLinkProps) => ReactElement;
|
|
38
|
+
/** What `aria-current` says on the current pill. Default `page` where the items
|
|
39
|
+
* are links and `true` where they are buttons (keksdose's swipe-surface picker
|
|
40
|
+
* switches a panel on the same page, which is not a page). */
|
|
41
|
+
currentType?: NavPillsCurrent;
|
|
42
|
+
/** `sm`: 12px pills — the compact size of a {@link ToggleGroup}. Default `md`. */
|
|
43
|
+
size?: "sm" | "md";
|
|
44
|
+
/** Render a `<nav>` landmark (default) or a plain `<div role="group">` for a picker
|
|
45
|
+
* that is not the page's navigation. Name either with `aria-label`. */
|
|
46
|
+
landmark?: boolean;
|
|
47
|
+
/** Classes for every pill. */
|
|
48
|
+
itemClassName?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The pill is a ToggleGroup segment's (`rounded`, `px-3 py-1.5`, the inverse fill for
|
|
52
|
+
// the current one), so a strip of these beside a segmented control reads as the same
|
|
53
|
+
// family. Unlike the segments, a pill never truncates: the strip WRAPS instead.
|
|
54
|
+
const PILL =
|
|
55
|
+
"inline-flex items-center gap-1.5 whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--border-strong)] [&_svg]:size-4 [&_svg]:shrink-0";
|
|
56
|
+
const PILL_SM = "gap-1 px-2 py-1 text-xs [&_svg]:size-3.5";
|
|
57
|
+
const PILL_CURRENT = "bg-[var(--bg-inverse)] text-[var(--text-inverse)]";
|
|
58
|
+
const PILL_IDLE = "text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]";
|
|
59
|
+
const PILL_DISABLED = "cursor-not-allowed opacity-50 hover:bg-transparent hover:text-[var(--text-secondary)]";
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A set of links or buttons that WRAPS onto as many rows as it needs, the current one
|
|
63
|
+
* filled and marked `aria-current`.
|
|
64
|
+
*
|
|
65
|
+
* keksdose's swipe-settings card (swipe-settings-card:152) hand-rolled this: four
|
|
66
|
+
* surface names that do not fit a 406px phone, and a sideways scroll would hide the
|
|
67
|
+
* ones that do not fit behind a gesture nothing announces (live #332: *"Have the tabs
|
|
68
|
+
* in more rows, rather than scrolling sideways"*). It is not {@link Tabs} with `wrap`
|
|
69
|
+
* because its items are not tabs: a `role="tab"` promises a tabpanel, a roving tab
|
|
70
|
+
* stop and arrow keys, and a set of links to pages (or of buttons that switch the rest
|
|
71
|
+
* of a card) promises none of that. So every pill is its own tab stop, the set is a
|
|
72
|
+
* list (a screen reader hears "list, 4 items"), and "current" is `aria-current` — the
|
|
73
|
+
* attribute a link to where you already are carries.
|
|
74
|
+
*/
|
|
75
|
+
export function NavPills<T extends string>({
|
|
76
|
+
items,
|
|
77
|
+
current,
|
|
78
|
+
onSelect,
|
|
79
|
+
renderLink,
|
|
80
|
+
currentType,
|
|
81
|
+
size = "md",
|
|
82
|
+
landmark = true,
|
|
83
|
+
itemClassName,
|
|
84
|
+
className,
|
|
85
|
+
...rest
|
|
86
|
+
}: NavPillsProps<T>) {
|
|
87
|
+
const Root = landmark ? "nav" : "div";
|
|
88
|
+
return (
|
|
89
|
+
<Root {...rest} role={landmark ? undefined : "group"} className={cn("min-w-0", className)}>
|
|
90
|
+
<ul className="m-0 flex list-none flex-wrap gap-1 p-0">
|
|
91
|
+
{items.map((item) => {
|
|
92
|
+
const isCurrent = item.value === current;
|
|
93
|
+
const Icon = item.icon;
|
|
94
|
+
const pillClass = cn(
|
|
95
|
+
PILL,
|
|
96
|
+
size === "sm" && PILL_SM,
|
|
97
|
+
isCurrent ? PILL_CURRENT : PILL_IDLE,
|
|
98
|
+
item.disabled && item.href === undefined && PILL_DISABLED,
|
|
99
|
+
itemClassName,
|
|
100
|
+
);
|
|
101
|
+
const children = (
|
|
102
|
+
<>
|
|
103
|
+
{Icon && <Icon aria-hidden />}
|
|
104
|
+
{item.label}
|
|
105
|
+
</>
|
|
106
|
+
);
|
|
107
|
+
let pill: ReactNode;
|
|
108
|
+
if (item.href !== undefined) {
|
|
109
|
+
const props: NavPillLinkProps = {
|
|
110
|
+
href: item.href,
|
|
111
|
+
className: pillClass,
|
|
112
|
+
children,
|
|
113
|
+
"aria-current": isCurrent ? (currentType ?? "page") : undefined,
|
|
114
|
+
onClick: () => onSelect?.(item.value),
|
|
115
|
+
};
|
|
116
|
+
pill = renderLink ? (
|
|
117
|
+
renderLink(props)
|
|
118
|
+
) : (
|
|
119
|
+
<a href={props.href} className={props.className} aria-current={props["aria-current"]} onClick={props.onClick}>
|
|
120
|
+
{props.children}
|
|
121
|
+
</a>
|
|
122
|
+
);
|
|
123
|
+
} else {
|
|
124
|
+
pill = (
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
disabled={item.disabled}
|
|
128
|
+
aria-current={isCurrent ? (currentType ?? "true") : undefined}
|
|
129
|
+
onClick={() => onSelect?.(item.value)}
|
|
130
|
+
className={pillClass}
|
|
131
|
+
>
|
|
132
|
+
{children}
|
|
133
|
+
</button>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return (
|
|
137
|
+
<li key={item.value} className="flex">
|
|
138
|
+
{pill}
|
|
139
|
+
</li>
|
|
140
|
+
);
|
|
141
|
+
})}
|
|
142
|
+
</ul>
|
|
143
|
+
</Root>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -176,14 +176,14 @@ export function PageContents({
|
|
|
176
176
|
<li key={item.id} data-entry={item.id}>
|
|
177
177
|
<a
|
|
178
178
|
href={hrefFor(item.id)}
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
|
|
179
|
+
// Entries WRAP in the rail rather than truncate: a cut-off heading needed a
|
|
180
|
+
// native `title` to be read at all, which a keyboard never sees and which
|
|
181
|
+
// broke keksdose's one-tooltip rule (dev#523).
|
|
182
182
|
aria-current={current ? "location" : undefined}
|
|
183
183
|
onClick={() => setClicked(item.id)}
|
|
184
184
|
className={cn(
|
|
185
185
|
"-ms-px block border-s-2 py-1 pe-1 text-[13px] leading-snug transition-colors duration-150",
|
|
186
|
-
variant === "rail" && "
|
|
186
|
+
variant === "rail" && "[overflow-wrap:anywhere]",
|
|
187
187
|
item.level === 2 ? "ps-6" : "ps-3",
|
|
188
188
|
current
|
|
189
189
|
? "border-[var(--brand)] font-medium text-[var(--brand)]"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ElementType, ReactNode } from "react";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
import { SECTION_LABEL_CLASS } from "./text";
|
|
4
|
+
|
|
5
|
+
/** `md` (default) is kastlan's `text-2xl` page title from `sm` up, a step smaller on a
|
|
6
|
+
* phone; `sm` is lenkbank's `text-lg` title on a dense tool page (projects-page.tsx). */
|
|
7
|
+
export type PageHeaderSize = "sm" | "md";
|
|
8
|
+
|
|
9
|
+
const TITLE: Record<PageHeaderSize, string> = {
|
|
10
|
+
sm: "text-lg font-semibold",
|
|
11
|
+
md: "text-xl font-semibold tracking-tight sm:text-2xl",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export interface PageHeaderProps extends Omit<ComponentPropsWithoutRef<"div">, "title"> {
|
|
15
|
+
title: ReactNode;
|
|
16
|
+
/** A sentence under the title. */
|
|
17
|
+
description?: ReactNode;
|
|
18
|
+
/** Buttons for the page — "New", "Export". At the title's end from `sm` up; under it,
|
|
19
|
+
* wrapping, on a phone. */
|
|
20
|
+
actions?: ReactNode;
|
|
21
|
+
/** A `Breadcrumbs` trail, drawn above everything else. */
|
|
22
|
+
breadcrumbs?: ReactNode;
|
|
23
|
+
/** A small uppercase line above the title — the section or the kind of record
|
|
24
|
+
* ("Building", "Settings"). The section-label type. */
|
|
25
|
+
eyebrow?: ReactNode;
|
|
26
|
+
/** The title's element. Default `h1`: a page has one, and this is it. `h2` for a
|
|
27
|
+
* header inside a page that already has its `h1` (a tab's own header). */
|
|
28
|
+
as?: "h1" | "h2" | "h3";
|
|
29
|
+
size?: PageHeaderSize;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A page's head: optional breadcrumbs, an eyebrow, the title, a description, and the
|
|
34
|
+
* page's actions.
|
|
35
|
+
*
|
|
36
|
+
* kastlan's (shared/components/display/page-header.tsx) is the model — title and
|
|
37
|
+
* description on one side, actions on the other, stacked on a phone — and this adds
|
|
38
|
+
* what its pages kept writing around it: a trail above, an eyebrow, a `ReactNode`
|
|
39
|
+
* title (a status chip beside the name), and actions that WRAP when there are more
|
|
40
|
+
* than fit a phone's width, instead of running off it. No outer margin: the page's
|
|
41
|
+
* own rhythm (`space-y-*`, a grid gap) places it.
|
|
42
|
+
*/
|
|
43
|
+
export function PageHeader({
|
|
44
|
+
title,
|
|
45
|
+
description,
|
|
46
|
+
actions,
|
|
47
|
+
breadcrumbs,
|
|
48
|
+
eyebrow,
|
|
49
|
+
as = "h1",
|
|
50
|
+
size = "md",
|
|
51
|
+
className,
|
|
52
|
+
...rest
|
|
53
|
+
}: PageHeaderProps) {
|
|
54
|
+
const Heading = as as ElementType;
|
|
55
|
+
return (
|
|
56
|
+
<div {...rest} className={cn("flex min-w-0 flex-col gap-2", className)}>
|
|
57
|
+
{breadcrumbs}
|
|
58
|
+
<div className="flex min-w-0 flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4">
|
|
59
|
+
<div className="min-w-0 flex-1">
|
|
60
|
+
{eyebrow != null && <p className={cn(SECTION_LABEL_CLASS.xs, "mb-1")}>{eyebrow}</p>}
|
|
61
|
+
<Heading className={cn("break-words text-[var(--text-primary)]", TITLE[size])}>{title}</Heading>
|
|
62
|
+
{description != null && <p className="mt-1 text-sm text-[var(--text-muted)]">{description}</p>}
|
|
63
|
+
</div>
|
|
64
|
+
{actions != null && <div className="flex shrink-0 flex-wrap items-center gap-2">{actions}</div>}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -41,6 +41,49 @@ const FILL: Record<ProgressBarTone, string> = {
|
|
|
41
41
|
|
|
42
42
|
const TRACK_HEIGHT: Record<ProgressBarSize, string> = { sm: "h-1", slim: "h-1.5", md: "h-2", lg: "h-3" };
|
|
43
43
|
|
|
44
|
+
// A segment with no tone of its own takes the next categorical chart colour, so a
|
|
45
|
+
// stacked bar of unnamed parts is still a set of tellable parts rather than one fill.
|
|
46
|
+
const SERIES_FILL = [
|
|
47
|
+
"bg-[var(--chart-1)]",
|
|
48
|
+
"bg-[var(--chart-2)]",
|
|
49
|
+
"bg-[var(--chart-3)]",
|
|
50
|
+
"bg-[var(--chart-4)]",
|
|
51
|
+
"bg-[var(--chart-5)]",
|
|
52
|
+
"bg-[var(--chart-6)]",
|
|
53
|
+
"bg-[var(--chart-7)]",
|
|
54
|
+
"bg-[var(--chart-8)]",
|
|
55
|
+
"bg-[var(--chart-9)]",
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/** Each segment's fill classes: its own classes, else its tone, else (with no `color`)
|
|
59
|
+
* the next chart colour. "Next" counts the UNCOLOURED segments only — a segment with
|
|
60
|
+
* its own `className`/`tone`/`color` does not use up `--chart-1`, so an "Other" in grey
|
|
61
|
+
* ahead of two plain parts leaves them `--chart-1` and `--chart-2`. */
|
|
62
|
+
function segmentFills(segments: ProgressBarSegment[]): Array<string | undefined> {
|
|
63
|
+
let next = 0;
|
|
64
|
+
return segments.map((seg) => {
|
|
65
|
+
if (seg.className) return seg.className;
|
|
66
|
+
if (seg.tone) return FILL[seg.tone];
|
|
67
|
+
return seg.color ? undefined : SERIES_FILL[next++ % SERIES_FILL.length];
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** One part of a stacked bar. See {@link ProgressBarProps.segments}. */
|
|
72
|
+
export interface ProgressBarSegment {
|
|
73
|
+
/** Its size, on the bar's own `min`–`max` scale. */
|
|
74
|
+
value: number;
|
|
75
|
+
/** A kit tone. Left out (and no `color`/`className`), the next `--chart-N` colour. */
|
|
76
|
+
tone?: ProgressBarTone;
|
|
77
|
+
/** Any CSS colour (`var(--token)`), for a palette the tones do not cover. Wins over `tone`. */
|
|
78
|
+
color?: string;
|
|
79
|
+
/** Classes for the segment's fill — keksdose's `STEP_CLASS` ramp. Wins over `tone`. */
|
|
80
|
+
className?: string;
|
|
81
|
+
/** The part's name: said in the bar's `aria-valuetext` and shown in the legend. */
|
|
82
|
+
label?: ReactNode;
|
|
83
|
+
/** A stable React key, when `label` is not a string. */
|
|
84
|
+
key?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
44
87
|
export interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "role"> {
|
|
45
88
|
/** Where it stands. Leave it undefined for an INDETERMINATE bar — work is under way
|
|
46
89
|
* and nobody knows how much is left (an import that reports no count). */
|
|
@@ -69,6 +112,20 @@ export interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">,
|
|
|
69
112
|
tone?: ProgressBarTone;
|
|
70
113
|
size?: ProgressBarSize;
|
|
71
114
|
locale?: string;
|
|
115
|
+
/**
|
|
116
|
+
* Draw a METER OF PARTS: one bar, several fills side by side in order, a 2px gap
|
|
117
|
+
* between them — keksdose's discretionary-spending bar (cut-card:133), a whole
|
|
118
|
+
* split into the steps of a scale. Forces `variant="meter"`: parts of a whole are
|
|
119
|
+
* never "busy". `aria-valuenow` is the sum; `aria-valuetext` names every part
|
|
120
|
+
* ("Rent: 40%, Food: 25%"), each part's value through `formatValue` and joined in the
|
|
121
|
+
* kit's locale — the words keksdose's hand-drawn bar had to hide from a reader
|
|
122
|
+
* altogether, leaving the legend as the only way in. The sum is clamped to `max`,
|
|
123
|
+
* and parts past it are cut off at the track's end.
|
|
124
|
+
*/
|
|
125
|
+
segments?: ProgressBarSegment[];
|
|
126
|
+
/** With `segments`: a list under the bar naming each part with its swatch and value
|
|
127
|
+
* — the table view of the bar, in text tokens rather than the segment colour. */
|
|
128
|
+
legend?: boolean;
|
|
72
129
|
}
|
|
73
130
|
|
|
74
131
|
/**
|
|
@@ -99,6 +156,8 @@ export function ProgressBar({
|
|
|
99
156
|
tone = "brand",
|
|
100
157
|
size = "md",
|
|
101
158
|
locale,
|
|
159
|
+
segments,
|
|
160
|
+
legend = false,
|
|
102
161
|
className,
|
|
103
162
|
"aria-label": ariaLabel,
|
|
104
163
|
"aria-labelledby": ariaLabelledBy,
|
|
@@ -108,19 +167,37 @@ export function ProgressBar({
|
|
|
108
167
|
const kitLocale = useKitLocale(locale);
|
|
109
168
|
const labelId = useId();
|
|
110
169
|
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const indeterminate = current === undefined;
|
|
170
|
+
const stacked = segments !== undefined;
|
|
171
|
+
const meter = stacked || variant === "meter";
|
|
114
172
|
const span = max - min;
|
|
173
|
+
const partValue = (v: number) => (Number.isFinite(v) ? Math.max(0, v - min) : 0);
|
|
174
|
+
const fills = stacked ? segmentFills(segments) : [];
|
|
175
|
+
const partTotal = stacked ? min + segments.reduce((sum, seg) => sum + partValue(seg.value), 0) : undefined;
|
|
176
|
+
const format = (v: number) =>
|
|
177
|
+
formatValue
|
|
178
|
+
? formatValue(v, max, min)
|
|
179
|
+
: new Intl.NumberFormat(kitLocale, { style: "percent", maximumFractionDigits: 0 }).format(
|
|
180
|
+
span <= 0 ? 0 : (Math.min(max, Math.max(min, v)) - min) / span,
|
|
181
|
+
);
|
|
182
|
+
const partTexts = stacked
|
|
183
|
+
? segments.map((seg) => {
|
|
184
|
+
const text = format(min + partValue(seg.value));
|
|
185
|
+
return typeof seg.label === "string" || typeof seg.label === "number"
|
|
186
|
+
? common.fieldValue(String(seg.label), text)
|
|
187
|
+
: text;
|
|
188
|
+
})
|
|
189
|
+
: [];
|
|
190
|
+
const current =
|
|
191
|
+
partTotal ?? (value === undefined || !Number.isFinite(value) ? (meter ? min : undefined) : value);
|
|
192
|
+
const indeterminate = current === undefined;
|
|
115
193
|
const clamped = indeterminate ? undefined : Math.min(max, Math.max(min, current));
|
|
116
194
|
const fraction = clamped === undefined || span <= 0 ? 0 : (clamped - min) / span;
|
|
117
195
|
|
|
118
|
-
const valueText =
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
:
|
|
122
|
-
|
|
123
|
-
: new Intl.NumberFormat(kitLocale, { style: "percent", maximumFractionDigits: 0 }).format(fraction);
|
|
196
|
+
const valueText = clamped === undefined ? undefined : format(clamped);
|
|
197
|
+
const ariaValueText =
|
|
198
|
+
stacked && partTexts.length > 0
|
|
199
|
+
? new Intl.ListFormat(kitLocale, { style: "short", type: "unit" }).format(partTexts)
|
|
200
|
+
: valueText;
|
|
124
201
|
|
|
125
202
|
const labelledBy = ariaLabelledBy ?? (label != null ? labelId : undefined);
|
|
126
203
|
const name = labelledBy ? undefined : (ariaLabel ?? (indeterminate ? common.loading : undefined));
|
|
@@ -151,13 +228,32 @@ export function ProgressBar({
|
|
|
151
228
|
aria-valuemin={indeterminate ? undefined : min}
|
|
152
229
|
aria-valuemax={indeterminate ? undefined : max}
|
|
153
230
|
aria-valuenow={clamped}
|
|
154
|
-
aria-valuetext={
|
|
231
|
+
aria-valuetext={ariaValueText}
|
|
155
232
|
aria-busy={!meter && indeterminate ? true : undefined}
|
|
156
233
|
// `relative` + `overflow-hidden`: the indeterminate segment is positioned in
|
|
157
234
|
// here and must not paint past the rounded ends.
|
|
158
235
|
className={cn("relative w-full overflow-hidden rounded-full bg-[var(--bg-active)]", TRACK_HEIGHT[size])}
|
|
159
236
|
>
|
|
160
|
-
{
|
|
237
|
+
{stacked ? (
|
|
238
|
+
// The parts in a flex row with a 2px gap, so two neighbouring steps of one
|
|
239
|
+
// hue are parted by the track rather than by their difference in lightness.
|
|
240
|
+
<div className="flex h-full w-full gap-0.5">
|
|
241
|
+
{segments.map((seg, i) => (
|
|
242
|
+
<div
|
|
243
|
+
key={seg.key ?? i}
|
|
244
|
+
data-part="segment"
|
|
245
|
+
className={cn(
|
|
246
|
+
"h-full shrink-0 rounded-full transition-[width] duration-300 motion-reduce:transition-none",
|
|
247
|
+
fills[i],
|
|
248
|
+
)}
|
|
249
|
+
style={{
|
|
250
|
+
width: `${span <= 0 ? 0 : (partValue(seg.value) / span) * 100}%`,
|
|
251
|
+
backgroundColor: seg.className ? undefined : seg.color,
|
|
252
|
+
}}
|
|
253
|
+
/>
|
|
254
|
+
))}
|
|
255
|
+
</div>
|
|
256
|
+
) : indeterminate ? (
|
|
161
257
|
<div
|
|
162
258
|
data-part="fill"
|
|
163
259
|
className={cn(
|
|
@@ -182,6 +278,28 @@ export function ProgressBar({
|
|
|
182
278
|
/>
|
|
183
279
|
)}
|
|
184
280
|
</div>
|
|
281
|
+
{stacked && legend && segments.length > 0 && (
|
|
282
|
+
<ul className="mt-2 space-y-1 text-sm" data-part="legend">
|
|
283
|
+
{segments.map((seg, i) => (
|
|
284
|
+
<li key={seg.key ?? i} className="flex items-center gap-2">
|
|
285
|
+
<span
|
|
286
|
+
aria-hidden
|
|
287
|
+
className={cn(
|
|
288
|
+
"size-2.5 shrink-0 rounded-sm",
|
|
289
|
+
fills[i],
|
|
290
|
+
)}
|
|
291
|
+
style={{ backgroundColor: seg.className ? undefined : seg.color }}
|
|
292
|
+
/>
|
|
293
|
+
{seg.label != null && (
|
|
294
|
+
<span className="min-w-0 flex-1 truncate text-[var(--text-secondary)]">{seg.label}</span>
|
|
295
|
+
)}
|
|
296
|
+
<span className="ms-auto shrink-0 text-xs tabular-nums text-[var(--text-muted)]">
|
|
297
|
+
{format(min + partValue(seg.value))}
|
|
298
|
+
</span>
|
|
299
|
+
</li>
|
|
300
|
+
))}
|
|
301
|
+
</ul>
|
|
302
|
+
)}
|
|
185
303
|
</div>
|
|
186
304
|
);
|
|
187
305
|
}
|
|
@@ -15,6 +15,9 @@ export interface SeriesChartLabels {
|
|
|
15
15
|
empty: string;
|
|
16
16
|
/** Accessible name of a `ToggleLegend` — a group of switches, one per series. */
|
|
17
17
|
legend: string;
|
|
18
|
+
/** Accessible name of the group of keyboard stops a clickable chart (`onPointClick`)
|
|
19
|
+
* lays over its bars or periods — one tab stop, arrow keys between them. */
|
|
20
|
+
points: string;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
export const DEFAULT_SERIES_CHART_LABELS: SeriesChartLabels = {
|
|
@@ -23,4 +26,5 @@ export const DEFAULT_SERIES_CHART_LABELS: SeriesChartLabels = {
|
|
|
23
26
|
"Drag to zoom: a roughly square selection zooms both axes, a long thin one only its own. Double-click to reset.",
|
|
24
27
|
empty: "No data",
|
|
25
28
|
legend: "Series",
|
|
29
|
+
points: "Chart values",
|
|
26
30
|
};
|