@estiva-app/ui 0.23.0 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/dist/AppShell.d.ts.map +1 -1
  2. package/dist/AttachmentCard.d.ts.map +1 -1
  3. package/dist/Form.d.ts.map +1 -1
  4. package/dist/IconButton.d.ts +9 -1
  5. package/dist/IconButton.d.ts.map +1 -1
  6. package/dist/ListColumn.d.ts +41 -0
  7. package/dist/ListColumn.d.ts.map +1 -0
  8. package/dist/PreviewCard.d.ts +9 -2
  9. package/dist/PreviewCard.d.ts.map +1 -1
  10. package/dist/SectionHeader.d.ts +7 -1
  11. package/dist/SectionHeader.d.ts.map +1 -1
  12. package/dist/Select.d.ts +12 -3
  13. package/dist/Select.d.ts.map +1 -1
  14. package/dist/Toolbar.d.ts +18 -0
  15. package/dist/Toolbar.d.ts.map +1 -1
  16. package/dist/Tooltip.d.ts +13 -1
  17. package/dist/Tooltip.d.ts.map +1 -1
  18. package/dist/eslint/index.js +2 -1
  19. package/dist/eslint/index.js.map +2 -2
  20. package/dist/eslint/no-restyled-part.d.ts.map +1 -1
  21. package/dist/index.d.ts +2 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +189 -143
  24. package/dist/index.js.map +4 -4
  25. package/package.json +1 -1
  26. package/registry.json +214 -11
  27. package/src/AppShell.mdx +18 -1
  28. package/src/AppShell.test.tsx +16 -0
  29. package/src/AppShell.tsx +4 -1
  30. package/src/AttachmentCard.mdx +12 -1
  31. package/src/AttachmentCard.test.tsx +10 -0
  32. package/src/AttachmentCard.tsx +8 -5
  33. package/src/Avatar.mdx +20 -5
  34. package/src/AvatarGroup.mdx +4 -0
  35. package/src/Banner.mdx +17 -8
  36. package/src/Breadcrumb.mdx +12 -0
  37. package/src/Button.mdx +17 -5
  38. package/src/Button.tsx +2 -2
  39. package/src/Card.mdx +14 -1
  40. package/src/Checkbox.mdx +11 -0
  41. package/src/Chip.mdx +4 -0
  42. package/src/ChipInput.mdx +13 -1
  43. package/src/CollapsibleSection.mdx +12 -3
  44. package/src/CommandPalette.mdx +14 -2
  45. package/src/ConfirmDialog.mdx +13 -2
  46. package/src/ContainerHeader.mdx +15 -2
  47. package/src/DialogShell.mdx +15 -1
  48. package/src/Divider.mdx +12 -1
  49. package/src/EditableText.mdx +17 -1
  50. package/src/EmptyState.mdx +17 -6
  51. package/src/Field.mdx +13 -5
  52. package/src/FieldLine.mdx +9 -1
  53. package/src/FilePicker.mdx +8 -0
  54. package/src/Form.mdx +11 -1
  55. package/src/Form.test.tsx +26 -0
  56. package/src/Form.tsx +7 -0
  57. package/src/IconButton.mdx +17 -3
  58. package/src/IconButton.stories.tsx +3 -0
  59. package/src/IconButton.test.tsx +25 -0
  60. package/src/IconButton.tsx +61 -46
  61. package/src/IdentityMenu.mdx +10 -1
  62. package/src/InlineChip.mdx +11 -0
  63. package/src/Kbd.mdx +6 -2
  64. package/src/Link.mdx +13 -1
  65. package/src/ListColumn.mdx +87 -0
  66. package/src/ListColumn.stories.tsx +136 -0
  67. package/src/ListColumn.test.tsx +50 -0
  68. package/src/ListColumn.tsx +63 -0
  69. package/src/Menu.mdx +18 -1
  70. package/src/MenuItem.mdx +15 -2
  71. package/src/NavItem.mdx +12 -0
  72. package/src/Person.mdx +7 -0
  73. package/src/PersonTrigger.mdx +11 -1
  74. package/src/PersonTrigger.tsx +1 -1
  75. package/src/Popover.mdx +23 -0
  76. package/src/PreviewCard.mdx +19 -4
  77. package/src/PreviewCard.tsx +11 -4
  78. package/src/ProgressBar.mdx +8 -0
  79. package/src/Property.mdx +4 -0
  80. package/src/Rail.mdx +12 -1
  81. package/src/RailItem.mdx +12 -0
  82. package/src/RailItem.tsx +1 -1
  83. package/src/Reaction.mdx +10 -0
  84. package/src/Reaction.tsx +1 -1
  85. package/src/ReactionPicker.mdx +8 -0
  86. package/src/ScrollArea.mdx +16 -2
  87. package/src/SearchInput.mdx +9 -0
  88. package/src/SectionHeader.mdx +18 -1
  89. package/src/SectionHeader.stories.tsx +9 -0
  90. package/src/SectionHeader.test.tsx +9 -0
  91. package/src/SectionHeader.tsx +9 -3
  92. package/src/SectionLabel.mdx +10 -2
  93. package/src/Select.mdx +24 -4
  94. package/src/Select.stories.tsx +4 -1
  95. package/src/Select.test.tsx +29 -0
  96. package/src/Select.tsx +36 -14
  97. package/src/Sidebar.mdx +8 -0
  98. package/src/Skeleton.mdx +8 -0
  99. package/src/Skeleton.tsx +1 -1
  100. package/src/Tabs.mdx +12 -1
  101. package/src/TextInput.mdx +9 -0
  102. package/src/TextInput.tsx +1 -1
  103. package/src/Textarea.mdx +8 -0
  104. package/src/Toast.mdx +12 -1
  105. package/src/Toolbar.mdx +20 -2
  106. package/src/Toolbar.stories.tsx +19 -2
  107. package/src/Toolbar.test.tsx +24 -1
  108. package/src/Toolbar.tsx +22 -0
  109. package/src/Tooltip.mdx +27 -2
  110. package/src/Tooltip.stories.tsx +26 -0
  111. package/src/Tooltip.test.tsx +51 -0
  112. package/src/Tooltip.tsx +17 -4
  113. package/src/TopBar.mdx +11 -0
  114. package/src/eslint/no-rebuilt-behaviour.ts +1 -1
  115. package/src/eslint/no-restyled-part.ts +1 -0
  116. package/src/heights.test.tsx +78 -0
  117. package/src/index.ts +2 -1
  118. package/src/pages.test.ts +142 -0
  119. package/src/registry/registry.test.ts +16 -8
  120. package/tailwind-preset.js +22 -1
package/dist/index.js CHANGED
@@ -91,25 +91,30 @@ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
91
91
  function AppShell({ variant = "solid", menu, logo, search, identity, banner, nav, children }) {
92
92
  const bar = /* @__PURE__ */ jsx3(TopBar, { variant, menu, logo, search, right: identity });
93
93
  if (variant === "floating") {
94
- return /* @__PURE__ */ jsxs3("div", { className: "relative h-full min-h-0 overflow-hidden bg-bg-base", children: [
95
- bar,
96
- /* @__PURE__ */ jsxs3("div", { className: "flex h-full pb-4 pr-4 pt-[52px]", children: [
97
- nav,
98
- /* @__PURE__ */ jsxs3(
99
- "div",
100
- {
101
- className: cn(
102
- "flex min-w-0 flex-1 flex-col overflow-hidden rounded-2xl bg-bg-surface",
103
- "signal:border signal:border-border-subtle signal:shadow-highlight-inset"
104
- ),
105
- children: [
106
- banner,
107
- /* @__PURE__ */ jsx3("main", { className: "flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden", children })
108
- ]
109
- }
110
- )
94
+ return (
95
+ // `signal-canvas`: in the Signal theme, the preset draws the control-room dot
96
+ // grid behind everything in the frame (Peek's, moved here with the frame:
97
+ // UIG-14, Katerina, 19 September — the canvas is the theme's, not an app's).
98
+ /* @__PURE__ */ jsxs3("div", { className: "signal-canvas relative h-full min-h-0 overflow-hidden bg-bg-base", children: [
99
+ bar,
100
+ /* @__PURE__ */ jsxs3("div", { className: "flex h-full pb-4 pr-4 pt-[52px]", children: [
101
+ nav,
102
+ /* @__PURE__ */ jsxs3(
103
+ "div",
104
+ {
105
+ className: cn(
106
+ "flex min-w-0 flex-1 flex-col overflow-hidden rounded-2xl bg-bg-surface",
107
+ "signal:border signal:border-border-subtle signal:shadow-highlight-inset"
108
+ ),
109
+ children: [
110
+ banner,
111
+ /* @__PURE__ */ jsx3("main", { className: "flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden", children })
112
+ ]
113
+ }
114
+ )
115
+ ] })
111
116
  ] })
112
- ] });
117
+ );
113
118
  }
114
119
  return (
115
120
  /* `relative overflow-hidden` is the seal the floating manner already has
@@ -283,8 +288,8 @@ function TooltipProvider({ delay = OPEN_DELAY, closeDelay, timeout, children })
283
288
  return /* @__PURE__ */ jsx7(BaseTooltip.Provider, { delay, closeDelay, timeout, children });
284
289
  }
285
290
  function Tooltip({ label, shortcut, className, ...props }) {
286
- return /* @__PURE__ */ jsxs4("div", { role: "tooltip", className: cn("bg-bg-elevated border border-border-default rounded-lg h-[30px] flex items-center justify-center gap-1.5 px-2 shadow-lg", className), ...props, children: [
287
- /* @__PURE__ */ jsx7("span", { className: "text-caption text-text-primary whitespace-nowrap", children: label }),
291
+ return /* @__PURE__ */ jsxs4("div", { role: "tooltip", className: cn("bg-bg-elevated border border-border-default rounded-lg min-h-[30px] max-w-80 flex items-center justify-center gap-1.5 px-2 py-1 shadow-lg", className), ...props, children: [
292
+ /* @__PURE__ */ jsx7("span", { className: "min-w-0 text-caption text-text-primary break-words", children: label }),
288
293
  shortcut && /* @__PURE__ */ jsx7(Kbd, { children: shortcut })
289
294
  ] });
290
295
  }
@@ -316,9 +321,10 @@ function TooltipSurface({ label, shortcut, placement }) {
316
321
  }
317
322
  ) });
318
323
  }
319
- function WithTooltip({ label, shortcut, placement = "top", wrapperClassName, children }) {
324
+ function WithTooltip({ label, shortcut, placement = "top", wrapperClassName, inline = false, children }) {
325
+ const Wrapper = inline ? "span" : "div";
320
326
  return /* @__PURE__ */ jsxs4(BaseTooltip.Root, { disableHoverablePopup: true, children: [
321
- /* @__PURE__ */ jsx7(BaseTooltip.Trigger, { delay: OPEN_DELAY, render: /* @__PURE__ */ jsx7("div", { className: cn("inline-flex shrink-0", wrapperClassName) }), children }),
327
+ /* @__PURE__ */ jsx7(BaseTooltip.Trigger, { delay: OPEN_DELAY, render: /* @__PURE__ */ jsx7(Wrapper, { className: cn("inline-flex shrink-0", inline && "align-top", wrapperClassName) }), children }),
322
328
  /* @__PURE__ */ jsx7(TooltipSurface, { label, shortcut, placement })
323
329
  ] });
324
330
  }
@@ -343,48 +349,51 @@ function IconButton({
343
349
  tooltipShortcut,
344
350
  tooltipPlacement,
345
351
  type = "button",
352
+ href,
346
353
  ...props
347
354
  }) {
348
355
  const formBusy = useFormBusy();
349
- const button = /* @__PURE__ */ jsx8(
356
+ const unusable = disabled || !!disabledReason || formBusy;
357
+ const look = (off) => cn(
358
+ // `shrink-0` stops a flex parent squashing the button; `self-center`
359
+ // stops one stretching it. Two different failures, and both have
360
+ // happened here: a row with no `items-*` drew this 24 wide and 228
361
+ // tall in Peek's TopicMoreMenu story (Katerina, 2026-09-11). A button
362
+ // is the size of its icon and its padding, whatever box it lands in.
363
+ //
364
+ // This is the one of the two that needs it: an IconButton states no
365
+ // height, so its cross size is `auto` and a stretching parent takes
366
+ // it. Measured in the same 260px row on 2026-09-12: 24px with this
367
+ // class, 226px without. `Button` states `h-8`/`h-6`, so it cannot be
368
+ // stretched and carries no `self-center` — see the note there.
369
+ "flex items-center justify-center p-1 rounded-lg transition-colors shrink-0 self-center cursor-pointer",
370
+ !off && variant === "primary" && "bg-accent-primary hover:bg-accent-hover text-text-inverse",
371
+ !off && (variant === "muted" || variant === "resolve") && "text-text-secondary hover:bg-bg-hover hover:text-text-primary",
372
+ !off && variant === "outlined" && "border border-border-default hover:bg-bg-hover text-text-secondary",
373
+ !off && variant === "current" && "hover:bg-bg-hover",
374
+ off && variant === "primary" && "bg-bg-disabled text-text-disabled",
375
+ off && (variant === "muted" || variant === "resolve") && "text-text-disabled",
376
+ off && variant === "outlined" && "border border-border-default text-text-disabled",
377
+ // Always, disabled too: the colour of where it sits, and Resolve's green.
378
+ variant === "current" && "text-current",
379
+ variant === "resolve" && "signal:hover:bg-success-muted signal:hover:text-success-default",
380
+ pressed && "bg-bg-active text-text-primary",
381
+ glow && "signal:shadow-glow-accent",
382
+ // `pointer-events-none` only where the button is truly out of reach:
383
+ // with a `disabledReason` the button IS the tooltip's trigger, and a
384
+ // trigger the pointer cannot land on never opens one. Base UI already
385
+ // swallows the click.
386
+ off && !disabledReason && "pointer-events-none",
387
+ off && "cursor-not-allowed",
388
+ className
389
+ );
390
+ const button = href && !unusable ? /* @__PURE__ */ jsx8("a", { href, className: look(false), ...props, children }) : /* @__PURE__ */ jsx8(
350
391
  BaseButton,
351
392
  {
352
393
  type,
353
- disabled: disabled || !!disabledReason || formBusy,
394
+ disabled: unusable,
354
395
  focusableWhenDisabled: !!disabledReason,
355
- className: (state) => cn(
356
- // `shrink-0` stops a flex parent squashing the button; `self-center`
357
- // stops one stretching it. Two different failures, and both have
358
- // happened here: a row with no `items-*` drew this 24 wide and 228
359
- // tall in Peek's TopicMoreMenu story (Katerina, 2026-09-11). A button
360
- // is the size of its icon and its padding, whatever box it lands in.
361
- //
362
- // This is the one of the two that needs it: an IconButton states no
363
- // height, so its cross size is `auto` and a stretching parent takes
364
- // it. Measured in the same 260px row on 2026-09-12: 24px with this
365
- // class, 226px without. `Button` states `h-8`/`h-6`, so it cannot be
366
- // stretched and carries no `self-center` — see the note there.
367
- "flex items-center justify-center p-1 rounded-lg transition-colors shrink-0 self-center cursor-pointer",
368
- !state.disabled && variant === "primary" && "bg-accent-primary hover:bg-accent-hover text-text-inverse",
369
- !state.disabled && (variant === "muted" || variant === "resolve") && "text-text-secondary hover:bg-bg-hover hover:text-text-primary",
370
- !state.disabled && variant === "outlined" && "border border-border-default hover:bg-bg-hover text-text-secondary",
371
- !state.disabled && variant === "current" && "hover:bg-bg-hover",
372
- state.disabled && variant === "primary" && "bg-bg-disabled text-text-disabled",
373
- state.disabled && (variant === "muted" || variant === "resolve") && "text-text-disabled",
374
- state.disabled && variant === "outlined" && "border border-border-default text-text-disabled",
375
- // Always, disabled too: the colour of where it sits, and Resolve's green.
376
- variant === "current" && "text-current",
377
- variant === "resolve" && "signal:hover:bg-success-muted signal:hover:text-success-default",
378
- pressed && "bg-bg-active text-text-primary",
379
- glow && "signal:shadow-glow-accent",
380
- // `pointer-events-none` only where the button is truly out of reach:
381
- // with a `disabledReason` the button IS the tooltip's trigger, and a
382
- // trigger the pointer cannot land on never opens one. Base UI already
383
- // swallows the click.
384
- state.disabled && !disabledReason && "pointer-events-none",
385
- state.disabled && "cursor-not-allowed",
386
- className
387
- ),
396
+ className: (state) => look(state.disabled),
388
397
  "aria-pressed": pressed,
389
398
  ...props,
390
399
  children
@@ -434,7 +443,7 @@ function TypeIcon({ name }) {
434
443
  const Icon = ICON_BY_EXTENSION[extensionOf(name)] ?? IconFile;
435
444
  return /* @__PURE__ */ jsx9(Icon, { size: 20, stroke: 1.5 });
436
445
  }
437
- function Truncating({ text, hint, className, wrapperClassName }) {
446
+ function Truncating({ text, hint, className, wrapperClassName, placement }) {
438
447
  const ref = useRef(null);
439
448
  const [cut, setCut] = useState(false);
440
449
  useLayoutEffect(() => {
@@ -456,7 +465,7 @@ function Truncating({ text, hint, className, wrapperClassName }) {
456
465
  }, [text, cut]);
457
466
  const line = /* @__PURE__ */ jsx9("span", { ref, className, children: text });
458
467
  if (!hint && !cut) return line;
459
- return /* @__PURE__ */ jsx9(WithTooltip, { label: hint ?? text, wrapperClassName: cn("min-w-0 flex-col", wrapperClassName), children: line });
468
+ return /* @__PURE__ */ jsx9(WithTooltip, { label: hint ?? text, placement, wrapperClassName: cn("min-w-0 flex-col", wrapperClassName), children: line });
460
469
  }
461
470
  function AttachmentCard({
462
471
  name,
@@ -490,7 +499,7 @@ function AttachmentCard({
490
499
  children: [
491
500
  /* @__PURE__ */ jsx9("div", { className: cn(TILE_CLASSES, "overflow-hidden"), children: state === "uploading" ? /* @__PURE__ */ jsx9(IconLoader2, { size: 16, stroke: 1.5, className: "animate-spin" }) : failed ? /* @__PURE__ */ jsx9(IconAlertCircle, { size: 16, stroke: 1.5, className: "text-error-default" }) : warning ? /* @__PURE__ */ jsx9(IconAlertTriangle, { size: 16, stroke: 1.5, className: "text-warning-default" }) : image && src ? /* @__PURE__ */ jsx9("img", { src, alt: name, className: "size-full object-cover" }) : /* @__PURE__ */ jsx9("span", { className: "text-menu font-semibold", children: typeLabelOf(name) }) }),
492
501
  /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-[1px] min-w-0", children: [
493
- /* @__PURE__ */ jsx9(Truncating, { text: name, className: NAME_CLASSES }),
502
+ /* @__PURE__ */ jsx9(Truncating, { text: name, className: NAME_CLASSES, placement: onRemove ? "bottom" : void 0 }),
494
503
  /* @__PURE__ */ jsx9(
495
504
  Truncating,
496
505
  {
@@ -505,7 +514,7 @@ function AttachmentCard({
505
514
  {
506
515
  type: "button",
507
516
  "aria-label": `Remove ${name}`,
508
- className: "absolute -top-1.5 -right-1.5 size-5 rounded-full bg-bg-elevated border border-border-strong flex items-center justify-center text-text-secondary hover:text-text-primary opacity-0 group-hover:opacity-100 transition-opacity",
517
+ className: "absolute -top-1.5 -right-1.5 size-5 rounded-full bg-bg-elevated border border-border-strong flex items-center justify-center text-text-secondary hover:text-text-primary opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100 transition-opacity",
509
518
  onClick: (event) => {
510
519
  event.stopPropagation();
511
520
  onRemove();
@@ -714,8 +723,8 @@ function Button({
714
723
  // this package's own story frames, each of which asks for the top.
715
724
  // A control does not get to decide where its caller puts it.
716
725
  "inline-flex items-center justify-center gap-1 rounded-md transition-colors font-sans font-medium",
717
- size === "default" && "h-8 text-btn-default",
718
- size === "small" && "h-6 text-btn-small",
726
+ size === "default" && "h-8 min-h-8 text-btn-default",
727
+ size === "small" && "h-6 min-h-6 text-btn-small",
719
728
  // Extra right padding beside a leading icon, for optical balance.
720
729
  size === "default" && (hasLeadingIcon ? "pl-2 pr-3" : "px-2"),
721
730
  size === "small" && (hasLeadingIcon ? "pl-1.5 pr-2" : "px-1.5"),
@@ -1406,6 +1415,7 @@ function Form({ onSubmit, busy: ownBusy = false, enterSends = true, className, c
1406
1415
  },
1407
1416
  onKeyDown,
1408
1417
  onSubmit: (event) => {
1418
+ if (event.target !== event.currentTarget) return;
1409
1419
  event.preventDefault();
1410
1420
  if (busyNow.current) return;
1411
1421
  void onSubmit();
@@ -1422,7 +1432,7 @@ function SkeletonBar({ className, style }) {
1422
1432
  }
1423
1433
  var ROW_BAR_WIDTHS = [150, 100, 170, 120, 90, 140, 110, 160];
1424
1434
  function SkeletonRow({ barWidth = 130 }) {
1425
- return /* @__PURE__ */ jsxs16("div", { className: "flex items-center gap-2 px-2 h-[32px] rounded-lg", children: [
1435
+ return /* @__PURE__ */ jsxs16("div", { className: "flex shrink-0 items-center gap-2 px-2 h-[32px] rounded-lg", children: [
1426
1436
  /* @__PURE__ */ jsx23(SkeletonBar, { className: "w-4 h-4 shrink-0" }),
1427
1437
  /* @__PURE__ */ jsx23(SkeletonBar, { className: "h-3.5", style: { width: barWidth } })
1428
1438
  ] });
@@ -1808,7 +1818,7 @@ function PersonTrigger({ name, picture, fallback, size, open = false, compact =
1808
1818
  "aria-haspopup": "menu",
1809
1819
  "aria-expanded": open,
1810
1820
  className: cn(
1811
- "flex h-8 cursor-pointer items-center gap-1.5 rounded-md pl-1.5 pr-1.5 text-body-2 text-text-primary transition-colors hover:bg-bg-hover",
1821
+ "flex h-8 min-h-8 cursor-pointer items-center gap-1.5 rounded-md pl-1.5 pr-1.5 text-body-2 text-text-primary transition-colors hover:bg-bg-hover",
1812
1822
  open && "bg-bg-hover",
1813
1823
  // Under a `Menu` the open state is Base UI's, not a prop: the trigger
1814
1824
  // carries `data-popup-open` while its menu is up, and sets its own
@@ -1944,56 +1954,65 @@ import { Select as BaseSelect } from "@base-ui/react/select";
1944
1954
  import { jsx as jsx30, jsxs as jsxs22 } from "react/jsx-runtime";
1945
1955
  var GAP4 = 4;
1946
1956
  var VIEWPORT_PAD4 = 8;
1947
- function Select({ value, onChange, options, size = "default", ariaLabel, placeholder = "Select\u2026", disabled, className, ...aria }) {
1957
+ function Select({ value, onChange, options, size = "default", ariaLabel, placeholder = "Select\u2026", disabled, disabledReason, className, ...aria }) {
1948
1958
  const selected = options.find((o) => o.value === value);
1959
+ const held = Boolean(disabledReason);
1960
+ const trigger = /* @__PURE__ */ jsxs22(
1961
+ BaseSelect.Trigger,
1962
+ {
1963
+ "aria-label": ariaLabel,
1964
+ "aria-required": aria["aria-required"],
1965
+ "aria-disabled": held || void 0,
1966
+ className: cn(
1967
+ /*
1968
+ * `min-w-0 max-w-full`: a trigger must never outgrow its container
1969
+ * (Katerina, 2026-09-01 — a long label stretched the files panel's
1970
+ * Lead row past its card). In a flex row the default min-width:auto
1971
+ * forbids shrinking below the label's width, which is what kept
1972
+ * `truncate` from ever engaging; in a block container max-w-full is
1973
+ * the cap. Full-width callers are unaffected.
1974
+ */
1975
+ "flex w-full min-w-0 max-w-full items-center justify-between gap-2 rounded-lg border bg-bg-inset text-left",
1976
+ "border-border-default text-text-primary outline-none transition-colors",
1977
+ // The focused border survives a hover: hover alone strengthens the
1978
+ // hairline, but hover while focused must not grey the focus colour —
1979
+ // the stacked variant outranks plain hover by specificity.
1980
+ !held && "hover:border-border-strong focus-visible:hover:border-border-focus aria-expanded:hover:border-border-focus",
1981
+ "disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled",
1982
+ held && "cursor-not-allowed bg-bg-disabled text-text-disabled",
1983
+ "focus-visible:border-border-focus aria-expanded:border-border-focus",
1984
+ "signal:transition-shadow signal:focus-visible:shadow-focus-ring",
1985
+ size === "default" && "px-3 py-2 text-input-value",
1986
+ size === "small" && "h-6 min-h-6 px-2 text-caption",
1987
+ className
1988
+ ),
1989
+ children: [
1990
+ /* @__PURE__ */ jsxs22("span", { className: cn("flex min-w-0 items-center gap-2", !selected && "text-text-muted"), children: [
1991
+ selected?.leading && /* @__PURE__ */ jsx30("span", { className: "flex shrink-0 items-center", children: selected.leading }),
1992
+ /* @__PURE__ */ jsx30(BaseSelect.Value, { className: "truncate", children: () => selected?.label ?? placeholder })
1993
+ ] }),
1994
+ /* @__PURE__ */ jsx30(
1995
+ BaseSelect.Icon,
1996
+ {
1997
+ render: /* @__PURE__ */ jsx30(IconChevronDown2, { size: size === "small" ? 14 : 16, stroke: 1.5, className: "shrink-0 text-text-secondary" })
1998
+ }
1999
+ )
2000
+ ]
2001
+ }
2002
+ );
1949
2003
  return /* @__PURE__ */ jsxs22(
1950
2004
  BaseSelect.Root,
1951
2005
  {
1952
2006
  value,
1953
- onValueChange: (next) => onChange(next),
1954
- disabled,
2007
+ onValueChange: (next) => {
2008
+ if (!held) onChange(next);
2009
+ },
2010
+ ...held ? { open: false, onOpenChange: () => {
2011
+ } } : {},
2012
+ disabled: disabled && !held,
1955
2013
  modal: false,
1956
2014
  children: [
1957
- /* @__PURE__ */ jsxs22(
1958
- BaseSelect.Trigger,
1959
- {
1960
- "aria-label": ariaLabel,
1961
- "aria-required": aria["aria-required"],
1962
- className: cn(
1963
- /*
1964
- * `min-w-0 max-w-full`: a trigger must never outgrow its container
1965
- * (Katerina, 2026-09-01 — a long label stretched the files panel's
1966
- * Lead row past its card). In a flex row the default min-width:auto
1967
- * forbids shrinking below the label's width, which is what kept
1968
- * `truncate` from ever engaging; in a block container max-w-full is
1969
- * the cap. Full-width callers are unaffected.
1970
- */
1971
- "flex w-full min-w-0 max-w-full items-center justify-between gap-2 rounded-lg border bg-bg-inset text-left",
1972
- "border-border-default text-text-primary outline-none transition-colors",
1973
- // The focused border survives a hover: hover alone strengthens the
1974
- // hairline, but hover while focused must not grey the focus colour —
1975
- // the stacked variant outranks plain hover by specificity.
1976
- "hover:border-border-strong focus-visible:hover:border-border-focus aria-expanded:hover:border-border-focus disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled",
1977
- "focus-visible:border-border-focus aria-expanded:border-border-focus",
1978
- "signal:transition-shadow signal:focus-visible:shadow-focus-ring",
1979
- size === "default" && "px-3 py-2 text-input-value",
1980
- size === "small" && "h-6 px-2 text-caption",
1981
- className
1982
- ),
1983
- children: [
1984
- /* @__PURE__ */ jsxs22("span", { className: cn("flex min-w-0 items-center gap-2", !selected && "text-text-muted"), children: [
1985
- selected?.leading && /* @__PURE__ */ jsx30("span", { className: "flex shrink-0 items-center", children: selected.leading }),
1986
- /* @__PURE__ */ jsx30(BaseSelect.Value, { className: "truncate", children: () => selected?.label ?? placeholder })
1987
- ] }),
1988
- /* @__PURE__ */ jsx30(
1989
- BaseSelect.Icon,
1990
- {
1991
- render: /* @__PURE__ */ jsx30(IconChevronDown2, { size: size === "small" ? 14 : 16, stroke: 1.5, className: "shrink-0 text-text-secondary" })
1992
- }
1993
- )
1994
- ]
1995
- }
1996
- ),
2015
+ held ? /* @__PURE__ */ jsx30(TooltipTrigger, { label: disabledReason ?? "", children: trigger }) : trigger,
1997
2016
  /* @__PURE__ */ jsx30(BaseSelect.Portal, { children: /* @__PURE__ */ jsx30(
1998
2017
  BaseSelect.Positioner,
1999
2018
  {
@@ -2054,7 +2073,7 @@ var TextInput = forwardRef2(function TextInput2({ className, type = "text", size
2054
2073
  "bg-bg-inset border border-border-default hover:border-border-strong focus:border-border-focus rounded-lg",
2055
2074
  // The small size is the small Select's trigger, class for class.
2056
2075
  size === "default" && "px-3 py-2 text-input-value",
2057
- size === "small" && "h-6 px-2 text-caption",
2076
+ size === "small" && "h-6 min-h-6 px-2 text-caption",
2058
2077
  "text-text-primary placeholder:text-text-muted",
2059
2078
  "outline-none transition-colors",
2060
2079
  "disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled",
@@ -2474,6 +2493,9 @@ function ToolbarButton({ ref, disabled, disabledReason, ...props }) {
2474
2493
  }
2475
2494
  );
2476
2495
  }
2496
+ function ToolbarLink({ ref, href, ...props }) {
2497
+ return /* @__PURE__ */ jsx40(BaseToolbar.Link, { ref, render: /* @__PURE__ */ jsx40(IconButton, { href, ...props }) });
2498
+ }
2477
2499
  function ToolbarInput({ size, ...props }) {
2478
2500
  return /* @__PURE__ */ jsx40(BaseToolbar.Input, { render: /* @__PURE__ */ jsx40(TextInput, { size }), ...props });
2479
2501
  }
@@ -2521,7 +2543,7 @@ var GAP6 = 12;
2521
2543
  var VIEWPORT_PAD6 = 8;
2522
2544
  var OPEN_DELAY2 = 350;
2523
2545
  var CLOSE_DELAY = 200;
2524
- function PreviewCard({ content, children, side = "right", delay = OPEN_DELAY2, closeDelay = CLOSE_DELAY, className, wrapperClassName }) {
2546
+ function PreviewCard({ content, children, side = "right", delay = OPEN_DELAY2, closeDelay = CLOSE_DELAY, className, contentClassName, wrapperClassName }) {
2525
2547
  return /* @__PURE__ */ jsxs28(BasePreviewCard.Root, { children: [
2526
2548
  /* @__PURE__ */ jsx42(
2527
2549
  BasePreviewCard.Trigger,
@@ -2543,9 +2565,9 @@ function PreviewCard({ content, children, side = "right", delay = OPEN_DELAY2, c
2543
2565
  children: /* @__PURE__ */ jsx42(
2544
2566
  BasePreviewCard.Popup,
2545
2567
  {
2546
- className: cn("w-[360px] p-3 outline-none", className),
2568
+ className: cn("w-[360px] p-0 outline-none", className),
2547
2569
  render: /* @__PURE__ */ jsx42(MenuPanel, {}),
2548
- children: /* @__PURE__ */ jsx42(ScrollArea, { viewportClassName: "max-h-[calc(min(300px,var(--available-height))_-_1.5rem)]", contentClassName: "flex flex-col gap-3", children: content })
2570
+ children: /* @__PURE__ */ jsx42(ScrollArea, { viewportClassName: "max-h-[min(300px,var(--available-height))]", contentClassName: cn("flex flex-col gap-3 p-3", contentClassName), children: content })
2549
2571
  }
2550
2572
  )
2551
2573
  }
@@ -2567,7 +2589,7 @@ function RailItem({ href, icon, label, active = false, className, ...props }) {
2567
2589
  {
2568
2590
  href,
2569
2591
  "aria-current": active ? "page" : void 0,
2570
- className: cn("group flex w-full shrink-0 flex-col items-center gap-0.5 px-2 py-0.5", className),
2592
+ className: cn("group flex h-12 w-full shrink-0 flex-col items-center gap-0.5 px-2 py-0.5", className),
2571
2593
  ...props,
2572
2594
  children: [
2573
2595
  /* @__PURE__ */ jsx44(
@@ -2640,7 +2662,7 @@ function Reaction({ emoji, count, pressed = false, className, ...props }) {
2640
2662
  className: cn(
2641
2663
  // Chip's pill at a control's height, so a reaction and a status chip
2642
2664
  // read as the same family — 24px matches Button `small`.
2643
- "inline-flex h-6 items-center justify-center gap-1.5 rounded-full px-2",
2665
+ "inline-flex h-6 min-h-6 items-center justify-center gap-1.5 rounded-full px-2",
2644
2666
  "border transition-colors",
2645
2667
  "disabled:cursor-not-allowed disabled:opacity-50",
2646
2668
  pressed ? (
@@ -2697,7 +2719,7 @@ function SearchInput({ shortcut, className, placeholder = "Search\u2026", ...pro
2697
2719
  import { IconChevronRight as IconChevronRight2 } from "@tabler/icons-react";
2698
2720
  import { useRender } from "@base-ui/react/use-render";
2699
2721
  import { Fragment as Fragment8, jsx as jsx49, jsxs as jsxs33 } from "react/jsx-runtime";
2700
- function SectionHeader({ title, chevron = false, isExpanded = true, onToggle, trailing, actions, showActions = "hover", render, className }) {
2722
+ function SectionHeader({ title, chevron = false, isExpanded = true, onToggle, trailing, actions, showActions = "hover", hover = "fill", render, className }) {
2701
2723
  const titleElement = useRender({
2702
2724
  render: render ?? (chevron ? /* @__PURE__ */ jsx49("button", { type: "button", onClick: onToggle, "aria-expanded": isExpanded }) : /* @__PURE__ */ jsx49("span", {})),
2703
2725
  props: {
@@ -2722,11 +2744,11 @@ function SectionHeader({ title, chevron = false, isExpanded = true, onToggle, tr
2722
2744
  "div",
2723
2745
  {
2724
2746
  className: cn(
2725
- "group flex h-[32px] items-center gap-1 rounded-lg px-2 transition-colors",
2747
+ "group flex h-[32px] shrink-0 items-center gap-1 rounded-lg px-2 transition-colors",
2726
2748
  // The fill says "this does something": a row with a toggle or actions
2727
2749
  // lights up, a fixed heading over rows does not (2026-09-09, the
2728
2750
  // Sidebar's fixed group).
2729
- (chevron || actions && actions.length > 0) && "hover:bg-bg-hover",
2751
+ hover === "fill" && (chevron || actions && actions.length > 0) && "hover:bg-bg-hover",
2730
2752
  className
2731
2753
  ),
2732
2754
  children: [
@@ -2761,10 +2783,32 @@ function ContainerHeader({ title, chevron = false, actions, className }) {
2761
2783
  ] });
2762
2784
  }
2763
2785
 
2786
+ // src/ListColumn.tsx
2787
+ import { jsx as jsx51, jsxs as jsxs35 } from "react/jsx-runtime";
2788
+ function ListColumn({ title, chevron = false, actions, above, spacing = "rows", collapsed = false, children, className }) {
2789
+ return /* @__PURE__ */ jsxs35(
2790
+ "div",
2791
+ {
2792
+ className: cn(
2793
+ // `shrink-0`: the column keeps its 290px beside a main area that grows.
2794
+ "flex shrink-0 flex-col overflow-hidden border-r border-border-subtle transition-[width,opacity] duration-300 ease-in-out",
2795
+ collapsed ? "w-0 border-r-0 opacity-0" : "w-[290px] opacity-100",
2796
+ className
2797
+ ),
2798
+ "data-collapsed": collapsed || void 0,
2799
+ children: [
2800
+ /* @__PURE__ */ jsx51(ContainerHeader, { title, chevron, actions }),
2801
+ above,
2802
+ /* @__PURE__ */ jsx51(ScrollArea, { className: "flex-1", contentClassName: cn("flex flex-col px-3 pt-4 pb-3", spacing === "rows" ? "gap-0.5" : "gap-1"), children })
2803
+ ]
2804
+ }
2805
+ );
2806
+ }
2807
+
2764
2808
  // src/CollapsibleSection.tsx
2765
2809
  import { useState as useState7 } from "react";
2766
2810
  import { Collapsible } from "@base-ui/react/collapsible";
2767
- import { jsx as jsx51, jsxs as jsxs35 } from "react/jsx-runtime";
2811
+ import { jsx as jsx52, jsxs as jsxs36 } from "react/jsx-runtime";
2768
2812
  var OPEN = "open";
2769
2813
  var CLOSED = "closed";
2770
2814
  function readStored(key) {
@@ -2791,14 +2835,14 @@ function CollapsibleSection({ title, defaultOpen = true, open: openProp, onOpenC
2791
2835
  writeStored(storageKey, next);
2792
2836
  onOpenChange?.(next);
2793
2837
  };
2794
- return /* @__PURE__ */ jsxs35(Collapsible.Root, { open, onOpenChange: setOpen, className: cn("flex flex-col", className), children: [
2795
- /* @__PURE__ */ jsx51(SectionHeader, { title, chevron: true, isExpanded: open, trailing, actions, showActions, className: "shrink-0", render: /* @__PURE__ */ jsx51(Collapsible.Trigger, {}) }),
2796
- /* @__PURE__ */ jsx51(
2838
+ return /* @__PURE__ */ jsxs36(Collapsible.Root, { open, onOpenChange: setOpen, className: cn("flex flex-col", className), children: [
2839
+ /* @__PURE__ */ jsx52(SectionHeader, { title, chevron: true, isExpanded: open, trailing, actions, showActions, className: "shrink-0", render: /* @__PURE__ */ jsx52(Collapsible.Trigger, {}) }),
2840
+ /* @__PURE__ */ jsx52(
2797
2841
  Collapsible.Panel,
2798
2842
  {
2799
2843
  hiddenUntilFound: true,
2800
2844
  className: "h-[var(--collapsible-panel-height)] overflow-hidden transition-[height] duration-150 ease-out motion-reduce:transition-none data-[starting-style]:h-0 data-[ending-style]:h-0",
2801
- children: /* @__PURE__ */ jsx51("div", { className: cn("flex flex-col", contentClassName), children })
2845
+ children: /* @__PURE__ */ jsx52("div", { className: cn("flex flex-col", contentClassName), children })
2802
2846
  }
2803
2847
  )
2804
2848
  ] });
@@ -2806,9 +2850,9 @@ function CollapsibleSection({ title, defaultOpen = true, open: openProp, onOpenC
2806
2850
 
2807
2851
  // src/Tabs.tsx
2808
2852
  import { Tabs as BaseTabs } from "@base-ui/react/tabs";
2809
- import { jsx as jsx52, jsxs as jsxs36 } from "react/jsx-runtime";
2853
+ import { jsx as jsx53, jsxs as jsxs37 } from "react/jsx-runtime";
2810
2854
  function Tabs({ tabs, active, onChange, size = "default", className, ...aria }) {
2811
- return /* @__PURE__ */ jsx52(
2855
+ return /* @__PURE__ */ jsx53(
2812
2856
  BaseTabs.Root,
2813
2857
  {
2814
2858
  value: active,
@@ -2816,14 +2860,14 @@ function Tabs({ tabs, active, onChange, size = "default", className, ...aria })
2816
2860
  if (details.reason === "none") onChange(value);
2817
2861
  },
2818
2862
  className,
2819
- children: /* @__PURE__ */ jsx52(
2863
+ children: /* @__PURE__ */ jsx53(
2820
2864
  BaseTabs.List,
2821
2865
  {
2822
2866
  activateOnFocus: true,
2823
2867
  "aria-label": aria["aria-label"],
2824
2868
  "aria-labelledby": aria["aria-labelledby"],
2825
2869
  className: "flex items-center gap-2",
2826
- children: tabs.map((tab) => /* @__PURE__ */ jsxs36(
2870
+ children: tabs.map((tab) => /* @__PURE__ */ jsxs37(
2827
2871
  BaseTabs.Tab,
2828
2872
  {
2829
2873
  value: tab.id,
@@ -2836,9 +2880,9 @@ function Tabs({ tabs, active, onChange, size = "default", className, ...aria })
2836
2880
  ),
2837
2881
  children: [
2838
2882
  tab.icon,
2839
- /* @__PURE__ */ jsxs36("span", { className: "flex items-baseline", children: [
2883
+ /* @__PURE__ */ jsxs37("span", { className: "flex items-baseline", children: [
2840
2884
  tab.label,
2841
- tab.count !== void 0 ? /* @__PURE__ */ jsx52("span", { className: "ml-2.5 font-mono text-caption tabular-nums text-text-secondary", children: tab.count }) : null
2885
+ tab.count !== void 0 ? /* @__PURE__ */ jsx53("span", { className: "ml-2.5 font-mono text-caption tabular-nums text-text-secondary", children: tab.count }) : null
2842
2886
  ] })
2843
2887
  ]
2844
2888
  },
@@ -2854,7 +2898,7 @@ function Tabs({ tabs, active, onChange, size = "default", className, ...aria })
2854
2898
  import { createContext as createContext5, useContext as useContext5, useMemo as useMemo4 } from "react";
2855
2899
  import { Toast as BaseToast } from "@base-ui/react/toast";
2856
2900
  import { IconAlertCircle as IconAlertCircle2, IconCircleCheck, IconCircleX } from "@tabler/icons-react";
2857
- import { jsx as jsx53, jsxs as jsxs37 } from "react/jsx-runtime";
2901
+ import { jsx as jsx54, jsxs as jsxs38 } from "react/jsx-runtime";
2858
2902
  var SURFACE_STYLES = {
2859
2903
  success: "bg-success-muted signal:bg-bg-inset signal:border signal:border-border-default signal:shadow-md",
2860
2904
  brand: "bg-accent-muted signal:bg-bg-inset signal:border signal:border-border-default signal:shadow-md",
@@ -2890,16 +2934,16 @@ var pillClassName = (type, hasAction, className) => cn(
2890
2934
  );
2891
2935
  function LeadingIcon({ type }) {
2892
2936
  const Icon = ICONS[type];
2893
- return /* @__PURE__ */ jsx53(Icon, { size: 16, stroke: 1.5, className: cn("text-text-primary shrink-0", ICON_STYLES[type]) });
2937
+ return /* @__PURE__ */ jsx54(Icon, { size: 16, stroke: 1.5, className: cn("text-text-primary shrink-0", ICON_STYLES[type]) });
2894
2938
  }
2895
2939
  function Toast({ label, type = "neutral", leadingIcon = true, actionLabel, onAction, className }) {
2896
2940
  const hasAction = !!(actionLabel && onAction);
2897
- return /* @__PURE__ */ jsxs37("div", { className: pillClassName(type, hasAction, className), children: [
2898
- /* @__PURE__ */ jsxs37("div", { className: "flex items-center gap-2 shrink-0", children: [
2899
- leadingIcon && /* @__PURE__ */ jsx53(LeadingIcon, { type }),
2900
- /* @__PURE__ */ jsx53("span", { className: LABEL_CLASSES, children: label })
2941
+ return /* @__PURE__ */ jsxs38("div", { className: pillClassName(type, hasAction, className), children: [
2942
+ /* @__PURE__ */ jsxs38("div", { className: "flex items-center gap-2 shrink-0", children: [
2943
+ leadingIcon && /* @__PURE__ */ jsx54(LeadingIcon, { type }),
2944
+ /* @__PURE__ */ jsx54("span", { className: LABEL_CLASSES, children: label })
2901
2945
  ] }),
2902
- hasAction && /* @__PURE__ */ jsx53(Button, { variant: "outlined", size: "small", onClick: onAction, children: actionLabel })
2946
+ hasAction && /* @__PURE__ */ jsx54(Button, { variant: "outlined", size: "small", onClick: onAction, children: actionLabel })
2903
2947
  ] });
2904
2948
  }
2905
2949
  var ToastContext = createContext5(null);
@@ -2920,9 +2964,9 @@ function ToastProvider({ children }) {
2920
2964
  }),
2921
2965
  [manager]
2922
2966
  );
2923
- return /* @__PURE__ */ jsx53(ToastContext.Provider, { value, children: /* @__PURE__ */ jsxs37(BaseToast.Provider, { toastManager: manager, limit: VISIBLE_TOASTS, children: [
2967
+ return /* @__PURE__ */ jsx54(ToastContext.Provider, { value, children: /* @__PURE__ */ jsxs38(BaseToast.Provider, { toastManager: manager, limit: VISIBLE_TOASTS, children: [
2924
2968
  children,
2925
- /* @__PURE__ */ jsx53(BaseToast.Portal, { children: /* @__PURE__ */ jsx53(BaseToast.Viewport, { className: "fixed bottom-4 left-4 z-[100] flex flex-col-reverse items-start gap-2 pointer-events-none", children: /* @__PURE__ */ jsx53(ToastList, {}) }) })
2969
+ /* @__PURE__ */ jsx54(BaseToast.Portal, { children: /* @__PURE__ */ jsx54(BaseToast.Viewport, { className: "fixed bottom-4 left-4 z-[100] flex flex-col-reverse items-start gap-2 pointer-events-none", children: /* @__PURE__ */ jsx54(ToastList, {}) }) })
2926
2970
  ] }) });
2927
2971
  }
2928
2972
  function ToastList() {
@@ -2930,25 +2974,25 @@ function ToastList() {
2930
2974
  return toasts.map((toast) => {
2931
2975
  const type = toast.type ?? "neutral";
2932
2976
  const { leadingIcon = true, actionLabel, onAction } = toast.data ?? {};
2933
- return /* @__PURE__ */ jsxs37(
2977
+ return /* @__PURE__ */ jsxs38(
2934
2978
  BaseToast.Root,
2935
2979
  {
2936
2980
  toast,
2937
2981
  swipeDirection: ["left", "down"],
2938
2982
  className: pillClassName(type, !!actionLabel, "pointer-events-auto data-[limited]:hidden"),
2939
2983
  children: [
2940
- /* @__PURE__ */ jsxs37("div", { className: "flex items-center gap-2 shrink-0", children: [
2941
- leadingIcon && /* @__PURE__ */ jsx53(LeadingIcon, { type }),
2942
- /* @__PURE__ */ jsx53(BaseToast.Title, { render: /* @__PURE__ */ jsx53("span", {}), className: LABEL_CLASSES })
2984
+ /* @__PURE__ */ jsxs38("div", { className: "flex items-center gap-2 shrink-0", children: [
2985
+ leadingIcon && /* @__PURE__ */ jsx54(LeadingIcon, { type }),
2986
+ /* @__PURE__ */ jsx54(BaseToast.Title, { render: /* @__PURE__ */ jsx54("span", {}), className: LABEL_CLASSES })
2943
2987
  ] }),
2944
- actionLabel && /* @__PURE__ */ jsx53(
2988
+ actionLabel && /* @__PURE__ */ jsx54(
2945
2989
  BaseToast.Action,
2946
2990
  {
2947
2991
  onClick: () => {
2948
2992
  onAction?.();
2949
2993
  close(toast.id);
2950
2994
  },
2951
- render: /* @__PURE__ */ jsx53(Button, { variant: "outlined", size: "small", children: actionLabel })
2995
+ render: /* @__PURE__ */ jsx54(Button, { variant: "outlined", size: "small", children: actionLabel })
2952
2996
  }
2953
2997
  )
2954
2998
  ]
@@ -3001,6 +3045,7 @@ export {
3001
3045
  InputChip,
3002
3046
  Kbd,
3003
3047
  Link,
3048
+ ListColumn,
3004
3049
  Menu,
3005
3050
  MenuItem,
3006
3051
  MenuPanel,
@@ -3035,6 +3080,7 @@ export {
3035
3080
  Toolbar,
3036
3081
  ToolbarButton,
3037
3082
  ToolbarInput,
3083
+ ToolbarLink,
3038
3084
  ToolbarSeparator,
3039
3085
  Tooltip,
3040
3086
  TooltipProvider,