@estiva-app/ui 0.55.0 → 0.57.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 (65) hide show
  1. package/dist/ChipInput.d.ts.map +1 -1
  2. package/dist/CollapsibleSection.d.ts +8 -2
  3. package/dist/CollapsibleSection.d.ts.map +1 -1
  4. package/dist/CommandPalette.d.ts.map +1 -1
  5. package/dist/ContainerHeader.d.ts +7 -1
  6. package/dist/ContainerHeader.d.ts.map +1 -1
  7. package/dist/IconButton.d.ts.map +1 -1
  8. package/dist/ListColumn.d.ts +2 -0
  9. package/dist/ListColumn.d.ts.map +1 -1
  10. package/dist/Menu.d.ts.map +1 -1
  11. package/dist/NavItem.d.ts.map +1 -1
  12. package/dist/NavTree.d.ts +11 -3
  13. package/dist/NavTree.d.ts.map +1 -1
  14. package/dist/PersonTrigger.d.ts.map +1 -1
  15. package/dist/SectionHeader.d.ts +24 -5
  16. package/dist/SectionHeader.d.ts.map +1 -1
  17. package/dist/SectionLabel.d.ts.map +1 -1
  18. package/dist/Select.d.ts.map +1 -1
  19. package/dist/SuggestionMenu.d.ts.map +1 -1
  20. package/dist/gates/index.js +1 -1
  21. package/dist/gates/index.js.map +2 -2
  22. package/dist/index.js +138 -77
  23. package/dist/index.js.map +4 -4
  24. package/dist/looks.d.ts +4 -0
  25. package/dist/looks.d.ts.map +1 -1
  26. package/package.json +1 -1
  27. package/registry.json +98 -26
  28. package/src/ChipInput.test.tsx +8 -0
  29. package/src/ChipInput.tsx +4 -2
  30. package/src/CollapsibleSection.mdx +1 -1
  31. package/src/CollapsibleSection.stories.tsx +14 -1
  32. package/src/CollapsibleSection.test.tsx +24 -0
  33. package/src/CollapsibleSection.tsx +11 -5
  34. package/src/CommandPalette.test.tsx +6 -0
  35. package/src/CommandPalette.tsx +3 -2
  36. package/src/ContainerHeader.mdx +4 -0
  37. package/src/ContainerHeader.stories.tsx +14 -0
  38. package/src/ContainerHeader.test.tsx +8 -0
  39. package/src/ContainerHeader.tsx +34 -6
  40. package/src/IconButton.tsx +2 -1
  41. package/src/ListColumn.test.tsx +8 -0
  42. package/src/ListColumn.tsx +4 -2
  43. package/src/Menu.tsx +5 -4
  44. package/src/NavItem.mdx +3 -1
  45. package/src/NavItem.test.tsx +32 -2
  46. package/src/NavItem.tsx +28 -12
  47. package/src/NavTree.mdx +16 -1
  48. package/src/NavTree.stories.tsx +49 -2
  49. package/src/NavTree.test.tsx +28 -0
  50. package/src/NavTree.tsx +27 -6
  51. package/src/PersonTrigger.tsx +6 -5
  52. package/src/SectionHeader.mdx +10 -3
  53. package/src/SectionHeader.stories.tsx +18 -0
  54. package/src/SectionHeader.test.tsx +89 -0
  55. package/src/SectionHeader.tsx +114 -26
  56. package/src/SectionLabel.tsx +6 -3
  57. package/src/Select.test.tsx +8 -0
  58. package/src/Select.tsx +5 -2
  59. package/src/Sidebar.tsx +1 -1
  60. package/src/SuggestionMenu.stories.tsx +15 -0
  61. package/src/SuggestionMenu.test.tsx +8 -0
  62. package/src/SuggestionMenu.tsx +4 -2
  63. package/src/eslint/no-copied-look.test.ts +2 -2
  64. package/src/gates/app-checks.ts +1 -1
  65. package/src/looks.ts +6 -0
package/dist/index.js CHANGED
@@ -168,7 +168,7 @@ function Sidebar({ "aria-label": ariaLabel = "Workspace", children, className })
168
168
  {
169
169
  "aria-label": ariaLabel,
170
170
  className: cn("flex w-60 shrink-0 flex-col", !onGround && "border-r border-border-default bg-bg-surface", className),
171
- children: /* @__PURE__ */ jsx4(ScrollArea, { className: "min-h-0 flex-1", contentClassName: "flex flex-col gap-px px-2.5 py-3", children })
171
+ children: /* @__PURE__ */ jsx4(ScrollArea, { className: "min-h-0 flex-1", contentClassName: "flex flex-col gap-0.5 px-2.5 py-3", children })
172
172
  }
173
173
  );
174
174
  }
@@ -276,6 +276,25 @@ import {
276
276
  // src/IconButton.tsx
277
277
  import { Button as BaseButton } from "@base-ui/react/button";
278
278
 
279
+ // src/looks.ts
280
+ var FIELD_BOX_CLASSES = "bg-bg-field border border-border-field shadow-field hover:border-border-strong group-hover/field:border-border-strong focus:border-border-focus group-hover/field:focus:border-border-focus rounded-lg";
281
+ var FIELD_TEXT_CLASSES = "text-text-primary placeholder:text-text-muted";
282
+ var FIELD_DISABLED_CLASSES = "disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled disabled:border-border-field group-hover/field:disabled:border-border-field";
283
+ var FIELD_FOCUS_RING_CLASSES = "signal:transition-shadow signal:focus:shadow-focus-ring";
284
+ var FIELD_SIZE_CLASSES = {
285
+ default: "px-3 py-2 text-input-value",
286
+ small: "h-6 min-h-6 px-2 text-caption"
287
+ };
288
+ var FIELD_SHELL_CLASSES = "bg-bg-field border border-border-field shadow-field hover:border-border-strong group-hover/field:border-border-strong rounded-lg transition-colors";
289
+ var FLOATING_SURFACE_CLASSES = "rounded-lg border border-border-default bg-bg-elevated shadow-lg";
290
+ var CHIP_TEXT_CLASSES = "text-chip signal:font-mono signal:text-small signal:font-semibold signal:tabular-nums";
291
+ var BARE_INPUT_CLASSES = "bg-transparent text-text-primary placeholder:text-text-muted outline-none";
292
+ var SIDEBAR_ROW_CLASSES = "rounded-lg transition-colors";
293
+ var SIDEBAR_ROW_TEXT_CLASSES = "text-text-secondary hover:text-text-primary";
294
+ var MUTED_CONTROL_CLASSES = "text-text-secondary hover:bg-bg-hover hover:text-text-primary";
295
+ var MENU_TRIGGER_CLASSES = "cursor-pointer rounded-md transition-colors hover:bg-bg-hover data-[popup-open]:bg-bg-hover";
296
+ var COLLAPSIBLE_PANEL_CLASSES = "h-[var(--collapsible-panel-height)] overflow-hidden transition-[height] duration-150 ease-out motion-reduce:transition-none data-[starting-style]:h-0 data-[ending-style]:h-0";
297
+
279
298
  // src/formBusy.ts
280
299
  import { createContext as createContext2, useContext as useContext2 } from "react";
281
300
  var FormBusyContext = createContext2(false);
@@ -306,23 +325,6 @@ function Kbd({ children, className, tone = "default" }) {
306
325
  );
307
326
  }
308
327
 
309
- // src/looks.ts
310
- var FIELD_BOX_CLASSES = "bg-bg-field border border-border-field shadow-field hover:border-border-strong group-hover/field:border-border-strong focus:border-border-focus group-hover/field:focus:border-border-focus rounded-lg";
311
- var FIELD_TEXT_CLASSES = "text-text-primary placeholder:text-text-muted";
312
- var FIELD_DISABLED_CLASSES = "disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled disabled:border-border-field group-hover/field:disabled:border-border-field";
313
- var FIELD_FOCUS_RING_CLASSES = "signal:transition-shadow signal:focus:shadow-focus-ring";
314
- var FIELD_SIZE_CLASSES = {
315
- default: "px-3 py-2 text-input-value",
316
- small: "h-6 min-h-6 px-2 text-caption"
317
- };
318
- var FIELD_SHELL_CLASSES = "bg-bg-field border border-border-field shadow-field hover:border-border-strong group-hover/field:border-border-strong rounded-lg transition-colors";
319
- var FLOATING_SURFACE_CLASSES = "rounded-lg border border-border-default bg-bg-elevated shadow-lg";
320
- var CHIP_TEXT_CLASSES = "text-chip signal:font-mono signal:text-small signal:font-semibold signal:tabular-nums";
321
- var BARE_INPUT_CLASSES = "bg-transparent text-text-primary placeholder:text-text-muted outline-none";
322
- var SIDEBAR_ROW_CLASSES = "rounded-lg transition-colors";
323
- var SIDEBAR_ROW_TEXT_CLASSES = "text-text-secondary hover:text-text-primary";
324
- var COLLAPSIBLE_PANEL_CLASSES = "h-[var(--collapsible-panel-height)] overflow-hidden transition-[height] duration-150 ease-out motion-reduce:transition-none data-[starting-style]:h-0 data-[ending-style]:h-0";
325
-
326
328
  // src/Tooltip.tsx
327
329
  import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
328
330
  var OPEN_DELAY = 300;
@@ -410,7 +412,7 @@ function IconButton({
410
412
  // stretched and carries no `self-center` — see the note there.
411
413
  "flex items-center justify-center p-1 rounded-lg transition-colors shrink-0 self-center cursor-pointer",
412
414
  !off && variant === "primary" && "bg-accent-primary hover:bg-accent-hover text-text-inverse",
413
- !off && (variant === "muted" || variant === "resolve") && "text-text-secondary hover:bg-bg-hover hover:text-text-primary",
415
+ !off && (variant === "muted" || variant === "resolve") && MUTED_CONTROL_CLASSES,
414
416
  !off && variant === "outlined" && "border border-border-default hover:bg-bg-hover text-text-secondary",
415
417
  !off && variant === "current" && "hover:bg-bg-hover",
416
418
  off && variant === "primary" && "bg-bg-disabled text-text-disabled",
@@ -1008,9 +1010,12 @@ function SectionLabel({
1008
1010
  "span",
1009
1011
  {
1010
1012
  className: cn(
1011
- "text-h5 leading-3 signal:font-mono signal:text-small signal:uppercase signal:tracking-widest",
1012
- // Leaf's section labels are Signal's type (Katerina, 28 September),
1013
- // but a group that folds keeps Ship's (Katerina, 2 October).
1013
+ "text-h5 leading-3",
1014
+ // Ship's type in every theme but Leaf, whose section labels are the
1015
+ // micro-label (Katerina, 28 September) unless the group folds (2
1016
+ // October). Signal dropped its micro-label on 5 October: every one of
1017
+ // Peek's section labels is Ship's type, folding or not, so Desk's
1018
+ // Screener and its Urgent and Open work read as one.
1014
1019
  !folds && "leaf:font-mono leaf:text-small leaf:uppercase leaf:tracking-widest",
1015
1020
  tone === "secondary" ? "text-text-secondary" : tone === "muted" ? "text-text-muted" : "text-text-primary",
1016
1021
  truncate && "min-w-0 truncate",
@@ -1029,7 +1034,8 @@ function MenuPanel({ children, className, ...props }) {
1029
1034
  "div",
1030
1035
  {
1031
1036
  className: cn(
1032
- "flex flex-col p-2",
1037
+ // 2px between rows, as a list column's and a sidebar's (Katerina, 5 October).
1038
+ "flex flex-col gap-0.5 p-2",
1033
1039
  FLOATING_SURFACE_CLASSES,
1034
1040
  /*
1035
1041
  * A divider in a menu runs the width of the rows it separates.
@@ -1088,7 +1094,7 @@ function Menu({ trigger, align = "left", openOnHover = false, open, onOpenChange
1088
1094
  "data-interactive": true,
1089
1095
  className: cn("min-w-[180px] outline-none p-0", className),
1090
1096
  render: /* @__PURE__ */ jsx18(MenuPanel, {}),
1091
- children: /* @__PURE__ */ jsx18(ScrollArea, { viewportClassName: "max-h-[var(--available-height)]", contentClassName: cn("flex flex-col p-2", contentClassName), children: /* @__PURE__ */ jsx18(MenuContext.Provider, { value: { openOnHover }, children: /* @__PURE__ */ jsx18(DividerInPanel.Provider, { value: true, children }) }) })
1097
+ children: /* @__PURE__ */ jsx18(ScrollArea, { viewportClassName: "max-h-[var(--available-height)]", contentClassName: cn("flex flex-col gap-0.5 p-2", contentClassName), children: /* @__PURE__ */ jsx18(MenuContext.Provider, { value: { openOnHover }, children: /* @__PURE__ */ jsx18(DividerInPanel.Provider, { value: true, children }) }) })
1092
1098
  }
1093
1099
  )
1094
1100
  }
@@ -1219,12 +1225,12 @@ function MenuSection({ label, children, className }) {
1219
1225
  const inMenu = useContext4(MenuContext) !== null;
1220
1226
  const heading = /* @__PURE__ */ jsx18("div", { className: cn("flex h-8 items-center px-2", className), children: /* @__PURE__ */ jsx18(SectionLabel, { tone: "secondary", children: label }) });
1221
1227
  if (!inMenu) {
1222
- return /* @__PURE__ */ jsxs11("div", { className: "flex flex-col", children: [
1228
+ return /* @__PURE__ */ jsxs11("div", { className: "flex flex-col gap-0.5", children: [
1223
1229
  heading,
1224
1230
  children
1225
1231
  ] });
1226
1232
  }
1227
- return /* @__PURE__ */ jsxs11(BaseMenu.Group, { className: "flex flex-col", children: [
1233
+ return /* @__PURE__ */ jsxs11(BaseMenu.Group, { className: "flex flex-col gap-0.5", children: [
1228
1234
  /* @__PURE__ */ jsx18(BaseMenu.GroupLabel, { render: heading }),
1229
1235
  children
1230
1236
  ] });
@@ -1597,7 +1603,7 @@ function ChipInput({
1597
1603
  sideOffset: GAP3,
1598
1604
  collisionPadding: VIEWPORT_PAD3,
1599
1605
  className: "z-50 data-[anchor-hidden]:hidden",
1600
- children: /* @__PURE__ */ jsx22(Combobox.Popup, { className: "w-[var(--anchor-width)] p-0", render: /* @__PURE__ */ jsx22(MenuPanel, {}), children: /* @__PURE__ */ jsx22(ScrollArea, { viewportClassName: "max-h-[240px]", contentClassName: "flex flex-col p-2", children: /* @__PURE__ */ jsx22(Combobox.List, { children: (option) => /* @__PURE__ */ jsx22(
1606
+ children: /* @__PURE__ */ jsx22(Combobox.Popup, { className: "w-[var(--anchor-width)] p-0", render: /* @__PURE__ */ jsx22(MenuPanel, {}), children: /* @__PURE__ */ jsx22(ScrollArea, { viewportClassName: "max-h-[240px]", contentClassName: "flex flex-col p-2", children: /* @__PURE__ */ jsx22(Combobox.List, { className: "flex flex-col gap-0.5", children: (option) => /* @__PURE__ */ jsx22(
1601
1607
  Combobox.Item,
1602
1608
  {
1603
1609
  value: option,
@@ -1953,7 +1959,7 @@ function CommandPaletteSearch({ query, onQueryChange, placeholder, groups, chip,
1953
1959
  ] }),
1954
1960
  /* @__PURE__ */ jsxs19(ScrollArea, { viewportClassName: "max-h-[420px]", contentClassName: "flex flex-col p-2", children: [
1955
1961
  children != null && /* @__PURE__ */ jsx27("div", { className: "flex flex-col gap-2 px-3 pb-2 pt-3", children }),
1956
- /* @__PURE__ */ jsx27(Autocomplete.List, { className: "flex flex-col", children: (group) => /* @__PURE__ */ jsxs19(Autocomplete.Group, { items: group.items, className: "flex flex-col", children: [
1962
+ /* @__PURE__ */ jsx27(Autocomplete.List, { className: "flex flex-col", children: (group) => /* @__PURE__ */ jsxs19(Autocomplete.Group, { items: group.items, className: "flex flex-col gap-0.5", children: [
1957
1963
  /* @__PURE__ */ jsx27(Autocomplete.GroupLabel, { className: "flex h-7 shrink-0 items-center px-3", children: /* @__PURE__ */ jsx27(SectionLabel, { tone: "secondary", children: group.value }) }),
1958
1964
  /* @__PURE__ */ jsx27(Autocomplete.Collection, { children: (row) => (
1959
1965
  /* The menu row, as the list's own option — the way Select
@@ -2304,13 +2310,13 @@ function PersonTrigger({ name, picture, fallback, size, open = false, compact =
2304
2310
  "aria-haspopup": "menu",
2305
2311
  "aria-expanded": open,
2306
2312
  className: cn(
2307
- "flex h-8 min-h-8 cursor-pointer items-center gap-1.5 rounded-md pl-1.5 pr-1.5 text-body-2 text-text-primary transition-colors hover:bg-bg-hover",
2313
+ "flex h-8 min-h-8 items-center gap-1.5 pl-1.5 pr-1.5 text-body-2 text-text-primary",
2314
+ MENU_TRIGGER_CLASSES,
2308
2315
  open && "bg-bg-hover",
2309
2316
  // Under a `Menu` the open state is Base UI's, not a prop: the trigger
2310
- // carries `data-popup-open` while its menu is up, and sets its own
2311
- // `aria-expanded`. Both spellings hold the fill, so this works whether
2312
- // the caller drives it or the menu does.
2313
- "data-[popup-open]:bg-bg-hover",
2317
+ // carries `data-popup-open` while its menu is up (MENU_TRIGGER_CLASSES
2318
+ // holds the fill for it), and sets its own `aria-expanded`; `open`
2319
+ // holds it for a caller that drives it.
2314
2320
  className
2315
2321
  ),
2316
2322
  ...props,
@@ -2520,7 +2526,7 @@ function Select({ value, onChange, options, size = "default", ariaLabel, placeho
2520
2526
  {
2521
2527
  render: /* @__PURE__ */ jsx34(MenuPanel, {}),
2522
2528
  className: "min-w-[var(--anchor-width)] p-0",
2523
- children: /* @__PURE__ */ jsx34(ScrollArea, { viewportClassName: "max-h-[min(288px,var(--available-height))]", contentClassName: "flex flex-col p-2", children: options.map((option) => /* @__PURE__ */ jsxs25(
2529
+ children: /* @__PURE__ */ jsx34(ScrollArea, { viewportClassName: "max-h-[min(288px,var(--available-height))]", contentClassName: "flex flex-col gap-0.5 p-2", children: options.map((option) => /* @__PURE__ */ jsxs25(
2524
2530
  BaseSelect.Item,
2525
2531
  {
2526
2532
  value: option.value,
@@ -2615,16 +2621,30 @@ import { IconX as IconX5 } from "@tabler/icons-react";
2615
2621
 
2616
2622
  // src/ContainerHeader.tsx
2617
2623
  import { IconChevronDown as IconChevronDown3 } from "@tabler/icons-react";
2624
+ import { Button as BaseButton6 } from "@base-ui/react/button";
2618
2625
  import { jsx as jsx37, jsxs as jsxs26 } from "react/jsx-runtime";
2619
- function ContainerHeader({ title, chevron = false, actions, className }) {
2626
+ function ContainerHeader({ title, chevron = false, titleMenu, actions, className }) {
2620
2627
  const text = typeof title === "string";
2621
- return /* @__PURE__ */ jsxs26("div", { className: cn("flex h-12 shrink-0 items-center justify-between overflow-hidden border-b border-border-subtle py-2 pr-4 pl-5", className), children: [
2622
- /* @__PURE__ */ jsx37("div", { className: cn("flex items-center gap-2 overflow-hidden", text ? "shrink-0" : "min-w-0 flex-1"), children: /* @__PURE__ */ jsxs26("div", { className: cn("flex items-center gap-1", text ? "shrink-0" : "min-w-0 flex-1"), children: [
2623
- text ? /* @__PURE__ */ jsx37("span", { className: "whitespace-nowrap text-body-2-strong text-text-primary", children: title }) : /* @__PURE__ */ jsx37("div", { className: "min-w-0 flex-1", children: title }),
2624
- chevron && /* @__PURE__ */ jsx37(IconChevronDown3, { size: 12, stroke: 1.5, className: "shrink-0 text-text-secondary" })
2625
- ] }) }),
2626
- actions && /* @__PURE__ */ jsx37("div", { className: "flex shrink-0 items-center justify-end gap-3", children: /* @__PURE__ */ jsx37("div", { className: "flex shrink-0 items-center gap-1", children: actions }) })
2628
+ const name = /* @__PURE__ */ jsxs26("div", { className: cn("flex items-center gap-1", text ? "shrink-0" : "min-w-0 flex-1"), children: [
2629
+ text ? /* @__PURE__ */ jsx37("span", { className: "whitespace-nowrap text-body-2-strong text-text-primary", children: title }) : /* @__PURE__ */ jsx37("div", { className: "min-w-0 flex-1", children: title }),
2630
+ (chevron || titleMenu != null) && /* @__PURE__ */ jsx37(IconChevronDown3, { size: 12, stroke: 1.5, className: "shrink-0 text-text-secondary" })
2627
2631
  ] });
2632
+ return (
2633
+ // With a title menu the bar gives the button 6px of its left padding
2634
+ // (pl-3.5 + the button's px-1.5 = pl-5): the words stay where a title's
2635
+ // are, and the fill is inside the bar — a negative margin had it clipped
2636
+ // (Katerina, 5 October).
2637
+ /* @__PURE__ */ jsxs26("div", { className: cn("flex h-12 shrink-0 items-center justify-between overflow-hidden border-b border-border-subtle py-2 pr-4", titleMenu != null ? "pl-3.5" : "pl-5", className), children: [
2638
+ /* @__PURE__ */ jsx37("div", { className: cn("flex items-center gap-2 overflow-hidden", text ? "shrink-0" : "min-w-0 flex-1"), children: titleMenu != null ? /* @__PURE__ */ jsx37(
2639
+ Menu,
2640
+ {
2641
+ trigger: /* @__PURE__ */ jsx37(BaseButton6, { type: "button", className: cn("flex min-w-0 items-center px-1.5 py-1", MENU_TRIGGER_CLASSES), children: name }),
2642
+ children: titleMenu
2643
+ }
2644
+ ) : name }),
2645
+ actions && /* @__PURE__ */ jsx37("div", { className: "flex shrink-0 items-center justify-end gap-3", children: /* @__PURE__ */ jsx37("div", { className: "flex shrink-0 items-center gap-1", children: actions }) })
2646
+ ] })
2647
+ );
2628
2648
  }
2629
2649
 
2630
2650
  // src/DialogShell.tsx
@@ -2979,7 +2999,7 @@ function NavItem({
2979
2999
  };
2980
3000
  const besides = menu != null || hasRows;
2981
3001
  const mark = unread ? urgent ? /* @__PURE__ */ jsx45(UrgentMark, {}) : /* @__PURE__ */ jsx45(UnreadDot, {}) : null;
2982
- const stepsAside = menu != null && "transition-opacity group-hover/nav:opacity-0 group-focus-within/nav:opacity-0 group-has-[[data-nav-menu]_[aria-expanded=true]]/nav:opacity-0";
3002
+ const stepsAside = menu != null && "transition-opacity group-hover/nav:opacity-0 group-has-[:focus-visible]/nav:opacity-0 group-has-[[data-nav-menu]_[aria-expanded=true]]/nav:opacity-0 [@media(hover:none)]:opacity-0";
2983
3003
  const row = /* @__PURE__ */ jsxs30(
2984
3004
  "a",
2985
3005
  {
@@ -3007,14 +3027,14 @@ function NavItem({
3007
3027
  className: cn(
3008
3028
  // With rows under it the slot is kept even with no icon: the arrow lands in it.
3009
3029
  "flex min-w-4 shrink-0 items-center",
3010
- hasRows && "transition-opacity group-hover/nav:opacity-0 group-focus-within/nav:opacity-0"
3030
+ hasRows && "transition-opacity group-hover/nav:opacity-0 group-has-[:focus-visible]/nav:opacity-0"
3011
3031
  ),
3012
3032
  children: icon
3013
3033
  }
3014
3034
  ),
3015
3035
  /* @__PURE__ */ jsx45("span", { className: cn("flex-1 truncate", unread && "font-medium"), children: label }),
3016
3036
  hint != null && // Takes room only while it shows, so the label gives way to it.
3017
- /* @__PURE__ */ jsx45("span", { className: "hidden shrink-0 items-center group-hover/nav:flex group-focus-within/nav:flex", children: hint }),
3037
+ /* @__PURE__ */ jsx45("span", { className: "hidden shrink-0 items-center group-hover/nav:flex group-has-[:focus-visible]/nav:flex", children: hint }),
3018
3038
  count ? /* @__PURE__ */ jsx45(WithTooltip, { label: countLabel ?? `${count} open`, children: /* @__PURE__ */ jsx45(
3019
3039
  "span",
3020
3040
  {
@@ -3026,11 +3046,19 @@ function NavItem({
3026
3046
  children: count
3027
3047
  }
3028
3048
  ) }) : null,
3029
- mark ? /* @__PURE__ */ jsx45("span", { className: cn("flex shrink-0", stepsAside), children: mark }) : menu != null && !count ? (
3049
+ mark ? (
3050
+ // `-mr-1`: the 24px slot's dot then sits on the axis of a count and
3051
+ // of the ⋮ (8px from the edge to the 16px box), one column with a
3052
+ // folder heading's dot and ⋮ (Katerina, 5 October).
3053
+ /* @__PURE__ */ jsx45("span", { className: cn("-mr-1 flex shrink-0", stepsAside), children: mark })
3054
+ ) : menu != null && !count ? (
3030
3055
  // Nothing at the right, but a menu: no room is kept at rest, so the
3031
3056
  // label has the row's whole width; while the ⋮ shows, its 24px slot
3032
3057
  // is kept, so a long label ends before it instead of running under it.
3033
- /* @__PURE__ */ jsx45("span", { "aria-hidden": "true", className: "hidden w-6 shrink-0 group-hover/nav:block group-focus-within/nav:block group-has-[[data-nav-menu]_[aria-expanded=true]]/nav:block" })
3058
+ // `-mr-1`: the ⋮ sits at `right-1`, 4px into the row's padding, so the
3059
+ // slot does too — at the padding's edge it kept 4px the ⋮ never
3060
+ // covers, and the label stopped 12px short of the button, not 8.
3061
+ /* @__PURE__ */ jsx45("span", { "aria-hidden": "true", className: "-mr-1 hidden w-6 shrink-0 group-hover/nav:block group-has-[:focus-visible]/nav:block group-has-[[data-nav-menu]_[aria-expanded=true]]/nav:block [@media(hover:none)]:block" })
3034
3062
  ) : null
3035
3063
  ]
3036
3064
  }
@@ -3041,7 +3069,7 @@ function NavItem({
3041
3069
  row,
3042
3070
  hasRows && // `left-1`: the 24px button's 16px arrow then sits where the icon
3043
3071
  // sits (the row's px-2), so the icon turns into the arrow in place.
3044
- /* @__PURE__ */ jsx45("div", { className: "absolute inset-y-0 left-1 flex items-center opacity-0 transition-opacity focus-within:opacity-100 group-hover/nav:opacity-100", children: /* @__PURE__ */ jsx45(
3072
+ /* @__PURE__ */ jsx45("div", { className: "absolute inset-y-0 left-1 flex items-center opacity-0 transition-opacity group-hover/nav:opacity-100 group-has-[:focus-visible]/nav:opacity-100", children: /* @__PURE__ */ jsx45(
3045
3073
  IconButton,
3046
3074
  {
3047
3075
  tooltip: open ? "Collapse" : "Expand",
@@ -3052,11 +3080,15 @@ function NavItem({
3052
3080
  children: /* @__PURE__ */ jsx45(IconChevronRight2, { size: 16, stroke: 1.5, className: cn("transition-transform duration-150", open && "rotate-90") })
3053
3081
  }
3054
3082
  ) }),
3055
- menu != null && // On a count, `right-1`: the 24px button's 16px icon then ends 8px
3056
- // from the row's edge, where the count ends (px-2), so the ⋮ lands on
3057
- // the number. On the mark or the empty slot, `right-2`: the button
3058
- // sits in the 24px slot itself.
3059
- /* @__PURE__ */ jsx45("div", { "data-nav-menu": true, className: cn("absolute inset-y-0 flex items-center", count && !mark ? "right-1" : "right-2", "opacity-0 transition-opacity focus-within:opacity-100 group-hover/nav:opacity-100 has-[[aria-expanded=true]]:opacity-100"), children: /* @__PURE__ */ jsx45(
3083
+ menu != null && // `right-1`: the 24px button's 16px icon ends 8px from the row's
3084
+ // edge, where a count ends (px-2) — and where a SectionHeader's
3085
+ // actions and ⋮ end, so a folder's ⋮ and its rows' ⋮ are one column
3086
+ // (Katerina, 5 October: the rows move to the heading, not the
3087
+ // heading to the rows). Over a dot or the empty slot it sits 4px
3088
+ // right of the slot's centre.
3089
+ // On a screen with no hover it always shows: nothing there can point
3090
+ // at the row, and the menu has no other way in.
3091
+ /* @__PURE__ */ jsx45("div", { "data-nav-menu": true, className: cn("absolute inset-y-0 right-1 flex items-center", "opacity-0 transition-opacity group-hover/nav:opacity-100 group-has-[:focus-visible]/nav:opacity-100 has-[[aria-expanded=true]]:opacity-100 [@media(hover:none)]:opacity-100"), children: /* @__PURE__ */ jsx45(
3060
3092
  Menu,
3061
3093
  {
3062
3094
  align: "right",
@@ -3074,7 +3106,7 @@ function NavItem({
3074
3106
  id: panelId,
3075
3107
  hiddenUntilFound: true,
3076
3108
  className: COLLAPSIBLE_PANEL_CLASSES,
3077
- children: /* @__PURE__ */ jsx45("div", { className: "flex flex-col gap-px pl-6", children })
3109
+ children: /* @__PURE__ */ jsx45("div", { className: "flex flex-col gap-0.5 pt-0.5 pl-6", children })
3078
3110
  }
3079
3111
  )
3080
3112
  ] });
@@ -3091,23 +3123,37 @@ import { useState as useState9 } from "react";
3091
3123
  import { Collapsible as Collapsible2 } from "@base-ui/react/collapsible";
3092
3124
 
3093
3125
  // src/SectionHeader.tsx
3094
- import { IconChevronRight as IconChevronRight3 } from "@tabler/icons-react";
3126
+ import { IconChevronRight as IconChevronRight3, IconDotsVertical as IconDotsVertical2 } from "@tabler/icons-react";
3095
3127
  import { useRender } from "@base-ui/react/use-render";
3096
3128
  import { Fragment as Fragment11, jsx as jsx46, jsxs as jsxs31 } from "react/jsx-runtime";
3097
- function SectionHeader({ title, chevron = false, isExpanded = true, onToggle, trailing, actions, showActions = "hover", hover = "fill", look = "heading", render, className }) {
3129
+ function SectionHeader({ title, chevron = false, icon, isExpanded = true, onToggle, trailing, actions, menu, menuLabel, showActions = "hover", hover = "fill", look = "heading", render, className }) {
3098
3130
  const row = look === "row";
3131
+ const acts = actions && actions.length > 0 || menu != null;
3132
+ const trailingClasses = !acts ? "has-[[data-unread]]:-mr-1" : showActions === "hover" && "has-[[data-unread]]:pointer-events-none has-[[data-unread]]:absolute has-[[data-unread]]:inset-y-0 has-[[data-unread]]:right-1 has-[[data-unread]]:w-6 has-[[data-unread]]:justify-center has-[[data-unread]]:transition-opacity group-hover:has-[[data-unread]]:opacity-0 group-has-[:focus-visible]:has-[[data-unread]]:opacity-0 group-has-[[data-section-menu]_[aria-expanded=true]]:has-[[data-unread]]:opacity-0 [@media(hover:none)]:has-[[data-unread]]:opacity-0";
3133
+ const reveal = showActions === "hover" && "opacity-0 transition-opacity group-hover:opacity-100 group-has-[:focus-visible]:opacity-100 has-[[aria-expanded=true]]:opacity-100 [@media(hover:none)]:opacity-100";
3099
3134
  const titleElement = useRender({
3100
3135
  render: render ?? (chevron ? /* @__PURE__ */ jsx46("button", { type: "button", onClick: onToggle, "aria-expanded": isExpanded }) : /* @__PURE__ */ jsx46("span", {})),
3101
3136
  props: {
3102
3137
  // `text-left`: a button centres its text. `h-full` and `flex-1`: the
3103
3138
  // whole row up to the actions is the hit target, as it was when the
3104
3139
  // row itself carried the click.
3105
- className: cn("flex h-full min-w-0 flex-1 items-center text-left", row ? "gap-2" : "gap-1"),
3140
+ // An icon is a row's: NavItem's 8px after it.
3141
+ className: cn("flex h-full min-w-0 flex-1 items-center text-left", row || chevron || icon != null ? "gap-2" : "gap-1"),
3106
3142
  children: /* @__PURE__ */ jsxs31(Fragment11, { children: [
3107
- chevron && /* @__PURE__ */ jsx46(
3143
+ icon != null ? /* @__PURE__ */ jsxs31("span", { className: "relative flex size-4 shrink-0 items-center justify-center text-text-secondary", children: [
3144
+ /* @__PURE__ */ jsx46("span", { className: cn("flex", chevron && "transition-opacity group-hover:opacity-0 group-has-[:focus-visible]:opacity-0"), children: icon }),
3145
+ chevron && /* @__PURE__ */ jsx46(
3146
+ IconChevronRight3,
3147
+ {
3148
+ size: 16,
3149
+ stroke: 1.5,
3150
+ className: cn("absolute opacity-0 transition-[opacity,transform] duration-150 group-hover:opacity-100 group-has-[:focus-visible]:opacity-100", isExpanded && "rotate-90")
3151
+ }
3152
+ )
3153
+ ] }) : chevron && /* @__PURE__ */ jsx46(
3108
3154
  IconChevronRight3,
3109
3155
  {
3110
- size: row ? 16 : 12,
3156
+ size: 16,
3111
3157
  stroke: 1.5,
3112
3158
  className: cn("shrink-0 transition-transform duration-150", !row && "text-text-secondary", isExpanded && "rotate-90")
3113
3159
  }
@@ -3123,7 +3169,8 @@ function SectionHeader({ title, chevron = false, isExpanded = true, onToggle, tr
3123
3169
  "div",
3124
3170
  {
3125
3171
  className: cn(
3126
- "group flex h-[32px] shrink-0 items-center gap-1 px-2",
3172
+ // `relative`: a dot in the last button's place is placed against the row.
3173
+ "group relative flex h-[32px] shrink-0 items-center gap-1 px-2",
3127
3174
  !row && "transition-colors",
3128
3175
  // A row is NavItem's shape and colour: 6px corners, secondary words that brighten on hover.
3129
3176
  row ? cn(SIDEBAR_ROW_CLASSES, SIDEBAR_ROW_TEXT_CLASSES) : "rounded-lg",
@@ -3131,20 +3178,30 @@ function SectionHeader({ title, chevron = false, isExpanded = true, onToggle, tr
3131
3178
  // lights up, a fixed heading over rows does not (2026-09-09, the
3132
3179
  // Sidebar's fixed group).
3133
3180
  // A quiet label never lights up: it is a label; its button does.
3134
- hover === "fill" && look !== "quiet" && (chevron || actions && actions.length > 0) && "hover:bg-bg-hover",
3181
+ hover === "fill" && look !== "quiet" && (chevron || acts) && "hover:bg-bg-hover",
3182
+ // While its menu is open the pointer is on the menu, not the row: the
3183
+ // row keeps its hover look, as a NavItem's does.
3184
+ hover === "fill" && look !== "quiet" && menu != null && "has-[[data-section-menu]_[aria-expanded=true]]:bg-bg-hover",
3135
3185
  className
3136
3186
  ),
3137
3187
  children: [
3138
3188
  titleElement,
3139
- trailing != null && /* @__PURE__ */ jsx46("div", { className: "flex shrink-0 items-center", children: trailing }),
3140
- actions && actions.length > 0 && /* @__PURE__ */ jsx46(
3189
+ trailing != null && /* @__PURE__ */ jsx46("div", { className: cn("flex shrink-0 items-center", trailingClasses), children: trailing }),
3190
+ acts && /* @__PURE__ */ jsx46(
3141
3191
  "div",
3142
3192
  {
3143
- className: cn(
3144
- "-mr-1 flex shrink-0 items-center gap-1",
3145
- showActions === "hover" && "opacity-0 transition-opacity focus-within:opacity-100 group-hover:opacity-100"
3146
- ),
3147
- children: actions.map((action) => /* @__PURE__ */ jsx46(IconButton, { tooltip: action.tooltip, "aria-label": action.tooltip, onClick: action.onClick, children: action.icon }, action.tooltip))
3193
+ className: "-mr-1 flex shrink-0 items-center",
3194
+ children: /* @__PURE__ */ jsxs31("div", { className: cn("flex items-center gap-1", reveal), children: [
3195
+ actions?.map((action) => /* @__PURE__ */ jsx46(IconButton, { tooltip: action.tooltip, "aria-label": action.tooltip, onClick: action.onClick, children: action.icon }, action.tooltip)),
3196
+ menu != null && /* @__PURE__ */ jsx46("div", { "data-section-menu": true, className: "flex items-center", children: /* @__PURE__ */ jsx46(
3197
+ Menu,
3198
+ {
3199
+ align: "right",
3200
+ trigger: /* @__PURE__ */ jsx46(IconButton, { tooltip: "More options", "aria-label": menuLabel ?? `More options for ${title}`, children: /* @__PURE__ */ jsx46(IconDotsVertical2, { size: 16, stroke: 1.5 }) }),
3201
+ children: menu
3202
+ }
3203
+ ) })
3204
+ ] })
3148
3205
  }
3149
3206
  )
3150
3207
  ]
@@ -3172,7 +3229,7 @@ function writeStored(key, open) {
3172
3229
  } catch {
3173
3230
  }
3174
3231
  }
3175
- function CollapsibleSection({ title, defaultOpen = true, open: openProp, onOpenChange, storageKey, trailing, actions, showActions, look = "heading", indent = false, children, className, contentClassName }) {
3232
+ function CollapsibleSection({ title, defaultOpen = true, open: openProp, onOpenChange, storageKey, icon, trailing, actions, menu, menuLabel, showActions, look = "heading", indent = false, children, className, contentClassName }) {
3176
3233
  const [openState, setOpenState] = useState9(() => readStored(storageKey) ?? defaultOpen);
3177
3234
  const open = openProp ?? openState;
3178
3235
  const setOpen = (next) => {
@@ -3181,13 +3238,13 @@ function CollapsibleSection({ title, defaultOpen = true, open: openProp, onOpenC
3181
3238
  onOpenChange?.(next);
3182
3239
  };
3183
3240
  return /* @__PURE__ */ jsxs32(Collapsible2.Root, { open, onOpenChange: setOpen, className: cn("flex flex-col", className), children: [
3184
- /* @__PURE__ */ jsx47(SectionHeader, { title, chevron: true, isExpanded: open, trailing, actions, showActions, look, className: "shrink-0", render: /* @__PURE__ */ jsx47(Collapsible2.Trigger, {}) }),
3241
+ /* @__PURE__ */ jsx47(SectionHeader, { title, chevron: true, icon, isExpanded: open, trailing, actions, menu, menuLabel, showActions, look, className: "shrink-0", render: /* @__PURE__ */ jsx47(Collapsible2.Trigger, {}) }),
3185
3242
  /* @__PURE__ */ jsx47(
3186
3243
  Collapsible2.Panel,
3187
3244
  {
3188
3245
  hiddenUntilFound: true,
3189
3246
  className: COLLAPSIBLE_PANEL_CLASSES,
3190
- children: /* @__PURE__ */ jsx47("div", { className: cn("flex flex-col", look === "row" ? "pl-6" : indent && "pl-4", contentClassName), children })
3247
+ children: /* @__PURE__ */ jsx47("div", { className: cn("flex flex-col pt-0.5", (look === "row" || indent) && "pl-6", contentClassName), children })
3191
3248
  }
3192
3249
  )
3193
3250
  ] });
@@ -3235,6 +3292,8 @@ function Group({ group, selected, onSelect, storageKey }) {
3235
3292
  loading: group.nodes === null,
3236
3293
  trailing: group.trailing,
3237
3294
  actions: group.actions,
3295
+ menu: group.menu,
3296
+ icon: group.icon,
3238
3297
  defaultOpen: group.defaultOpen ?? holds(group.nodes, selected),
3239
3298
  open: group.open,
3240
3299
  onOpenChange: group.onOpenChange,
@@ -3244,14 +3303,14 @@ function Group({ group, selected, onSelect, storageKey }) {
3244
3303
  }
3245
3304
  );
3246
3305
  }
3247
- function NavTreeSection({ id, title, message, loading = false, trailing, actions, defaultOpen = true, open, onOpenChange, storageKey, onIntent, children }) {
3306
+ function NavTreeSection({ id, title, message, loading = false, trailing, actions, menu, icon, defaultOpen = true, open, onOpenChange, storageKey, onIntent, children }) {
3248
3307
  const timer = useRef10(void 0);
3249
3308
  useEffect5(() => () => clearTimeout(timer.current), []);
3250
3309
  const intent = onIntent;
3251
3310
  return /* @__PURE__ */ jsx48(
3252
3311
  "div",
3253
3312
  {
3254
- className: "shrink-0",
3313
+ className: "shrink-0 [&+&]:mt-1",
3255
3314
  "data-nav-group": id,
3256
3315
  onPointerEnter: intent ? () => {
3257
3316
  clearTimeout(timer.current);
@@ -3270,8 +3329,10 @@ function NavTreeSection({ id, title, message, loading = false, trailing, actions
3270
3329
  storageKey,
3271
3330
  trailing,
3272
3331
  actions,
3273
- className: "mt-2 shrink-0",
3274
- contentClassName: "gap-px",
3332
+ menu,
3333
+ icon,
3334
+ className: "shrink-0",
3335
+ contentClassName: "gap-0.5",
3275
3336
  children: message !== void 0 ? /* @__PURE__ */ jsx48(Line, { message }) : loading ? /* @__PURE__ */ jsx48(SkeletonList, { rows: 3 }) : children
3276
3337
  }
3277
3338
  )
@@ -3428,7 +3489,7 @@ function SuggestionMenu({ rect, editorElement, ariaLabel, width, maxHeight, sect
3428
3489
  )
3429
3490
  );
3430
3491
  };
3431
- return /* @__PURE__ */ jsx50(Popover, { anchor: rect, open: rect !== null, side: "top", align: "left", className: width, maxHeight, ariaLabel, children: /* @__PURE__ */ jsx50("div", { id: listboxId, role: "listbox", "aria-label": ariaLabel, className: "flex flex-col", children: shown.map((section, i) => /* @__PURE__ */ jsxs35("div", { className: "contents", children: [
3492
+ return /* @__PURE__ */ jsx50(Popover, { anchor: rect, open: rect !== null, side: "top", align: "left", className: width, maxHeight, ariaLabel, children: /* @__PURE__ */ jsx50("div", { id: listboxId, role: "listbox", "aria-label": ariaLabel, className: "flex flex-col gap-0.5", children: shown.map((section, i) => /* @__PURE__ */ jsxs35("div", { className: "contents", children: [
3432
3493
  i > 0 && /* @__PURE__ */ jsx50(MenuSeparator, {}),
3433
3494
  section.label ? /* @__PURE__ */ jsx50("div", { role: "group", "aria-label": section.label, children: /* @__PURE__ */ jsx50(MenuSection, { label: section.label, className: section.className, children: section.items.map(drawRow) }) }) : section.items.map(drawRow)
3434
3495
  ] }, section.label ?? i)) }) });
@@ -3702,7 +3763,7 @@ function Panel(props) {
3702
3763
 
3703
3764
  // src/ListColumn.tsx
3704
3765
  import { jsx as jsx60, jsxs as jsxs42 } from "react/jsx-runtime";
3705
- function Column({ title, chevron = false, actions, above, collapsed = false, children, className, broken = false }) {
3766
+ function Column({ title, chevron = false, titleMenu, actions, above, collapsed = false, children, className, broken = false }) {
3706
3767
  return /* @__PURE__ */ jsxs42(
3707
3768
  "div",
3708
3769
  {
@@ -3714,7 +3775,7 @@ function Column({ title, chevron = false, actions, above, collapsed = false, chi
3714
3775
  ),
3715
3776
  "data-collapsed": collapsed || void 0,
3716
3777
  children: [
3717
- /* @__PURE__ */ jsx60(ContainerHeader, { title, chevron, actions }),
3778
+ /* @__PURE__ */ jsx60(ContainerHeader, { title, chevron, titleMenu, actions }),
3718
3779
  above,
3719
3780
  broken ? (
3720
3781
  // The message takes the room the list had and centres itself in it (Katerina, 19 September).