@estiva-app/ui 0.9.0 → 0.10.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 (151) hide show
  1. package/dist/Avatar.d.ts +13 -1
  2. package/dist/Avatar.d.ts.map +1 -1
  3. package/dist/AvatarGroup.d.ts.map +1 -1
  4. package/dist/Banner.d.ts +16 -1
  5. package/dist/Banner.d.ts.map +1 -1
  6. package/dist/Breadcrumb.d.ts +9 -0
  7. package/dist/Breadcrumb.d.ts.map +1 -1
  8. package/dist/Button.d.ts +12 -2
  9. package/dist/Button.d.ts.map +1 -1
  10. package/dist/Checkbox.d.ts +2 -0
  11. package/dist/Checkbox.d.ts.map +1 -1
  12. package/dist/Chip.d.ts +3 -2
  13. package/dist/Chip.d.ts.map +1 -1
  14. package/dist/ChipInput.d.ts +3 -1
  15. package/dist/ChipInput.d.ts.map +1 -1
  16. package/dist/ConfirmDialog.d.ts +5 -0
  17. package/dist/ConfirmDialog.d.ts.map +1 -1
  18. package/dist/DialogShell.d.ts +27 -3
  19. package/dist/DialogShell.d.ts.map +1 -1
  20. package/dist/EditableText.d.ts +6 -0
  21. package/dist/EditableText.d.ts.map +1 -1
  22. package/dist/Field.d.ts +42 -25
  23. package/dist/Field.d.ts.map +1 -1
  24. package/dist/IconButton.d.ts +3 -2
  25. package/dist/IconButton.d.ts.map +1 -1
  26. package/dist/IdentityMenu.d.ts +25 -13
  27. package/dist/IdentityMenu.d.ts.map +1 -1
  28. package/dist/Menu.d.ts +65 -82
  29. package/dist/Menu.d.ts.map +1 -1
  30. package/dist/PersonTrigger.d.ts.map +1 -1
  31. package/dist/Popover.d.ts +86 -0
  32. package/dist/Popover.d.ts.map +1 -0
  33. package/dist/PreviewCard.d.ts +39 -0
  34. package/dist/PreviewCard.d.ts.map +1 -0
  35. package/dist/Reaction.d.ts.map +1 -1
  36. package/dist/ReactionPicker.d.ts +55 -0
  37. package/dist/ReactionPicker.d.ts.map +1 -0
  38. package/dist/SearchInput.d.ts +5 -0
  39. package/dist/SearchInput.d.ts.map +1 -1
  40. package/dist/Select.d.ts +26 -16
  41. package/dist/Select.d.ts.map +1 -1
  42. package/dist/Tabs.d.ts +11 -1
  43. package/dist/Tabs.d.ts.map +1 -1
  44. package/dist/TextInput.d.ts +7 -1
  45. package/dist/TextInput.d.ts.map +1 -1
  46. package/dist/Textarea.d.ts +8 -1
  47. package/dist/Textarea.d.ts.map +1 -1
  48. package/dist/Toolbar.d.ts +95 -0
  49. package/dist/Toolbar.d.ts.map +1 -0
  50. package/dist/Tooltip.d.ts +36 -10
  51. package/dist/Tooltip.d.ts.map +1 -1
  52. package/dist/cn.d.ts.map +1 -1
  53. package/dist/fit.d.ts +7 -72
  54. package/dist/fit.d.ts.map +1 -1
  55. package/dist/index.d.ts +6 -3
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +882 -763
  58. package/dist/index.js.map +4 -4
  59. package/package.json +1 -1
  60. package/src/Avatar.mdx +17 -0
  61. package/src/Avatar.name.test.tsx +99 -0
  62. package/src/Avatar.tsx +25 -3
  63. package/src/AvatarGroup.tsx +4 -1
  64. package/src/Banner.mdx +10 -2
  65. package/src/Banner.stories.tsx +29 -0
  66. package/src/Banner.test.tsx +60 -0
  67. package/src/Banner.tsx +35 -3
  68. package/src/Breadcrumb.mdx +5 -2
  69. package/src/Breadcrumb.test.tsx +44 -0
  70. package/src/Breadcrumb.tsx +10 -2
  71. package/src/Button.compose.test.tsx +119 -0
  72. package/src/Button.mdx +7 -5
  73. package/src/Button.test.tsx +2 -1
  74. package/src/Button.tsx +21 -5
  75. package/src/Checkbox.tsx +3 -0
  76. package/src/Chip.tsx +3 -2
  77. package/src/ChipInput.tsx +4 -0
  78. package/src/ConfirmDialog.mdx +20 -1
  79. package/src/ConfirmDialog.test.tsx +109 -0
  80. package/src/ConfirmDialog.tsx +6 -0
  81. package/src/DialogShell.mdx +18 -4
  82. package/src/DialogShell.stories.tsx +56 -6
  83. package/src/DialogShell.test.tsx +184 -0
  84. package/src/DialogShell.tsx +108 -39
  85. package/src/EditableText.mdx +6 -1
  86. package/src/EditableText.test.tsx +156 -0
  87. package/src/EditableText.tsx +23 -5
  88. package/src/Field.mdx +23 -3
  89. package/src/Field.stories.tsx +35 -0
  90. package/src/Field.test.tsx +117 -27
  91. package/src/Field.tsx +86 -58
  92. package/src/IconButton.mdx +6 -2
  93. package/src/IconButton.test.tsx +5 -2
  94. package/src/IconButton.tsx +12 -6
  95. package/src/IdentityMenu.mdx +29 -6
  96. package/src/IdentityMenu.stories.tsx +41 -6
  97. package/src/IdentityMenu.tsx +74 -47
  98. package/src/Kbd.stories.tsx +3 -3
  99. package/src/Menu.mdx +88 -75
  100. package/src/Menu.stories.tsx +120 -52
  101. package/src/Menu.test.tsx +315 -0
  102. package/src/Menu.tsx +341 -251
  103. package/src/MenuItem.stories.tsx +135 -98
  104. package/src/NavItem.mdx +1 -1
  105. package/src/PersonTrigger.mdx +20 -14
  106. package/src/PersonTrigger.tsx +23 -1
  107. package/src/Popover.mdx +130 -0
  108. package/src/Popover.stories.tsx +222 -0
  109. package/src/Popover.test.tsx +188 -0
  110. package/src/Popover.tsx +153 -0
  111. package/src/PreviewCard.mdx +98 -0
  112. package/src/PreviewCard.stories.tsx +168 -0
  113. package/src/PreviewCard.test.tsx +83 -0
  114. package/src/PreviewCard.tsx +91 -0
  115. package/src/Rail.mdx +63 -0
  116. package/src/Rail.stories.tsx +81 -0
  117. package/src/Reaction.mdx +4 -2
  118. package/src/Reaction.tsx +4 -2
  119. package/src/ReactionPicker.mdx +85 -0
  120. package/src/ReactionPicker.stories.tsx +120 -0
  121. package/src/ReactionPicker.test.tsx +118 -0
  122. package/src/ReactionPicker.tsx +88 -0
  123. package/src/SearchInput.mdx +6 -2
  124. package/src/SearchInput.tsx +7 -1
  125. package/src/Select.mdx +16 -5
  126. package/src/Select.test.tsx +157 -0
  127. package/src/Select.tsx +89 -214
  128. package/src/Sidebar.mdx +6 -2
  129. package/src/Tabs.mdx +5 -0
  130. package/src/Tabs.test.tsx +15 -0
  131. package/src/Tabs.tsx +17 -2
  132. package/src/TextInput.mdx +8 -2
  133. package/src/TextInput.tsx +10 -7
  134. package/src/Textarea.mdx +6 -2
  135. package/src/Textarea.tsx +14 -9
  136. package/src/Toolbar.mdx +104 -0
  137. package/src/Toolbar.stories.tsx +142 -0
  138. package/src/Toolbar.test.tsx +198 -0
  139. package/src/Toolbar.tsx +158 -0
  140. package/src/Tooltip.mdx +43 -5
  141. package/src/Tooltip.stories.tsx +26 -0
  142. package/src/Tooltip.test.tsx +195 -0
  143. package/src/Tooltip.tsx +150 -48
  144. package/src/cn.ts +1 -1
  145. package/src/fit.test.ts +101 -0
  146. package/src/fit.ts +19 -63
  147. package/src/index.ts +6 -3
  148. package/stories/Choosing.mdx +8 -3
  149. package/tailwind-preset.js +7 -0
  150. package/src/Menu.fit.test.ts +0 -90
  151. package/src/Select.fit.test.ts +0 -101
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ var FONT_SIZE_TOKENS = [
19
19
  "input-helper",
20
20
  "chip"
21
21
  ];
22
- var BOX_SHADOW_TOKENS = ["sm", "md", "lg", "focus-ring", "glow-warning", "highlight-inset"];
22
+ var BOX_SHADOW_TOKENS = ["sm", "md", "lg", "focus-ring", "glow-warning", "glow-success", "glow-accent", "highlight-inset"];
23
23
  var DROP_SHADOW_TOKENS = ["glow-success", "glow-accent"];
24
24
  var twMerge = extendTailwindMerge({
25
25
  extend: {
@@ -119,22 +119,33 @@ var initialsFor = (name) => {
119
119
  const initials = words.map((w) => w[0]).join("") || name.trim().charAt(0);
120
120
  return initials.toUpperCase();
121
121
  };
122
- function Avatar({ src, name, alt = "", size = 36, className }) {
122
+ function Avatar({ src, name, alt = "", size = 36, label: spoken, className }) {
123
123
  const [broken, setBroken] = useState(false);
124
124
  const label = name || alt;
125
125
  const picture = src && !broken ? src : void 0;
126
- return /* @__PURE__ */ jsx3("div", { className: cn("rounded-sm overflow-hidden shrink-0 bg-bg-inset", className), style: { width: size, height: size }, children: picture ? /* @__PURE__ */ jsx3("img", { src: picture, alt: alt || name || "", className: "w-full h-full object-cover", onError: () => setBroken(true) }) : label ? /* @__PURE__ */ jsx3(
126
+ return /* @__PURE__ */ jsx3(
127
127
  "div",
128
128
  {
129
- className: "w-full h-full flex items-center justify-center font-semibold leading-none",
130
- style: {
131
- color: "#08121c",
132
- fontSize: Math.round(size * 0.36),
133
- background: `linear-gradient(160deg, color-mix(in srgb, ${hueFor(label)} 92%, #fff) 0%, color-mix(in srgb, ${hueFor(label)} 70%, #0b0d11) 100%)`
134
- },
135
- children: initialsFor(label)
129
+ ...spoken ? { role: "img", "aria-label": spoken } : { "aria-hidden": true },
130
+ className: cn("rounded-sm overflow-hidden shrink-0 bg-bg-inset", className),
131
+ style: { width: size, height: size },
132
+ children: picture ? /* @__PURE__ */ jsx3("img", { src: picture, alt: "", className: "w-full h-full object-cover", onError: () => setBroken(true) }) : label ? /* @__PURE__ */ jsx3(
133
+ "div",
134
+ {
135
+ className: "w-full h-full flex items-center justify-center font-semibold leading-none",
136
+ style: {
137
+ // The one ink colour that reads on all eight hues, which are a
138
+ // palette rather than tokens (see the note at the top) — so its
139
+ // ink cannot be a token either.
140
+ color: "#08121c",
141
+ fontSize: Math.round(size * 0.36),
142
+ background: `linear-gradient(160deg, color-mix(in srgb, ${hueFor(label)} 92%, #fff) 0%, color-mix(in srgb, ${hueFor(label)} 70%, #0b0d11) 100%)`
143
+ },
144
+ children: initialsFor(label)
145
+ }
146
+ ) : /* @__PURE__ */ jsx3("div", { className: "w-full h-full bg-accent-muted flex items-center justify-center text-text-muted", children: /* @__PURE__ */ jsx3(IconUserFilled, { size: Math.round(size * 0.5) }) })
136
147
  }
137
- ) : /* @__PURE__ */ jsx3("div", { className: "w-full h-full bg-accent-muted flex items-center justify-center text-text-muted", children: /* @__PURE__ */ jsx3(IconUserFilled, { size: Math.round(size * 0.5) }) }) });
148
+ );
138
149
  }
139
150
 
140
151
  // src/AvatarGroup.tsx
@@ -154,7 +165,7 @@ function AvatarGroup({ members, size = 24 }) {
154
165
  {
155
166
  className: "relative flex rounded-sm",
156
167
  style: { marginRight: -overlap, boxShadow: `0 0 0 ${ring}px var(--bg-surface)` },
157
- children: /* @__PURE__ */ jsx4(Avatar, { size, name: member.name, src: member.picture, alt: member.name })
168
+ children: /* @__PURE__ */ jsx4(Avatar, { size, name: member.name, src: member.picture, label: member.name })
158
169
  },
159
170
  i
160
171
  )
@@ -162,28 +173,18 @@ function AvatarGroup({ members, size = 24 }) {
162
173
  }
163
174
 
164
175
  // src/Banner.tsx
165
- import { jsx as jsx5 } from "react/jsx-runtime";
166
- var TONE_STYLES = {
167
- ok: "bg-success-muted text-success-default",
168
- error: "bg-error-muted text-error-default",
169
- info: "bg-info-muted text-info-default",
170
- warning: "bg-warning-muted text-warning-default"
171
- };
172
- function Banner({ tone, children, className }) {
173
- return /* @__PURE__ */ jsx5("div", { role: tone === "error" ? "alert" : "status", className: cn("px-4 py-2 text-body-2", TONE_STYLES[tone], className), children });
174
- }
176
+ import { IconX } from "@tabler/icons-react";
175
177
 
176
- // src/Button.tsx
178
+ // src/IconButton.tsx
177
179
  import { Button as BaseButton } from "@base-ui/react/button";
178
180
 
179
181
  // src/Tooltip.tsx
180
- import { useCallback, useLayoutEffect, useRef, useState as useState2 } from "react";
181
- import { createPortal } from "react-dom";
182
+ import { Tooltip as BaseTooltip } from "@base-ui/react/tooltip";
182
183
 
183
184
  // src/Kbd.tsx
184
- import { jsx as jsx6 } from "react/jsx-runtime";
185
+ import { jsx as jsx5 } from "react/jsx-runtime";
185
186
  function Kbd({ children, className }) {
186
- return /* @__PURE__ */ jsx6(
187
+ return /* @__PURE__ */ jsx5(
187
188
  "kbd",
188
189
  {
189
190
  className: cn(
@@ -198,48 +199,133 @@ function Kbd({ children, className }) {
198
199
  }
199
200
 
200
201
  // src/Tooltip.tsx
201
- import { jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
202
- function Tooltip({ label, shortcut, className }) {
203
- return /* @__PURE__ */ jsxs3("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), children: [
204
- /* @__PURE__ */ jsx7("span", { className: "text-caption text-text-primary whitespace-nowrap", children: label }),
205
- shortcut && /* @__PURE__ */ jsx7(Kbd, { children: shortcut })
202
+ import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
203
+ var OPEN_DELAY = 300;
204
+ function TooltipProvider({ delay = OPEN_DELAY, closeDelay, timeout, children }) {
205
+ return /* @__PURE__ */ jsx6(BaseTooltip.Provider, { delay, closeDelay, timeout, children });
206
+ }
207
+ function Tooltip({ label, shortcut, className, ...props }) {
208
+ return /* @__PURE__ */ jsxs3("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: [
209
+ /* @__PURE__ */ jsx6("span", { className: "text-caption text-text-primary whitespace-nowrap", children: label }),
210
+ shortcut && /* @__PURE__ */ jsx6(Kbd, { children: shortcut })
206
211
  ] });
207
212
  }
208
213
  var GAP = 6;
209
214
  var VIEWPORT_PAD = 8;
215
+ function TooltipSurface({ label, shortcut, placement }) {
216
+ return /* @__PURE__ */ jsx6(BaseTooltip.Portal, { children: /* @__PURE__ */ jsx6(
217
+ BaseTooltip.Positioner,
218
+ {
219
+ side: placement,
220
+ align: "center",
221
+ sideOffset: GAP,
222
+ collisionPadding: VIEWPORT_PAD,
223
+ positionMethod: "fixed",
224
+ className: "pointer-events-none z-[9999]",
225
+ children: /* @__PURE__ */ jsx6(
226
+ BaseTooltip.Popup,
227
+ {
228
+ render: /* @__PURE__ */ jsx6(
229
+ Tooltip,
230
+ {
231
+ label,
232
+ shortcut,
233
+ className: cn(
234
+ "transition-[opacity,transform] duration-[120ms] ease-out motion-reduce:transition-none",
235
+ "data-[ending-style]:duration-[80ms] data-[instant]:duration-0",
236
+ "data-[starting-style]:opacity-0 data-[ending-style]:opacity-0",
237
+ "data-[side=top]:data-[starting-style]:translate-y-[4px] data-[side=top]:data-[ending-style]:translate-y-[4px]",
238
+ "data-[side=bottom]:data-[starting-style]:-translate-y-[4px] data-[side=bottom]:data-[ending-style]:-translate-y-[4px]"
239
+ )
240
+ }
241
+ )
242
+ }
243
+ )
244
+ }
245
+ ) });
246
+ }
210
247
  function WithTooltip({ label, shortcut, placement = "top", wrapperClassName, children }) {
211
- const [show, setShow] = useState2(false);
212
- const ref = useRef(null);
213
- const tooltipRef = useRef(null);
214
- const [style, setStyle] = useState2({ position: "fixed", zIndex: 9999, pointerEvents: "none", visibility: "hidden" });
215
- const reposition = useCallback(() => {
216
- const trigger = ref.current;
217
- const tip = tooltipRef.current;
218
- if (!trigger || !tip) return;
219
- const triggerRect = trigger.getBoundingClientRect();
220
- const tipRect = tip.getBoundingClientRect();
221
- let top = placement === "bottom" ? triggerRect.bottom + GAP : triggerRect.top - tipRect.height - GAP;
222
- if (placement === "top" && top < VIEWPORT_PAD) top = triggerRect.bottom + GAP;
223
- else if (placement === "bottom" && top + tipRect.height > window.innerHeight - VIEWPORT_PAD) top = triggerRect.top - tipRect.height - GAP;
224
- top = Math.max(VIEWPORT_PAD, Math.min(top, window.innerHeight - tipRect.height - VIEWPORT_PAD));
225
- let left = triggerRect.left + triggerRect.width / 2 - tipRect.width / 2;
226
- left = Math.max(VIEWPORT_PAD, Math.min(left, window.innerWidth - tipRect.width - VIEWPORT_PAD));
227
- setStyle({ position: "fixed", top, left, zIndex: 9999, pointerEvents: "none", visibility: "visible" });
228
- }, [placement]);
229
- useLayoutEffect(() => {
230
- if (show) reposition();
231
- }, [show, reposition]);
232
- return /* @__PURE__ */ jsxs3("div", { ref, className: cn("inline-flex shrink-0", wrapperClassName), onMouseEnter: () => setShow(true), onMouseLeave: () => setShow(false), children: [
233
- children,
234
- show && createPortal(
235
- /* @__PURE__ */ jsx7("div", { ref: tooltipRef, style, children: /* @__PURE__ */ jsx7(Tooltip, { label, shortcut }) }),
236
- document.body
237
- )
248
+ return /* @__PURE__ */ jsxs3(BaseTooltip.Root, { disableHoverablePopup: true, children: [
249
+ /* @__PURE__ */ jsx6(BaseTooltip.Trigger, { delay: OPEN_DELAY, render: /* @__PURE__ */ jsx6("div", { className: cn("inline-flex shrink-0", wrapperClassName) }), children }),
250
+ /* @__PURE__ */ jsx6(TooltipSurface, { label, shortcut, placement })
251
+ ] });
252
+ }
253
+ function TooltipTrigger({ label, shortcut, placement = "top", children }) {
254
+ return /* @__PURE__ */ jsxs3(BaseTooltip.Root, { disableHoverablePopup: true, children: [
255
+ /* @__PURE__ */ jsx6(BaseTooltip.Trigger, { delay: OPEN_DELAY, render: children }),
256
+ /* @__PURE__ */ jsx6(TooltipSurface, { label, shortcut, placement })
238
257
  ] });
239
258
  }
240
259
 
241
- // src/Button.tsx
260
+ // src/IconButton.tsx
261
+ import { jsx as jsx7 } from "react/jsx-runtime";
262
+ function IconButton({
263
+ variant = "muted",
264
+ className,
265
+ children,
266
+ disabled,
267
+ disabledReason,
268
+ tooltip,
269
+ tooltipShortcut,
270
+ tooltipPlacement,
271
+ type = "button",
272
+ ...props
273
+ }) {
274
+ const button = /* @__PURE__ */ jsx7(
275
+ BaseButton,
276
+ {
277
+ type,
278
+ disabled: disabled || !!disabledReason,
279
+ focusableWhenDisabled: !!disabledReason,
280
+ className: (state) => cn(
281
+ "flex items-center justify-center p-1 rounded-lg transition-colors shrink-0 cursor-pointer",
282
+ !state.disabled && variant === "primary" && "bg-accent-primary hover:bg-accent-hover text-text-inverse",
283
+ !state.disabled && variant === "muted" && "text-text-secondary hover:bg-bg-hover hover:text-text-primary",
284
+ !state.disabled && variant === "outlined" && "border border-border-default hover:bg-bg-hover text-text-secondary",
285
+ state.disabled && variant === "primary" && "bg-bg-disabled text-text-disabled",
286
+ state.disabled && variant === "muted" && "text-text-disabled",
287
+ state.disabled && variant === "outlined" && "border border-border-default text-text-disabled",
288
+ // `pointer-events-none` only where the button is truly out of reach:
289
+ // with a `disabledReason` the button IS the tooltip's trigger, and a
290
+ // trigger the pointer cannot land on never opens one. Base UI already
291
+ // swallows the click.
292
+ state.disabled && !disabledReason && "pointer-events-none",
293
+ state.disabled && "cursor-not-allowed",
294
+ className
295
+ ),
296
+ ...props,
297
+ children
298
+ }
299
+ );
300
+ const label = disabledReason ?? tooltip;
301
+ if (label) {
302
+ return /* @__PURE__ */ jsx7(TooltipTrigger, { label, shortcut: disabledReason ? void 0 : tooltipShortcut, placement: tooltipPlacement, children: button });
303
+ }
304
+ return button;
305
+ }
306
+
307
+ // src/Banner.tsx
242
308
  import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
309
+ var TONE_STYLES = {
310
+ ok: "bg-success-muted text-success-default",
311
+ error: "bg-error-muted text-error-default",
312
+ info: "bg-info-muted text-info-default",
313
+ warning: "bg-warning-muted text-warning-default"
314
+ };
315
+ function Banner({ tone, children, onDismiss, dismissLabel = "Dismiss", className }) {
316
+ const role = tone === "error" ? "alert" : "status";
317
+ if (!onDismiss) {
318
+ return /* @__PURE__ */ jsx8("div", { role, className: cn("px-4 py-2 text-body-2", TONE_STYLES[tone], className), children });
319
+ }
320
+ return /* @__PURE__ */ jsxs4("div", { role, className: cn("flex items-center gap-3 px-4 py-2 text-body-2", TONE_STYLES[tone], className), children: [
321
+ /* @__PURE__ */ jsx8("span", { className: "min-w-0 flex-1", children }),
322
+ /* @__PURE__ */ jsx8(IconButton, { "aria-label": dismissLabel, onClick: onDismiss, className: "-mr-1 shrink-0 text-current hover:text-current", children: /* @__PURE__ */ jsx8(IconX, { size: 16, stroke: 1.5 }) })
323
+ ] });
324
+ }
325
+
326
+ // src/Button.tsx
327
+ import { Button as BaseButton2 } from "@base-ui/react/button";
328
+ import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
243
329
  function Button({
244
330
  variant = "muted",
245
331
  size = "default",
@@ -252,8 +338,8 @@ function Button({
252
338
  ...props
253
339
  }) {
254
340
  const hasLeadingIcon = !!leadingIcon;
255
- const button = /* @__PURE__ */ jsxs4(
256
- BaseButton,
341
+ const button = /* @__PURE__ */ jsxs5(
342
+ BaseButton2,
257
343
  {
258
344
  type,
259
345
  disabled: disabled || !!disabledReason,
@@ -269,7 +355,13 @@ function Button({
269
355
  !state.disabled && variant === "outlined" && "border border-border-default hover:bg-bg-hover text-text-primary cursor-pointer",
270
356
  !state.disabled && variant === "muted" && "hover:bg-bg-hover text-text-primary cursor-pointer",
271
357
  !state.disabled && variant === "destructive" && "hover:bg-error-muted text-error-default cursor-pointer",
272
- state.disabled && "bg-bg-disabled text-text-disabled pointer-events-none",
358
+ state.disabled && "bg-bg-disabled text-text-disabled",
359
+ // `pointer-events-none` only where the button is truly out of reach.
360
+ // With a `disabledReason` the button IS the tooltip's trigger, and a
361
+ // trigger the pointer cannot land on never opens one — Base UI already
362
+ // swallows the click (`focusableWhenDisabled` gives `aria-disabled`
363
+ // and a prevented `onClick`), so nothing else needs it.
364
+ state.disabled && !disabledReason && "pointer-events-none",
273
365
  state.disabled && variant === "outlined" && "border border-border-default",
274
366
  className
275
367
  ),
@@ -280,13 +372,13 @@ function Button({
280
372
  ]
281
373
  }
282
374
  );
283
- return disabledReason ? /* @__PURE__ */ jsx8(WithTooltip, { label: disabledReason, children: button }) : button;
375
+ return disabledReason ? /* @__PURE__ */ jsx9(TooltipTrigger, { label: disabledReason, children: button }) : button;
284
376
  }
285
377
 
286
378
  // src/Checkbox.tsx
287
379
  import { Checkbox as BaseCheckbox } from "@base-ui/react/checkbox";
288
380
  import { IconCheck } from "@tabler/icons-react";
289
- import { jsx as jsx9 } from "react/jsx-runtime";
381
+ import { jsx as jsx10 } from "react/jsx-runtime";
290
382
  function squareClasses(checked, disabled, interactive, className) {
291
383
  return cn(
292
384
  "inline-flex items-center justify-center size-4 shrink-0 rounded-[4px] border transition-colors",
@@ -299,24 +391,25 @@ function squareClasses(checked, disabled, interactive, className) {
299
391
  var tickClasses = (checked) => cn("flex", !checked && "invisible");
300
392
  function Checkbox({ checked, onChange, disabled = false, className, ...aria }) {
301
393
  if (!onChange) {
302
- return /* @__PURE__ */ jsx9("span", { "aria-hidden": "true", className: squareClasses(checked, disabled, false, className), children: /* @__PURE__ */ jsx9("span", { className: tickClasses(checked), children: /* @__PURE__ */ jsx9(IconCheck, { size: 12, stroke: 3 }) }) });
394
+ return /* @__PURE__ */ jsx10("span", { "aria-hidden": "true", className: squareClasses(checked, disabled, false, className), children: /* @__PURE__ */ jsx10("span", { className: tickClasses(checked), children: /* @__PURE__ */ jsx10(IconCheck, { size: 12, stroke: 3 }) }) });
303
395
  }
304
- return /* @__PURE__ */ jsx9(
396
+ return /* @__PURE__ */ jsx10(
305
397
  BaseCheckbox.Root,
306
398
  {
307
399
  checked,
308
400
  disabled,
309
401
  "aria-label": aria["aria-label"],
402
+ "aria-required": aria["aria-required"],
310
403
  onCheckedChange: (next) => onChange(next),
311
404
  onClick: (e) => e.stopPropagation(),
312
405
  className: (state) => squareClasses(state.checked, state.disabled, true, className),
313
- children: /* @__PURE__ */ jsx9(BaseCheckbox.Indicator, { keepMounted: true, className: (state) => tickClasses(state.checked), children: /* @__PURE__ */ jsx9(IconCheck, { size: 12, stroke: 3 }) })
406
+ children: /* @__PURE__ */ jsx10(BaseCheckbox.Indicator, { keepMounted: true, className: (state) => tickClasses(state.checked), children: /* @__PURE__ */ jsx10(IconCheck, { size: 12, stroke: 3 }) })
314
407
  }
315
408
  );
316
409
  }
317
410
 
318
411
  // src/Chip.tsx
319
- import { jsx as jsx10, jsxs as jsxs5 } from "react/jsx-runtime";
412
+ import { jsx as jsx11, jsxs as jsxs6 } from "react/jsx-runtime";
320
413
  var typeStyles = {
321
414
  neutral: "bg-bg-inset text-text-primary",
322
415
  brand: "bg-accent-muted text-accent-primary signal:border signal:border-accent-outline",
@@ -326,17 +419,17 @@ var typeStyles = {
326
419
  error: "bg-error-muted text-error-default signal:border signal:border-error-outline"
327
420
  };
328
421
  function Chip({ type = "neutral", label, leadingIcon, trailingIcon, className }) {
329
- return /* @__PURE__ */ jsxs5("div", { className: cn("inline-flex items-center justify-center gap-1.5 rounded-full max-h-[20px] min-w-[16px] px-2 py-1", typeStyles[type], className), children: [
330
- leadingIcon && /* @__PURE__ */ jsx10("span", { className: "flex size-3 shrink-0 items-center justify-center", children: leadingIcon }),
331
- label && /* @__PURE__ */ jsx10("span", { className: "text-chip whitespace-nowrap signal:font-mono signal:text-[10px] signal:font-semibold signal:tracking-[0.02em] signal:tabular-nums", children: label }),
332
- trailingIcon && /* @__PURE__ */ jsx10("span", { className: "flex size-3 shrink-0 items-center justify-center", children: trailingIcon })
422
+ return /* @__PURE__ */ jsxs6("div", { className: cn("inline-flex items-center justify-center gap-1.5 rounded-full max-h-[20px] min-w-[16px] px-2 py-1", typeStyles[type], className), children: [
423
+ leadingIcon && /* @__PURE__ */ jsx11("span", { className: "flex size-3 shrink-0 items-center justify-center", children: leadingIcon }),
424
+ label && /* @__PURE__ */ jsx11("span", { className: "text-chip whitespace-nowrap signal:font-mono signal:text-[10px] signal:font-semibold signal:tracking-[0.02em] signal:tabular-nums", children: label }),
425
+ trailingIcon && /* @__PURE__ */ jsx11("span", { className: "flex size-3 shrink-0 items-center justify-center", children: trailingIcon })
333
426
  ] });
334
427
  }
335
428
 
336
429
  // src/ChipInput.tsx
337
- import { useState as useState4, useRef as useRef3, useMemo, useEffect as useEffect2, useLayoutEffect as useLayoutEffect3 } from "react";
338
- import { createPortal as createPortal3 } from "react-dom";
339
- import { IconX } from "@tabler/icons-react";
430
+ import { useState as useState2, useRef, useMemo, useEffect, useLayoutEffect } from "react";
431
+ import { createPortal } from "react-dom";
432
+ import { IconX as IconX2 } from "@tabler/icons-react";
340
433
 
341
434
  // src/fit.ts
342
435
  var MARGIN = 8;
@@ -354,35 +447,16 @@ function fitMenu({
354
447
  const maxHeight = Math.max(Math.min(cap, openUp ? above : below), 120);
355
448
  return openUp ? { left, bottom: viewport.height - anchor.top + GAP2, maxHeight } : { left, top: anchor.bottom + GAP2, maxHeight };
356
449
  }
357
- function clampBox({
358
- box,
359
- viewport
360
- }) {
361
- const left = Math.max(MARGIN, Math.min(box.left, viewport.width - box.width - MARGIN));
362
- const maxHeight = Math.max(Math.min(box.height, viewport.height - 2 * MARGIN), 120);
363
- const top = Math.max(MARGIN, Math.min(box.top, viewport.height - maxHeight - MARGIN));
364
- return { left, top, maxHeight };
365
- }
366
- function fitSubmenu({
367
- row,
368
- panel,
369
- viewport
370
- }) {
371
- const fitsRight = row.right + GAP2 + panel.width <= viewport.width - MARGIN;
372
- const left = Math.max(MARGIN, fitsRight ? row.right + GAP2 : row.left - GAP2 - panel.width);
373
- const top = Math.max(MARGIN, Math.min(row.top, viewport.height - panel.height - MARGIN));
374
- return { left, top };
375
- }
376
450
 
377
451
  // src/Menu.tsx
378
- import { createContext, useCallback as useCallback2, useContext, useEffect, useLayoutEffect as useLayoutEffect2, useRef as useRef2, useState as useState3 } from "react";
452
+ import { createContext, useContext } from "react";
379
453
  import { IconChevronRight } from "@tabler/icons-react";
380
- import { createPortal as createPortal2 } from "react-dom";
454
+ import { Menu as BaseMenu } from "@base-ui/react/menu";
381
455
 
382
456
  // src/SectionLabel.tsx
383
- import { jsx as jsx11 } from "react/jsx-runtime";
457
+ import { jsx as jsx12 } from "react/jsx-runtime";
384
458
  function SectionLabel({ children, className }) {
385
- return /* @__PURE__ */ jsx11(
459
+ return /* @__PURE__ */ jsx12(
386
460
  "span",
387
461
  {
388
462
  className: cn(
@@ -395,224 +469,216 @@ function SectionLabel({ children, className }) {
395
469
  }
396
470
 
397
471
  // src/Menu.tsx
398
- import { jsx as jsx12, jsxs as jsxs6 } from "react/jsx-runtime";
399
- var MenuHoverContext = createContext(null);
472
+ import { Fragment, jsx as jsx13, jsxs as jsxs7 } from "react/jsx-runtime";
473
+ var MenuContext = createContext(null);
400
474
  function MenuPanel({ children, className, ...props }) {
401
- return /* @__PURE__ */ jsx12(
475
+ return /* @__PURE__ */ jsx13(
402
476
  "div",
403
477
  {
404
- className: cn("flex flex-col rounded-lg border border-border-default bg-bg-elevated p-2 shadow-lg", className),
478
+ className: cn(
479
+ "flex flex-col rounded-lg border border-border-default bg-bg-elevated p-2 shadow-lg",
480
+ /*
481
+ * A divider in a menu runs the width of the rows it separates.
482
+ *
483
+ * `Divider` is inset 12px each side, which is right in a page and
484
+ * wrong here: measured in a 180px menu, the rule started 21px from the
485
+ * panel's edge where the rows start at 9px — 12px narrower on each side
486
+ * than the things it divides. `IdentityMenu` was already cancelling it
487
+ * by hand with `mx-0`, which is the sign it belonged here (Katerina,
488
+ * 2026-09-08). Direct children only, so a divider a caller puts inside
489
+ * a row keeps its own spacing.
490
+ */
491
+ "[&>[role=separator]]:mx-0",
492
+ className
493
+ ),
405
494
  ...props,
406
495
  children
407
496
  }
408
497
  );
409
498
  }
410
- function Menu({ onClose, anchor, align = "left", position, closeOnLeave = false, children, className }) {
411
- const ref = useRef2(null);
412
- const leaveTimer = useRef2(void 0);
413
- const hold = useCallback2(() => clearTimeout(leaveTimer.current), []);
414
- const release = useCallback2(() => {
415
- if (!closeOnLeave) return;
416
- clearTimeout(leaveTimer.current);
417
- leaveTimer.current = setTimeout(onClose, 150);
418
- }, [closeOnLeave, onClose]);
419
- useEffect(() => () => clearTimeout(leaveTimer.current), []);
420
- const portalled = Boolean(anchor || position);
421
- const [placed, setPlaced] = useState3(null);
422
- useEffect(() => {
423
- const onDown = (event) => {
424
- if (!ref.current?.contains(event.target)) onClose();
425
- };
426
- const onKey = (event) => {
427
- if (event.key === "Escape") onClose();
428
- };
429
- document.addEventListener("mousedown", onDown);
430
- document.addEventListener("keydown", onKey);
431
- return () => {
432
- document.removeEventListener("mousedown", onDown);
433
- document.removeEventListener("keydown", onKey);
434
- };
435
- }, [onClose]);
436
- const posLeft = position && "left" in position ? position.left : void 0;
437
- const posRight = position && "right" in position ? position.right : void 0;
438
- const posTop = position?.top;
439
- useLayoutEffect2(() => {
440
- if (!portalled || !ref.current) return;
441
- const viewport = { width: window.innerWidth, height: window.innerHeight };
442
- const menu = ref.current;
443
- if (anchor) {
444
- const rect = anchor instanceof Element ? anchor.getBoundingClientRect() : anchor;
445
- const left = align === "right" ? rect.right - menu.offsetWidth : rect.left;
446
- setPlaced(
447
- fitMenu({
448
- anchor: { left, top: rect.top, bottom: rect.bottom },
449
- menu: { width: menu.offsetWidth, contentHeight: menu.scrollHeight },
450
- viewport
451
- })
452
- );
453
- } else if (posTop !== void 0) {
454
- const left = posLeft ?? viewport.width - (posRight ?? 0) - menu.offsetWidth;
455
- setPlaced(clampBox({ box: { left, top: posTop, width: menu.offsetWidth, height: menu.offsetHeight }, viewport }));
456
- }
457
- }, [portalled, anchor, align, posLeft, posRight, posTop]);
458
- useEffect(() => {
459
- if (!anchor) return;
460
- const onScroll = (event) => {
461
- if (event.target instanceof Node && ref.current?.contains(event.target)) return;
462
- onClose();
463
- };
464
- window.addEventListener("resize", onClose);
465
- window.addEventListener("scroll", onScroll, true);
466
- return () => {
467
- window.removeEventListener("resize", onClose);
468
- window.removeEventListener("scroll", onScroll, true);
469
- };
470
- }, [anchor, onClose]);
471
- const style = portalled ? placed ? { left: placed.left, top: placed.top, bottom: placed.bottom, maxHeight: placed.maxHeight } : (
472
- // The provisional render: measured by the layout effect, never seen.
473
- { left: 0, top: 0, visibility: "hidden" }
474
- ) : void 0;
475
- const node = /* @__PURE__ */ jsx12(MenuHoverContext.Provider, { value: { hold, release }, children: /* @__PURE__ */ jsx12(
476
- MenuPanel,
499
+ var isProduction = () => globalThis.process?.env?.NODE_ENV === "production";
500
+ var GAP3 = 4;
501
+ var VIEWPORT_PAD2 = 8;
502
+ var HOVER_OPEN_DELAY = 0;
503
+ var HOVER_CLOSE_DELAY = 150;
504
+ function Menu({ trigger, align = "left", openOnHover = false, open, onOpenChange, actionsRef, children, className }) {
505
+ return /* @__PURE__ */ jsxs7(
506
+ BaseMenu.Root,
477
507
  {
478
- ref,
479
- role: "menu",
480
- "data-interactive": true,
481
- className: cn(
482
- "z-50 min-w-[180px]",
483
- portalled ? "fixed overflow-y-auto" : "absolute right-0 top-full mt-1",
484
- className
485
- ),
486
- style,
487
- onClick: (event) => event.stopPropagation(),
488
- onMouseEnter: closeOnLeave ? hold : void 0,
489
- onMouseLeave: closeOnLeave ? release : void 0,
490
- children
508
+ open,
509
+ onOpenChange: onOpenChange ? (next) => onOpenChange(next) : void 0,
510
+ actionsRef,
511
+ modal: false,
512
+ children: [
513
+ /* @__PURE__ */ jsx13(
514
+ BaseMenu.Trigger,
515
+ {
516
+ render: trigger,
517
+ openOnHover,
518
+ delay: HOVER_OPEN_DELAY,
519
+ closeDelay: HOVER_CLOSE_DELAY
520
+ }
521
+ ),
522
+ /* @__PURE__ */ jsx13(BaseMenu.Portal, { children: /* @__PURE__ */ jsx13(
523
+ BaseMenu.Positioner,
524
+ {
525
+ side: "bottom",
526
+ align: align === "right" ? "end" : "start",
527
+ sideOffset: GAP3,
528
+ collisionPadding: VIEWPORT_PAD2,
529
+ className: "z-50 data-[anchor-hidden]:hidden",
530
+ children: /* @__PURE__ */ jsx13(
531
+ BaseMenu.Popup,
532
+ {
533
+ "data-interactive": true,
534
+ className: cn("min-w-[180px] max-h-[var(--available-height)] overflow-y-auto outline-none", className),
535
+ render: /* @__PURE__ */ jsx13(MenuPanel, {}),
536
+ children: /* @__PURE__ */ jsx13(MenuContext.Provider, { value: { openOnHover }, children })
537
+ }
538
+ )
539
+ }
540
+ ) })
541
+ ]
491
542
  }
492
- ) });
493
- return portalled ? createPortal2(node, document.body) : node;
543
+ );
494
544
  }
495
545
  function MenuSub({ label, leading, selected, children, className }) {
496
- const [open, setOpen] = useState3(false);
497
- const rowRef = useRef2(null);
498
- const panelRef = useRef2(null);
499
- const closeTimer = useRef2(void 0);
500
- const [placed, setPlaced] = useState3(null);
501
- const menuHover = useContext(MenuHoverContext);
502
- const enter = () => {
503
- clearTimeout(closeTimer.current);
504
- menuHover?.hold();
505
- setOpen(true);
506
- };
507
- const leave = () => {
508
- closeTimer.current = setTimeout(() => setOpen(false), 150);
509
- menuHover?.release();
510
- };
511
- useEffect(() => () => clearTimeout(closeTimer.current), []);
512
- useLayoutEffect2(() => {
513
- if (!open || !rowRef.current || !panelRef.current) {
514
- setPlaced(null);
515
- return;
516
- }
517
- const row = rowRef.current.getBoundingClientRect();
518
- setPlaced(
519
- fitSubmenu({
520
- row: { left: row.left, right: row.right, top: row.top },
521
- panel: { width: panelRef.current.offsetWidth, height: panelRef.current.offsetHeight },
522
- viewport: { width: window.innerWidth, height: window.innerHeight }
523
- })
546
+ const menu = useContext(MenuContext);
547
+ if (!isProduction() && menu?.openOnHover) {
548
+ console.error(
549
+ '[@estiva-app/ui] Menu: `openOnHover` and `MenuSub` cannot be used together \u2014 leaving the submenu panel strands the menu open. Open this menu on a press instead. See Menu.mdx, "Hover-opened menus".'
524
550
  );
525
- }, [open]);
526
- return /* @__PURE__ */ jsxs6("div", { ref: rowRef, onMouseEnter: enter, onMouseLeave: leave, children: [
527
- /* @__PURE__ */ jsx12(MenuItem, { label, leading, selected, submenu: true }),
528
- open && createPortal2(
529
- /* @__PURE__ */ jsx12(
530
- MenuPanel,
531
- {
532
- ref: panelRef,
533
- role: "menu",
534
- "data-interactive": true,
535
- className: cn("fixed z-50 w-[160px]", className),
536
- style: placed ?? { left: 0, top: 0, visibility: "hidden" },
537
- onMouseEnter: enter,
538
- onMouseLeave: leave,
539
- children
540
- }
541
- ),
542
- document.body
543
- )
551
+ }
552
+ return /* @__PURE__ */ jsxs7(BaseMenu.SubmenuRoot, { children: [
553
+ /* @__PURE__ */ jsx13(
554
+ BaseMenu.SubmenuTrigger,
555
+ {
556
+ openOnHover: true,
557
+ delay: 0,
558
+ closeDelay: 150,
559
+ nativeButton: true,
560
+ render: /* @__PURE__ */ jsx13("button", { type: "button" }),
561
+ className: menuItemClassName({ size: "default", selected }),
562
+ children: /* @__PURE__ */ jsx13(MenuItemBody, { label, leading, submenu: true })
563
+ }
564
+ ),
565
+ /* @__PURE__ */ jsx13(BaseMenu.Portal, { children: /* @__PURE__ */ jsx13(
566
+ BaseMenu.Positioner,
567
+ {
568
+ side: "inline-end",
569
+ align: "start",
570
+ sideOffset: GAP3,
571
+ collisionPadding: VIEWPORT_PAD2,
572
+ className: "z-50 data-[anchor-hidden]:hidden",
573
+ children: /* @__PURE__ */ jsx13(BaseMenu.Popup, { className: cn("w-[160px]", className), "data-interactive": true, render: /* @__PURE__ */ jsx13(MenuPanel, {}), children })
574
+ }
575
+ ) })
576
+ ] });
577
+ }
578
+ function menuItemClassName({ size, selected, className }) {
579
+ return cn(
580
+ // shrink-0: a menu is a flex column that scrolls at its max height,
581
+ // and a flex child shrinks before its container does — so every row
582
+ // in an overflowing menu was squashed to its `min-h`, and a row given
583
+ // an explicit height silently lost it (Peek's `[` menu: h-12 rows
584
+ // measured 40px). The same fix NavItem took on 2026-09-02.
585
+ // `group`: the `hint` slot reveals itself from this row's own :hover,
586
+ // so the hint and the fill are one CSS state change, not two engines.
587
+ //
588
+ // No transition on the fill (Katerina, 2026-09-05). It faded over
589
+ // 150ms, and anything appearing with it had to fade too or arrive
590
+ // ahead of it — which, sweeping a pointer down a list, read as the
591
+ // hint flickering in and out. Both are instant now: they still change
592
+ // on exactly the same :hover, so they cannot come apart, and a row
593
+ // lights and unlights crisply as the pointer crosses it.
594
+ "group flex w-full shrink-0 cursor-pointer items-center rounded-lg text-left outline-none hover:bg-bg-hover data-[highlighted]:bg-bg-hover",
595
+ // tall: as tall as its content, never shorter than 40px (Katerina,
596
+ // 2026-09-01) — a single-line picker row sits at 40, a row with a
597
+ // 32px face and a role line comes out at its natural 48. One rule,
598
+ // not a hand-picked height per file.
599
+ size === "tall" ? "min-h-10 gap-3 px-3 py-1.5" : "gap-2 px-2 py-1.5",
600
+ selected && "bg-bg-hover",
601
+ className
602
+ );
603
+ }
604
+ function MenuItemBody({ label, children, size = "default", description, leading, trailing, hint, shortcut, submenu, destructive, selected }) {
605
+ const edge = trailing ?? (shortcut ? /* @__PURE__ */ jsx13(Kbd, { children: shortcut }) : submenu ? /* @__PURE__ */ jsx13(IconChevronRight, { size: 16, stroke: 1.5, className: "shrink-0 text-text-muted" }) : null);
606
+ return /* @__PURE__ */ jsxs7(Fragment, { children: [
607
+ leading && /* @__PURE__ */ jsx13("span", { className: "flex shrink-0 items-center", children: leading }),
608
+ children ?? /* @__PURE__ */ jsxs7("span", { className: cn("flex min-w-0 flex-1 flex-col", size === "tall" && "gap-[2px]"), children: [
609
+ /* @__PURE__ */ jsx13("span", { className: cn("truncate text-[14px] leading-[140%]", destructive ? "text-error-default" : "text-text-primary"), children: label }),
610
+ description && /* @__PURE__ */ jsx13("span", { className: "truncate text-[12px] leading-[120%] text-text-secondary", children: description })
611
+ ] }),
612
+ (edge || hint) && // One grid cell holding both, right-aligned: the slot is as wide as
613
+ // the wider of the two and never changes, so revealing the hint moves
614
+ // nothing. No transition here either — the hint switches on the same
615
+ // :hover / `selected` as the fill, in the same frame.
616
+ /* @__PURE__ */ jsxs7("span", { className: "grid shrink-0 items-center justify-items-end [&>*]:col-start-1 [&>*]:row-start-1", children: [
617
+ edge && /* @__PURE__ */ jsx13("span", { className: cn("flex items-center", hint && "group-hover:opacity-0 group-data-[highlighted]:opacity-0", hint && selected && "opacity-0"), children: edge }),
618
+ hint && /* @__PURE__ */ jsx13("span", { className: cn("flex items-center opacity-0 group-hover:opacity-100 group-data-[highlighted]:opacity-100", selected && "opacity-100"), children: hint })
619
+ ] })
544
620
  ] });
545
621
  }
546
622
  function MenuItem({ label, children, size = "default", description, leading, trailing, hint, shortcut, submenu, destructive, selected, className, ...props }) {
547
- const edge = trailing ?? (shortcut ? /* @__PURE__ */ jsx12(Kbd, { children: shortcut }) : submenu ? /* @__PURE__ */ jsx12(IconChevronRight, { size: 16, stroke: 1.5, className: "shrink-0 text-text-muted" }) : null);
548
- return /* @__PURE__ */ jsxs6(
549
- "button",
623
+ const body = /* @__PURE__ */ jsx13(
624
+ MenuItemBody,
550
625
  {
551
- type: "button",
552
- role: "menuitem",
553
- className: cn(
554
- // shrink-0: a menu is a flex column that scrolls at its max height,
555
- // and a flex child shrinks before its container does — so every row
556
- // in an overflowing menu was squashed to its `min-h`, and a row given
557
- // an explicit height silently lost it (Peek's `[` menu: h-12 rows
558
- // measured 40px). The same fix NavItem took on 2026-09-02.
559
- // `group`: the `hint` slot reveals itself from this row's own :hover,
560
- // so the hint and the fill are one CSS state change, not two engines.
561
- //
562
- // No transition on the fill (Katerina, 2026-09-05). It faded over
563
- // 150ms, and anything appearing with it had to fade too or arrive
564
- // ahead of it — which, sweeping a pointer down a list, read as the
565
- // hint flickering in and out. Both are instant now: they still change
566
- // on exactly the same :hover, so they cannot come apart, and a row
567
- // lights and unlights crisply as the pointer crosses it.
568
- "group flex w-full shrink-0 cursor-pointer items-center rounded-lg text-left hover:bg-bg-hover",
569
- // tall: as tall as its content, never shorter than 40px (Katerina,
570
- // 2026-09-01) — a single-line picker row sits at 40, a row with a
571
- // 32px face and a role line comes out at its natural 48. One rule,
572
- // not a hand-picked height per file.
573
- size === "tall" ? "min-h-10 gap-3 px-3 py-1.5" : "gap-2 px-2 py-1.5",
574
- selected && "bg-bg-hover",
575
- className
576
- ),
626
+ label,
627
+ size,
628
+ description,
629
+ leading,
630
+ trailing,
631
+ hint,
632
+ shortcut,
633
+ submenu,
634
+ destructive,
635
+ selected,
636
+ children
637
+ }
638
+ );
639
+ const rowClassName = menuItemClassName({ size, selected, className });
640
+ if (!useContext(MenuContext)) {
641
+ return /* @__PURE__ */ jsx13("button", { type: "button", className: rowClassName, ...props, children: body });
642
+ }
643
+ return /* @__PURE__ */ jsx13(
644
+ BaseMenu.Item,
645
+ {
646
+ nativeButton: true,
647
+ render: /* @__PURE__ */ jsx13("button", { type: "button" }),
648
+ className: rowClassName,
577
649
  ...props,
578
- children: [
579
- leading && /* @__PURE__ */ jsx12("span", { className: "flex shrink-0 items-center", children: leading }),
580
- children ?? /* @__PURE__ */ jsxs6("span", { className: cn("flex min-w-0 flex-1 flex-col", size === "tall" && "gap-[2px]"), children: [
581
- /* @__PURE__ */ jsx12("span", { className: cn("truncate text-[14px] leading-[140%]", destructive ? "text-error-default" : "text-text-primary"), children: label }),
582
- description && /* @__PURE__ */ jsx12("span", { className: "truncate text-[12px] leading-[120%] text-text-secondary", children: description })
583
- ] }),
584
- (edge || hint) && // One grid cell holding both, right-aligned: the slot is as wide as
585
- // the wider of the two and never changes, so revealing the hint moves
586
- // nothing. No transition here either — the hint switches on the same
587
- // :hover / `selected` as the fill, in the same frame.
588
- /* @__PURE__ */ jsxs6("span", { className: "grid shrink-0 items-center justify-items-end [&>*]:col-start-1 [&>*]:row-start-1", children: [
589
- edge && /* @__PURE__ */ jsx12("span", { className: cn("flex items-center", hint && "group-hover:opacity-0", hint && selected && "opacity-0"), children: edge }),
590
- hint && /* @__PURE__ */ jsx12("span", { className: cn("flex items-center opacity-0 group-hover:opacity-100", selected && "opacity-100"), children: hint })
591
- ] })
592
- ]
650
+ children: body
593
651
  }
594
652
  );
595
653
  }
596
654
  function EnterHint({ target }) {
597
- return /* @__PURE__ */ jsxs6("span", { className: "flex shrink-0 items-center gap-1.5 text-text-muted", children: [
598
- /* @__PURE__ */ jsx12(Kbd, { children: "\u21A9 Enter" }),
599
- target && /* @__PURE__ */ jsx12("span", { className: "text-[9px] font-medium leading-[115%] signal:font-mono signal:text-[9.5px] signal:tracking-[0.04em]", children: target })
655
+ return /* @__PURE__ */ jsxs7("span", { className: "flex shrink-0 items-center gap-1.5 text-text-muted", children: [
656
+ /* @__PURE__ */ jsx13(Kbd, { children: "\u21A9 Enter" }),
657
+ target && /* @__PURE__ */ jsx13("span", { className: "text-[9px] font-medium leading-[115%] signal:font-mono signal:text-[9.5px] signal:tracking-[0.04em]", children: target })
600
658
  ] });
601
659
  }
602
660
  function MenuSection({ label, children, className }) {
603
- return /* @__PURE__ */ jsxs6("div", { className: "flex flex-col", children: [
604
- /* @__PURE__ */ jsx12("div", { className: cn("flex h-8 items-center px-2", className), children: /* @__PURE__ */ jsx12(SectionLabel, { className: "text-text-secondary", children: label }) }),
661
+ const inMenu = useContext(MenuContext) !== null;
662
+ const heading = /* @__PURE__ */ jsx13("div", { className: cn("flex h-8 items-center px-2", className), children: /* @__PURE__ */ jsx13(SectionLabel, { className: "text-text-secondary", children: label }) });
663
+ if (!inMenu) {
664
+ return /* @__PURE__ */ jsxs7("div", { className: "flex flex-col", children: [
665
+ heading,
666
+ children
667
+ ] });
668
+ }
669
+ return /* @__PURE__ */ jsxs7(BaseMenu.Group, { className: "flex flex-col", children: [
670
+ /* @__PURE__ */ jsx13(BaseMenu.GroupLabel, { render: heading }),
605
671
  children
606
672
  ] });
607
673
  }
608
674
  function MenuRow({ children, className }) {
609
- return /* @__PURE__ */ jsx12("div", { className: cn("flex items-center gap-2 px-2 py-1.5", className), children });
675
+ return /* @__PURE__ */ jsx13("div", { className: cn("flex items-center gap-2 px-2 py-1.5", className), children });
610
676
  }
611
677
 
612
678
  // src/ChipInput.tsx
613
- import { jsx as jsx13, jsxs as jsxs7 } from "react/jsx-runtime";
679
+ import { jsx as jsx14, jsxs as jsxs8 } from "react/jsx-runtime";
614
680
  function InputChip({ label, leading, onRemove, className }) {
615
- return /* @__PURE__ */ jsxs7(
681
+ return /* @__PURE__ */ jsxs8(
616
682
  "div",
617
683
  {
618
684
  className: cn(
@@ -628,9 +694,9 @@ function InputChip({ label, leading, onRemove, className }) {
628
694
  className
629
695
  ),
630
696
  children: [
631
- leading && /* @__PURE__ */ jsx13("span", { className: "flex shrink-0 items-center", children: leading }),
632
- /* @__PURE__ */ jsx13("span", { className: "text-caption font-medium text-text-primary", children: label }),
633
- onRemove && /* @__PURE__ */ jsx13(
697
+ leading && /* @__PURE__ */ jsx14("span", { className: "flex shrink-0 items-center", children: leading }),
698
+ /* @__PURE__ */ jsx14("span", { className: "text-caption font-medium text-text-primary", children: label }),
699
+ onRemove && /* @__PURE__ */ jsx14(
634
700
  "button",
635
701
  {
636
702
  type: "button",
@@ -640,7 +706,7 @@ function InputChip({ label, leading, onRemove, className }) {
640
706
  },
641
707
  className: "size-4 flex items-center justify-center rounded-full hover:bg-bg-hover text-text-secondary",
642
708
  "aria-label": `Remove ${label}`,
643
- children: /* @__PURE__ */ jsx13(IconX, { size: 10, stroke: 1.5 })
709
+ children: /* @__PURE__ */ jsx14(IconX2, { size: 10, stroke: 1.5 })
644
710
  }
645
711
  )
646
712
  ]
@@ -655,14 +721,15 @@ function ChipInput({
655
721
  autoFocus,
656
722
  excludeIds = [],
657
723
  chipLeading,
658
- rowLeading
724
+ rowLeading,
725
+ ...aria
659
726
  }) {
660
- const [query, setQuery] = useState4("");
661
- const [highlight, setHighlight] = useState4(0);
662
- const [isFocused, setIsFocused] = useState4(false);
663
- const [anchorRect, setAnchorRect] = useState4(null);
664
- const wrapperRef = useRef3(null);
665
- const inputRef = useRef3(null);
727
+ const [query, setQuery] = useState2("");
728
+ const [highlight, setHighlight] = useState2(0);
729
+ const [isFocused, setIsFocused] = useState2(false);
730
+ const [anchorRect, setAnchorRect] = useState2(null);
731
+ const wrapperRef = useRef(null);
732
+ const inputRef = useRef(null);
666
733
  const matches = useMemo(() => {
667
734
  const selectedIds = new Set(value.map((o) => o.id));
668
735
  const excludedIds = new Set(excludeIds);
@@ -674,11 +741,11 @@ function ChipInput({
674
741
  return o.label.toLowerCase().includes(q) || (o.description ?? "").toLowerCase().includes(q);
675
742
  });
676
743
  }, [query, value, excludeIds, options]);
677
- useEffect2(() => {
744
+ useEffect(() => {
678
745
  setHighlight(0);
679
746
  }, [query, matches.length]);
680
747
  const showDropdown = isFocused && query.trim().length > 0 && matches.length > 0;
681
- useLayoutEffect3(() => {
748
+ useLayoutEffect(() => {
682
749
  if (!showDropdown) return;
683
750
  const update = () => {
684
751
  if (wrapperRef.current) setAnchorRect(wrapperRef.current.getBoundingClientRect());
@@ -728,16 +795,16 @@ function ChipInput({
728
795
  }
729
796
  }
730
797
  }
731
- return /* @__PURE__ */ jsxs7("div", { className: "relative", children: [
732
- /* @__PURE__ */ jsxs7(
798
+ return /* @__PURE__ */ jsxs8("div", { className: "relative", children: [
799
+ /* @__PURE__ */ jsxs8(
733
800
  "div",
734
801
  {
735
802
  ref: wrapperRef,
736
803
  className: "bg-bg-inset border border-border-default hover:border-border-strong focus-within:border-border-focus focus-within:hover:border-border-focus rounded-lg px-3 py-1.5 flex flex-wrap items-center gap-1.5 transition-colors min-h-[38px] cursor-text signal:transition-shadow signal:focus-within:shadow-focus-ring",
737
804
  onClick: () => inputRef.current?.focus(),
738
805
  children: [
739
- value.map((o) => /* @__PURE__ */ jsx13(InputChip, { label: o.label, leading: chipLeading?.(o), onRemove: () => removeOption(o.id) }, o.id)),
740
- /* @__PURE__ */ jsx13(
806
+ value.map((o) => /* @__PURE__ */ jsx14(InputChip, { label: o.label, leading: chipLeading?.(o), onRemove: () => removeOption(o.id) }, o.id)),
807
+ /* @__PURE__ */ jsx14(
741
808
  "input",
742
809
  {
743
810
  ref: inputRef,
@@ -751,14 +818,15 @@ function ChipInput({
751
818
  setTimeout(() => setIsFocused(false), 150);
752
819
  },
753
820
  placeholder: value.length === 0 ? placeholder : "",
821
+ "aria-required": aria["aria-required"],
754
822
  className: "flex-1 min-w-[120px] bg-transparent text-body-2 text-text-primary placeholder:text-text-muted outline-none border-none"
755
823
  }
756
824
  )
757
825
  ]
758
826
  }
759
827
  ),
760
- showDropdown && anchorRect && createPortal3(
761
- /* @__PURE__ */ jsx13(
828
+ showDropdown && anchorRect && createPortal(
829
+ /* @__PURE__ */ jsx14(
762
830
  "div",
763
831
  {
764
832
  className: "fixed z-[60] overflow-y-auto bg-bg-elevated border border-border-default rounded-lg shadow-lg",
@@ -771,7 +839,7 @@ function ChipInput({
771
839
  }),
772
840
  width: anchorRect.width
773
841
  },
774
- children: matches.map((o, i) => /* @__PURE__ */ jsx13(
842
+ children: matches.map((o, i) => /* @__PURE__ */ jsx14(
775
843
  MenuItem,
776
844
  {
777
845
  size: "tall",
@@ -795,51 +863,8 @@ function ChipInput({
795
863
  ] });
796
864
  }
797
865
 
798
- // src/IconButton.tsx
799
- import { Button as BaseButton2 } from "@base-ui/react/button";
800
- import { jsx as jsx14 } from "react/jsx-runtime";
801
- function IconButton({
802
- variant = "muted",
803
- className,
804
- children,
805
- disabled,
806
- disabledReason,
807
- tooltip,
808
- tooltipShortcut,
809
- tooltipPlacement,
810
- type = "button",
811
- ...props
812
- }) {
813
- const button = /* @__PURE__ */ jsx14(
814
- BaseButton2,
815
- {
816
- type,
817
- disabled: disabled || !!disabledReason,
818
- focusableWhenDisabled: !!disabledReason,
819
- className: (state) => cn(
820
- "flex items-center justify-center p-1 rounded-lg transition-colors shrink-0 cursor-pointer",
821
- !state.disabled && variant === "primary" && "bg-accent-primary hover:bg-accent-hover text-text-inverse",
822
- !state.disabled && variant === "muted" && "text-text-secondary hover:bg-bg-hover hover:text-text-primary",
823
- !state.disabled && variant === "outlined" && "border border-border-default hover:bg-bg-hover text-text-secondary",
824
- state.disabled && variant === "primary" && "bg-bg-disabled text-text-disabled",
825
- state.disabled && variant === "muted" && "text-text-disabled",
826
- state.disabled && variant === "outlined" && "border border-border-default text-text-disabled",
827
- state.disabled && "pointer-events-none cursor-not-allowed",
828
- className
829
- ),
830
- ...props,
831
- children
832
- }
833
- );
834
- const label = disabledReason ?? tooltip;
835
- if (label) {
836
- return /* @__PURE__ */ jsx14(WithTooltip, { label, shortcut: disabledReason ? void 0 : tooltipShortcut, placement: tooltipPlacement, children: button });
837
- }
838
- return button;
839
- }
840
-
841
866
  // src/IdentityMenu.tsx
842
- import { useRef as useRef4, useState as useState5 } from "react";
867
+ import { useCallback, useRef as useRef2 } from "react";
843
868
 
844
869
  // src/Divider.tsx
845
870
  import { jsx as jsx15 } from "react/jsx-runtime";
@@ -855,13 +880,13 @@ function Divider({ orientation = "horizontal", className }) {
855
880
  }
856
881
 
857
882
  // src/Person.tsx
858
- import { jsx as jsx16, jsxs as jsxs8 } from "react/jsx-runtime";
883
+ import { jsx as jsx16, jsxs as jsxs9 } from "react/jsx-runtime";
859
884
  function Person({ name, picture, fallback = "\u2014", size = 20, className }) {
860
885
  return (
861
886
  // gap-2: the face-to-name distance is one rule (8px) wherever a person
862
887
  // appears — Katerina, 2026-09-01, set on Person so PersonTrigger and every
863
888
  // row agree by construction.
864
- /* @__PURE__ */ jsxs8("span", { className: cn("flex min-w-0 items-center gap-2", className), children: [
889
+ /* @__PURE__ */ jsxs9("span", { className: cn("flex min-w-0 items-center gap-2", className), children: [
865
890
  /* @__PURE__ */ jsx16(Avatar, { name, src: picture, size }),
866
891
  /* @__PURE__ */ jsx16("span", { className: cn("truncate", !name && "text-text-muted"), children: name ?? fallback })
867
892
  ] })
@@ -871,7 +896,7 @@ function Person({ name, picture, fallback = "\u2014", size = 20, className }) {
871
896
  // src/PersonTrigger.tsx
872
897
  import { Button as BaseButton3 } from "@base-ui/react/button";
873
898
  import { IconChevronDown } from "@tabler/icons-react";
874
- import { jsx as jsx17, jsxs as jsxs9 } from "react/jsx-runtime";
899
+ import { jsx as jsx17, jsxs as jsxs10 } from "react/jsx-runtime";
875
900
  function PersonTrigger({ name, picture, fallback, size, open = false, compact = false, className, ...props }) {
876
901
  if (compact) {
877
902
  return /* @__PURE__ */ jsx17(
@@ -880,13 +905,14 @@ function PersonTrigger({ name, picture, fallback, size, open = false, compact =
880
905
  type: "button",
881
906
  "aria-haspopup": "menu",
882
907
  "aria-expanded": open,
883
- className: cn("cursor-pointer rounded-full focus:outline-none", className),
908
+ "aria-label": props["aria-label"] ?? name ?? fallback,
909
+ className: cn("cursor-pointer rounded-full", className),
884
910
  ...props,
885
911
  children: /* @__PURE__ */ jsx17(Avatar, { name, src: picture, size: size ?? 36 })
886
912
  }
887
913
  );
888
914
  }
889
- return /* @__PURE__ */ jsxs9(
915
+ return /* @__PURE__ */ jsxs10(
890
916
  BaseButton3,
891
917
  {
892
918
  type: "button",
@@ -895,6 +921,11 @@ function PersonTrigger({ name, picture, fallback, size, open = false, compact =
895
921
  className: cn(
896
922
  "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",
897
923
  open && "bg-bg-hover",
924
+ // Under a `Menu` the open state is Base UI's, not a prop: the trigger
925
+ // carries `data-popup-open` while its menu is up, and sets its own
926
+ // `aria-expanded`. Both spellings hold the fill, so this works whether
927
+ // the caller drives it or the menu does.
928
+ "data-[popup-open]:bg-bg-hover",
898
929
  className
899
930
  ),
900
931
  ...props,
@@ -907,30 +938,34 @@ function PersonTrigger({ name, picture, fallback, size, open = false, compact =
907
938
  }
908
939
 
909
940
  // src/IdentityMenu.tsx
910
- import { Fragment, jsx as jsx18, jsxs as jsxs10 } from "react/jsx-runtime";
911
- function IdentityPanel({ me, signedIn, relayUrl, idBase, onCopyKey, onSignOut, onClose, anchor, className, children }) {
941
+ import { Fragment as Fragment2, jsx as jsx18, jsxs as jsxs11 } from "react/jsx-runtime";
942
+ function IdentityPanel({ className, onClose = () => {
943
+ }, ...rest }) {
944
+ return /* @__PURE__ */ jsx18(MenuPanel, { className: cn("w-72", className), children: /* @__PURE__ */ jsx18(IdentityRows, { ...rest, onClose }) });
945
+ }
946
+ function IdentityRows({ me, signedIn, relayUrl, idBase, onCopyKey, onSignOut, onClose, children }) {
912
947
  const act = (action) => () => {
913
948
  onClose();
914
949
  action();
915
950
  };
916
951
  const appRows = typeof children === "function" ? children(onClose) : children;
917
- return /* @__PURE__ */ jsxs10(Menu, { onClose, anchor, align: "right", className: cn("w-72", className), children: [
918
- /* @__PURE__ */ jsxs10(MenuSection, { label: signedIn ? "Signed in as" : "Acting as", children: [
952
+ return /* @__PURE__ */ jsxs11(Fragment2, { children: [
953
+ /* @__PURE__ */ jsxs11(MenuSection, { label: signedIn ? "Signed in as" : "Acting as", children: [
919
954
  /* @__PURE__ */ jsx18(MenuRow, { children: /* @__PURE__ */ jsx18(Person, { name: me.name, picture: me.picture, fallback: "Anonymous", size: 28, className: "text-body-2-strong" }) }),
920
955
  /* @__PURE__ */ jsx18(Note, { children: signedIn ? "Your Estiva ID. The same person you are in every Estiva app." : "A key held by this browser only. Nobody else knows who you are." }),
921
- me.email && /* @__PURE__ */ jsxs10(Note, { children: [
956
+ me.email && /* @__PURE__ */ jsxs11(Note, { children: [
922
957
  me.email,
923
958
  " \xB7 known to Estiva, never published to the relay"
924
959
  ] })
925
960
  ] }),
926
- relayUrl && /* @__PURE__ */ jsxs10(Fragment, { children: [
961
+ relayUrl && /* @__PURE__ */ jsxs11(Fragment2, { children: [
927
962
  /* @__PURE__ */ jsx18(Divider, { className: "mx-0 my-2" }),
928
- /* @__PURE__ */ jsxs10(MenuSection, { label: "Workspace", children: [
963
+ /* @__PURE__ */ jsxs11(MenuSection, { label: "Workspace", children: [
929
964
  /* @__PURE__ */ jsx18(MenuRow, { children: /* @__PURE__ */ jsx18("span", { className: "break-all font-mono text-caption text-text-primary", children: relayUrl }) }),
930
965
  /* @__PURE__ */ jsx18(Note, { children: "Everyone on this relay shares this workspace, in every app." })
931
966
  ] })
932
967
  ] }),
933
- appRows && /* @__PURE__ */ jsxs10(Fragment, { children: [
968
+ appRows && /* @__PURE__ */ jsxs11(Fragment2, { children: [
934
969
  /* @__PURE__ */ jsx18(Divider, { className: "mx-0 my-2" }),
935
970
  appRows
936
971
  ] }),
@@ -947,280 +982,179 @@ function IdentityPanel({ me, signedIn, relayUrl, idBase, onCopyKey, onSignOut, o
947
982
  ] });
948
983
  }
949
984
  function IdentityMenu({ me, signedIn, relayUrl, idBase, onCopyKey, onSignOut, compact = false, className, children }) {
950
- const [open, setOpen] = useState5(false);
951
- const anchorRef = useRef4(null);
952
- return /* @__PURE__ */ jsxs10("div", { ref: anchorRef, className: cn("relative", className), children: [
953
- /* @__PURE__ */ jsx18(
954
- PersonTrigger,
955
- {
956
- name: me.name,
957
- picture: me.picture,
958
- fallback: "Anonymous",
959
- compact,
960
- size: compact ? 36 : void 0,
961
- open,
962
- onMouseDown: (event) => event.stopPropagation(),
963
- onClick: () => setOpen((value) => !value),
964
- "aria-label": compact ? "Account menu" : void 0
965
- }
966
- ),
967
- open && /* @__PURE__ */ jsx18(
968
- IdentityPanel,
985
+ const actions = useRef2(null);
986
+ const close = useCallback(() => actions.current?.close(), []);
987
+ return (
988
+ /* The wrapper carries the caller's `className` and nothing else. It used
989
+ to be `relative`, because the panel was positioned against it; the panel
990
+ hangs from the trigger and portals now, so a positioning context here
991
+ would only be a lie about what this box does. */
992
+ /* @__PURE__ */ jsx18("div", { className, children: /* @__PURE__ */ jsx18(
993
+ Menu,
969
994
  {
970
- me,
971
- signedIn,
972
- relayUrl,
973
- idBase,
974
- onCopyKey,
975
- onSignOut,
976
- onClose: () => setOpen(false),
977
- anchor: anchorRef.current,
978
- children
995
+ align: "right",
996
+ actionsRef: actions,
997
+ className: "w-72",
998
+ trigger: /* @__PURE__ */ jsx18(
999
+ PersonTrigger,
1000
+ {
1001
+ name: me.name,
1002
+ picture: me.picture,
1003
+ fallback: "Anonymous",
1004
+ compact,
1005
+ size: compact ? 36 : void 0,
1006
+ "aria-label": compact ? "Account menu" : void 0
1007
+ }
1008
+ ),
1009
+ children: /* @__PURE__ */ jsx18(
1010
+ IdentityRows,
1011
+ {
1012
+ me,
1013
+ signedIn,
1014
+ relayUrl,
1015
+ idBase,
1016
+ onCopyKey,
1017
+ onSignOut,
1018
+ onClose: close,
1019
+ children
1020
+ }
1021
+ )
979
1022
  }
980
- )
981
- ] });
1023
+ ) })
1024
+ );
982
1025
  }
983
1026
  function Note({ children }) {
984
1027
  return /* @__PURE__ */ jsx18("span", { className: "px-2 pb-1.5 text-caption text-text-secondary", children });
985
1028
  }
986
1029
 
987
1030
  // src/Field.tsx
988
- import { createContext as createContext2, useContext as useContext2, useId } from "react";
989
- import { jsx as jsx19, jsxs as jsxs11 } from "react/jsx-runtime";
990
- var FieldControlIdContext = createContext2(void 0);
991
- function useFieldControlId(ownId) {
992
- const fromField = useContext2(FieldControlIdContext);
993
- return fromField ?? ownId;
994
- }
995
- function Field({ label, required = false, htmlFor, children }) {
996
- const generated = useId();
997
- const id = htmlFor ?? generated;
998
- return /* @__PURE__ */ jsxs11("div", { className: "flex flex-col gap-2", children: [
999
- /* @__PURE__ */ jsxs11(
1000
- "label",
1001
- {
1002
- htmlFor: id,
1003
- className: `text-input-label text-text-primary${required ? " flex items-center" : ""}`,
1004
- children: [
1005
- label,
1006
- required && /* @__PURE__ */ jsx19("span", { className: "text-error-default ml-0.5", children: "*" })
1007
- ]
1008
- }
1009
- ),
1010
- /* @__PURE__ */ jsx19(FieldControlIdContext.Provider, { value: id, children })
1031
+ import { cloneElement, isValidElement } from "react";
1032
+ import { Field as BaseField } from "@base-ui/react/field";
1033
+ import { jsx as jsx19, jsxs as jsxs12 } from "react/jsx-runtime";
1034
+ function Field({ label, required = false, helper, error, children }) {
1035
+ const line = error ?? helper;
1036
+ const control = required && isValidElement(children) ? cloneElement(children, {
1037
+ "aria-required": children.props["aria-required"] ?? true
1038
+ }) : children;
1039
+ return /* @__PURE__ */ jsxs12(BaseField.Root, { invalid: !!error, className: "flex flex-col gap-2", children: [
1040
+ /* @__PURE__ */ jsxs12(BaseField.Label, { className: cn("text-input-label text-text-primary", required && "flex items-center"), children: [
1041
+ label,
1042
+ required && /* @__PURE__ */ jsx19("span", { className: "text-error-default ml-0.5", children: "*" })
1043
+ ] }),
1044
+ line == null ? control : /* @__PURE__ */ jsxs12("div", { className: "flex flex-col gap-1.5", children: [
1045
+ control,
1046
+ error != null ? /* @__PURE__ */ jsx19(BaseField.Error, { match: true, className: "text-caption text-error-default", children: error }) : /* @__PURE__ */ jsx19(BaseField.Description, { className: "text-caption text-text-muted", children: helper })
1047
+ ] })
1011
1048
  ] });
1012
1049
  }
1013
1050
 
1014
1051
  // src/Select.tsx
1015
1052
  import { IconCheck as IconCheck2, IconChevronDown as IconChevronDown2 } from "@tabler/icons-react";
1016
- import { useCallback as useCallback3, useEffect as useEffect3, useId as useId2, useLayoutEffect as useLayoutEffect4, useMemo as useMemo2, useRef as useRef5, useState as useState6 } from "react";
1017
- import { createPortal as createPortal4 } from "react-dom";
1018
- import { Fragment as Fragment2, jsx as jsx20, jsxs as jsxs12 } from "react/jsx-runtime";
1019
- function Select({ value, onChange, options, size = "default", ariaLabel, placeholder = "Select\u2026", disabled, className }) {
1020
- const id = useId2();
1021
- const triggerRef = useRef5(null);
1022
- const menuRef = useRef5(null);
1023
- const [rect, setRect] = useState6(null);
1024
- const [placement, setPlacement] = useState6(null);
1025
- const open = rect !== null;
1026
- const selectedIndex = useMemo2(() => Math.max(0, options.findIndex((o) => o.value === value)), [options, value]);
1027
- const [activeIndex, setActiveIndex] = useState6(selectedIndex);
1028
- const close = useCallback3((refocus) => {
1029
- setRect(null);
1030
- if (refocus) triggerRef.current?.focus();
1031
- }, []);
1032
- const openMenu = useCallback3(() => {
1033
- setActiveIndex(selectedIndex);
1034
- setRect(triggerRef.current?.getBoundingClientRect() ?? null);
1035
- }, [selectedIndex]);
1036
- useLayoutEffect4(() => {
1037
- if (!rect || !menuRef.current) {
1038
- setPlacement(null);
1039
- return;
1040
- }
1041
- setPlacement(
1042
- fitMenu({
1043
- anchor: { left: rect.left, top: rect.top, bottom: rect.bottom },
1044
- menu: { width: menuRef.current.offsetWidth, contentHeight: menuRef.current.scrollHeight },
1045
- viewport: { width: window.innerWidth, height: window.innerHeight },
1046
- // The option list keeps its classic height (the old max-h-72); a menu
1047
- // panel passes no cap and stands as tall as the room it opens into.
1048
- cap: 288
1049
- })
1050
- );
1051
- }, [rect, options.length]);
1052
- useEffect3(() => {
1053
- if (!open) return;
1054
- document.getElementById(`${id}-${activeIndex}`)?.scrollIntoView?.({ block: "nearest" });
1055
- }, [open, activeIndex, id]);
1056
- useEffect3(() => {
1057
- if (!open) return;
1058
- const onDown = (e) => {
1059
- if (menuRef.current?.contains(e.target)) return;
1060
- if (triggerRef.current?.contains(e.target)) return;
1061
- setRect(null);
1062
- };
1063
- const onResize = () => setRect(null);
1064
- const onScroll = (e) => {
1065
- if (e.target instanceof Node && menuRef.current?.contains(e.target)) return;
1066
- setRect(null);
1067
- };
1068
- document.addEventListener("mousedown", onDown);
1069
- window.addEventListener("resize", onResize);
1070
- window.addEventListener("scroll", onScroll, true);
1071
- return () => {
1072
- document.removeEventListener("mousedown", onDown);
1073
- window.removeEventListener("resize", onResize);
1074
- window.removeEventListener("scroll", onScroll, true);
1075
- };
1076
- }, [open]);
1077
- const pick = (index) => {
1078
- const option = options[index];
1079
- if (!option) return;
1080
- onChange(option.value);
1081
- close(true);
1082
- };
1083
- const onKeyDown = (e) => {
1084
- if (!open) {
1085
- if (["ArrowDown", "ArrowUp", "Enter", " "].includes(e.key)) {
1086
- e.preventDefault();
1087
- openMenu();
1088
- }
1089
- return;
1090
- }
1091
- switch (e.key) {
1092
- case "Escape":
1093
- e.preventDefault();
1094
- close(true);
1095
- break;
1096
- case "ArrowDown":
1097
- e.preventDefault();
1098
- setActiveIndex((i) => Math.min(options.length - 1, i + 1));
1099
- break;
1100
- case "ArrowUp":
1101
- e.preventDefault();
1102
- setActiveIndex((i) => Math.max(0, i - 1));
1103
- break;
1104
- case "Home":
1105
- e.preventDefault();
1106
- setActiveIndex(0);
1107
- break;
1108
- case "End":
1109
- e.preventDefault();
1110
- setActiveIndex(options.length - 1);
1111
- break;
1112
- case "Enter":
1113
- case " ":
1114
- e.preventDefault();
1115
- pick(activeIndex);
1116
- break;
1117
- case "Tab":
1118
- close(false);
1119
- break;
1120
- }
1121
- };
1053
+ import { Select as BaseSelect } from "@base-ui/react/select";
1054
+ import { jsx as jsx20, jsxs as jsxs13 } from "react/jsx-runtime";
1055
+ var GAP4 = 4;
1056
+ var VIEWPORT_PAD3 = 8;
1057
+ function Select({ value, onChange, options, size = "default", ariaLabel, placeholder = "Select\u2026", disabled, className, ...aria }) {
1122
1058
  const selected = options.find((o) => o.value === value);
1123
- return /* @__PURE__ */ jsxs12(Fragment2, { children: [
1124
- /* @__PURE__ */ jsxs12(
1125
- "button",
1126
- {
1127
- ref: triggerRef,
1128
- type: "button",
1129
- disabled,
1130
- "aria-haspopup": "listbox",
1131
- "aria-expanded": open,
1132
- "aria-label": ariaLabel,
1133
- onClick: () => open ? close(false) : openMenu(),
1134
- onKeyDown,
1135
- className: cn(
1136
- /*
1137
- * `min-w-0 max-w-full`: a trigger must never outgrow its container
1138
- * (Katerina, 2026-09-01 a long label stretched the files panel's
1139
- * Lead row past its card). In a flex row the default min-width:auto
1140
- * forbids shrinking below the label's width, which is what kept
1141
- * `truncate` from ever engaging; in a block container max-w-full is
1142
- * the cap. Full-width callers are unaffected.
1143
- */
1144
- "flex w-full min-w-0 max-w-full items-center justify-between gap-2 rounded-lg border bg-bg-inset text-left",
1145
- "border-border-default text-text-primary outline-none transition-colors",
1146
- // The focused border survives a hover: hover alone strengthens the
1147
- // hairline, but hover while focused must not grey the focus colour —
1148
- // the stacked variant outranks plain hover by specificity.
1149
- "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",
1150
- "focus-visible:border-border-focus aria-expanded:border-border-focus",
1151
- "signal:transition-shadow signal:focus-visible:shadow-focus-ring",
1152
- size === "default" && "px-3 py-2 text-[14px] leading-[1.4] font-normal",
1153
- size === "small" && "h-6 px-2 text-[12px] leading-[1.4] font-normal",
1154
- className
1059
+ return /* @__PURE__ */ jsxs13(
1060
+ BaseSelect.Root,
1061
+ {
1062
+ value,
1063
+ onValueChange: (next) => onChange(next),
1064
+ disabled,
1065
+ modal: false,
1066
+ children: [
1067
+ /* @__PURE__ */ jsxs13(
1068
+ BaseSelect.Trigger,
1069
+ {
1070
+ "aria-label": ariaLabel,
1071
+ "aria-required": aria["aria-required"],
1072
+ className: cn(
1073
+ /*
1074
+ * `min-w-0 max-w-full`: a trigger must never outgrow its container
1075
+ * (Katerina, 2026-09-01 a long label stretched the files panel's
1076
+ * Lead row past its card). In a flex row the default min-width:auto
1077
+ * forbids shrinking below the label's width, which is what kept
1078
+ * `truncate` from ever engaging; in a block container max-w-full is
1079
+ * the cap. Full-width callers are unaffected.
1080
+ */
1081
+ "flex w-full min-w-0 max-w-full items-center justify-between gap-2 rounded-lg border bg-bg-inset text-left",
1082
+ "border-border-default text-text-primary outline-none transition-colors",
1083
+ // The focused border survives a hover: hover alone strengthens the
1084
+ // hairline, but hover while focused must not grey the focus colour —
1085
+ // the stacked variant outranks plain hover by specificity.
1086
+ "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",
1087
+ "focus-visible:border-border-focus aria-expanded:border-border-focus",
1088
+ "signal:transition-shadow signal:focus-visible:shadow-focus-ring",
1089
+ size === "default" && "px-3 py-2 text-[14px] leading-[1.4] font-normal",
1090
+ size === "small" && "h-6 px-2 text-[12px] leading-[1.4] font-normal",
1091
+ className
1092
+ ),
1093
+ children: [
1094
+ /* @__PURE__ */ jsxs13("span", { className: cn("flex min-w-0 items-center gap-2", !selected && "text-text-muted"), children: [
1095
+ selected?.leading && /* @__PURE__ */ jsx20("span", { className: "flex shrink-0 items-center", children: selected.leading }),
1096
+ /* @__PURE__ */ jsx20(BaseSelect.Value, { className: "truncate", children: () => selected?.label ?? placeholder })
1097
+ ] }),
1098
+ /* @__PURE__ */ jsx20(
1099
+ BaseSelect.Icon,
1100
+ {
1101
+ render: /* @__PURE__ */ jsx20(IconChevronDown2, { size: size === "small" ? 14 : 16, stroke: 1.5, className: "shrink-0 text-text-secondary" })
1102
+ }
1103
+ )
1104
+ ]
1105
+ }
1155
1106
  ),
1156
- children: [
1157
- /* @__PURE__ */ jsxs12("span", { className: cn("flex min-w-0 items-center gap-2", !selected && "text-text-muted"), children: [
1158
- selected?.leading && /* @__PURE__ */ jsx20("span", { className: "flex shrink-0 items-center", children: selected.leading }),
1159
- /* @__PURE__ */ jsx20("span", { className: "truncate", children: selected?.label ?? placeholder })
1160
- ] }),
1161
- /* @__PURE__ */ jsx20(IconChevronDown2, { size: size === "small" ? 14 : 16, stroke: 1.5, className: "shrink-0 text-text-secondary" })
1162
- ]
1163
- }
1164
- ),
1165
- open && rect && createPortal4(
1166
- /* @__PURE__ */ jsx20(
1167
- "div",
1168
- {
1169
- ref: menuRef,
1170
- role: "listbox",
1171
- "aria-activedescendant": `${id}-${activeIndex}`,
1172
- tabIndex: -1,
1173
- style: placement ? { ...placement, minWidth: rect.width } : (
1174
- // Provisional frame: measured by the layout effect above,
1175
- // replaced before paint. Hidden so a cut-off position is
1176
- // never visible, not even for a frame.
1177
- { top: rect.bottom + 4, left: rect.left, minWidth: rect.width, visibility: "hidden" }
1178
- ),
1179
- className: "fixed z-50 overflow-y-auto rounded-lg border border-border-default bg-bg-elevated p-1 shadow-lg",
1180
- children: options.map((option, index) => /* @__PURE__ */ jsxs12(
1181
- "div",
1182
- {
1183
- id: `${id}-${index}`,
1184
- role: "option",
1185
- "aria-selected": option.value === value,
1186
- onMouseEnter: () => setActiveIndex(index),
1187
- onMouseDown: (e) => {
1188
- e.preventDefault();
1189
- pick(index);
1190
- },
1191
- className: cn(
1192
- "flex h-9 cursor-pointer items-center justify-between gap-2 rounded-lg px-3 transition-colors",
1193
- "text-[14px] font-normal leading-[1.4] text-text-primary",
1194
- index === activeIndex && "bg-bg-hover",
1195
- option.value === value && "font-medium"
1196
- ),
1197
- children: [
1198
- /* @__PURE__ */ jsxs12("span", { className: "flex min-w-0 items-center gap-2", children: [
1199
- option.leading && /* @__PURE__ */ jsx20("span", { className: "flex shrink-0 items-center", children: option.leading }),
1200
- /* @__PURE__ */ jsx20("span", { className: "truncate", children: option.label })
1201
- ] }),
1202
- option.value === value && /* @__PURE__ */ jsx20(IconCheck2, { size: 16, stroke: 1.5, className: "shrink-0 text-text-secondary" })
1203
- ]
1204
- },
1205
- option.value
1206
- ))
1207
- }
1208
- ),
1209
- document.body
1210
- )
1211
- ] });
1107
+ /* @__PURE__ */ jsx20(BaseSelect.Portal, { children: /* @__PURE__ */ jsx20(
1108
+ BaseSelect.Positioner,
1109
+ {
1110
+ side: "bottom",
1111
+ align: "start",
1112
+ sideOffset: GAP4,
1113
+ collisionPadding: VIEWPORT_PAD3,
1114
+ alignItemWithTrigger: false,
1115
+ className: "z-50 data-[anchor-hidden]:hidden",
1116
+ children: /* @__PURE__ */ jsx20(
1117
+ BaseSelect.Popup,
1118
+ {
1119
+ className: "max-h-[min(288px,var(--available-height))] min-w-[var(--anchor-width)] overflow-y-auto rounded-lg border border-border-default bg-bg-elevated p-1 shadow-lg",
1120
+ children: options.map((option) => /* @__PURE__ */ jsxs13(
1121
+ BaseSelect.Item,
1122
+ {
1123
+ value: option.value,
1124
+ className: "flex h-9 cursor-pointer items-center justify-between gap-2 rounded-lg px-3 text-[14px] font-normal leading-[1.4] text-text-primary transition-colors data-[highlighted]:bg-bg-hover data-[selected]:font-medium",
1125
+ children: [
1126
+ /* @__PURE__ */ jsxs13("span", { className: "flex min-w-0 items-center gap-2", children: [
1127
+ option.leading && /* @__PURE__ */ jsx20("span", { className: "flex shrink-0 items-center", children: option.leading }),
1128
+ /* @__PURE__ */ jsx20(BaseSelect.ItemText, { className: "truncate", children: option.label })
1129
+ ] }),
1130
+ /* @__PURE__ */ jsx20(
1131
+ BaseSelect.ItemIndicator,
1132
+ {
1133
+ render: /* @__PURE__ */ jsx20(IconCheck2, { size: 16, stroke: 1.5, className: "shrink-0 text-text-secondary" })
1134
+ }
1135
+ )
1136
+ ]
1137
+ },
1138
+ option.value
1139
+ ))
1140
+ }
1141
+ )
1142
+ }
1143
+ ) })
1144
+ ]
1145
+ }
1146
+ );
1212
1147
  }
1213
1148
 
1214
1149
  // src/TextInput.tsx
1215
1150
  import { forwardRef } from "react";
1151
+ import { Input } from "@base-ui/react/input";
1216
1152
  import { jsx as jsx21 } from "react/jsx-runtime";
1217
- var TextInput = forwardRef(function TextInput2({ className, type = "text", id, ...props }, ref) {
1218
- const controlId = useFieldControlId(id);
1153
+ var TextInput = forwardRef(function TextInput2({ className, type = "text", ...props }, ref) {
1219
1154
  return /* @__PURE__ */ jsx21(
1220
- "input",
1155
+ Input,
1221
1156
  {
1222
1157
  ref,
1223
- id: controlId,
1224
1158
  type,
1225
1159
  className: cn(
1226
1160
  "bg-bg-inset border border-border-default focus:border-border-focus rounded-lg px-3 py-2",
@@ -1237,14 +1171,14 @@ var TextInput = forwardRef(function TextInput2({ className, type = "text", id, .
1237
1171
 
1238
1172
  // src/Textarea.tsx
1239
1173
  import { forwardRef as forwardRef2 } from "react";
1174
+ import { Field as BaseField2 } from "@base-ui/react/field";
1240
1175
  import { jsx as jsx22 } from "react/jsx-runtime";
1241
- var Textarea = forwardRef2(function Textarea2({ className, id, ...props }, ref) {
1242
- const controlId = useFieldControlId(id);
1176
+ var Textarea = forwardRef2(function Textarea2({ className, ...props }, ref) {
1243
1177
  return /* @__PURE__ */ jsx22(
1244
- "textarea",
1178
+ BaseField2.Control,
1245
1179
  {
1246
1180
  ref,
1247
- id: controlId,
1181
+ render: /* @__PURE__ */ jsx22("textarea", {}),
1248
1182
  className: cn(
1249
1183
  "bg-bg-inset border border-border-default focus:border-border-focus rounded-lg px-3 py-2",
1250
1184
  "text-[14px] leading-[1.4] font-normal text-text-primary placeholder:text-text-muted",
@@ -1259,51 +1193,58 @@ var Textarea = forwardRef2(function Textarea2({ className, id, ...props }, ref)
1259
1193
  });
1260
1194
 
1261
1195
  // src/ConfirmDialog.tsx
1262
- import { useState as useState7 } from "react";
1196
+ import { useState as useState3 } from "react";
1263
1197
 
1264
1198
  // src/DialogShell.tsx
1265
- import { useEffect as useEffect4 } from "react";
1266
- import { createPortal as createPortal5 } from "react-dom";
1267
- import { IconX as IconX2 } from "@tabler/icons-react";
1268
- import { Fragment as Fragment3, jsx as jsx23, jsxs as jsxs13 } from "react/jsx-runtime";
1269
- function DialogShell({ title, onClose, headerContent, footer, children, bodyClassName, width = 502 }) {
1270
- useEffect4(() => {
1271
- const onKey = (event) => {
1272
- if (event.key === "Escape") onClose();
1273
- };
1274
- document.addEventListener("keydown", onKey);
1275
- return () => document.removeEventListener("keydown", onKey);
1276
- }, [onClose]);
1277
- return createPortal5(
1278
- /* @__PURE__ */ jsxs13(Fragment3, { children: [
1279
- /* @__PURE__ */ jsx23("div", { className: "fixed inset-0 z-40 bg-scrim", onClick: onClose }),
1280
- /* @__PURE__ */ jsx23("div", { className: "fixed inset-0 z-50 flex items-center justify-center pointer-events-none", children: /* @__PURE__ */ jsxs13(
1281
- "div",
1282
- {
1283
- role: "dialog",
1284
- "aria-modal": "true",
1285
- "aria-label": title,
1286
- className: "bg-bg-elevated border border-border-subtle rounded-lg shadow-lg pointer-events-auto flex flex-col overflow-hidden",
1287
- style: { width },
1288
- children: [
1289
- /* @__PURE__ */ jsxs13("div", { className: "h-12 flex items-center justify-between pl-5 pr-4 border-b border-border-subtle shrink-0", children: [
1290
- headerContent ?? /* @__PURE__ */ jsx23("span", { className: "text-h4 text-text-primary", children: title }),
1291
- /* @__PURE__ */ jsx23(IconButton, { tooltip: "Close", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx23(IconX2, { size: 16, stroke: 1.5 }) })
1292
- ] }),
1293
- /* @__PURE__ */ jsx23("div", { className: cn("pl-5 pr-4 py-4", footer != null && "border-b border-border-subtle", bodyClassName), children }),
1294
- footer != null && /* @__PURE__ */ jsx23("div", { className: "h-12 flex items-center justify-end gap-2 pl-5 pr-4 shrink-0", children: footer })
1295
- ]
1296
- }
1297
- ) })
1298
- ] }),
1299
- document.body
1199
+ import { useRef as useRef3 } from "react";
1200
+ import { Dialog } from "@base-ui/react/dialog";
1201
+ import { AlertDialog } from "@base-ui/react/alert-dialog";
1202
+ import { IconX as IconX3 } from "@tabler/icons-react";
1203
+ import { jsx as jsx23, jsxs as jsxs14 } from "react/jsx-runtime";
1204
+ function DialogShell({ title, onClose, headerContent, footer, children, bodyClassName, width = 502, alert = false }) {
1205
+ const Parts = alert ? AlertDialog : Dialog;
1206
+ const popupRef = useRef3(null);
1207
+ return /* @__PURE__ */ jsx23(
1208
+ Parts.Root,
1209
+ {
1210
+ open: true,
1211
+ onOpenChange: (open) => {
1212
+ if (!open) onClose();
1213
+ },
1214
+ children: /* @__PURE__ */ jsxs14(Parts.Portal, { children: [
1215
+ /* @__PURE__ */ jsx23(Parts.Backdrop, { className: "fixed inset-0 z-40 bg-scrim" }),
1216
+ /* @__PURE__ */ jsx23("div", { className: "fixed inset-0 z-50 flex items-center justify-center pointer-events-none", children: /* @__PURE__ */ jsxs14(
1217
+ Parts.Popup,
1218
+ {
1219
+ ref: popupRef,
1220
+ initialFocus: popupRef,
1221
+ "aria-label": headerContent != null ? title : void 0,
1222
+ className: "bg-bg-elevated border border-border-subtle rounded-lg shadow-lg pointer-events-auto flex flex-col overflow-hidden outline-none",
1223
+ style: { width },
1224
+ children: [
1225
+ /* @__PURE__ */ jsxs14("div", { className: "h-12 flex items-center justify-between pl-5 pr-4 border-b border-border-subtle shrink-0", children: [
1226
+ headerContent ?? /* @__PURE__ */ jsx23(Parts.Title, { className: "text-h4 text-text-primary", render: /* @__PURE__ */ jsx23("span", {}), children: title }),
1227
+ /* @__PURE__ */ jsx23(
1228
+ Parts.Close,
1229
+ {
1230
+ render: /* @__PURE__ */ jsx23(IconButton, { tooltip: "Close", "aria-label": "Close", children: /* @__PURE__ */ jsx23(IconX3, { size: 16, stroke: 1.5 }) })
1231
+ }
1232
+ )
1233
+ ] }),
1234
+ /* @__PURE__ */ jsx23("div", { className: cn("pl-5 pr-4 py-4", footer != null && "border-b border-border-subtle", bodyClassName), children }),
1235
+ footer != null && /* @__PURE__ */ jsx23("div", { className: "h-12 flex items-center justify-end gap-2 pl-5 pr-4 shrink-0", children: footer })
1236
+ ]
1237
+ }
1238
+ ) })
1239
+ ] })
1240
+ }
1300
1241
  );
1301
1242
  }
1302
1243
 
1303
1244
  // src/ConfirmDialog.tsx
1304
- import { Fragment as Fragment4, jsx as jsx24, jsxs as jsxs14 } from "react/jsx-runtime";
1245
+ import { Fragment as Fragment3, jsx as jsx24, jsxs as jsxs15 } from "react/jsx-runtime";
1305
1246
  function ConfirmDialog({ title, children, confirmLabel, destructive = false, onConfirm, onClose }) {
1306
- const [busy, setBusy] = useState7(false);
1247
+ const [busy, setBusy] = useState3(false);
1307
1248
  const confirm = async () => {
1308
1249
  setBusy(true);
1309
1250
  try {
@@ -1316,10 +1257,11 @@ function ConfirmDialog({ title, children, confirmLabel, destructive = false, onC
1316
1257
  return /* @__PURE__ */ jsx24(
1317
1258
  DialogShell,
1318
1259
  {
1260
+ alert: true,
1319
1261
  title,
1320
1262
  onClose,
1321
1263
  bodyClassName: "flex flex-col gap-3 text-body-2 text-text-primary",
1322
- footer: /* @__PURE__ */ jsxs14(Fragment4, { children: [
1264
+ footer: /* @__PURE__ */ jsxs15(Fragment3, { children: [
1323
1265
  /* @__PURE__ */ jsx24(Button, { variant: "muted", onClick: onClose, disabled: busy, children: "Cancel" }),
1324
1266
  /* @__PURE__ */ jsx24(Button, { variant: destructive ? "destructive" : "primary", onClick: () => void confirm(), disabled: busy, children: confirmLabel })
1325
1267
  ] }),
@@ -1329,14 +1271,16 @@ function ConfirmDialog({ title, children, confirmLabel, destructive = false, onC
1329
1271
  }
1330
1272
 
1331
1273
  // src/EditableText.tsx
1332
- import { useEffect as useEffect5, useRef as useRef6, useState as useState8 } from "react";
1274
+ import { useEffect as useEffect2, useRef as useRef4, useState as useState4 } from "react";
1275
+ import { Field as BaseField3 } from "@base-ui/react/field";
1276
+ import { Input as Input2 } from "@base-ui/react/input";
1333
1277
  import { jsx as jsx25 } from "react/jsx-runtime";
1334
1278
  function EditableText({ value, display, displayNode, placeholder, onCommit, multiline = false, className, label, readOnly = false }) {
1335
- const [editing, setEditing] = useState8(false);
1336
- const [draft, setDraft] = useState8(value);
1337
- const [busy, setBusy] = useState8(false);
1338
- const fieldRef = useRef6(null);
1339
- useEffect5(() => {
1279
+ const [editing, setEditing] = useState4(false);
1280
+ const [draft, setDraft] = useState4(value);
1281
+ const [busy, setBusy] = useState4(false);
1282
+ const fieldRef = useRef4(null);
1283
+ useEffect2(() => {
1340
1284
  if (editing) {
1341
1285
  fieldRef.current?.focus();
1342
1286
  fieldRef.current?.select();
@@ -1381,23 +1325,30 @@ function EditableText({ value, display, displayNode, placeholder, onCommit, mult
1381
1325
  className
1382
1326
  );
1383
1327
  if (readOnly) {
1384
- return /* @__PURE__ */ jsx25(
1385
- "div",
1386
- {
1387
- "aria-label": label,
1388
- className: cn("w-full px-2 py-1", multiline && !displayNode && "whitespace-pre-wrap", display ?? value ? "text-text-primary" : "text-text-muted", className),
1389
- children: display ?? value ? displayNode ?? (display ?? value) : placeholder
1390
- }
1328
+ return (
1329
+ /*
1330
+ No `aria-label` here. It sat on a bare `<div>`, which has no role, and
1331
+ ARIA does not let an author name a generic element — so the name was
1332
+ written and never read. Read-only, this is a line of text: whatever
1333
+ names the region around it (a `Field`, a `Property`) names this too.
1334
+ */
1335
+ /* @__PURE__ */ jsx25(
1336
+ "div",
1337
+ {
1338
+ className: cn("w-full px-2 py-1", multiline && !displayNode && "whitespace-pre-wrap", display ?? value ? "text-text-primary" : "text-text-muted", className),
1339
+ children: display ?? value ? displayNode ?? (display ?? value) : placeholder
1340
+ }
1341
+ )
1391
1342
  );
1392
1343
  }
1393
1344
  if (editing) {
1394
1345
  return multiline ? /* @__PURE__ */ jsx25(
1395
- "textarea",
1346
+ BaseField3.Control,
1396
1347
  {
1397
1348
  ref: fieldRef,
1349
+ render: /* @__PURE__ */ jsx25("textarea", { rows: 4 }),
1398
1350
  "aria-label": label,
1399
1351
  value: draft,
1400
- rows: 4,
1401
1352
  disabled: busy,
1402
1353
  onChange: (event) => setDraft(event.target.value),
1403
1354
  onKeyDown,
@@ -1405,7 +1356,7 @@ function EditableText({ value, display, displayNode, placeholder, onCommit, mult
1405
1356
  className: fieldClass
1406
1357
  }
1407
1358
  ) : /* @__PURE__ */ jsx25(
1408
- "input",
1359
+ Input2,
1409
1360
  {
1410
1361
  ref: fieldRef,
1411
1362
  "aria-label": label,
@@ -1423,7 +1374,6 @@ function EditableText({ value, display, displayNode, placeholder, onCommit, mult
1423
1374
  {
1424
1375
  type: "button",
1425
1376
  onClick: open,
1426
- title: "Click to edit",
1427
1377
  "aria-label": `Edit ${label.toLowerCase()}`,
1428
1378
  className: cn(
1429
1379
  "w-full rounded-md border border-transparent px-2 py-1 text-left transition-colors hover:border-border-default",
@@ -1438,22 +1388,22 @@ function EditableText({ value, display, displayNode, placeholder, onCommit, mult
1438
1388
 
1439
1389
  // src/EmptyState.tsx
1440
1390
  import { IconMessage2 } from "@tabler/icons-react";
1441
- import { jsx as jsx26, jsxs as jsxs15 } from "react/jsx-runtime";
1391
+ import { jsx as jsx26, jsxs as jsxs16 } from "react/jsx-runtime";
1442
1392
  function EmptyState({ icon, message, className }) {
1443
- return /* @__PURE__ */ jsxs15("div", { className: cn("flex flex-col gap-2 items-center", className), children: [
1393
+ return /* @__PURE__ */ jsxs16("div", { className: cn("flex flex-col gap-2 items-center", className), children: [
1444
1394
  /* @__PURE__ */ jsx26("span", { className: "text-text-secondary", children: icon ?? /* @__PURE__ */ jsx26(IconMessage2, { size: 16, stroke: 1.5 }) }),
1445
1395
  /* @__PURE__ */ jsx26("p", { className: "text-body-2 text-text-secondary text-center", children: message })
1446
1396
  ] });
1447
1397
  }
1448
1398
 
1449
1399
  // src/Skeleton.tsx
1450
- import { jsx as jsx27, jsxs as jsxs16 } from "react/jsx-runtime";
1400
+ import { jsx as jsx27, jsxs as jsxs17 } from "react/jsx-runtime";
1451
1401
  function SkeletonBar({ className, style }) {
1452
1402
  return /* @__PURE__ */ jsx27("div", { className: cn("bg-bg-inset rounded-sm animate-pulse", className), style });
1453
1403
  }
1454
1404
  var ROW_BAR_WIDTHS = [150, 100, 170, 120, 90, 140, 110, 160];
1455
1405
  function SkeletonRow({ barWidth = 130 }) {
1456
- return /* @__PURE__ */ jsxs16("div", { className: "flex items-center gap-2 px-2 h-[32px] rounded-lg", children: [
1406
+ return /* @__PURE__ */ jsxs17("div", { className: "flex items-center gap-2 px-2 h-[32px] rounded-lg", children: [
1457
1407
  /* @__PURE__ */ jsx27(SkeletonBar, { className: "w-4 h-4 shrink-0" }),
1458
1408
  /* @__PURE__ */ jsx27(SkeletonBar, { className: "h-3.5", style: { width: barWidth } })
1459
1409
  ] });
@@ -1463,20 +1413,20 @@ function SkeletonList({ rows = 8, className }) {
1463
1413
  }
1464
1414
 
1465
1415
  // src/Breadcrumb.tsx
1466
- import { Fragment as Fragment5, useCallback as useCallback4, useLayoutEffect as useLayoutEffect5, useRef as useRef7, useState as useState9 } from "react";
1467
- import { jsx as jsx28, jsxs as jsxs17 } from "react/jsx-runtime";
1416
+ import { Fragment as Fragment4, useCallback as useCallback2, useLayoutEffect as useLayoutEffect2, useRef as useRef5, useState as useState5 } from "react";
1417
+ import { jsx as jsx28, jsxs as jsxs18 } from "react/jsx-runtime";
1468
1418
  function Breadcrumb({ items, className }) {
1469
- const navRef = useRef7(null);
1470
- const labelRefs = useRef7([]);
1471
- const [truncated, setTruncated] = useState9(/* @__PURE__ */ new Set());
1472
- const measure = useCallback4(() => {
1419
+ const navRef = useRef5(null);
1420
+ const labelRefs = useRef5([]);
1421
+ const [truncated, setTruncated] = useState5(/* @__PURE__ */ new Set());
1422
+ const measure = useCallback2(() => {
1473
1423
  const next = /* @__PURE__ */ new Set();
1474
1424
  labelRefs.current.forEach((el, index) => {
1475
1425
  if (el && el.scrollWidth > el.clientWidth) next.add(index);
1476
1426
  });
1477
1427
  setTruncated((prev) => prev.size === next.size && [...next].every((i) => prev.has(i)) ? prev : next);
1478
1428
  }, []);
1479
- useLayoutEffect5(() => {
1429
+ useLayoutEffect2(() => {
1480
1430
  measure();
1481
1431
  const nav = navRef.current;
1482
1432
  if (!nav || typeof ResizeObserver === "undefined") return;
@@ -1497,8 +1447,8 @@ function Breadcrumb({ items, className }) {
1497
1447
  const setLabelRef = (el) => {
1498
1448
  labelRefs.current[index] = el;
1499
1449
  };
1500
- const crumb = item.href ? /* @__PURE__ */ jsx28("a", { ref: setLabelRef, href: item.href, className: cn(text, "hover:text-text-primary"), children: item.label }) : /* @__PURE__ */ jsx28("span", { ref: setLabelRef, className: text, "aria-current": last ? "page" : void 0, children: item.label });
1501
- return /* @__PURE__ */ jsxs17(Fragment5, { children: [
1450
+ const crumb = item.href ? /* @__PURE__ */ jsx28("a", { ref: setLabelRef, href: item.href, onClick: item.onClick, className: cn(text, "hover:text-text-primary"), children: item.label }) : /* @__PURE__ */ jsx28("span", { ref: setLabelRef, className: text, "aria-current": last ? "page" : void 0, children: item.label });
1451
+ return /* @__PURE__ */ jsxs18(Fragment4, { children: [
1502
1452
  index > 0 && /* @__PURE__ */ jsx28("span", { "aria-hidden": "true", className: "shrink-0 text-text-muted", children: "/" }),
1503
1453
  truncated.has(index) ? (
1504
1454
  // `min-w-0 shrink` undoes the wrapper's own shrink-0 — the crumb
@@ -1511,9 +1461,9 @@ function Breadcrumb({ items, className }) {
1511
1461
  }
1512
1462
 
1513
1463
  // src/NavItem.tsx
1514
- import { jsx as jsx29, jsxs as jsxs18 } from "react/jsx-runtime";
1464
+ import { jsx as jsx29, jsxs as jsxs19 } from "react/jsx-runtime";
1515
1465
  function NavItem({ label, href, count, countLabel, active = false, icon, className, ...props }) {
1516
- return /* @__PURE__ */ jsxs18(
1466
+ return /* @__PURE__ */ jsxs19(
1517
1467
  "a",
1518
1468
  {
1519
1469
  href,
@@ -1536,16 +1486,171 @@ function NavItem({ label, href, count, countLabel, active = false, icon, classNa
1536
1486
  );
1537
1487
  }
1538
1488
 
1489
+ // src/Popover.tsx
1490
+ import { useMemo as useMemo2 } from "react";
1491
+ import { Popover as BasePopover } from "@base-ui/react/popover";
1492
+ import { jsx as jsx30, jsxs as jsxs20 } from "react/jsx-runtime";
1493
+ var GAP5 = 4;
1494
+ var VIEWPORT_PAD4 = 8;
1495
+ function Popover({ trigger, anchor, align = "left", side = "bottom", open, onOpenChange, finalFocus, actionsRef, ariaLabel, children, className }) {
1496
+ const anchorTarget = useMemo2(() => {
1497
+ if (!anchor) return void 0;
1498
+ if (anchor instanceof Element) return anchor;
1499
+ return { getBoundingClientRect: () => anchor };
1500
+ }, [anchor]);
1501
+ return /* @__PURE__ */ jsxs20(
1502
+ BasePopover.Root,
1503
+ {
1504
+ open,
1505
+ onOpenChange: onOpenChange ? (next) => onOpenChange(next) : void 0,
1506
+ actionsRef,
1507
+ modal: false,
1508
+ children: [
1509
+ trigger && /* @__PURE__ */ jsx30(BasePopover.Trigger, { render: trigger }),
1510
+ /* @__PURE__ */ jsx30(BasePopover.Portal, { children: /* @__PURE__ */ jsx30(
1511
+ BasePopover.Positioner,
1512
+ {
1513
+ anchor: anchorTarget,
1514
+ side,
1515
+ align: align === "right" ? "end" : "start",
1516
+ sideOffset: GAP5,
1517
+ collisionPadding: VIEWPORT_PAD4,
1518
+ className: "z-50 data-[anchor-hidden]:hidden",
1519
+ children: /* @__PURE__ */ jsx30(
1520
+ BasePopover.Popup,
1521
+ {
1522
+ "aria-label": ariaLabel,
1523
+ initialFocus: trigger ? void 0 : false,
1524
+ finalFocus,
1525
+ className: cn("min-w-[180px] max-h-[var(--available-height)] overflow-y-auto outline-none", className),
1526
+ render: /* @__PURE__ */ jsx30(MenuPanel, {}),
1527
+ children
1528
+ }
1529
+ )
1530
+ }
1531
+ ) })
1532
+ ]
1533
+ }
1534
+ );
1535
+ }
1536
+
1537
+ // src/Toolbar.tsx
1538
+ import { Toolbar as BaseToolbar } from "@base-ui/react/toolbar";
1539
+ import { jsx as jsx31 } from "react/jsx-runtime";
1540
+ function Toolbar({ "aria-label": ariaLabel, orientation = "horizontal", loopFocus = true, surface = true, children, className }) {
1541
+ const strip = /* @__PURE__ */ jsx31(
1542
+ BaseToolbar.Root,
1543
+ {
1544
+ "aria-label": ariaLabel,
1545
+ orientation,
1546
+ loopFocus,
1547
+ className: cn("flex items-center gap-1", orientation === "vertical" && "flex-col", className),
1548
+ children
1549
+ }
1550
+ );
1551
+ if (!surface) return strip;
1552
+ return /* @__PURE__ */ jsx31(MenuPanel, { className: "w-fit p-1", children: strip });
1553
+ }
1554
+ function ToolbarButton({ ref, disabled, disabledReason, ...props }) {
1555
+ return /* @__PURE__ */ jsx31(
1556
+ BaseToolbar.Button,
1557
+ {
1558
+ ref,
1559
+ disabled: disabled || !!disabledReason,
1560
+ focusableWhenDisabled: true,
1561
+ render: /* @__PURE__ */ jsx31(IconButton, { disabled, disabledReason, ...props })
1562
+ }
1563
+ );
1564
+ }
1565
+ function ToolbarInput(props) {
1566
+ return /* @__PURE__ */ jsx31(BaseToolbar.Input, { render: /* @__PURE__ */ jsx31(TextInput, {}), ...props });
1567
+ }
1568
+ function ToolbarSeparator({ className }) {
1569
+ return /* @__PURE__ */ jsx31(
1570
+ BaseToolbar.Separator,
1571
+ {
1572
+ orientation: "vertical",
1573
+ className: cn("mx-1 h-4 w-px shrink-0 bg-border-default", className)
1574
+ }
1575
+ );
1576
+ }
1577
+
1578
+ // src/ReactionPicker.tsx
1579
+ import { jsx as jsx32 } from "react/jsx-runtime";
1580
+ function ReactionPicker({ options, onSelect, "aria-label": ariaLabel = "Reactions", surface = true, className }) {
1581
+ return /* @__PURE__ */ jsx32(Toolbar, { "aria-label": ariaLabel, surface, className: cn("gap-0.5", className), children: options.map((option) => (
1582
+ /*
1583
+ A `ToolbarButton`, not a button in a tooltip wrapper. The wrapper
1584
+ would become the toolbar's item and the button inside it would never
1585
+ join the walk — and it is unnecessary since stage 4, because an
1586
+ `IconButton` with a `tooltip` IS the trigger.
1587
+
1588
+ The tooltip names it for a pointer, `aria-label` for everything else,
1589
+ and both say the meaning rather than the glyph.
1590
+ */
1591
+ /* @__PURE__ */ jsx32(
1592
+ ToolbarButton,
1593
+ {
1594
+ "aria-label": option.label,
1595
+ tooltip: option.label,
1596
+ onClick: () => onSelect(option.emoji),
1597
+ className: "size-7 text-[18px] leading-none",
1598
+ children: /* @__PURE__ */ jsx32("span", { "aria-hidden": "true", children: option.emoji })
1599
+ },
1600
+ option.emoji
1601
+ )
1602
+ )) });
1603
+ }
1604
+
1605
+ // src/PreviewCard.tsx
1606
+ import { PreviewCard as BasePreviewCard } from "@base-ui/react/preview-card";
1607
+ import { jsx as jsx33, jsxs as jsxs21 } from "react/jsx-runtime";
1608
+ var GAP6 = 12;
1609
+ var VIEWPORT_PAD5 = 8;
1610
+ var OPEN_DELAY2 = 350;
1611
+ var CLOSE_DELAY = 200;
1612
+ function PreviewCard({ content, children, side = "right", delay = OPEN_DELAY2, closeDelay = CLOSE_DELAY, className, wrapperClassName }) {
1613
+ return /* @__PURE__ */ jsxs21(BasePreviewCard.Root, { children: [
1614
+ /* @__PURE__ */ jsx33(
1615
+ BasePreviewCard.Trigger,
1616
+ {
1617
+ delay,
1618
+ closeDelay,
1619
+ render: /* @__PURE__ */ jsx33("span", { className: cn("inline-flex", wrapperClassName) }),
1620
+ children
1621
+ }
1622
+ ),
1623
+ /* @__PURE__ */ jsx33(BasePreviewCard.Portal, { children: /* @__PURE__ */ jsx33(
1624
+ BasePreviewCard.Positioner,
1625
+ {
1626
+ side,
1627
+ align: "start",
1628
+ sideOffset: GAP6,
1629
+ collisionPadding: VIEWPORT_PAD5,
1630
+ className: "z-50 data-[anchor-hidden]:hidden",
1631
+ children: /* @__PURE__ */ jsx33(
1632
+ BasePreviewCard.Popup,
1633
+ {
1634
+ className: cn("w-[360px] max-h-[min(300px,var(--available-height))] gap-3 overflow-y-auto p-3 outline-none [&>*]:shrink-0", className),
1635
+ render: /* @__PURE__ */ jsx33(MenuPanel, {}),
1636
+ children: content
1637
+ }
1638
+ )
1639
+ }
1640
+ ) })
1641
+ ] });
1642
+ }
1643
+
1539
1644
  // src/Rail.tsx
1540
- import { jsx as jsx30 } from "react/jsx-runtime";
1645
+ import { jsx as jsx34 } from "react/jsx-runtime";
1541
1646
  function Rail({ "aria-label": ariaLabel = "Navigation", children, className }) {
1542
- return /* @__PURE__ */ jsx30("nav", { "aria-label": ariaLabel, className: cn("flex w-16 shrink-0 flex-col items-start gap-2 px-2 py-3", className), children });
1647
+ return /* @__PURE__ */ jsx34("nav", { "aria-label": ariaLabel, className: cn("flex w-16 shrink-0 flex-col items-start gap-2 px-2 py-3", className), children });
1543
1648
  }
1544
1649
 
1545
1650
  // src/RailItem.tsx
1546
- import { jsx as jsx31, jsxs as jsxs19 } from "react/jsx-runtime";
1651
+ import { jsx as jsx35, jsxs as jsxs22 } from "react/jsx-runtime";
1547
1652
  function RailItem({ href, icon, label, active = false, className, ...props }) {
1548
- return /* @__PURE__ */ jsxs19(
1653
+ return /* @__PURE__ */ jsxs22(
1549
1654
  "a",
1550
1655
  {
1551
1656
  href,
@@ -1553,14 +1658,14 @@ function RailItem({ href, icon, label, active = false, className, ...props }) {
1553
1658
  className: cn("group flex w-full shrink-0 flex-col items-center gap-0.5 px-2 py-0.5", className),
1554
1659
  ...props,
1555
1660
  children: [
1556
- /* @__PURE__ */ jsx31(
1661
+ /* @__PURE__ */ jsx35(
1557
1662
  "div",
1558
1663
  {
1559
1664
  className: cn(
1560
1665
  "flex items-center justify-center rounded-lg p-2 transition-colors",
1561
1666
  active ? "bg-bg-selected" : "group-hover:bg-bg-hover"
1562
1667
  ),
1563
- children: /* @__PURE__ */ jsx31(
1668
+ children: /* @__PURE__ */ jsx35(
1564
1669
  "span",
1565
1670
  {
1566
1671
  className: cn(
@@ -1572,16 +1677,16 @@ function RailItem({ href, icon, label, active = false, className, ...props }) {
1572
1677
  )
1573
1678
  }
1574
1679
  ),
1575
- /* @__PURE__ */ jsx31("span", { className: cn("text-center text-[9px] font-medium leading-[115%]", active ? "text-text-primary" : "text-text-secondary"), children: label })
1680
+ /* @__PURE__ */ jsx35("span", { className: cn("text-center text-[9px] font-medium leading-[115%]", active ? "text-text-primary" : "text-text-secondary"), children: label })
1576
1681
  ]
1577
1682
  }
1578
1683
  );
1579
1684
  }
1580
1685
 
1581
1686
  // src/Sidebar.tsx
1582
- import { jsx as jsx32 } from "react/jsx-runtime";
1687
+ import { jsx as jsx36 } from "react/jsx-runtime";
1583
1688
  function Sidebar({ "aria-label": ariaLabel = "Workspace", children, className }) {
1584
- return /* @__PURE__ */ jsx32(
1689
+ return /* @__PURE__ */ jsx36(
1585
1690
  "nav",
1586
1691
  {
1587
1692
  "aria-label": ariaLabel,
@@ -1595,29 +1700,29 @@ function Sidebar({ "aria-label": ariaLabel = "Workspace", children, className })
1595
1700
  }
1596
1701
 
1597
1702
  // src/Property.tsx
1598
- import { jsx as jsx33, jsxs as jsxs20 } from "react/jsx-runtime";
1703
+ import { jsx as jsx37, jsxs as jsxs23 } from "react/jsx-runtime";
1599
1704
  function Property({ label, layout = "row", children, className }) {
1600
1705
  if (layout === "stacked") {
1601
1706
  return (
1602
1707
  // gap-3 — the one label-above-content distance (Katerina, 2026-09-01):
1603
1708
  // Peek's topic-details sections already sit at 12px, Ship's rail was
1604
1709
  // 6px, and unifying means the bigger, calmer one.
1605
- /* @__PURE__ */ jsxs20("div", { className: cn("flex flex-col gap-3", className), children: [
1606
- /* @__PURE__ */ jsx33("span", { className: "text-menu uppercase tracking-[0.08em] text-text-secondary", children: label }),
1710
+ /* @__PURE__ */ jsxs23("div", { className: cn("flex flex-col gap-3", className), children: [
1711
+ /* @__PURE__ */ jsx37("span", { className: "text-menu uppercase tracking-[0.08em] text-text-secondary", children: label }),
1607
1712
  children
1608
1713
  ] })
1609
1714
  );
1610
1715
  }
1611
- return /* @__PURE__ */ jsxs20("div", { className: cn("flex items-center gap-2", className), children: [
1612
- /* @__PURE__ */ jsx33("span", { className: "w-[68px] shrink-0 text-caption text-text-secondary", children: label }),
1716
+ return /* @__PURE__ */ jsxs23("div", { className: cn("flex items-center gap-2", className), children: [
1717
+ /* @__PURE__ */ jsx37("span", { className: "w-[68px] shrink-0 text-caption text-text-secondary", children: label }),
1613
1718
  children
1614
1719
  ] });
1615
1720
  }
1616
1721
 
1617
1722
  // src/Reaction.tsx
1618
- import { jsx as jsx34, jsxs as jsxs21 } from "react/jsx-runtime";
1723
+ import { jsx as jsx38, jsxs as jsxs24 } from "react/jsx-runtime";
1619
1724
  function Reaction({ emoji, count, pressed = false, className, type, ...props }) {
1620
- return /* @__PURE__ */ jsxs21(
1725
+ return /* @__PURE__ */ jsxs24(
1621
1726
  "button",
1622
1727
  {
1623
1728
  type: type ?? "button",
@@ -1633,8 +1738,8 @@ function Reaction({ emoji, count, pressed = false, className, type, ...props })
1633
1738
  ),
1634
1739
  ...props,
1635
1740
  children: [
1636
- /* @__PURE__ */ jsx34("span", { "aria-hidden": "true", className: "shrink-0 text-[16px] leading-none", children: emoji }),
1637
- /* @__PURE__ */ jsx34("span", { className: "text-chip signal:font-mono signal:text-[10px] signal:font-semibold signal:tabular-nums", children: count })
1741
+ /* @__PURE__ */ jsx38("span", { "aria-hidden": "true", className: "shrink-0 text-[16px] leading-none", children: emoji }),
1742
+ /* @__PURE__ */ jsx38("span", { className: "text-chip signal:font-mono signal:text-[10px] signal:font-semibold signal:tabular-nums", children: count })
1638
1743
  ]
1639
1744
  }
1640
1745
  );
@@ -1642,9 +1747,10 @@ function Reaction({ emoji, count, pressed = false, className, type, ...props })
1642
1747
 
1643
1748
  // src/SearchInput.tsx
1644
1749
  import "react";
1645
- import { jsx as jsx35, jsxs as jsxs22 } from "react/jsx-runtime";
1750
+ import { Input as Input3 } from "@base-ui/react/input";
1751
+ import { jsx as jsx39, jsxs as jsxs25 } from "react/jsx-runtime";
1646
1752
  function SearchInput({ shortcut, className, placeholder = "Search\u2026", ...props }) {
1647
- return /* @__PURE__ */ jsxs22(
1753
+ return /* @__PURE__ */ jsxs25(
1648
1754
  "div",
1649
1755
  {
1650
1756
  className: cn(
@@ -1654,27 +1760,27 @@ function SearchInput({ shortcut, className, placeholder = "Search\u2026", ...pro
1654
1760
  className
1655
1761
  ),
1656
1762
  children: [
1657
- /* @__PURE__ */ jsx35(
1658
- "input",
1763
+ /* @__PURE__ */ jsx39(
1764
+ Input3,
1659
1765
  {
1660
1766
  className: "flex-1 min-w-0 bg-transparent text-input-value text-text-primary placeholder:text-text-muted outline-none",
1661
1767
  placeholder,
1662
1768
  ...props
1663
1769
  }
1664
1770
  ),
1665
- shortcut && /* @__PURE__ */ jsx35(Kbd, { children: shortcut })
1771
+ shortcut && /* @__PURE__ */ jsx39(Kbd, { children: shortcut })
1666
1772
  ]
1667
1773
  }
1668
1774
  );
1669
1775
  }
1670
1776
 
1671
1777
  // src/SectionHeader.tsx
1672
- import { useState as useState10 } from "react";
1778
+ import { useState as useState6 } from "react";
1673
1779
  import { IconChevronRight as IconChevronRight2 } from "@tabler/icons-react";
1674
- import { jsx as jsx36, jsxs as jsxs23 } from "react/jsx-runtime";
1780
+ import { jsx as jsx40, jsxs as jsxs26 } from "react/jsx-runtime";
1675
1781
  function SectionHeader({ title, chevron = false, isExpanded = true, onToggle, actions, showActions = "hover", className }) {
1676
- const [isHovered, setIsHovered] = useState10(false);
1677
- return /* @__PURE__ */ jsxs23(
1782
+ const [isHovered, setIsHovered] = useState6(false);
1783
+ return /* @__PURE__ */ jsxs26(
1678
1784
  "div",
1679
1785
  {
1680
1786
  className: cn(
@@ -1687,8 +1793,8 @@ function SectionHeader({ title, chevron = false, isExpanded = true, onToggle, ac
1687
1793
  onMouseEnter: () => setIsHovered(true),
1688
1794
  onMouseLeave: () => setIsHovered(false),
1689
1795
  children: [
1690
- /* @__PURE__ */ jsxs23("div", { className: "flex shrink-0 items-center gap-1", children: [
1691
- chevron && /* @__PURE__ */ jsx36(
1796
+ /* @__PURE__ */ jsxs26("div", { className: "flex shrink-0 items-center gap-1", children: [
1797
+ chevron && /* @__PURE__ */ jsx40(
1692
1798
  IconChevronRight2,
1693
1799
  {
1694
1800
  size: 12,
@@ -1696,9 +1802,9 @@ function SectionHeader({ title, chevron = false, isExpanded = true, onToggle, ac
1696
1802
  className: cn("text-text-secondary transition-transform duration-150", isExpanded && "rotate-90")
1697
1803
  }
1698
1804
  ),
1699
- /* @__PURE__ */ jsx36(SectionLabel, { children: title })
1805
+ /* @__PURE__ */ jsx40(SectionLabel, { children: title })
1700
1806
  ] }),
1701
- (showActions === "always" || isHovered) && actions && actions.length > 0 && /* @__PURE__ */ jsx36("div", { className: "flex items-center gap-1", children: actions.map((action) => /* @__PURE__ */ jsx36(
1807
+ (showActions === "always" || isHovered) && actions && actions.length > 0 && /* @__PURE__ */ jsx40("div", { className: "flex items-center gap-1", children: actions.map((action) => /* @__PURE__ */ jsx40(
1702
1808
  IconButton,
1703
1809
  {
1704
1810
  tooltip: action.tooltip,
@@ -1718,9 +1824,9 @@ function SectionHeader({ title, chevron = false, isExpanded = true, onToggle, ac
1718
1824
 
1719
1825
  // src/Tabs.tsx
1720
1826
  import { Tabs as BaseTabs } from "@base-ui/react/tabs";
1721
- import { jsx as jsx37, jsxs as jsxs24 } from "react/jsx-runtime";
1722
- function Tabs({ tabs, active, onChange, size = "default", className }) {
1723
- return /* @__PURE__ */ jsx37(
1827
+ import { jsx as jsx41, jsxs as jsxs27 } from "react/jsx-runtime";
1828
+ function Tabs({ tabs, active, onChange, size = "default", className, ...aria }) {
1829
+ return /* @__PURE__ */ jsx41(
1724
1830
  BaseTabs.Root,
1725
1831
  {
1726
1832
  value: active,
@@ -1728,36 +1834,45 @@ function Tabs({ tabs, active, onChange, size = "default", className }) {
1728
1834
  if (details.reason === "none") onChange(value);
1729
1835
  },
1730
1836
  className,
1731
- children: /* @__PURE__ */ jsx37(BaseTabs.List, { activateOnFocus: true, className: "flex items-center gap-2", children: tabs.map((tab) => /* @__PURE__ */ jsxs24(
1732
- BaseTabs.Tab,
1837
+ children: /* @__PURE__ */ jsx41(
1838
+ BaseTabs.List,
1733
1839
  {
1734
- value: tab.id,
1735
- className: (state) => cn(
1736
- "flex cursor-pointer items-center transition-colors",
1737
- // gap: default is Ship's 6px; small keeps Peek's original 4px, or
1738
- // "small is Peek's geometry" stops being true.
1739
- size === "default" ? "gap-1.5 rounded-md px-2 py-1 text-body-2" : "gap-1 rounded px-1.5 py-0.5 text-caption",
1740
- state.active ? "bg-bg-active text-text-primary" : "text-text-secondary hover:bg-bg-hover"
1741
- ),
1742
- children: [
1743
- tab.icon,
1744
- /* @__PURE__ */ jsxs24("span", { className: "flex items-baseline", children: [
1745
- tab.label,
1746
- tab.count !== void 0 ? /* @__PURE__ */ jsx37("span", { className: "ml-2.5 font-mono text-caption tabular-nums text-text-secondary", children: tab.count }) : null
1747
- ] })
1748
- ]
1749
- },
1750
- tab.id
1751
- )) })
1840
+ activateOnFocus: true,
1841
+ "aria-label": aria["aria-label"],
1842
+ "aria-labelledby": aria["aria-labelledby"],
1843
+ className: "flex items-center gap-2",
1844
+ children: tabs.map((tab) => /* @__PURE__ */ jsxs27(
1845
+ BaseTabs.Tab,
1846
+ {
1847
+ value: tab.id,
1848
+ className: (state) => cn(
1849
+ "flex cursor-pointer items-center transition-colors",
1850
+ // gap: default is Ship's 6px; small keeps Peek's original 4px, or
1851
+ // "small is Peek's geometry" stops being true.
1852
+ size === "default" ? "gap-1.5 rounded-md px-2 py-1 text-body-2" : "gap-1 rounded px-1.5 py-0.5 text-caption",
1853
+ state.active ? "bg-bg-active text-text-primary" : "text-text-secondary hover:bg-bg-hover"
1854
+ ),
1855
+ children: [
1856
+ tab.icon,
1857
+ /* @__PURE__ */ jsxs27("span", { className: "flex items-baseline", children: [
1858
+ tab.label,
1859
+ tab.count !== void 0 ? /* @__PURE__ */ jsx41("span", { className: "ml-2.5 font-mono text-caption tabular-nums text-text-secondary", children: tab.count }) : null
1860
+ ] })
1861
+ ]
1862
+ },
1863
+ tab.id
1864
+ ))
1865
+ }
1866
+ )
1752
1867
  }
1753
1868
  );
1754
1869
  }
1755
1870
 
1756
1871
  // src/Toast.tsx
1757
- import { createContext as createContext3, useCallback as useCallback5, useContext as useContext3, useEffect as useEffect6, useMemo as useMemo3, useState as useState11 } from "react";
1758
- import { createPortal as createPortal6 } from "react-dom";
1872
+ import { createContext as createContext2, useCallback as useCallback3, useContext as useContext2, useEffect as useEffect3, useMemo as useMemo3, useState as useState7 } from "react";
1873
+ import { createPortal as createPortal2 } from "react-dom";
1759
1874
  import { IconCircleCheck } from "@tabler/icons-react";
1760
- import { jsx as jsx38, jsxs as jsxs25 } from "react/jsx-runtime";
1875
+ import { jsx as jsx42, jsxs as jsxs28 } from "react/jsx-runtime";
1761
1876
  var SURFACE_STYLES = {
1762
1877
  success: "bg-success-muted signal:bg-bg-inset signal:border signal:border-border-default signal:shadow-[shadow:var(--shadow-md)]",
1763
1878
  brand: "bg-accent-muted signal:bg-bg-inset signal:border signal:border-border-default signal:shadow-[shadow:var(--shadow-md)]",
@@ -1775,7 +1890,7 @@ var ACTION_BORDER_STYLES = {
1775
1890
  };
1776
1891
  function Toast({ label, type = "neutral", leadingIcon = true, actionLabel, onAction, className }) {
1777
1892
  const hasAction = !!(actionLabel && onAction);
1778
- return /* @__PURE__ */ jsxs25(
1893
+ return /* @__PURE__ */ jsxs28(
1779
1894
  "div",
1780
1895
  {
1781
1896
  className: cn(
@@ -1787,11 +1902,11 @@ function Toast({ label, type = "neutral", leadingIcon = true, actionLabel, onAct
1787
1902
  className
1788
1903
  ),
1789
1904
  children: [
1790
- /* @__PURE__ */ jsxs25("div", { className: "flex items-center gap-2 shrink-0", children: [
1791
- leadingIcon && /* @__PURE__ */ jsx38(IconCircleCheck, { size: 16, stroke: 1.5, className: cn("text-text-primary shrink-0", ICON_STYLES[type]) }),
1792
- /* @__PURE__ */ jsx38("span", { className: "font-normal text-[14px] leading-[1.4] text-text-primary whitespace-nowrap", children: label })
1905
+ /* @__PURE__ */ jsxs28("div", { className: "flex items-center gap-2 shrink-0", children: [
1906
+ leadingIcon && /* @__PURE__ */ jsx42(IconCircleCheck, { size: 16, stroke: 1.5, className: cn("text-text-primary shrink-0", ICON_STYLES[type]) }),
1907
+ /* @__PURE__ */ jsx42("span", { className: "font-normal text-[14px] leading-[1.4] text-text-primary whitespace-nowrap", children: label })
1793
1908
  ] }),
1794
- hasAction && /* @__PURE__ */ jsx38(
1909
+ hasAction && /* @__PURE__ */ jsx42(
1795
1910
  "button",
1796
1911
  {
1797
1912
  type: "button",
@@ -1801,24 +1916,24 @@ function Toast({ label, type = "neutral", leadingIcon = true, actionLabel, onAct
1801
1916
  ACTION_BORDER_STYLES[type],
1802
1917
  type === "neutral" ? "hover:border-border-strong" : "hover:opacity-80"
1803
1918
  ),
1804
- children: /* @__PURE__ */ jsx38("span", { className: "font-medium text-[12px] leading-[12px] text-text-primary whitespace-nowrap", children: actionLabel })
1919
+ children: /* @__PURE__ */ jsx42("span", { className: "font-medium text-[12px] leading-[12px] text-text-primary whitespace-nowrap", children: actionLabel })
1805
1920
  }
1806
1921
  )
1807
1922
  ]
1808
1923
  }
1809
1924
  );
1810
1925
  }
1811
- var ToastContext = createContext3(null);
1926
+ var ToastContext = createContext2(null);
1812
1927
  var toastSeq = 0;
1813
1928
  function ToastProvider({ children }) {
1814
- const [toast, setToast] = useState11(null);
1815
- const showToast = useCallback5((opts) => {
1929
+ const [toast, setToast] = useState7(null);
1930
+ const showToast = useCallback3((opts) => {
1816
1931
  setToast({ ...opts, id: ++toastSeq });
1817
1932
  }, []);
1818
- const dismissToast = useCallback5(() => {
1933
+ const dismissToast = useCallback3(() => {
1819
1934
  setToast(null);
1820
1935
  }, []);
1821
- useEffect6(() => {
1936
+ useEffect3(() => {
1822
1937
  if (!toast) return;
1823
1938
  const duration = toast.durationMs ?? 5e3;
1824
1939
  if (duration <= 0) return;
@@ -1828,10 +1943,10 @@ function ToastProvider({ children }) {
1828
1943
  return () => clearTimeout(timer);
1829
1944
  }, [toast]);
1830
1945
  const value = useMemo3(() => ({ showToast, dismissToast }), [showToast, dismissToast]);
1831
- return /* @__PURE__ */ jsxs25(ToastContext.Provider, { value, children: [
1946
+ return /* @__PURE__ */ jsxs28(ToastContext.Provider, { value, children: [
1832
1947
  children,
1833
- toast && createPortal6(
1834
- /* @__PURE__ */ jsx38("div", { className: "fixed bottom-4 left-4 z-[100] pointer-events-none", children: /* @__PURE__ */ jsx38(
1948
+ toast && createPortal2(
1949
+ /* @__PURE__ */ jsx42("div", { className: "fixed bottom-4 left-4 z-[100] pointer-events-none", children: /* @__PURE__ */ jsx42(
1835
1950
  Toast,
1836
1951
  {
1837
1952
  className: "pointer-events-auto",
@@ -1850,7 +1965,7 @@ function ToastProvider({ children }) {
1850
1965
  ] });
1851
1966
  }
1852
1967
  function useToast() {
1853
- const ctx = useContext3(ToastContext);
1968
+ const ctx = useContext2(ToastContext);
1854
1969
  if (!ctx) throw new Error("useToast must be used within ToastProvider");
1855
1970
  return ctx;
1856
1971
  }
@@ -1885,10 +2000,13 @@ export {
1885
2000
  NavItem,
1886
2001
  Person,
1887
2002
  PersonTrigger,
2003
+ Popover,
2004
+ PreviewCard,
1888
2005
  Property,
1889
2006
  Rail,
1890
2007
  RailItem,
1891
2008
  Reaction,
2009
+ ReactionPicker,
1892
2010
  SearchInput,
1893
2011
  SectionHeader,
1894
2012
  SectionLabel,
@@ -1902,16 +2020,17 @@ export {
1902
2020
  Textarea,
1903
2021
  Toast,
1904
2022
  ToastProvider,
2023
+ Toolbar,
2024
+ ToolbarButton,
2025
+ ToolbarInput,
2026
+ ToolbarSeparator,
1905
2027
  Tooltip,
2028
+ TooltipProvider,
1906
2029
  TopBar,
1907
2030
  WithTooltip,
1908
- clampBox,
1909
2031
  cn,
1910
- fitMenu,
1911
- fitSubmenu,
1912
2032
  hueFor,
1913
2033
  initialsFor,
1914
- useFieldControlId,
1915
2034
  useToast
1916
2035
  };
1917
2036
  //# sourceMappingURL=index.js.map