@eifi1/ui-kit 0.5.1 → 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 +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 +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/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/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 +425 -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 +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/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/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
package/src/components/ui.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { forwardRef, useId, useState } from "react";
|
|
2
|
-
import { ChevronDown, Eye, EyeOff, HelpCircle } from "lucide-react";
|
|
1
|
+
import { forwardRef, useId, useLayoutEffect, useRef, useState } from "react";
|
|
2
|
+
import { ChevronDown, Eye, EyeOff, HelpCircle, Plus, X } from "lucide-react";
|
|
3
3
|
import type { ButtonHTMLAttributes, ComponentPropsWithoutRef, InputHTMLAttributes, KeyboardEvent, MouseEvent, ReactNode, SelectHTMLAttributes, TextareaHTMLAttributes } from "react";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
import { useMediaQuery } from "../hooks/use-media-query";
|
|
@@ -71,33 +71,101 @@ export function Button({ variant = "primary", stretch, className, ...rest }: But
|
|
|
71
71
|
// Canonical square icon-only button. <Button> carries TEXT geometry — px-3 py-2
|
|
72
72
|
// gap-2 — so rendering a bare glyph through it gives a small icon in a wide,
|
|
73
73
|
// text-shaped box. This fixes a square box instead and, crucially, forces the
|
|
74
|
-
// child icon to 20px via `[&_svg]:size
|
|
75
|
-
// it wherever an action is a bare icon
|
|
76
|
-
// top-bar icon buttons and can never
|
|
74
|
+
// child icon to a size set by the BOX (20px at md/sm) via `[&_svg]:size-*` so a
|
|
75
|
+
// caller cannot under-size it. Use it wherever an action is a bare icon
|
|
76
|
+
// (edit/delete/tools) so they all match the top-bar icon buttons and can never
|
|
77
|
+
// drift apart again.
|
|
77
78
|
//
|
|
78
79
|
// Draws its colours from the same `buttonVariantClasses` map as <Button>, so the
|
|
79
80
|
// two re-skin together with the palette.
|
|
80
81
|
const ICON_BUTTON_BASE =
|
|
81
|
-
"inline-flex items-center justify-center rounded-md transition-colors focus:outline-none focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed
|
|
82
|
+
"inline-flex items-center justify-center rounded-md transition-colors focus:outline-none focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed";
|
|
83
|
+
|
|
84
|
+
export type IconButtonSize = "md" | "sm" | "xs" | "2xs";
|
|
85
|
+
|
|
86
|
+
// Box and glyph together, so a 24px chip action cannot end up holding a 20px icon
|
|
87
|
+
// that touches its edges. The two small steps are lenkbank's list-row (28px) and
|
|
88
|
+
// chip (24px) actions, which it had hand-rolled beside the kit's 32/36px ones.
|
|
89
|
+
const ICON_BUTTON_SIZES: Record<IconButtonSize, string> = {
|
|
90
|
+
md: "size-9 [&_svg]:size-5",
|
|
91
|
+
sm: "size-8 [&_svg]:size-5",
|
|
92
|
+
xs: "size-7 rounded [&_svg]:size-4",
|
|
93
|
+
"2xs": "size-6 rounded [&_svg]:size-3.5",
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// A tone re-colours the glyph without changing what the variant draws around it.
|
|
97
|
+
// `muted` and `danger` both sit quiet at rest — an action in every row of a list
|
|
98
|
+
// must not shout from every row — and `danger` answers the pointer in the
|
|
99
|
+
// destructive family, so the red arrives only on the one row you are about to
|
|
100
|
+
// act on.
|
|
101
|
+
const ICON_BUTTON_TONES = {
|
|
102
|
+
default: "",
|
|
103
|
+
muted:
|
|
104
|
+
"text-[var(--text-placeholder)] hover:bg-[var(--bg-surface-2)] hover:text-[var(--text-primary)]",
|
|
105
|
+
danger:
|
|
106
|
+
"text-[var(--text-placeholder)] hover:bg-[var(--danger-bg)] hover:text-[var(--danger)] focus:ring-[var(--danger-border)]",
|
|
107
|
+
} as const;
|
|
82
108
|
|
|
83
109
|
export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
84
110
|
variant?: ButtonVariant;
|
|
85
|
-
/** Box size: md = 36px (matches the top bar), sm = 32px
|
|
86
|
-
|
|
111
|
+
/** Box size: md = 36px (matches the top bar), sm = 32px — both with a 20px icon;
|
|
112
|
+
* xs = 28px with a 16px icon (an action in a list row), 2xs = 24px with a 14px
|
|
113
|
+
* icon (an action on a chip or a tab). */
|
|
114
|
+
size?: IconButtonSize;
|
|
115
|
+
/** Glyph colour over the variant. `muted`: placeholder grey, full text colour on
|
|
116
|
+
* hover. `danger`: the same grey at rest, `--danger` on hover and focus — for a
|
|
117
|
+
* remove/delete that repeats down a list. Default: the variant's own colours. */
|
|
118
|
+
tone?: keyof typeof ICON_BUTTON_TONES;
|
|
119
|
+
/** Keep the click (and the Enter/Space that produces it) from reaching an
|
|
120
|
+
* ancestor's handler — for an action inside a clickable table row or card.
|
|
121
|
+
*
|
|
122
|
+
* **Not a licence to put this inside another `<button>`.** HTML forbids any
|
|
123
|
+
* interactive content, and any element with a `tabindex`, inside a button — so
|
|
124
|
+
* the `<span role="button" tabIndex={0}>` workaround is invalid too — and ARIA
|
|
125
|
+
* makes a button's children presentational, so a screen reader flattens the
|
|
126
|
+
* inner one into the outer one's name and it cannot be reached at all. Make the
|
|
127
|
+
* row's main action a button that fills the row and put this one BESIDE it, on
|
|
128
|
+
* top:
|
|
129
|
+
*
|
|
130
|
+
* ```tsx
|
|
131
|
+
* <div className="relative">
|
|
132
|
+
* <button className="w-full pe-16 …" onClick={open}>…row…</button>
|
|
133
|
+
* <div className="absolute inset-y-0 end-2 flex items-center gap-0.5">
|
|
134
|
+
* <IconButton size="xs" tone="danger" aria-label="Delete" onClick={remove}>
|
|
135
|
+
* <Trash2 />
|
|
136
|
+
* </IconButton>
|
|
137
|
+
* </div>
|
|
138
|
+
* </div>
|
|
139
|
+
* ```
|
|
140
|
+
*
|
|
141
|
+
* The pair look exactly like the nested version, click exactly like it, and
|
|
142
|
+
* are two tab stops a screen reader can tell apart. */
|
|
143
|
+
stopPropagation?: boolean;
|
|
87
144
|
}
|
|
88
145
|
|
|
89
146
|
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(function IconButton(
|
|
90
|
-
{ variant = "ghost", size = "md", className, ...rest },
|
|
147
|
+
{ variant = "ghost", size = "md", tone = "default", stopPropagation, className, onClick, onKeyDown, ...rest },
|
|
91
148
|
ref,
|
|
92
149
|
) {
|
|
93
150
|
return (
|
|
94
151
|
<button
|
|
95
152
|
ref={ref}
|
|
96
153
|
{...rest}
|
|
154
|
+
onClick={(e) => {
|
|
155
|
+
if (stopPropagation) e.stopPropagation();
|
|
156
|
+
onClick?.(e);
|
|
157
|
+
}}
|
|
158
|
+
onKeyDown={(e) => {
|
|
159
|
+
// A button turns Enter/Space into a click of its own, but the KEY still
|
|
160
|
+
// bubbles — and a clickable row listening for Enter would act on it too.
|
|
161
|
+
if (stopPropagation && (e.key === "Enter" || e.key === " ")) e.stopPropagation();
|
|
162
|
+
onKeyDown?.(e);
|
|
163
|
+
}}
|
|
97
164
|
className={cn(
|
|
98
165
|
ICON_BUTTON_BASE,
|
|
99
|
-
size
|
|
166
|
+
ICON_BUTTON_SIZES[size],
|
|
100
167
|
buttonVariantClasses[variant],
|
|
168
|
+
ICON_BUTTON_TONES[tone],
|
|
101
169
|
className,
|
|
102
170
|
)}
|
|
103
171
|
/>
|
|
@@ -451,6 +519,54 @@ export function FieldLabel({ children, className, ...rest }: FieldLabelProps) {
|
|
|
451
519
|
);
|
|
452
520
|
}
|
|
453
521
|
|
|
522
|
+
export interface LabelProps extends ComponentPropsWithoutRef<"label"> {
|
|
523
|
+
/** Draw the required mark after the text. The mark is `aria-hidden`: what a
|
|
524
|
+
* screen reader hears is the control's own `required` / `aria-required`, which
|
|
525
|
+
* the caller still sets — a "star" read out as the last word of every name is
|
|
526
|
+
* noise, and a control that is not marked required is not required however its
|
|
527
|
+
* label looks. */
|
|
528
|
+
required?: boolean;
|
|
529
|
+
/** `sm` for a toolbar or a dense filter row (12px), `md` otherwise (14px). */
|
|
530
|
+
size?: "sm" | "md";
|
|
531
|
+
/** Dim the label with its control. A label ABOVE its field is not the field's
|
|
532
|
+
* `peer`, so `peer-disabled:` cannot reach it the way it reaches a floating one. */
|
|
533
|
+
disabled?: boolean;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* The label ABOVE a field, for the places a floating label does not fit: a control
|
|
538
|
+
* the kit did not render (a third-party picker, a range slider, a group of radios),
|
|
539
|
+
* a filter bar whose fields are unlabelled {@link Select}s, a form that sets labels
|
|
540
|
+
* above its fields throughout.
|
|
541
|
+
*
|
|
542
|
+
* A new component rather than a mode of {@link FieldLabel}, because the two share
|
|
543
|
+
* nothing but the word. `FieldLabel` is an absolutely positioned `<span>` inside a
|
|
544
|
+
* trigger's `relative` box and has to stay exactly that for every dropdown that
|
|
545
|
+
* already lines up with it; this is a real `<label>` in normal flow, with `htmlFor`,
|
|
546
|
+
* that a click focuses the control through. Spacing below it belongs to the caller's
|
|
547
|
+
* layout (`space-y-1.5` on the pair, typically), as it would for any block.
|
|
548
|
+
*/
|
|
549
|
+
export function Label({ required, size = "md", disabled, className, children, ...rest }: LabelProps) {
|
|
550
|
+
return (
|
|
551
|
+
<label
|
|
552
|
+
{...rest}
|
|
553
|
+
className={cn(
|
|
554
|
+
"inline-flex items-center gap-0.5 font-medium leading-none text-[var(--text-primary)]",
|
|
555
|
+
size === "sm" ? "text-xs" : "text-sm",
|
|
556
|
+
disabled && "cursor-default opacity-50",
|
|
557
|
+
className,
|
|
558
|
+
)}
|
|
559
|
+
>
|
|
560
|
+
{children}
|
|
561
|
+
{required && (
|
|
562
|
+
<span aria-hidden className="text-[var(--danger)]">
|
|
563
|
+
*
|
|
564
|
+
</span>
|
|
565
|
+
)}
|
|
566
|
+
</label>
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
454
570
|
/**
|
|
455
571
|
* The message under a field that is wrong, and the wiring that attaches it.
|
|
456
572
|
*
|
|
@@ -482,6 +598,10 @@ function useFieldError(
|
|
|
482
598
|
error: ReactNode,
|
|
483
599
|
invalid: boolean | undefined,
|
|
484
600
|
describedBy: string | undefined,
|
|
601
|
+
/** The caller's own `aria-invalid`. A form library sets THIS (see `@eifi1/ui-kit/rhf`
|
|
602
|
+
* `FormControl`), not our `invalid` prop — and a field that announces invalid should
|
|
603
|
+
* look it too, as NumberField and the pickers already did. */
|
|
604
|
+
ariaInvalid?: unknown,
|
|
485
605
|
) {
|
|
486
606
|
const errorId = useId();
|
|
487
607
|
// `null`, `false` and `""` are what a caller's `touched && errors.iban` evaluates to
|
|
@@ -490,7 +610,7 @@ function useFieldError(
|
|
|
490
610
|
const hasError = error !== undefined && error !== null && error !== false && error !== "";
|
|
491
611
|
return {
|
|
492
612
|
/** A field carrying a message that says what is wrong with it IS wrong. */
|
|
493
|
-
isInvalid: Boolean(invalid) || hasError,
|
|
613
|
+
isInvalid: Boolean(invalid) || hasError || ariaInvalid === true || ariaInvalid === "true",
|
|
494
614
|
describedBy: hasError ? (describedBy ? `${describedBy} ${errorId}` : errorId) : describedBy,
|
|
495
615
|
errorEl: hasError ? (
|
|
496
616
|
<p id={errorId} className={FIELD_ERROR_CLASS}>
|
|
@@ -610,6 +730,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|
|
610
730
|
error,
|
|
611
731
|
invalid,
|
|
612
732
|
rest["aria-describedby"],
|
|
733
|
+
rest["aria-invalid"],
|
|
613
734
|
);
|
|
614
735
|
const asDisplay = useMediaQuery(PHONE_QUERY, false) && variant === "display";
|
|
615
736
|
// Password fields get a reveal toggle so users can check what they typed.
|
|
@@ -722,7 +843,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|
|
722
843
|
});
|
|
723
844
|
Input.displayName = "Input";
|
|
724
845
|
|
|
725
|
-
export interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
|
846
|
+
export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "size"> {
|
|
726
847
|
label?: ReactNode;
|
|
727
848
|
/** See {@link Input}'s `invalid`. */
|
|
728
849
|
invalid?: boolean;
|
|
@@ -730,25 +851,50 @@ export interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
|
|
730
851
|
error?: ReactNode;
|
|
731
852
|
/** A {@link FieldHint} for the label line — see {@link FloatingField}. */
|
|
732
853
|
hint?: ReactNode;
|
|
854
|
+
/**
|
|
855
|
+
* `"sm"`: a 28px, 12px-type select for a toolbar or a table header, where the
|
|
856
|
+
* 36px field stands a head taller than the buttons beside it. `"md"` (default)
|
|
857
|
+
* is the field every form uses. Unlabelled selects only — a floating label needs
|
|
858
|
+
* the tall box to float in, so a labelled Select ignores `"sm"`.
|
|
859
|
+
*
|
|
860
|
+
* A NUMBER is still the native attribute (the rows of a list box) and is passed
|
|
861
|
+
* straight through, so the one HTML meaning of `size` keeps working.
|
|
862
|
+
*/
|
|
863
|
+
size?: "sm" | "md" | number;
|
|
864
|
+
/** Classes for the `<select>` itself. `className` styles the WRAPPER — the box
|
|
865
|
+
* the chevron is positioned against — so it could set a width and nothing
|
|
866
|
+
* else; this is the way to the element. See {@link Input}'s `inputClassName`. */
|
|
867
|
+
selectClassName?: string;
|
|
733
868
|
}
|
|
734
869
|
|
|
870
|
+
// The compact select: the field's colours, a toolbar button's height. `py-0` and a
|
|
871
|
+
// fixed height rather than a smaller padding, so the box is 28px whatever line
|
|
872
|
+
// height the caller's type brings with it.
|
|
873
|
+
const SELECT_SM = "h-7 py-0 pl-2 pr-7 text-xs";
|
|
874
|
+
|
|
735
875
|
export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select(
|
|
736
|
-
{ className, label, id, children, invalid, error, hint, ...rest },
|
|
876
|
+
{ className, label, id, children, invalid, error, hint, size, selectClassName, ...rest },
|
|
737
877
|
ref,
|
|
738
878
|
) {
|
|
739
879
|
const generated = useId();
|
|
880
|
+
// Only a number reaches the DOM; the two words are this component's own.
|
|
881
|
+
const nativeSize = typeof size === "number" ? size : undefined;
|
|
882
|
+
const small = size === "sm" && label === undefined;
|
|
740
883
|
const fieldId = id ?? generated;
|
|
741
884
|
const { isInvalid, describedBy, errorEl } = useFieldError(
|
|
742
885
|
error,
|
|
743
886
|
invalid,
|
|
744
887
|
rest["aria-describedby"],
|
|
888
|
+
rest["aria-invalid"],
|
|
745
889
|
);
|
|
746
890
|
// Custom chevron (native arrow hidden via appearance-none) so it sits a touch
|
|
747
891
|
// in from the right border and matches both themes — feedback #223. A DISABLED
|
|
748
892
|
// select has no menu to drop, so it drops the chevron too: the arrow is the one
|
|
749
893
|
// thing on the control that promises a choice (Keksdose dev#474, where the
|
|
750
894
|
// account type became read-only and still looked exactly like a picker).
|
|
751
|
-
const chevron = rest.disabled ? null :
|
|
895
|
+
const chevron = rest.disabled ? null : (
|
|
896
|
+
<FieldChevron className={small ? "right-1.5 size-3.5" : undefined} />
|
|
897
|
+
);
|
|
752
898
|
if (label === undefined) {
|
|
753
899
|
return (
|
|
754
900
|
<FieldGroup errorEl={errorEl}>
|
|
@@ -762,6 +908,7 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select
|
|
|
762
908
|
// when it was reached. Input and Textarea both forward it here; this is
|
|
763
909
|
// the third one doing the same thing.
|
|
764
910
|
id={id}
|
|
911
|
+
size={nativeSize}
|
|
765
912
|
{...rest}
|
|
766
913
|
// OR-ed with the spread for the reason spelled out on Input's copy: this
|
|
767
914
|
// branch wrote `invalid || undefined`, so passing `aria-invalid` by hand
|
|
@@ -770,7 +917,13 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select
|
|
|
770
917
|
// attribute silently dropped. Input has never done that.
|
|
771
918
|
aria-invalid={isInvalid || rest["aria-invalid"] || undefined}
|
|
772
919
|
aria-describedby={describedBy}
|
|
773
|
-
className={cn(
|
|
920
|
+
className={cn(
|
|
921
|
+
FIELD_BASE,
|
|
922
|
+
"appearance-none pr-9",
|
|
923
|
+
small && SELECT_SM,
|
|
924
|
+
selectClassName,
|
|
925
|
+
isInvalid && FIELD_INVALID,
|
|
926
|
+
)}
|
|
774
927
|
>
|
|
775
928
|
{children}
|
|
776
929
|
</select>
|
|
@@ -785,6 +938,7 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select
|
|
|
785
938
|
<select
|
|
786
939
|
ref={ref}
|
|
787
940
|
id={fieldId}
|
|
941
|
+
size={nativeSize}
|
|
788
942
|
{...rest}
|
|
789
943
|
aria-invalid={isInvalid || rest["aria-invalid"] || undefined}
|
|
790
944
|
aria-describedby={describedBy}
|
|
@@ -792,6 +946,7 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select
|
|
|
792
946
|
FIELD_BASE,
|
|
793
947
|
FIELD_FLOATING_PAD,
|
|
794
948
|
"peer appearance-none pr-9",
|
|
949
|
+
selectClassName,
|
|
795
950
|
isInvalid && FIELD_INVALID,
|
|
796
951
|
)}
|
|
797
952
|
>
|
|
@@ -822,6 +977,7 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(function
|
|
|
822
977
|
error,
|
|
823
978
|
invalid,
|
|
824
979
|
rest["aria-describedby"],
|
|
980
|
+
rest["aria-invalid"],
|
|
825
981
|
);
|
|
826
982
|
if (label === undefined) {
|
|
827
983
|
return (
|
|
@@ -1038,6 +1194,49 @@ export function EmptyState({ title, hint, className, ...rest }: EmptyStateProps)
|
|
|
1038
1194
|
);
|
|
1039
1195
|
}
|
|
1040
1196
|
|
|
1197
|
+
/** One tab of a {@link Tabs} strip. */
|
|
1198
|
+
export interface TabItem<T extends string> {
|
|
1199
|
+
id: T;
|
|
1200
|
+
/** A ReactNode so a tab can pair an icon with text. */
|
|
1201
|
+
label: ReactNode;
|
|
1202
|
+
/** Optional trailing node (e.g. a count pill). */
|
|
1203
|
+
badge?: ReactNode;
|
|
1204
|
+
/** Marks a tab that IS a route — see {@link TabsProps}. */
|
|
1205
|
+
href?: string;
|
|
1206
|
+
/** Leading decoration: an icon, or the colour swatches of the lines this tab
|
|
1207
|
+
* draws in a chart above the strip. Hidden from assistive tech — it repeats
|
|
1208
|
+
* what the label says, or it says something the label must say instead. */
|
|
1209
|
+
icon?: ReactNode;
|
|
1210
|
+
/** A second, smaller line under the label — the one fact that tells two tabs
|
|
1211
|
+
* with similar labels apart ("80 km/h", "Level 3"). Part of the tab's name. */
|
|
1212
|
+
detail?: ReactNode;
|
|
1213
|
+
/** A tab that exists as a slot with nothing in it yet, drawn with a dashed
|
|
1214
|
+
* outline so a gap in a sequence looks like a gap. Visual only: say "empty" in
|
|
1215
|
+
* the label or `detail` if a screen reader needs to know. */
|
|
1216
|
+
empty?: boolean;
|
|
1217
|
+
/** `false` keeps this one tab when the strip has `onRemove` — the one sheet a
|
|
1218
|
+
* workbook cannot be without. Default: removable. */
|
|
1219
|
+
removable?: boolean;
|
|
1220
|
+
/** Plain-text name for the labels the strip composes about this tab ("Remove
|
|
1221
|
+
* …"), for when `label` is not a string. Default: `label` if it is a string,
|
|
1222
|
+
* else the id. */
|
|
1223
|
+
name?: string;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/** The strings the strip renders on its own behalf — only when it can add or
|
|
1227
|
+
* remove tabs. */
|
|
1228
|
+
export interface TabsLabels {
|
|
1229
|
+
/** The "add" button's visible text when the caller gives no `addLabel`. */
|
|
1230
|
+
add: string;
|
|
1231
|
+
/** Accessible name of a tab's × — "Remove <tab>". */
|
|
1232
|
+
remove: (tab: string) => string;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
export const DEFAULT_TABS_LABELS: TabsLabels = {
|
|
1236
|
+
add: "Add tab",
|
|
1237
|
+
remove: (tab) => `Remove ${tab}`,
|
|
1238
|
+
};
|
|
1239
|
+
|
|
1041
1240
|
/**
|
|
1042
1241
|
* The strip's own props sit on a `<div>`: the tablist IS the root element, so anything
|
|
1043
1242
|
* a caller hangs on it — a `data-tour` anchor for the kit's guided tour, a test id, an
|
|
@@ -1054,7 +1253,7 @@ export interface TabsProps<T extends string>
|
|
|
1054
1253
|
* feedback #451), while a plain click still goes through `onChange` and stays
|
|
1055
1254
|
* client-side. Tabs that only flip local state leave it unset — a link to a URL
|
|
1056
1255
|
* that does not select the tab would be worse than no link. */
|
|
1057
|
-
tabs:
|
|
1256
|
+
tabs: TabItem<T>[];
|
|
1058
1257
|
active: T;
|
|
1059
1258
|
onChange: (id: T) => void;
|
|
1060
1259
|
className?: string;
|
|
@@ -1120,6 +1319,34 @@ export interface TabsProps<T extends string>
|
|
|
1120
1319
|
* describes a tab as opening something that is not there.
|
|
1121
1320
|
*/
|
|
1122
1321
|
panelId?: string;
|
|
1322
|
+
/**
|
|
1323
|
+
* Makes the tabs removable: an × on the OPEN tab, and Delete on a focused tab.
|
|
1324
|
+
*
|
|
1325
|
+
* The × is on the open tab only — a row of tabs each carrying one is a row of
|
|
1326
|
+
* things to hit by accident, and the tab you can remove should be the one you
|
|
1327
|
+
* are looking at — but its room is reserved on every removable tab, so opening a
|
|
1328
|
+
* tab never widens it and never pushes the rest of the strip along (lenkbank
|
|
1329
|
+
* feedback #72). It is named "Remove <tab>" and is not a tab stop of its own: the
|
|
1330
|
+
* strip stays one stop, and the keyboard path is Delete (announced through
|
|
1331
|
+
* `aria-keyshortcuts`), which removes whichever tab has focus.
|
|
1332
|
+
*
|
|
1333
|
+
* The strip only ASKS: it calls this, and the tab goes when the caller drops it
|
|
1334
|
+
* from `tabs` — after a confirmation, a request, whatever it needs. When it does,
|
|
1335
|
+
* focus that was on the removed tab (or its ×) moves to the open tab if the
|
|
1336
|
+
* removed one was open, else to its neighbour, instead of falling to the page.
|
|
1337
|
+
*/
|
|
1338
|
+
onRemove?: (id: T) => void;
|
|
1339
|
+
/** Renders an "add" button after the last tab, outside the tablist — it is an
|
|
1340
|
+
* action, not a tab, and a tablist may own only tabs. */
|
|
1341
|
+
onAdd?: () => void;
|
|
1342
|
+
/** The add button's text. Say what is added — "Add loop", "Add report" says
|
|
1343
|
+
* more than "Add". Default: `tabs.add` from the {@link UiKitProvider}. */
|
|
1344
|
+
addLabel?: string;
|
|
1345
|
+
/** Overrides for this strip's strings. See {@link TabsLabels}. */
|
|
1346
|
+
labels?: Partial<TabsLabels>;
|
|
1347
|
+
/** An add or a remove is in flight: the × and the add button are disabled and
|
|
1348
|
+
* Delete is ignored, so a double click cannot remove two tabs. Nothing moves. */
|
|
1349
|
+
busy?: boolean;
|
|
1123
1350
|
}
|
|
1124
1351
|
|
|
1125
1352
|
// The two shapes are written out as whole strings rather than as one base plus a
|
|
@@ -1156,6 +1383,15 @@ const TAB_WRAP_ACTIVE_CLASSES =
|
|
|
1156
1383
|
const TAB_WRAP_INACTIVE_CLASSES =
|
|
1157
1384
|
"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)]";
|
|
1158
1385
|
|
|
1386
|
+
// The add/remove chrome. The × reserves its gutter on every removable tab (see
|
|
1387
|
+
// `onRemove`); `md:pe-8` is spelled out because the wrapped chip's `md:px-3` would
|
|
1388
|
+
// otherwise hand the gutter back from 768px up.
|
|
1389
|
+
const TAB_REMOVABLE_CLASSES = "pe-8 md:pe-8";
|
|
1390
|
+
const TAB_EMPTY_CLASSES = "-mx-1.5 rounded border border-dashed border-[var(--border-strong)] px-1.5";
|
|
1391
|
+
const TAB_ADD_CLASSES =
|
|
1392
|
+
"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";
|
|
1393
|
+
const TAB_ADD_WRAP_CLASSES = "px-2.5 text-xs md:px-3 md:text-sm";
|
|
1394
|
+
|
|
1159
1395
|
export function Tabs<T extends string>({
|
|
1160
1396
|
tabs,
|
|
1161
1397
|
active,
|
|
@@ -1164,9 +1400,53 @@ export function Tabs<T extends string>({
|
|
|
1164
1400
|
wrap = false,
|
|
1165
1401
|
label,
|
|
1166
1402
|
panelId,
|
|
1403
|
+
onRemove,
|
|
1404
|
+
onAdd,
|
|
1405
|
+
addLabel,
|
|
1406
|
+
labels,
|
|
1407
|
+
busy = false,
|
|
1167
1408
|
"aria-label": ariaLabel,
|
|
1168
1409
|
...rest
|
|
1169
1410
|
}: TabsProps<T>) {
|
|
1411
|
+
const text = useKitLabels("tabs", DEFAULT_TABS_LABELS, labels);
|
|
1412
|
+
const stripRef = useRef<HTMLDivElement>(null);
|
|
1413
|
+
const isRemovable = (tab: TabItem<T>) => onRemove !== undefined && tab.removable !== false;
|
|
1414
|
+
const nameOf = (tab: TabItem<T>) =>
|
|
1415
|
+
tab.name ?? (typeof tab.label === "string" ? tab.label : tab.id);
|
|
1416
|
+
|
|
1417
|
+
// Where focus goes once a removal the strip asked for has happened. The caller
|
|
1418
|
+
// owns `tabs` and may take its time (a confirmation, a request), so this waits
|
|
1419
|
+
// for the id to actually leave the list rather than guessing at the moment.
|
|
1420
|
+
const pendingFocus = useRef<{ removed: T; wasActive: boolean; neighbour?: T } | null>(null);
|
|
1421
|
+
const requestRemove = (id: T) => {
|
|
1422
|
+
const index = tabs.findIndex((t) => t.id === id);
|
|
1423
|
+
pendingFocus.current = {
|
|
1424
|
+
removed: id,
|
|
1425
|
+
wasActive: id === active,
|
|
1426
|
+
neighbour: (tabs[index + 1] ?? tabs[index - 1])?.id,
|
|
1427
|
+
};
|
|
1428
|
+
onRemove?.(id);
|
|
1429
|
+
};
|
|
1430
|
+
useLayoutEffect(() => {
|
|
1431
|
+
const pending = pendingFocus.current;
|
|
1432
|
+
if (!pending || tabs.some((t) => t.id === pending.removed)) return;
|
|
1433
|
+
pendingFocus.current = null;
|
|
1434
|
+
const strip = stripRef.current;
|
|
1435
|
+
if (!strip) return;
|
|
1436
|
+
// Only focus that fell out of the strip with the removed tab is ours to place.
|
|
1437
|
+
// If the user has moved on — into the panel, into a dialog — leave it there.
|
|
1438
|
+
const focused = document.activeElement;
|
|
1439
|
+
if (focused && focused !== document.body && !strip.contains(focused)) return;
|
|
1440
|
+
// The removed tab was open: focus follows the caller's new selection, which is
|
|
1441
|
+
// also the strip's tab stop. It was not: its neighbour, as the ARIA pattern has it.
|
|
1442
|
+
const target =
|
|
1443
|
+
!pending.wasActive && tabs.some((t) => t.id === pending.neighbour)
|
|
1444
|
+
? pending.neighbour
|
|
1445
|
+
: active;
|
|
1446
|
+
const index = tabs.findIndex((t) => t.id === target);
|
|
1447
|
+
strip.querySelectorAll<HTMLElement>('[role="tab"]')[index]?.focus();
|
|
1448
|
+
}, [tabs, active]);
|
|
1449
|
+
|
|
1170
1450
|
// Arrow keys walk the strip in DOM order (ARIA tabs pattern), which is what keeps
|
|
1171
1451
|
// a WRAPPED strip navigable: the rows flow in DOM order too, so Right off the end
|
|
1172
1452
|
// of row one lands on the first chip of row two rather than nowhere. Home/End jump
|
|
@@ -1180,7 +1460,15 @@ export function Tabs<T extends string>({
|
|
|
1180
1460
|
// the handler on the elements that are natively focusable and actually receive the
|
|
1181
1461
|
// keystroke. Those are the tabs, so the warning goes away by being right rather
|
|
1182
1462
|
// than by being satisfied.
|
|
1183
|
-
const onTabKeyDown = (e: KeyboardEvent<HTMLElement>) => {
|
|
1463
|
+
const onTabKeyDown = (e: KeyboardEvent<HTMLElement>, tab: TabItem<T>) => {
|
|
1464
|
+
// Delete removes the FOCUSED tab (the ARIA pattern's optional key for deletable
|
|
1465
|
+
// tabs) — the keyboard's way to the × that is not a tab stop. `busy` swallows it
|
|
1466
|
+
// rather than letting it fall through to the page.
|
|
1467
|
+
if (e.key === "Delete" && isRemovable(tab)) {
|
|
1468
|
+
e.preventDefault();
|
|
1469
|
+
if (!busy) requestRemove(tab.id);
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1184
1472
|
const step = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
|
|
1185
1473
|
if (step === 0 && e.key !== "Home" && e.key !== "End") return;
|
|
1186
1474
|
const strip = e.currentTarget.closest('[role="tablist"]');
|
|
@@ -1197,19 +1485,26 @@ export function Tabs<T extends string>({
|
|
|
1197
1485
|
: (from + step + items.length) % items.length;
|
|
1198
1486
|
items[to]?.focus();
|
|
1199
1487
|
};
|
|
1200
|
-
|
|
1488
|
+
const tablist = (
|
|
1201
1489
|
<div
|
|
1202
1490
|
// Before the role and the name: a caller's arbitrary attribute is welcome on the
|
|
1203
1491
|
// strip, but a `role` or an `aria-label` arriving through a spread props object
|
|
1204
1492
|
// must not be able to unmake the tablist the tabs below are registered against.
|
|
1205
1493
|
{...rest}
|
|
1494
|
+
ref={stripRef}
|
|
1206
1495
|
role="tablist"
|
|
1207
1496
|
// The DOM spelling wins over the deprecated `label`; see {@link TabsProps}.
|
|
1208
1497
|
aria-label={ariaLabel ?? label}
|
|
1209
|
-
|
|
1498
|
+
// With an add button the strip gains an outer box, and `className` goes there
|
|
1499
|
+
// — it is the box a caller's margin or width is meant for.
|
|
1500
|
+
className={cn(
|
|
1501
|
+
wrap ? TABLIST_WRAP_CLASSES : TABLIST_CLASSES,
|
|
1502
|
+
onAdd ? "min-w-0" : className,
|
|
1503
|
+
)}
|
|
1210
1504
|
>
|
|
1211
1505
|
{tabs.map((tab) => {
|
|
1212
1506
|
const isActive = tab.id === active;
|
|
1507
|
+
const removable = isRemovable(tab);
|
|
1213
1508
|
// Written once and worn by either tag below, so a routed tab and a
|
|
1214
1509
|
// state-only tab stay indistinguishable to the eye and to a screen reader.
|
|
1215
1510
|
const shared = {
|
|
@@ -1231,7 +1526,8 @@ export function Tabs<T extends string>({
|
|
|
1231
1526
|
// Only the open tab: see `panelId`.
|
|
1232
1527
|
"aria-controls": isActive ? panelId : undefined,
|
|
1233
1528
|
id: isActive && panelId ? `${panelId}-tab` : undefined,
|
|
1234
|
-
|
|
1529
|
+
"aria-keyshortcuts": removable ? "Delete" : undefined,
|
|
1530
|
+
onKeyDown: (e: KeyboardEvent<HTMLElement>) => onTabKeyDown(e, tab),
|
|
1235
1531
|
className: cn(
|
|
1236
1532
|
wrap ? TAB_WRAP_CLASSES : TAB_CLASSES,
|
|
1237
1533
|
wrap
|
|
@@ -1241,15 +1537,33 @@ export function Tabs<T extends string>({
|
|
|
1241
1537
|
: isActive
|
|
1242
1538
|
? TAB_ACTIVE_CLASSES
|
|
1243
1539
|
: TAB_INACTIVE_CLASSES,
|
|
1540
|
+
removable && TAB_REMOVABLE_CLASSES,
|
|
1244
1541
|
),
|
|
1245
1542
|
};
|
|
1246
1543
|
const inner = (
|
|
1247
|
-
<span className="inline-flex items-center gap-1.5">
|
|
1248
|
-
{tab.
|
|
1544
|
+
<span className={cn("inline-flex items-center gap-1.5", tab.empty && TAB_EMPTY_CLASSES)}>
|
|
1545
|
+
{tab.icon != null && (
|
|
1546
|
+
<span aria-hidden className="inline-flex shrink-0 items-center gap-0.5">
|
|
1547
|
+
{tab.icon}
|
|
1548
|
+
</span>
|
|
1549
|
+
)}
|
|
1550
|
+
{tab.detail != null ? (
|
|
1551
|
+
<span className="flex flex-col items-start text-start">
|
|
1552
|
+
<span>{tab.label}</span>
|
|
1553
|
+
{/* A separator for the NAME only — whitespace between flex items is
|
|
1554
|
+
not drawn, and without it the two lines ran together into one
|
|
1555
|
+
word for a screen reader ("80 km/hloop 2"). */}{" "}
|
|
1556
|
+
<span className="text-[11px] font-normal leading-tight text-[var(--text-muted)]">
|
|
1557
|
+
{tab.detail}
|
|
1558
|
+
</span>
|
|
1559
|
+
</span>
|
|
1560
|
+
) : (
|
|
1561
|
+
tab.label
|
|
1562
|
+
)}
|
|
1249
1563
|
{tab.badge != null ? tab.badge : null}
|
|
1250
1564
|
</span>
|
|
1251
1565
|
);
|
|
1252
|
-
|
|
1566
|
+
const element = tab.href ? (
|
|
1253
1567
|
<a
|
|
1254
1568
|
key={tab.id}
|
|
1255
1569
|
{...shared}
|
|
@@ -1276,7 +1590,7 @@ export function Tabs<T extends string>({
|
|
|
1276
1590
|
// prop overrides the one in `shared`, and an anchor tab that lost the
|
|
1277
1591
|
// arrows would be a strip that is navigable only where it is not a link.
|
|
1278
1592
|
onKeyDown={(e) => {
|
|
1279
|
-
onTabKeyDown(e);
|
|
1593
|
+
onTabKeyDown(e, tab);
|
|
1280
1594
|
if (e.defaultPrevented || e.key !== " ") return;
|
|
1281
1595
|
e.preventDefault();
|
|
1282
1596
|
onChange(tab.id);
|
|
@@ -1289,7 +1603,69 @@ export function Tabs<T extends string>({
|
|
|
1289
1603
|
{inner}
|
|
1290
1604
|
</button>
|
|
1291
1605
|
);
|
|
1606
|
+
if (!removable) return element;
|
|
1607
|
+
return (
|
|
1608
|
+
// The × cannot go INSIDE the tab: a button in a button is invalid HTML, and
|
|
1609
|
+
// a tab's children are presentational, so a reader would flatten it into the
|
|
1610
|
+
// tab's name. It sits beside the tab, over the gutter the tab reserves.
|
|
1611
|
+
<div key={tab.id} className="relative flex shrink-0">
|
|
1612
|
+
{element}
|
|
1613
|
+
{isActive && (
|
|
1614
|
+
<IconButton
|
|
1615
|
+
size="2xs"
|
|
1616
|
+
tone="danger"
|
|
1617
|
+
// Not a tab stop: the strip is one, and Delete is the keyboard's path.
|
|
1618
|
+
// Still a named button, so a screen reader's cursor and a touch
|
|
1619
|
+
// reader's swipe both reach it.
|
|
1620
|
+
tabIndex={-1}
|
|
1621
|
+
type="button"
|
|
1622
|
+
aria-label={text.remove(nameOf(tab))}
|
|
1623
|
+
disabled={busy}
|
|
1624
|
+
onClick={() => requestRemove(tab.id)}
|
|
1625
|
+
className={cn(
|
|
1626
|
+
"absolute inset-y-0 end-1 my-auto",
|
|
1627
|
+
// On a wrapped strip's filled chip the quiet grey would vanish into
|
|
1628
|
+
// the brand fill below `md`.
|
|
1629
|
+
wrap && "text-[var(--brand-contrast)] md:text-[var(--text-placeholder)]",
|
|
1630
|
+
)}
|
|
1631
|
+
>
|
|
1632
|
+
<X />
|
|
1633
|
+
</IconButton>
|
|
1634
|
+
)}
|
|
1635
|
+
</div>
|
|
1636
|
+
);
|
|
1292
1637
|
})}
|
|
1293
1638
|
</div>
|
|
1294
1639
|
);
|
|
1640
|
+
if (!onAdd) return tablist;
|
|
1641
|
+
return (
|
|
1642
|
+
<div
|
|
1643
|
+
className={cn(
|
|
1644
|
+
wrap ? "flex flex-wrap items-end gap-1.5 md:flex-nowrap md:gap-0" : "flex items-end",
|
|
1645
|
+
className,
|
|
1646
|
+
)}
|
|
1647
|
+
>
|
|
1648
|
+
{tablist}
|
|
1649
|
+
{/* Outside the tablist: an action, not a tab — and a tablist may own only
|
|
1650
|
+
tabs. The rule under the strip carries on beneath it. */}
|
|
1651
|
+
<div
|
|
1652
|
+
className={cn(
|
|
1653
|
+
"flex flex-1 items-center",
|
|
1654
|
+
wrap
|
|
1655
|
+
? "md:self-stretch md:border-b md:border-[var(--border)]"
|
|
1656
|
+
: "self-stretch border-b border-[var(--border)]",
|
|
1657
|
+
)}
|
|
1658
|
+
>
|
|
1659
|
+
<button
|
|
1660
|
+
type="button"
|
|
1661
|
+
onClick={onAdd}
|
|
1662
|
+
disabled={busy}
|
|
1663
|
+
className={cn(TAB_ADD_CLASSES, wrap && TAB_ADD_WRAP_CLASSES)}
|
|
1664
|
+
>
|
|
1665
|
+
<Plus aria-hidden />
|
|
1666
|
+
{addLabel ?? text.add}
|
|
1667
|
+
</button>
|
|
1668
|
+
</div>
|
|
1669
|
+
</div>
|
|
1670
|
+
);
|
|
1295
1671
|
}
|
package/src/i18n/defaults.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { DEFAULT_PAGE_CONTENTS_LABELS } from "../components/page-contents";
|
|
|
5
5
|
import { DEFAULT_POPOVER_LABELS } from "../components/popover";
|
|
6
6
|
import { DEFAULT_CHIP_INPUT_LABELS } from "../components/chip";
|
|
7
7
|
import { DEFAULT_FIELD_SYNC_LABELS } from "../components/field-sync";
|
|
8
|
-
import { DEFAULT_PASSWORD_REVEAL_LABELS } from "../components/ui";
|
|
8
|
+
import { DEFAULT_PASSWORD_REVEAL_LABELS, DEFAULT_TABS_LABELS } from "../components/ui";
|
|
9
9
|
import { DEFAULT_WIZARD_LABELS } from "../wizard/types";
|
|
10
10
|
import { DEFAULT_TOUR_LABELS } from "../tour/tour";
|
|
11
11
|
import { DEFAULT_COMMAND_PALETTE_LABELS } from "../search/command-palette";
|
|
@@ -14,6 +14,11 @@ import { DEFAULT_SPARKLINE_LABELS } from "../components/sparkline";
|
|
|
14
14
|
import { DEFAULT_STAT_TILE_LABELS } from "../components/stat-tile";
|
|
15
15
|
import { DEFAULT_SIGNATURE_PAD_LABELS } from "../components/signature-pad";
|
|
16
16
|
import { DEFAULT_PASSWORD_STRENGTH_LABELS } from "../components/password-strength";
|
|
17
|
+
import { DEFAULT_DANGER_CONFIRM_LABELS } from "../components/danger-confirm";
|
|
18
|
+
import { DEFAULT_SWATCH_PICKER_LABELS } from "../components/swatch-picker";
|
|
19
|
+
import { DEFAULT_ICON_PICKER_LABELS } from "../components/icon-picker";
|
|
20
|
+
import { DEFAULT_DIALOG_FRAME_LABELS } from "../components/dialog-frame";
|
|
21
|
+
import { DEFAULT_FILE_PICKER_LABELS } from "../components/file-button";
|
|
17
22
|
import {
|
|
18
23
|
DEFAULT_APP_SHELL_LABELS,
|
|
19
24
|
DEFAULT_CALCULATOR_LABELS,
|
|
@@ -51,6 +56,7 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
|
|
|
51
56
|
chipInput: DEFAULT_CHIP_INPUT_LABELS,
|
|
52
57
|
fieldSync: DEFAULT_FIELD_SYNC_LABELS,
|
|
53
58
|
passwordReveal: DEFAULT_PASSWORD_REVEAL_LABELS,
|
|
59
|
+
tabs: DEFAULT_TABS_LABELS,
|
|
54
60
|
appShell: DEFAULT_APP_SHELL_LABELS,
|
|
55
61
|
pageContents: DEFAULT_PAGE_CONTENTS_LABELS,
|
|
56
62
|
topBar: DEFAULT_TOP_BAR_LABELS,
|
|
@@ -65,4 +71,9 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
|
|
|
65
71
|
statTile: DEFAULT_STAT_TILE_LABELS,
|
|
66
72
|
signaturePad: DEFAULT_SIGNATURE_PAD_LABELS,
|
|
67
73
|
passwordStrength: DEFAULT_PASSWORD_STRENGTH_LABELS,
|
|
74
|
+
dangerConfirm: DEFAULT_DANGER_CONFIRM_LABELS,
|
|
75
|
+
swatchPicker: DEFAULT_SWATCH_PICKER_LABELS,
|
|
76
|
+
iconPicker: DEFAULT_ICON_PICKER_LABELS,
|
|
77
|
+
dialogFrame: DEFAULT_DIALOG_FRAME_LABELS,
|
|
78
|
+
filePicker: DEFAULT_FILE_PICKER_LABELS,
|
|
68
79
|
};
|