@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
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface BulkActionBarLabels {
|
|
5
|
+
/** The count, shown in the bar, spoken by its live region and used as the toolbar's
|
|
6
|
+
* name: "3 selected". */
|
|
7
|
+
selected: (count: number) => string;
|
|
8
|
+
/** The clear/cancel action's name — the X of the floating bar, the text button of the
|
|
9
|
+
* others. */
|
|
10
|
+
clear: string;
|
|
11
|
+
/** Spoken when the selection empties and the bar goes. */
|
|
12
|
+
cleared: string;
|
|
13
|
+
}
|
|
14
|
+
declare const DEFAULT_BULK_ACTION_BAR_LABELS: BulkActionBarLabels;
|
|
15
|
+
/**
|
|
16
|
+
* `floating` — the phone's: fixed above `AppShell`'s bottom nav (`--app-nav-h`) and the
|
|
17
|
+
* home indicator, inset from both sides, a rounded card with a shadow. `sticky` — the
|
|
18
|
+
* desktop's: pinned to the top of its scroll container, over the rows it acts on.
|
|
19
|
+
* `inline` — in the flow, where the caller places it.
|
|
20
|
+
*/
|
|
21
|
+
type BulkActionBarVariant = "floating" | "sticky" | "inline";
|
|
22
|
+
interface BulkActionBarProps extends Omit<ComponentPropsWithoutRef<"div">, "role" | "children"> {
|
|
23
|
+
/** How many items are selected. The bar shows only while this is above zero. */
|
|
24
|
+
count: number;
|
|
25
|
+
/** Clear the selection / leave selection mode. */
|
|
26
|
+
onClear: () => void;
|
|
27
|
+
/** The actions: buttons (`IconButton size="lg"` on a phone, `Button` from md up),
|
|
28
|
+
* in the caller's order. They are the toolbar's arrow-key stops, after the clear. */
|
|
29
|
+
children?: ReactNode;
|
|
30
|
+
/** Default `floating`. See {@link BulkActionBarVariant}. */
|
|
31
|
+
variant?: BulkActionBarVariant;
|
|
32
|
+
/** Default: `bulkActionBar` from the {@link UiKitProvider}, else English. */
|
|
33
|
+
labels?: Partial<BulkActionBarLabels>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The bar that appears while rows are selected: the count, a way out, and what can be
|
|
37
|
+
* done to all of them at once.
|
|
38
|
+
*
|
|
39
|
+
* keksdose had written it three times — the transactions phone bar
|
|
40
|
+
* (mobile-bulk-bar.tsx), the invoice lines' phone and desktop bars
|
|
41
|
+
* (invoice-lines-bulk-bar.tsx) and the payees selection bar — and the copies had
|
|
42
|
+
* drifted where it matters: two parked themselves with a guessed `bottom-16` that the
|
|
43
|
+
* third had already found leaves a 24px gap (live #314: the nav is content-sized and
|
|
44
|
+
* `AppShell` publishes its height as `--app-nav-h`), none of them announced the count,
|
|
45
|
+
* and all three were `role="toolbar"` without the arrow keys that role promises.
|
|
46
|
+
*
|
|
47
|
+
* ## Keep it mounted
|
|
48
|
+
*
|
|
49
|
+
* Render it unconditionally and let `count` decide. At zero it renders only its live
|
|
50
|
+
* region — which must already be in the DOM for the first "3 selected" to be heard —
|
|
51
|
+
* and when the count drops to zero it plays its exit before it goes, which a
|
|
52
|
+
* `{count > 0 && <Bar/>}` gate would cut off.
|
|
53
|
+
*
|
|
54
|
+
* ## The toolbar
|
|
55
|
+
*
|
|
56
|
+
* One Tab stop: ←/→ (mirrored in RTL), Home and End move between the controls, and Tab
|
|
57
|
+
* leaves — the WAI-ARIA toolbar pattern, since that is what `role="toolbar"` tells a
|
|
58
|
+
* screen reader to expect. The stop remembers the control last used. Arrow keys inside
|
|
59
|
+
* a text field stay the field's.
|
|
60
|
+
*
|
|
61
|
+
* It is NOT a dialog: no focus trap, no scroll lock — the list behind stays live,
|
|
62
|
+
* because tapping more rows is how a selection grows.
|
|
63
|
+
*/
|
|
64
|
+
declare function BulkActionBar({ count, onClear, children, variant, labels: labelsProp, className, style, onKeyDown, onFocus, ...rest }: BulkActionBarProps): react.JSX.Element;
|
|
65
|
+
|
|
66
|
+
export { BulkActionBar, type BulkActionBarLabels, type BulkActionBarProps, type BulkActionBarVariant, DEFAULT_BULK_ACTION_BAR_LABELS };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import { X } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { horizontalStep } from "../lib/direction.js";
|
|
7
|
+
import { useKitLabels } from "../i18n/kit-labels.js";
|
|
8
|
+
import { useAnnounce } from "../hooks/use-announce.js";
|
|
9
|
+
import { OVERLAY_EXIT_MS, prefersReducedMotion } from "../hooks/use-close-transition.js";
|
|
10
|
+
import { Button, IconButton } from "./ui.js";
|
|
11
|
+
const DEFAULT_BULK_ACTION_BAR_LABELS = {
|
|
12
|
+
selected: (count) => `${count} selected`,
|
|
13
|
+
clear: "Clear selection",
|
|
14
|
+
cleared: "Selection cleared"
|
|
15
|
+
};
|
|
16
|
+
const FLOATING_STYLE = {
|
|
17
|
+
bottom: "calc(max(var(--app-nav-h, 0px), env(safe-area-inset-bottom, 0px)) + 0.5rem)",
|
|
18
|
+
// Both insets on both sides: a logical side has no one physical inset to name, and
|
|
19
|
+
// the bar is symmetric, so RTL needs nothing more.
|
|
20
|
+
insetInline: "max(0.5rem, env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px))"
|
|
21
|
+
};
|
|
22
|
+
const ITEMS = "button:not([disabled]),a[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[data-toolbar-item]";
|
|
23
|
+
function ownsArrows(el) {
|
|
24
|
+
if (el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) return true;
|
|
25
|
+
if (el instanceof HTMLInputElement) return !["button", "checkbox", "radio", "submit", "reset"].includes(el.type);
|
|
26
|
+
return el.isContentEditable;
|
|
27
|
+
}
|
|
28
|
+
function BulkActionBar({
|
|
29
|
+
count,
|
|
30
|
+
onClear,
|
|
31
|
+
children,
|
|
32
|
+
variant = "floating",
|
|
33
|
+
labels: labelsProp,
|
|
34
|
+
className,
|
|
35
|
+
style,
|
|
36
|
+
onKeyDown,
|
|
37
|
+
onFocus,
|
|
38
|
+
...rest
|
|
39
|
+
}) {
|
|
40
|
+
const labels = useKitLabels("bulkActionBar", DEFAULT_BULK_ACTION_BAR_LABELS, labelsProp);
|
|
41
|
+
const { announce, regionProps } = useAnnounce();
|
|
42
|
+
const barRef = useRef(null);
|
|
43
|
+
const shown = count > 0;
|
|
44
|
+
const [shownCount, setShownCount] = useState(count);
|
|
45
|
+
const [leaving, setLeaving] = useState(false);
|
|
46
|
+
const [prevCount, setPrevCount] = useState(count);
|
|
47
|
+
if (count !== prevCount) {
|
|
48
|
+
setPrevCount(count);
|
|
49
|
+
if (shown) {
|
|
50
|
+
setShownCount(count);
|
|
51
|
+
setLeaving(false);
|
|
52
|
+
} else {
|
|
53
|
+
setLeaving(prevCount > 0 && !prefersReducedMotion());
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!leaving) return;
|
|
58
|
+
const timer = setTimeout(() => setLeaving(false), OVERLAY_EXIT_MS);
|
|
59
|
+
return () => clearTimeout(timer);
|
|
60
|
+
}, [leaving]);
|
|
61
|
+
const spoken = useRef(count);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (spoken.current === count) return;
|
|
64
|
+
spoken.current = count;
|
|
65
|
+
announce(count > 0 ? labels.selected(count) : labels.cleared);
|
|
66
|
+
}, [count, labels, announce]);
|
|
67
|
+
const active = useRef(null);
|
|
68
|
+
useLayoutEffect(() => {
|
|
69
|
+
rove(barRef.current, active);
|
|
70
|
+
});
|
|
71
|
+
const handleKeyDown = (e) => {
|
|
72
|
+
onKeyDown?.(e);
|
|
73
|
+
if (e.defaultPrevented || ownsArrows(e.target)) return;
|
|
74
|
+
const list = itemsOf(barRef.current);
|
|
75
|
+
const at = list.indexOf(document.activeElement);
|
|
76
|
+
if (at === -1 || list.length === 0) return;
|
|
77
|
+
const step = horizontalStep(e.key, barRef.current);
|
|
78
|
+
let next = -1;
|
|
79
|
+
if (step !== 0) next = (at + step + list.length) % list.length;
|
|
80
|
+
else if (e.key === "Home") next = 0;
|
|
81
|
+
else if (e.key === "End") next = list.length - 1;
|
|
82
|
+
if (next === -1) return;
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
active.current = list[next];
|
|
85
|
+
rove(barRef.current, active);
|
|
86
|
+
list[next].focus();
|
|
87
|
+
};
|
|
88
|
+
if (!shown && !leaving) {
|
|
89
|
+
return /* @__PURE__ */ jsx("span", { ...regionProps });
|
|
90
|
+
}
|
|
91
|
+
const floating = variant === "floating";
|
|
92
|
+
const clear = floating ? /* @__PURE__ */ jsx(IconButton, { size: "lg", onClick: onClear, "aria-label": labels.clear, children: /* @__PURE__ */ jsx(X, {}) }) : /* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: onClear, children: labels.clear });
|
|
93
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
94
|
+
/* @__PURE__ */ jsx("span", { ...regionProps }),
|
|
95
|
+
/* @__PURE__ */ jsxs(
|
|
96
|
+
"div",
|
|
97
|
+
{
|
|
98
|
+
...rest,
|
|
99
|
+
ref: barRef,
|
|
100
|
+
role: "toolbar",
|
|
101
|
+
"aria-label": labels.selected(shownCount),
|
|
102
|
+
"aria-orientation": "horizontal",
|
|
103
|
+
"data-bulk-action-bar": variant,
|
|
104
|
+
inert: !shown || void 0,
|
|
105
|
+
onKeyDown: handleKeyDown,
|
|
106
|
+
onFocus: (e) => {
|
|
107
|
+
onFocus?.(e);
|
|
108
|
+
const hit = itemsOf(barRef.current).find((el) => el === e.target);
|
|
109
|
+
if (hit) {
|
|
110
|
+
active.current = hit;
|
|
111
|
+
rove(barRef.current, active);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
style: floating ? { ...FLOATING_STYLE, ...style } : style,
|
|
115
|
+
className: cn(
|
|
116
|
+
"flex items-center",
|
|
117
|
+
floating ? cn(
|
|
118
|
+
"fixed z-30 gap-1 rounded-2xl border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1 shadow-lg",
|
|
119
|
+
shown ? "animate-sheet" : "animate-sheet-out"
|
|
120
|
+
) : cn(
|
|
121
|
+
"gap-2 px-3 py-2",
|
|
122
|
+
variant === "sticky" && "sticky top-0 z-20 border-b border-[var(--border)] bg-[color-mix(in_srgb,var(--brand)_8%,var(--bg-surface))] backdrop-blur",
|
|
123
|
+
shown ? "animate-overlay" : "animate-overlay-out"
|
|
124
|
+
),
|
|
125
|
+
className
|
|
126
|
+
),
|
|
127
|
+
children: [
|
|
128
|
+
floating && clear,
|
|
129
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-sm font-medium text-[var(--text-primary)]", children: labels.selected(shownCount) }),
|
|
130
|
+
/* @__PURE__ */ jsx("div", { className: "contents", children }),
|
|
131
|
+
!floating && clear
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
] });
|
|
136
|
+
}
|
|
137
|
+
function itemsOf(bar) {
|
|
138
|
+
return bar ? Array.from(bar.querySelectorAll(ITEMS)) : [];
|
|
139
|
+
}
|
|
140
|
+
function rove(bar, active) {
|
|
141
|
+
const list = itemsOf(bar);
|
|
142
|
+
if (!active.current || !list.includes(active.current)) active.current = list[0] ?? null;
|
|
143
|
+
for (const el of list) el.tabIndex = el === active.current ? 0 : -1;
|
|
144
|
+
}
|
|
145
|
+
export {
|
|
146
|
+
BulkActionBar,
|
|
147
|
+
DEFAULT_BULK_ACTION_BAR_LABELS
|
|
148
|
+
};
|
|
149
|
+
//# sourceMappingURL=bulk-action-bar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/bulk-action-bar.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, CSSProperties, KeyboardEvent, ReactNode } from \"react\";\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { useAnnounce } from \"../hooks/use-announce\";\nimport { OVERLAY_EXIT_MS, prefersReducedMotion } from \"../hooks/use-close-transition\";\nimport { Button, IconButton } from \"./ui\";\n\nexport interface BulkActionBarLabels {\n /** The count, shown in the bar, spoken by its live region and used as the toolbar's\n * name: \"3 selected\". */\n selected: (count: number) => string;\n /** The clear/cancel action's name — the X of the floating bar, the text button of the\n * others. */\n clear: string;\n /** Spoken when the selection empties and the bar goes. */\n cleared: string;\n}\n\nexport const DEFAULT_BULK_ACTION_BAR_LABELS: BulkActionBarLabels = {\n selected: (count) => `${count} selected`,\n clear: \"Clear selection\",\n cleared: \"Selection cleared\",\n};\n\n/**\n * `floating` — the phone's: fixed above `AppShell`'s bottom nav (`--app-nav-h`) and the\n * home indicator, inset from both sides, a rounded card with a shadow. `sticky` — the\n * desktop's: pinned to the top of its scroll container, over the rows it acts on.\n * `inline` — in the flow, where the caller places it.\n */\nexport type BulkActionBarVariant = \"floating\" | \"sticky\" | \"inline\";\n\nexport interface BulkActionBarProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"role\" | \"children\"> {\n /** How many items are selected. The bar shows only while this is above zero. */\n count: number;\n /** Clear the selection / leave selection mode. */\n onClear: () => void;\n /** The actions: buttons (`IconButton size=\"lg\"` on a phone, `Button` from md up),\n * in the caller's order. They are the toolbar's arrow-key stops, after the clear. */\n children?: ReactNode;\n /** Default `floating`. See {@link BulkActionBarVariant}. */\n variant?: BulkActionBarVariant;\n /** Default: `bulkActionBar` from the {@link UiKitProvider}, else English. */\n labels?: Partial<BulkActionBarLabels>;\n}\n\n/** Where the floating bar sits: clear of the phone nav and of the home indicator,\n * whichever reaches higher — `FloatingPanel`'s rule, plus the bar's own 0.5rem gap.\n * Set as inline style, so reposition it through `style` (which is merged over this,\n * e.g. `style={{ insetInline: \"auto 2rem\", width: \"24rem\" }}`), not `className`. */\nconst FLOATING_STYLE: CSSProperties = {\n bottom: \"calc(max(var(--app-nav-h, 0px), env(safe-area-inset-bottom, 0px)) + 0.5rem)\",\n // Both insets on both sides: a logical side has no one physical inset to name, and\n // the bar is symmetric, so RTL needs nothing more.\n insetInline: \"max(0.5rem, env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px))\",\n};\n\n/** What the arrow keys walk: the toolbar's enabled controls. Matched by element, not by\n * `tabindex`, because the roving below sets every one but the current to `-1` — a\n * custom control joins with `data-toolbar-item`. */\nconst ITEMS =\n \"button:not([disabled]),a[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[data-toolbar-item]\";\n\n/** Keys a text field needs for itself — the toolbar must not take its caret away. */\nfunction ownsArrows(el: Element): boolean {\n if (el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) return true;\n if (el instanceof HTMLInputElement) return ![\"button\", \"checkbox\", \"radio\", \"submit\", \"reset\"].includes(el.type);\n return (el as HTMLElement).isContentEditable;\n}\n\n/**\n * The bar that appears while rows are selected: the count, a way out, and what can be\n * done to all of them at once.\n *\n * keksdose had written it three times — the transactions phone bar\n * (mobile-bulk-bar.tsx), the invoice lines' phone and desktop bars\n * (invoice-lines-bulk-bar.tsx) and the payees selection bar — and the copies had\n * drifted where it matters: two parked themselves with a guessed `bottom-16` that the\n * third had already found leaves a 24px gap (live #314: the nav is content-sized and\n * `AppShell` publishes its height as `--app-nav-h`), none of them announced the count,\n * and all three were `role=\"toolbar\"` without the arrow keys that role promises.\n *\n * ## Keep it mounted\n *\n * Render it unconditionally and let `count` decide. At zero it renders only its live\n * region — which must already be in the DOM for the first \"3 selected\" to be heard —\n * and when the count drops to zero it plays its exit before it goes, which a\n * `{count > 0 && <Bar/>}` gate would cut off.\n *\n * ## The toolbar\n *\n * One Tab stop: ←/→ (mirrored in RTL), Home and End move between the controls, and Tab\n * leaves — the WAI-ARIA toolbar pattern, since that is what `role=\"toolbar\"` tells a\n * screen reader to expect. The stop remembers the control last used. Arrow keys inside\n * a text field stay the field's.\n *\n * It is NOT a dialog: no focus trap, no scroll lock — the list behind stays live,\n * because tapping more rows is how a selection grows.\n */\nexport function BulkActionBar({\n count,\n onClear,\n children,\n variant = \"floating\",\n labels: labelsProp,\n className,\n style,\n onKeyDown,\n onFocus,\n ...rest\n}: BulkActionBarProps) {\n const labels = useKitLabels(\"bulkActionBar\", DEFAULT_BULK_ACTION_BAR_LABELS, labelsProp);\n const { announce, regionProps } = useAnnounce();\n const barRef = useRef<HTMLDivElement>(null);\n const shown = count > 0;\n\n // The count the bar last SHOWED, so a bar lowering itself after \"clear\" still reads\n // \"3 selected\" on its way out rather than \"0 selected\". Adjusted during render\n // (`Collapse`'s pattern), as is the exit below.\n const [shownCount, setShownCount] = useState(count);\n const [leaving, setLeaving] = useState(false);\n const [prevCount, setPrevCount] = useState(count);\n if (count !== prevCount) {\n setPrevCount(count);\n if (shown) {\n setShownCount(count);\n setLeaving(false);\n } else {\n setLeaving(prevCount > 0 && !prefersReducedMotion());\n }\n }\n useEffect(() => {\n if (!leaving) return;\n const timer = setTimeout(() => setLeaving(false), OVERLAY_EXIT_MS);\n return () => clearTimeout(timer);\n }, [leaving]);\n\n // Spoken on every CHANGE, not on mount: a bar mounted with rows already selected\n // was not the result of anything the user just did.\n const spoken = useRef(count);\n useEffect(() => {\n if (spoken.current === count) return;\n spoken.current = count;\n announce(count > 0 ? labels.selected(count) : labels.cleared);\n }, [count, labels, announce]);\n\n // Roving tab stop. The controls are the caller's, so the stop is kept on the DOM\n // rather than through props: after every render the remembered control (or the\n // first) is the one with `tabindex=0`, every other one `-1`.\n const active = useRef<HTMLElement | null>(null);\n // Every render: the caller's controls come and go (a \"remove added\" that exists\n // only while some are removable), and a stop on a control that left must move on.\n useLayoutEffect(() => {\n rove(barRef.current, active);\n });\n\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n onKeyDown?.(e);\n if (e.defaultPrevented || ownsArrows(e.target as Element)) return;\n const list = itemsOf(barRef.current);\n const at = list.indexOf(document.activeElement as HTMLElement);\n if (at === -1 || list.length === 0) return;\n const step = horizontalStep(e.key, barRef.current);\n let next = -1;\n if (step !== 0) next = (at + step + list.length) % list.length;\n else if (e.key === \"Home\") next = 0;\n else if (e.key === \"End\") next = list.length - 1;\n if (next === -1) return;\n e.preventDefault();\n active.current = list[next];\n rove(barRef.current, active);\n list[next].focus();\n };\n\n if (!shown && !leaving) {\n return <span {...regionProps} />;\n }\n\n const floating = variant === \"floating\";\n const clear = floating ? (\n <IconButton size=\"lg\" onClick={onClear} aria-label={labels.clear}>\n <X />\n </IconButton>\n ) : (\n <Button type=\"button\" variant=\"secondary\" size=\"sm\" onClick={onClear}>\n {labels.clear}\n </Button>\n );\n\n return (\n <>\n <span {...regionProps} />\n <div\n {...rest}\n ref={barRef}\n role=\"toolbar\"\n aria-label={labels.selected(shownCount)}\n aria-orientation=\"horizontal\"\n data-bulk-action-bar={variant}\n // Leaving: out of the tab order and the accessibility tree at once, while the\n // bar still animates. The selection it acted on is already gone.\n inert={!shown || undefined}\n onKeyDown={handleKeyDown}\n onFocus={(e) => {\n onFocus?.(e);\n // Clicking a control is using it: the stop follows the pointer too.\n const hit = itemsOf(barRef.current).find((el) => el === e.target);\n if (hit) {\n active.current = hit;\n rove(barRef.current, active);\n }\n }}\n style={floating ? { ...FLOATING_STYLE, ...style } : style}\n className={cn(\n \"flex items-center\",\n floating\n ? cn(\n \"fixed z-30 gap-1 rounded-2xl border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1 shadow-lg\",\n shown ? \"animate-sheet\" : \"animate-sheet-out\",\n )\n : cn(\n \"gap-2 px-3 py-2\",\n variant === \"sticky\" &&\n \"sticky top-0 z-20 border-b border-[var(--border)] bg-[color-mix(in_srgb,var(--brand)_8%,var(--bg-surface))] backdrop-blur\",\n shown ? \"animate-overlay\" : \"animate-overlay-out\",\n ),\n className,\n )}\n >\n {floating && clear}\n <span className=\"min-w-0 flex-1 truncate text-sm font-medium text-[var(--text-primary)]\">\n {labels.selected(shownCount)}\n </span>\n {/* `contents`, so the caller's controls are flex items of the bar itself. */}\n <div className=\"contents\">{children}</div>\n {!floating && clear}\n </div>\n </>\n );\n}\n\nfunction itemsOf(bar: HTMLElement | null): HTMLElement[] {\n return bar ? Array.from(bar.querySelectorAll<HTMLElement>(ITEMS)) : [];\n}\n\n/** Put the one Tab stop on the remembered control — or the first, when that one has\n * gone or was never set — and take it off every other. */\nfunction rove(bar: HTMLElement | null, active: { current: HTMLElement | null }): void {\n const list = itemsOf(bar);\n if (!active.current || !list.includes(active.current)) active.current = list[0] ?? null;\n for (const el of list) el.tabIndex = el === active.current ? 0 : -1;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -12,6 +12,8 @@ import '../hooks/use-anchored-rect.js';
|
|
|
12
12
|
import '../wizard/types.js';
|
|
13
13
|
import '../tour/tour.js';
|
|
14
14
|
import '../search/command-palette.js';
|
|
15
|
+
import '../search/global-search.js';
|
|
16
|
+
import '../search/search-index.js';
|
|
15
17
|
import './month-picker.js';
|
|
16
18
|
import './page-contents.js';
|
|
17
19
|
import './series-chart-labels.js';
|
|
@@ -32,6 +34,10 @@ import '../feedback/feedback-dialog.js';
|
|
|
32
34
|
import './confirm-dialog.js';
|
|
33
35
|
import './floating-panel.js';
|
|
34
36
|
import './copy-button.js';
|
|
37
|
+
import './bulk-action-bar.js';
|
|
38
|
+
import './list.js';
|
|
39
|
+
import './breadcrumbs.js';
|
|
40
|
+
import './toast.js';
|
|
35
41
|
|
|
36
42
|
/** Screen-reader names for the calculator panel's own controls — any part of the
|
|
37
43
|
* kit's `calculator` namespace. Once three optional keys of its own; widened, not
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ReactNode, KeyboardEvent, Ref,
|
|
2
|
+
import { ReactNode, KeyboardEvent, Ref, MouseEvent, ReactElement, ComponentPropsWithoutRef } from 'react';
|
|
3
3
|
import { LucideIcon } from 'lucide-react';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -16,9 +16,21 @@ import { LucideIcon } from 'lucide-react';
|
|
|
16
16
|
* "choose one of eight things to do" and a row of eight chips says "here are eight
|
|
17
17
|
* things". Using `Button` for both is the mistake this exists to prevent.
|
|
18
18
|
*/
|
|
19
|
-
type ChipTone = "neutral" | "brand" | "danger" | "warning" | "success" | "info" | "income" | "expense";
|
|
20
|
-
/**
|
|
21
|
-
|
|
19
|
+
type ChipTone = "neutral" | "brand" | "danger" | "warning" | "success" | "info" | "income" | "expense" | ChipHue;
|
|
20
|
+
/**
|
|
21
|
+
* Categorical hues (0.10.0): colour that says "a different value" and nothing more —
|
|
22
|
+
* kastlan's status-badge.tsx paints ~15 enums in 9 Tailwind hues by hand. Four of those
|
|
23
|
+
* nine are semantic and already tones (green = `success`, yellow = `warning`, red =
|
|
24
|
+
* `danger`, gray = `neutral`); these five are the rest, each a `--hue-*` token triple
|
|
25
|
+
* in tokens.css with a light and a dark value. Use a semantic tone when the value HAS a
|
|
26
|
+
* meaning (overdue is `danger`), a hue when it only needs telling apart (sent vs
|
|
27
|
+
* accepted vs invoiced).
|
|
28
|
+
*/
|
|
29
|
+
type ChipHue = "blue" | "indigo" | "purple" | "teal" | "orange";
|
|
30
|
+
/** `lg` is the 44px touch target (`min-h-11`) a phone surface wants; `md` is ~28px.
|
|
31
|
+
* `xs` (0.10.0) is 11px type at `px-1.5` — keksdose's goal markers and budget-cell
|
|
32
|
+
* badges, which sit inside a figure's line and must not make it taller. */
|
|
33
|
+
type ChipSize = "xs" | "sm" | "md" | "lg";
|
|
22
34
|
/**
|
|
23
35
|
* How much of the tone the chip wears.
|
|
24
36
|
*
|
|
@@ -29,8 +41,12 @@ type ChipSize = "sm" | "md" | "lg";
|
|
|
29
41
|
* - `solid`: the tone as a fill under contrasting text — the "count" pill (unread
|
|
30
42
|
* messages, open tasks). It centres its content and sets figures tabular, so a
|
|
31
43
|
* one-digit count is a round dot and "9" → "10" does not jiggle the row.
|
|
44
|
+
* - `dot` (0.10.0): no pill at all — a dot in the tone's colour before plain text, for
|
|
45
|
+
* a status in a dense list or a table column where eight tinted pills in a column
|
|
46
|
+
* would outshout the figures beside them (kastlan's unit picker already draws its
|
|
47
|
+
* UnitStatus this way, from a separate `dotColor` map).
|
|
32
48
|
*/
|
|
33
|
-
type ChipVariant = "soft" | "outline" | "solid";
|
|
49
|
+
type ChipVariant = "soft" | "outline" | "solid" | "dot";
|
|
34
50
|
/** `pill` (default) is fully rounded; `square` has the small radius of a field or a
|
|
35
51
|
* button — for a chip that sits in a table cell or beside square controls. */
|
|
36
52
|
type ChipShape = "pill" | "square";
|
|
@@ -45,6 +61,14 @@ interface ChipLinkProps {
|
|
|
45
61
|
/** The chip's forwarded ref. React 19 passes it to a function component as a prop. */
|
|
46
62
|
ref?: Ref<HTMLAnchorElement>;
|
|
47
63
|
"aria-current"?: "true";
|
|
64
|
+
/**
|
|
65
|
+
* The chip's `onClick`, when it was given one — spread it onto the link with the
|
|
66
|
+
* rest. Carried here so a caller need not close over it twice: keksdose's admin pill
|
|
67
|
+
* in the account menu closes the menu on the way to /admin (account-menu.tsx), and
|
|
68
|
+
* with `onClick` on the Chip its `renderLink` stays the one-liner
|
|
69
|
+
* `({ href, ...p }) => <Link to={href} {...p} />`.
|
|
70
|
+
*/
|
|
71
|
+
onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
|
|
48
72
|
id?: string;
|
|
49
73
|
title?: string;
|
|
50
74
|
[key: `aria-${string}`]: string | boolean | number | undefined;
|
|
@@ -85,6 +109,16 @@ interface ChipBaseProps {
|
|
|
85
109
|
* it — "+ Outflow" in green next to an inflow (keksdose #417).
|
|
86
110
|
*/
|
|
87
111
|
selected?: boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Checkbox mode (0.10.0), for a toggle chip (`onClick` + `selected`): a small box
|
|
114
|
+
* at the start that shows a tick when `selected`, and the button reports
|
|
115
|
+
* `role="checkbox"` + `aria-checked` instead of `aria-pressed`. For a set of
|
|
116
|
+
* include/exclude choices where "on" must read as TICKED, not merely tinted —
|
|
117
|
+
* keksdose's add-group-card (the "which categories come along" pills), which put a
|
|
118
|
+
* real `<Checkbox>` inside a `<label>` pill by hand. The box is drawn in both
|
|
119
|
+
* states so ticking one never shifts its label.
|
|
120
|
+
*/
|
|
121
|
+
checkbox?: boolean;
|
|
88
122
|
/** Renders a dismiss affordance. Works alongside `href`/`onClick` — see the note below. */
|
|
89
123
|
onRemove?: () => void;
|
|
90
124
|
/** Accessible name for the dismiss button. Default: `common.remove` from the
|
|
@@ -119,12 +153,14 @@ interface ChipBaseProps {
|
|
|
119
153
|
}
|
|
120
154
|
/**
|
|
121
155
|
* A chip is a link OR a button, never both — so the types say so. Before 0.7.0 both were
|
|
122
|
-
* accepted and `onClick` was silently dropped whenever `href` was set
|
|
123
|
-
*
|
|
124
|
-
*
|
|
156
|
+
* accepted and `onClick` was silently dropped whenever `href` was set. A LINK may still
|
|
157
|
+
* run code on its way (close the menu it sits in): that is an `onClick` typed for the
|
|
158
|
+
* anchor, on the link shape, and the pill stays a link — no `aria-pressed`, no button.
|
|
159
|
+
* That shape wants a definite `href` string, so a bare `onClick` is always the
|
|
160
|
+
* button's, with a button's event.
|
|
125
161
|
*/
|
|
126
162
|
type ChipProps = ChipBaseProps & ({
|
|
127
|
-
/** Renders the chip as a link.
|
|
163
|
+
/** Renders the chip as a link. */
|
|
128
164
|
href?: string;
|
|
129
165
|
onClick?: never;
|
|
130
166
|
/**
|
|
@@ -142,6 +178,18 @@ type ChipProps = ChipBaseProps & ({
|
|
|
142
178
|
* href, whichever element draws it.
|
|
143
179
|
*/
|
|
144
180
|
renderLink?: (props: ChipLinkProps) => ReactElement;
|
|
181
|
+
} | {
|
|
182
|
+
/** The link shape again, with an `onClick`. `href` is a required string here,
|
|
183
|
+
* which is what lets TypeScript tell this shape from the button's: a bare
|
|
184
|
+
* `onClick` can only be the button's, and keeps its button event. */
|
|
185
|
+
href: string;
|
|
186
|
+
/** Runs on the link's click, before the navigation — keksdose's admin pill
|
|
187
|
+
* closes the account menu with it. Reaches a `renderLink` link as
|
|
188
|
+
* {@link ChipLinkProps.onClick}. Not called on a disabled chip, which renders
|
|
189
|
+
* no link. */
|
|
190
|
+
onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
|
|
191
|
+
/** See the link shape above. */
|
|
192
|
+
renderLink?: (props: ChipLinkProps) => ReactElement;
|
|
145
193
|
} | {
|
|
146
194
|
href?: never;
|
|
147
195
|
renderLink?: never;
|
|
@@ -225,4 +273,4 @@ interface ChipInputProps extends Omit<ComponentPropsWithoutRef<"div">, "onChange
|
|
|
225
273
|
*/
|
|
226
274
|
declare function ChipInput({ value, onChange, placeholder, label, ariaLabel, validate, separators, allowDuplicates, max, tone, size, disabled, invalid, error, labels, className, "aria-label": ariaLabelAttr, ...rest }: ChipInputProps): react.JSX.Element;
|
|
227
275
|
|
|
228
|
-
export { Chip, ChipInput, type ChipInputLabels, type ChipInputProps, type ChipLinkProps, type ChipProps, type ChipShape, type ChipSize, type ChipTone, type ChipVariant, DEFAULT_CHIP_INPUT_LABELS, resolveChipInputLabels };
|
|
276
|
+
export { Chip, type ChipHue, ChipInput, type ChipInputLabels, type ChipInputProps, type ChipLinkProps, type ChipProps, type ChipShape, type ChipSize, type ChipTone, type ChipVariant, DEFAULT_CHIP_INPUT_LABELS, resolveChipInputLabels };
|
package/dist/components/chip.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { forwardRef, useId, useRef, useState } from "react";
|
|
4
|
-
import { X } from "lucide-react";
|
|
4
|
+
import { Check, X } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/cn.js";
|
|
6
6
|
import { horizontalStep } from "../lib/direction.js";
|
|
7
7
|
import { FIELD_INVALID } from "./ui.js";
|
|
@@ -42,6 +42,28 @@ const TONE = {
|
|
|
42
42
|
expense: {
|
|
43
43
|
idle: "border-current/40 bg-[var(--bg-surface-2)] text-[var(--money-expense)]",
|
|
44
44
|
selected: "border-current bg-[var(--bg-active)] text-[var(--money-expense)]"
|
|
45
|
+
},
|
|
46
|
+
// The categorical hues: the `info` recipe with its own token triple, so a hue chip
|
|
47
|
+
// and a status chip in one row carry the same weight.
|
|
48
|
+
blue: {
|
|
49
|
+
idle: "border-[var(--hue-blue-border)] bg-[var(--hue-blue-bg)] text-[var(--hue-blue)]",
|
|
50
|
+
selected: "border-[var(--hue-blue)] bg-[var(--hue-blue-bg)] text-[var(--hue-blue)]"
|
|
51
|
+
},
|
|
52
|
+
indigo: {
|
|
53
|
+
idle: "border-[var(--hue-indigo-border)] bg-[var(--hue-indigo-bg)] text-[var(--hue-indigo)]",
|
|
54
|
+
selected: "border-[var(--hue-indigo)] bg-[var(--hue-indigo-bg)] text-[var(--hue-indigo)]"
|
|
55
|
+
},
|
|
56
|
+
purple: {
|
|
57
|
+
idle: "border-[var(--hue-purple-border)] bg-[var(--hue-purple-bg)] text-[var(--hue-purple)]",
|
|
58
|
+
selected: "border-[var(--hue-purple)] bg-[var(--hue-purple-bg)] text-[var(--hue-purple)]"
|
|
59
|
+
},
|
|
60
|
+
teal: {
|
|
61
|
+
idle: "border-[var(--hue-teal-border)] bg-[var(--hue-teal-bg)] text-[var(--hue-teal)]",
|
|
62
|
+
selected: "border-[var(--hue-teal)] bg-[var(--hue-teal-bg)] text-[var(--hue-teal)]"
|
|
63
|
+
},
|
|
64
|
+
orange: {
|
|
65
|
+
idle: "border-[var(--hue-orange-border)] bg-[var(--hue-orange-bg)] text-[var(--hue-orange)]",
|
|
66
|
+
selected: "border-[var(--hue-orange)] bg-[var(--hue-orange-bg)] text-[var(--hue-orange)]"
|
|
45
67
|
}
|
|
46
68
|
};
|
|
47
69
|
const OUTLINE = {
|
|
@@ -52,7 +74,12 @@ const OUTLINE = {
|
|
|
52
74
|
success: "border-[var(--success-border)] bg-transparent text-[var(--success)]",
|
|
53
75
|
info: "border-[var(--info-border)] bg-transparent text-[var(--info)]",
|
|
54
76
|
income: "border-current/60 bg-transparent text-[var(--money-income)]",
|
|
55
|
-
expense: "border-current/60 bg-transparent text-[var(--money-expense)]"
|
|
77
|
+
expense: "border-current/60 bg-transparent text-[var(--money-expense)]",
|
|
78
|
+
blue: "border-[var(--hue-blue-border)] bg-transparent text-[var(--hue-blue)]",
|
|
79
|
+
indigo: "border-[var(--hue-indigo-border)] bg-transparent text-[var(--hue-indigo)]",
|
|
80
|
+
purple: "border-[var(--hue-purple-border)] bg-transparent text-[var(--hue-purple)]",
|
|
81
|
+
teal: "border-[var(--hue-teal-border)] bg-transparent text-[var(--hue-teal)]",
|
|
82
|
+
orange: "border-[var(--hue-orange-border)] bg-transparent text-[var(--hue-orange)]"
|
|
56
83
|
};
|
|
57
84
|
const SOLID = {
|
|
58
85
|
neutral: "border-transparent bg-[var(--bg-inverse)] text-[var(--text-inverse)]",
|
|
@@ -62,15 +89,40 @@ const SOLID = {
|
|
|
62
89
|
success: "border-transparent bg-[var(--success)] text-[var(--text-inverse)]",
|
|
63
90
|
info: "border-transparent bg-[var(--info)] text-[var(--text-inverse)]",
|
|
64
91
|
income: "border-transparent bg-[var(--money-income)] text-[var(--text-inverse)]",
|
|
65
|
-
expense: "border-transparent bg-[var(--money-expense)] text-[var(--text-inverse)]"
|
|
92
|
+
expense: "border-transparent bg-[var(--money-expense)] text-[var(--text-inverse)]",
|
|
93
|
+
blue: "border-transparent bg-[var(--hue-blue)] text-[var(--text-inverse)]",
|
|
94
|
+
indigo: "border-transparent bg-[var(--hue-indigo)] text-[var(--text-inverse)]",
|
|
95
|
+
purple: "border-transparent bg-[var(--hue-purple)] text-[var(--text-inverse)]",
|
|
96
|
+
teal: "border-transparent bg-[var(--hue-teal)] text-[var(--text-inverse)]",
|
|
97
|
+
orange: "border-transparent bg-[var(--hue-orange)] text-[var(--text-inverse)]"
|
|
98
|
+
};
|
|
99
|
+
const DOT = {
|
|
100
|
+
neutral: "bg-[var(--text-muted)]",
|
|
101
|
+
brand: "bg-[var(--brand)]",
|
|
102
|
+
danger: "bg-[var(--danger)]",
|
|
103
|
+
warning: "bg-[var(--warning)]",
|
|
104
|
+
success: "bg-[var(--success)]",
|
|
105
|
+
info: "bg-[var(--info)]",
|
|
106
|
+
income: "bg-[var(--money-income)]",
|
|
107
|
+
expense: "bg-[var(--money-expense)]",
|
|
108
|
+
blue: "bg-[var(--hue-blue)]",
|
|
109
|
+
indigo: "bg-[var(--hue-indigo)]",
|
|
110
|
+
purple: "bg-[var(--hue-purple)]",
|
|
111
|
+
teal: "bg-[var(--hue-teal)]",
|
|
112
|
+
orange: "bg-[var(--hue-orange)]"
|
|
66
113
|
};
|
|
114
|
+
const DOT_SIZE = { xs: "size-1.5", sm: "size-2", md: "size-2", lg: "size-2.5" };
|
|
67
115
|
const CAPS = {
|
|
116
|
+
xs: "text-[10px] font-semibold uppercase tracking-wider",
|
|
68
117
|
sm: "text-[10px] font-semibold uppercase tracking-wider",
|
|
69
118
|
md: "text-[11px] font-semibold uppercase tracking-wider",
|
|
70
119
|
lg: "text-xs font-semibold uppercase tracking-wider"
|
|
71
120
|
};
|
|
72
121
|
function surfaceOf(tone, variant, selected) {
|
|
73
122
|
const palette = TONE[tone];
|
|
123
|
+
if (variant === "dot") {
|
|
124
|
+
return selected ? "border-transparent bg-transparent font-medium text-[var(--text-primary)]" : "border-transparent bg-transparent text-[var(--text-secondary)]";
|
|
125
|
+
}
|
|
74
126
|
if (variant === "solid") {
|
|
75
127
|
return cn(SOLID[tone], selected && "ring-2 ring-[var(--border-strong)] ring-offset-1 ring-offset-[var(--bg-surface)]");
|
|
76
128
|
}
|
|
@@ -78,6 +130,13 @@ function surfaceOf(tone, variant, selected) {
|
|
|
78
130
|
return variant === "outline" ? OUTLINE[tone] : palette.idle;
|
|
79
131
|
}
|
|
80
132
|
const SIZE = {
|
|
133
|
+
xs: {
|
|
134
|
+
body: "gap-1 px-1.5 py-0 text-[11px] leading-4",
|
|
135
|
+
split: "gap-1 ps-1.5 pe-0.5 py-0 text-[11px] leading-4",
|
|
136
|
+
tail: "pe-1",
|
|
137
|
+
icon: "size-3",
|
|
138
|
+
remove: "size-2.5"
|
|
139
|
+
},
|
|
81
140
|
sm: {
|
|
82
141
|
body: "gap-1 px-2 py-0.5 text-xs",
|
|
83
142
|
split: "gap-1 ps-2 pe-1 py-0.5 text-xs",
|
|
@@ -112,6 +171,7 @@ const Chip = forwardRef(function Chip2({
|
|
|
112
171
|
caps = false,
|
|
113
172
|
icon: Icon,
|
|
114
173
|
selected,
|
|
174
|
+
checkbox = false,
|
|
115
175
|
href,
|
|
116
176
|
renderLink,
|
|
117
177
|
onClick,
|
|
@@ -129,7 +189,8 @@ const Chip = forwardRef(function Chip2({
|
|
|
129
189
|
const common = useKitLabels("common", DEFAULT_COMMON_LABELS, { remove: removeLabel });
|
|
130
190
|
const s = SIZE[size];
|
|
131
191
|
const interactive = !!href || !!onClick;
|
|
132
|
-
const radius = shape === "square" ? size === "sm" ? "rounded" : "rounded-md" : "rounded-full";
|
|
192
|
+
const radius = shape === "square" ? size === "sm" || size === "xs" ? "rounded" : "rounded-md" : "rounded-full";
|
|
193
|
+
const dot = variant === "dot";
|
|
133
194
|
const type = cn(caps && CAPS[size], variant === "solid" && "justify-center tabular-nums");
|
|
134
195
|
const surface = cn(
|
|
135
196
|
surfaceOf(tone, variant, selected),
|
|
@@ -142,9 +203,26 @@ const Chip = forwardRef(function Chip2({
|
|
|
142
203
|
radius,
|
|
143
204
|
surface,
|
|
144
205
|
interactive && !disabled && "cursor-pointer hover:brightness-[0.97] dark:hover:brightness-110",
|
|
206
|
+
// An inert dot chip is text in a column: no inset, so it aligns with the header.
|
|
207
|
+
dot && !interactive && "px-0",
|
|
145
208
|
className
|
|
146
209
|
);
|
|
210
|
+
const isCheckbox = checkbox && !!onClick && !href;
|
|
147
211
|
const body = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
212
|
+
isCheckbox && /* @__PURE__ */ jsx(
|
|
213
|
+
"span",
|
|
214
|
+
{
|
|
215
|
+
"aria-hidden": true,
|
|
216
|
+
"data-chip-check": "",
|
|
217
|
+
className: cn(
|
|
218
|
+
"flex shrink-0 items-center justify-center rounded-sm border",
|
|
219
|
+
s.icon,
|
|
220
|
+
selected ? "border-current bg-current/15" : "border-current/50"
|
|
221
|
+
),
|
|
222
|
+
children: selected && /* @__PURE__ */ jsx(Check, { className: "size-full", strokeWidth: 3 })
|
|
223
|
+
}
|
|
224
|
+
),
|
|
225
|
+
dot && /* @__PURE__ */ jsx("span", { "aria-hidden": true, "data-chip-dot": "", className: cn("shrink-0 rounded-full", DOT_SIZE[size], DOT[tone]) }),
|
|
148
226
|
Icon && /* @__PURE__ */ jsx(Icon, { className: cn(s.icon, "shrink-0"), "aria-hidden": true }),
|
|
149
227
|
/* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children })
|
|
150
228
|
] });
|
|
@@ -186,10 +264,12 @@ const Chip = forwardRef(function Chip2({
|
|
|
186
264
|
disabled && "cursor-default"
|
|
187
265
|
);
|
|
188
266
|
if (href && !disabled) {
|
|
267
|
+
const onLinkClick = onClick;
|
|
189
268
|
const linkProps = {
|
|
190
269
|
...rest,
|
|
191
270
|
ref,
|
|
192
271
|
href,
|
|
272
|
+
onClick: onLinkClick,
|
|
193
273
|
"aria-current": selected ? "true" : void 0,
|
|
194
274
|
className: remove ? inner : look,
|
|
195
275
|
children: body
|
|
@@ -204,6 +284,7 @@ const Chip = forwardRef(function Chip2({
|
|
|
204
284
|
ref,
|
|
205
285
|
href,
|
|
206
286
|
"aria-current": selected ? "true" : void 0,
|
|
287
|
+
onClick: onLinkClick,
|
|
207
288
|
className: remove ? inner : look,
|
|
208
289
|
...rest,
|
|
209
290
|
children: body
|
|
@@ -211,15 +292,18 @@ const Chip = forwardRef(function Chip2({
|
|
|
211
292
|
);
|
|
212
293
|
return remove ? pill(link) : link;
|
|
213
294
|
}
|
|
214
|
-
if (onClick) {
|
|
295
|
+
if (onClick && !href) {
|
|
296
|
+
const onButtonClick = onClick;
|
|
215
297
|
const button = /* @__PURE__ */ jsx(
|
|
216
298
|
"button",
|
|
217
299
|
{
|
|
218
300
|
ref,
|
|
219
301
|
type: "button",
|
|
220
|
-
onClick,
|
|
302
|
+
onClick: onButtonClick,
|
|
221
303
|
disabled,
|
|
222
|
-
"
|
|
304
|
+
role: isCheckbox ? "checkbox" : void 0,
|
|
305
|
+
"aria-pressed": isCheckbox ? void 0 : selected,
|
|
306
|
+
"aria-checked": isCheckbox ? !!selected : void 0,
|
|
223
307
|
className: remove ? inner : look,
|
|
224
308
|
...rest,
|
|
225
309
|
children: body
|