@geomak/ui 2.0.0 → 3.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.cjs CHANGED
@@ -1465,70 +1465,208 @@ function ContextMenuLabel({ icon, value }) {
1465
1465
  function ChevronRight2() {
1466
1466
  return /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M9 5l7 7-7 7" }) });
1467
1467
  }
1468
- function Wizard({ children, steps, storageKey = "po_wizard" }) {
1469
- const wizardRef = React8.useRef(null);
1470
- const [activeStep, setActiveStep] = React8.useState(0);
1471
- const [targetBbox, setTargetBbox] = React8.useState(null);
1472
- const HIGHLIGHTED = React8.useMemo(
1473
- () => ["border", "border-4", "border-prussian-blue", "pointer-events-none"],
1474
- []
1475
- );
1476
- const closeWizard = React8.useCallback(() => {
1477
- steps[activeStep]?.stepRef.current?.classList.remove(...HIGHLIGHTED);
1478
- if (wizardRef.current) wizardRef.current.style.display = "none";
1479
- }, [HIGHLIGHTED, steps, activeStep]);
1480
- React8.useEffect(() => {
1481
- const visited = JSON.parse(localStorage.getItem(storageKey) ?? "false");
1482
- if (visited) {
1483
- closeWizard();
1468
+ function readDismissed(key) {
1469
+ if (key === null) return false;
1470
+ if (typeof window === "undefined") return false;
1471
+ try {
1472
+ return window.localStorage.getItem(key) === "true";
1473
+ } catch {
1474
+ return false;
1475
+ }
1476
+ }
1477
+ function writeDismissed(key) {
1478
+ if (key === null) return;
1479
+ if (typeof window === "undefined") return;
1480
+ try {
1481
+ window.localStorage.setItem(key, "true");
1482
+ } catch {
1483
+ }
1484
+ }
1485
+ function useTargetBbox(ref) {
1486
+ const [bbox, setBbox] = React8.useState(null);
1487
+ React8.useLayoutEffect(() => {
1488
+ const el = ref?.current;
1489
+ if (!el) {
1490
+ setBbox(null);
1484
1491
  return;
1485
1492
  }
1486
- const el = steps[activeStep]?.stepRef.current;
1487
- if (el) {
1488
- setTargetBbox(el.getBoundingClientRect());
1489
- el.classList.add(...HIGHLIGHTED);
1490
- }
1491
- }, [closeWizard, steps, activeStep, HIGHLIGHTED, storageKey]);
1492
- const onStepChange = () => {
1493
- if (steps[activeStep + 1]) {
1494
- steps[activeStep].stepRef.current?.classList.remove(...HIGHLIGHTED);
1495
- const nextEl = steps[activeStep + 1].stepRef.current;
1496
- if (nextEl) setTargetBbox(nextEl.getBoundingClientRect());
1497
- setActiveStep(activeStep + 1);
1498
- } else {
1499
- localStorage.setItem(storageKey, "true");
1500
- closeWizard();
1501
- }
1502
- };
1503
- const step = steps[activeStep];
1504
- const left = step && targetBbox ? step.positioning === "natural" ? isNaN(targetBbox.width + 20) ? 0 : targetBbox.width + 20 : isNaN(targetBbox.width / 2) ? 0 : targetBbox.width / 2 : "auto";
1505
- const top = step && targetBbox ? step.positioning === "natural" ? isNaN(targetBbox.y + 10) ? 0 : targetBbox.y + 10 : isNaN(targetBbox.height / 2) ? 0 : targetBbox.height / 2 : "auto";
1506
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "h-full p-1 rounded-lg w-full", children: [
1507
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute", ref: wizardRef, children: /* @__PURE__ */ jsxRuntime.jsxs(
1508
- "div",
1509
- {
1510
- style: { left, top },
1511
- className: "absolute bg-white rounded-lg p-2 w-[220px] z-50 text-prussian-blue drop-shadow-md transition-all duration-300",
1512
- children: [
1513
- step?.description,
1514
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-end", children: [
1515
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex h-3 w-3 relative left-14 top-4", children: [
1516
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "animate-ping absolute inline-flex h-3 w-3 rounded-full bg-dark-cornflower-blue opacity-75" }),
1517
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relative inline-flex rounded-full h-3 w-3 bg-dark-cornflower-blue" })
1518
- ] }),
1519
- /* @__PURE__ */ jsxRuntime.jsx(
1520
- Button,
1521
- {
1522
- onClick: onStepChange,
1523
- content: activeStep === steps.length - 1 ? "Close" : "Next",
1524
- style: { width: 55, padding: "5px 10px", margin: "0" }
1525
- }
1526
- )
1527
- ] })
1528
- ]
1493
+ const update = () => setBbox(el.getBoundingClientRect());
1494
+ update();
1495
+ window.addEventListener("scroll", update, true);
1496
+ window.addEventListener("resize", update);
1497
+ const ro = typeof ResizeObserver !== "undefined" ? new ResizeObserver(update) : null;
1498
+ ro?.observe(el);
1499
+ return () => {
1500
+ window.removeEventListener("scroll", update, true);
1501
+ window.removeEventListener("resize", update);
1502
+ ro?.disconnect();
1503
+ };
1504
+ }, [ref]);
1505
+ return bbox;
1506
+ }
1507
+ var TOOLTIP_WIDTH = 280;
1508
+ var TOOLTIP_GAP = 12;
1509
+ function tooltipStyleFor(bbox, placement) {
1510
+ const pl = placement ?? "right";
1511
+ if (pl === "right") return { left: bbox.right + TOOLTIP_GAP, top: bbox.top + bbox.height / 2, transform: "translateY(-50%)", width: TOOLTIP_WIDTH };
1512
+ if (pl === "left") return { left: bbox.left - TOOLTIP_WIDTH - TOOLTIP_GAP, top: bbox.top + bbox.height / 2, transform: "translateY(-50%)", width: TOOLTIP_WIDTH };
1513
+ if (pl === "bottom") return { left: bbox.left + bbox.width / 2, top: bbox.bottom + TOOLTIP_GAP, transform: "translateX(-50%)", width: TOOLTIP_WIDTH };
1514
+ return { left: bbox.left + bbox.width / 2, top: bbox.top - TOOLTIP_GAP, transform: "translate(-50%, -100%)", width: TOOLTIP_WIDTH };
1515
+ }
1516
+ function useFocusTrap(containerRef, active) {
1517
+ React8.useEffect(() => {
1518
+ if (!active) return;
1519
+ const el = containerRef.current;
1520
+ if (!el) return;
1521
+ const t = setTimeout(() => {
1522
+ const first = el.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
1523
+ first?.focus();
1524
+ }, 0);
1525
+ const onKey = (e) => {
1526
+ if (e.key !== "Tab") return;
1527
+ const focusables = el.querySelectorAll(
1528
+ 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
1529
+ );
1530
+ if (focusables.length === 0) return;
1531
+ const first = focusables[0];
1532
+ const last = focusables[focusables.length - 1];
1533
+ if (e.shiftKey && document.activeElement === first) {
1534
+ e.preventDefault();
1535
+ last.focus();
1536
+ } else if (!e.shiftKey && document.activeElement === last) {
1537
+ e.preventDefault();
1538
+ first.focus();
1539
+ }
1540
+ };
1541
+ document.addEventListener("keydown", onKey);
1542
+ return () => {
1543
+ clearTimeout(t);
1544
+ document.removeEventListener("keydown", onKey);
1545
+ };
1546
+ }, [containerRef, active]);
1547
+ }
1548
+ function Wizard({
1549
+ children,
1550
+ steps,
1551
+ storageKey = "oxygen.wizard.completed",
1552
+ dismissible = true,
1553
+ onComplete,
1554
+ onSkip
1555
+ }) {
1556
+ const tooltipRef = React8.useRef(null);
1557
+ const tooltipTitleId = React8.useId();
1558
+ const tooltipBodyId = React8.useId();
1559
+ const [open, setOpen] = React8.useState(() => steps.length > 0 && !readDismissed(storageKey));
1560
+ const [activeIndex, setActiveIndex] = React8.useState(0);
1561
+ const step = steps[activeIndex];
1562
+ const bbox = useTargetBbox(step?.stepRef);
1563
+ useFocusTrap(tooltipRef, open);
1564
+ React8.useEffect(() => {
1565
+ if (!open || !dismissible) return;
1566
+ const onKey = (e) => {
1567
+ if (e.key === "Escape") {
1568
+ e.preventDefault();
1569
+ handleSkip();
1529
1570
  }
1530
- ) }),
1531
- children
1571
+ };
1572
+ document.addEventListener("keydown", onKey);
1573
+ return () => document.removeEventListener("keydown", onKey);
1574
+ }, [open, dismissible]);
1575
+ const handleSkip = React8.useCallback(() => {
1576
+ writeDismissed(storageKey);
1577
+ setOpen(false);
1578
+ onSkip?.();
1579
+ }, [storageKey, onSkip]);
1580
+ const handleComplete = React8.useCallback(() => {
1581
+ writeDismissed(storageKey);
1582
+ setOpen(false);
1583
+ onComplete?.();
1584
+ }, [storageKey, onComplete]);
1585
+ const handleNext = () => {
1586
+ if (activeIndex < steps.length - 1) setActiveIndex((i) => i + 1);
1587
+ else handleComplete();
1588
+ };
1589
+ const handlePrev = () => {
1590
+ if (activeIndex > 0) setActiveIndex((i) => i - 1);
1591
+ };
1592
+ const highlightStyle = bbox ? {
1593
+ left: bbox.left - 4,
1594
+ top: bbox.top - 4,
1595
+ width: bbox.width + 8,
1596
+ height: bbox.height + 8
1597
+ } : { display: "none" };
1598
+ const tooltipStyle = bbox ? tooltipStyleFor(bbox, step?.placement) : { display: "none" };
1599
+ const isLast = activeIndex === steps.length - 1;
1600
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1601
+ children,
1602
+ open && step && /* @__PURE__ */ jsxRuntime.jsxs(Portal, { children: [
1603
+ /* @__PURE__ */ jsxRuntime.jsx(
1604
+ "div",
1605
+ {
1606
+ className: "fixed inset-0 z-[7000000] bg-foreground/40 backdrop-blur-[1px] pointer-events-auto",
1607
+ "aria-hidden": "true"
1608
+ }
1609
+ ),
1610
+ /* @__PURE__ */ jsxRuntime.jsx(
1611
+ "div",
1612
+ {
1613
+ className: "fixed z-[7000001] pointer-events-none rounded-md ring-2 ring-accent ring-offset-2 ring-offset-background transition-all duration-200",
1614
+ style: highlightStyle,
1615
+ "aria-hidden": "true"
1616
+ }
1617
+ ),
1618
+ /* @__PURE__ */ jsxRuntime.jsxs(
1619
+ "div",
1620
+ {
1621
+ ref: tooltipRef,
1622
+ role: "dialog",
1623
+ "aria-modal": "true",
1624
+ "aria-labelledby": step.title ? tooltipTitleId : void 0,
1625
+ "aria-describedby": tooltipBodyId,
1626
+ className: "fixed z-[7000002] rounded-lg bg-surface text-foreground border border-border shadow-xl p-4 pointer-events-auto",
1627
+ style: tooltipStyle,
1628
+ children: [
1629
+ step.title && /* @__PURE__ */ jsxRuntime.jsx("h3", { id: tooltipTitleId, className: "text-sm font-semibold text-foreground mb-1", children: step.title }),
1630
+ /* @__PURE__ */ jsxRuntime.jsx("div", { id: tooltipBodyId, className: "text-sm text-foreground-secondary leading-relaxed", children: step.description }),
1631
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-4 flex items-center justify-between", children: [
1632
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-foreground-muted tabular-nums", children: [
1633
+ activeIndex + 1,
1634
+ " / ",
1635
+ steps.length
1636
+ ] }),
1637
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
1638
+ dismissible && !isLast && /* @__PURE__ */ jsxRuntime.jsx(
1639
+ Button,
1640
+ {
1641
+ variant: "ghost",
1642
+ size: "sm",
1643
+ content: "Skip",
1644
+ onClick: handleSkip
1645
+ }
1646
+ ),
1647
+ activeIndex > 0 && /* @__PURE__ */ jsxRuntime.jsx(
1648
+ Button,
1649
+ {
1650
+ variant: "secondary",
1651
+ size: "sm",
1652
+ content: "Back",
1653
+ onClick: handlePrev
1654
+ }
1655
+ ),
1656
+ /* @__PURE__ */ jsxRuntime.jsx(
1657
+ Button,
1658
+ {
1659
+ size: "sm",
1660
+ content: isLast ? "Done" : "Next",
1661
+ onClick: handleNext
1662
+ }
1663
+ )
1664
+ ] })
1665
+ ] })
1666
+ ]
1667
+ }
1668
+ )
1669
+ ] })
1532
1670
  ] });
1533
1671
  }
1534
1672
  var SearchInput = React8__default.default.forwardRef(function SearchInput2({