aeon-ui-engine 1.4.0 → 1.8.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/CONSUMER.md +67 -27
  3. package/INTEGRATION.md +1 -1
  4. package/README.md +17 -8
  5. package/package.json +5 -2
  6. package/packages/cli/dist/bin.js +29 -3
  7. package/packages/cli/dist/{chunk-5L47SLNV.js → chunk-JMNRJCWF.js} +63 -9
  8. package/packages/cli/dist/index.d.ts +15 -1
  9. package/packages/cli/dist/index.js +3 -1
  10. package/packages/cli/node_modules/.bin/vitest +2 -2
  11. package/packages/cli/src/bin.ts +30 -2
  12. package/packages/cli/src/index.ts +1 -0
  13. package/packages/cli/src/init.ts +10 -4
  14. package/packages/cli/src/link.ts +61 -0
  15. package/packages/core/node_modules/.bin/vitest +2 -2
  16. package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  17. package/packages/panda/styled-system/styles.css +0 -4
  18. package/packages/primitives/dist/{chunk-PB3LFHXU.js → chunk-VUPWYBGU.js} +14 -0
  19. package/packages/primitives/dist/index.js +1 -1
  20. package/packages/primitives/dist/machines/range-slider.d.ts +3 -3
  21. package/packages/primitives/dist/machines/slider.d.ts +22 -0
  22. package/packages/primitives/dist/machines/slider.js +1 -1
  23. package/packages/primitives/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  24. package/packages/primitives/src/machines/slider.ts +17 -0
  25. package/packages/react/dist/index.d.ts +44 -5
  26. package/packages/react/dist/index.js +173 -48
  27. package/packages/react/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  28. package/packages/react/src/dialog/dialog.test.tsx +27 -0
  29. package/packages/react/src/dialog/index.tsx +11 -3
  30. package/packages/react/src/floating/FloatingPositioner.tsx +19 -1
  31. package/packages/react/src/hooks/use-anchor-position.ts +88 -17
  32. package/packages/react/src/menu/index.tsx +45 -6
  33. package/packages/react/src/popover/index.tsx +31 -4
  34. package/packages/react/src/slider/index.tsx +66 -19
  35. package/packages/react/src/tooltip/index.tsx +7 -1
  36. package/packages/schemas/dist/index.js +3 -1
  37. package/packages/schemas/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  38. package/packages/schemas/prompts/cursorrules.md +3 -1
  39. package/packages/schemas/registry/machines/slider.json +18 -1
  40. package/packages/schemas/src/prompts.ts +3 -1
  41. package/packages/tree/README.md +29 -0
  42. package/packages/tree/dist/index.d.ts +97 -0
  43. package/packages/tree/dist/index.js +107 -0
  44. package/packages/tree/node_modules/.bin/esbuild +21 -0
  45. package/packages/tree/node_modules/.bin/jiti +21 -0
  46. package/packages/tree/node_modules/.bin/tsc +21 -0
  47. package/packages/tree/node_modules/.bin/tsserver +21 -0
  48. package/packages/tree/node_modules/.bin/tsup +21 -0
  49. package/packages/tree/node_modules/.bin/tsup-node +21 -0
  50. package/packages/tree/node_modules/.bin/tsx +21 -0
  51. package/packages/tree/node_modules/.bin/vite +21 -0
  52. package/packages/tree/node_modules/.bin/vitest +21 -0
  53. package/packages/tree/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  54. package/packages/tree/package.json +42 -0
  55. package/packages/tree/src/exclusive-action.test.ts +54 -0
  56. package/packages/tree/src/exclusive-action.ts +104 -0
  57. package/packages/tree/src/index.ts +7 -0
  58. package/packages/tree/src/interpret.tsx +45 -0
  59. package/packages/tree/src/totality.ts +20 -0
  60. package/packages/tree/src/widget.ts +31 -0
  61. package/packages/tree/tsconfig.json +8 -0
  62. package/packages/tree/vitest.config.ts +8 -0
  63. package/scripts/link-scoped-packages.mjs +206 -0
  64. package/scripts/vite-aliases.mjs +9 -8
@@ -519,11 +519,15 @@ var Root4 = forwardRef5(function DialogRoot({ open, defaultOpen = false, onOpenC
519
519
  const [snapshot, send] = useAeonMachine(dialogMachine, {
520
520
  input: { open: defaultOpen }
521
521
  });
522
- const resolvedOpen = open ?? snapshot.context.open;
522
+ const machineOpen = snapshot.context.open;
523
+ const resolvedOpen = open ?? machineOpen;
523
524
  const contentRef = useRef(null);
525
+ const reportedOpen = useRef(machineOpen);
524
526
  useEffect6(() => {
525
- onOpenChange?.(resolvedOpen);
526
- }, [resolvedOpen, onOpenChange]);
527
+ if (reportedOpen.current === machineOpen) return;
528
+ reportedOpen.current = machineOpen;
529
+ onOpenChange?.(machineOpen);
530
+ }, [machineOpen, onOpenChange]);
527
531
  useEffect6(() => {
528
532
  if (open !== void 0) {
529
533
  send({ type: "SET_OPEN", open });
@@ -1247,7 +1251,7 @@ import {
1247
1251
  } from "react";
1248
1252
  import { jsx as jsx11 } from "react/jsx-runtime";
1249
1253
  var SliderCtx = createContext8(null);
1250
- function useSliderCtx() {
1254
+ function useSliderContext() {
1251
1255
  const ctx = useContext8(SliderCtx);
1252
1256
  if (!ctx) throw new Error("Slider parts must be used within Slider.Root");
1253
1257
  return ctx;
@@ -1280,12 +1284,24 @@ var Root11 = forwardRef12(function SliderRoot({
1280
1284
  });
1281
1285
  const resolvedValue = value ?? snapshot.context.value;
1282
1286
  const dragging = snapshot.matches({ interaction: "dragging" });
1283
- useEffect9(() => {
1284
- onValueChange?.(resolvedValue);
1285
- }, [resolvedValue, onValueChange]);
1286
1287
  useEffect9(() => {
1287
1288
  if (value !== void 0) send({ type: "SET_VALUE", value });
1288
1289
  }, [value, send]);
1290
+ useEffect9(() => {
1291
+ send({ type: "SET_BOUNDS", min, max, step });
1292
+ }, [min, max, step, send]);
1293
+ useEffect9(() => {
1294
+ send({ type: "SET_DISABLED", disabled });
1295
+ }, [disabled, send]);
1296
+ const commit = useCallback2(
1297
+ (next) => {
1298
+ if (disabled) return;
1299
+ const snapped = snapSliderValue(next, min, max, step);
1300
+ send({ type: "SET_VALUE", value: snapped });
1301
+ onValueChange?.(snapped);
1302
+ },
1303
+ [disabled, min, max, step, send, onValueChange]
1304
+ );
1289
1305
  const setValueFromClientX = useCallback2(
1290
1306
  (clientX) => {
1291
1307
  const track = trackRef.current;
@@ -1293,11 +1309,9 @@ var Root11 = forwardRef12(function SliderRoot({
1293
1309
  const rect = track.getBoundingClientRect();
1294
1310
  if (rect.width <= 0) return;
1295
1311
  const ratio = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
1296
- const next = snapSliderValue(min + ratio * (max - min), min, max, step);
1297
- send({ type: "SET_VALUE", value: next });
1298
- if (value === void 0) onValueChange?.(next);
1312
+ commit(min + ratio * (max - min));
1299
1313
  },
1300
- [disabled, min, max, step, send, value, onValueChange]
1314
+ [commit, disabled, min, max]
1301
1315
  );
1302
1316
  useEffect9(() => {
1303
1317
  if (!dragging) return;
@@ -1318,12 +1332,25 @@ var Root11 = forwardRef12(function SliderRoot({
1318
1332
  step,
1319
1333
  disabled,
1320
1334
  dragging,
1335
+ interaction: dragging ? "dragging" : "idle",
1321
1336
  send,
1322
1337
  onValueChange,
1338
+ commit,
1323
1339
  setValueFromClientX,
1324
1340
  trackRef
1325
1341
  }),
1326
- [resolvedValue, min, max, step, disabled, dragging, send, onValueChange, setValueFromClientX]
1342
+ [
1343
+ resolvedValue,
1344
+ min,
1345
+ max,
1346
+ step,
1347
+ disabled,
1348
+ dragging,
1349
+ send,
1350
+ onValueChange,
1351
+ commit,
1352
+ setValueFromClientX
1353
+ ]
1327
1354
  );
1328
1355
  return /* @__PURE__ */ jsx11(SliderCtx.Provider, { value: ctx, children: /* @__PURE__ */ jsx11(
1329
1356
  "div",
@@ -1342,7 +1369,7 @@ var Root11 = forwardRef12(function SliderRoot({
1342
1369
  ) });
1343
1370
  });
1344
1371
  var Track2 = forwardRef12(function SliderTrack({ onPointerDown, ...rest }, ref) {
1345
- const { disabled, send, setValueFromClientX, trackRef } = useSliderCtx();
1372
+ const { disabled, send, setValueFromClientX, trackRef } = useSliderContext();
1346
1373
  return /* @__PURE__ */ jsx11(
1347
1374
  "div",
1348
1375
  {
@@ -1368,7 +1395,7 @@ var Track2 = forwardRef12(function SliderTrack({ onPointerDown, ...rest }, ref)
1368
1395
  );
1369
1396
  });
1370
1397
  var Range2 = forwardRef12(function SliderRange({ style, ...rest }, ref) {
1371
- const { value, min, max } = useSliderCtx();
1398
+ const { value, min, max } = useSliderContext();
1372
1399
  return /* @__PURE__ */ jsx11(
1373
1400
  "div",
1374
1401
  {
@@ -1380,7 +1407,7 @@ var Range2 = forwardRef12(function SliderRange({ style, ...rest }, ref) {
1380
1407
  );
1381
1408
  });
1382
1409
  var Thumb2 = forwardRef12(function SliderThumb({ onKeyDown, onPointerDown, style, ...rest }, ref) {
1383
- const { value, min, max, step, disabled, dragging, send, setValueFromClientX } = useSliderCtx();
1410
+ const { value, min, max, step, disabled, dragging, send, commit, setValueFromClientX } = useSliderContext();
1384
1411
  return /* @__PURE__ */ jsx11(
1385
1412
  "span",
1386
1413
  {
@@ -1408,19 +1435,27 @@ var Thumb2 = forwardRef12(function SliderThumb({ onKeyDown, onPointerDown, style
1408
1435
  if (disabled || e.defaultPrevented) return;
1409
1436
  if (e.key === "ArrowRight" || e.key === "ArrowUp") {
1410
1437
  e.preventDefault();
1411
- send({ type: "STEP", delta: step });
1438
+ commit(value + step);
1412
1439
  }
1413
1440
  if (e.key === "ArrowLeft" || e.key === "ArrowDown") {
1414
1441
  e.preventDefault();
1415
- send({ type: "STEP", delta: -step });
1442
+ commit(value - step);
1443
+ }
1444
+ if (e.key === "PageUp") {
1445
+ e.preventDefault();
1446
+ commit(value + (max - min) / 10);
1447
+ }
1448
+ if (e.key === "PageDown") {
1449
+ e.preventDefault();
1450
+ commit(value - (max - min) / 10);
1416
1451
  }
1417
1452
  if (e.key === "Home") {
1418
1453
  e.preventDefault();
1419
- send({ type: "HOME" });
1454
+ commit(min);
1420
1455
  }
1421
1456
  if (e.key === "End") {
1422
1457
  e.preventDefault();
1423
- send({ type: "END" });
1458
+ commit(max);
1424
1459
  }
1425
1460
  }
1426
1461
  },
@@ -1430,7 +1465,7 @@ var Thumb2 = forwardRef12(function SliderThumb({ onKeyDown, onPointerDown, style
1430
1465
  );
1431
1466
  });
1432
1467
  var ValueText = forwardRef12(function SliderValueText(props, ref) {
1433
- const { value } = useSliderCtx();
1468
+ const { value } = useSliderContext();
1434
1469
  return /* @__PURE__ */ jsx11("span", { ref, ...mergeProps(partOnlyAttrs3(sliderAnatomy.valueText), props), children: props.children ?? value });
1435
1470
  });
1436
1471
  var Slider = { Root: Root11, Track: Track2, Range: Range2, Thumb: Thumb2, ValueText };
@@ -2628,20 +2663,35 @@ import { forwardRef as forwardRef23 } from "react";
2628
2663
 
2629
2664
  // src/hooks/use-anchor-position.ts
2630
2665
  import { useEffect as useEffect13 } from "react";
2666
+ var DEFAULT_GUTTER = 4;
2667
+ var DEFAULT_Z_INDEX = 50;
2668
+ var DEFAULT_PADDING = 8;
2669
+ function splitPlacement(placement) {
2670
+ const [side, align] = placement.split("-");
2671
+ return [side, align];
2672
+ }
2631
2673
  function useAnchorPosition(anchorRef, floatingRef, active, placement = "bottom-start", options = {}) {
2632
- const { matchWidth = false } = options;
2674
+ const {
2675
+ matchWidth = false,
2676
+ gutter = DEFAULT_GUTTER,
2677
+ zIndex = DEFAULT_Z_INDEX,
2678
+ padding = DEFAULT_PADDING
2679
+ } = options;
2633
2680
  useEffect13(() => {
2634
2681
  if (!active || !anchorRef.current || !floatingRef.current) return;
2682
+ let frame = 0;
2635
2683
  const update = () => {
2636
2684
  const anchor = anchorRef.current;
2637
2685
  const floating = floatingRef.current;
2638
2686
  if (!anchor || !floating) return;
2687
+ const [preferredSide, align] = splitPlacement(placement);
2639
2688
  const rect = anchor.getBoundingClientRect();
2689
+ const viewportWidth = window.innerWidth;
2690
+ const viewportHeight = window.innerHeight;
2640
2691
  floating.style.position = "fixed";
2641
- floating.style.left = `${rect.left}px`;
2642
2692
  floating.style.bottom = "";
2643
2693
  floating.style.right = "";
2644
- floating.style.zIndex = "50";
2694
+ floating.style.zIndex = String(zIndex);
2645
2695
  if (matchWidth) {
2646
2696
  floating.style.width = `${rect.width}px`;
2647
2697
  floating.style.minWidth = `${rect.width}px`;
@@ -2649,22 +2699,43 @@ function useAnchorPosition(anchorRef, floatingRef, active, placement = "bottom-s
2649
2699
  floating.style.width = "";
2650
2700
  floating.style.minWidth = "";
2651
2701
  }
2652
- if (placement === "top-start") {
2653
- const height = floating.offsetHeight || floating.getBoundingClientRect().height;
2654
- floating.style.top = `${rect.top - height - 4}px`;
2655
- } else {
2656
- floating.style.top = `${rect.bottom + 4}px`;
2657
- }
2702
+ const width = floating.offsetWidth || floating.getBoundingClientRect().width;
2703
+ const height = floating.offsetHeight || floating.getBoundingClientRect().height;
2704
+ const roomBelow = viewportHeight - rect.bottom - gutter - padding;
2705
+ const roomAbove = rect.top - gutter - padding;
2706
+ const room = preferredSide === "top" ? roomAbove : roomBelow;
2707
+ const side = room >= height ? preferredSide : roomAbove > roomBelow ? "top" : "bottom";
2708
+ const available = side === "top" ? roomAbove : roomBelow;
2709
+ let left = rect.left;
2710
+ if (align === "end") left = rect.right - width;
2711
+ else if (align === "center") left = rect.left + rect.width / 2 - width / 2;
2712
+ const maxLeft = Math.max(padding, viewportWidth - width - padding);
2713
+ left = Math.min(Math.max(padding, left), maxLeft);
2714
+ const top = side === "top" ? Math.max(padding, rect.top - gutter - Math.min(height, Math.max(available, 0))) : Math.min(
2715
+ rect.bottom + gutter,
2716
+ Math.max(padding, viewportHeight - height - padding)
2717
+ );
2718
+ floating.style.left = `${Math.round(left)}px`;
2719
+ floating.style.top = `${Math.round(top)}px`;
2720
+ floating.style.setProperty("--aeon-available-height", `${Math.round(Math.max(available, 0))}px`);
2721
+ floating.style.setProperty("--aeon-anchor-width", `${Math.round(rect.width)}px`);
2722
+ floating.dataset.side = side;
2723
+ floating.dataset.align = align;
2724
+ };
2725
+ const schedule = () => {
2726
+ cancelAnimationFrame(frame);
2727
+ frame = requestAnimationFrame(update);
2658
2728
  };
2659
2729
  update();
2660
- requestAnimationFrame(update);
2661
- window.addEventListener("resize", update);
2662
- window.addEventListener("scroll", update, true);
2730
+ schedule();
2731
+ window.addEventListener("resize", schedule);
2732
+ window.addEventListener("scroll", schedule, true);
2663
2733
  return () => {
2664
- window.removeEventListener("resize", update);
2665
- window.removeEventListener("scroll", update, true);
2734
+ cancelAnimationFrame(frame);
2735
+ window.removeEventListener("resize", schedule);
2736
+ window.removeEventListener("scroll", schedule, true);
2666
2737
  };
2667
- }, [active, anchorRef, floatingRef, placement, matchWidth]);
2738
+ }, [active, anchorRef, floatingRef, placement, matchWidth, gutter, zIndex, padding]);
2668
2739
  }
2669
2740
 
2670
2741
  // src/floating/FloatingPositioner.tsx
@@ -2676,16 +2747,21 @@ var FloatingPositioner = forwardRef23(
2676
2747
  part,
2677
2748
  triggerRef,
2678
2749
  positionerRef,
2750
+ anchorRef,
2679
2751
  portalled = true,
2680
2752
  placement = "bottom-start",
2681
2753
  matchAnchorWidth = false,
2754
+ gutter,
2755
+ zIndex,
2682
2756
  children,
2683
2757
  className,
2684
2758
  ...rest
2685
2759
  }, ref) {
2686
2760
  const mounted = usePortalledMount();
2687
- useAnchorPosition(triggerRef, positionerRef, open && portalled, placement, {
2688
- matchWidth: matchAnchorWidth
2761
+ useAnchorPosition(anchorRef ?? triggerRef, positionerRef, open && portalled, placement, {
2762
+ matchWidth: matchAnchorWidth,
2763
+ gutter,
2764
+ zIndex
2689
2765
  });
2690
2766
  if (!open) return null;
2691
2767
  const node = /* @__PURE__ */ jsx23(
@@ -3361,15 +3437,19 @@ var Root20 = forwardRef26(function PopoverRoot({ open, defaultOpen = false, onOp
3361
3437
  const [snapshot, send] = useAeonMachine(popoverMachine3, {
3362
3438
  input: { open: defaultOpen }
3363
3439
  });
3364
- const resolvedOpen = open ?? snapshot.context.open;
3440
+ const machineOpen = snapshot.context.open;
3441
+ const resolvedOpen = open ?? machineOpen;
3365
3442
  const rootRef = useRef9(null);
3366
3443
  const triggerRef = useRef9(null);
3367
3444
  const positionerRef = useRef9(null);
3368
3445
  const triggerId = useId5();
3369
3446
  const contentId = useId5();
3447
+ const reportedOpen = useRef9(machineOpen);
3370
3448
  useEffect16(() => {
3371
- onOpenChange?.(resolvedOpen);
3372
- }, [resolvedOpen, onOpenChange]);
3449
+ if (reportedOpen.current === machineOpen) return;
3450
+ reportedOpen.current = machineOpen;
3451
+ onOpenChange?.(machineOpen);
3452
+ }, [machineOpen, onOpenChange]);
3373
3453
  useEffect16(() => {
3374
3454
  if (open !== void 0) send({ type: "SET_OPEN", open });
3375
3455
  }, [open, send]);
@@ -3425,7 +3505,17 @@ var Trigger4 = forwardRef26(function PopoverTrigger({ onClick, ...rest }, ref) {
3425
3505
  }
3426
3506
  );
3427
3507
  });
3428
- var Positioner3 = forwardRef26(function PopoverPositioner({ portalled = true, placement = "bottom-start", children, className, ...rest }, ref) {
3508
+ var Positioner3 = forwardRef26(function PopoverPositioner({
3509
+ portalled = true,
3510
+ placement = "bottom-start",
3511
+ anchorRef,
3512
+ matchAnchorWidth = false,
3513
+ gutter,
3514
+ zIndex,
3515
+ children,
3516
+ className,
3517
+ ...rest
3518
+ }, ref) {
3429
3519
  const { open, triggerRef, positionerRef } = usePopoverCtx();
3430
3520
  return /* @__PURE__ */ jsx26(
3431
3521
  FloatingPositioner,
@@ -3436,8 +3526,12 @@ var Positioner3 = forwardRef26(function PopoverPositioner({ portalled = true, pl
3436
3526
  part: popoverAnatomy.positioner,
3437
3527
  triggerRef,
3438
3528
  positionerRef,
3529
+ anchorRef,
3530
+ matchAnchorWidth,
3439
3531
  portalled,
3440
3532
  placement,
3533
+ gutter,
3534
+ zIndex,
3441
3535
  className,
3442
3536
  ...rest,
3443
3537
  children
@@ -3520,15 +3614,19 @@ function useMenuCtx() {
3520
3614
  }
3521
3615
  var Root21 = forwardRef27(function MenuRoot({ open, defaultOpen = false, onOpenChange, children, className, ...rest }, ref) {
3522
3616
  const [snapshot, send] = useAeonMachine(popoverMachine4, { input: { open: defaultOpen } });
3523
- const resolvedOpen = open ?? snapshot.context.open;
3617
+ const machineOpen = snapshot.context.open;
3618
+ const resolvedOpen = open ?? machineOpen;
3524
3619
  const rootRef = useRef10(null);
3525
3620
  const triggerRef = useRef10(null);
3526
3621
  const positionerRef = useRef10(null);
3527
3622
  const triggerId = useId6();
3528
3623
  const menuId = useId6();
3624
+ const reportedOpen = useRef10(machineOpen);
3529
3625
  useEffect17(() => {
3530
- onOpenChange?.(resolvedOpen);
3531
- }, [resolvedOpen, onOpenChange]);
3626
+ if (reportedOpen.current === machineOpen) return;
3627
+ reportedOpen.current = machineOpen;
3628
+ onOpenChange?.(machineOpen);
3629
+ }, [machineOpen, onOpenChange]);
3532
3630
  useEffect17(() => {
3533
3631
  if (open !== void 0) send({ type: "SET_OPEN", open });
3534
3632
  }, [open, send]);
@@ -3583,7 +3681,7 @@ var Trigger5 = forwardRef27(function MenuTrigger({ onClick, ...rest }, ref) {
3583
3681
  }
3584
3682
  );
3585
3683
  });
3586
- var Positioner4 = forwardRef27(function MenuPositioner({ portalled = true, placement = "bottom-start", children, className, ...rest }, ref) {
3684
+ var Positioner4 = forwardRef27(function MenuPositioner({ portalled = true, placement = "bottom-start", gutter, zIndex, children, className, ...rest }, ref) {
3587
3685
  const { open, triggerRef, positionerRef } = useMenuCtx();
3588
3686
  return /* @__PURE__ */ jsx27(
3589
3687
  FloatingPositioner,
@@ -3596,6 +3694,8 @@ var Positioner4 = forwardRef27(function MenuPositioner({ portalled = true, place
3596
3694
  positionerRef,
3597
3695
  portalled,
3598
3696
  placement,
3697
+ gutter,
3698
+ zIndex,
3599
3699
  className,
3600
3700
  ...rest,
3601
3701
  children
@@ -3603,14 +3703,36 @@ var Positioner4 = forwardRef27(function MenuPositioner({ portalled = true, place
3603
3703
  );
3604
3704
  });
3605
3705
  var Content7 = forwardRef27(function MenuContent({ onKeyDown, ...rest }, ref) {
3606
- const { open, send, menuId } = useMenuCtx();
3706
+ const { open, send, menuId, triggerRef } = useMenuCtx();
3707
+ const contentRef = useRef10(null);
3708
+ useEffect17(() => {
3709
+ if (!open) return;
3710
+ const trigger = triggerRef.current;
3711
+ contentRef.current?.focus({ preventScroll: true });
3712
+ return () => {
3713
+ if (trigger?.isConnected) trigger.focus({ preventScroll: true });
3714
+ };
3715
+ }, [open, triggerRef]);
3716
+ useEffect17(() => {
3717
+ if (!open) return;
3718
+ const onDocumentKeyDown = (event) => {
3719
+ if (event.key === "Escape") send({ type: "ESCAPE" });
3720
+ };
3721
+ document.addEventListener("keydown", onDocumentKeyDown);
3722
+ return () => document.removeEventListener("keydown", onDocumentKeyDown);
3723
+ }, [open, send]);
3607
3724
  if (!open) return null;
3608
3725
  return /* @__PURE__ */ jsx27(
3609
3726
  "div",
3610
3727
  {
3611
- ref,
3728
+ ref: (node) => {
3729
+ contentRef.current = node;
3730
+ if (typeof ref === "function") ref(node);
3731
+ else if (ref) ref.current = node;
3732
+ },
3612
3733
  id: menuId,
3613
3734
  role: "menu",
3735
+ tabIndex: -1,
3614
3736
  ...mergeProps(
3615
3737
  partAttrs23(menuAnatomy.scope, menuAnatomy.content, { state: "open" }),
3616
3738
  {
@@ -3870,7 +3992,7 @@ var Trigger6 = forwardRef28(function TooltipTrigger({ onClick, onMouseEnter, onM
3870
3992
  }
3871
3993
  );
3872
3994
  });
3873
- var Positioner5 = forwardRef28(function TooltipPositioner({ portalled = true, placement = "top-start", children, className, ...rest }, ref) {
3995
+ var Positioner5 = forwardRef28(function TooltipPositioner({ portalled = true, placement = "top-start", gutter, zIndex, children, className, ...rest }, ref) {
3874
3996
  const { open, triggerRef, positionerRef } = useTooltipCtx();
3875
3997
  return /* @__PURE__ */ jsx28(
3876
3998
  FloatingPositioner,
@@ -3883,6 +4005,8 @@ var Positioner5 = forwardRef28(function TooltipPositioner({ portalled = true, pl
3883
4005
  positionerRef,
3884
4006
  portalled,
3885
4007
  placement,
4008
+ gutter,
4009
+ zIndex,
3886
4010
  className,
3887
4011
  ...rest,
3888
4012
  children
@@ -5864,6 +5988,7 @@ export {
5864
5988
  useLayoutMode,
5865
5989
  useScrollContext,
5866
5990
  useScrollState,
5991
+ useSliderContext,
5867
5992
  useStickyBarContext,
5868
5993
  useToast,
5869
5994
  useToastStore
@@ -1 +1 @@
1
- {"version":"3.2.4","results":[[":src/select/select.test.tsx",{"duration":233.12287500000002,"failed":false}],[":src/dialog/dialog.test.tsx",{"duration":197.85458300000005,"failed":false}],[":src/avatar/avatar.test.tsx",{"duration":175.06837500000006,"failed":false}],[":src/tooltip/tooltip.test.tsx",{"duration":236.69991700000014,"failed":false}],[":src/toast/toast.test.tsx",{"duration":174.01087499999994,"failed":false}]]}
1
+ {"version":"3.2.4","results":[[":src/select/select.test.tsx",{"duration":196.07670899999994,"failed":false}],[":src/dialog/dialog.test.tsx",{"duration":181.65270800000008,"failed":false}],[":src/avatar/avatar.test.tsx",{"duration":117.92833299999984,"failed":false}],[":src/tooltip/tooltip.test.tsx",{"duration":210.20837500000016,"failed":false}],[":src/toast/toast.test.tsx",{"duration":134.57233399999996,"failed":false}]]}
@@ -1,5 +1,6 @@
1
1
  import { render, screen } from '@testing-library/react'
2
2
  import userEvent from '@testing-library/user-event'
3
+ import { useState } from 'react'
3
4
  import { describe, expect, it, vi } from 'vitest'
4
5
  import { Dialog } from '../dialog/index.js'
5
6
 
@@ -47,4 +48,30 @@ describe('Dialog', () => {
47
48
  unmount()
48
49
  expect(document.body.style.overflow).not.toBe('hidden')
49
50
  })
51
+
52
+ it('reports the trigger to a controlled owner', async () => {
53
+ const user = userEvent.setup()
54
+
55
+ function Controlled() {
56
+ const [open, setOpen] = useState(false)
57
+ return (
58
+ <Dialog.Root open={open} onOpenChange={setOpen}>
59
+ <Dialog.Trigger>Open</Dialog.Trigger>
60
+ <Dialog.Portal portalled={false}>
61
+ <Dialog.Content>
62
+ <Dialog.Title>Title</Dialog.Title>
63
+ <Dialog.CloseTrigger>Close</Dialog.CloseTrigger>
64
+ </Dialog.Content>
65
+ </Dialog.Portal>
66
+ </Dialog.Root>
67
+ )
68
+ }
69
+
70
+ render(<Controlled />)
71
+ await user.click(screen.getByRole('button', { name: 'Open' }))
72
+ expect(screen.getByRole('dialog')).toBeInTheDocument()
73
+
74
+ await user.click(screen.getByRole('button', { name: 'Close dialog' }))
75
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
76
+ })
50
77
  })
@@ -52,12 +52,20 @@ const Root = forwardRef<HTMLDivElement, DialogRootProps>(function DialogRoot(
52
52
  input: { open: defaultOpen },
53
53
  })
54
54
 
55
- const resolvedOpen = open ?? snapshot.context.open
55
+ const machineOpen = snapshot.context.open
56
+ const resolvedOpen = open ?? machineOpen
56
57
  const contentRef = useRef<HTMLDivElement>(null)
57
58
 
59
+ /**
60
+ * Report the machine's intent, not the resolved value: a controlled owner
61
+ * holding `open` would otherwise never hear about its own trigger or escape.
62
+ */
63
+ const reportedOpen = useRef(machineOpen)
58
64
  useEffect(() => {
59
- onOpenChange?.(resolvedOpen)
60
- }, [resolvedOpen, onOpenChange])
65
+ if (reportedOpen.current === machineOpen) return
66
+ reportedOpen.current = machineOpen
67
+ onOpenChange?.(machineOpen)
68
+ }, [machineOpen, onOpenChange])
61
69
 
62
70
  useEffect(() => {
63
71
  if (open !== undefined) {
@@ -10,9 +10,22 @@ export interface FloatingPositionerProps extends HTMLAttributes<HTMLDivElement>
10
10
  part: string
11
11
  triggerRef: RefObject<HTMLElement | null>
12
12
  positionerRef: RefObject<HTMLDivElement | null>
13
+ /**
14
+ * Position against this element instead of the trigger.
15
+ *
16
+ * A trigger is often a small icon button pinned to one edge of a much larger
17
+ * region. Aligning to the button then throws a wide surface off-screen, and
18
+ * clamping only slides it back to the viewport edge. Anchoring to the region
19
+ * the surface belongs to puts it where the eye already is.
20
+ */
21
+ anchorRef?: RefObject<HTMLElement | null>
13
22
  portalled?: boolean
14
23
  placement?: AnchorPlacement
15
24
  matchAnchorWidth?: boolean
25
+ /** Gap between trigger and surface. */
26
+ gutter?: number
27
+ /** Stack level for the portalled surface. */
28
+ zIndex?: number
16
29
  children?: ReactNode
17
30
  }
18
31
 
@@ -24,9 +37,12 @@ export const FloatingPositioner = forwardRef<HTMLDivElement, FloatingPositionerP
24
37
  part,
25
38
  triggerRef,
26
39
  positionerRef,
40
+ anchorRef,
27
41
  portalled = true,
28
42
  placement = 'bottom-start',
29
43
  matchAnchorWidth = false,
44
+ gutter,
45
+ zIndex,
30
46
  children,
31
47
  className,
32
48
  ...rest
@@ -34,8 +50,10 @@ export const FloatingPositioner = forwardRef<HTMLDivElement, FloatingPositionerP
34
50
  ref,
35
51
  ) {
36
52
  const mounted = usePortalledMount()
37
- useAnchorPosition(triggerRef, positionerRef, open && portalled, placement, {
53
+ useAnchorPosition(anchorRef ?? triggerRef, positionerRef, open && portalled, placement, {
38
54
  matchWidth: matchAnchorWidth,
55
+ gutter,
56
+ zIndex,
39
57
  })
40
58
 
41
59
  if (!open) return null