@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.
Files changed (128) hide show
  1. package/README.md +50 -1
  2. package/dist/components/amount-input.d.ts +7 -0
  3. package/dist/components/autocomplete.d.ts +101 -0
  4. package/dist/components/autocomplete.js +260 -0
  5. package/dist/components/autocomplete.js.map +1 -0
  6. package/dist/components/calculator.d.ts +7 -0
  7. package/dist/components/chip.d.ts +3 -2
  8. package/dist/components/chip.js +14 -1
  9. package/dist/components/chip.js.map +1 -1
  10. package/dist/components/choice-card.d.ts +100 -0
  11. package/dist/components/choice-card.js +170 -0
  12. package/dist/components/choice-card.js.map +1 -0
  13. package/dist/components/combobox-core.d.ts +76 -6
  14. package/dist/components/combobox-core.js +119 -49
  15. package/dist/components/combobox-core.js.map +1 -1
  16. package/dist/components/combobox.d.ts +12 -2
  17. package/dist/components/combobox.js +42 -17
  18. package/dist/components/combobox.js.map +1 -1
  19. package/dist/components/danger-confirm.d.ts +91 -0
  20. package/dist/components/danger-confirm.js +181 -0
  21. package/dist/components/danger-confirm.js.map +1 -0
  22. package/dist/components/dialog-frame.d.ts +84 -0
  23. package/dist/components/dialog-frame.js +86 -0
  24. package/dist/components/dialog-frame.js.map +1 -0
  25. package/dist/components/disclosure.d.ts +108 -0
  26. package/dist/components/disclosure.js +127 -0
  27. package/dist/components/disclosure.js.map +1 -0
  28. package/dist/components/entity-combobox.d.ts +17 -3
  29. package/dist/components/entity-combobox.js +25 -5
  30. package/dist/components/entity-combobox.js.map +1 -1
  31. package/dist/components/file-button.d.ts +161 -0
  32. package/dist/components/file-button.js +225 -0
  33. package/dist/components/file-button.js.map +1 -0
  34. package/dist/components/file-dropzone.d.ts +72 -23
  35. package/dist/components/file-dropzone.js +219 -94
  36. package/dist/components/file-dropzone.js.map +1 -1
  37. package/dist/components/icon-picker.d.ts +72 -0
  38. package/dist/components/icon-picker.js +104 -0
  39. package/dist/components/icon-picker.js.map +1 -0
  40. package/dist/components/mini-calendar.d.ts +3 -0
  41. package/dist/components/mini-calendar.js +4 -3
  42. package/dist/components/mini-calendar.js.map +1 -1
  43. package/dist/components/modal.d.ts +8 -1
  44. package/dist/components/modal.js +4 -2
  45. package/dist/components/modal.js.map +1 -1
  46. package/dist/components/multi-entity-combobox.d.ts +16 -3
  47. package/dist/components/multi-entity-combobox.js +25 -5
  48. package/dist/components/multi-entity-combobox.js.map +1 -1
  49. package/dist/components/number-field.d.ts +41 -1
  50. package/dist/components/number-field.js +42 -10
  51. package/dist/components/number-field.js.map +1 -1
  52. package/dist/components/number-input.d.ts +35 -2
  53. package/dist/components/number-input.js +35 -4
  54. package/dist/components/number-input.js.map +1 -1
  55. package/dist/components/numpad-sheet.d.ts +7 -0
  56. package/dist/components/search-field.d.ts +16 -0
  57. package/dist/components/search-field.js +29 -7
  58. package/dist/components/search-field.js.map +1 -1
  59. package/dist/components/signature-pad.d.ts +43 -1
  60. package/dist/components/signature-pad.js +74 -2
  61. package/dist/components/signature-pad.js.map +1 -1
  62. package/dist/components/swatch-picker.d.ts +69 -0
  63. package/dist/components/swatch-picker.js +75 -0
  64. package/dist/components/swatch-picker.js.map +1 -0
  65. package/dist/components/tile-radio.d.ts +50 -0
  66. package/dist/components/tile-radio.js +140 -0
  67. package/dist/components/tile-radio.js.map +1 -0
  68. package/dist/components/toggle-group.d.ts +27 -5
  69. package/dist/components/toggle-group.js +22 -15
  70. package/dist/components/toggle-group.js.map +1 -1
  71. package/dist/components/ui.d.ts +150 -12
  72. package/dist/components/ui.js +196 -22
  73. package/dist/components/ui.js.map +1 -1
  74. package/dist/i18n/defaults.d.ts +7 -0
  75. package/dist/i18n/defaults.js +13 -2
  76. package/dist/i18n/defaults.js.map +1 -1
  77. package/dist/i18n/kit-labels.d.ts +38 -5
  78. package/dist/i18n/kit-labels.js +12 -4
  79. package/dist/i18n/kit-labels.js.map +1 -1
  80. package/dist/index.d.ts +16 -7
  81. package/dist/index.js +12 -0
  82. package/dist/index.js.map +1 -1
  83. package/dist/lib/table-text.d.ts +127 -0
  84. package/dist/lib/table-text.js +82 -0
  85. package/dist/lib/table-text.js.map +1 -0
  86. package/dist/rhf/form.d.ts +79 -0
  87. package/dist/rhf/form.js +143 -0
  88. package/dist/rhf/form.js.map +1 -0
  89. package/dist/rhf.d.ts +4 -0
  90. package/dist/rhf.js +3 -0
  91. package/dist/rhf.js.map +1 -0
  92. package/dist/shell/app-shell.js +3 -1
  93. package/dist/shell/app-shell.js.map +1 -1
  94. package/dist/table-text.d.ts +1 -0
  95. package/dist/table-text.js +3 -0
  96. package/dist/table-text.js.map +1 -0
  97. package/package.json +14 -1
  98. package/src/components/autocomplete.tsx +425 -0
  99. package/src/components/chip.tsx +24 -2
  100. package/src/components/choice-card.tsx +305 -0
  101. package/src/components/combobox-core.tsx +228 -58
  102. package/src/components/combobox.tsx +58 -21
  103. package/src/components/danger-confirm.tsx +286 -0
  104. package/src/components/dialog-frame.tsx +179 -0
  105. package/src/components/disclosure.tsx +259 -0
  106. package/src/components/entity-combobox.tsx +41 -6
  107. package/src/components/file-button.tsx +431 -0
  108. package/src/components/file-dropzone.tsx +323 -117
  109. package/src/components/icon-picker.tsx +181 -0
  110. package/src/components/mini-calendar.tsx +7 -3
  111. package/src/components/modal.tsx +10 -2
  112. package/src/components/multi-entity-combobox.tsx +40 -6
  113. package/src/components/number-field.tsx +86 -10
  114. package/src/components/number-input.tsx +79 -2
  115. package/src/components/search-field.tsx +49 -6
  116. package/src/components/signature-pad.tsx +112 -0
  117. package/src/components/swatch-picker.tsx +141 -0
  118. package/src/components/tile-radio.tsx +228 -0
  119. package/src/components/toggle-group.tsx +54 -18
  120. package/src/components/ui.tsx +400 -24
  121. package/src/i18n/defaults.ts +12 -1
  122. package/src/i18n/kit-labels.tsx +45 -5
  123. package/src/index.ts +19 -0
  124. package/src/lib/table-text.ts +265 -0
  125. package/src/rhf/form.tsx +300 -0
  126. package/src/rhf.ts +9 -0
  127. package/src/shell/app-shell.tsx +3 -1
  128. package/src/table-text.ts +8 -0
@@ -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 [&_svg]:size-5";
38
- const IconButton = forwardRef(function IconButton2({ variant = "ghost", size = "md", className, ...rest }, ref) {
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 === "sm" ? "size-8" : "size-9",
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 Select = forwardRef(function Select2({ className, label, id, children, invalid, error, hint, ...rest }, ref) {
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(FIELD_BASE, "appearance-none pr-9", isInvalid && FIELD_INVALID),
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(
@@ -485,6 +538,10 @@ function EmptyState({ title, hint, className, ...rest }) {
485
538
  }
486
539
  );
487
540
  }
541
+ const DEFAULT_TABS_LABELS = {
542
+ add: "Add tab",
543
+ remove: (tab) => `Remove ${tab}`
544
+ };
488
545
  const TABLIST_CLASSES = "flex gap-1 overflow-x-auto overflow-y-hidden border-b border-[var(--border)]";
489
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)]";
490
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)]";
@@ -493,6 +550,10 @@ const TAB_INACTIVE_CLASSES = "border-transparent text-[var(--text-muted)] hover:
493
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";
494
551
  const TAB_WRAP_ACTIVE_CLASSES = "bg-[var(--brand)] text-[var(--brand-contrast)] md:border-[var(--text-primary)] md:text-[var(--text-primary)]";
495
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";
496
557
  function Tabs({
497
558
  tabs,
498
559
  active,
@@ -501,10 +562,46 @@ function Tabs({
501
562
  wrap = false,
502
563
  label,
503
564
  panelId,
565
+ onRemove,
566
+ onAdd,
567
+ addLabel,
568
+ labels,
569
+ busy = false,
504
570
  "aria-label": ariaLabel,
505
571
  ...rest
506
572
  }) {
507
- const onTabKeyDown = (e) => {
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
+ }
508
605
  const step = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
509
606
  if (step === 0 && e.key !== "Home" && e.key !== "End") return;
510
607
  const strip = e.currentTarget.closest('[role="tablist"]');
@@ -516,15 +613,20 @@ function Tabs({
516
613
  const to = e.key === "Home" ? 0 : e.key === "End" ? items.length - 1 : (from + step + items.length) % items.length;
517
614
  items[to]?.focus();
518
615
  };
519
- return /* @__PURE__ */ jsx(
616
+ const tablist = /* @__PURE__ */ jsx(
520
617
  "div",
521
618
  {
522
619
  ...rest,
620
+ ref: stripRef,
523
621
  role: "tablist",
524
622
  "aria-label": ariaLabel ?? label,
525
- className: cn(wrap ? TABLIST_WRAP_CLASSES : TABLIST_CLASSES, className),
623
+ className: cn(
624
+ wrap ? TABLIST_WRAP_CLASSES : TABLIST_CLASSES,
625
+ onAdd ? "min-w-0" : className
626
+ ),
526
627
  children: tabs.map((tab) => {
527
628
  const isActive = tab.id === active;
629
+ const removable = isRemovable(tab);
528
630
  const shared = {
529
631
  role: "tab",
530
632
  "aria-selected": isActive,
@@ -544,17 +646,24 @@ function Tabs({
544
646
  // Only the open tab: see `panelId`.
545
647
  "aria-controls": isActive ? panelId : void 0,
546
648
  id: isActive && panelId ? `${panelId}-tab` : void 0,
547
- onKeyDown: onTabKeyDown,
649
+ "aria-keyshortcuts": removable ? "Delete" : void 0,
650
+ onKeyDown: (e) => onTabKeyDown(e, tab),
548
651
  className: cn(
549
652
  wrap ? TAB_WRAP_CLASSES : TAB_CLASSES,
550
- 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
551
655
  )
552
656
  };
553
- const inner = /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5", children: [
554
- tab.label,
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,
555
664
  tab.badge != null ? tab.badge : null
556
665
  ] });
557
- return tab.href ? /* @__PURE__ */ jsx(
666
+ const element = tab.href ? /* @__PURE__ */ jsx(
558
667
  "a",
559
668
  {
560
669
  ...shared,
@@ -566,7 +675,7 @@ function Tabs({
566
675
  onChange(tab.id);
567
676
  },
568
677
  onKeyDown: (e) => {
569
- onTabKeyDown(e);
678
+ onTabKeyDown(e, tab);
570
679
  if (e.defaultPrevented || e.key !== " ") return;
571
680
  e.preventDefault();
572
681
  onChange(tab.id);
@@ -575,9 +684,72 @@ function Tabs({
575
684
  },
576
685
  tab.id
577
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
+ );
578
715
  })
579
716
  }
580
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
+ );
581
753
  }
582
754
  export {
583
755
  Button,
@@ -589,6 +761,7 @@ export {
589
761
  CardHeader,
590
762
  CardTitle,
591
763
  DEFAULT_PASSWORD_REVEAL_LABELS,
764
+ DEFAULT_TABS_LABELS,
592
765
  EmptyState,
593
766
  FIELD_BASE,
594
767
  FIELD_DISPLAY,
@@ -605,6 +778,7 @@ export {
605
778
  FloatingField,
606
779
  IconButton,
607
780
  Input,
781
+ Label,
608
782
  PHONE_QUERY,
609
783
  Select,
610
784
  Spinner,