@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.
Files changed (204) hide show
  1. package/README.md +25 -11
  2. package/dist/components/alert-banner.d.ts +32 -5
  3. package/dist/components/alert-banner.js +57 -14
  4. package/dist/components/alert-banner.js.map +1 -1
  5. package/dist/components/amount-input.d.ts +6 -0
  6. package/dist/components/breadcrumbs.d.ts +60 -0
  7. package/dist/components/breadcrumbs.js +88 -0
  8. package/dist/components/breadcrumbs.js.map +1 -0
  9. package/dist/components/bulk-action-bar.d.ts +66 -0
  10. package/dist/components/bulk-action-bar.js +149 -0
  11. package/dist/components/bulk-action-bar.js.map +1 -0
  12. package/dist/components/calculator.d.ts +6 -0
  13. package/dist/components/chip.d.ts +58 -10
  14. package/dist/components/chip.js +91 -7
  15. package/dist/components/chip.js.map +1 -1
  16. package/dist/components/copy-button.d.ts +13 -4
  17. package/dist/components/copy-button.js +17 -4
  18. package/dist/components/copy-button.js.map +1 -1
  19. package/dist/components/data-table-filter-popover.d.ts +1 -1
  20. package/dist/components/data-table-filters.d.ts +1 -1
  21. package/dist/components/data-table.d.ts +1 -1
  22. package/dist/components/data-table.js +122 -91
  23. package/dist/components/data-table.js.map +1 -1
  24. package/dist/components/date-picker.d.ts +89 -3
  25. package/dist/components/date-picker.js +163 -68
  26. package/dist/components/date-picker.js.map +1 -1
  27. package/dist/components/description-list.d.ts +43 -6
  28. package/dist/components/description-list.js +91 -11
  29. package/dist/components/description-list.js.map +1 -1
  30. package/dist/components/dialog-frame.d.ts +7 -0
  31. package/dist/components/dialog-frame.js.map +1 -1
  32. package/dist/components/disclosure.d.ts +68 -4
  33. package/dist/components/disclosure.js +33 -15
  34. package/dist/components/disclosure.js.map +1 -1
  35. package/dist/components/file-dropzone.d.ts +3 -4
  36. package/dist/components/file-dropzone.js +2 -1
  37. package/dist/components/file-dropzone.js.map +1 -1
  38. package/dist/components/floating-panel.d.ts +29 -2
  39. package/dist/components/floating-panel.js +16 -2
  40. package/dist/components/floating-panel.js.map +1 -1
  41. package/dist/components/full-bleed-dialog.d.ts +23 -4
  42. package/dist/components/full-bleed-dialog.js +11 -2
  43. package/dist/components/full-bleed-dialog.js.map +1 -1
  44. package/dist/components/list.d.ts +174 -0
  45. package/dist/components/list.js +215 -0
  46. package/dist/components/list.js.map +1 -0
  47. package/dist/components/menu-item.d.ts +99 -0
  48. package/dist/components/menu-item.js +83 -0
  49. package/dist/components/menu-item.js.map +1 -0
  50. package/dist/components/modal.d.ts +23 -1
  51. package/dist/components/modal.js +36 -10
  52. package/dist/components/modal.js.map +1 -1
  53. package/dist/components/number-field.d.ts +6 -0
  54. package/dist/components/number-input.d.ts +6 -0
  55. package/dist/components/numpad-sheet.d.ts +6 -0
  56. package/dist/components/page-contents.js +1 -2
  57. package/dist/components/page-contents.js.map +1 -1
  58. package/dist/components/page-header.d.ts +37 -0
  59. package/dist/components/page-header.js +36 -0
  60. package/dist/components/page-header.js.map +1 -0
  61. package/dist/components/progress-bar.d.ts +16 -2
  62. package/dist/components/progress-bar.js +4 -2
  63. package/dist/components/progress-bar.js.map +1 -1
  64. package/dist/components/series-chart-labels.d.ts +3 -0
  65. package/dist/components/series-chart-labels.js +2 -1
  66. package/dist/components/series-chart-labels.js.map +1 -1
  67. package/dist/components/series-chart.d.ts +16 -0
  68. package/dist/components/series-chart.js +137 -3
  69. package/dist/components/series-chart.js.map +1 -1
  70. package/dist/components/status-dot.d.ts +49 -0
  71. package/dist/components/status-dot.js +52 -0
  72. package/dist/components/status-dot.js.map +1 -0
  73. package/dist/components/table.d.ts +39 -6
  74. package/dist/components/table.js +43 -4
  75. package/dist/components/table.js.map +1 -1
  76. package/dist/components/text.d.ts +63 -0
  77. package/dist/components/text.js +23 -0
  78. package/dist/components/text.js.map +1 -0
  79. package/dist/components/toast.d.ts +167 -0
  80. package/dist/components/toast.js +226 -0
  81. package/dist/components/toast.js.map +1 -0
  82. package/dist/components/toggle-group.d.ts +29 -1
  83. package/dist/components/toggle-group.js +52 -3
  84. package/dist/components/toggle-group.js.map +1 -1
  85. package/dist/components/tooltip.d.ts +38 -15
  86. package/dist/components/tooltip.js +85 -47
  87. package/dist/components/tooltip.js.map +1 -1
  88. package/dist/components/ui.d.ts +139 -16
  89. package/dist/components/ui.js +136 -26
  90. package/dist/components/ui.js.map +1 -1
  91. package/dist/components/use-table-state.d.ts +1 -1
  92. package/dist/{data-table-filters-CF1PXqjQ.d.ts → data-table-filters-Dh9uF_S-.d.ts} +33 -2
  93. package/dist/data-table.d.ts +1 -1
  94. package/dist/hooks/use-close-transition.d.ts +7 -1
  95. package/dist/hooks/use-close-transition.js +5 -2
  96. package/dist/hooks/use-close-transition.js.map +1 -1
  97. package/dist/hooks/use-copy-to-clipboard.js +1 -1
  98. package/dist/hooks/use-copy-to-clipboard.js.map +1 -1
  99. package/dist/hooks/use-overlay-history.js +84 -4
  100. package/dist/hooks/use-overlay-history.js.map +1 -1
  101. package/dist/i18n/defaults.d.ts +6 -0
  102. package/dist/i18n/defaults.js +11 -1
  103. package/dist/i18n/defaults.js.map +1 -1
  104. package/dist/i18n/kit-labels.d.ts +11 -0
  105. package/dist/i18n/kit-labels.js.map +1 -1
  106. package/dist/i18n/locales/de-CH-informal.d.ts +6 -0
  107. package/dist/i18n/locales/de-CH.d.ts +6 -0
  108. package/dist/i18n/locales/de-informal.d.ts +6 -0
  109. package/dist/i18n/locales/de.d.ts +6 -0
  110. package/dist/i18n/locales/de.js +31 -1
  111. package/dist/i18n/locales/de.js.map +1 -1
  112. package/dist/i18n/locales/es.d.ts +6 -0
  113. package/dist/i18n/locales/es.js +31 -1
  114. package/dist/i18n/locales/es.js.map +1 -1
  115. package/dist/i18n/locales/fr.d.ts +6 -0
  116. package/dist/i18n/locales/fr.js +31 -1
  117. package/dist/i18n/locales/fr.js.map +1 -1
  118. package/dist/i18n/locales/hu.d.ts +6 -0
  119. package/dist/i18n/locales/hu.js +31 -1
  120. package/dist/i18n/locales/hu.js.map +1 -1
  121. package/dist/i18n/locales/it.d.ts +6 -0
  122. package/dist/i18n/locales/it.js +31 -1
  123. package/dist/i18n/locales/it.js.map +1 -1
  124. package/dist/i18n/locales/zh.d.ts +6 -0
  125. package/dist/i18n/locales/zh.js +31 -1
  126. package/dist/i18n/locales/zh.js.map +1 -1
  127. package/dist/index.d.ts +18 -8
  128. package/dist/index.js +14 -0
  129. package/dist/index.js.map +1 -1
  130. package/dist/lib/clipping.d.ts +9 -0
  131. package/dist/lib/clipping.js +14 -0
  132. package/dist/lib/clipping.js.map +1 -0
  133. package/dist/search/command-palette.d.ts +67 -2
  134. package/dist/search/command-palette.js +147 -33
  135. package/dist/search/command-palette.js.map +1 -1
  136. package/dist/search/global-search.d.ts +166 -0
  137. package/dist/search/global-search.js +293 -0
  138. package/dist/search/global-search.js.map +1 -0
  139. package/dist/search/search-index.d.ts +105 -0
  140. package/dist/search/search-index.js +142 -0
  141. package/dist/search/search-index.js.map +1 -0
  142. package/dist/search.d.ts +3 -1
  143. package/dist/search.js +2 -0
  144. package/dist/search.js.map +1 -1
  145. package/dist/wizard/stepper-nav.d.ts +10 -1
  146. package/dist/wizard/stepper-nav.js +3 -2
  147. package/dist/wizard/stepper-nav.js.map +1 -1
  148. package/dist/wizard/types.d.ts +2 -2
  149. package/dist/wizard/types.js.map +1 -1
  150. package/dist/wizard/use-wizard.js +1 -1
  151. package/dist/wizard/use-wizard.js.map +1 -1
  152. package/dist/wizard/wizard-summary.js +3 -3
  153. package/dist/wizard/wizard-summary.js.map +1 -1
  154. package/package.json +25 -12
  155. package/src/components/alert-banner.tsx +117 -18
  156. package/src/components/breadcrumbs.tsx +168 -0
  157. package/src/components/bulk-action-bar.tsx +256 -0
  158. package/src/components/chip.tsx +164 -13
  159. package/src/components/copy-button.tsx +29 -7
  160. package/src/components/data-table.tsx +81 -5
  161. package/src/components/date-picker.tsx +377 -118
  162. package/src/components/description-list.tsx +168 -15
  163. package/src/components/dialog-frame.tsx +7 -0
  164. package/src/components/disclosure.tsx +116 -28
  165. package/src/components/file-dropzone.tsx +9 -10
  166. package/src/components/floating-panel.tsx +46 -2
  167. package/src/components/full-bleed-dialog.tsx +42 -5
  168. package/src/components/list.tsx +417 -0
  169. package/src/components/menu-item.tsx +214 -0
  170. package/src/components/modal.tsx +80 -17
  171. package/src/components/page-contents.tsx +4 -4
  172. package/src/components/page-header.tsx +68 -0
  173. package/src/components/progress-bar.tsx +27 -3
  174. package/src/components/series-chart-labels.ts +4 -0
  175. package/src/components/series-chart.tsx +253 -3
  176. package/src/components/status-dot.tsx +99 -0
  177. package/src/components/table.tsx +91 -6
  178. package/src/components/text.tsx +89 -0
  179. package/src/components/toast.tsx +441 -0
  180. package/src/components/toggle-group.tsx +82 -3
  181. package/src/components/tooltip.tsx +186 -100
  182. package/src/components/ui.tsx +321 -41
  183. package/src/hooks/use-close-transition.ts +14 -5
  184. package/src/hooks/use-copy-to-clipboard.ts +1 -1
  185. package/src/hooks/use-overlay-history.ts +181 -4
  186. package/src/i18n/defaults.ts +10 -0
  187. package/src/i18n/kit-labels.tsx +10 -0
  188. package/src/i18n/locales/de.ts +30 -0
  189. package/src/i18n/locales/es.ts +30 -0
  190. package/src/i18n/locales/fr.ts +30 -0
  191. package/src/i18n/locales/hu.ts +30 -0
  192. package/src/i18n/locales/it.ts +30 -0
  193. package/src/i18n/locales/zh.ts +30 -0
  194. package/src/index.ts +30 -0
  195. package/src/lib/clipping.ts +16 -0
  196. package/src/search/command-palette.tsx +241 -31
  197. package/src/search/global-search.tsx +525 -0
  198. package/src/search/search-index.ts +295 -0
  199. package/src/search.ts +2 -0
  200. package/src/wizard/stepper-nav.tsx +13 -2
  201. package/src/wizard/types.ts +2 -2
  202. package/src/wizard/use-wizard.ts +3 -3
  203. package/src/wizard/wizard-summary.tsx +14 -10
  204. 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, ReactElement, MouseEvent, ComponentPropsWithoutRef } from 'react';
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
- /** `lg` is the 44px touch target (`min-h-11`) a phone surface wants; `md` is ~28px. */
21
- type ChipSize = "sm" | "md" | "lg";
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; a link that must
123
- * also run code wants a router `Link`-style `onClick` on the anchor, which is a different
124
- * component's job.
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. Mutually exclusive with `onClick`. */
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 };
@@ -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
- "aria-pressed": selected,
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