@eifi1/ui-kit 0.5.1 → 0.6.1
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 +50 -1
- package/dist/components/amount-input.d.ts +7 -0
- package/dist/components/autocomplete.d.ts +103 -0
- package/dist/components/autocomplete.js +260 -0
- package/dist/components/autocomplete.js.map +1 -0
- package/dist/components/calculator.d.ts +7 -0
- package/dist/components/checkbox.d.ts +9 -0
- package/dist/components/checkbox.js +7 -2
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/chip.d.ts +3 -2
- package/dist/components/chip.js +14 -1
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +100 -0
- package/dist/components/choice-card.js +170 -0
- package/dist/components/choice-card.js.map +1 -0
- package/dist/components/combobox-core.d.ts +76 -6
- package/dist/components/combobox-core.js +119 -49
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +12 -2
- package/dist/components/combobox.js +42 -17
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +91 -0
- package/dist/components/danger-confirm.js +181 -0
- package/dist/components/danger-confirm.js.map +1 -0
- package/dist/components/dialog-frame.d.ts +84 -0
- package/dist/components/dialog-frame.js +86 -0
- package/dist/components/dialog-frame.js.map +1 -0
- package/dist/components/disclosure.d.ts +108 -0
- package/dist/components/disclosure.js +127 -0
- package/dist/components/disclosure.js.map +1 -0
- package/dist/components/entity-combobox.d.ts +17 -3
- package/dist/components/entity-combobox.js +25 -5
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/file-button.d.ts +161 -0
- package/dist/components/file-button.js +243 -0
- package/dist/components/file-button.js.map +1 -0
- package/dist/components/file-dropzone.d.ts +72 -23
- package/dist/components/file-dropzone.js +219 -94
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/icon-picker.d.ts +72 -0
- package/dist/components/icon-picker.js +104 -0
- package/dist/components/icon-picker.js.map +1 -0
- package/dist/components/mini-calendar.d.ts +3 -0
- package/dist/components/mini-calendar.js +4 -3
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +8 -1
- package/dist/components/modal.js +4 -2
- package/dist/components/modal.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +16 -3
- package/dist/components/multi-entity-combobox.js +25 -5
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/number-field.d.ts +41 -1
- package/dist/components/number-field.js +42 -10
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +35 -2
- package/dist/components/number-input.js +35 -4
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +7 -0
- package/dist/components/search-field.d.ts +16 -0
- package/dist/components/search-field.js +29 -7
- package/dist/components/search-field.js.map +1 -1
- package/dist/components/signature-pad.d.ts +43 -1
- package/dist/components/signature-pad.js +74 -2
- package/dist/components/signature-pad.js.map +1 -1
- package/dist/components/swatch-picker.d.ts +69 -0
- package/dist/components/swatch-picker.js +75 -0
- package/dist/components/swatch-picker.js.map +1 -0
- package/dist/components/switch.d.ts +9 -0
- package/dist/components/switch.js +7 -2
- package/dist/components/switch.js.map +1 -1
- package/dist/components/tile-radio.d.ts +50 -0
- package/dist/components/tile-radio.js +140 -0
- package/dist/components/tile-radio.js.map +1 -0
- package/dist/components/toggle-group.d.ts +27 -5
- package/dist/components/toggle-group.js +22 -15
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/ui.d.ts +150 -12
- package/dist/components/ui.js +196 -22
- package/dist/components/ui.js.map +1 -1
- package/dist/i18n/defaults.d.ts +7 -0
- package/dist/i18n/defaults.js +13 -2
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +38 -5
- package/dist/i18n/kit-labels.js +12 -4
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/index.d.ts +16 -7
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/table-text.d.ts +127 -0
- package/dist/lib/table-text.js +82 -0
- package/dist/lib/table-text.js.map +1 -0
- package/dist/rhf/form.d.ts +79 -0
- package/dist/rhf/form.js +143 -0
- package/dist/rhf/form.js.map +1 -0
- package/dist/rhf.d.ts +4 -0
- package/dist/rhf.js +3 -0
- package/dist/rhf.js.map +1 -0
- package/dist/shell/app-shell.js +3 -1
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/table-text.d.ts +1 -0
- package/dist/table-text.js +3 -0
- package/dist/table-text.js.map +1 -0
- package/package.json +14 -1
- package/src/components/autocomplete.tsx +429 -0
- package/src/components/checkbox.tsx +16 -0
- package/src/components/chip.tsx +24 -2
- package/src/components/choice-card.tsx +305 -0
- package/src/components/combobox-core.tsx +228 -58
- package/src/components/combobox.tsx +58 -21
- package/src/components/danger-confirm.tsx +286 -0
- package/src/components/dialog-frame.tsx +179 -0
- package/src/components/disclosure.tsx +259 -0
- package/src/components/entity-combobox.tsx +41 -6
- package/src/components/file-button.tsx +458 -0
- package/src/components/file-dropzone.tsx +323 -117
- package/src/components/icon-picker.tsx +181 -0
- package/src/components/mini-calendar.tsx +7 -3
- package/src/components/modal.tsx +10 -2
- package/src/components/multi-entity-combobox.tsx +40 -6
- package/src/components/number-field.tsx +86 -10
- package/src/components/number-input.tsx +79 -2
- package/src/components/search-field.tsx +49 -6
- package/src/components/signature-pad.tsx +112 -0
- package/src/components/swatch-picker.tsx +141 -0
- package/src/components/switch.tsx +16 -0
- package/src/components/tile-radio.tsx +228 -0
- package/src/components/toggle-group.tsx +54 -18
- package/src/components/ui.tsx +400 -24
- package/src/i18n/defaults.ts +12 -1
- package/src/i18n/kit-labels.tsx +45 -5
- package/src/index.ts +19 -0
- package/src/lib/table-text.ts +265 -0
- package/src/rhf/form.tsx +300 -0
- package/src/rhf.ts +9 -0
- package/src/shell/app-shell.tsx +3 -1
- package/src/table-text.ts +8 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId, useRef } from "react";
|
|
4
|
+
import { Ban, Check } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { Tooltip } from "./tooltip.js";
|
|
7
|
+
const TILE_SIZE = {
|
|
8
|
+
sm: { tile: "size-7", glyph: "size-3.5", swatch: "size-4" },
|
|
9
|
+
md: { tile: "size-8", glyph: "size-4", swatch: "size-5" },
|
|
10
|
+
lg: { tile: "size-11", glyph: "size-5", swatch: "size-6" }
|
|
11
|
+
};
|
|
12
|
+
const TILE_BASE = "relative inline-flex shrink-0 items-center justify-center rounded-md border transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-surface)] disabled:cursor-not-allowed disabled:opacity-50";
|
|
13
|
+
const TILE_IDLE = "border-[var(--border)] bg-[var(--bg-surface)] hover:border-[var(--border-strong)] hover:bg-[var(--bg-hover)]";
|
|
14
|
+
const TILE_SELECTED = "border-[var(--text-primary)] bg-[var(--bg-surface)] ring-1 ring-[var(--text-primary)]";
|
|
15
|
+
function TileRadioGroup({
|
|
16
|
+
items,
|
|
17
|
+
checked,
|
|
18
|
+
onSelect,
|
|
19
|
+
activation,
|
|
20
|
+
disabled,
|
|
21
|
+
size,
|
|
22
|
+
renderTile,
|
|
23
|
+
tileClassName
|
|
24
|
+
}) {
|
|
25
|
+
const refs = useRef([]);
|
|
26
|
+
const noteId = useId();
|
|
27
|
+
const s = TILE_SIZE[size];
|
|
28
|
+
const checkedIndex = checked === void 0 ? -1 : items.findIndex((it) => it.key === checked);
|
|
29
|
+
const tabStop = checkedIndex >= 0 && !items[checkedIndex].disabled ? checkedIndex : items.findIndex((it) => !it.disabled);
|
|
30
|
+
const onKeyDown = (e, index) => {
|
|
31
|
+
const rtl = e.currentTarget.closest("[dir]")?.getAttribute("dir") === "rtl";
|
|
32
|
+
let step;
|
|
33
|
+
switch (e.key) {
|
|
34
|
+
case "ArrowRight":
|
|
35
|
+
step = rtl ? -1 : 1;
|
|
36
|
+
break;
|
|
37
|
+
case "ArrowLeft":
|
|
38
|
+
step = rtl ? 1 : -1;
|
|
39
|
+
break;
|
|
40
|
+
case "ArrowDown":
|
|
41
|
+
step = 1;
|
|
42
|
+
break;
|
|
43
|
+
case "ArrowUp":
|
|
44
|
+
step = -1;
|
|
45
|
+
break;
|
|
46
|
+
case "Home":
|
|
47
|
+
step = "first";
|
|
48
|
+
break;
|
|
49
|
+
case "End":
|
|
50
|
+
step = "last";
|
|
51
|
+
break;
|
|
52
|
+
default:
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
const enabled = items.map((it, i) => it.disabled ? -1 : i).filter((i) => i >= 0);
|
|
57
|
+
if (!enabled.length) return;
|
|
58
|
+
const at = enabled.indexOf(index);
|
|
59
|
+
let target;
|
|
60
|
+
if (step === "first") target = enabled[0];
|
|
61
|
+
else if (step === "last") target = enabled[enabled.length - 1];
|
|
62
|
+
else target = enabled[(at + step + enabled.length) % enabled.length];
|
|
63
|
+
refs.current[target]?.focus();
|
|
64
|
+
if (activation === "automatic") onSelect(items[target].key);
|
|
65
|
+
};
|
|
66
|
+
return /* @__PURE__ */ jsx(Fragment, { children: items.map((item, i) => {
|
|
67
|
+
const selected = i === checkedIndex;
|
|
68
|
+
const button = /* @__PURE__ */ jsxs(
|
|
69
|
+
"button",
|
|
70
|
+
{
|
|
71
|
+
ref: (el) => {
|
|
72
|
+
refs.current[i] = el;
|
|
73
|
+
},
|
|
74
|
+
type: "button",
|
|
75
|
+
role: "radio",
|
|
76
|
+
"aria-checked": selected,
|
|
77
|
+
"aria-label": item.label,
|
|
78
|
+
"aria-describedby": item.note ? `${noteId}-${i}` : void 0,
|
|
79
|
+
tabIndex: i === tabStop ? 0 : -1,
|
|
80
|
+
disabled: disabled || item.disabled,
|
|
81
|
+
onClick: () => !selected && onSelect(item.key),
|
|
82
|
+
onKeyDown: (e) => onKeyDown(e, i),
|
|
83
|
+
className: cn(
|
|
84
|
+
TILE_BASE,
|
|
85
|
+
s.tile,
|
|
86
|
+
selected ? TILE_SELECTED : TILE_IDLE,
|
|
87
|
+
tileClassName?.(item, selected)
|
|
88
|
+
),
|
|
89
|
+
children: [
|
|
90
|
+
item.key === null ? /* @__PURE__ */ jsx(Ban, { "aria-hidden": true, className: cn(s.glyph, "text-[var(--text-muted)]") }) : renderTile(item, selected),
|
|
91
|
+
item.note && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
92
|
+
/* @__PURE__ */ jsx(
|
|
93
|
+
"span",
|
|
94
|
+
{
|
|
95
|
+
"aria-hidden": true,
|
|
96
|
+
className: "absolute end-0.5 top-0.5 size-1.5 rounded-full bg-[var(--text-muted)]"
|
|
97
|
+
}
|
|
98
|
+
),
|
|
99
|
+
/* @__PURE__ */ jsx("span", { id: `${noteId}-${i}`, hidden: true, children: item.note })
|
|
100
|
+
] }),
|
|
101
|
+
selected && /* @__PURE__ */ jsx(SelectedTick, {})
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
return (
|
|
106
|
+
// The bubble is the SIGHTED user's name for the tile — eight unlabelled
|
|
107
|
+
// squares are the complaint that put a bubble on Keksdose's (live #289).
|
|
108
|
+
// It wraps a span rather than the button, because a Tooltip hands its
|
|
109
|
+
// child an `aria-describedby`, and a tile named "Red" and described as
|
|
110
|
+
// "Red" is read out twice.
|
|
111
|
+
/* @__PURE__ */ jsx(
|
|
112
|
+
Tooltip,
|
|
113
|
+
{
|
|
114
|
+
label: item.note ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
115
|
+
/* @__PURE__ */ jsx("span", { className: "block", children: item.label }),
|
|
116
|
+
/* @__PURE__ */ jsx("span", { className: "block font-normal text-[var(--text-muted)]", children: item.note })
|
|
117
|
+
] }) : item.label,
|
|
118
|
+
portal: true,
|
|
119
|
+
children: /* @__PURE__ */ jsx("span", { className: "inline-flex", children: button })
|
|
120
|
+
},
|
|
121
|
+
item.key ?? "\0none"
|
|
122
|
+
)
|
|
123
|
+
);
|
|
124
|
+
}) });
|
|
125
|
+
}
|
|
126
|
+
function SelectedTick() {
|
|
127
|
+
return /* @__PURE__ */ jsx(
|
|
128
|
+
"span",
|
|
129
|
+
{
|
|
130
|
+
"aria-hidden": true,
|
|
131
|
+
className: "absolute -bottom-1 -end-1 inline-flex size-3.5 items-center justify-center rounded-full bg-[var(--text-primary)] text-[var(--bg-surface)]",
|
|
132
|
+
children: /* @__PURE__ */ jsx(Check, { strokeWidth: 3, className: "size-2.5" })
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
export {
|
|
137
|
+
TILE_SIZE,
|
|
138
|
+
TileRadioGroup
|
|
139
|
+
};
|
|
140
|
+
//# sourceMappingURL=tile-radio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/tile-radio.tsx"],"sourcesContent":["import { useId, useRef } from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\nimport { Ban, Check } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { Tooltip } from \"./tooltip\";\n\n/**\n * The machinery {@link SwatchPicker} and {@link IconPicker} share: a row of square\n * tiles that behaves as ONE radio group. Private to the package — not re-exported\n * from the barrel.\n *\n * Keksdose had three of these (the category colours, the category symbols, the\n * transaction flags) and all three were rows of `aria-pressed` buttons: a separate\n * tab stop per tile, and \"pressed\" on a choice that is really one-of-n. The APG radio\n * group is the shape a screen reader expects for that (\"Red, radio, 2 of 7, checked\")\n * and the one a keyboard expects too — a single tab stop, arrows to move.\n */\n\nexport type TileSize = \"sm\" | \"md\" | \"lg\";\n\n/** 28 / 32 / 44px. `lg` is the touch target a phone surface wants (`min-h-11`). */\nexport const TILE_SIZE: Record<TileSize, { tile: string; glyph: string; swatch: string }> = {\n sm: { tile: \"size-7\", glyph: \"size-3.5\", swatch: \"size-4\" },\n md: { tile: \"size-8\", glyph: \"size-4\", swatch: \"size-5\" },\n lg: { tile: \"size-11\", glyph: \"size-5\", swatch: \"size-6\" },\n};\n\n/** One tile, whatever it shows. `key` is `null` for the \"none\" entry. */\nexport interface TileItem<T extends string> {\n key: T | null;\n label: string;\n /** Said after the name, and shown in the bubble under it. Marks the tile with a\n * dot, so the note is visible without hovering every tile to find it. */\n note?: string;\n disabled?: boolean;\n}\n\nexport interface TileRadioGroupProps<T extends string> {\n items: TileItem<T>[];\n /** The checked key, or `undefined` for \"nothing is checked\" (the mixed state, or\n * a value that is not among the options). */\n checked: T | null | undefined;\n onSelect: (key: T | null) => void;\n /** \"automatic\": an arrow key moves AND checks, the APG default. \"manual\": arrows\n * move focus only, Space or Enter checks — for a picker whose change is expensive\n * (a bulk edit that saves on every change). */\n activation: \"automatic\" | \"manual\";\n disabled: boolean;\n size: TileSize;\n /** What goes inside a tile. The frame, the tick and the note's dot are drawn here. */\n renderTile: (item: TileItem<T>, selected: boolean) => ReactNode;\n /** Extra classes for a tile, per item. */\n tileClassName?: (item: TileItem<T>, selected: boolean) => string | undefined;\n}\n\n/**\n * The tile's frame. Selection is a thicker, darker frame AND a tick in the corner —\n * never a colour change alone,\n * because the thing inside a swatch tile IS a colour, and a red frame round a red dot\n * says nothing to a reader who cannot see red.\n *\n * `focus-visible` with an offset in the surface colour, as the checkbox does: the ring\n * must not be painted in the colour of the frame it touches.\n */\nconst TILE_BASE =\n \"relative inline-flex shrink-0 items-center justify-center rounded-md border transition-colors \" +\n \"focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-surface)] \" +\n \"disabled:cursor-not-allowed disabled:opacity-50\";\nconst TILE_IDLE =\n \"border-[var(--border)] bg-[var(--bg-surface)] hover:border-[var(--border-strong)] hover:bg-[var(--bg-hover)]\";\nconst TILE_SELECTED =\n \"border-[var(--text-primary)] bg-[var(--bg-surface)] ring-1 ring-[var(--text-primary)]\";\n\nexport function TileRadioGroup<T extends string>({\n items,\n checked,\n onSelect,\n activation,\n disabled,\n size,\n renderTile,\n tileClassName,\n}: TileRadioGroupProps<T>) {\n const refs = useRef<Array<HTMLButtonElement | null>>([]);\n const noteId = useId();\n const s = TILE_SIZE[size];\n const checkedIndex =\n checked === undefined ? -1 : items.findIndex((it) => it.key === checked);\n // The ONE tab stop: the checked tile, else the first that can be chosen — the APG\n // rule, so Tab into a group with nothing checked still lands somewhere.\n const tabStop =\n checkedIndex >= 0 && !items[checkedIndex].disabled\n ? checkedIndex\n : items.findIndex((it) => !it.disabled);\n\n const onKeyDown = (e: KeyboardEvent<HTMLButtonElement>, index: number) => {\n // Left and right are VISUAL directions: in a right-to-left page the row runs the\n // other way. Up and down follow reading order either way — the APG's radio group\n // treats them as previous/next, and the tiles wrap, so there is no column to keep.\n const rtl = e.currentTarget.closest(\"[dir]\")?.getAttribute(\"dir\") === \"rtl\";\n let step: number | \"first\" | \"last\";\n switch (e.key) {\n case \"ArrowRight\":\n step = rtl ? -1 : 1;\n break;\n case \"ArrowLeft\":\n step = rtl ? 1 : -1;\n break;\n case \"ArrowDown\":\n step = 1;\n break;\n case \"ArrowUp\":\n step = -1;\n break;\n case \"Home\":\n step = \"first\";\n break;\n case \"End\":\n step = \"last\";\n break;\n default:\n return;\n }\n e.preventDefault();\n const enabled = items.map((it, i) => (it.disabled ? -1 : i)).filter((i) => i >= 0);\n if (!enabled.length) return;\n const at = enabled.indexOf(index);\n let target: number;\n if (step === \"first\") target = enabled[0];\n else if (step === \"last\") target = enabled[enabled.length - 1];\n // Wrapping, as a native radio group does: the last tile's \"next\" is the first.\n else target = enabled[(at + step + enabled.length) % enabled.length];\n refs.current[target]?.focus();\n if (activation === \"automatic\") onSelect(items[target].key);\n };\n\n return (\n <>\n {items.map((item, i) => {\n const selected = i === checkedIndex;\n const button = (\n <button\n ref={(el) => {\n refs.current[i] = el;\n }}\n type=\"button\"\n role=\"radio\"\n aria-checked={selected}\n aria-label={item.label}\n // The note is a DESCRIPTION, read after the name and the state, not glued\n // into the name — the punctuation between the two would be English's.\n aria-describedby={item.note ? `${noteId}-${i}` : undefined}\n tabIndex={i === tabStop ? 0 : -1}\n disabled={disabled || item.disabled}\n // Clicking the checked tile again does nothing: a radio is not a toggle.\n // \"None\" is the way back to nothing, and it is a tile of its own.\n onClick={() => !selected && onSelect(item.key)}\n onKeyDown={(e) => onKeyDown(e, i)}\n className={cn(\n TILE_BASE,\n s.tile,\n selected ? TILE_SELECTED : TILE_IDLE,\n tileClassName?.(item, selected),\n )}\n >\n {item.key === null ? (\n <Ban aria-hidden className={cn(s.glyph, \"text-[var(--text-muted)]\")} />\n ) : (\n renderTile(item, selected)\n )}\n {item.note && (\n <>\n {/* A dot rather than a dimmed tile: dimming reads as \"disabled\", and a\n tile with a note is entirely available. */}\n <span\n aria-hidden\n className=\"absolute end-0.5 top-0.5 size-1.5 rounded-full bg-[var(--text-muted)]\"\n />\n {/* `hidden` still serves as a description: aria-describedby reads\n hidden text, which is what keeps it out of the layout. */}\n <span id={`${noteId}-${i}`} hidden>\n {item.note}\n </span>\n </>\n )}\n {selected && <SelectedTick />}\n </button>\n );\n return (\n // The bubble is the SIGHTED user's name for the tile — eight unlabelled\n // squares are the complaint that put a bubble on Keksdose's (live #289).\n // It wraps a span rather than the button, because a Tooltip hands its\n // child an `aria-describedby`, and a tile named \"Red\" and described as\n // \"Red\" is read out twice.\n <Tooltip\n key={item.key ?? \"\\u0000none\"}\n label={\n item.note ? (\n <>\n <span className=\"block\">{item.label}</span>\n <span className=\"block font-normal text-[var(--text-muted)]\">{item.note}</span>\n </>\n ) : (\n item.label\n )\n }\n portal\n >\n <span className=\"inline-flex\">{button}</span>\n </Tooltip>\n );\n })}\n </>\n );\n}\n\n/** The tick on a selected tile — a small badge in the corner, in the text colour on\n * the surface colour, so it reads the same over every swatch colour. */\nfunction SelectedTick() {\n return (\n <span\n aria-hidden\n className=\"absolute -bottom-1 -end-1 inline-flex size-3.5 items-center justify-center rounded-full bg-[var(--text-primary)] text-[var(--bg-surface)]\"\n >\n <Check strokeWidth={3} className=\"size-2.5\" />\n </span>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -15,9 +15,7 @@ interface ToggleOption<T extends string> {
|
|
|
15
15
|
* accepted and the component ignored would be a prop that silently does nothing —
|
|
16
16
|
* worse than one that does not compile.
|
|
17
17
|
*/
|
|
18
|
-
interface
|
|
19
|
-
value: T;
|
|
20
|
-
onChange: (value: T) => void;
|
|
18
|
+
interface ToggleGroupBaseProps<T extends string> extends Omit<ComponentPropsWithoutRef<"div">, "onChange" | "children"> {
|
|
21
19
|
options: ToggleOption<T>[];
|
|
22
20
|
className?: string;
|
|
23
21
|
/** Applied to every option button (e.g. to tune height/rounding to match
|
|
@@ -44,6 +42,30 @@ interface ToggleGroupProps<T extends string> extends Omit<ComponentPropsWithoutR
|
|
|
44
42
|
*/
|
|
45
43
|
disabled?: boolean;
|
|
46
44
|
}
|
|
47
|
-
|
|
45
|
+
/** The group as it has always been: one option is always the answer. */
|
|
46
|
+
interface ToggleGroupRequiredProps<T extends string> extends ToggleGroupBaseProps<T> {
|
|
47
|
+
allowEmpty?: false;
|
|
48
|
+
value: T;
|
|
49
|
+
onChange: (value: T) => void;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A group that can be emptied: clicking the active option clears it, and `onChange`
|
|
53
|
+
* receives `null` (Keksdose's support-panel filters, where "no filter" is reached by
|
|
54
|
+
* clicking the filter that is on).
|
|
55
|
+
*
|
|
56
|
+
* The options become TOGGLE BUTTONS (`aria-pressed`, in a `role="group"`) rather than
|
|
57
|
+
* radios. A radio cannot be unchecked by activating it — no screen reader user expects
|
|
58
|
+
* a second press on "Open, radio, checked" to leave nothing checked, and nothing would
|
|
59
|
+
* tell them it had. "Open, toggle button, pressed" says exactly what a press will do.
|
|
60
|
+
*/
|
|
61
|
+
interface ToggleGroupClearableProps<T extends string> extends ToggleGroupBaseProps<T> {
|
|
62
|
+
allowEmpty: true;
|
|
63
|
+
value: T | null;
|
|
64
|
+
onChange: (value: T | null) => void;
|
|
65
|
+
}
|
|
66
|
+
/** `allowEmpty` picks the shape, so `onChange` is typed `(T) => void` unless the group
|
|
67
|
+
* can actually emit `null` — no existing caller has a `null` to handle. */
|
|
68
|
+
type ToggleGroupProps<T extends string> = ToggleGroupRequiredProps<T> | ToggleGroupClearableProps<T>;
|
|
69
|
+
declare function ToggleGroup<T extends string>(props: ToggleGroupProps<T>): react.JSX.Element;
|
|
48
70
|
|
|
49
|
-
export { ToggleGroup, type ToggleGroupProps, type ToggleOption };
|
|
71
|
+
export { ToggleGroup, type ToggleGroupBaseProps, type ToggleGroupClearableProps, type ToggleGroupProps, type ToggleGroupRequiredProps, type ToggleOption };
|
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
import { cn } from "../lib/cn.js";
|
|
4
|
-
function ToggleGroup({
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
4
|
+
function ToggleGroup(props) {
|
|
5
|
+
const {
|
|
6
|
+
value,
|
|
7
|
+
options,
|
|
8
|
+
className,
|
|
9
|
+
optionClassName,
|
|
10
|
+
ariaLabel,
|
|
11
|
+
disabled = false,
|
|
12
|
+
"aria-label": ariaLabelAttr,
|
|
13
|
+
...restWithMode
|
|
14
|
+
} = props;
|
|
15
|
+
const { allowEmpty: _allowEmpty, onChange: _onChange, ...rest } = restWithMode;
|
|
16
|
+
const choose = (next) => {
|
|
17
|
+
if (props.allowEmpty) props.onChange(next === value ? null : next);
|
|
18
|
+
else props.onChange(next);
|
|
19
|
+
};
|
|
20
|
+
const clearable = props.allowEmpty === true;
|
|
15
21
|
return /* @__PURE__ */ jsx(
|
|
16
22
|
"div",
|
|
17
23
|
{
|
|
18
24
|
...rest,
|
|
19
|
-
role: "radiogroup",
|
|
25
|
+
role: clearable ? "group" : "radiogroup",
|
|
20
26
|
"aria-label": ariaLabelAttr ?? ariaLabel,
|
|
21
27
|
"aria-disabled": disabled || void 0,
|
|
22
28
|
className: cn(
|
|
@@ -45,10 +51,11 @@ function ToggleGroup({
|
|
|
45
51
|
"button",
|
|
46
52
|
{
|
|
47
53
|
type: "button",
|
|
48
|
-
role: "radio",
|
|
49
|
-
"aria-checked": active,
|
|
54
|
+
role: clearable ? void 0 : "radio",
|
|
55
|
+
"aria-checked": clearable ? void 0 : active,
|
|
56
|
+
"aria-pressed": clearable ? active : void 0,
|
|
50
57
|
disabled,
|
|
51
|
-
onClick: () =>
|
|
58
|
+
onClick: () => choose(opt.value),
|
|
52
59
|
className: cn(
|
|
53
60
|
// `truncate` (which carries whitespace-nowrap) rather than letting a
|
|
54
61
|
// label wrap: a segmented control sizes its whole row to the tallest
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/toggle-group.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef } from \"react\";\nimport { cn } from \"../lib/cn\";\n\nexport interface ToggleOption<T extends string> {\n value: T;\n label: string;\n className?: string;\n}\n\n/**\n * `onChange` is the group's own — the chosen VALUE, not a DOM event — so the div's\n * `onChange` is omitted rather than shadowed: leaving both in scope would give the\n * prop two incompatible meanings depending on which overload TypeScript picked.\n *\n * `children` is omitted too: this renders its `options` and nothing else, so a `children` the type\n * accepted and the component ignored would be a prop that silently does nothing —\n * worse than one that does not compile.\n */\nexport interface ToggleGroupProps<T extends string>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"children\"> {\n value: T;\n onChange: (value: T) => void;\n options: ToggleOption<T>[];\n className?: string;\n /** Applied to every option button (e.g. to tune height/rounding to match\n * adjacent fields). Per-option `className` still wins over this. */\n optionClassName?: string;\n /**\n * @deprecated Pass `aria-label` instead — the DOM spelling, which every other\n * control in this kit now answers to. Kept working because three applications ship\n * this one today; it names the group only when `aria-label` is absent.\n */\n ariaLabel?: string;\n /**\n * Show, refuse the change (Keksdose live #288: a payment dated in the future has no\n * state to set).\n *\n * Whatever `value` says stays pressed and keeps its own fill rather than going grey\n * with the rest — a reader who cannot see WHICH option is chosen has been told less\n * than before it was disabled. A caller with nothing to show passes no value, and\n * the group renders dimmed with nothing pressed, which is the shape Keksdose's\n * status picker uses for a row whose status does not exist yet.\n *\n * On the whole GROUP, not per option: a segmented control where some segments are\n * live and others are not is a menu with holes in it, and no caller here wants one.\n */\n disabled?: boolean;\n}\n\nexport function ToggleGroup<T extends string>({\n value,\n onChange,\n options,\n className,\n optionClassName,\n ariaLabel,\n disabled = false,\n \"aria-label\": ariaLabelAttr,\n ...rest\n}: ToggleGroupProps<T>) {\n return (\n <div\n // The audit's named example of a closed prop list (§\"Public API design\"): the\n // tour locates a step by CSS SELECTOR, so a component that drops every attribute\n // it was not expecting cannot be spotlighted at all — and Keksdose's rule editor\n // carries a comment explaining that it wraps this group in a bare <div> for\n // exactly that reason.\n //\n // `...rest` first, then the attributes the group cannot do without: a caller\n // hanging an anchor or a test id on the group must not be able to overwrite the\n // radiogroup role or the disabled state by accident. `className` is destructured\n // out entirely and merged through `cn`, so it is never in here.\n {...rest}\n role=\"radiogroup\"\n // The DOM spelling wins; `ariaLabel` is the fallback for the call sites that\n // have not moved yet.\n aria-label={ariaLabelAttr ?? ariaLabel}\n // `aria-disabled` on the group as well as `disabled` on each button: a radio\n // group is what the user is being refused, and a screen reader announcing\n // three separately-disabled radios does not say that.\n aria-disabled={disabled || undefined}\n className={cn(\n // `gap-0.5` — the same 2px as the container's own padding, so EVERY segment\n // sits in a uniform 2px moat and no two fills ever touch. Flush segments were\n // Keksdose live #268's rework: the pressed segment wears a saturated fill and\n // an unpressed neighbour wears a pale hover fill, and with a shared edge the\n // two rectangles read as one smeared shape — *\"the boundary of the selected\n // option and hovering next to it overlays the boundary of the selected\n // button\"*. A gap is what makes each segment its own chip; it cannot be\n // undone by a caller's per-option colour, which a hover-only fix could.\n //\n // (The hover fill is the DESKTOP half of that report: Tailwind v4 wraps every\n // `hover:` in `@media (hover: hover)`, so a phone never paints it. The half a\n // phone does see is the focus ring — see the segment's own note below.)\n \"inline-flex w-full gap-0.5 rounded-md border border-[var(--border-strong)] bg-[var(--bg-surface)] p-0.5 shadow-sm\",\n // The whole group fades, the way every other disabled control in this\n // package does; `cursor-not-allowed` is on the buttons, which is what a\n // pointer is actually over.\n disabled && \"opacity-60\",\n className,\n )}\n >\n {options.map((opt) => {\n const active = opt.value === value;\n return (\n <button\n key={opt.value}\n type=\"button\"\n role=\"radio\"\n aria-checked={active}\n disabled={disabled}\n onClick={() => onChange(opt.value)}\n className={cn(\n // `truncate` (which carries whitespace-nowrap) rather than letting a\n // label wrap: a segmented control sizes its whole row to the tallest\n // option, so one two-word option — Keksdose feedback #147's \"Where I\n // am\" — silently doubles the height of every segment beside it.\n //\n // `basis-auto` is what keeps that ellipsis a LAST resort rather than\n // the normal state (Keksdose dev#475). With flex-1's `basis-0`, a\n // shrink-to-fit group (`w-auto`) still resolves to the sum of the\n // labels' widths — and then splits it EQUALLY, so the short option\n // got 66px it did not need and \"Where I am\" got 66 of the 81 it did:\n // truncated at 1778px of free screen. Basing each segment on its own\n // content and sharing only the LEFTOVER space keeps a full-width\n // group's segments near-equal and an auto-width group's exact.\n //\n // `focus-visible` + `ring-inset`, not `focus` + an outset ring. A ring\n // is a box-shadow that spreads OUTWARD, so on a flush group it painted\n // 2px of ring over both neighbours and over the container's own border\n // — and on a phone it appeared on every TAP, because a tap focuses the\n // button. That is the other half of what live #268's rework saw\n // overlaying the selected segment's boundary. Inset keeps the ring\n // inside the segment it belongs to; focus-visible keeps it for the\n // keyboard, which is the only input that needs it.\n \"min-w-0 flex-1 basis-auto truncate rounded px-3 py-1.5 text-sm font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--border-strong)]\",\n active\n ? \"bg-[var(--bg-inverse)] text-[var(--text-inverse)]\"\n : \"text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]\",\n // No hover fill on a group that cannot be changed — a segment that\n // lights up under the pointer is an offer, and there is none here.\n disabled && \"cursor-not-allowed hover:bg-transparent dark:hover:bg-transparent\",\n optionClassName,\n opt.className,\n )}\n >\n {opt.label}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";AAyGU;AAxGV,SAAS,UAAU;AAgDZ,SAAS,YAA8B;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,cAAc;AAAA,EACd,GAAG;AACL,GAAwB;AACtB,SACE;AAAA,IAAC;AAAA;AAAA,MAWE,GAAG;AAAA,MACJ,MAAK;AAAA,MAGL,cAAY,iBAAiB;AAAA,MAI7B,iBAAe,YAAY;AAAA,MAC3B,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAaT;AAAA;AAAA;AAAA;AAAA,QAIA,YAAY;AAAA,QACZ;AAAA,MACF;AAAA,MAEC,kBAAQ,IAAI,CAAC,QAAQ;AACpB,cAAM,SAAS,IAAI,UAAU;AAC7B,eACE;AAAA,UAAC;AAAA;AAAA,YAEC,MAAK;AAAA,YACL,MAAK;AAAA,YACL,gBAAc;AAAA,YACd;AAAA,YACA,SAAS,MAAM,SAAS,IAAI,KAAK;AAAA,YACjC,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,cAuBT;AAAA,cACA,SACI,sDACA;AAAA;AAAA;AAAA,cAGJ,YAAY;AAAA,cACZ;AAAA,cACA,IAAI;AAAA,YACN;AAAA,YAEC,cAAI;AAAA;AAAA,UAxCA,IAAI;AAAA,QAyCX;AAAA,MAEJ,CAAC;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/toggle-group.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef } from \"react\";\nimport { cn } from \"../lib/cn\";\n\nexport interface ToggleOption<T extends string> {\n value: T;\n label: string;\n className?: string;\n}\n\n/**\n * `onChange` is the group's own — the chosen VALUE, not a DOM event — so the div's\n * `onChange` is omitted rather than shadowed: leaving both in scope would give the\n * prop two incompatible meanings depending on which overload TypeScript picked.\n *\n * `children` is omitted too: this renders its `options` and nothing else, so a `children` the type\n * accepted and the component ignored would be a prop that silently does nothing —\n * worse than one that does not compile.\n */\nexport interface ToggleGroupBaseProps<T extends string>\n extends Omit<ComponentPropsWithoutRef<\"div\">, \"onChange\" | \"children\"> {\n options: ToggleOption<T>[];\n className?: string;\n /** Applied to every option button (e.g. to tune height/rounding to match\n * adjacent fields). Per-option `className` still wins over this. */\n optionClassName?: string;\n /**\n * @deprecated Pass `aria-label` instead — the DOM spelling, which every other\n * control in this kit now answers to. Kept working because three applications ship\n * this one today; it names the group only when `aria-label` is absent.\n */\n ariaLabel?: string;\n /**\n * Show, refuse the change (Keksdose live #288: a payment dated in the future has no\n * state to set).\n *\n * Whatever `value` says stays pressed and keeps its own fill rather than going grey\n * with the rest — a reader who cannot see WHICH option is chosen has been told less\n * than before it was disabled. A caller with nothing to show passes no value, and\n * the group renders dimmed with nothing pressed, which is the shape Keksdose's\n * status picker uses for a row whose status does not exist yet.\n *\n * On the whole GROUP, not per option: a segmented control where some segments are\n * live and others are not is a menu with holes in it, and no caller here wants one.\n */\n disabled?: boolean;\n}\n\n/** The group as it has always been: one option is always the answer. */\nexport interface ToggleGroupRequiredProps<T extends string> extends ToggleGroupBaseProps<T> {\n allowEmpty?: false;\n value: T;\n onChange: (value: T) => void;\n}\n\n/**\n * A group that can be emptied: clicking the active option clears it, and `onChange`\n * receives `null` (Keksdose's support-panel filters, where \"no filter\" is reached by\n * clicking the filter that is on).\n *\n * The options become TOGGLE BUTTONS (`aria-pressed`, in a `role=\"group\"`) rather than\n * radios. A radio cannot be unchecked by activating it — no screen reader user expects\n * a second press on \"Open, radio, checked\" to leave nothing checked, and nothing would\n * tell them it had. \"Open, toggle button, pressed\" says exactly what a press will do.\n */\nexport interface ToggleGroupClearableProps<T extends string> extends ToggleGroupBaseProps<T> {\n allowEmpty: true;\n value: T | null;\n onChange: (value: T | null) => void;\n}\n\n/** `allowEmpty` picks the shape, so `onChange` is typed `(T) => void` unless the group\n * can actually emit `null` — no existing caller has a `null` to handle. */\nexport type ToggleGroupProps<T extends string> =\n | ToggleGroupRequiredProps<T>\n | ToggleGroupClearableProps<T>;\n\nexport function ToggleGroup<T extends string>(props: ToggleGroupProps<T>) {\n const {\n value,\n options,\n className,\n optionClassName,\n ariaLabel,\n disabled = false,\n \"aria-label\": ariaLabelAttr,\n ...restWithMode\n } = props;\n // Taken off the rest so neither reaches the DOM; `props` keeps them paired, which is\n // what lets the `onChange` below be called with `null` only in the mode that allows it.\n const { allowEmpty: _allowEmpty, onChange: _onChange, ...rest } = restWithMode;\n const choose = (next: T) => {\n if (props.allowEmpty) props.onChange(next === value ? null : next);\n else props.onChange(next);\n };\n const clearable = props.allowEmpty === true;\n return (\n <div\n // The audit's named example of a closed prop list (§\"Public API design\"): the\n // tour locates a step by CSS SELECTOR, so a component that drops every attribute\n // it was not expecting cannot be spotlighted at all — and Keksdose's rule editor\n // carries a comment explaining that it wraps this group in a bare <div> for\n // exactly that reason.\n //\n // `...rest` first, then the attributes the group cannot do without: a caller\n // hanging an anchor or a test id on the group must not be able to overwrite the\n // radiogroup role or the disabled state by accident. `className` is destructured\n // out entirely and merged through `cn`, so it is never in here.\n {...rest}\n role={clearable ? \"group\" : \"radiogroup\"}\n // The DOM spelling wins; `ariaLabel` is the fallback for the call sites that\n // have not moved yet.\n aria-label={ariaLabelAttr ?? ariaLabel}\n // `aria-disabled` on the group as well as `disabled` on each button: a radio\n // group is what the user is being refused, and a screen reader announcing\n // three separately-disabled radios does not say that.\n aria-disabled={disabled || undefined}\n className={cn(\n // `gap-0.5` — the same 2px as the container's own padding, so EVERY segment\n // sits in a uniform 2px moat and no two fills ever touch. Flush segments were\n // Keksdose live #268's rework: the pressed segment wears a saturated fill and\n // an unpressed neighbour wears a pale hover fill, and with a shared edge the\n // two rectangles read as one smeared shape — *\"the boundary of the selected\n // option and hovering next to it overlays the boundary of the selected\n // button\"*. A gap is what makes each segment its own chip; it cannot be\n // undone by a caller's per-option colour, which a hover-only fix could.\n //\n // (The hover fill is the DESKTOP half of that report: Tailwind v4 wraps every\n // `hover:` in `@media (hover: hover)`, so a phone never paints it. The half a\n // phone does see is the focus ring — see the segment's own note below.)\n \"inline-flex w-full gap-0.5 rounded-md border border-[var(--border-strong)] bg-[var(--bg-surface)] p-0.5 shadow-sm\",\n // The whole group fades, the way every other disabled control in this\n // package does; `cursor-not-allowed` is on the buttons, which is what a\n // pointer is actually over.\n disabled && \"opacity-60\",\n className,\n )}\n >\n {options.map((opt) => {\n const active = opt.value === value;\n return (\n <button\n key={opt.value}\n type=\"button\"\n role={clearable ? undefined : \"radio\"}\n aria-checked={clearable ? undefined : active}\n aria-pressed={clearable ? active : undefined}\n disabled={disabled}\n onClick={() => choose(opt.value)}\n className={cn(\n // `truncate` (which carries whitespace-nowrap) rather than letting a\n // label wrap: a segmented control sizes its whole row to the tallest\n // option, so one two-word option — Keksdose feedback #147's \"Where I\n // am\" — silently doubles the height of every segment beside it.\n //\n // `basis-auto` is what keeps that ellipsis a LAST resort rather than\n // the normal state (Keksdose dev#475). With flex-1's `basis-0`, a\n // shrink-to-fit group (`w-auto`) still resolves to the sum of the\n // labels' widths — and then splits it EQUALLY, so the short option\n // got 66px it did not need and \"Where I am\" got 66 of the 81 it did:\n // truncated at 1778px of free screen. Basing each segment on its own\n // content and sharing only the LEFTOVER space keeps a full-width\n // group's segments near-equal and an auto-width group's exact.\n //\n // `focus-visible` + `ring-inset`, not `focus` + an outset ring. A ring\n // is a box-shadow that spreads OUTWARD, so on a flush group it painted\n // 2px of ring over both neighbours and over the container's own border\n // — and on a phone it appeared on every TAP, because a tap focuses the\n // button. That is the other half of what live #268's rework saw\n // overlaying the selected segment's boundary. Inset keeps the ring\n // inside the segment it belongs to; focus-visible keeps it for the\n // keyboard, which is the only input that needs it.\n \"min-w-0 flex-1 basis-auto truncate rounded px-3 py-1.5 text-sm font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--border-strong)]\",\n active\n ? \"bg-[var(--bg-inverse)] text-[var(--text-inverse)]\"\n : \"text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]\",\n // No hover fill on a group that cannot be changed — a segment that\n // lights up under the pointer is an offer, and there is none here.\n disabled && \"cursor-not-allowed hover:bg-transparent dark:hover:bg-transparent\",\n optionClassName,\n opt.className,\n )}\n >\n {opt.label}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";AA4IU;AA3IV,SAAS,UAAU;AA2EZ,SAAS,YAA8B,OAA4B;AACxE,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX,cAAc;AAAA,IACd,GAAG;AAAA,EACL,IAAI;AAGJ,QAAM,EAAE,YAAY,aAAa,UAAU,WAAW,GAAG,KAAK,IAAI;AAClE,QAAM,SAAS,CAAC,SAAY;AAC1B,QAAI,MAAM,WAAY,OAAM,SAAS,SAAS,QAAQ,OAAO,IAAI;AAAA,QAC5D,OAAM,SAAS,IAAI;AAAA,EAC1B;AACA,QAAM,YAAY,MAAM,eAAe;AACvC,SACE;AAAA,IAAC;AAAA;AAAA,MAWE,GAAG;AAAA,MACJ,MAAM,YAAY,UAAU;AAAA,MAG5B,cAAY,iBAAiB;AAAA,MAI7B,iBAAe,YAAY;AAAA,MAC3B,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAaT;AAAA;AAAA;AAAA;AAAA,QAIA,YAAY;AAAA,QACZ;AAAA,MACF;AAAA,MAEC,kBAAQ,IAAI,CAAC,QAAQ;AACpB,cAAM,SAAS,IAAI,UAAU;AAC7B,eACE;AAAA,UAAC;AAAA;AAAA,YAEC,MAAK;AAAA,YACL,MAAM,YAAY,SAAY;AAAA,YAC9B,gBAAc,YAAY,SAAY;AAAA,YACtC,gBAAc,YAAY,SAAS;AAAA,YACnC;AAAA,YACA,SAAS,MAAM,OAAO,IAAI,KAAK;AAAA,YAC/B,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,cAuBT;AAAA,cACA,SACI,sDACA;AAAA;AAAA;AAAA,cAGJ,YAAY;AAAA,cACZ;AAAA,cACA,IAAI;AAAA,YACN;AAAA,YAEC,cAAI;AAAA;AAAA,UAzCA,IAAI;AAAA,QA0CX;AAAA,MAEJ,CAAC;AAAA;AAAA,EACH;AAEJ;","names":[]}
|
package/dist/components/ui.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { SelectHTMLAttributes, ReactNode, InputHTMLAttributes,
|
|
2
|
+
import { ButtonHTMLAttributes, SelectHTMLAttributes, ReactNode, InputHTMLAttributes, ComponentPropsWithoutRef, TextareaHTMLAttributes } from 'react';
|
|
3
3
|
|
|
4
4
|
type ButtonVariant = "primary" | "secondary" | "ghost" | "danger" | "brand";
|
|
5
5
|
/**
|
|
@@ -17,10 +17,47 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
|
17
17
|
stretch?: boolean;
|
|
18
18
|
}
|
|
19
19
|
declare function Button({ variant, stretch, className, ...rest }: ButtonProps): react.JSX.Element;
|
|
20
|
+
type IconButtonSize = "md" | "sm" | "xs" | "2xs";
|
|
21
|
+
declare const ICON_BUTTON_TONES: {
|
|
22
|
+
readonly default: "";
|
|
23
|
+
readonly muted: "text-[var(--text-placeholder)] hover:bg-[var(--bg-surface-2)] hover:text-[var(--text-primary)]";
|
|
24
|
+
readonly danger: "text-[var(--text-placeholder)] hover:bg-[var(--danger-bg)] hover:text-[var(--danger)] focus:ring-[var(--danger-border)]";
|
|
25
|
+
};
|
|
20
26
|
interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
21
27
|
variant?: ButtonVariant;
|
|
22
|
-
/** Box size: md = 36px (matches the top bar), sm = 32px
|
|
23
|
-
|
|
28
|
+
/** Box size: md = 36px (matches the top bar), sm = 32px — both with a 20px icon;
|
|
29
|
+
* xs = 28px with a 16px icon (an action in a list row), 2xs = 24px with a 14px
|
|
30
|
+
* icon (an action on a chip or a tab). */
|
|
31
|
+
size?: IconButtonSize;
|
|
32
|
+
/** Glyph colour over the variant. `muted`: placeholder grey, full text colour on
|
|
33
|
+
* hover. `danger`: the same grey at rest, `--danger` on hover and focus — for a
|
|
34
|
+
* remove/delete that repeats down a list. Default: the variant's own colours. */
|
|
35
|
+
tone?: keyof typeof ICON_BUTTON_TONES;
|
|
36
|
+
/** Keep the click (and the Enter/Space that produces it) from reaching an
|
|
37
|
+
* ancestor's handler — for an action inside a clickable table row or card.
|
|
38
|
+
*
|
|
39
|
+
* **Not a licence to put this inside another `<button>`.** HTML forbids any
|
|
40
|
+
* interactive content, and any element with a `tabindex`, inside a button — so
|
|
41
|
+
* the `<span role="button" tabIndex={0}>` workaround is invalid too — and ARIA
|
|
42
|
+
* makes a button's children presentational, so a screen reader flattens the
|
|
43
|
+
* inner one into the outer one's name and it cannot be reached at all. Make the
|
|
44
|
+
* row's main action a button that fills the row and put this one BESIDE it, on
|
|
45
|
+
* top:
|
|
46
|
+
*
|
|
47
|
+
* ```tsx
|
|
48
|
+
* <div className="relative">
|
|
49
|
+
* <button className="w-full pe-16 …" onClick={open}>…row…</button>
|
|
50
|
+
* <div className="absolute inset-y-0 end-2 flex items-center gap-0.5">
|
|
51
|
+
* <IconButton size="xs" tone="danger" aria-label="Delete" onClick={remove}>
|
|
52
|
+
* <Trash2 />
|
|
53
|
+
* </IconButton>
|
|
54
|
+
* </div>
|
|
55
|
+
* </div>
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* The pair look exactly like the nested version, click exactly like it, and
|
|
59
|
+
* are two tab stops a screen reader can tell apart. */
|
|
60
|
+
stopPropagation?: boolean;
|
|
24
61
|
}
|
|
25
62
|
declare const IconButton: react.ForwardRefExoticComponent<IconButtonProps & react.RefAttributes<HTMLButtonElement>>;
|
|
26
63
|
declare const FIELD_BASE: string;
|
|
@@ -138,6 +175,33 @@ interface FieldLabelProps extends ComponentPropsWithoutRef<"span"> {
|
|
|
138
175
|
children: ReactNode;
|
|
139
176
|
}
|
|
140
177
|
declare function FieldLabel({ children, className, ...rest }: FieldLabelProps): react.JSX.Element;
|
|
178
|
+
interface LabelProps extends ComponentPropsWithoutRef<"label"> {
|
|
179
|
+
/** Draw the required mark after the text. The mark is `aria-hidden`: what a
|
|
180
|
+
* screen reader hears is the control's own `required` / `aria-required`, which
|
|
181
|
+
* the caller still sets — a "star" read out as the last word of every name is
|
|
182
|
+
* noise, and a control that is not marked required is not required however its
|
|
183
|
+
* label looks. */
|
|
184
|
+
required?: boolean;
|
|
185
|
+
/** `sm` for a toolbar or a dense filter row (12px), `md` otherwise (14px). */
|
|
186
|
+
size?: "sm" | "md";
|
|
187
|
+
/** Dim the label with its control. A label ABOVE its field is not the field's
|
|
188
|
+
* `peer`, so `peer-disabled:` cannot reach it the way it reaches a floating one. */
|
|
189
|
+
disabled?: boolean;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* The label ABOVE a field, for the places a floating label does not fit: a control
|
|
193
|
+
* the kit did not render (a third-party picker, a range slider, a group of radios),
|
|
194
|
+
* a filter bar whose fields are unlabelled {@link Select}s, a form that sets labels
|
|
195
|
+
* above its fields throughout.
|
|
196
|
+
*
|
|
197
|
+
* A new component rather than a mode of {@link FieldLabel}, because the two share
|
|
198
|
+
* nothing but the word. `FieldLabel` is an absolutely positioned `<span>` inside a
|
|
199
|
+
* trigger's `relative` box and has to stay exactly that for every dropdown that
|
|
200
|
+
* already lines up with it; this is a real `<label>` in normal flow, with `htmlFor`,
|
|
201
|
+
* that a click focuses the control through. Spacing below it belongs to the caller's
|
|
202
|
+
* layout (`space-y-1.5` on the pair, typically), as it would for any block.
|
|
203
|
+
*/
|
|
204
|
+
declare function Label({ required, size, disabled, className, children, ...rest }: LabelProps): react.JSX.Element;
|
|
141
205
|
/** The two names the password reveal toggle can wear. See {@link Input}. */
|
|
142
206
|
interface PasswordRevealLabels {
|
|
143
207
|
/** While the value is hidden — activating the toggle will show it. */
|
|
@@ -185,7 +249,7 @@ interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
|
185
249
|
passwordLabels?: Partial<PasswordRevealLabels>;
|
|
186
250
|
}
|
|
187
251
|
declare const Input: react.ForwardRefExoticComponent<InputProps & react.RefAttributes<HTMLInputElement>>;
|
|
188
|
-
interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
|
252
|
+
interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "size"> {
|
|
189
253
|
label?: ReactNode;
|
|
190
254
|
/** See {@link Input}'s `invalid`. */
|
|
191
255
|
invalid?: boolean;
|
|
@@ -193,6 +257,20 @@ interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
|
|
193
257
|
error?: ReactNode;
|
|
194
258
|
/** A {@link FieldHint} for the label line — see {@link FloatingField}. */
|
|
195
259
|
hint?: ReactNode;
|
|
260
|
+
/**
|
|
261
|
+
* `"sm"`: a 28px, 12px-type select for a toolbar or a table header, where the
|
|
262
|
+
* 36px field stands a head taller than the buttons beside it. `"md"` (default)
|
|
263
|
+
* is the field every form uses. Unlabelled selects only — a floating label needs
|
|
264
|
+
* the tall box to float in, so a labelled Select ignores `"sm"`.
|
|
265
|
+
*
|
|
266
|
+
* A NUMBER is still the native attribute (the rows of a list box) and is passed
|
|
267
|
+
* straight through, so the one HTML meaning of `size` keeps working.
|
|
268
|
+
*/
|
|
269
|
+
size?: "sm" | "md" | number;
|
|
270
|
+
/** Classes for the `<select>` itself. `className` styles the WRAPPER — the box
|
|
271
|
+
* the chevron is positioned against — so it could set a width and nothing
|
|
272
|
+
* else; this is the way to the element. See {@link Input}'s `inputClassName`. */
|
|
273
|
+
selectClassName?: string;
|
|
196
274
|
}
|
|
197
275
|
declare const Select: react.ForwardRefExoticComponent<SelectProps & react.RefAttributes<HTMLSelectElement>>;
|
|
198
276
|
interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
@@ -268,6 +346,43 @@ interface EmptyStateProps extends Omit<ComponentPropsWithoutRef<"div">, "childre
|
|
|
268
346
|
hint?: string;
|
|
269
347
|
}
|
|
270
348
|
declare function EmptyState({ title, hint, className, ...rest }: EmptyStateProps): react.JSX.Element;
|
|
349
|
+
/** One tab of a {@link Tabs} strip. */
|
|
350
|
+
interface TabItem<T extends string> {
|
|
351
|
+
id: T;
|
|
352
|
+
/** A ReactNode so a tab can pair an icon with text. */
|
|
353
|
+
label: ReactNode;
|
|
354
|
+
/** Optional trailing node (e.g. a count pill). */
|
|
355
|
+
badge?: ReactNode;
|
|
356
|
+
/** Marks a tab that IS a route — see {@link TabsProps}. */
|
|
357
|
+
href?: string;
|
|
358
|
+
/** Leading decoration: an icon, or the colour swatches of the lines this tab
|
|
359
|
+
* draws in a chart above the strip. Hidden from assistive tech — it repeats
|
|
360
|
+
* what the label says, or it says something the label must say instead. */
|
|
361
|
+
icon?: ReactNode;
|
|
362
|
+
/** A second, smaller line under the label — the one fact that tells two tabs
|
|
363
|
+
* with similar labels apart ("80 km/h", "Level 3"). Part of the tab's name. */
|
|
364
|
+
detail?: ReactNode;
|
|
365
|
+
/** A tab that exists as a slot with nothing in it yet, drawn with a dashed
|
|
366
|
+
* outline so a gap in a sequence looks like a gap. Visual only: say "empty" in
|
|
367
|
+
* the label or `detail` if a screen reader needs to know. */
|
|
368
|
+
empty?: boolean;
|
|
369
|
+
/** `false` keeps this one tab when the strip has `onRemove` — the one sheet a
|
|
370
|
+
* workbook cannot be without. Default: removable. */
|
|
371
|
+
removable?: boolean;
|
|
372
|
+
/** Plain-text name for the labels the strip composes about this tab ("Remove
|
|
373
|
+
* …"), for when `label` is not a string. Default: `label` if it is a string,
|
|
374
|
+
* else the id. */
|
|
375
|
+
name?: string;
|
|
376
|
+
}
|
|
377
|
+
/** The strings the strip renders on its own behalf — only when it can add or
|
|
378
|
+
* remove tabs. */
|
|
379
|
+
interface TabsLabels {
|
|
380
|
+
/** The "add" button's visible text when the caller gives no `addLabel`. */
|
|
381
|
+
add: string;
|
|
382
|
+
/** Accessible name of a tab's × — "Remove <tab>". */
|
|
383
|
+
remove: (tab: string) => string;
|
|
384
|
+
}
|
|
385
|
+
declare const DEFAULT_TABS_LABELS: TabsLabels;
|
|
271
386
|
/**
|
|
272
387
|
* The strip's own props sit on a `<div>`: the tablist IS the root element, so anything
|
|
273
388
|
* a caller hangs on it — a `data-tour` anchor for the kit's guided tour, a test id, an
|
|
@@ -283,12 +398,7 @@ interface TabsProps<T extends string> extends Omit<ComponentPropsWithoutRef<"div
|
|
|
283
398
|
* feedback #451), while a plain click still goes through `onChange` and stays
|
|
284
399
|
* client-side. Tabs that only flip local state leave it unset — a link to a URL
|
|
285
400
|
* that does not select the tab would be worse than no link. */
|
|
286
|
-
tabs:
|
|
287
|
-
id: T;
|
|
288
|
-
label: ReactNode;
|
|
289
|
-
badge?: ReactNode;
|
|
290
|
-
href?: string;
|
|
291
|
-
}[];
|
|
401
|
+
tabs: TabItem<T>[];
|
|
292
402
|
active: T;
|
|
293
403
|
onChange: (id: T) => void;
|
|
294
404
|
className?: string;
|
|
@@ -354,7 +464,35 @@ interface TabsProps<T extends string> extends Omit<ComponentPropsWithoutRef<"div
|
|
|
354
464
|
* describes a tab as opening something that is not there.
|
|
355
465
|
*/
|
|
356
466
|
panelId?: string;
|
|
467
|
+
/**
|
|
468
|
+
* Makes the tabs removable: an × on the OPEN tab, and Delete on a focused tab.
|
|
469
|
+
*
|
|
470
|
+
* The × is on the open tab only — a row of tabs each carrying one is a row of
|
|
471
|
+
* things to hit by accident, and the tab you can remove should be the one you
|
|
472
|
+
* are looking at — but its room is reserved on every removable tab, so opening a
|
|
473
|
+
* tab never widens it and never pushes the rest of the strip along (lenkbank
|
|
474
|
+
* feedback #72). It is named "Remove <tab>" and is not a tab stop of its own: the
|
|
475
|
+
* strip stays one stop, and the keyboard path is Delete (announced through
|
|
476
|
+
* `aria-keyshortcuts`), which removes whichever tab has focus.
|
|
477
|
+
*
|
|
478
|
+
* The strip only ASKS: it calls this, and the tab goes when the caller drops it
|
|
479
|
+
* from `tabs` — after a confirmation, a request, whatever it needs. When it does,
|
|
480
|
+
* focus that was on the removed tab (or its ×) moves to the open tab if the
|
|
481
|
+
* removed one was open, else to its neighbour, instead of falling to the page.
|
|
482
|
+
*/
|
|
483
|
+
onRemove?: (id: T) => void;
|
|
484
|
+
/** Renders an "add" button after the last tab, outside the tablist — it is an
|
|
485
|
+
* action, not a tab, and a tablist may own only tabs. */
|
|
486
|
+
onAdd?: () => void;
|
|
487
|
+
/** The add button's text. Say what is added — "Add loop", "Add report" says
|
|
488
|
+
* more than "Add". Default: `tabs.add` from the {@link UiKitProvider}. */
|
|
489
|
+
addLabel?: string;
|
|
490
|
+
/** Overrides for this strip's strings. See {@link TabsLabels}. */
|
|
491
|
+
labels?: Partial<TabsLabels>;
|
|
492
|
+
/** An add or a remove is in flight: the × and the add button are disabled and
|
|
493
|
+
* Delete is ignored, so a double click cannot remove two tabs. Nothing moves. */
|
|
494
|
+
busy?: boolean;
|
|
357
495
|
}
|
|
358
|
-
declare function Tabs<T extends string>({ tabs, active, onChange, className, wrap, label, panelId, "aria-label": ariaLabel, ...rest }: TabsProps<T>): react.JSX.Element;
|
|
496
|
+
declare function Tabs<T extends string>({ tabs, active, onChange, className, wrap, label, panelId, onRemove, onAdd, addLabel, labels, busy, "aria-label": ariaLabel, ...rest }: TabsProps<T>): react.JSX.Element;
|
|
359
497
|
|
|
360
|
-
export { Button, type ButtonProps, type ButtonVariant, Card, CardAction, type CardActionProps, CardContent, type CardContentProps, CardDescription, type CardDescriptionProps, CardFooter, type CardFooterProps, CardHeader, type CardHeaderProps, type CardProps, CardTitle, type CardTitleProps, DEFAULT_PASSWORD_REVEAL_LABELS, EmptyState, type EmptyStateProps, FIELD_BASE, FIELD_DISPLAY, FIELD_FLOATING_PAD, FIELD_INVALID, FIELD_TRIGGER, FIELD_WRITABLE_LOOK, FLOATING_INPUT_CLASS, FLOATING_LABEL_CLASS, FLOATING_LABEL_STATIC, FieldChevron, type FieldChevronProps, FieldHint, type FieldHintProps, FieldLabel, type FieldLabelProps, FloatingField, type FloatingFieldProps, IconButton, type IconButtonProps, Input, type InputProps, PHONE_QUERY, type PasswordRevealLabels, Select, type SelectProps, Spinner, type SpinnerProps, Tabs, type TabsProps, Textarea, type TextareaProps, buttonClasses, resolvePasswordRevealLabels };
|
|
498
|
+
export { Button, type ButtonProps, type ButtonVariant, Card, CardAction, type CardActionProps, CardContent, type CardContentProps, CardDescription, type CardDescriptionProps, CardFooter, type CardFooterProps, CardHeader, type CardHeaderProps, type CardProps, CardTitle, type CardTitleProps, DEFAULT_PASSWORD_REVEAL_LABELS, DEFAULT_TABS_LABELS, EmptyState, type EmptyStateProps, FIELD_BASE, FIELD_DISPLAY, FIELD_FLOATING_PAD, FIELD_INVALID, FIELD_TRIGGER, FIELD_WRITABLE_LOOK, FLOATING_INPUT_CLASS, FLOATING_LABEL_CLASS, FLOATING_LABEL_STATIC, FieldChevron, type FieldChevronProps, FieldHint, type FieldHintProps, FieldLabel, type FieldLabelProps, FloatingField, type FloatingFieldProps, IconButton, type IconButtonProps, type IconButtonSize, Input, type InputProps, Label, type LabelProps, PHONE_QUERY, type PasswordRevealLabels, Select, type SelectProps, Spinner, type SpinnerProps, type TabItem, Tabs, type TabsLabels, type TabsProps, Textarea, type TextareaProps, buttonClasses, resolvePasswordRevealLabels };
|