@estiva-app/ui 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -75,16 +75,25 @@ function AppShell({ variant = "solid", menu, logo, search, identity, banner, nav
75
75
  ] })
76
76
  ] });
77
77
  }
78
- return /* @__PURE__ */ jsxs2("div", { className: "flex h-full min-h-0 flex-col bg-bg-base text-text-primary", children: [
79
- bar,
80
- /* @__PURE__ */ jsxs2("div", { className: "flex min-h-0 flex-1", children: [
81
- nav,
82
- /* @__PURE__ */ jsxs2("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col", children: [
83
- banner,
84
- /* @__PURE__ */ jsx2("main", { className: "min-w-0 flex-1 overflow-y-auto", children })
78
+ return (
79
+ /* `relative overflow-hidden` is the seal the floating manner already has,
80
+ and it takes both halves: an absolutely positioned descendant with no
81
+ positioned ancestor belongs to the *viewport*, so a scroll container
82
+ never clips it and the document itself gains its position as scroll
83
+ range — the whole page scrolls, navigation and top bar included.
84
+ `relative` claims such strays for the shell; `overflow-hidden` clips
85
+ them at its edge. Either alone seals nothing. */
86
+ /* @__PURE__ */ jsxs2("div", { className: "relative flex h-full min-h-0 flex-col overflow-hidden bg-bg-base text-text-primary", children: [
87
+ bar,
88
+ /* @__PURE__ */ jsxs2("div", { className: "flex min-h-0 flex-1", children: [
89
+ nav,
90
+ /* @__PURE__ */ jsxs2("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col", children: [
91
+ banner,
92
+ /* @__PURE__ */ jsx2("main", { className: "min-w-0 flex-1 overflow-y-auto", children })
93
+ ] })
85
94
  ] })
86
95
  ] })
87
- ] });
96
+ );
88
97
  }
89
98
 
90
99
  // src/Avatar.tsx
@@ -109,7 +118,7 @@ function Avatar({ src, name, alt = "", size = 36, className }) {
109
118
  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(
110
119
  "div",
111
120
  {
112
- className: "w-full h-full flex items-center justify-center font-semibold",
121
+ className: "w-full h-full flex items-center justify-center font-semibold leading-none",
113
122
  style: {
114
123
  color: "#08121c",
115
124
  fontSize: Math.round(size * 0.36),
@@ -122,18 +131,25 @@ function Avatar({ src, name, alt = "", size = 36, className }) {
122
131
 
123
132
  // src/AvatarGroup.tsx
124
133
  import { jsx as jsx4 } from "react/jsx-runtime";
125
- function AvatarGroup({ members }) {
134
+ function AvatarGroup({ members, size = 24 }) {
126
135
  const visible = members.slice(0, 3);
127
- return /* @__PURE__ */ jsx4("div", { className: "flex items-center pr-2", children: visible.map((member, i) => /* @__PURE__ */ jsx4(
128
- Avatar,
129
- {
130
- size: 24,
131
- name: member.name,
132
- src: member.picture,
133
- alt: member.name,
134
- className: "-mr-2 relative border-2 border-bg-surface"
135
- },
136
- i
136
+ const overlap = Math.round(size / 3);
137
+ const ring = size / 12;
138
+ return /* @__PURE__ */ jsx4("div", { className: "flex items-center", style: { paddingRight: overlap }, children: visible.map((member, i) => (
139
+ /*
140
+ The wrapper carries the overlap and the ring; the face carries
141
+ neither. It must never clip — see the note at the top of the file —
142
+ so it has the avatar's own radius and no overflow of its own.
143
+ */
144
+ /* @__PURE__ */ jsx4(
145
+ "span",
146
+ {
147
+ className: "relative flex rounded-sm",
148
+ style: { marginRight: -overlap, boxShadow: `0 0 0 ${ring}px var(--bg-surface)` },
149
+ children: /* @__PURE__ */ jsx4(Avatar, { size, name: member.name, src: member.picture, alt: member.name })
150
+ },
151
+ i
152
+ )
137
153
  )) });
138
154
  }
139
155
 
@@ -238,12 +254,48 @@ function Chip({ type = "neutral", label, leadingIcon, trailingIcon, className })
238
254
  }
239
255
 
240
256
  // src/ChipInput.tsx
241
- import { useState as useState2, useRef as useRef2, useMemo, useEffect as useEffect2, useLayoutEffect } from "react";
257
+ import { useState as useState3, useRef as useRef2, useMemo, useEffect as useEffect2, useLayoutEffect as useLayoutEffect2 } from "react";
242
258
  import { createPortal as createPortal2 } from "react-dom";
243
259
  import { IconX } from "@tabler/icons-react";
244
260
 
261
+ // src/fit.ts
262
+ var MARGIN = 8;
263
+ var GAP = 4;
264
+ function fitMenu({
265
+ anchor,
266
+ menu,
267
+ viewport,
268
+ cap = Number.POSITIVE_INFINITY
269
+ }) {
270
+ const left = Math.max(MARGIN, Math.min(anchor.left, viewport.width - menu.width - MARGIN));
271
+ const below = viewport.height - anchor.bottom - GAP - MARGIN;
272
+ const above = anchor.top - GAP - MARGIN;
273
+ const openUp = below < Math.min(menu.contentHeight, cap) && above > below;
274
+ const maxHeight = Math.max(Math.min(cap, openUp ? above : below), 120);
275
+ return openUp ? { left, bottom: viewport.height - anchor.top + GAP, maxHeight } : { left, top: anchor.bottom + GAP, maxHeight };
276
+ }
277
+ function clampBox({
278
+ box,
279
+ viewport
280
+ }) {
281
+ const left = Math.max(MARGIN, Math.min(box.left, viewport.width - box.width - MARGIN));
282
+ const maxHeight = Math.max(Math.min(box.height, viewport.height - 2 * MARGIN), 120);
283
+ const top = Math.max(MARGIN, Math.min(box.top, viewport.height - maxHeight - MARGIN));
284
+ return { left, top, maxHeight };
285
+ }
286
+ function fitSubmenu({
287
+ row,
288
+ panel,
289
+ viewport
290
+ }) {
291
+ const fitsRight = row.right + GAP + panel.width <= viewport.width - MARGIN;
292
+ const left = Math.max(MARGIN, fitsRight ? row.right + GAP : row.left - GAP - panel.width);
293
+ const top = Math.max(MARGIN, Math.min(row.top, viewport.height - panel.height - MARGIN));
294
+ return { left, top };
295
+ }
296
+
245
297
  // src/Menu.tsx
246
- import { useEffect, useRef } from "react";
298
+ import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useRef, useState as useState2 } from "react";
247
299
  import { IconChevronRight } from "@tabler/icons-react";
248
300
  import { createPortal } from "react-dom";
249
301
 
@@ -264,8 +316,19 @@ function SectionLabel({ children, className }) {
264
316
 
265
317
  // src/Menu.tsx
266
318
  import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
267
- function Menu({ onClose, position, children, className }) {
319
+ var MenuHoverContext = createContext(null);
320
+ function Menu({ onClose, anchor, align = "left", position, closeOnLeave = false, children, className }) {
268
321
  const ref = useRef(null);
322
+ const leaveTimer = useRef(void 0);
323
+ const hold = useCallback(() => clearTimeout(leaveTimer.current), []);
324
+ const release = useCallback(() => {
325
+ if (!closeOnLeave) return;
326
+ clearTimeout(leaveTimer.current);
327
+ leaveTimer.current = setTimeout(onClose, 150);
328
+ }, [closeOnLeave, onClose]);
329
+ useEffect(() => () => clearTimeout(leaveTimer.current), []);
330
+ const portalled = Boolean(anchor || position);
331
+ const [placed, setPlaced] = useState2(null);
269
332
  useEffect(() => {
270
333
  const onDown = (event) => {
271
334
  if (!ref.current?.contains(event.target)) onClose();
@@ -280,8 +343,46 @@ function Menu({ onClose, position, children, className }) {
280
343
  document.removeEventListener("keydown", onKey);
281
344
  };
282
345
  }, [onClose]);
283
- const style = position ? { top: position.top, ..."left" in position ? { left: position.left } : { right: position.right } } : void 0;
284
- const node = /* @__PURE__ */ jsx9(
346
+ const posLeft = position && "left" in position ? position.left : void 0;
347
+ const posRight = position && "right" in position ? position.right : void 0;
348
+ const posTop = position?.top;
349
+ useLayoutEffect(() => {
350
+ if (!portalled || !ref.current) return;
351
+ const viewport = { width: window.innerWidth, height: window.innerHeight };
352
+ const menu = ref.current;
353
+ if (anchor) {
354
+ const rect = anchor instanceof Element ? anchor.getBoundingClientRect() : anchor;
355
+ const left = align === "right" ? rect.right - menu.offsetWidth : rect.left;
356
+ setPlaced(
357
+ fitMenu({
358
+ anchor: { left, top: rect.top, bottom: rect.bottom },
359
+ menu: { width: menu.offsetWidth, contentHeight: menu.scrollHeight },
360
+ viewport
361
+ })
362
+ );
363
+ } else if (posTop !== void 0) {
364
+ const left = posLeft ?? viewport.width - (posRight ?? 0) - menu.offsetWidth;
365
+ setPlaced(clampBox({ box: { left, top: posTop, width: menu.offsetWidth, height: menu.offsetHeight }, viewport }));
366
+ }
367
+ }, [portalled, anchor, align, posLeft, posRight, posTop]);
368
+ useEffect(() => {
369
+ if (!anchor) return;
370
+ const onScroll = (event) => {
371
+ if (event.target instanceof Node && ref.current?.contains(event.target)) return;
372
+ onClose();
373
+ };
374
+ window.addEventListener("resize", onClose);
375
+ window.addEventListener("scroll", onScroll, true);
376
+ return () => {
377
+ window.removeEventListener("resize", onClose);
378
+ window.removeEventListener("scroll", onScroll, true);
379
+ };
380
+ }, [anchor, onClose]);
381
+ const style = portalled ? placed ? { left: placed.left, top: placed.top, bottom: placed.bottom, maxHeight: placed.maxHeight } : (
382
+ // The provisional render: measured by the layout effect, never seen.
383
+ { left: 0, top: 0, visibility: "hidden" }
384
+ ) : void 0;
385
+ const node = /* @__PURE__ */ jsx9(MenuHoverContext.Provider, { value: { hold, release }, children: /* @__PURE__ */ jsx9(
285
386
  "div",
286
387
  {
287
388
  ref,
@@ -289,15 +390,68 @@ function Menu({ onClose, position, children, className }) {
289
390
  "data-interactive": true,
290
391
  className: cn(
291
392
  "z-50 flex min-w-[180px] flex-col rounded-lg border border-border-default bg-bg-elevated p-2 shadow-lg",
292
- position ? "fixed" : "absolute right-0 top-full mt-1",
393
+ portalled ? "fixed overflow-y-auto" : "absolute right-0 top-full mt-1",
293
394
  className
294
395
  ),
295
396
  style,
296
397
  onClick: (event) => event.stopPropagation(),
398
+ onMouseEnter: closeOnLeave ? hold : void 0,
399
+ onMouseLeave: closeOnLeave ? release : void 0,
297
400
  children
298
401
  }
299
- );
300
- return position ? createPortal(node, document.body) : node;
402
+ ) });
403
+ return portalled ? createPortal(node, document.body) : node;
404
+ }
405
+ function MenuSub({ label, leading, selected, children, className }) {
406
+ const [open, setOpen] = useState2(false);
407
+ const rowRef = useRef(null);
408
+ const panelRef = useRef(null);
409
+ const closeTimer = useRef(void 0);
410
+ const [placed, setPlaced] = useState2(null);
411
+ const menuHover = useContext(MenuHoverContext);
412
+ const enter = () => {
413
+ clearTimeout(closeTimer.current);
414
+ menuHover?.hold();
415
+ setOpen(true);
416
+ };
417
+ const leave = () => {
418
+ closeTimer.current = setTimeout(() => setOpen(false), 150);
419
+ menuHover?.release();
420
+ };
421
+ useEffect(() => () => clearTimeout(closeTimer.current), []);
422
+ useLayoutEffect(() => {
423
+ if (!open || !rowRef.current || !panelRef.current) {
424
+ setPlaced(null);
425
+ return;
426
+ }
427
+ const row = rowRef.current.getBoundingClientRect();
428
+ setPlaced(
429
+ fitSubmenu({
430
+ row: { left: row.left, right: row.right, top: row.top },
431
+ panel: { width: panelRef.current.offsetWidth, height: panelRef.current.offsetHeight },
432
+ viewport: { width: window.innerWidth, height: window.innerHeight }
433
+ })
434
+ );
435
+ }, [open]);
436
+ return /* @__PURE__ */ jsxs5("div", { ref: rowRef, onMouseEnter: enter, onMouseLeave: leave, children: [
437
+ /* @__PURE__ */ jsx9(MenuItem, { label, leading, selected, submenu: true }),
438
+ open && createPortal(
439
+ /* @__PURE__ */ jsx9(
440
+ "div",
441
+ {
442
+ ref: panelRef,
443
+ role: "menu",
444
+ "data-interactive": true,
445
+ className: cn("fixed z-50 flex w-[160px] flex-col rounded-lg border border-border-default bg-bg-elevated p-2 shadow-lg", className),
446
+ style: placed ?? { left: 0, top: 0, visibility: "hidden" },
447
+ onMouseEnter: enter,
448
+ onMouseLeave: leave,
449
+ children
450
+ }
451
+ ),
452
+ document.body
453
+ )
454
+ ] });
301
455
  }
302
456
  function MenuItem({ label, children, size = "default", description, leading, trailing, shortcut, submenu, destructive, selected, className, ...props }) {
303
457
  const edge = trailing ?? (shortcut ? /* @__PURE__ */ jsx9("kbd", { className: "inline-flex shrink-0 items-center justify-center rounded-sm border border-border-strong bg-bg-inset px-1 py-px text-caption text-text-secondary", children: shortcut }) : submenu ? /* @__PURE__ */ jsx9(IconChevronRight, { size: 16, stroke: 1.5, className: "shrink-0 text-text-muted" }) : null);
@@ -392,10 +546,10 @@ function ChipInput({
392
546
  chipLeading,
393
547
  rowLeading
394
548
  }) {
395
- const [query, setQuery] = useState2("");
396
- const [highlight, setHighlight] = useState2(0);
397
- const [isFocused, setIsFocused] = useState2(false);
398
- const [anchorRect, setAnchorRect] = useState2(null);
549
+ const [query, setQuery] = useState3("");
550
+ const [highlight, setHighlight] = useState3(0);
551
+ const [isFocused, setIsFocused] = useState3(false);
552
+ const [anchorRect, setAnchorRect] = useState3(null);
399
553
  const wrapperRef = useRef2(null);
400
554
  const inputRef = useRef2(null);
401
555
  const matches = useMemo(() => {
@@ -413,7 +567,7 @@ function ChipInput({
413
567
  setHighlight(0);
414
568
  }, [query, matches.length]);
415
569
  const showDropdown = isFocused && query.trim().length > 0 && matches.length > 0;
416
- useLayoutEffect(() => {
570
+ useLayoutEffect2(() => {
417
571
  if (!showDropdown) return;
418
572
  const update = () => {
419
573
  if (wrapperRef.current) setAnchorRect(wrapperRef.current.getBoundingClientRect());
@@ -496,10 +650,14 @@ function ChipInput({
496
650
  /* @__PURE__ */ jsx10(
497
651
  "div",
498
652
  {
499
- className: "fixed z-[60] max-h-[240px] overflow-y-auto bg-bg-elevated border border-border-default rounded-lg shadow-lg",
653
+ className: "fixed z-[60] overflow-y-auto bg-bg-elevated border border-border-default rounded-lg shadow-lg",
500
654
  style: {
501
- top: anchorRect.bottom + 4,
502
- left: anchorRect.left,
655
+ ...fitMenu({
656
+ anchor: { left: anchorRect.left, top: anchorRect.top, bottom: anchorRect.bottom },
657
+ menu: { width: anchorRect.width, contentHeight: matches.length * 48 },
658
+ viewport: { width: window.innerWidth, height: window.innerHeight },
659
+ cap: 240
660
+ }),
503
661
  width: anchorRect.width
504
662
  },
505
663
  children: matches.map((o, i) => /* @__PURE__ */ jsx10(
@@ -527,31 +685,34 @@ function ChipInput({
527
685
  }
528
686
 
529
687
  // src/Tooltip.tsx
530
- import { useCallback, useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as useState3 } from "react";
688
+ import { useCallback as useCallback2, useLayoutEffect as useLayoutEffect3, useRef as useRef3, useState as useState4 } from "react";
531
689
  import { createPortal as createPortal3 } from "react-dom";
532
690
  import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
533
691
  function Tooltip({ label, className }) {
534
692
  return /* @__PURE__ */ jsx11("div", { role: "tooltip", className: cn("bg-bg-elevated border border-border-default rounded-lg h-[30px] flex items-center justify-center px-2 shadow-lg", className), children: /* @__PURE__ */ jsx11("span", { className: "text-caption text-text-primary whitespace-nowrap", children: label }) });
535
693
  }
536
- var GAP = 6;
694
+ var GAP2 = 6;
537
695
  var VIEWPORT_PAD = 8;
538
696
  function WithTooltip({ label, placement = "top", wrapperClassName, children }) {
539
- const [show, setShow] = useState3(false);
697
+ const [show, setShow] = useState4(false);
540
698
  const ref = useRef3(null);
541
699
  const tooltipRef = useRef3(null);
542
- const [style, setStyle] = useState3({ position: "fixed", zIndex: 9999, pointerEvents: "none", visibility: "hidden" });
543
- const reposition = useCallback(() => {
700
+ const [style, setStyle] = useState4({ position: "fixed", zIndex: 9999, pointerEvents: "none", visibility: "hidden" });
701
+ const reposition = useCallback2(() => {
544
702
  const trigger = ref.current;
545
703
  const tip = tooltipRef.current;
546
704
  if (!trigger || !tip) return;
547
705
  const triggerRect = trigger.getBoundingClientRect();
548
706
  const tipRect = tip.getBoundingClientRect();
549
- const top = placement === "bottom" ? triggerRect.bottom + GAP : triggerRect.top - tipRect.height - GAP;
707
+ let top = placement === "bottom" ? triggerRect.bottom + GAP2 : triggerRect.top - tipRect.height - GAP2;
708
+ if (placement === "top" && top < VIEWPORT_PAD) top = triggerRect.bottom + GAP2;
709
+ else if (placement === "bottom" && top + tipRect.height > window.innerHeight - VIEWPORT_PAD) top = triggerRect.top - tipRect.height - GAP2;
710
+ top = Math.max(VIEWPORT_PAD, Math.min(top, window.innerHeight - tipRect.height - VIEWPORT_PAD));
550
711
  let left = triggerRect.left + triggerRect.width / 2 - tipRect.width / 2;
551
712
  left = Math.max(VIEWPORT_PAD, Math.min(left, window.innerWidth - tipRect.width - VIEWPORT_PAD));
552
713
  setStyle({ position: "fixed", top, left, zIndex: 9999, pointerEvents: "none", visibility: "visible" });
553
714
  }, [placement]);
554
- useLayoutEffect2(() => {
715
+ useLayoutEffect3(() => {
555
716
  if (show) reposition();
556
717
  }, [show, reposition]);
557
718
  return /* @__PURE__ */ jsxs7("div", { ref, className: cn("inline-flex shrink-0", wrapperClassName), onMouseEnter: () => setShow(true), onMouseLeave: () => setShow(false), children: [
@@ -602,7 +763,7 @@ function IconButton({
602
763
  }
603
764
 
604
765
  // src/IdentityMenu.tsx
605
- import { useState as useState4 } from "react";
766
+ import { useRef as useRef4, useState as useState5 } from "react";
606
767
 
607
768
  // src/Divider.tsx
608
769
  import { jsx as jsx13 } from "react/jsx-runtime";
@@ -673,13 +834,13 @@ function PersonTrigger({ name, picture, fallback, size, open = false, compact =
673
834
 
674
835
  // src/IdentityMenu.tsx
675
836
  import { Fragment, jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
676
- function IdentityPanel({ me, signedIn, relayUrl, idBase, onCopyKey, onSignOut, onClose, className, children }) {
837
+ function IdentityPanel({ me, signedIn, relayUrl, idBase, onCopyKey, onSignOut, onClose, anchor, className, children }) {
677
838
  const act = (action) => () => {
678
839
  onClose();
679
840
  action();
680
841
  };
681
842
  const appRows = typeof children === "function" ? children(onClose) : children;
682
- return /* @__PURE__ */ jsxs10(Menu, { onClose, className: cn("w-72", className), children: [
843
+ return /* @__PURE__ */ jsxs10(Menu, { onClose, anchor, align: "right", className: cn("w-72", className), children: [
683
844
  /* @__PURE__ */ jsxs10(MenuSection, { label: signedIn ? "Signed in as" : "Acting as", children: [
684
845
  /* @__PURE__ */ jsx16(MenuRow, { children: /* @__PURE__ */ jsx16(Person, { name: me.name, picture: me.picture, fallback: "Anonymous", size: 28, className: "text-body-2-strong" }) }),
685
846
  /* @__PURE__ */ jsx16(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." }),
@@ -712,8 +873,9 @@ function IdentityPanel({ me, signedIn, relayUrl, idBase, onCopyKey, onSignOut, o
712
873
  ] });
713
874
  }
714
875
  function IdentityMenu({ me, signedIn, relayUrl, idBase, onCopyKey, onSignOut, compact = false, className, children }) {
715
- const [open, setOpen] = useState4(false);
716
- return /* @__PURE__ */ jsxs10("div", { className: cn("relative", className), children: [
876
+ const [open, setOpen] = useState5(false);
877
+ const anchorRef = useRef4(null);
878
+ return /* @__PURE__ */ jsxs10("div", { ref: anchorRef, className: cn("relative", className), children: [
717
879
  /* @__PURE__ */ jsx16(
718
880
  PersonTrigger,
719
881
  {
@@ -738,6 +900,7 @@ function IdentityMenu({ me, signedIn, relayUrl, idBase, onCopyKey, onSignOut, co
738
900
  onCopyKey,
739
901
  onSignOut,
740
902
  onClose: () => setOpen(false),
903
+ anchor: anchorRef.current,
741
904
  children
742
905
  }
743
906
  )
@@ -748,11 +911,11 @@ function Note({ children }) {
748
911
  }
749
912
 
750
913
  // src/Field.tsx
751
- import { createContext, useContext, useId } from "react";
914
+ import { createContext as createContext2, useContext as useContext2, useId } from "react";
752
915
  import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
753
- var FieldControlIdContext = createContext(void 0);
916
+ var FieldControlIdContext = createContext2(void 0);
754
917
  function useFieldControlId(ownId) {
755
- const fromField = useContext(FieldControlIdContext);
918
+ const fromField = useContext2(FieldControlIdContext);
756
919
  return fromField ?? ownId;
757
920
  }
758
921
  function Field({ label, required = false, htmlFor, children }) {
@@ -776,42 +939,27 @@ function Field({ label, required = false, htmlFor, children }) {
776
939
 
777
940
  // src/Select.tsx
778
941
  import { IconCheck as IconCheck2, IconChevronDown as IconChevronDown2 } from "@tabler/icons-react";
779
- import { useCallback as useCallback2, useEffect as useEffect3, useId as useId2, useLayoutEffect as useLayoutEffect3, useMemo as useMemo2, useRef as useRef4, useState as useState5 } from "react";
942
+ import { useCallback as useCallback3, useEffect as useEffect3, useId as useId2, useLayoutEffect as useLayoutEffect4, useMemo as useMemo2, useRef as useRef5, useState as useState6 } from "react";
780
943
  import { createPortal as createPortal4 } from "react-dom";
781
944
  import { Fragment as Fragment2, jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
782
- function fitMenu({
783
- anchor,
784
- menu,
785
- viewport
786
- }) {
787
- const MARGIN = 8;
788
- const GAP2 = 4;
789
- const CAP = 288;
790
- const left = Math.max(MARGIN, Math.min(anchor.left, viewport.width - menu.width - MARGIN));
791
- const below = viewport.height - anchor.bottom - GAP2 - MARGIN;
792
- const above = anchor.top - GAP2 - MARGIN;
793
- const openUp = below < Math.min(menu.contentHeight, CAP) && above > below;
794
- const maxHeight = Math.max(Math.min(CAP, openUp ? above : below), 120);
795
- return openUp ? { left, bottom: viewport.height - anchor.top + GAP2, maxHeight } : { left, top: anchor.bottom + GAP2, maxHeight };
796
- }
797
945
  function Select({ value, onChange, options, size = "default", ariaLabel, placeholder = "Select\u2026", disabled, className }) {
798
946
  const id = useId2();
799
- const triggerRef = useRef4(null);
800
- const menuRef = useRef4(null);
801
- const [rect, setRect] = useState5(null);
802
- const [placement, setPlacement] = useState5(null);
947
+ const triggerRef = useRef5(null);
948
+ const menuRef = useRef5(null);
949
+ const [rect, setRect] = useState6(null);
950
+ const [placement, setPlacement] = useState6(null);
803
951
  const open = rect !== null;
804
952
  const selectedIndex = useMemo2(() => Math.max(0, options.findIndex((o) => o.value === value)), [options, value]);
805
- const [activeIndex, setActiveIndex] = useState5(selectedIndex);
806
- const close = useCallback2((refocus) => {
953
+ const [activeIndex, setActiveIndex] = useState6(selectedIndex);
954
+ const close = useCallback3((refocus) => {
807
955
  setRect(null);
808
956
  if (refocus) triggerRef.current?.focus();
809
957
  }, []);
810
- const openMenu = useCallback2(() => {
958
+ const openMenu = useCallback3(() => {
811
959
  setActiveIndex(selectedIndex);
812
960
  setRect(triggerRef.current?.getBoundingClientRect() ?? null);
813
961
  }, [selectedIndex]);
814
- useLayoutEffect3(() => {
962
+ useLayoutEffect4(() => {
815
963
  if (!rect || !menuRef.current) {
816
964
  setPlacement(null);
817
965
  return;
@@ -820,7 +968,10 @@ function Select({ value, onChange, options, size = "default", ariaLabel, placeho
820
968
  fitMenu({
821
969
  anchor: { left: rect.left, top: rect.top, bottom: rect.bottom },
822
970
  menu: { width: menuRef.current.offsetWidth, contentHeight: menuRef.current.scrollHeight },
823
- viewport: { width: window.innerWidth, height: window.innerHeight }
971
+ viewport: { width: window.innerWidth, height: window.innerHeight },
972
+ // The option list keeps its classic height (the old max-h-72); a menu
973
+ // panel passes no cap and stands as tall as the room it opens into.
974
+ cap: 288
824
975
  })
825
976
  );
826
977
  }, [rect, options.length]);
@@ -1034,7 +1185,7 @@ var Textarea = forwardRef2(function Textarea2({ className, id, ...props }, ref)
1034
1185
  });
1035
1186
 
1036
1187
  // src/ConfirmDialog.tsx
1037
- import { useState as useState6 } from "react";
1188
+ import { useState as useState7 } from "react";
1038
1189
 
1039
1190
  // src/DialogShell.tsx
1040
1191
  import { useEffect as useEffect4 } from "react";
@@ -1078,7 +1229,7 @@ function DialogShell({ title, onClose, headerContent, footer, children, bodyClas
1078
1229
  // src/ConfirmDialog.tsx
1079
1230
  import { Fragment as Fragment4, jsx as jsx22, jsxs as jsxs14 } from "react/jsx-runtime";
1080
1231
  function ConfirmDialog({ title, children, confirmLabel, destructive = false, onConfirm, onClose }) {
1081
- const [busy, setBusy] = useState6(false);
1232
+ const [busy, setBusy] = useState7(false);
1082
1233
  const confirm = async () => {
1083
1234
  setBusy(true);
1084
1235
  try {
@@ -1104,13 +1255,13 @@ function ConfirmDialog({ title, children, confirmLabel, destructive = false, onC
1104
1255
  }
1105
1256
 
1106
1257
  // src/EditableText.tsx
1107
- import { useEffect as useEffect5, useRef as useRef5, useState as useState7 } from "react";
1258
+ import { useEffect as useEffect5, useRef as useRef6, useState as useState8 } from "react";
1108
1259
  import { jsx as jsx23 } from "react/jsx-runtime";
1109
1260
  function EditableText({ value, display, displayNode, placeholder, onCommit, multiline = false, className, label, readOnly = false }) {
1110
- const [editing, setEditing] = useState7(false);
1111
- const [draft, setDraft] = useState7(value);
1112
- const [busy, setBusy] = useState7(false);
1113
- const fieldRef = useRef5(null);
1261
+ const [editing, setEditing] = useState8(false);
1262
+ const [draft, setDraft] = useState8(value);
1263
+ const [busy, setBusy] = useState8(false);
1264
+ const fieldRef = useRef6(null);
1114
1265
  useEffect5(() => {
1115
1266
  if (editing) {
1116
1267
  fieldRef.current?.focus();
@@ -1238,20 +1389,20 @@ function SkeletonList({ rows = 8, className }) {
1238
1389
  }
1239
1390
 
1240
1391
  // src/Breadcrumb.tsx
1241
- import { Fragment as Fragment5, useCallback as useCallback3, useLayoutEffect as useLayoutEffect4, useRef as useRef6, useState as useState8 } from "react";
1392
+ import { Fragment as Fragment5, useCallback as useCallback4, useLayoutEffect as useLayoutEffect5, useRef as useRef7, useState as useState9 } from "react";
1242
1393
  import { jsx as jsx26, jsxs as jsxs17 } from "react/jsx-runtime";
1243
1394
  function Breadcrumb({ items, className }) {
1244
- const navRef = useRef6(null);
1245
- const labelRefs = useRef6([]);
1246
- const [truncated, setTruncated] = useState8(/* @__PURE__ */ new Set());
1247
- const measure = useCallback3(() => {
1395
+ const navRef = useRef7(null);
1396
+ const labelRefs = useRef7([]);
1397
+ const [truncated, setTruncated] = useState9(/* @__PURE__ */ new Set());
1398
+ const measure = useCallback4(() => {
1248
1399
  const next = /* @__PURE__ */ new Set();
1249
1400
  labelRefs.current.forEach((el, index) => {
1250
1401
  if (el && el.scrollWidth > el.clientWidth) next.add(index);
1251
1402
  });
1252
1403
  setTruncated((prev) => prev.size === next.size && [...next].every((i) => prev.has(i)) ? prev : next);
1253
1404
  }, []);
1254
- useLayoutEffect4(() => {
1405
+ useLayoutEffect5(() => {
1255
1406
  measure();
1256
1407
  const nav = navRef.current;
1257
1408
  if (!nav || typeof ResizeObserver === "undefined") return;
@@ -1418,11 +1569,11 @@ function SearchInput({ shortcut, className, placeholder = "Search\u2026", ...pro
1418
1569
  }
1419
1570
 
1420
1571
  // src/SectionHeader.tsx
1421
- import { useState as useState9 } from "react";
1572
+ import { useState as useState10 } from "react";
1422
1573
  import { IconChevronRight as IconChevronRight2 } from "@tabler/icons-react";
1423
1574
  import { jsx as jsx33, jsxs as jsxs22 } from "react/jsx-runtime";
1424
1575
  function SectionHeader({ title, chevron = false, isExpanded = true, onToggle, actions, showActions = "hover", className }) {
1425
- const [isHovered, setIsHovered] = useState9(false);
1576
+ const [isHovered, setIsHovered] = useState10(false);
1426
1577
  return /* @__PURE__ */ jsxs22(
1427
1578
  "div",
1428
1579
  {
@@ -1498,7 +1649,7 @@ function Tabs({ tabs, active, onChange, size = "default", className }) {
1498
1649
  }
1499
1650
 
1500
1651
  // src/Toast.tsx
1501
- import { createContext as createContext2, useCallback as useCallback4, useContext as useContext2, useEffect as useEffect6, useMemo as useMemo3, useState as useState10 } from "react";
1652
+ import { createContext as createContext3, useCallback as useCallback5, useContext as useContext3, useEffect as useEffect6, useMemo as useMemo3, useState as useState11 } from "react";
1502
1653
  import { createPortal as createPortal6 } from "react-dom";
1503
1654
  import { IconCircleCheck } from "@tabler/icons-react";
1504
1655
  import { jsx as jsx35, jsxs as jsxs24 } from "react/jsx-runtime";
@@ -1552,14 +1703,14 @@ function Toast({ label, type = "neutral", leadingIcon = true, actionLabel, onAct
1552
1703
  }
1553
1704
  );
1554
1705
  }
1555
- var ToastContext = createContext2(null);
1706
+ var ToastContext = createContext3(null);
1556
1707
  var toastSeq = 0;
1557
1708
  function ToastProvider({ children }) {
1558
- const [toast, setToast] = useState10(null);
1559
- const showToast = useCallback4((opts) => {
1709
+ const [toast, setToast] = useState11(null);
1710
+ const showToast = useCallback5((opts) => {
1560
1711
  setToast({ ...opts, id: ++toastSeq });
1561
1712
  }, []);
1562
- const dismissToast = useCallback4(() => {
1713
+ const dismissToast = useCallback5(() => {
1563
1714
  setToast(null);
1564
1715
  }, []);
1565
1716
  useEffect6(() => {
@@ -1594,7 +1745,7 @@ function ToastProvider({ children }) {
1594
1745
  ] });
1595
1746
  }
1596
1747
  function useToast() {
1597
- const ctx = useContext2(ToastContext);
1748
+ const ctx = useContext3(ToastContext);
1598
1749
  if (!ctx) throw new Error("useToast must be used within ToastProvider");
1599
1750
  return ctx;
1600
1751
  }
@@ -1623,6 +1774,7 @@ export {
1623
1774
  MenuItem,
1624
1775
  MenuRow,
1625
1776
  MenuSection,
1777
+ MenuSub,
1626
1778
  NavItem,
1627
1779
  Person,
1628
1780
  PersonTrigger,
@@ -1645,7 +1797,10 @@ export {
1645
1797
  Tooltip,
1646
1798
  TopBar,
1647
1799
  WithTooltip,
1800
+ clampBox,
1648
1801
  cn,
1802
+ fitMenu,
1803
+ fitSubmenu,
1649
1804
  hueFor,
1650
1805
  initialsFor,
1651
1806
  useFieldControlId,