@geomak/ui 2.0.0 → 4.0.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
@@ -1,7 +1,7 @@
1
1
  import { colors_default } from './chunk-GKXP6OJJ.js';
2
2
  export { colors_default as COLORS, PALETTE as palette, semanticTokens, vars } from './chunk-GKXP6OJJ.js';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
- import React8, { createContext, useState, useEffect, useMemo, useContext, useRef, useCallback, useId } from 'react';
4
+ import React8, { createContext, useState, useEffect, useMemo, useContext, useRef, useId, useCallback, useLayoutEffect } from 'react';
5
5
  import { createPortal } from 'react-dom';
6
6
  import * as Dialog from '@radix-ui/react-dialog';
7
7
  import { useReducedMotion, AnimatePresence, motion } from 'framer-motion';
@@ -1432,70 +1432,208 @@ function ContextMenuLabel({ icon, value }) {
1432
1432
  function ChevronRight2() {
1433
1433
  return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4 flex-shrink-0", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M9 5l7 7-7 7" }) });
1434
1434
  }
1435
- function Wizard({ children, steps, storageKey = "po_wizard" }) {
1436
- const wizardRef = useRef(null);
1437
- const [activeStep, setActiveStep] = useState(0);
1438
- const [targetBbox, setTargetBbox] = useState(null);
1439
- const HIGHLIGHTED = useMemo(
1440
- () => ["border", "border-4", "border-prussian-blue", "pointer-events-none"],
1441
- []
1442
- );
1443
- const closeWizard = useCallback(() => {
1444
- steps[activeStep]?.stepRef.current?.classList.remove(...HIGHLIGHTED);
1445
- if (wizardRef.current) wizardRef.current.style.display = "none";
1446
- }, [HIGHLIGHTED, steps, activeStep]);
1447
- useEffect(() => {
1448
- const visited = JSON.parse(localStorage.getItem(storageKey) ?? "false");
1449
- if (visited) {
1450
- closeWizard();
1435
+ function readDismissed(key) {
1436
+ if (key === null) return false;
1437
+ if (typeof window === "undefined") return false;
1438
+ try {
1439
+ return window.localStorage.getItem(key) === "true";
1440
+ } catch {
1441
+ return false;
1442
+ }
1443
+ }
1444
+ function writeDismissed(key) {
1445
+ if (key === null) return;
1446
+ if (typeof window === "undefined") return;
1447
+ try {
1448
+ window.localStorage.setItem(key, "true");
1449
+ } catch {
1450
+ }
1451
+ }
1452
+ function useTargetBbox(ref) {
1453
+ const [bbox, setBbox] = useState(null);
1454
+ useLayoutEffect(() => {
1455
+ const el = ref?.current;
1456
+ if (!el) {
1457
+ setBbox(null);
1451
1458
  return;
1452
1459
  }
1453
- const el = steps[activeStep]?.stepRef.current;
1454
- if (el) {
1455
- setTargetBbox(el.getBoundingClientRect());
1456
- el.classList.add(...HIGHLIGHTED);
1457
- }
1458
- }, [closeWizard, steps, activeStep, HIGHLIGHTED, storageKey]);
1459
- const onStepChange = () => {
1460
- if (steps[activeStep + 1]) {
1461
- steps[activeStep].stepRef.current?.classList.remove(...HIGHLIGHTED);
1462
- const nextEl = steps[activeStep + 1].stepRef.current;
1463
- if (nextEl) setTargetBbox(nextEl.getBoundingClientRect());
1464
- setActiveStep(activeStep + 1);
1465
- } else {
1466
- localStorage.setItem(storageKey, "true");
1467
- closeWizard();
1468
- }
1469
- };
1470
- const step = steps[activeStep];
1471
- const left = step && targetBbox ? step.positioning === "natural" ? isNaN(targetBbox.width + 20) ? 0 : targetBbox.width + 20 : isNaN(targetBbox.width / 2) ? 0 : targetBbox.width / 2 : "auto";
1472
- const top = step && targetBbox ? step.positioning === "natural" ? isNaN(targetBbox.y + 10) ? 0 : targetBbox.y + 10 : isNaN(targetBbox.height / 2) ? 0 : targetBbox.height / 2 : "auto";
1473
- return /* @__PURE__ */ jsxs("div", { className: "h-full p-1 rounded-lg w-full", children: [
1474
- /* @__PURE__ */ jsx("div", { className: "absolute", ref: wizardRef, children: /* @__PURE__ */ jsxs(
1475
- "div",
1476
- {
1477
- style: { left, top },
1478
- className: "absolute bg-white rounded-lg p-2 w-[220px] z-50 text-prussian-blue drop-shadow-md transition-all duration-300",
1479
- children: [
1480
- step?.description,
1481
- /* @__PURE__ */ jsxs("div", { className: "flex justify-end", children: [
1482
- /* @__PURE__ */ jsxs("span", { className: "flex h-3 w-3 relative left-14 top-4", children: [
1483
- /* @__PURE__ */ jsx("span", { className: "animate-ping absolute inline-flex h-3 w-3 rounded-full bg-dark-cornflower-blue opacity-75" }),
1484
- /* @__PURE__ */ jsx("span", { className: "relative inline-flex rounded-full h-3 w-3 bg-dark-cornflower-blue" })
1485
- ] }),
1486
- /* @__PURE__ */ jsx(
1487
- Button,
1488
- {
1489
- onClick: onStepChange,
1490
- content: activeStep === steps.length - 1 ? "Close" : "Next",
1491
- style: { width: 55, padding: "5px 10px", margin: "0" }
1492
- }
1493
- )
1494
- ] })
1495
- ]
1460
+ const update = () => setBbox(el.getBoundingClientRect());
1461
+ update();
1462
+ window.addEventListener("scroll", update, true);
1463
+ window.addEventListener("resize", update);
1464
+ const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(update) : null;
1465
+ ro?.observe(el);
1466
+ return () => {
1467
+ window.removeEventListener("scroll", update, true);
1468
+ window.removeEventListener("resize", update);
1469
+ ro?.disconnect();
1470
+ };
1471
+ }, [ref]);
1472
+ return bbox;
1473
+ }
1474
+ var TOOLTIP_WIDTH = 280;
1475
+ var TOOLTIP_GAP = 12;
1476
+ function tooltipStyleFor(bbox, placement) {
1477
+ const pl = placement ?? "right";
1478
+ if (pl === "right") return { left: bbox.right + TOOLTIP_GAP, top: bbox.top + bbox.height / 2, transform: "translateY(-50%)", width: TOOLTIP_WIDTH };
1479
+ if (pl === "left") return { left: bbox.left - TOOLTIP_WIDTH - TOOLTIP_GAP, top: bbox.top + bbox.height / 2, transform: "translateY(-50%)", width: TOOLTIP_WIDTH };
1480
+ if (pl === "bottom") return { left: bbox.left + bbox.width / 2, top: bbox.bottom + TOOLTIP_GAP, transform: "translateX(-50%)", width: TOOLTIP_WIDTH };
1481
+ return { left: bbox.left + bbox.width / 2, top: bbox.top - TOOLTIP_GAP, transform: "translate(-50%, -100%)", width: TOOLTIP_WIDTH };
1482
+ }
1483
+ function useFocusTrap(containerRef, active) {
1484
+ useEffect(() => {
1485
+ if (!active) return;
1486
+ const el = containerRef.current;
1487
+ if (!el) return;
1488
+ const t = setTimeout(() => {
1489
+ const first = el.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
1490
+ first?.focus();
1491
+ }, 0);
1492
+ const onKey = (e) => {
1493
+ if (e.key !== "Tab") return;
1494
+ const focusables = el.querySelectorAll(
1495
+ 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
1496
+ );
1497
+ if (focusables.length === 0) return;
1498
+ const first = focusables[0];
1499
+ const last = focusables[focusables.length - 1];
1500
+ if (e.shiftKey && document.activeElement === first) {
1501
+ e.preventDefault();
1502
+ last.focus();
1503
+ } else if (!e.shiftKey && document.activeElement === last) {
1504
+ e.preventDefault();
1505
+ first.focus();
1496
1506
  }
1497
- ) }),
1498
- children
1507
+ };
1508
+ document.addEventListener("keydown", onKey);
1509
+ return () => {
1510
+ clearTimeout(t);
1511
+ document.removeEventListener("keydown", onKey);
1512
+ };
1513
+ }, [containerRef, active]);
1514
+ }
1515
+ function Wizard({
1516
+ children,
1517
+ steps,
1518
+ storageKey = "oxygen.wizard.completed",
1519
+ dismissible = true,
1520
+ onComplete,
1521
+ onSkip
1522
+ }) {
1523
+ const tooltipRef = useRef(null);
1524
+ const tooltipTitleId = useId();
1525
+ const tooltipBodyId = useId();
1526
+ const [open, setOpen] = useState(() => steps.length > 0 && !readDismissed(storageKey));
1527
+ const [activeIndex, setActiveIndex] = useState(0);
1528
+ const step = steps[activeIndex];
1529
+ const bbox = useTargetBbox(step?.stepRef);
1530
+ useFocusTrap(tooltipRef, open);
1531
+ useEffect(() => {
1532
+ if (!open || !dismissible) return;
1533
+ const onKey = (e) => {
1534
+ if (e.key === "Escape") {
1535
+ e.preventDefault();
1536
+ handleSkip();
1537
+ }
1538
+ };
1539
+ document.addEventListener("keydown", onKey);
1540
+ return () => document.removeEventListener("keydown", onKey);
1541
+ }, [open, dismissible]);
1542
+ const handleSkip = useCallback(() => {
1543
+ writeDismissed(storageKey);
1544
+ setOpen(false);
1545
+ onSkip?.();
1546
+ }, [storageKey, onSkip]);
1547
+ const handleComplete = useCallback(() => {
1548
+ writeDismissed(storageKey);
1549
+ setOpen(false);
1550
+ onComplete?.();
1551
+ }, [storageKey, onComplete]);
1552
+ const handleNext = () => {
1553
+ if (activeIndex < steps.length - 1) setActiveIndex((i) => i + 1);
1554
+ else handleComplete();
1555
+ };
1556
+ const handlePrev = () => {
1557
+ if (activeIndex > 0) setActiveIndex((i) => i - 1);
1558
+ };
1559
+ const highlightStyle = bbox ? {
1560
+ left: bbox.left - 4,
1561
+ top: bbox.top - 4,
1562
+ width: bbox.width + 8,
1563
+ height: bbox.height + 8
1564
+ } : { display: "none" };
1565
+ const tooltipStyle = bbox ? tooltipStyleFor(bbox, step?.placement) : { display: "none" };
1566
+ const isLast = activeIndex === steps.length - 1;
1567
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1568
+ children,
1569
+ open && step && /* @__PURE__ */ jsxs(Portal, { children: [
1570
+ /* @__PURE__ */ jsx(
1571
+ "div",
1572
+ {
1573
+ className: "fixed inset-0 z-[7000000] bg-foreground/40 backdrop-blur-[1px] pointer-events-auto",
1574
+ "aria-hidden": "true"
1575
+ }
1576
+ ),
1577
+ /* @__PURE__ */ jsx(
1578
+ "div",
1579
+ {
1580
+ className: "fixed z-[7000001] pointer-events-none rounded-md ring-2 ring-accent ring-offset-2 ring-offset-background transition-all duration-200",
1581
+ style: highlightStyle,
1582
+ "aria-hidden": "true"
1583
+ }
1584
+ ),
1585
+ /* @__PURE__ */ jsxs(
1586
+ "div",
1587
+ {
1588
+ ref: tooltipRef,
1589
+ role: "dialog",
1590
+ "aria-modal": "true",
1591
+ "aria-labelledby": step.title ? tooltipTitleId : void 0,
1592
+ "aria-describedby": tooltipBodyId,
1593
+ className: "fixed z-[7000002] rounded-lg bg-surface text-foreground border border-border shadow-xl p-4 pointer-events-auto",
1594
+ style: tooltipStyle,
1595
+ children: [
1596
+ step.title && /* @__PURE__ */ jsx("h3", { id: tooltipTitleId, className: "text-sm font-semibold text-foreground mb-1", children: step.title }),
1597
+ /* @__PURE__ */ jsx("div", { id: tooltipBodyId, className: "text-sm text-foreground-secondary leading-relaxed", children: step.description }),
1598
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 flex items-center justify-between", children: [
1599
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-foreground-muted tabular-nums", children: [
1600
+ activeIndex + 1,
1601
+ " / ",
1602
+ steps.length
1603
+ ] }),
1604
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1605
+ dismissible && !isLast && /* @__PURE__ */ jsx(
1606
+ Button,
1607
+ {
1608
+ variant: "ghost",
1609
+ size: "sm",
1610
+ content: "Skip",
1611
+ onClick: handleSkip
1612
+ }
1613
+ ),
1614
+ activeIndex > 0 && /* @__PURE__ */ jsx(
1615
+ Button,
1616
+ {
1617
+ variant: "secondary",
1618
+ size: "sm",
1619
+ content: "Back",
1620
+ onClick: handlePrev
1621
+ }
1622
+ ),
1623
+ /* @__PURE__ */ jsx(
1624
+ Button,
1625
+ {
1626
+ size: "sm",
1627
+ content: isLast ? "Done" : "Next",
1628
+ onClick: handleNext
1629
+ }
1630
+ )
1631
+ ] })
1632
+ ] })
1633
+ ]
1634
+ }
1635
+ )
1636
+ ] })
1499
1637
  ] });
1500
1638
  }
1501
1639
  var SearchInput = React8.forwardRef(function SearchInput2({
@@ -3005,6 +3143,26 @@ function AutoComplete({
3005
3143
  }
3006
3144
  ) });
3007
3145
  }
3146
+ function flattenVisible(items, expanded, depth = 0, out = []) {
3147
+ for (const node of items) {
3148
+ const isParent2 = !!node.children && node.children.length > 0;
3149
+ out.push({ node, depth, isParent: isParent2 });
3150
+ if (isParent2 && expanded.has(node.key)) {
3151
+ flattenVisible(node.children, expanded, depth + 1, out);
3152
+ }
3153
+ }
3154
+ return out;
3155
+ }
3156
+ function findNodeByKey(items, key) {
3157
+ for (const n of items) {
3158
+ if (n.key === key) return n;
3159
+ if (n.children) {
3160
+ const found = findNodeByKey(n.children, key);
3161
+ if (found) return found;
3162
+ }
3163
+ }
3164
+ return null;
3165
+ }
3008
3166
  function TreeSelect({
3009
3167
  label,
3010
3168
  name,
@@ -3013,106 +3171,231 @@ function TreeSelect({
3013
3171
  disabled,
3014
3172
  layout = "horizontal",
3015
3173
  errorMessage,
3016
- style = {},
3174
+ style,
3017
3175
  htmlFor,
3018
- items = []
3176
+ items = [],
3177
+ placeholder = "Select\u2026",
3178
+ parentsSelectable = true,
3179
+ defaultExpandedKeys = []
3019
3180
  }) {
3181
+ const errorId = useId();
3182
+ const hasError = errorMessage != null;
3020
3183
  const [open, setOpen] = useState(false);
3021
- const [hoveredItem, setHoveredItem] = useState(null);
3022
- const [innerItems, setInnerItems] = useState([]);
3184
+ const [expanded, setExpanded] = useState(() => new Set(defaultExpandedKeys));
3185
+ const [activeIndex, setActiveIndex] = useState(0);
3186
+ const listRef = useRef(null);
3187
+ const visible = useMemo(() => flattenVisible(items, expanded), [items, expanded]);
3023
3188
  useEffect(() => {
3024
- setInnerItems(items);
3025
- }, [items]);
3026
- const selectItem = (key) => {
3189
+ if (!open) return;
3190
+ const selectedIdx = visible.findIndex((v) => v.node.key === value);
3191
+ setActiveIndex(selectedIdx >= 0 ? selectedIdx : 0);
3192
+ }, [open, visible, value]);
3193
+ const selectedNode = useMemo(
3194
+ () => value != null ? findNodeByKey(items, value) : null,
3195
+ [items, value]
3196
+ );
3197
+ const toggleExpand = (key) => {
3198
+ setExpanded((prev) => {
3199
+ const next = new Set(prev);
3200
+ if (next.has(key)) next.delete(key);
3201
+ else next.add(key);
3202
+ return next;
3203
+ });
3204
+ };
3205
+ const selectKey = (key) => {
3027
3206
  onChange?.({ target: { value: key, id: htmlFor, name } });
3028
3207
  setOpen(false);
3029
3208
  };
3030
- return /* @__PURE__ */ jsxs("div", { className: "mt-2", children: [
3031
- /* @__PURE__ */ jsxs(
3032
- "div",
3033
- {
3034
- className: `flex ${layout === "vertical" ? "flex-col" : "flex-row items-center gap-2"}`,
3035
- children: [
3036
- label && /* @__PURE__ */ jsx(
3037
- "label",
3038
- {
3039
- className: "text-md font-bold ml-1 max-content select-none text-prussian-blue dark:text-white",
3040
- htmlFor,
3041
- children: label
3042
- }
3043
- ),
3044
- /* @__PURE__ */ jsxs(Popover.Root, { open: open && !disabled, onOpenChange: (o) => !disabled && setOpen(o), children: [
3045
- /* @__PURE__ */ jsx(Popover.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(
3209
+ const onListKey = (e) => {
3210
+ if (visible.length === 0) return;
3211
+ const cur = visible[activeIndex];
3212
+ if (e.key === "ArrowDown") {
3213
+ e.preventDefault();
3214
+ setActiveIndex((i) => Math.min(i + 1, visible.length - 1));
3215
+ } else if (e.key === "ArrowUp") {
3216
+ e.preventDefault();
3217
+ setActiveIndex((i) => Math.max(i - 1, 0));
3218
+ } else if (e.key === "ArrowRight") {
3219
+ e.preventDefault();
3220
+ if (cur.isParent) {
3221
+ if (!expanded.has(cur.node.key)) toggleExpand(cur.node.key);
3222
+ else setActiveIndex((i) => Math.min(i + 1, visible.length - 1));
3223
+ }
3224
+ } else if (e.key === "ArrowLeft") {
3225
+ e.preventDefault();
3226
+ if (cur.isParent && expanded.has(cur.node.key)) {
3227
+ toggleExpand(cur.node.key);
3228
+ } else if (cur.depth > 0) {
3229
+ for (let i = activeIndex - 1; i >= 0; i--) {
3230
+ if (visible[i].depth < cur.depth) {
3231
+ setActiveIndex(i);
3232
+ break;
3233
+ }
3234
+ }
3235
+ }
3236
+ } else if (e.key === "Enter" || e.key === " ") {
3237
+ e.preventDefault();
3238
+ if (cur.node.disabled) return;
3239
+ if (cur.isParent && !parentsSelectable) {
3240
+ toggleExpand(cur.node.key);
3241
+ } else {
3242
+ selectKey(cur.node.key);
3243
+ }
3244
+ } else if (e.key === "Escape") {
3245
+ e.preventDefault();
3246
+ setOpen(false);
3247
+ }
3248
+ };
3249
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
3250
+ /* @__PURE__ */ jsxs("div", { className: `flex ${layout === "vertical" ? "flex-col gap-1" : "flex-row items-center gap-2"}`, children: [
3251
+ label && /* @__PURE__ */ jsx(
3252
+ "label",
3253
+ {
3254
+ className: "text-sm font-medium ml-1 max-content select-none text-foreground",
3255
+ htmlFor,
3256
+ children: label
3257
+ }
3258
+ ),
3259
+ /* @__PURE__ */ jsxs(Popover.Root, { open: open && !disabled, onOpenChange: (o) => !disabled && setOpen(o), children: [
3260
+ /* @__PURE__ */ jsx(Popover.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(
3261
+ "button",
3262
+ {
3263
+ id: htmlFor,
3264
+ type: "button",
3265
+ style,
3266
+ role: "combobox",
3267
+ "aria-expanded": open,
3268
+ "aria-haspopup": "listbox",
3269
+ "aria-invalid": hasError || void 0,
3270
+ "aria-describedby": hasError ? errorId : void 0,
3271
+ disabled,
3272
+ className: `flex items-center justify-between h-9 rounded-lg border ${hasError ? "border-status-error" : "border-border"} px-3 cursor-pointer select-none focus:outline-none focus-visible:ring-2 focus-visible:ring-accent ${disabled ? "cursor-not-allowed bg-surface-raised text-foreground-muted" : "bg-surface text-foreground"} ${!style?.width ? "min-w-[240px]" : ""}`,
3273
+ children: [
3274
+ /* @__PURE__ */ jsx("span", { className: "text-sm truncate text-left", children: selectedNode ? selectedNode.label : /* @__PURE__ */ jsx("span", { className: "text-foreground-muted", children: placeholder }) }),
3275
+ /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: `h-4 w-4 flex-shrink-0 transition-transform duration-200 ${open ? "rotate-180" : ""}`, "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" }) })
3276
+ ]
3277
+ }
3278
+ ) }),
3279
+ /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(
3280
+ Popover.Content,
3281
+ {
3282
+ align: "start",
3283
+ sideOffset: 4,
3284
+ style: { width: style?.width || 280 },
3285
+ className: "bg-surface text-foreground border border-border rounded-lg shadow-md z-50 p-1 animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
3286
+ onOpenAutoFocus: (e) => {
3287
+ e.preventDefault();
3288
+ listRef.current?.focus();
3289
+ },
3290
+ children: /* @__PURE__ */ jsx(
3046
3291
  "div",
3047
3292
  {
3048
- id: htmlFor,
3049
- style,
3050
- role: "combobox",
3051
- "aria-expanded": open,
3052
- "aria-haspopup": "listbox",
3053
- className: `flex items-center justify-between relative h-9 rounded-lg p-2 cursor-pointer ${disabled ? "cursor-not-allowed bg-disabled" : "bg-white"}`,
3054
- tabIndex: disabled ? -1 : 0,
3055
- children: [
3056
- /* @__PURE__ */ jsx("div", { className: `h-7 ${!style?.width ? "min-w-[240px]" : ""} focus:outline-none text-prussian-blue flex items-center gap-1`, children: Array.isArray(value) ? /* @__PURE__ */ jsxs(Fragment, { children: [
3057
- value.slice(0, 1).map((val, id) => /* @__PURE__ */ jsx(
3058
- DropdownPill,
3059
- {
3060
- hasSiblings: value.length > 1,
3061
- value: innerItems.find((it) => it.key === val)?.label
3062
- },
3063
- id
3064
- )),
3065
- value.length > 1 && /* @__PURE__ */ jsx(DropdownPill, { value: `+${value.length - 1} more` })
3066
- ] }) : value != null ? /* @__PURE__ */ jsx(DropdownPill, { value: innerItems.find((it) => it.key === value)?.label }) : null }),
3067
- /* @__PURE__ */ jsx("div", { className: `transition-transform duration-300 ml-2 ${open ? "rotate-180" : "rotate-0"}`, children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: colors_default.PALETTE["prussian-blue"], strokeWidth: 2, className: "h-4 w-4", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" }) }) })
3068
- ]
3069
- }
3070
- ) }),
3071
- /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(
3072
- Popover.Content,
3073
- {
3074
- align: "start",
3075
- sideOffset: 4,
3076
- style: { width: style?.width || 240 },
3077
- className: "bg-ice rounded-lg shadow-md z-50 p-2 animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
3078
- children: /* @__PURE__ */ jsx("div", { role: "listbox", className: "max-h-40 overflow-y-auto", children: innerItems.map((item, idx) => /* @__PURE__ */ jsxs(
3079
- "div",
3293
+ ref: listRef,
3294
+ role: "tree",
3295
+ tabIndex: -1,
3296
+ "aria-activedescendant": visible[activeIndex] ? `tree-opt-${visible[activeIndex].node.key}` : void 0,
3297
+ onKeyDown: onListKey,
3298
+ className: "max-h-72 overflow-y-auto outline-none",
3299
+ children: visible.length === 0 ? /* @__PURE__ */ jsx("div", { className: "px-3 py-4 text-sm text-foreground-secondary text-center", children: "No items" }) : visible.map((v, idx) => /* @__PURE__ */ jsx(
3300
+ TreeNodeRow,
3080
3301
  {
3081
- role: "option",
3082
- "aria-selected": value === item.key,
3083
- "aria-rowindex": idx,
3084
- className: `flex items-center justify-between p-2 hover:bg-prussian-blue hover:text-white transition-all duration-150 text-sm text-prussian-blue rounded-lg cursor-pointer`,
3085
- onClick: () => selectItem(item.key),
3086
- onMouseEnter: () => setHoveredItem(item.key),
3087
- onMouseLeave: () => setHoveredItem(null),
3088
- children: [
3089
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs", children: [
3090
- item.icon && /* @__PURE__ */ jsx("div", { children: item.icon }),
3091
- item.label
3092
- ] }),
3093
- value === item.key && /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 20 20", fill: "none", children: /* @__PURE__ */ jsx(
3094
- "path",
3095
- {
3096
- d: "M4 10l4.5 4.5L16 6",
3097
- stroke: hoveredItem === item.key ? "#fff" : colors_default.PALETTE["prussian-blue"],
3098
- strokeWidth: "2",
3099
- strokeLinecap: "round",
3100
- strokeLinejoin: "round"
3101
- }
3102
- ) })
3103
- ]
3302
+ node: v.node,
3303
+ depth: v.depth,
3304
+ isParent: v.isParent,
3305
+ isExpanded: expanded.has(v.node.key),
3306
+ isActive: idx === activeIndex,
3307
+ isSelected: value === v.node.key,
3308
+ parentsSelectable,
3309
+ onActivate: () => {
3310
+ if (v.node.disabled) return;
3311
+ if (v.isParent && !parentsSelectable) toggleExpand(v.node.key);
3312
+ else selectKey(v.node.key);
3313
+ },
3314
+ onToggle: () => toggleExpand(v.node.key),
3315
+ onHover: () => setActiveIndex(idx)
3104
3316
  },
3105
- item.key
3106
- )) })
3317
+ v.node.key
3318
+ ))
3107
3319
  }
3108
- ) })
3109
- ] })
3110
- ]
3111
- }
3112
- ),
3113
- /* @__PURE__ */ jsx("div", { className: "text-center text-error dark:text-prussian-blue min-h-0", children: errorMessage })
3320
+ )
3321
+ }
3322
+ ) })
3323
+ ] })
3324
+ ] }),
3325
+ hasError && /* @__PURE__ */ jsx("div", { id: errorId, className: "text-xs text-status-error ml-1", children: errorMessage })
3114
3326
  ] });
3115
3327
  }
3328
+ function TreeNodeRow({
3329
+ node,
3330
+ depth,
3331
+ isParent: isParent2,
3332
+ isExpanded,
3333
+ isActive,
3334
+ isSelected,
3335
+ parentsSelectable,
3336
+ onActivate,
3337
+ onToggle,
3338
+ onHover
3339
+ }) {
3340
+ const disabled = node.disabled;
3341
+ return /* @__PURE__ */ jsxs(
3342
+ "div",
3343
+ {
3344
+ id: `tree-opt-${node.key}`,
3345
+ role: "treeitem",
3346
+ "aria-selected": isSelected,
3347
+ "aria-expanded": isParent2 ? isExpanded : void 0,
3348
+ "aria-disabled": disabled || void 0,
3349
+ "data-active": isActive ? "" : void 0,
3350
+ onMouseEnter: onHover,
3351
+ className: `flex items-center gap-1 rounded-md px-1 py-1.5 select-none cursor-pointer transition-colors duration-100 ${disabled ? "opacity-40 cursor-not-allowed" : isActive ? "bg-accent text-accent-fg" : isSelected ? "bg-surface-raised" : "hover:bg-surface-raised"}`,
3352
+ style: { paddingLeft: depth * 16 + 4 },
3353
+ children: [
3354
+ isParent2 ? /* @__PURE__ */ jsx(
3355
+ "button",
3356
+ {
3357
+ type: "button",
3358
+ onClick: (e) => {
3359
+ e.stopPropagation();
3360
+ onToggle();
3361
+ },
3362
+ "aria-label": isExpanded ? "Collapse" : "Expand",
3363
+ tabIndex: -1,
3364
+ className: "w-5 h-5 inline-flex items-center justify-center rounded hover:bg-black/10 focus:outline-none",
3365
+ children: /* @__PURE__ */ jsx(
3366
+ "svg",
3367
+ {
3368
+ viewBox: "0 0 24 24",
3369
+ fill: "none",
3370
+ stroke: "currentColor",
3371
+ strokeWidth: 2.5,
3372
+ className: `h-3 w-3 transition-transform duration-150 ${isExpanded ? "rotate-0" : "-rotate-90"}`,
3373
+ "aria-hidden": "true",
3374
+ children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" })
3375
+ }
3376
+ )
3377
+ }
3378
+ ) : /* @__PURE__ */ jsx("span", { className: "w-5 h-5 inline-block", "aria-hidden": "true" }),
3379
+ /* @__PURE__ */ jsxs(
3380
+ "button",
3381
+ {
3382
+ type: "button",
3383
+ onClick: onActivate,
3384
+ disabled,
3385
+ tabIndex: -1,
3386
+ className: "flex-1 flex items-center gap-2 text-sm text-left focus:outline-none disabled:cursor-not-allowed",
3387
+ children: [
3388
+ node.icon,
3389
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: node.label }),
3390
+ isParent2 && !parentsSelectable && /* @__PURE__ */ jsx("span", { className: "ml-auto text-xs text-foreground-muted", children: "parent" })
3391
+ ]
3392
+ }
3393
+ ),
3394
+ isSelected && /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true", className: "ml-1", children: /* @__PURE__ */ jsx("path", { d: "M4 10l4.5 4.5L16 6", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) })
3395
+ ]
3396
+ }
3397
+ );
3398
+ }
3116
3399
  function FileInput({
3117
3400
  allowMultiple = false,
3118
3401
  onChange,