@eifi1/ui-kit 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -1
- package/dist/components/amount-input.d.ts +7 -0
- package/dist/components/autocomplete.d.ts +101 -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/chip.d.ts +22 -7
- 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/currency-select.js +10 -2
- package/dist/components/currency-select.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/date-picker.js +19 -7
- package/dist/components/date-picker.js.map +1 -1
- 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 +225 -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/month-picker.js +16 -5
- package/dist/components/month-picker.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/popover.js +4 -1
- package/dist/components/popover.js.map +1 -1
- 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/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/trigger-aria.d.ts +18 -0
- package/dist/components/trigger-aria.js +25 -0
- package/dist/components/trigger-aria.js.map +1 -0
- package/dist/components/ui.d.ts +164 -18
- package/dist/components/ui.js +207 -34
- 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 +425 -0
- package/src/components/chip.tsx +43 -7
- 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/currency-select.tsx +16 -2
- package/src/components/danger-confirm.tsx +286 -0
- package/src/components/date-picker.tsx +31 -6
- 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 +431 -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/month-picker.tsx +22 -4
- 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/popover.tsx +4 -1
- 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/tile-radio.tsx +228 -0
- package/src/components/toggle-group.tsx +54 -18
- package/src/components/trigger-aria.ts +42 -0
- package/src/components/ui.tsx +427 -40
- 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
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> {
|
|
@@ -232,9 +310,17 @@ declare function CardFooter({ className, ...props }: CardFooterProps): react.JSX
|
|
|
232
310
|
/** A `<span>`'s props plus the words a reader hears — see {@link CardHeaderProps}.
|
|
233
311
|
* The ring is drawn with a border, so there is nothing inside it to put children in. */
|
|
234
312
|
interface SpinnerProps extends Omit<ComponentPropsWithoutRef<"span">, "children"> {
|
|
235
|
-
/**
|
|
236
|
-
*
|
|
237
|
-
|
|
313
|
+
/**
|
|
314
|
+
* What the spinner means, for a screen reader. Default: `common.loading` from the
|
|
315
|
+
* {@link UiKitProvider}, else "Loading…".
|
|
316
|
+
*
|
|
317
|
+
* `null` makes it DECORATIVE — no role, no text, hidden from assistive tech. Use it
|
|
318
|
+
* whenever words are already there: beside visible "Loading…" text (otherwise it is
|
|
319
|
+
* announced twice), inside a labelled button (otherwise its text joins the button's
|
|
320
|
+
* name), or inside a live region of the app's own (otherwise that region and this
|
|
321
|
+
* one both announce).
|
|
322
|
+
*/
|
|
323
|
+
label?: string | null;
|
|
238
324
|
}
|
|
239
325
|
/**
|
|
240
326
|
* A spinning ring that also SAYS it is loading.
|
|
@@ -247,9 +333,9 @@ interface SpinnerProps extends Omit<ComponentPropsWithoutRef<"span">, "children"
|
|
|
247
333
|
* ignore a name on one.
|
|
248
334
|
*
|
|
249
335
|
* `relative` so the `sr-only` text has a local containing block (see
|
|
250
|
-
* sr-only-containment.test).
|
|
251
|
-
*
|
|
252
|
-
*
|
|
336
|
+
* sr-only-containment.test). Where the words are already on screen, pass
|
|
337
|
+
* `label={null}`: announcing is right for a spinner standing alone and wrong for one
|
|
338
|
+
* beside text, inside a button, or inside the app's own live region.
|
|
253
339
|
*/
|
|
254
340
|
declare function Spinner({ className, label, ...rest }: SpinnerProps): react.JSX.Element;
|
|
255
341
|
interface EmptyStateProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
|
@@ -260,6 +346,43 @@ interface EmptyStateProps extends Omit<ComponentPropsWithoutRef<"div">, "childre
|
|
|
260
346
|
hint?: string;
|
|
261
347
|
}
|
|
262
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;
|
|
263
386
|
/**
|
|
264
387
|
* The strip's own props sit on a `<div>`: the tablist IS the root element, so anything
|
|
265
388
|
* a caller hangs on it — a `data-tour` anchor for the kit's guided tour, a test id, an
|
|
@@ -275,12 +398,7 @@ interface TabsProps<T extends string> extends Omit<ComponentPropsWithoutRef<"div
|
|
|
275
398
|
* feedback #451), while a plain click still goes through `onChange` and stays
|
|
276
399
|
* client-side. Tabs that only flip local state leave it unset — a link to a URL
|
|
277
400
|
* that does not select the tab would be worse than no link. */
|
|
278
|
-
tabs:
|
|
279
|
-
id: T;
|
|
280
|
-
label: ReactNode;
|
|
281
|
-
badge?: ReactNode;
|
|
282
|
-
href?: string;
|
|
283
|
-
}[];
|
|
401
|
+
tabs: TabItem<T>[];
|
|
284
402
|
active: T;
|
|
285
403
|
onChange: (id: T) => void;
|
|
286
404
|
className?: string;
|
|
@@ -346,7 +464,35 @@ interface TabsProps<T extends string> extends Omit<ComponentPropsWithoutRef<"div
|
|
|
346
464
|
* describes a tab as opening something that is not there.
|
|
347
465
|
*/
|
|
348
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;
|
|
349
495
|
}
|
|
350
|
-
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;
|
|
351
497
|
|
|
352
|
-
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 };
|
package/dist/components/ui.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { forwardRef, useId, useState } from "react";
|
|
4
|
-
import { ChevronDown, Eye, EyeOff, HelpCircle } from "lucide-react";
|
|
3
|
+
import { forwardRef, useId, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import { ChevronDown, Eye, EyeOff, HelpCircle, Plus, X } from "lucide-react";
|
|
5
5
|
import { cn } from "../lib/cn.js";
|
|
6
6
|
import { useMediaQuery } from "../hooks/use-media-query.js";
|
|
7
7
|
import { Tooltip } from "./tooltip.js";
|
|
@@ -34,17 +34,37 @@ function Button({ variant = "primary", stretch, className, ...rest }) {
|
|
|
34
34
|
}
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
|
-
const ICON_BUTTON_BASE = "inline-flex items-center justify-center rounded-md transition-colors focus:outline-none focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed
|
|
38
|
-
const
|
|
37
|
+
const ICON_BUTTON_BASE = "inline-flex items-center justify-center rounded-md transition-colors focus:outline-none focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed";
|
|
38
|
+
const ICON_BUTTON_SIZES = {
|
|
39
|
+
md: "size-9 [&_svg]:size-5",
|
|
40
|
+
sm: "size-8 [&_svg]:size-5",
|
|
41
|
+
xs: "size-7 rounded [&_svg]:size-4",
|
|
42
|
+
"2xs": "size-6 rounded [&_svg]:size-3.5"
|
|
43
|
+
};
|
|
44
|
+
const ICON_BUTTON_TONES = {
|
|
45
|
+
default: "",
|
|
46
|
+
muted: "text-[var(--text-placeholder)] hover:bg-[var(--bg-surface-2)] hover:text-[var(--text-primary)]",
|
|
47
|
+
danger: "text-[var(--text-placeholder)] hover:bg-[var(--danger-bg)] hover:text-[var(--danger)] focus:ring-[var(--danger-border)]"
|
|
48
|
+
};
|
|
49
|
+
const IconButton = forwardRef(function IconButton2({ variant = "ghost", size = "md", tone = "default", stopPropagation, className, onClick, onKeyDown, ...rest }, ref) {
|
|
39
50
|
return /* @__PURE__ */ jsx(
|
|
40
51
|
"button",
|
|
41
52
|
{
|
|
42
53
|
ref,
|
|
43
54
|
...rest,
|
|
55
|
+
onClick: (e) => {
|
|
56
|
+
if (stopPropagation) e.stopPropagation();
|
|
57
|
+
onClick?.(e);
|
|
58
|
+
},
|
|
59
|
+
onKeyDown: (e) => {
|
|
60
|
+
if (stopPropagation && (e.key === "Enter" || e.key === " ")) e.stopPropagation();
|
|
61
|
+
onKeyDown?.(e);
|
|
62
|
+
},
|
|
44
63
|
className: cn(
|
|
45
64
|
ICON_BUTTON_BASE,
|
|
46
|
-
size
|
|
65
|
+
ICON_BUTTON_SIZES[size],
|
|
47
66
|
buttonVariantClasses[variant],
|
|
67
|
+
ICON_BUTTON_TONES[tone],
|
|
48
68
|
className
|
|
49
69
|
)
|
|
50
70
|
}
|
|
@@ -167,13 +187,31 @@ function FieldHint({
|
|
|
167
187
|
function FieldLabel({ children, className, ...rest }) {
|
|
168
188
|
return /* @__PURE__ */ jsx("span", { ...rest, className: cn(FLOATING_LABEL_STATIC, "z-10", className), children });
|
|
169
189
|
}
|
|
190
|
+
function Label({ required, size = "md", disabled, className, children, ...rest }) {
|
|
191
|
+
return /* @__PURE__ */ jsxs(
|
|
192
|
+
"label",
|
|
193
|
+
{
|
|
194
|
+
...rest,
|
|
195
|
+
className: cn(
|
|
196
|
+
"inline-flex items-center gap-0.5 font-medium leading-none text-[var(--text-primary)]",
|
|
197
|
+
size === "sm" ? "text-xs" : "text-sm",
|
|
198
|
+
disabled && "cursor-default opacity-50",
|
|
199
|
+
className
|
|
200
|
+
),
|
|
201
|
+
children: [
|
|
202
|
+
children,
|
|
203
|
+
required && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "text-[var(--danger)]", children: "*" })
|
|
204
|
+
]
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
}
|
|
170
208
|
const FIELD_ERROR_CLASS = "mt-1 text-[11px] leading-tight text-[var(--danger)]";
|
|
171
|
-
function useFieldError(error, invalid, describedBy) {
|
|
209
|
+
function useFieldError(error, invalid, describedBy, ariaInvalid) {
|
|
172
210
|
const errorId = useId();
|
|
173
211
|
const hasError = error !== void 0 && error !== null && error !== false && error !== "";
|
|
174
212
|
return {
|
|
175
213
|
/** A field carrying a message that says what is wrong with it IS wrong. */
|
|
176
|
-
isInvalid: Boolean(invalid) || hasError,
|
|
214
|
+
isInvalid: Boolean(invalid) || hasError || ariaInvalid === true || ariaInvalid === "true",
|
|
177
215
|
describedBy: hasError ? describedBy ? `${describedBy} ${errorId}` : errorId : describedBy,
|
|
178
216
|
errorEl: hasError ? /* @__PURE__ */ jsx("p", { id: errorId, className: FIELD_ERROR_CLASS, children: error }) : null
|
|
179
217
|
};
|
|
@@ -212,7 +250,8 @@ const Input = forwardRef(function Input2({
|
|
|
212
250
|
const { isInvalid, describedBy, errorEl } = useFieldError(
|
|
213
251
|
error,
|
|
214
252
|
invalid,
|
|
215
|
-
rest["aria-describedby"]
|
|
253
|
+
rest["aria-describedby"],
|
|
254
|
+
rest["aria-invalid"]
|
|
216
255
|
);
|
|
217
256
|
const asDisplay = useMediaQuery(PHONE_QUERY, false) && variant === "display";
|
|
218
257
|
const isPassword = type === "password";
|
|
@@ -296,15 +335,19 @@ const Input = forwardRef(function Input2({
|
|
|
296
335
|
] }) });
|
|
297
336
|
});
|
|
298
337
|
Input.displayName = "Input";
|
|
299
|
-
const
|
|
338
|
+
const SELECT_SM = "h-7 py-0 pl-2 pr-7 text-xs";
|
|
339
|
+
const Select = forwardRef(function Select2({ className, label, id, children, invalid, error, hint, size, selectClassName, ...rest }, ref) {
|
|
300
340
|
const generated = useId();
|
|
341
|
+
const nativeSize = typeof size === "number" ? size : void 0;
|
|
342
|
+
const small = size === "sm" && label === void 0;
|
|
301
343
|
const fieldId = id ?? generated;
|
|
302
344
|
const { isInvalid, describedBy, errorEl } = useFieldError(
|
|
303
345
|
error,
|
|
304
346
|
invalid,
|
|
305
|
-
rest["aria-describedby"]
|
|
347
|
+
rest["aria-describedby"],
|
|
348
|
+
rest["aria-invalid"]
|
|
306
349
|
);
|
|
307
|
-
const chevron = rest.disabled ? null : /* @__PURE__ */ jsx(FieldChevron, {});
|
|
350
|
+
const chevron = rest.disabled ? null : /* @__PURE__ */ jsx(FieldChevron, { className: small ? "right-1.5 size-3.5" : void 0 });
|
|
308
351
|
if (label === void 0) {
|
|
309
352
|
return /* @__PURE__ */ jsx(FieldGroup, { errorEl, children: /* @__PURE__ */ jsxs("div", { className: cn("relative", className), children: [
|
|
310
353
|
/* @__PURE__ */ jsx(
|
|
@@ -312,10 +355,17 @@ const Select = forwardRef(function Select2({ className, label, id, children, inv
|
|
|
312
355
|
{
|
|
313
356
|
ref,
|
|
314
357
|
id,
|
|
358
|
+
size: nativeSize,
|
|
315
359
|
...rest,
|
|
316
360
|
"aria-invalid": isInvalid || rest["aria-invalid"] || void 0,
|
|
317
361
|
"aria-describedby": describedBy,
|
|
318
|
-
className: cn(
|
|
362
|
+
className: cn(
|
|
363
|
+
FIELD_BASE,
|
|
364
|
+
"appearance-none pr-9",
|
|
365
|
+
small && SELECT_SM,
|
|
366
|
+
selectClassName,
|
|
367
|
+
isInvalid && FIELD_INVALID
|
|
368
|
+
),
|
|
319
369
|
children
|
|
320
370
|
}
|
|
321
371
|
),
|
|
@@ -328,6 +378,7 @@ const Select = forwardRef(function Select2({ className, label, id, children, inv
|
|
|
328
378
|
{
|
|
329
379
|
ref,
|
|
330
380
|
id: fieldId,
|
|
381
|
+
size: nativeSize,
|
|
331
382
|
...rest,
|
|
332
383
|
"aria-invalid": isInvalid || rest["aria-invalid"] || void 0,
|
|
333
384
|
"aria-describedby": describedBy,
|
|
@@ -335,6 +386,7 @@ const Select = forwardRef(function Select2({ className, label, id, children, inv
|
|
|
335
386
|
FIELD_BASE,
|
|
336
387
|
FIELD_FLOATING_PAD,
|
|
337
388
|
"peer appearance-none pr-9",
|
|
389
|
+
selectClassName,
|
|
338
390
|
isInvalid && FIELD_INVALID
|
|
339
391
|
),
|
|
340
392
|
children
|
|
@@ -350,7 +402,8 @@ const Textarea = forwardRef(function Textarea2({ className, label, id, placehold
|
|
|
350
402
|
const { isInvalid, describedBy, errorEl } = useFieldError(
|
|
351
403
|
error,
|
|
352
404
|
invalid,
|
|
353
|
-
rest["aria-describedby"]
|
|
405
|
+
rest["aria-describedby"],
|
|
406
|
+
rest["aria-invalid"]
|
|
354
407
|
);
|
|
355
408
|
if (label === void 0) {
|
|
356
409
|
return /* @__PURE__ */ jsx(FieldGroup, { errorEl, children: /* @__PURE__ */ jsx(
|
|
@@ -456,19 +509,18 @@ function CardFooter({ className, ...props }) {
|
|
|
456
509
|
);
|
|
457
510
|
}
|
|
458
511
|
function Spinner({ className, label, ...rest }) {
|
|
459
|
-
const common = useKitLabels("common", DEFAULT_COMMON_LABELS, {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
className: cn(
|
|
466
|
-
"relative inline-block h-5 w-5 animate-spin rounded-full border-2 border-[var(--border)] border-t-[var(--text-primary)]",
|
|
467
|
-
className
|
|
468
|
-
),
|
|
469
|
-
children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: common.loading })
|
|
470
|
-
}
|
|
512
|
+
const common = useKitLabels("common", DEFAULT_COMMON_LABELS, {
|
|
513
|
+
loading: label ?? void 0
|
|
514
|
+
});
|
|
515
|
+
const ring = cn(
|
|
516
|
+
"relative inline-block h-5 w-5 animate-spin rounded-full border-2 border-[var(--border)] border-t-[var(--text-primary)]",
|
|
517
|
+
className
|
|
471
518
|
);
|
|
519
|
+
if (label === null) return /* @__PURE__ */ jsx("span", { "aria-hidden": true, ...rest, className: ring });
|
|
520
|
+
return /* @__PURE__ */ jsx("span", { role: "status", ...rest, className: ring, children: /* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
|
|
521
|
+
common.loading,
|
|
522
|
+
" "
|
|
523
|
+
] }) });
|
|
472
524
|
}
|
|
473
525
|
function EmptyState({ title, hint, className, ...rest }) {
|
|
474
526
|
return /* @__PURE__ */ jsxs(
|
|
@@ -486,6 +538,10 @@ function EmptyState({ title, hint, className, ...rest }) {
|
|
|
486
538
|
}
|
|
487
539
|
);
|
|
488
540
|
}
|
|
541
|
+
const DEFAULT_TABS_LABELS = {
|
|
542
|
+
add: "Add tab",
|
|
543
|
+
remove: (tab) => `Remove ${tab}`
|
|
544
|
+
};
|
|
489
545
|
const TABLIST_CLASSES = "flex gap-1 overflow-x-auto overflow-y-hidden border-b border-[var(--border)]";
|
|
490
546
|
const TABLIST_WRAP_CLASSES = "flex flex-wrap gap-1.5 md:flex-nowrap md:gap-1 md:overflow-x-auto md:overflow-y-hidden md:border-b md:border-[var(--border)]";
|
|
491
547
|
const TAB_CLASSES = "whitespace-nowrap px-3 py-2 text-sm font-medium transition-colors border-b-2 -mb-px focus:outline-none focus:ring-2 focus:ring-[var(--border-strong)]";
|
|
@@ -494,6 +550,10 @@ const TAB_INACTIVE_CLASSES = "border-transparent text-[var(--text-muted)] hover:
|
|
|
494
550
|
const TAB_WRAP_CLASSES = "whitespace-nowrap rounded-md px-2.5 py-2 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--border-strong)] md:rounded-none md:border-b-2 md:-mb-px md:bg-transparent md:px-3 md:py-2 md:text-sm";
|
|
495
551
|
const TAB_WRAP_ACTIVE_CLASSES = "bg-[var(--brand)] text-[var(--brand-contrast)] md:border-[var(--text-primary)] md:text-[var(--text-primary)]";
|
|
496
552
|
const TAB_WRAP_INACTIVE_CLASSES = "bg-[var(--bg-surface)] text-[var(--text-primary)] md:border-transparent md:text-[var(--text-muted)] md:hover:border-[var(--border-strong)] md:hover:text-[var(--text-secondary)]";
|
|
553
|
+
const TAB_REMOVABLE_CLASSES = "pe-8 md:pe-8";
|
|
554
|
+
const TAB_EMPTY_CLASSES = "-mx-1.5 rounded border border-dashed border-[var(--border-strong)] px-1.5";
|
|
555
|
+
const TAB_ADD_CLASSES = "inline-flex items-center gap-1 whitespace-nowrap rounded-md px-3 py-2 text-sm font-medium text-[var(--text-muted)] transition-colors hover:text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-[var(--border-strong)] disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:size-4";
|
|
556
|
+
const TAB_ADD_WRAP_CLASSES = "px-2.5 text-xs md:px-3 md:text-sm";
|
|
497
557
|
function Tabs({
|
|
498
558
|
tabs,
|
|
499
559
|
active,
|
|
@@ -502,10 +562,46 @@ function Tabs({
|
|
|
502
562
|
wrap = false,
|
|
503
563
|
label,
|
|
504
564
|
panelId,
|
|
565
|
+
onRemove,
|
|
566
|
+
onAdd,
|
|
567
|
+
addLabel,
|
|
568
|
+
labels,
|
|
569
|
+
busy = false,
|
|
505
570
|
"aria-label": ariaLabel,
|
|
506
571
|
...rest
|
|
507
572
|
}) {
|
|
508
|
-
const
|
|
573
|
+
const text = useKitLabels("tabs", DEFAULT_TABS_LABELS, labels);
|
|
574
|
+
const stripRef = useRef(null);
|
|
575
|
+
const isRemovable = (tab) => onRemove !== void 0 && tab.removable !== false;
|
|
576
|
+
const nameOf = (tab) => tab.name ?? (typeof tab.label === "string" ? tab.label : tab.id);
|
|
577
|
+
const pendingFocus = useRef(null);
|
|
578
|
+
const requestRemove = (id) => {
|
|
579
|
+
const index = tabs.findIndex((t) => t.id === id);
|
|
580
|
+
pendingFocus.current = {
|
|
581
|
+
removed: id,
|
|
582
|
+
wasActive: id === active,
|
|
583
|
+
neighbour: (tabs[index + 1] ?? tabs[index - 1])?.id
|
|
584
|
+
};
|
|
585
|
+
onRemove?.(id);
|
|
586
|
+
};
|
|
587
|
+
useLayoutEffect(() => {
|
|
588
|
+
const pending = pendingFocus.current;
|
|
589
|
+
if (!pending || tabs.some((t) => t.id === pending.removed)) return;
|
|
590
|
+
pendingFocus.current = null;
|
|
591
|
+
const strip = stripRef.current;
|
|
592
|
+
if (!strip) return;
|
|
593
|
+
const focused = document.activeElement;
|
|
594
|
+
if (focused && focused !== document.body && !strip.contains(focused)) return;
|
|
595
|
+
const target = !pending.wasActive && tabs.some((t) => t.id === pending.neighbour) ? pending.neighbour : active;
|
|
596
|
+
const index = tabs.findIndex((t) => t.id === target);
|
|
597
|
+
strip.querySelectorAll('[role="tab"]')[index]?.focus();
|
|
598
|
+
}, [tabs, active]);
|
|
599
|
+
const onTabKeyDown = (e, tab) => {
|
|
600
|
+
if (e.key === "Delete" && isRemovable(tab)) {
|
|
601
|
+
e.preventDefault();
|
|
602
|
+
if (!busy) requestRemove(tab.id);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
509
605
|
const step = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
|
|
510
606
|
if (step === 0 && e.key !== "Home" && e.key !== "End") return;
|
|
511
607
|
const strip = e.currentTarget.closest('[role="tablist"]');
|
|
@@ -517,15 +613,20 @@ function Tabs({
|
|
|
517
613
|
const to = e.key === "Home" ? 0 : e.key === "End" ? items.length - 1 : (from + step + items.length) % items.length;
|
|
518
614
|
items[to]?.focus();
|
|
519
615
|
};
|
|
520
|
-
|
|
616
|
+
const tablist = /* @__PURE__ */ jsx(
|
|
521
617
|
"div",
|
|
522
618
|
{
|
|
523
619
|
...rest,
|
|
620
|
+
ref: stripRef,
|
|
524
621
|
role: "tablist",
|
|
525
622
|
"aria-label": ariaLabel ?? label,
|
|
526
|
-
className: cn(
|
|
623
|
+
className: cn(
|
|
624
|
+
wrap ? TABLIST_WRAP_CLASSES : TABLIST_CLASSES,
|
|
625
|
+
onAdd ? "min-w-0" : className
|
|
626
|
+
),
|
|
527
627
|
children: tabs.map((tab) => {
|
|
528
628
|
const isActive = tab.id === active;
|
|
629
|
+
const removable = isRemovable(tab);
|
|
529
630
|
const shared = {
|
|
530
631
|
role: "tab",
|
|
531
632
|
"aria-selected": isActive,
|
|
@@ -545,17 +646,24 @@ function Tabs({
|
|
|
545
646
|
// Only the open tab: see `panelId`.
|
|
546
647
|
"aria-controls": isActive ? panelId : void 0,
|
|
547
648
|
id: isActive && panelId ? `${panelId}-tab` : void 0,
|
|
548
|
-
|
|
649
|
+
"aria-keyshortcuts": removable ? "Delete" : void 0,
|
|
650
|
+
onKeyDown: (e) => onTabKeyDown(e, tab),
|
|
549
651
|
className: cn(
|
|
550
652
|
wrap ? TAB_WRAP_CLASSES : TAB_CLASSES,
|
|
551
|
-
wrap ? isActive ? TAB_WRAP_ACTIVE_CLASSES : TAB_WRAP_INACTIVE_CLASSES : isActive ? TAB_ACTIVE_CLASSES : TAB_INACTIVE_CLASSES
|
|
653
|
+
wrap ? isActive ? TAB_WRAP_ACTIVE_CLASSES : TAB_WRAP_INACTIVE_CLASSES : isActive ? TAB_ACTIVE_CLASSES : TAB_INACTIVE_CLASSES,
|
|
654
|
+
removable && TAB_REMOVABLE_CLASSES
|
|
552
655
|
)
|
|
553
656
|
};
|
|
554
|
-
const inner = /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5", children: [
|
|
555
|
-
tab.
|
|
657
|
+
const inner = /* @__PURE__ */ jsxs("span", { className: cn("inline-flex items-center gap-1.5", tab.empty && TAB_EMPTY_CLASSES), children: [
|
|
658
|
+
tab.icon != null && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "inline-flex shrink-0 items-center gap-0.5", children: tab.icon }),
|
|
659
|
+
tab.detail != null ? /* @__PURE__ */ jsxs("span", { className: "flex flex-col items-start text-start", children: [
|
|
660
|
+
/* @__PURE__ */ jsx("span", { children: tab.label }),
|
|
661
|
+
" ",
|
|
662
|
+
/* @__PURE__ */ jsx("span", { className: "text-[11px] font-normal leading-tight text-[var(--text-muted)]", children: tab.detail })
|
|
663
|
+
] }) : tab.label,
|
|
556
664
|
tab.badge != null ? tab.badge : null
|
|
557
665
|
] });
|
|
558
|
-
|
|
666
|
+
const element = tab.href ? /* @__PURE__ */ jsx(
|
|
559
667
|
"a",
|
|
560
668
|
{
|
|
561
669
|
...shared,
|
|
@@ -567,7 +675,7 @@ function Tabs({
|
|
|
567
675
|
onChange(tab.id);
|
|
568
676
|
},
|
|
569
677
|
onKeyDown: (e) => {
|
|
570
|
-
onTabKeyDown(e);
|
|
678
|
+
onTabKeyDown(e, tab);
|
|
571
679
|
if (e.defaultPrevented || e.key !== " ") return;
|
|
572
680
|
e.preventDefault();
|
|
573
681
|
onChange(tab.id);
|
|
@@ -576,9 +684,72 @@ function Tabs({
|
|
|
576
684
|
},
|
|
577
685
|
tab.id
|
|
578
686
|
) : /* @__PURE__ */ jsx("button", { ...shared, onClick: () => onChange(tab.id), children: inner }, tab.id);
|
|
687
|
+
if (!removable) return element;
|
|
688
|
+
return (
|
|
689
|
+
// The × cannot go INSIDE the tab: a button in a button is invalid HTML, and
|
|
690
|
+
// a tab's children are presentational, so a reader would flatten it into the
|
|
691
|
+
// tab's name. It sits beside the tab, over the gutter the tab reserves.
|
|
692
|
+
/* @__PURE__ */ jsxs("div", { className: "relative flex shrink-0", children: [
|
|
693
|
+
element,
|
|
694
|
+
isActive && /* @__PURE__ */ jsx(
|
|
695
|
+
IconButton,
|
|
696
|
+
{
|
|
697
|
+
size: "2xs",
|
|
698
|
+
tone: "danger",
|
|
699
|
+
tabIndex: -1,
|
|
700
|
+
type: "button",
|
|
701
|
+
"aria-label": text.remove(nameOf(tab)),
|
|
702
|
+
disabled: busy,
|
|
703
|
+
onClick: () => requestRemove(tab.id),
|
|
704
|
+
className: cn(
|
|
705
|
+
"absolute inset-y-0 end-1 my-auto",
|
|
706
|
+
// On a wrapped strip's filled chip the quiet grey would vanish into
|
|
707
|
+
// the brand fill below `md`.
|
|
708
|
+
wrap && "text-[var(--brand-contrast)] md:text-[var(--text-placeholder)]"
|
|
709
|
+
),
|
|
710
|
+
children: /* @__PURE__ */ jsx(X, {})
|
|
711
|
+
}
|
|
712
|
+
)
|
|
713
|
+
] }, tab.id)
|
|
714
|
+
);
|
|
579
715
|
})
|
|
580
716
|
}
|
|
581
717
|
);
|
|
718
|
+
if (!onAdd) return tablist;
|
|
719
|
+
return /* @__PURE__ */ jsxs(
|
|
720
|
+
"div",
|
|
721
|
+
{
|
|
722
|
+
className: cn(
|
|
723
|
+
wrap ? "flex flex-wrap items-end gap-1.5 md:flex-nowrap md:gap-0" : "flex items-end",
|
|
724
|
+
className
|
|
725
|
+
),
|
|
726
|
+
children: [
|
|
727
|
+
tablist,
|
|
728
|
+
/* @__PURE__ */ jsx(
|
|
729
|
+
"div",
|
|
730
|
+
{
|
|
731
|
+
className: cn(
|
|
732
|
+
"flex flex-1 items-center",
|
|
733
|
+
wrap ? "md:self-stretch md:border-b md:border-[var(--border)]" : "self-stretch border-b border-[var(--border)]"
|
|
734
|
+
),
|
|
735
|
+
children: /* @__PURE__ */ jsxs(
|
|
736
|
+
"button",
|
|
737
|
+
{
|
|
738
|
+
type: "button",
|
|
739
|
+
onClick: onAdd,
|
|
740
|
+
disabled: busy,
|
|
741
|
+
className: cn(TAB_ADD_CLASSES, wrap && TAB_ADD_WRAP_CLASSES),
|
|
742
|
+
children: [
|
|
743
|
+
/* @__PURE__ */ jsx(Plus, { "aria-hidden": true }),
|
|
744
|
+
addLabel ?? text.add
|
|
745
|
+
]
|
|
746
|
+
}
|
|
747
|
+
)
|
|
748
|
+
}
|
|
749
|
+
)
|
|
750
|
+
]
|
|
751
|
+
}
|
|
752
|
+
);
|
|
582
753
|
}
|
|
583
754
|
export {
|
|
584
755
|
Button,
|
|
@@ -590,6 +761,7 @@ export {
|
|
|
590
761
|
CardHeader,
|
|
591
762
|
CardTitle,
|
|
592
763
|
DEFAULT_PASSWORD_REVEAL_LABELS,
|
|
764
|
+
DEFAULT_TABS_LABELS,
|
|
593
765
|
EmptyState,
|
|
594
766
|
FIELD_BASE,
|
|
595
767
|
FIELD_DISPLAY,
|
|
@@ -606,6 +778,7 @@ export {
|
|
|
606
778
|
FloatingField,
|
|
607
779
|
IconButton,
|
|
608
780
|
Input,
|
|
781
|
+
Label,
|
|
609
782
|
PHONE_QUERY,
|
|
610
783
|
Select,
|
|
611
784
|
Spinner,
|