@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/AppShell.d.ts.map +1 -1
- package/dist/Avatar.d.ts.map +1 -1
- package/dist/AvatarGroup.d.ts +14 -5
- package/dist/AvatarGroup.d.ts.map +1 -1
- package/dist/ChipInput.d.ts.map +1 -1
- package/dist/IdentityMenu.d.ts +6 -1
- package/dist/IdentityMenu.d.ts.map +1 -1
- package/dist/Menu.d.ts +59 -6
- package/dist/Menu.d.ts.map +1 -1
- package/dist/Select.d.ts +2 -31
- package/dist/Select.d.ts.map +1 -1
- package/dist/Tooltip.d.ts.map +1 -1
- package/dist/fit.d.ts +98 -0
- package/dist/fit.d.ts.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +252 -97
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/src/AppShell.tsx +8 -1
- package/src/Avatar.tsx +14 -1
- package/src/AvatarGroup.stories.tsx +5 -0
- package/src/AvatarGroup.tsx +36 -13
- package/src/ChipInput.tsx +15 -3
- package/src/IdentityMenu.tsx +14 -4
- package/src/Menu.fit.test.ts +90 -0
- package/src/Menu.stories.tsx +25 -2
- package/src/Menu.tsx +204 -22
- package/src/Select.fit.test.ts +12 -8
- package/src/Select.tsx +8 -32
- package/src/Tooltip.tsx +7 -1
- package/src/fit.ts +94 -0
- package/src/index.ts +2 -1
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
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
284
|
-
const
|
|
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
|
-
|
|
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
|
|
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] =
|
|
396
|
-
const [highlight, setHighlight] =
|
|
397
|
-
const [isFocused, setIsFocused] =
|
|
398
|
-
const [anchorRect, setAnchorRect] =
|
|
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
|
-
|
|
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]
|
|
653
|
+
className: "fixed z-[60] overflow-y-auto bg-bg-elevated border border-border-default rounded-lg shadow-lg",
|
|
500
654
|
style: {
|
|
501
|
-
|
|
502
|
-
|
|
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
|
|
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
|
|
694
|
+
var GAP2 = 6;
|
|
537
695
|
var VIEWPORT_PAD = 8;
|
|
538
696
|
function WithTooltip({ label, placement = "top", wrapperClassName, children }) {
|
|
539
|
-
const [show, setShow] =
|
|
697
|
+
const [show, setShow] = useState4(false);
|
|
540
698
|
const ref = useRef3(null);
|
|
541
699
|
const tooltipRef = useRef3(null);
|
|
542
|
-
const [style, setStyle] =
|
|
543
|
-
const reposition =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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] =
|
|
716
|
-
|
|
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 =
|
|
916
|
+
var FieldControlIdContext = createContext2(void 0);
|
|
754
917
|
function useFieldControlId(ownId) {
|
|
755
|
-
const fromField =
|
|
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
|
|
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 =
|
|
800
|
-
const menuRef =
|
|
801
|
-
const [rect, setRect] =
|
|
802
|
-
const [placement, setPlacement] =
|
|
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] =
|
|
806
|
-
const close =
|
|
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 =
|
|
958
|
+
const openMenu = useCallback3(() => {
|
|
811
959
|
setActiveIndex(selectedIndex);
|
|
812
960
|
setRect(triggerRef.current?.getBoundingClientRect() ?? null);
|
|
813
961
|
}, [selectedIndex]);
|
|
814
|
-
|
|
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
|
|
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] =
|
|
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
|
|
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] =
|
|
1111
|
-
const [draft, setDraft] =
|
|
1112
|
-
const [busy, setBusy] =
|
|
1113
|
-
const fieldRef =
|
|
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
|
|
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 =
|
|
1245
|
-
const labelRefs =
|
|
1246
|
-
const [truncated, setTruncated] =
|
|
1247
|
-
const measure =
|
|
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
|
-
|
|
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
|
|
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] =
|
|
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
|
|
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 =
|
|
1706
|
+
var ToastContext = createContext3(null);
|
|
1556
1707
|
var toastSeq = 0;
|
|
1557
1708
|
function ToastProvider({ children }) {
|
|
1558
|
-
const [toast, setToast] =
|
|
1559
|
-
const showToast =
|
|
1709
|
+
const [toast, setToast] = useState11(null);
|
|
1710
|
+
const showToast = useCallback5((opts) => {
|
|
1560
1711
|
setToast({ ...opts, id: ++toastSeq });
|
|
1561
1712
|
}, []);
|
|
1562
|
-
const dismissToast =
|
|
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 =
|
|
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,
|