@astryxdesign/core 0.2.0-canary.11b423e → 0.2.0-canary.15ccc97

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 (81) hide show
  1. package/README.md +5 -3
  2. package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
  3. package/dist/AvatarGroup/AvatarGroup.js +3 -7
  4. package/dist/Carousel/Carousel.d.ts +53 -2
  5. package/dist/Carousel/Carousel.d.ts.map +1 -1
  6. package/dist/Carousel/Carousel.js +81 -14
  7. package/dist/Carousel/index.d.ts +1 -1
  8. package/dist/Carousel/index.d.ts.map +1 -1
  9. package/dist/CommandPalette/CommandPaletteInput.d.ts +1 -1
  10. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  11. package/dist/CommandPalette/CommandPaletteInput.js +3 -1
  12. package/dist/ContextMenu/ContextMenu.d.ts +1 -1
  13. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  14. package/dist/ContextMenu/ContextMenu.js +13 -4
  15. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  16. package/dist/DropdownMenu/DropdownMenu.js +20 -17
  17. package/dist/Icon/globalIconRegistry.d.ts +42 -2
  18. package/dist/Icon/globalIconRegistry.d.ts.map +1 -1
  19. package/dist/Icon/globalIconRegistry.js +49 -1
  20. package/dist/Icon/index.d.ts +2 -2
  21. package/dist/Icon/index.d.ts.map +1 -1
  22. package/dist/Icon/index.js +1 -1
  23. package/dist/Markdown/Markdown.d.ts.map +1 -1
  24. package/dist/Markdown/Markdown.js +31 -9
  25. package/dist/MultiSelector/MultiSelector.d.ts +12 -3
  26. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  27. package/dist/MultiSelector/MultiSelector.js +99 -20
  28. package/dist/Selector/Selector.d.ts +11 -2
  29. package/dist/Selector/Selector.d.ts.map +1 -1
  30. package/dist/Selector/Selector.js +99 -20
  31. package/dist/Timestamp/Timestamp.d.ts +4 -1
  32. package/dist/Timestamp/Timestamp.d.ts.map +1 -1
  33. package/dist/Timestamp/Timestamp.js +76 -5
  34. package/dist/Timestamp/formatInstant.d.ts +3 -3
  35. package/dist/Timestamp/formatInstant.d.ts.map +1 -1
  36. package/dist/Timestamp/formatInstant.js +2 -2
  37. package/dist/Timestamp/tooltipEntries.d.ts +3 -2
  38. package/dist/Timestamp/tooltipEntries.d.ts.map +1 -1
  39. package/dist/Timestamp/tooltipEntries.js +3 -2
  40. package/dist/astryx.css +3 -1
  41. package/dist/astryx.umd.js +51 -51
  42. package/dist/astryx.umd.js.map +4 -4
  43. package/dist/hooks/useGridFocus.d.ts +8 -6
  44. package/dist/hooks/useGridFocus.d.ts.map +1 -1
  45. package/dist/hooks/useListFocus.d.ts +8 -7
  46. package/dist/hooks/useListFocus.d.ts.map +1 -1
  47. package/dist/hooks/useTreeFocus.d.ts.map +1 -1
  48. package/dist/hooks/useTreeFocus.js +13 -2
  49. package/package.json +2 -2
  50. package/src/AvatarGroup/AvatarGroup.tsx +2 -6
  51. package/src/Carousel/Carousel.doc.mjs +7 -0
  52. package/src/Carousel/Carousel.test.tsx +232 -1
  53. package/src/Carousel/Carousel.tsx +143 -20
  54. package/src/Carousel/index.ts +1 -1
  55. package/src/CommandPalette/CommandPaletteInput.tsx +4 -0
  56. package/src/ContextMenu/ContextMenu.tsx +16 -4
  57. package/src/DropdownMenu/DropdownMenu.test.tsx +71 -0
  58. package/src/DropdownMenu/DropdownMenu.tsx +20 -17
  59. package/src/Icon/Icon.doc.mjs +1 -0
  60. package/src/Icon/globalIconRegistry.test.tsx +35 -0
  61. package/src/Icon/globalIconRegistry.tsx +68 -5
  62. package/src/Icon/index.ts +2 -0
  63. package/src/Markdown/Markdown.doc.mjs +80 -0
  64. package/src/Markdown/Markdown.test.tsx +92 -0
  65. package/src/Markdown/Markdown.tsx +93 -65
  66. package/src/Markdown/parser.perf.test.ts +47 -35
  67. package/src/MultiSelector/MultiSelector.doc.mjs +32 -7
  68. package/src/MultiSelector/MultiSelector.test.tsx +186 -0
  69. package/src/MultiSelector/MultiSelector.tsx +188 -48
  70. package/src/Selector/Selector.doc.mjs +25 -5
  71. package/src/Selector/Selector.test.tsx +163 -0
  72. package/src/Selector/Selector.tsx +187 -48
  73. package/src/Timestamp/Timestamp.doc.mjs +2 -2
  74. package/src/Timestamp/Timestamp.test.tsx +131 -0
  75. package/src/Timestamp/Timestamp.tsx +91 -8
  76. package/src/Timestamp/formatInstant.ts +3 -4
  77. package/src/Timestamp/tooltipEntries.ts +3 -2
  78. package/src/hooks/useGridFocus.ts +8 -6
  79. package/src/hooks/useListFocus.ts +8 -7
  80. package/src/hooks/useTreeFocus.test.tsx +53 -1
  81. package/src/hooks/useTreeFocus.ts +13 -2
package/README.md CHANGED
@@ -75,10 +75,12 @@ astryx gap-report # report a missing capability
75
75
 
76
76
  ## Quick Start
77
77
 
78
+ Astryx requires **React 19** or later (`react` and `react-dom` >= 19.0.0 are peer dependencies).
79
+
78
80
  Install Astryx and a theme:
79
81
 
80
82
  ```bash
81
- npm install @astryxdesign/core @astryxdesign/theme-neutral
83
+ npm install @astryxdesign/core @astryxdesign/theme-neutral @stylexjs/stylex
82
84
  ```
83
85
 
84
86
  Then pick your setup below based on your framework and styling approach.
@@ -226,7 +228,7 @@ export default function Page() {
226
228
  Use the pre-built dist alongside StyleX for your own styles.
227
229
 
228
230
  ```bash
229
- npm install @astryxdesign/core @astryxdesign/theme-neutral
231
+ npm install @astryxdesign/core @astryxdesign/theme-neutral @stylexjs/stylex
230
232
  ```
231
233
 
232
234
  **`src/app/globals.css`**
@@ -242,7 +244,7 @@ Providers and layout are the same as the Tailwind example (use `@astryxdesign/th
242
244
  ### Vite
243
245
 
244
246
  ```bash
245
- npm install @astryxdesign/core @astryxdesign/theme-neutral
247
+ npm install @astryxdesign/core @astryxdesign/theme-neutral @stylexjs/stylex
246
248
  ```
247
249
 
248
250
  Same CSS imports and providers as above. No build plugins needed; Astryx ships pre-built.
@@ -1 +1 @@
1
- {"version":3,"file":"AvatarGroup.d.ts","sourceRoot":"","sources":["../../src/AvatarGroup/AvatarGroup.tsx"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAA2B,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAC/D,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAc,KAAK,UAAU,EAAC,MAAM,WAAW,CAAC;AAavD,MAAM,WAAW,gBAAiB,SAAQ,SAAS,CAAC,cAAc,CAAC;IACjE,yCAAyC;IACzC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;OAGG;IACH,QAAQ,EAAE,SAAS,CAAC;IACpB;;;OAGG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AASD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,IAAW,EACX,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,kBAAkB,EAChC,kBAAkB,EAAE,wBAAwB,EAC5C,SAAS,EACT,OAAO,EACP,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,KAAK,EACT,EAAE,gBAAgB,GAAG,SAAS,CAuE9B;yBApFe,WAAW"}
1
+ {"version":3,"file":"AvatarGroup.d.ts","sourceRoot":"","sources":["../../src/AvatarGroup/AvatarGroup.tsx"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAA2B,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAC/D,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAc,KAAK,UAAU,EAAC,MAAM,WAAW,CAAC;AAavD,MAAM,WAAW,gBAAiB,SAAQ,SAAS,CAAC,cAAc,CAAC;IACjE,yCAAyC;IACzC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;OAGG;IACH,QAAQ,EAAE,SAAS,CAAC;IACpB;;;OAGG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AASD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,IAAW,EACX,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,kBAAkB,EAChC,kBAAkB,EAAE,wBAAwB,EAC5C,SAAS,EACT,OAAO,EACP,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,EACH,GAAG,KAAK,EACT,EAAE,gBAAgB,GAAG,SAAS,CAmE9B;yBAhFe,WAAW"}
@@ -87,11 +87,9 @@ export function AvatarGroup({
87
87
  }), [size, overlap, numericSize]);
88
88
 
89
89
  // The keyboard hint and roving tab stop only make sense once the group has
90
- // interactive children. Detect their presence (and the writing direction for
91
- // RTL-correct arrow navigation) from the rendered DOM after commit, matching
92
- // how the codebase reads direction elsewhere (`getComputedStyle(el).direction`).
90
+ // interactive children. Detect their presence from the rendered DOM after
91
+ // commit. (Arrow-key direction is auto-detected inside useListFocus.)
93
92
  const [hasInteractiveItems, setHasInteractiveItems] = useState(false);
94
- const [isRtl, setIsRtl] = useState(false);
95
93
 
96
94
  // Single tab stop + roving arrow focus over the group's interactive items.
97
95
  // `itemSelector` targets the shared `[data-avatar-item]` marker stamped on
@@ -105,8 +103,7 @@ export function AvatarGroup({
105
103
  } = useListFocus({
106
104
  itemSelector: '[data-avatar-item]',
107
105
  orientation: 'horizontal',
108
- hasRovingTabIndex: true,
109
- isRtl
106
+ hasRovingTabIndex: true
110
107
  });
111
108
  useIsomorphicLayoutEffect(() => {
112
109
  const root = listRef.current;
@@ -114,7 +111,6 @@ export function AvatarGroup({
114
111
  return;
115
112
  }
116
113
  setHasInteractiveItems(root.querySelector('[data-avatar-item]') != null);
117
- setIsRtl(getComputedStyle(root).direction === 'rtl');
118
114
  });
119
115
  const hintId = useId();
120
116
  const describedBy = [ariaDescribedByFromProps, hasInteractiveItems ? hintId : null].filter(Boolean).join(' ') || undefined;
@@ -5,7 +5,9 @@
5
5
  * @position Horizontal scroll container with fade-edge overflow indication,
6
6
  * optional prev/next buttons on the top layer, scroll-snap, a 1px
7
7
  * visual bleed allowance for child selection indicators, and Shift + wheel
8
- * mapping so mouse users can scroll horizontally. Exposes APG
8
+ * mapping so mouse users can scroll horizontally. Supports optional
9
+ * wrap-around looping and an imperative handle (handleRef) for programmatic
10
+ * scroll control. Exposes APG
9
11
  * carousel semantics: the root is a labelled region with
10
12
  * aria-roledescription="carousel" and each item wrapper is a group with
11
13
  * aria-roledescription="slide" named "Slide N of M".
@@ -18,8 +20,47 @@
18
20
  import { type ReactNode } from 'react';
19
21
  import type { BaseProps } from '../BaseProps';
20
22
  import type { SpacingStep } from '../utils/types';
23
+ /**
24
+ * Imperative control surface for the Carousel, accessed via the `handleRef`
25
+ * prop. Methods drive the same native-scroll machinery as the built-in
26
+ * buttons, so they respect RTL, reduced-motion, and `hasLoop`.
27
+ */
28
+ export interface CarouselHandle {
29
+ /**
30
+ * Scroll forward by roughly one viewport. With `hasLoop`, wraps to the
31
+ * start once the end is reached.
32
+ */
33
+ scrollNext(): void;
34
+ /**
35
+ * Scroll backward by roughly one viewport. With `hasLoop`, wraps to the
36
+ * end once the start is reached.
37
+ */
38
+ scrollPrev(): void;
39
+ /**
40
+ * Scroll the item at the given 0-based index to the start edge. The index
41
+ * is clamped to the item range, and only the carousel scrolls — the page
42
+ * position is left untouched.
43
+ */
44
+ scrollTo(index: number): void;
45
+ /**
46
+ * Whether there is scrollable content past the trailing edge. With
47
+ * `hasLoop`, returns true whenever the content overflows, since wrapping
48
+ * is always available. Reads live state — safe to call in an event handler.
49
+ */
50
+ canScrollNext(): boolean;
51
+ /**
52
+ * Whether there is scrollable content past the leading edge. With
53
+ * `hasLoop`, returns true whenever the content overflows. Reads live state.
54
+ */
55
+ canScrollPrev(): boolean;
56
+ }
21
57
  export interface CarouselProps extends BaseProps<HTMLDivElement> {
22
58
  ref?: React.Ref<HTMLDivElement>;
59
+ /**
60
+ * Imperative handle for programmatic scroll control. Exposes scrollNext,
61
+ * scrollPrev, scrollTo, and the canScrollNext/canScrollPrev queries.
62
+ */
63
+ handleRef?: React.Ref<CarouselHandle>;
23
64
  /** Carousel items — rendered in a horizontal scroll container. */
24
65
  children: ReactNode;
25
66
  /**
@@ -39,6 +80,16 @@ export interface CarouselProps extends BaseProps<HTMLDivElement> {
39
80
  * @default true
40
81
  */
41
82
  hasEdgeFade?: boolean;
83
+ /**
84
+ * Enable wrap-around scrolling. When the content overflows, pressing Next
85
+ * at the end scrolls back to the start, and Prev at the start scrolls to
86
+ * the end — for both the built-in buttons and the imperative handle. The
87
+ * navigation buttons stay visible at both edges instead of hiding, since a
88
+ * scroll is always available. Has no effect when the content fits without
89
+ * overflowing.
90
+ * @default false
91
+ */
92
+ hasLoop?: boolean;
42
93
  /**
43
94
  * Enable scroll-snap on items. Each direct child snaps to the start edge.
44
95
  * @default false
@@ -79,7 +130,7 @@ export interface CarouselProps extends BaseProps<HTMLDivElement> {
79
130
  * </Carousel>
80
131
  * ```
81
132
  */
82
- export declare function Carousel({ ref, children, gap, hasButtons, hasEdgeFade, hasSnap, padding, 'aria-label': ariaLabelFromProps, xstyle, className, style, 'data-testid': testId, ...htmlProps }: CarouselProps): import("react").JSX.Element;
133
+ export declare function Carousel({ ref, handleRef, children, gap, hasButtons, hasEdgeFade, hasLoop, hasSnap, padding, 'aria-label': ariaLabelFromProps, xstyle, className, style, 'data-testid': testId, ...htmlProps }: CarouselProps): import("react").JSX.Element;
83
134
  export declare namespace Carousel {
84
135
  var displayName: string;
85
136
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Carousel.d.ts","sourceRoot":"","sources":["../../src/Carousel/Carousel.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EACL,KAAK,SAAS,EAMf,MAAM,OAAO,CAAC;AAef,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAIhD,MAAM,WAAW,aAAc,SAAQ,SAAS,CAAC,cAAc,CAAC;IAC9D,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC,kEAAkE;IAClE,QAAQ,EAAE,SAAS,CAAC;IACpB;;;OAGG;IACH,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACpC;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA+JD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,QAAQ,CAAC,EACvB,GAAG,EACH,QAAQ,EACR,GAAO,EACP,UAAiB,EACjB,WAAkB,EAClB,OAAe,EACf,OAAO,EACP,YAAY,EAAE,kBAAkB,EAChC,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,GAAG,SAAS,EACb,EAAE,aAAa,+BA6Mf;yBA3Ne,QAAQ"}
1
+ {"version":3,"file":"Carousel.d.ts","sourceRoot":"","sources":["../../src/Carousel/Carousel.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EACL,KAAK,SAAS,EAOf,MAAM,OAAO,CAAC;AAef,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAIhD;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,UAAU,IAAI,IAAI,CAAC;IACnB;;;OAGG;IACH,UAAU,IAAI,IAAI,CAAC;IACnB;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B;;;;OAIG;IACH,aAAa,IAAI,OAAO,CAAC;IACzB;;;OAGG;IACH,aAAa,IAAI,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,aAAc,SAAQ,SAAS,CAAC,cAAc,CAAC;IAC9D,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;OAGG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IACtC,kEAAkE;IAClE,QAAQ,EAAE,SAAS,CAAC;IACpB;;;OAGG;IACH,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACpC;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA+JD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,QAAQ,CAAC,EACvB,GAAG,EACH,SAAS,EACT,QAAQ,EACR,GAAO,EACP,UAAiB,EACjB,WAAkB,EAClB,OAAe,EACf,OAAe,EACf,OAAO,EACP,YAAY,EAAE,kBAAkB,EAChC,MAAM,EACN,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,EACrB,GAAG,SAAS,EACb,EAAE,aAAa,+BAiRf;yBAjSe,QAAQ"}
@@ -9,7 +9,9 @@
9
9
  * @position Horizontal scroll container with fade-edge overflow indication,
10
10
  * optional prev/next buttons on the top layer, scroll-snap, a 1px
11
11
  * visual bleed allowance for child selection indicators, and Shift + wheel
12
- * mapping so mouse users can scroll horizontally. Exposes APG
12
+ * mapping so mouse users can scroll horizontally. Supports optional
13
+ * wrap-around looping and an imperative handle (handleRef) for programmatic
14
+ * scroll control. Exposes APG
13
15
  * carousel semantics: the root is a labelled region with
14
16
  * aria-roledescription="carousel" and each item wrapper is a group with
15
17
  * aria-roledescription="slide" named "Slide N of M".
@@ -19,7 +21,7 @@
19
21
  * - /apps/storybook/stories/Carousel.stories.tsx
20
22
  * - /packages/cli/assets/templates/blocks/components/Carousel/ (showcase blocks)
21
23
  */
22
- import { useRef, useCallback, useEffect, Children, isValidElement } from 'react';
24
+ import { useRef, useCallback, useEffect, useImperativeHandle, Children, isValidElement } from 'react';
23
25
  import * as stylex from '@stylexjs/stylex';
24
26
  import "../theme/tokens.stylex.js";
25
27
  import { spacingVars, colorVars, shadowVars, radiusVars, durationVars, easeVars } from "../theme/tokens.stylex.js";
@@ -31,6 +33,12 @@ import { isRtlElement } from "../hooks/isRtlElement.js";
31
33
  import { mergeProps, mergeRefs, rtlStyles } from "../utils/index.js";
32
34
  import { themeProps } from "../utils/themeProps.js";
33
35
  import { useTranslator } from "../i18n/index.js";
36
+
37
+ /**
38
+ * Imperative control surface for the Carousel, accessed via the `handleRef`
39
+ * prop. Methods drive the same native-scroll machinery as the built-in
40
+ * buttons, so they respect RTL, reduced-motion, and `hasLoop`.
41
+ */
34
42
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
35
43
  // =============================================================================
36
44
  // Styles
@@ -203,10 +211,12 @@ const paddingStyles = {
203
211
  */
204
212
  export function Carousel({
205
213
  ref,
214
+ handleRef,
206
215
  children,
207
216
  gap = 1,
208
217
  hasButtons = true,
209
218
  hasEdgeFade = true,
219
+ hasLoop = false,
210
220
  hasSnap = false,
211
221
  padding,
212
222
  'aria-label': ariaLabelFromProps,
@@ -222,7 +232,8 @@ export function Carousel({
222
232
  const {
223
233
  scrollRef,
224
234
  overflowStart,
225
- overflowEnd
235
+ overflowEnd,
236
+ hasOverflow
226
237
  } = useScrollOverflow();
227
238
 
228
239
  // Children.toArray drops null/undefined/boolean children and assigns
@@ -277,22 +288,78 @@ export function Carousel({
277
288
  if (!el) {
278
289
  return;
279
290
  }
280
- const firstChild = el.firstElementChild;
281
- const itemWidth = firstChild ? firstChild.offsetWidth : 0;
282
- const amount = el.clientWidth - itemWidth * 0.5;
283
291
  // Respect the user's reduced-motion preference — mirrors the CSS
284
292
  // scroll-behavior override so button-driven scrolling doesn't animate
285
293
  // for users who opted out of motion.
286
294
  const prefersReducedMotion = typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
295
+ const behavior = prefersReducedMotion ? 'auto' : 'smooth';
296
+ // Under RTL the scroll axis is inverted (start is scrollLeft 0, the end
297
+ // is negative), so flip the physical delta sign.
298
+ const rtlSign = isRtlElement(el) ? -1 : 1;
299
+
300
+ // Wrap-around: when looping over overflowing content, a press that would
301
+ // run past an edge jumps to the opposite edge instead. Overshoot by the
302
+ // full scroll width in the opposite logical direction and let the browser
303
+ // clamp to the far edge — this reuses the same RTL sign convention as a
304
+ // normal scroll, so it stays direction-correct without special-casing.
305
+ if (hasLoop && hasOverflow) {
306
+ const atEnd = direction === 1 && !overflowEnd;
307
+ const atStart = direction === -1 && !overflowStart;
308
+ if (atEnd || atStart) {
309
+ el.scrollBy({
310
+ left: rtlSign * -direction * el.scrollWidth,
311
+ behavior
312
+ });
313
+ return;
314
+ }
315
+ }
316
+ const firstChild = el.firstElementChild;
317
+ const itemWidth = firstChild ? firstChild.offsetWidth : 0;
318
+ const amount = el.clientWidth - itemWidth * 0.5;
287
319
  el.scrollBy({
288
320
  // `direction` is the logical intent (-1 = toward content start, +1 =
289
- // toward content end). Under RTL the scroll axis is inverted (start is
290
- // scrollLeft 0, the end is negative), so flip the physical delta sign.
291
- left: (isRtlElement(el) ? -1 : 1) * direction * Math.max(amount, itemWidth),
321
+ // toward content end).
322
+ left: rtlSign * direction * Math.max(amount, itemWidth),
323
+ behavior
324
+ });
325
+ }, [hasLoop, hasOverflow, overflowStart, overflowEnd]);
326
+ const scrollToIndex = useCallback(index => {
327
+ const el = scrollElRef.current;
328
+ const items = el?.children;
329
+ if (!el || !items || items.length === 0) {
330
+ return;
331
+ }
332
+ const clamped = Math.max(0, Math.min(index, items.length - 1));
333
+ const target = items[clamped];
334
+ const prefersReducedMotion = typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
335
+ // Bring the item to the start edge by scrolling the container by the
336
+ // measured gap between the item and the container edge. Using scrollBy
337
+ // (not scrollIntoView) keeps the scroll contained to the carousel and
338
+ // never moves ancestors or the page. In RTL the start edge is the right
339
+ // edge, so align the trailing edges instead of the leading ones.
340
+ const containerRect = el.getBoundingClientRect();
341
+ const itemRect = target.getBoundingClientRect();
342
+ const delta = isRtlElement(el) ? itemRect.right - containerRect.right : itemRect.left - containerRect.left;
343
+ el.scrollBy({
344
+ left: delta,
292
345
  behavior: prefersReducedMotion ? 'auto' : 'smooth'
293
346
  });
294
347
  }, []);
295
- const fadeStyle = hasEdgeFade ? overflowStart && overflowEnd ? styles.fadeBoth : overflowStart ? styles.fadeStart : overflowEnd ? styles.fadeEnd : null : null;
348
+ useImperativeHandle(handleRef, () => ({
349
+ scrollNext: () => scrollBy(1),
350
+ scrollPrev: () => scrollBy(-1),
351
+ scrollTo: index => scrollToIndex(index),
352
+ // With loop, either direction is reachable whenever the content
353
+ // overflows; otherwise reflect the live per-edge overflow state.
354
+ canScrollNext: () => hasLoop ? hasOverflow : overflowEnd,
355
+ canScrollPrev: () => hasLoop ? hasOverflow : overflowStart
356
+ }), [scrollBy, scrollToIndex, hasLoop, hasOverflow, overflowStart, overflowEnd]);
357
+
358
+ // With loop, the buttons stay reachable at both edges as long as there's
359
+ // something to scroll; without loop they follow the per-edge overflow state.
360
+ const canScrollStart = hasLoop && hasOverflow ? true : overflowStart;
361
+ const canScrollEnd = hasLoop && hasOverflow ? true : overflowEnd;
362
+ const fadeStyle = hasEdgeFade ? hasLoop && hasOverflow || overflowStart && overflowEnd ? styles.fadeBoth : overflowStart ? styles.fadeStart : overflowEnd ? styles.fadeEnd : null : null;
296
363
 
297
364
  // Self-authored position styles (positioning: 'custom' below): a cover
298
365
  // centered on the anchor, sized to it — direction-neutral by construction.
@@ -341,7 +408,7 @@ export function Carousel({
341
408
  1: {
342
409
  className: "x78zum5 x6s0dn4 xl56j7k x1prclbq xjspbzw x14hfi27 x19991ni xuedmi6 xlr8y92 xuuh30 x1nyx83j xg01cxk x47corl"
343
410
  }
344
- }[!!!overflowStart << 0],
411
+ }[!!!canScrollStart << 0],
345
412
  children: /*#__PURE__*/_jsx(Button, {
346
413
  icon: /*#__PURE__*/_jsx("span", {
347
414
  ...stylex.props(rtlStyles.mirror),
@@ -359,7 +426,7 @@ export function Carousel({
359
426
  // tab order and a11y tree while it's visually hidden, so
360
427
  // keyboard users don't land on an invisible control.
361
428
  ,
362
- isDisabled: !overflowStart,
429
+ isDisabled: !canScrollStart,
363
430
  onClick: () => scrollBy(-1),
364
431
  xstyle: styles.buttonRadiusOverride
365
432
  })
@@ -371,7 +438,7 @@ export function Carousel({
371
438
  1: {
372
439
  className: "x78zum5 x6s0dn4 xl56j7k x1prclbq xjspbzw x14hfi27 x19991ni xuedmi6 xlr8y92 xitnhlw x36dvrd xg01cxk x47corl"
373
440
  }
374
- }[!!!overflowEnd << 0],
441
+ }[!!!canScrollEnd << 0],
375
442
  children: /*#__PURE__*/_jsx(Button, {
376
443
  icon: /*#__PURE__*/_jsx("span", {
377
444
  ...stylex.props(rtlStyles.mirror),
@@ -387,7 +454,7 @@ export function Carousel({
387
454
  // See "Scroll left" — disabled while visually hidden so the
388
455
  // button stays mounted but out of the tab order / a11y tree.
389
456
  ,
390
- isDisabled: !overflowEnd,
457
+ isDisabled: !canScrollEnd,
391
458
  onClick: () => scrollBy(1),
392
459
  xstyle: styles.buttonRadiusOverride
393
460
  })
@@ -5,5 +5,5 @@
5
5
  * @position Public entry point for the Carousel module
6
6
  */
7
7
  export { Carousel } from './Carousel';
8
- export type { CarouselProps } from './Carousel';
8
+ export type { CarouselProps, CarouselHandle } from './Carousel';
9
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/Carousel/index.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AAEH,OAAO,EAAC,QAAQ,EAAC,MAAM,YAAY,CAAC;AACpC,YAAY,EAAC,aAAa,EAAC,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/Carousel/index.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AAEH,OAAO,EAAC,QAAQ,EAAC,MAAM,YAAY,CAAC;AACpC,YAAY,EAAC,aAAa,EAAE,cAAc,EAAC,MAAM,YAAY,CAAC"}
@@ -67,7 +67,7 @@ export interface CommandPaletteInputProps extends Omit<BaseProps<HTMLInputElemen
67
67
  * </CommandPalette>
68
68
  * ```
69
69
  */
70
- export declare function CommandPaletteInput({ value: controlledValue, onValueChange, placeholder: placeholderFromProps, label, hasAutoFocus, endContent, onChange, onKeyDown, ref, xstyle, ...props }: CommandPaletteInputProps): import("react").JSX.Element;
70
+ export declare function CommandPaletteInput({ value: controlledValue, onValueChange, placeholder: placeholderFromProps, label, hasAutoFocus, endContent, onChange, onKeyDown, ref, xstyle, className, style, ...props }: CommandPaletteInputProps): import("react").JSX.Element;
71
71
  export declare namespace CommandPaletteInput {
72
72
  var displayName: string;
73
73
  }
@@ -1 +1 @@
1
- {"version":3,"file":"CommandPaletteInput.d.ts","sourceRoot":"","sources":["../../src/CommandPalette/CommandPaletteInput.tsx"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AAEH,OAAO,EAAiC,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAarE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AA2D5C,MAAM,WAAW,wBAAyB,SAAQ,IAAI,CACpD,SAAS,CAAC,gBAAgB,CAAC,EAC3B,UAAU,CACX;IACC,iEAAiE;IACjE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAElC;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAExC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;;OAIG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;IAEvB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,KAAK,CAAC,kBAAkB,CAAC,gBAAgB,CAAC,CAAC;CACvD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EAAE,eAAe,EACtB,aAAa,EACb,WAAW,EAAE,oBAAoB,EACjC,KAAK,EACL,YAAmB,EACnB,UAAU,EACV,QAAQ,EACR,SAAS,EACT,GAAG,EACH,MAAM,EACN,GAAG,KAAK,EACT,EAAE,wBAAwB,+BAqF1B;yBAjGe,mBAAmB"}
1
+ {"version":3,"file":"CommandPaletteInput.d.ts","sourceRoot":"","sources":["../../src/CommandPalette/CommandPaletteInput.tsx"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AAEH,OAAO,EAAiC,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAarE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AA2D5C,MAAM,WAAW,wBAAyB,SAAQ,IAAI,CACpD,SAAS,CAAC,gBAAgB,CAAC,EAC3B,UAAU,CACX;IACC,iEAAiE;IACjE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAElC;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAExC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB;;;;OAIG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;IAEvB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,KAAK,CAAC,kBAAkB,CAAC,gBAAgB,CAAC,CAAC;CACvD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EAAE,eAAe,EACtB,aAAa,EACb,WAAW,EAAE,oBAAoB,EACjC,KAAK,EACL,YAAmB,EACnB,UAAU,EACV,QAAQ,EACR,SAAS,EACT,GAAG,EACH,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,KAAK,EACT,EAAE,wBAAwB,+BAuF1B;yBArGe,mBAAmB"}
@@ -65,6 +65,8 @@ export function CommandPaletteInput({
65
65
  onKeyDown,
66
66
  ref,
67
67
  xstyle,
68
+ className,
69
+ style,
68
70
  ...props
69
71
  }) {
70
72
  const t = useTranslator();
@@ -100,7 +102,7 @@ export function CommandPaletteInput({
100
102
  ctx?.onKeyDown(e);
101
103
  }, [ctx, onKeyDown]);
102
104
  return /*#__PURE__*/_jsxs("div", {
103
- ...mergeProps(themeProps('command-palette-input'), stylex.props(styles.wrapper, xstyle)),
105
+ ...mergeProps(themeProps('command-palette-input'), stylex.props(styles.wrapper, xstyle), className, style),
104
106
  children: [/*#__PURE__*/_jsx("span", {
105
107
  ...{
106
108
  className: "x78zum5 x6s0dn4 x2lah0s xv1l7n4"
@@ -96,7 +96,7 @@ export type ContextMenuProps = ContextMenuDataProps | ContextMenuCompoundProps;
96
96
  * </ContextMenu>
97
97
  * ```
98
98
  */
99
- export declare function ContextMenu({ children, menuWidth, size, label: labelFromProps, isDisabled, onOpenChange, ref, className, style, xstyle, triggerXstyle, 'data-testid': testId, ...props }: ContextMenuProps): React.JSX.Element;
99
+ export declare function ContextMenu({ children, menuWidth, size, label: labelFromProps, isDisabled, onOpenChange, ref, className, style, xstyle, triggerXstyle, 'data-testid': testId, ...rest }: ContextMenuProps): React.JSX.Element;
100
100
  export declare namespace ContextMenu {
101
101
  var displayName: string;
102
102
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ContextMenu.d.ts","sourceRoot":"","sources":["../../src/ContextMenu/ContextMenu.tsx"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAON,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,OAAO,CAAC;AA0BrC,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAGjD,OAAO,KAAK,EACV,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,8BAA8B,CAAC;AAqDtC,MAAM,MAAM,mBAAmB,GAAG,oBAAoB,CAAC;AAEvD,MAAM,MAAM,kBAAkB,GAAG,mBAAmB,CAAC;AAErD,MAAM,MAAM,kBAAkB,GAAG,mBAAmB,CAAC;AAErD,MAAM,MAAM,iBAAiB,GAAG,kBAAkB,CAAC;AAMnD,UAAU,oBAAqB,SAAQ,SAAS;IAC9C,oDAAoD;IACpD,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,YAAY,GAAG,YAAY,EAAE,CAAC;IAC9C,+DAA+D;IAC/D,QAAQ,EAAE,SAAS,CAAC;IACpB,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,wCAAwC;IACxC,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACzC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,UAAU,oBAAqB,SAAQ,oBAAoB;IACzD,gDAAgD;IAChD,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC3B,WAAW,CAAC,EAAE,SAAS,CAAC;CACzB;AAED,UAAU,wBAAyB,SAAQ,oBAAoB;IAC7D,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,+CAA+C;IAC/C,WAAW,EAAE,SAAS,CAAC;CACxB;AAED,MAAM,MAAM,gBAAgB,GAAG,oBAAoB,GAAG,wBAAwB,CAAC;AAM/E;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,SAAS,EACT,IAAW,EACX,KAAK,EAAE,cAAc,EACrB,UAAkB,EAClB,YAAY,EACZ,GAAG,EACH,SAAS,EACT,KAAK,EACL,MAAM,EACN,aAAa,EACb,aAAa,EAAE,MAAM,EACrB,GAAG,KAAK,EACT,EAAE,gBAAgB,qBA4RlB;yBA1Se,WAAW"}
1
+ {"version":3,"file":"ContextMenu.d.ts","sourceRoot":"","sources":["../../src/ContextMenu/ContextMenu.tsx"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAON,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,OAAO,CAAC;AA0BrC,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAGjD,OAAO,KAAK,EACV,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,8BAA8B,CAAC;AAqDtC,MAAM,MAAM,mBAAmB,GAAG,oBAAoB,CAAC;AAEvD,MAAM,MAAM,kBAAkB,GAAG,mBAAmB,CAAC;AAErD,MAAM,MAAM,kBAAkB,GAAG,mBAAmB,CAAC;AAErD,MAAM,MAAM,iBAAiB,GAAG,kBAAkB,CAAC;AAMnD,UAAU,oBAAqB,SAAQ,SAAS;IAC9C,oDAAoD;IACpD,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,YAAY,GAAG,YAAY,EAAE,CAAC;IAC9C,+DAA+D;IAC/D,QAAQ,EAAE,SAAS,CAAC;IACpB,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,wCAAwC;IACxC,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACzC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,UAAU,oBAAqB,SAAQ,oBAAoB;IACzD,gDAAgD;IAChD,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC3B,WAAW,CAAC,EAAE,SAAS,CAAC;CACzB;AAED,UAAU,wBAAyB,SAAQ,oBAAoB;IAC7D,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,+CAA+C;IAC/C,WAAW,EAAE,SAAS,CAAC;CACxB;AAED,MAAM,MAAM,gBAAgB,GAAG,oBAAoB,GAAG,wBAAwB,CAAC;AAM/E;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,SAAS,EACT,IAAW,EACX,KAAK,EAAE,cAAc,EACrB,UAAkB,EAClB,YAAY,EACZ,GAAG,EACH,SAAS,EACT,KAAK,EACL,MAAM,EACN,aAAa,EACb,aAAa,EAAE,MAAM,EACrB,GAAG,IAAI,EACR,EAAE,gBAAgB,qBAwSlB;yBAtTe,WAAW"}
@@ -131,12 +131,20 @@ export function ContextMenu({
131
131
  xstyle,
132
132
  triggerXstyle,
133
133
  'data-testid': testId,
134
- ...props
134
+ ...rest
135
135
  }) {
136
136
  const t = useTranslator();
137
137
  const label = labelFromProps ?? t('@astryx.contextMenu.label');
138
- const items = ('items' in props ? props.items : undefined) ?? [];
139
- const menuContent = 'menuContent' in props ? props.menuContent : undefined;
138
+ // Separate content props (union discriminant) from DOM pass-through attrs.
139
+ // The union means exactly one of items/menuContent exists in rest; destructure
140
+ // both so triggerProps contains only DOM-safe attributes.
141
+ const {
142
+ items: itemsProp,
143
+ menuContent: menuContentProp,
144
+ ...triggerProps
145
+ } = rest;
146
+ const items = itemsProp ?? [];
147
+ const menuContent = menuContentProp;
140
148
  const menuId = useId();
141
149
  // Cursor point in the trigger's local coordinate space (offset from the
142
150
  // trigger's top-left, in-flow). Stored here and written to the zero-size
@@ -325,12 +333,13 @@ export function ContextMenu({
325
333
  closeMenu,
326
334
  menuSize: size
327
335
  }), [closeMenu, size]);
328
- const resolvedMenuContent = props.items !== undefined ? renderDropdownItems(items) : menuContent;
336
+ const resolvedMenuContent = itemsProp !== undefined ? renderDropdownItems(items) : menuContent;
329
337
  return /*#__PURE__*/_jsxs(_Fragment, {
330
338
  children: [/*#__PURE__*/_jsxs("div", {
331
339
  ref: mergeRefs(ref, triggerRef),
332
340
  onContextMenu: handleContextMenu,
333
341
  ...longPressHandlers,
342
+ ...triggerProps,
334
343
  "data-testid": testId,
335
344
  ...stylex.props(styles.trigger, ...(triggerXstyle ? Array.isArray(triggerXstyle) ? triggerXstyle : [triggerXstyle] : [])),
336
345
  children: [children, /*#__PURE__*/_jsx("span", {
@@ -1 +1 @@
1
- {"version":3,"file":"DropdownMenu.d.ts","sourceRoot":"","sources":["../../src/DropdownMenu/DropdownMenu.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,EAOZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAGf,OAAO,EAAS,KAAK,WAAW,EAAC,MAAM,WAAW,CAAC;AAEnD,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,SAAS,CAAC;AAetC,OAAO,KAAK,EAAC,cAAc,EAAE,cAAc,EAAC,MAAM,mBAAmB,CAAC;AAQtE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAyC5C,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,IAAI,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC5B;;;;OAIG;IACH,KAAK,CAAC,EAAE,kBAAkB,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,SAAS,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,oBAAoB,EAAE,CAAC;CAC/B;AAED,MAAM,MAAM,kBAAkB,GAC5B,oBAAoB,GAAG,mBAAmB,GAAG,mBAAmB,CAAC;AAMnE,MAAM,MAAM,uBAAuB,GAAG,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;AAEnE,UAAU,qBAAsB,SAAQ,SAAS;IAC/C,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACzC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;;OAIG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,UAAU,qBAAsB,SAAQ,qBAAqB;IAC3D,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAC5B,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,UAAU,yBAA0B,SAAQ,qBAAqB;IAC/D,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,MAAM,MAAM,iBAAiB,GAC3B,qBAAqB,GAAG,yBAAyB,CAAC;AA+BpD,wBAAgB,YAAY,CAAC,EAC3B,MAAM,EAAE,eAAe,EACvB,UAAU,EAAE,gBAAgB,EAC5B,YAAY,EACZ,SAAS,EACT,OAAO,EACP,UAAiB,EACjB,SAAmB,EACnB,SAAmB,EACnB,SAAS,EACT,KAAK,EACL,MAAM,EACN,aAAa,EAAE,MAAM,EACrB,GAAG,KAAK,EACT,EAAE,iBAAiB,qBAqRnB;yBAnSe,YAAY"}
1
+ {"version":3,"file":"DropdownMenu.d.ts","sourceRoot":"","sources":["../../src/DropdownMenu/DropdownMenu.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,EAOZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAGf,OAAO,EAAS,KAAK,WAAW,EAAC,MAAM,WAAW,CAAC;AAEnD,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,SAAS,CAAC;AAetC,OAAO,KAAK,EAAC,cAAc,EAAE,cAAc,EAAC,MAAM,mBAAmB,CAAC;AAQtE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAyC5C,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,IAAI,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC5B;;;;OAIG;IACH,KAAK,CAAC,EAAE,kBAAkB,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,SAAS,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,oBAAoB,EAAE,CAAC;CAC/B;AAED,MAAM,MAAM,kBAAkB,GAC5B,oBAAoB,GAAG,mBAAmB,GAAG,mBAAmB,CAAC;AAMnE,MAAM,MAAM,uBAAuB,GAAG,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;AAEnE,UAAU,qBAAsB,SAAQ,SAAS;IAC/C,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACzC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B;;;;OAIG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAE3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,UAAU,qBAAsB,SAAQ,qBAAqB;IAC3D,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAC5B,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,UAAU,yBAA0B,SAAQ,qBAAqB;IAC/D,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,MAAM,MAAM,iBAAiB,GAC3B,qBAAqB,GAAG,yBAAyB,CAAC;AA+BpD,wBAAgB,YAAY,CAAC,EAC3B,MAAM,EAAE,eAAe,EACvB,UAAU,EAAE,gBAAgB,EAC5B,YAAY,EACZ,SAAS,EACT,OAAO,EACP,UAAiB,EACjB,SAAmB,EACnB,SAAmB,EACnB,SAAS,EACT,KAAK,EACL,MAAM,EACN,aAAa,EAAE,MAAM,EACrB,GAAG,KAAK,EACT,EAAE,iBAAiB,qBAwRnB;yBAtSe,YAAY"}
@@ -160,27 +160,21 @@ export function DropdownMenu({
160
160
  // Close menu + return focus to trigger
161
161
  const handleLayerHide = useCallback(() => {
162
162
  lastHideTimeRef.current = Date.now();
163
- if (isControlled) {
164
- onOpenChange?.(false);
165
- } else {
163
+ onOpenChange?.(false);
164
+ if (!isControlled) {
166
165
  setInternalIsOpen(false);
167
166
  }
168
167
  buttonRef.current?.focus();
169
168
  }, [isControlled, onOpenChange]);
170
169
 
171
- // Track whether to focus the first item when the menu opens
170
+ // Defer item focus until the layer has committed open, so focus restore
171
+ // captures the trigger instead of the first menu item.
172
172
  const shouldFocusOnOpenRef = useRef(false);
173
173
  const handleLayerShow = useCallback(() => {
174
- if (isControlled) {
175
- onOpenChange?.(true);
176
- } else {
174
+ onOpenChange?.(true);
175
+ if (!isControlled) {
177
176
  setInternalIsOpen(true);
178
177
  }
179
- if (shouldFocusOnOpenRef.current) {
180
- shouldFocusOnOpenRef.current = false;
181
- // focusFirst is called via openAndFocus below — defer to rAF
182
- // so the popover content is rendered before we query for items
183
- }
184
178
  }, [isControlled, onOpenChange]);
185
179
  const popover = usePopover({
186
180
  onHide: handleLayerHide,
@@ -223,17 +217,26 @@ export function DropdownMenu({
223
217
  getCurrentIndex: () => getMenuItems().findIndex(el => el === document.activeElement || el.contains(document.activeElement))
224
218
  });
225
219
 
226
- // Sync controlled open state → popover, and focus first item on open
220
+ // Sync controlled open state → popover.
227
221
  useEffect(() => {
228
222
  if (isControlled) {
229
223
  if (controlledIsOpen && !popover.isOpen) {
224
+ shouldFocusOnOpenRef.current = true;
230
225
  popover.show();
231
- requestAnimationFrame(() => focusFirst());
232
226
  } else if (!controlledIsOpen && popover.isOpen) {
233
227
  popover.hide();
234
228
  }
235
229
  }
236
- }, [controlledIsOpen, isControlled, popover, focusFirst]);
230
+ }, [controlledIsOpen, isControlled, popover]);
231
+
232
+ // Move focus into the menu only after the layer has committed open.
233
+ useEffect(() => {
234
+ if (!popover.isOpen || !shouldFocusOnOpenRef.current) {
235
+ return;
236
+ }
237
+ shouldFocusOnOpenRef.current = false;
238
+ requestAnimationFrame(() => focusFirst());
239
+ }, [popover.isOpen, focusFirst]);
237
240
 
238
241
  // Extend useListFocus with Enter/Space activation + typeahead
239
242
  const listKeyDown = useCallback(e => {
@@ -267,9 +270,9 @@ export function DropdownMenu({
267
270
  listNavKeyDown(e);
268
271
  }, [listNavKeyDown, closeMenu, typeahead, ownsEvent]);
269
272
  const openAndFocus = useCallback(() => {
273
+ shouldFocusOnOpenRef.current = true;
270
274
  popover.show();
271
- requestAnimationFrame(() => focusFirst());
272
- }, [popover, focusFirst]);
275
+ }, [popover]);
273
276
  const handleButtonClick = useCallback(() => {
274
277
  // If the menu was just closed by light dismiss (e.g. iOS Safari fires
275
278
  // pointerdown → hide before the trigger's click), the click would
@@ -16,6 +16,16 @@ import type { DefinedTheme } from '../theme/defineTheme';
16
16
  * visual representation. Themes provide the actual icon components.
17
17
  */
18
18
  export type IconName = 'close' | 'chevronDown' | 'chevronLeft' | 'chevronRight' | 'check' | 'success' | 'error' | 'warning' | 'info' | 'calendar' | 'clock' | 'externalLink' | 'menu' | 'moreHorizontal' | 'search' | 'arrowUp' | 'arrowDown' | 'arrowsUpDown' | 'funnel' | 'eyeSlash' | 'viewColumns' | 'copy' | 'checkDouble' | 'wrench' | 'stop' | 'microphone';
19
+ /**
20
+ * A semantic icon name — either one of the built-in {@link IconName}s or an
21
+ * arbitrary string key contributed by a library/app.
22
+ *
23
+ * The `(string & {})` intersection keeps the built-in names available for
24
+ * autocomplete while still allowing any string, so downstream libraries can
25
+ * register and resolve their own keys (e.g. `'richtext:bold'`) without having
26
+ * to widen the core `IconName` union.
27
+ */
28
+ export type ExtendedIconName = IconName | (string & {});
19
29
  /**
20
30
  * Icon registry mapping semantic names to React nodes.
21
31
  */
@@ -34,8 +44,17 @@ export type IconRegistrySource = DefinedTheme | string | null | undefined;
34
44
  * import { brandIcons } from './brand-icons';
35
45
  * registerIcons(brandIcons);
36
46
  * ```
47
+ *
48
+ * Libraries may also register their own extension keys (any string), so a
49
+ * theme can override them the same way it overrides built-in icons:
50
+ * @example
51
+ * ```
52
+ * // In a library that ships its own icons:
53
+ * registerIcons({ 'richtext:bold': <MyBoldIcon /> });
54
+ * // resolve with getIcon('richtext:bold')
55
+ * ```
37
56
  */
38
- export declare function registerIcons(icons: Partial<IconRegistry>): void;
57
+ export declare function registerIcons(icons: Partial<Record<ExtendedIconName, ReactNode>>): void;
39
58
  /**
40
59
  * Get a snapshot of the full icon registry, with registered icons overriding
41
60
  * built-in defaults.
@@ -50,8 +69,29 @@ export declare function getIconRegistry(source?: IconRegistrySource): Readonly<I
50
69
  *
51
70
  * Works in both server and client environments.
52
71
  * Falls back to built-in default icons when no override is registered.
72
+ *
73
+ * Accepts extension keys (any string) in addition to the built-in
74
+ * {@link IconName}s — useful for library-contributed icons. For a
75
+ * caller-supplied fallback when a key isn't registered, use
76
+ * {@link getExtendedIcon}.
77
+ */
78
+ export declare function getIcon(name: ExtendedIconName, source?: IconRegistrySource): ReactNode;
79
+ /**
80
+ * Resolve an extension icon by an arbitrary string key, falling back to a
81
+ * caller-supplied default when nothing is registered.
82
+ *
83
+ * This is the seam libraries use to make their own icons themeable: ship the
84
+ * inline SVG as `fallback`, resolve through this function, and a theme can
85
+ * override the key via {@link registerIcons} without the library having to
86
+ * widen the core {@link IconName} union.
87
+ *
88
+ * @example
89
+ * ```
90
+ * // Library default, overridable by a theme registering 'richtext:bold':
91
+ * getExtendedIcon('richtext:bold', <BoldGlyph />)
92
+ * ```
53
93
  */
54
- export declare function getIcon(name: IconName, source?: IconRegistrySource): ReactNode;
94
+ export declare function getExtendedIcon(name: ExtendedIconName, fallback?: ReactNode, source?: IconRegistrySource): ReactNode;
55
95
  /**
56
96
  * Reset the global registry. For testing only.
57
97
  * @internal
@@ -1 +1 @@
1
- {"version":3,"file":"globalIconRegistry.d.ts","sourceRoot":"","sources":["../../src/Icon/globalIconRegistry.tsx"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,OAAO,CAAC;AAErC,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,sBAAsB,CAAC;AAQvD;;;;;GAKG;AAEH,MAAM,MAAM,QAAQ,GAChB,OAAO,GACP,aAAa,GACb,aAAa,GACb,cAAc,GACd,OAAO,GACP,SAAS,GACT,OAAO,GACP,SAAS,GACT,MAAM,GACN,UAAU,GACV,OAAO,GACP,cAAc,GACd,MAAM,GACN,gBAAgB,GAChB,QAAQ,GACR,SAAS,GACT,WAAW,GACX,cAAc,GACd,QAAQ,GACR,UAAU,GACV,aAAa,GACb,MAAM,GACN,aAAa,GACb,QAAQ,GACR,MAAM,GACN,YAAY,CAAC;AAEjB;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AAEvD,MAAM,MAAM,kBAAkB,GAAG,YAAY,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;AAsB1E;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,IAAI,CAOhE;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,MAAM,CAAC,EAAE,kBAAkB,GAC1B,QAAQ,CAAC,YAAY,CAAC,CAexB;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CACrB,IAAI,EAAE,QAAQ,EACd,MAAM,CAAC,EAAE,kBAAkB,GAC1B,SAAS,CAGX;AAED;;;GAGG;AACH,wBAAgB,UAAU,IAAI,IAAI,CAEjC"}
1
+ {"version":3,"file":"globalIconRegistry.d.ts","sourceRoot":"","sources":["../../src/Icon/globalIconRegistry.tsx"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,OAAO,CAAC;AAErC,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,sBAAsB,CAAC;AAQvD;;;;;GAKG;AAEH,MAAM,MAAM,QAAQ,GAChB,OAAO,GACP,aAAa,GACb,aAAa,GACb,cAAc,GACd,OAAO,GACP,SAAS,GACT,OAAO,GACP,SAAS,GACT,MAAM,GACN,UAAU,GACV,OAAO,GACP,cAAc,GACd,MAAM,GACN,gBAAgB,GAChB,QAAQ,GACR,SAAS,GACT,WAAW,GACX,cAAc,GACd,QAAQ,GACR,UAAU,GACV,aAAa,GACb,MAAM,GACN,aAAa,GACb,QAAQ,GACR,MAAM,GACN,YAAY,CAAC;AAEjB;;;;;;;;GAQG;AACH,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAExD;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AAEvD,MAAM,MAAM,kBAAkB,GAAG,YAAY,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;AAsB1E;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,gBAAgB,EAAE,SAAS,CAAC,CAAC,GAClD,IAAI,CAON;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,MAAM,CAAC,EAAE,kBAAkB,GAC1B,QAAQ,CAAC,YAAY,CAAC,CAkBxB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,OAAO,CACrB,IAAI,EAAE,gBAAgB,EACtB,MAAM,CAAC,EAAE,kBAAkB,GAC1B,SAAS,CAOX;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,gBAAgB,EACtB,QAAQ,CAAC,EAAE,SAAS,EACpB,MAAM,CAAC,EAAE,kBAAkB,GAC1B,SAAS,CAQX;AAED;;;GAGG;AACH,wBAAgB,UAAU,IAAI,IAAI,CAEjC"}