@estiva-app/ui 0.3.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/Field.d.ts +32 -5
- package/dist/Field.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/TextInput.d.ts.map +1 -1
- package/dist/Textarea.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 +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +278 -103
- package/dist/index.js.map +4 -4
- package/package.json +5 -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/Field.test.tsx +117 -0
- package/src/Field.tsx +61 -7
- 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/TextInput.tsx +5 -1
- package/src/Textarea.tsx +5 -1
- package/src/Tooltip.tsx +7 -1
- package/src/fit.ts +94 -0
- package/src/index.ts +3 -2
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,55 +911,55 @@ function Note({ children }) {
|
|
|
748
911
|
}
|
|
749
912
|
|
|
750
913
|
// src/Field.tsx
|
|
914
|
+
import { createContext as createContext2, useContext as useContext2, useId } from "react";
|
|
751
915
|
import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
752
|
-
|
|
916
|
+
var FieldControlIdContext = createContext2(void 0);
|
|
917
|
+
function useFieldControlId(ownId) {
|
|
918
|
+
const fromField = useContext2(FieldControlIdContext);
|
|
919
|
+
return fromField ?? ownId;
|
|
920
|
+
}
|
|
921
|
+
function Field({ label, required = false, htmlFor, children }) {
|
|
922
|
+
const generated = useId();
|
|
923
|
+
const id = htmlFor ?? generated;
|
|
753
924
|
return /* @__PURE__ */ jsxs11("div", { className: "flex flex-col gap-2", children: [
|
|
754
|
-
/* @__PURE__ */ jsxs11(
|
|
755
|
-
label,
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
925
|
+
/* @__PURE__ */ jsxs11(
|
|
926
|
+
"label",
|
|
927
|
+
{
|
|
928
|
+
htmlFor: id,
|
|
929
|
+
className: `text-input-label text-text-primary${required ? " flex items-center" : ""}`,
|
|
930
|
+
children: [
|
|
931
|
+
label,
|
|
932
|
+
required && /* @__PURE__ */ jsx17("span", { className: "text-error-default ml-0.5", children: "*" })
|
|
933
|
+
]
|
|
934
|
+
}
|
|
935
|
+
),
|
|
936
|
+
/* @__PURE__ */ jsx17(FieldControlIdContext.Provider, { value: id, children })
|
|
759
937
|
] });
|
|
760
938
|
}
|
|
761
939
|
|
|
762
940
|
// src/Select.tsx
|
|
763
941
|
import { IconCheck as IconCheck2, IconChevronDown as IconChevronDown2 } from "@tabler/icons-react";
|
|
764
|
-
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";
|
|
765
943
|
import { createPortal as createPortal4 } from "react-dom";
|
|
766
944
|
import { Fragment as Fragment2, jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
767
|
-
function fitMenu({
|
|
768
|
-
anchor,
|
|
769
|
-
menu,
|
|
770
|
-
viewport
|
|
771
|
-
}) {
|
|
772
|
-
const MARGIN = 8;
|
|
773
|
-
const GAP2 = 4;
|
|
774
|
-
const CAP = 288;
|
|
775
|
-
const left = Math.max(MARGIN, Math.min(anchor.left, viewport.width - menu.width - MARGIN));
|
|
776
|
-
const below = viewport.height - anchor.bottom - GAP2 - MARGIN;
|
|
777
|
-
const above = anchor.top - GAP2 - MARGIN;
|
|
778
|
-
const openUp = below < Math.min(menu.contentHeight, CAP) && above > below;
|
|
779
|
-
const maxHeight = Math.max(Math.min(CAP, openUp ? above : below), 120);
|
|
780
|
-
return openUp ? { left, bottom: viewport.height - anchor.top + GAP2, maxHeight } : { left, top: anchor.bottom + GAP2, maxHeight };
|
|
781
|
-
}
|
|
782
945
|
function Select({ value, onChange, options, size = "default", ariaLabel, placeholder = "Select\u2026", disabled, className }) {
|
|
783
|
-
const id =
|
|
784
|
-
const triggerRef =
|
|
785
|
-
const menuRef =
|
|
786
|
-
const [rect, setRect] =
|
|
787
|
-
const [placement, setPlacement] =
|
|
946
|
+
const id = useId2();
|
|
947
|
+
const triggerRef = useRef5(null);
|
|
948
|
+
const menuRef = useRef5(null);
|
|
949
|
+
const [rect, setRect] = useState6(null);
|
|
950
|
+
const [placement, setPlacement] = useState6(null);
|
|
788
951
|
const open = rect !== null;
|
|
789
952
|
const selectedIndex = useMemo2(() => Math.max(0, options.findIndex((o) => o.value === value)), [options, value]);
|
|
790
|
-
const [activeIndex, setActiveIndex] =
|
|
791
|
-
const close =
|
|
953
|
+
const [activeIndex, setActiveIndex] = useState6(selectedIndex);
|
|
954
|
+
const close = useCallback3((refocus) => {
|
|
792
955
|
setRect(null);
|
|
793
956
|
if (refocus) triggerRef.current?.focus();
|
|
794
957
|
}, []);
|
|
795
|
-
const openMenu =
|
|
958
|
+
const openMenu = useCallback3(() => {
|
|
796
959
|
setActiveIndex(selectedIndex);
|
|
797
960
|
setRect(triggerRef.current?.getBoundingClientRect() ?? null);
|
|
798
961
|
}, [selectedIndex]);
|
|
799
|
-
|
|
962
|
+
useLayoutEffect4(() => {
|
|
800
963
|
if (!rect || !menuRef.current) {
|
|
801
964
|
setPlacement(null);
|
|
802
965
|
return;
|
|
@@ -805,7 +968,10 @@ function Select({ value, onChange, options, size = "default", ariaLabel, placeho
|
|
|
805
968
|
fitMenu({
|
|
806
969
|
anchor: { left: rect.left, top: rect.top, bottom: rect.bottom },
|
|
807
970
|
menu: { width: menuRef.current.offsetWidth, contentHeight: menuRef.current.scrollHeight },
|
|
808
|
-
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
|
|
809
975
|
})
|
|
810
976
|
);
|
|
811
977
|
}, [rect, options.length]);
|
|
@@ -974,11 +1140,13 @@ function Select({ value, onChange, options, size = "default", ariaLabel, placeho
|
|
|
974
1140
|
// src/TextInput.tsx
|
|
975
1141
|
import { forwardRef } from "react";
|
|
976
1142
|
import { jsx as jsx19 } from "react/jsx-runtime";
|
|
977
|
-
var TextInput = forwardRef(function TextInput2({ className, type = "text", ...props }, ref) {
|
|
1143
|
+
var TextInput = forwardRef(function TextInput2({ className, type = "text", id, ...props }, ref) {
|
|
1144
|
+
const controlId = useFieldControlId(id);
|
|
978
1145
|
return /* @__PURE__ */ jsx19(
|
|
979
1146
|
"input",
|
|
980
1147
|
{
|
|
981
1148
|
ref,
|
|
1149
|
+
id: controlId,
|
|
982
1150
|
type,
|
|
983
1151
|
className: cn(
|
|
984
1152
|
"bg-bg-inset border border-border-default focus:border-border-focus rounded-lg px-3 py-2",
|
|
@@ -996,11 +1164,13 @@ var TextInput = forwardRef(function TextInput2({ className, type = "text", ...pr
|
|
|
996
1164
|
// src/Textarea.tsx
|
|
997
1165
|
import { forwardRef as forwardRef2 } from "react";
|
|
998
1166
|
import { jsx as jsx20 } from "react/jsx-runtime";
|
|
999
|
-
var Textarea = forwardRef2(function Textarea2({ className, ...props }, ref) {
|
|
1167
|
+
var Textarea = forwardRef2(function Textarea2({ className, id, ...props }, ref) {
|
|
1168
|
+
const controlId = useFieldControlId(id);
|
|
1000
1169
|
return /* @__PURE__ */ jsx20(
|
|
1001
1170
|
"textarea",
|
|
1002
1171
|
{
|
|
1003
1172
|
ref,
|
|
1173
|
+
id: controlId,
|
|
1004
1174
|
className: cn(
|
|
1005
1175
|
"bg-bg-inset border border-border-default focus:border-border-focus rounded-lg px-3 py-2",
|
|
1006
1176
|
"text-[14px] leading-[1.4] font-normal text-text-primary placeholder:text-text-muted",
|
|
@@ -1015,7 +1185,7 @@ var Textarea = forwardRef2(function Textarea2({ className, ...props }, ref) {
|
|
|
1015
1185
|
});
|
|
1016
1186
|
|
|
1017
1187
|
// src/ConfirmDialog.tsx
|
|
1018
|
-
import { useState as
|
|
1188
|
+
import { useState as useState7 } from "react";
|
|
1019
1189
|
|
|
1020
1190
|
// src/DialogShell.tsx
|
|
1021
1191
|
import { useEffect as useEffect4 } from "react";
|
|
@@ -1059,7 +1229,7 @@ function DialogShell({ title, onClose, headerContent, footer, children, bodyClas
|
|
|
1059
1229
|
// src/ConfirmDialog.tsx
|
|
1060
1230
|
import { Fragment as Fragment4, jsx as jsx22, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
1061
1231
|
function ConfirmDialog({ title, children, confirmLabel, destructive = false, onConfirm, onClose }) {
|
|
1062
|
-
const [busy, setBusy] =
|
|
1232
|
+
const [busy, setBusy] = useState7(false);
|
|
1063
1233
|
const confirm = async () => {
|
|
1064
1234
|
setBusy(true);
|
|
1065
1235
|
try {
|
|
@@ -1085,13 +1255,13 @@ function ConfirmDialog({ title, children, confirmLabel, destructive = false, onC
|
|
|
1085
1255
|
}
|
|
1086
1256
|
|
|
1087
1257
|
// src/EditableText.tsx
|
|
1088
|
-
import { useEffect as useEffect5, useRef as
|
|
1258
|
+
import { useEffect as useEffect5, useRef as useRef6, useState as useState8 } from "react";
|
|
1089
1259
|
import { jsx as jsx23 } from "react/jsx-runtime";
|
|
1090
1260
|
function EditableText({ value, display, displayNode, placeholder, onCommit, multiline = false, className, label, readOnly = false }) {
|
|
1091
|
-
const [editing, setEditing] =
|
|
1092
|
-
const [draft, setDraft] =
|
|
1093
|
-
const [busy, setBusy] =
|
|
1094
|
-
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);
|
|
1095
1265
|
useEffect5(() => {
|
|
1096
1266
|
if (editing) {
|
|
1097
1267
|
fieldRef.current?.focus();
|
|
@@ -1219,20 +1389,20 @@ function SkeletonList({ rows = 8, className }) {
|
|
|
1219
1389
|
}
|
|
1220
1390
|
|
|
1221
1391
|
// src/Breadcrumb.tsx
|
|
1222
|
-
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";
|
|
1223
1393
|
import { jsx as jsx26, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
1224
1394
|
function Breadcrumb({ items, className }) {
|
|
1225
|
-
const navRef =
|
|
1226
|
-
const labelRefs =
|
|
1227
|
-
const [truncated, setTruncated] =
|
|
1228
|
-
const measure =
|
|
1395
|
+
const navRef = useRef7(null);
|
|
1396
|
+
const labelRefs = useRef7([]);
|
|
1397
|
+
const [truncated, setTruncated] = useState9(/* @__PURE__ */ new Set());
|
|
1398
|
+
const measure = useCallback4(() => {
|
|
1229
1399
|
const next = /* @__PURE__ */ new Set();
|
|
1230
1400
|
labelRefs.current.forEach((el, index) => {
|
|
1231
1401
|
if (el && el.scrollWidth > el.clientWidth) next.add(index);
|
|
1232
1402
|
});
|
|
1233
1403
|
setTruncated((prev) => prev.size === next.size && [...next].every((i) => prev.has(i)) ? prev : next);
|
|
1234
1404
|
}, []);
|
|
1235
|
-
|
|
1405
|
+
useLayoutEffect5(() => {
|
|
1236
1406
|
measure();
|
|
1237
1407
|
const nav = navRef.current;
|
|
1238
1408
|
if (!nav || typeof ResizeObserver === "undefined") return;
|
|
@@ -1399,11 +1569,11 @@ function SearchInput({ shortcut, className, placeholder = "Search\u2026", ...pro
|
|
|
1399
1569
|
}
|
|
1400
1570
|
|
|
1401
1571
|
// src/SectionHeader.tsx
|
|
1402
|
-
import { useState as
|
|
1572
|
+
import { useState as useState10 } from "react";
|
|
1403
1573
|
import { IconChevronRight as IconChevronRight2 } from "@tabler/icons-react";
|
|
1404
1574
|
import { jsx as jsx33, jsxs as jsxs22 } from "react/jsx-runtime";
|
|
1405
1575
|
function SectionHeader({ title, chevron = false, isExpanded = true, onToggle, actions, showActions = "hover", className }) {
|
|
1406
|
-
const [isHovered, setIsHovered] =
|
|
1576
|
+
const [isHovered, setIsHovered] = useState10(false);
|
|
1407
1577
|
return /* @__PURE__ */ jsxs22(
|
|
1408
1578
|
"div",
|
|
1409
1579
|
{
|
|
@@ -1479,7 +1649,7 @@ function Tabs({ tabs, active, onChange, size = "default", className }) {
|
|
|
1479
1649
|
}
|
|
1480
1650
|
|
|
1481
1651
|
// src/Toast.tsx
|
|
1482
|
-
import { createContext, useCallback as
|
|
1652
|
+
import { createContext as createContext3, useCallback as useCallback5, useContext as useContext3, useEffect as useEffect6, useMemo as useMemo3, useState as useState11 } from "react";
|
|
1483
1653
|
import { createPortal as createPortal6 } from "react-dom";
|
|
1484
1654
|
import { IconCircleCheck } from "@tabler/icons-react";
|
|
1485
1655
|
import { jsx as jsx35, jsxs as jsxs24 } from "react/jsx-runtime";
|
|
@@ -1533,14 +1703,14 @@ function Toast({ label, type = "neutral", leadingIcon = true, actionLabel, onAct
|
|
|
1533
1703
|
}
|
|
1534
1704
|
);
|
|
1535
1705
|
}
|
|
1536
|
-
var ToastContext =
|
|
1706
|
+
var ToastContext = createContext3(null);
|
|
1537
1707
|
var toastSeq = 0;
|
|
1538
1708
|
function ToastProvider({ children }) {
|
|
1539
|
-
const [toast, setToast] =
|
|
1540
|
-
const showToast =
|
|
1709
|
+
const [toast, setToast] = useState11(null);
|
|
1710
|
+
const showToast = useCallback5((opts) => {
|
|
1541
1711
|
setToast({ ...opts, id: ++toastSeq });
|
|
1542
1712
|
}, []);
|
|
1543
|
-
const dismissToast =
|
|
1713
|
+
const dismissToast = useCallback5(() => {
|
|
1544
1714
|
setToast(null);
|
|
1545
1715
|
}, []);
|
|
1546
1716
|
useEffect6(() => {
|
|
@@ -1575,7 +1745,7 @@ function ToastProvider({ children }) {
|
|
|
1575
1745
|
] });
|
|
1576
1746
|
}
|
|
1577
1747
|
function useToast() {
|
|
1578
|
-
const ctx =
|
|
1748
|
+
const ctx = useContext3(ToastContext);
|
|
1579
1749
|
if (!ctx) throw new Error("useToast must be used within ToastProvider");
|
|
1580
1750
|
return ctx;
|
|
1581
1751
|
}
|
|
@@ -1604,6 +1774,7 @@ export {
|
|
|
1604
1774
|
MenuItem,
|
|
1605
1775
|
MenuRow,
|
|
1606
1776
|
MenuSection,
|
|
1777
|
+
MenuSub,
|
|
1607
1778
|
NavItem,
|
|
1608
1779
|
Person,
|
|
1609
1780
|
PersonTrigger,
|
|
@@ -1626,9 +1797,13 @@ export {
|
|
|
1626
1797
|
Tooltip,
|
|
1627
1798
|
TopBar,
|
|
1628
1799
|
WithTooltip,
|
|
1800
|
+
clampBox,
|
|
1629
1801
|
cn,
|
|
1802
|
+
fitMenu,
|
|
1803
|
+
fitSubmenu,
|
|
1630
1804
|
hueFor,
|
|
1631
1805
|
initialsFor,
|
|
1806
|
+
useFieldControlId,
|
|
1632
1807
|
useToast
|
|
1633
1808
|
};
|
|
1634
1809
|
//# sourceMappingURL=index.js.map
|