@astryxdesign/core 0.4.1 → 0.4.2-canary.356d2f9

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 (201) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/Avatar/Avatar.d.ts +4 -1
  3. package/dist/Avatar/Avatar.d.ts.map +1 -1
  4. package/dist/Avatar/Avatar.js +19 -23
  5. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
  6. package/dist/AvatarGroup/AvatarGroupOverflow.js +7 -2
  7. package/dist/Button/Button.d.ts.map +1 -1
  8. package/dist/Button/Button.js +9 -7
  9. package/dist/Chat/ChatMessage.d.ts +7 -0
  10. package/dist/Chat/ChatMessage.d.ts.map +1 -1
  11. package/dist/Chat/ChatMessageBubble.d.ts +13 -1
  12. package/dist/Chat/ChatMessageBubble.d.ts.map +1 -1
  13. package/dist/Chat/ChatMessageBubble.js +19 -1
  14. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  15. package/dist/CheckboxInput/CheckboxInput.js +5 -0
  16. package/dist/CommandPalette/CommandPaletteFooter.d.ts.map +1 -1
  17. package/dist/CommandPalette/CommandPaletteFooter.js +5 -3
  18. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  19. package/dist/DateTimeInput/DateTimeInput.js +2 -2
  20. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  21. package/dist/DropdownMenu/DropdownMenuSubMenu.js +19 -15
  22. package/dist/HoverCard/HoverCard.d.ts +1 -1
  23. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  24. package/dist/HoverCard/HoverCard.js +5 -13
  25. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  26. package/dist/HoverCard/useHoverCard.js +6 -5
  27. package/dist/InputGroup/groupStyles.d.ts.map +1 -1
  28. package/dist/InputGroup/groupStyles.js +7 -2
  29. package/dist/Layer/layerHost.d.ts +24 -0
  30. package/dist/Layer/layerHost.d.ts.map +1 -0
  31. package/dist/Layer/layerHost.js +79 -0
  32. package/dist/Layer/useLayer.d.ts +14 -6
  33. package/dist/Layer/useLayer.d.ts.map +1 -1
  34. package/dist/Layer/useLayer.js +158 -30
  35. package/dist/NavItem/navItemStyles.stylex.d.ts +17 -5
  36. package/dist/NavItem/navItemStyles.stylex.d.ts.map +1 -1
  37. package/dist/NavItem/navItemStyles.stylex.js +11 -5
  38. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  39. package/dist/RadioList/RadioListItem.js +5 -0
  40. package/dist/SideNav/SideNav.d.ts +7 -9
  41. package/dist/SideNav/SideNav.d.ts.map +1 -1
  42. package/dist/SideNav/SideNav.js +32 -5
  43. package/dist/SideNav/SideNavCollapseButton.d.ts +25 -9
  44. package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
  45. package/dist/SideNav/SideNavCollapseButton.js +39 -15
  46. package/dist/SideNav/SideNavCollapseContext.d.ts +21 -0
  47. package/dist/SideNav/SideNavCollapseContext.d.ts.map +1 -1
  48. package/dist/SideNav/SideNavCollapseContext.js +16 -2
  49. package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
  50. package/dist/SideNav/SideNavHeading.js +89 -32
  51. package/dist/SideNav/SideNavItem.d.ts +7 -2
  52. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  53. package/dist/SideNav/SideNavItem.js +119 -75
  54. package/dist/SideNav/SideNavSection.d.ts.map +1 -1
  55. package/dist/SideNav/SideNavSection.js +7 -16
  56. package/dist/SideNav/index.d.ts +1 -1
  57. package/dist/SideNav/index.d.ts.map +1 -1
  58. package/dist/Slider/Slider.d.ts.map +1 -1
  59. package/dist/Slider/Slider.js +56 -15
  60. package/dist/Switch/Switch.d.ts.map +1 -1
  61. package/dist/Switch/Switch.js +5 -0
  62. package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
  63. package/dist/Thumbnail/Thumbnail.js +5 -0
  64. package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
  65. package/dist/TopNav/TopNavHeading.js +14 -6
  66. package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
  67. package/dist/TopNav/TopNavMegaMenu.js +25 -93
  68. package/dist/TopNav/TopNavMegaMenuItem.js +1 -1
  69. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  70. package/dist/TopNav/TopNavMenu.js +10 -5
  71. package/dist/astryx.css +40 -8
  72. package/dist/astryx.umd.js +50 -50
  73. package/dist/astryx.umd.js.map +4 -4
  74. package/dist/hooks/containerReveal.stylex.d.ts +71 -4
  75. package/dist/hooks/containerReveal.stylex.d.ts.map +1 -1
  76. package/dist/hooks/containerReveal.stylex.js +57 -5
  77. package/dist/hooks/index.d.ts +2 -2
  78. package/dist/hooks/index.d.ts.map +1 -1
  79. package/dist/hooks/index.js +1 -1
  80. package/dist/hooks/useContainerReveal.d.ts +60 -3
  81. package/dist/hooks/useContainerReveal.d.ts.map +1 -1
  82. package/dist/hooks/useContainerReveal.js +27 -5
  83. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  84. package/dist/hooks/useFocusTrap.js +16 -2
  85. package/dist/hooks/useMenuHover.d.ts +50 -5
  86. package/dist/hooks/useMenuHover.d.ts.map +1 -1
  87. package/dist/hooks/useMenuHover.js +171 -51
  88. package/dist/theme/defineTheme.d.ts +16 -5
  89. package/dist/theme/defineTheme.d.ts.map +1 -1
  90. package/dist/theme/defineTheme.js +44 -48
  91. package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
  92. package/dist/theme/derivedVarRegistry.js +7 -0
  93. package/dist/theme/expandColorScale.d.ts.map +1 -1
  94. package/dist/theme/expandColorScale.js +1 -0
  95. package/dist/theme/expandMotionScale.d.ts +1 -0
  96. package/dist/theme/expandMotionScale.d.ts.map +1 -1
  97. package/dist/theme/expandMotionScale.js +1 -0
  98. package/dist/theme/expandRadiusScale.d.ts +1 -0
  99. package/dist/theme/expandRadiusScale.d.ts.map +1 -1
  100. package/dist/theme/expandRadiusScale.js +1 -0
  101. package/dist/theme/expandTypeScale.d.ts +1 -0
  102. package/dist/theme/expandTypeScale.d.ts.map +1 -1
  103. package/dist/theme/expandTypeScale.js +1 -0
  104. package/dist/theme/mergeComponents.d.ts +20 -0
  105. package/dist/theme/mergeComponents.d.ts.map +1 -0
  106. package/dist/theme/mergeComponents.js +56 -0
  107. package/dist/theme/onMediaTokens.d.ts +6 -1
  108. package/dist/theme/onMediaTokens.d.ts.map +1 -1
  109. package/dist/theme/onMediaTokens.js +11 -3
  110. package/dist/theme/tokens.stylex.d.ts +3 -1
  111. package/dist/theme/tokens.stylex.d.ts.map +1 -1
  112. package/dist/theme/tokens.stylex.js +3 -1
  113. package/locales/en.json +84 -0
  114. package/locales/pseudo.json +63 -0
  115. package/package.json +3 -3
  116. package/src/Avatar/Avatar.doc.mjs +5 -2
  117. package/src/Avatar/Avatar.test.tsx +51 -0
  118. package/src/Avatar/Avatar.tsx +30 -20
  119. package/src/AvatarGroup/AvatarGroup.doc.mjs +1 -1
  120. package/src/AvatarGroup/AvatarGroup.test.tsx +14 -0
  121. package/src/AvatarGroup/AvatarGroupOverflow.tsx +3 -2
  122. package/src/Button/Button.tsx +5 -3
  123. package/src/ButtonGroup/ButtonGroup.test.tsx +7 -4
  124. package/src/Card/Card.doc.mjs +2 -0
  125. package/src/Chat/Chat.doc.mjs +4 -1
  126. package/src/Chat/ChatMessage.doc.mjs +3 -3
  127. package/src/Chat/ChatMessage.tsx +7 -0
  128. package/src/Chat/ChatMessageBubble.doc.mjs +7 -0
  129. package/src/Chat/ChatMessageBubble.test.tsx +95 -6
  130. package/src/Chat/ChatMessageBubble.tsx +25 -0
  131. package/src/CheckboxInput/CheckboxInput.tsx +20 -0
  132. package/src/CodeBlock/CodeBlock.doc.mjs +3 -0
  133. package/src/CommandPalette/CommandPaletteFooter.tsx +6 -3
  134. package/src/DateTimeInput/DateTimeInput.tsx +4 -2
  135. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +72 -1
  136. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +24 -20
  137. package/src/HoverCard/HoverCard.doc.mjs +3 -3
  138. package/src/HoverCard/HoverCard.test.tsx +283 -55
  139. package/src/HoverCard/HoverCard.tsx +5 -13
  140. package/src/HoverCard/useHoverCard.tsx +8 -11
  141. package/src/InputGroup/groupStyles.ts +7 -8
  142. package/src/Item/Item.doc.mjs +4 -0
  143. package/src/Layer/layerHost.test.ts +99 -0
  144. package/src/Layer/layerHost.ts +141 -0
  145. package/src/Layer/useLayer.doc.mjs +14 -4
  146. package/src/Layer/useLayer.test.tsx +332 -7
  147. package/src/Layer/useLayer.tsx +235 -36
  148. package/src/MobileNav/MobileNav.doc.mjs +7 -0
  149. package/src/NavItem/navItemStyles.stylex.ts +31 -5
  150. package/src/RadioList/RadioListItem.tsx +20 -0
  151. package/src/SideNav/SideNav.doc.mjs +13 -4
  152. package/src/SideNav/SideNav.test.tsx +858 -2
  153. package/src/SideNav/SideNav.tsx +37 -16
  154. package/src/SideNav/SideNavCollapseButton.doc.mjs +28 -6
  155. package/src/SideNav/SideNavCollapseButton.tsx +67 -15
  156. package/src/SideNav/SideNavCollapseContext.ts +29 -9
  157. package/src/SideNav/SideNavHeading.tsx +53 -25
  158. package/src/SideNav/SideNavItem.doc.mjs +13 -0
  159. package/src/SideNav/SideNavItem.tsx +97 -84
  160. package/src/SideNav/SideNavSection.tsx +6 -20
  161. package/src/SideNav/index.ts +2 -0
  162. package/src/Slider/Slider.test.tsx +69 -33
  163. package/src/Slider/Slider.tsx +65 -13
  164. package/src/Switch/Switch.tsx +20 -0
  165. package/src/TabList/TabList.doc.mjs +3 -0
  166. package/src/Text/Text.doc.mjs +1 -1
  167. package/src/Thumbnail/Thumbnail.doc.mjs +3 -0
  168. package/src/Thumbnail/Thumbnail.tsx +10 -0
  169. package/src/Timestamp/Timestamp.test.tsx +40 -22
  170. package/src/Toolbar/Toolbar.test.tsx +14 -14
  171. package/src/TopNav/TopNav.test.tsx +86 -1
  172. package/src/TopNav/TopNavHeading.tsx +21 -14
  173. package/src/TopNav/TopNavMegaMenu.test.tsx +43 -0
  174. package/src/TopNav/TopNavMegaMenu.tsx +29 -105
  175. package/src/TopNav/TopNavMegaMenuItem.tsx +1 -1
  176. package/src/TopNav/TopNavMenu.test.tsx +41 -0
  177. package/src/TopNav/TopNavMenu.tsx +14 -4
  178. package/src/TreeList/TreeList.doc.mjs +1 -0
  179. package/src/hooks/containerReveal.stylex.ts +140 -9
  180. package/src/hooks/index.ts +6 -1
  181. package/src/hooks/useContainerReveal.doc.mjs +11 -5
  182. package/src/hooks/useContainerReveal.test.tsx +70 -3
  183. package/src/hooks/useContainerReveal.ts +86 -7
  184. package/src/hooks/useFocusTrap.test.tsx +16 -0
  185. package/src/hooks/useFocusTrap.ts +20 -2
  186. package/src/hooks/useMenuHover.test.tsx +364 -0
  187. package/src/hooks/useMenuHover.ts +243 -47
  188. package/src/theme/defineTheme.test.ts +127 -0
  189. package/src/theme/defineTheme.ts +57 -51
  190. package/src/theme/derivedVarRegistry.test.ts +78 -12
  191. package/src/theme/derivedVarRegistry.ts +4 -0
  192. package/src/theme/expandColorScale.ts +1 -0
  193. package/src/theme/expandMotionScale.ts +1 -0
  194. package/src/theme/expandRadiusScale.ts +1 -0
  195. package/src/theme/expandTypeScale.ts +1 -0
  196. package/src/theme/mergeComponents.ts +59 -0
  197. package/src/theme/onMediaTokens.ts +9 -2
  198. package/src/theme/themingTargets.test.ts +152 -26
  199. package/src/theme/tokens.stylex.ts +3 -1
  200. package/src/theme/tokens.test.ts +12 -0
  201. package/src/theme/useTheme.test.tsx +18 -0
@@ -4,26 +4,53 @@
4
4
 
5
5
  /**
6
6
  * @file useMenuHover.ts
7
- * @input Uses React hooks, useListFocus
7
+ * @input Uses React hooks, useListFocus, useMediaQuery
8
8
  * @output Exports useMenuHover hook
9
- * @position Internal hook; used by nav heading components and TopNavMenu
9
+ * @position Internal hook; used by nav heading components, TopNavMenu,
10
+ * TopNavMegaMenu and DropdownMenuSubMenu
10
11
  *
11
- * Hover as a progressive enhancement on top of standard popover behavior:
12
+ * Hover as a progressive enhancement over standard popover behavior: click
13
+ * toggles, Escape and outside-click close, arrows navigate; on top of that,
14
+ * mouseenter opens after a delay and mouseleave closes one that hover opened.
12
15
  *
13
- * 1. Default popover: click toggles, Escape closes, outside-click closes,
14
- * arrow keys navigate menu items.
16
+ * Three behaviors here are not obvious and are load-bearing:
15
17
  *
16
- * 2. Hover add-on:
17
- * - mouseenter activates "hover mode" and opens after delay
18
- * - While in hover mode, mouseleave closes after delay
19
- * - Any close (click, Escape, outside-click) resets hover mode
20
- * - Click-to-close additionally skips the next mouseenter
18
+ * 1. Hover→click guard (#3121). A hover-opened menu is already open under the
19
+ * cursor when the pointer arrives, so the click that naturally follows would
20
+ * toggle it shut. Within `clickGuardMs` a click instead confirms it: the menu
21
+ * pins and behaves like a click-open from then on.
21
22
  *
22
- * Only uses mouseenter/mouseleave (not mouseover).
23
+ * 2. Focus moves synchronously. `useLayer.show()` calls `showPopover()` in the
24
+ * same tick and the layer's children are always mounted, so items are
25
+ * focusable the moment `show()` returns — a `requestAnimationFrame` here
26
+ * lands after paint and races anything else moving focus. Hover-opens are the
27
+ * exception: the pointer is driving, so focus stays on the trigger. The hook
28
+ * always passes `skipAutoFocus` and owns focus itself, because the popover's
29
+ * auto-focus targets the first *tabbable* node while a menu wants its first
30
+ * item — under roving tabindex those are different elements.
31
+ *
32
+ * 3. Native light dismiss. A `popover="auto"` is dismissed by the browser on
33
+ * pointer interaction outside the panel, and a trigger outside the panel
34
+ * counts — before React's click handler runs. `popoverId` makes the trigger
35
+ * the panel's invoker, which exempts it. jsdom implements neither light
36
+ * dismiss nor invokers, so that wiring is only verifiable in a browser.
23
37
  */
24
38
  import { useCallback, useEffect, useRef } from 'react';
39
+ import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect.js";
25
40
  import { useListFocus } from "./useListFocus.js";
26
41
  import { useMediaQuery } from "./useMediaQuery.js";
42
+ const DEFAULT_CLICK_GUARD_MS = 500;
43
+
44
+ /**
45
+ * How long after a close a mouseenter on the trigger is ignored.
46
+ *
47
+ * A panel positioned over its own trigger puts that trigger back under a
48
+ * stationary pointer when it closes, and the browser fires a fresh mouseenter —
49
+ * which reopened the menu the user had just dismissed. Time-bounded rather than
50
+ * a one-shot flag, so a deliberate re-hover seconds later still opens; a real
51
+ * mouseleave clears it early.
52
+ */
53
+ const REOPEN_SUPPRESS_MS = 300;
27
54
  export function useMenuHover(options) {
28
55
  const {
29
56
  show,
@@ -31,23 +58,35 @@ export function useMenuHover(options) {
31
58
  isOpen,
32
59
  isEnabled,
33
60
  showDelay = 150,
34
- hideDelay = 200
61
+ hideDelay = 200,
62
+ clickGuardMs = DEFAULT_CLICK_GUARD_MS,
63
+ itemSelector,
64
+ popoverId,
65
+ ownsFocus = true
35
66
  } = options;
36
67
  const hasHover = useMediaQuery('(hover: hover)');
37
68
  const showTimerRef = useRef(null);
38
69
  const hideTimerRef = useRef(null);
39
70
  const triggerElRef = useRef(null);
40
- // Whether the menu was opened/interacted via hover (enables mouseleave-to-close)
71
+ /** Menu was hover-opened, so mouseleave may close it. */
41
72
  const hoverModeRef = useRef(false);
42
- // One-shot: skip the next mouseenter after click-to-close
43
- const skipNextEnterRef = useRef(false);
73
+ const closedAtRef = useRef(0);
74
+ /** When the current open began as a hover-open; 0 once confirmed or closed. */
75
+ const hoverOpenedAtRef = useRef(0);
44
76
  const prevIsOpenRef = useRef(isOpen);
45
77
 
46
- // When popover closes (any reason), reset hover mode
47
- if (prevIsOpenRef.current && !isOpen) {
48
- hoverModeRef.current = false;
49
- }
50
- prevIsOpenRef.current = isOpen;
78
+ // Catches every close, whatever caused it. A layout effect, not render: it
79
+ // must land before the browser hit-tests the vanished panel and fires the
80
+ // mouseenter REOPEN_SUPPRESS_MS exists to swallow.
81
+ useIsomorphicLayoutEffect(() => {
82
+ const wasOpen = prevIsOpenRef.current;
83
+ prevIsOpenRef.current = isOpen;
84
+ if (wasOpen && !isOpen) {
85
+ hoverModeRef.current = false;
86
+ hoverOpenedAtRef.current = 0;
87
+ closedAtRef.current = Date.now();
88
+ }
89
+ }, [isOpen]);
51
90
  const clearTimeouts = useCallback(() => {
52
91
  if (showTimerRef.current) {
53
92
  clearTimeout(showTimerRef.current);
@@ -58,59 +97,133 @@ export function useMenuHover(options) {
58
97
  hideTimerRef.current = null;
59
98
  }
60
99
  }, []);
100
+
101
+ // useListFocus needs an onEscape that is defined in terms of its own listRef;
102
+ // the indirection avoids the use-before-declare.
103
+ const escapeHandlerRef = useRef(() => {});
61
104
  const {
62
105
  listRef: menuRef,
63
106
  handleKeyDown: handleListKeyDown,
64
107
  focusFirst
65
108
  } = useListFocus({
66
- onEscape: () => {
67
- clearTimeouts();
68
- hide();
69
- }
109
+ itemSelector,
110
+ onEscape: () => escapeHandlerRef.current()
70
111
  });
112
+ const hideAndRestoreFocus = useCallback(() => {
113
+ // Read before hiding: closing the layer moves focus itself.
114
+ const menuHadFocus = menuRef.current?.contains(document.activeElement) ?? false;
115
+ hide();
116
+ if (menuHadFocus) {
117
+ triggerElRef.current?.focus();
118
+ }
119
+ }, [hide, menuRef]);
120
+ useEffect(() => {
121
+ escapeHandlerRef.current = () => {
122
+ clearTimeouts();
123
+ hideAndRestoreFocus();
124
+ };
125
+ }, [clearTimeouts, hideAndRestoreFocus]);
71
126
  useEffect(() => {
72
127
  return () => clearTimeouts();
73
128
  }, [clearTimeouts]);
74
-
75
- // Click: always toggle
76
- const handleClick = useCallback(() => {
77
- clearTimeouts();
78
- if (isOpen) {
79
- skipNextEnterRef.current = true;
80
- hide();
81
- } else {
82
- skipNextEnterRef.current = false;
129
+ const focusMenu = useCallback(() => {
130
+ // An empty or still-loading menu has no focusable item; focus the container
131
+ // so keyboard ownership still transfers off the trigger's list.
132
+ if (!focusFirst()) {
133
+ menuRef.current?.focus();
134
+ }
135
+ }, [focusFirst, menuRef]);
136
+ const openAndFocus = useCallback(() => {
137
+ if (!ownsFocus) {
83
138
  show();
84
- requestAnimationFrame(() => focusFirst());
139
+ return;
85
140
  }
86
- }, [isOpen, clearTimeouts, show, hide, focusFirst]);
141
+ show({
142
+ skipAutoFocus: true
143
+ });
144
+ focusMenu();
145
+ }, [ownsFocus, show, focusMenu]);
146
+ const confirmHoverOpen = useCallback(() => {
147
+ const isConfirming = clickGuardMs > 0 && hoverOpenedAtRef.current > 0 && Date.now() - hoverOpenedAtRef.current < clickGuardMs;
148
+ if (!isConfirming) {
149
+ return false;
150
+ }
151
+ hoverModeRef.current = false;
152
+ hoverOpenedAtRef.current = 0;
153
+ return true;
154
+ }, [clickGuardMs]);
155
+ const handleClick = useCallback(event => {
156
+ // Cancel the invoker's default toggle so this handler stays the single
157
+ // source of truth; popoverTarget still exempts the trigger from dismissal.
158
+ if (popoverId) {
159
+ event?.preventDefault();
160
+ }
161
+ clearTimeouts();
87
162
 
88
- // Hover: mouseenter activates hover mode and opens
163
+ // Enter/Space arrive as a click with detail 0, and always open. A keyboard
164
+ // user reaches the trigger of an open menu only because a hover-open left
165
+ // focus behind; closing there would stranded them outside a visible menu.
166
+ const isKeyboardActivation = event != null && event.detail === 0;
167
+ if (isKeyboardActivation) {
168
+ closedAtRef.current = 0;
169
+ hoverModeRef.current = false;
170
+ hoverOpenedAtRef.current = 0;
171
+ if (isOpen) {
172
+ if (ownsFocus) {
173
+ focusMenu();
174
+ }
175
+ } else {
176
+ openAndFocus();
177
+ }
178
+ return;
179
+ }
180
+ if (!isOpen) {
181
+ closedAtRef.current = 0;
182
+ hoverModeRef.current = false;
183
+ hoverOpenedAtRef.current = 0;
184
+ openAndFocus();
185
+ return;
186
+ }
187
+ if (confirmHoverOpen()) {
188
+ if (ownsFocus) {
189
+ focusMenu();
190
+ }
191
+ return;
192
+ }
193
+ hideAndRestoreFocus();
194
+ }, [popoverId, clearTimeouts, isOpen, ownsFocus, confirmHoverOpen, openAndFocus, focusMenu, hideAndRestoreFocus]);
89
195
  const handleMouseEnter = useCallback(() => {
90
196
  if (!hasHover) {
91
197
  return;
92
198
  }
93
- if (skipNextEnterRef.current) {
94
- skipNextEnterRef.current = false;
199
+ if (closedAtRef.current > 0 && Date.now() - closedAtRef.current < REOPEN_SUPPRESS_MS) {
200
+ return;
201
+ }
202
+ // Re-entering an open menu's trigger must not un-pin it or re-arm the
203
+ // guard, which would make the next deliberate click another "confirm" and
204
+ // leave the menu undismissable.
205
+ if (isOpen) {
206
+ clearTimeouts();
95
207
  return;
96
208
  }
97
209
  hoverModeRef.current = true;
98
210
  clearTimeouts();
99
- if (showDelay > 0) {
100
- showTimerRef.current = setTimeout(() => {
101
- show({
102
- skipAutoFocus: true
103
- });
104
- }, showDelay);
105
- } else {
211
+ const openByHover = () => {
212
+ hoverOpenedAtRef.current = Date.now();
106
213
  show({
107
214
  skipAutoFocus: true
108
215
  });
216
+ };
217
+ if (showDelay > 0) {
218
+ showTimerRef.current = setTimeout(openByHover, showDelay);
219
+ } else {
220
+ openByHover();
109
221
  }
110
- }, [hasHover, clearTimeouts, show, showDelay]);
111
-
112
- // Hover: mouseleave only closes if in hover mode
222
+ }, [hasHover, isOpen, clearTimeouts, show, showDelay]);
113
223
  const handleMouseLeave = useCallback(() => {
224
+ // A real leave ends the stationary-pointer case the suppression window
225
+ // exists for, so the next enter is deliberate whenever it arrives.
226
+ closedAtRef.current = 0;
114
227
  if (!hoverModeRef.current) {
115
228
  return;
116
229
  }
@@ -119,8 +232,6 @@ export function useMenuHover(options) {
119
232
  hide();
120
233
  }, hideDelay);
121
234
  }, [clearTimeouts, hide, hideDelay]);
122
-
123
- // Content: mouseenter cancels pending hide
124
235
  const handleContentMouseEnter = useCallback(() => {
125
236
  clearTimeouts();
126
237
  }, [clearTimeouts]);
@@ -144,6 +255,9 @@ export function useMenuHover(options) {
144
255
  },
145
256
  menuRef,
146
257
  focusFirst,
258
+ focusMenu,
259
+ confirmHoverOpen,
260
+ close: hideAndRestoreFocus,
147
261
  setTriggerEl: noopRef
148
262
  };
149
263
  }
@@ -151,7 +265,10 @@ export function useMenuHover(options) {
151
265
  triggerProps: {
152
266
  onClick: handleClick,
153
267
  onMouseEnter: handleMouseEnter,
154
- onMouseLeave: handleMouseLeave
268
+ onMouseLeave: handleMouseLeave,
269
+ ...(popoverId ? {
270
+ popoverTarget: popoverId
271
+ } : null)
155
272
  },
156
273
  contentProps: {
157
274
  onMouseEnter: handleContentMouseEnter,
@@ -160,6 +277,9 @@ export function useMenuHover(options) {
160
277
  },
161
278
  menuRef,
162
279
  focusFirst,
280
+ focusMenu,
281
+ confirmHoverOpen,
282
+ close: hideAndRestoreFocus,
163
283
  setTriggerEl: setTriggerRef
164
284
  };
165
285
  }
@@ -26,19 +26,25 @@
26
26
  * <App />
27
27
  * </Theme>
28
28
  * ```
29
+ *
30
+ * SYNC: `DefineThemeInput` is the theme surface. Adding, removing, or renaming
31
+ * a field means updating:
32
+ * - /packages/cli/assets/theme.template.ts (documents every field; the
33
+ * drift guard is scripts/check-theme-template.test.mjs)
34
+ * - /packages/cli/assets/docs/theme.doc.mjs (`astryx docs theme`)
29
35
  */
30
36
  import type { IconRegistry } from '../Icon/globalIconRegistry';
31
37
  import type { IndicatorRegistry } from '../Indicator/types';
32
38
  import type { TypographyConfig } from './types';
33
39
  import { type OnMediaOverrides, type ResolvedOnMedia } from './onMediaTokens';
34
- import { colorDefaults, spacingDefaults, sizeDefaults, focusDefaults, radiusDefaults, shadowDefaults, durationDefaults, easeDefaults, typographyDefaults, textSizeDefaults, fontWeightDefaults, typeScaleDefaults } from './tokens.stylex';
40
+ import { colorDefaults, spacingDefaults, sizeDefaults, borderDefaults, focusDefaults, radiusDefaults, shadowDefaults, durationDefaults, easeDefaults, typographyDefaults, textSizeDefaults, fontWeightDefaults, typeScaleDefaults } from './tokens.stylex';
35
41
  import { type MotionScaleConfig } from './expandMotionScale';
36
42
  import { type RadiusScaleConfig } from './expandRadiusScale';
37
43
  import { type ColorScaleConfig } from './expandColorScale';
38
44
  import type { DomainTokenName } from './domainTokens';
39
45
  import type { SyntaxThemeDefinition } from './syntax';
40
46
  /** All valid Astryx core token names */
41
- export type CoreTokenName = keyof typeof colorDefaults | keyof typeof spacingDefaults | keyof typeof sizeDefaults | keyof typeof focusDefaults | keyof typeof radiusDefaults | keyof typeof shadowDefaults | keyof typeof durationDefaults | keyof typeof easeDefaults | keyof typeof typographyDefaults | keyof typeof textSizeDefaults | keyof typeof fontWeightDefaults | keyof typeof typeScaleDefaults;
47
+ export type CoreTokenName = keyof typeof colorDefaults | keyof typeof spacingDefaults | keyof typeof sizeDefaults | keyof typeof borderDefaults | keyof typeof focusDefaults | keyof typeof radiusDefaults | keyof typeof shadowDefaults | keyof typeof durationDefaults | keyof typeof easeDefaults | keyof typeof typographyDefaults | keyof typeof textSizeDefaults | keyof typeof fontWeightDefaults | keyof typeof typeScaleDefaults;
42
48
  /** All valid Astryx token names — core + domain tokens */
43
49
  export type TokenName = CoreTokenName | DomainTokenName;
44
50
  /**
@@ -106,9 +112,14 @@ export interface DefineThemeInput {
106
112
  /** Theme name — used for data-astryx-theme attribute and identification */
107
113
  name: string;
108
114
  /**
109
- * Base theme to extend. When provided, the new theme starts with the
110
- * base theme's tokens, components, and fonts, then applies overrides
111
- * from this input on top. The base theme's values have lowest precedence.
115
+ * Base theme to extend. When provided, the new theme starts with everything
116
+ * the base resolved to — tokens, component overrides, icons, indicators, and
117
+ * its `onDark`/`onLight` surfaces — then applies this input on top. The base
118
+ * theme's values have lowest precedence.
119
+ *
120
+ * The result is flat: an extended theme carries its inheritance in its own
121
+ * resolved output, so `astryx theme build` emits one self-contained
122
+ * stylesheet and the base's CSS does not need to be loaded alongside it.
112
123
  *
113
124
  * Use this to create variant themes that customize only a few aspects
114
125
  * (e.g. icons, accent color) without re-specifying the full theme.
@@ -1 +1 @@
1
- {"version":3,"file":"defineTheme.d.ts","sourceRoot":"","sources":["../../src/theme/defineTheme.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,4BAA4B,CAAC;AAC7D,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,oBAAoB,CAAC;AAC1D,OAAO,KAAK,EAAC,gBAAgB,EAAa,MAAM,SAAS,CAAC;AAC1D,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACrB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,aAAa,EACb,eAAe,EACf,YAAY,EACZ,aAAa,EACb,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AAMzB,OAAO,EAAoB,KAAK,iBAAiB,EAAC,MAAM,qBAAqB,CAAC;AAC9E,OAAO,EAAoB,KAAK,iBAAiB,EAAC,MAAM,qBAAqB,CAAC;AAC9E,OAAO,EAAmB,KAAK,gBAAgB,EAAC,MAAM,oBAAoB,CAAC;AAE3E,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,gBAAgB,CAAC;AAEpD,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,UAAU,CAAC;AAOpD,wCAAwC;AACxC,MAAM,MAAM,aAAa,GACrB,MAAM,OAAO,aAAa,GAC1B,MAAM,OAAO,eAAe,GAC5B,MAAM,OAAO,YAAY,GACzB,MAAM,OAAO,aAAa,GAC1B,MAAM,OAAO,cAAc,GAC3B,MAAM,OAAO,cAAc,GAC3B,MAAM,OAAO,gBAAgB,GAC7B,MAAM,OAAO,YAAY,GACzB,MAAM,OAAO,kBAAkB,GAC/B,MAAM,OAAO,gBAAgB,GAC7B,MAAM,OAAO,kBAAkB,GAC/B,MAAM,OAAO,iBAAiB,CAAC;AAEnC,0DAA0D;AAC1D,MAAM,MAAM,SAAS,GAAG,aAAa,GAAG,eAAe,CAAC;AAExD;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;AAEhE;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAE7E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;AAE/E,2BAA2B;AAC3B,MAAM,WAAW,gBAAgB;IAC/B,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IAEb;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB;;;;;;;;;;;;;;;;;;OAkBG;IACH,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B;;;;;;;;;;;;;;;;;;OAkBG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB;;4EAEwE;IACxE,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC;IAChD;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,8DAA8D;IAC9D,KAAK,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IAC9B;;;;;;;;;;;OAWG;IACH,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,qBAAqB,CAAC;IAC/B;;;;;;;;;;;;;;;;;;OAkBG;IACH,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B;;;OAGG;IACH,OAAO,CAAC,EAAE,gBAAgB,CAAC;CAC5B;AAED,iDAAiD;AACjD,MAAM,WAAW,YAAY;IAC3B,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,gCAAgC;IAChC,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,oBAAoB;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IAC9B,sEAAsE;IACtE,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,kEAAkE;IAClE,OAAO,CAAC,EAAE,IAAI,CAAC;IACf;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;IACpD;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B;;;OAGG;IACH,SAAS,CAAC,EAAE,eAAe,CAAC;CAC7B;AAMD,qFAAqF;AACrF,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAchD,CAAC;AA6FF;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,YAAY,CAyKjE;AAMD,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,kBAAkB,EAClB,gBAAgB,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,GACpB,MAAM,sBAAsB,CAAC;AAM9B,2DAA2D;AAC3D,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,CAQpE"}
1
+ {"version":3,"file":"defineTheme.d.ts","sourceRoot":"","sources":["../../src/theme/defineTheme.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,4BAA4B,CAAC;AAC7D,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,oBAAoB,CAAC;AAC1D,OAAO,KAAK,EAAC,gBAAgB,EAAa,MAAM,SAAS,CAAC;AAC1D,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACrB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,aAAa,EACb,eAAe,EACf,YAAY,EACZ,cAAc,EACd,aAAa,EACb,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AAMzB,OAAO,EAAoB,KAAK,iBAAiB,EAAC,MAAM,qBAAqB,CAAC;AAC9E,OAAO,EAAoB,KAAK,iBAAiB,EAAC,MAAM,qBAAqB,CAAC;AAC9E,OAAO,EAAmB,KAAK,gBAAgB,EAAC,MAAM,oBAAoB,CAAC;AAE3E,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,gBAAgB,CAAC;AAEpD,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,UAAU,CAAC;AAQpD,wCAAwC;AACxC,MAAM,MAAM,aAAa,GACrB,MAAM,OAAO,aAAa,GAC1B,MAAM,OAAO,eAAe,GAC5B,MAAM,OAAO,YAAY,GACzB,MAAM,OAAO,cAAc,GAC3B,MAAM,OAAO,aAAa,GAC1B,MAAM,OAAO,cAAc,GAC3B,MAAM,OAAO,cAAc,GAC3B,MAAM,OAAO,gBAAgB,GAC7B,MAAM,OAAO,YAAY,GACzB,MAAM,OAAO,kBAAkB,GAC/B,MAAM,OAAO,gBAAgB,GAC7B,MAAM,OAAO,kBAAkB,GAC/B,MAAM,OAAO,iBAAiB,CAAC;AAEnC,0DAA0D;AAC1D,MAAM,MAAM,SAAS,GAAG,aAAa,GAAG,eAAe,CAAC;AAExD;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;AAEhE;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAE7E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;AAE/E,2BAA2B;AAC3B,MAAM,WAAW,gBAAgB;IAC/B,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IAEb;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB;;;;;;;;;;;;;;;;;;OAkBG;IACH,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B;;;;;;;;;;;;;;;;;;OAkBG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB;;4EAEwE;IACxE,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC;IAChD;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,8DAA8D;IAC9D,KAAK,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IAC9B;;;;;;;;;;;OAWG;IACH,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,qBAAqB,CAAC;IAC/B;;;;;;;;;;;;;;;;;;OAkBG;IACH,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B;;;OAGG;IACH,OAAO,CAAC,EAAE,gBAAgB,CAAC;CAC5B;AAED,iDAAiD;AACjD,MAAM,WAAW,YAAY;IAC3B,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,gCAAgC;IAChC,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,oBAAoB;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IAC9B,sEAAsE;IACtE,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,kEAAkE;IAClE,OAAO,CAAC,EAAE,IAAI,CAAC;IACf;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;IACpD;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B;;;OAGG;IACH,SAAS,CAAC,EAAE,eAAe,CAAC;CAC7B;AAMD,qFAAqF;AACrF,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAehD,CAAC;AAoEF;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,YAAY,CAyLjE;AAMD,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,kBAAkB,EAClB,gBAAgB,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,GACpB,MAAM,sBAAsB,CAAC;AAM9B,2DAA2D;AAC3D,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,CAQpE"}
@@ -28,16 +28,23 @@
28
28
  * <App />
29
29
  * </Theme>
30
30
  * ```
31
+ *
32
+ * SYNC: `DefineThemeInput` is the theme surface. Adding, removing, or renaming
33
+ * a field means updating:
34
+ * - /packages/cli/assets/theme.template.ts (documents every field; the
35
+ * drift guard is scripts/check-theme-template.test.mjs)
36
+ * - /packages/cli/assets/docs/theme.doc.mjs (`astryx docs theme`)
31
37
  */
32
38
 
33
39
  import { resolveOnMedia } from "./onMediaTokens.js";
34
- import { colorDefaults, spacingDefaults, sizeDefaults, focusDefaults, radiusDefaults, shadowDefaults, durationDefaults, easeDefaults, typographyDefaults, textSizeDefaults, fontWeightDefaults, typeScaleDefaults } from "./tokens.stylex.js";
40
+ import { colorDefaults, spacingDefaults, sizeDefaults, borderDefaults, focusDefaults, radiusDefaults, shadowDefaults, durationDefaults, easeDefaults, typographyDefaults, textSizeDefaults, fontWeightDefaults, typeScaleDefaults } from "./tokens.stylex.js";
35
41
  import { expandTypeScale, generateTypeScaleComponents } from "./expandTypeScale.js";
36
42
  import { expandMotionScale } from "./expandMotionScale.js";
37
43
  import { expandRadiusScale } from "./expandRadiusScale.js";
38
44
  import { expandColorScale } from "./expandColorScale.js";
39
45
  import { domainTokenDefaults } from "./domainTokens/index.js";
40
46
  import { registerTheme } from "./themeRegistry.js";
47
+ import { deepMergeComponents } from "./mergeComponents.js";
41
48
 
42
49
  // =============================================================================
43
50
  // Types
@@ -120,6 +127,7 @@ export const tokenDefaults = {
120
127
  ...colorDefaults,
121
128
  ...spacingDefaults,
122
129
  ...sizeDefaults,
130
+ ...borderDefaults,
123
131
  ...focusDefaults,
124
132
  ...radiusDefaults,
125
133
  ...shadowDefaults,
@@ -148,48 +156,6 @@ function resolveTokenValue(value) {
148
156
  return value;
149
157
  }
150
158
 
151
- /**
152
- * Deep-merge two component style maps.
153
- * Properties in `overrides` take precedence over `base`.
154
- * This allows typeScale-generated rules to be overridden by explicit components.
155
- */
156
- function deepMergeComponents(base, overrides) {
157
- if (!base && !overrides) {
158
- return undefined;
159
- }
160
- if (!base) {
161
- return overrides;
162
- }
163
- if (!overrides) {
164
- return base;
165
- }
166
- const result = {};
167
-
168
- // Start with all base entries
169
- for (const [component, rules] of Object.entries(base)) {
170
- result[component] = {
171
- ...rules
172
- };
173
- }
174
-
175
- // Merge overrides on top
176
- for (const [component, rules] of Object.entries(overrides)) {
177
- if (!result[component]) {
178
- result[component] = {
179
- ...rules
180
- };
181
- } else {
182
- for (const [key, styles] of Object.entries(rules)) {
183
- result[component][key] = {
184
- ...result[component][key],
185
- ...styles
186
- };
187
- }
188
- }
189
- }
190
- return result;
191
- }
192
-
193
159
  /**
194
160
  * Resolve a FontWeight name to a var() reference.
195
161
  * Named weights map to var(--font-weight-*); raw values pass through.
@@ -219,6 +185,24 @@ function buildFontFamily(family, fallbacks) {
219
185
  return quoted;
220
186
  }
221
187
 
188
+ /**
189
+ * Describe a rejected `extends` value for the error message — enough to tell a
190
+ * missed import (`undefined`) from a module namespace or a plain object.
191
+ */
192
+ function describeBadBase(value) {
193
+ if (value === undefined) {
194
+ return 'undefined';
195
+ }
196
+ if (value === null) {
197
+ return 'null';
198
+ }
199
+ if (typeof value !== 'object') {
200
+ return typeof value;
201
+ }
202
+ const keys = Object.keys(value);
203
+ return `an object with keys [${keys.slice(0, 4).join(', ')}${keys.length > 4 ? ', …' : ''}]`;
204
+ }
205
+
222
206
  /**
223
207
  * Create an Astryx theme.
224
208
  *
@@ -232,7 +216,15 @@ function buildFontFamily(family, fallbacks) {
232
216
  export function defineTheme(input) {
233
217
  const tokens = {};
234
218
 
235
- // 0. Pre-seed from base theme when `extends` is provided (lowest precedence)
219
+ // 0. Pre-seed from base theme when `extends` is provided (lowest precedence).
220
+ // A base that is not a theme is refused rather than ignored: `extends` used
221
+ // to inherit nothing when its value was undefined, which is what a named
222
+ // import silently resolving to the wrong module hands over, and the theme
223
+ // then built into a plausible-looking stylesheet missing everything it was
224
+ // supposed to inherit.
225
+ if ('extends' in input && !isDefinedTheme(input.extends)) {
226
+ throw new Error(`defineTheme("${input.name}"): \`extends\` must be a theme from defineTheme(), got ${describeBadBase(input.extends)}. ` + `Check that the import naming your base theme resolves to its source and exports that name — ` + `a generated \`<theme>.js\` artifact sitting next to the source exports \`<name>Theme\`, not the source's own export.`);
227
+ }
236
228
  const base = input.extends;
237
229
  if (base) {
238
230
  for (const [key, value] of Object.entries(base.tokens)) {
@@ -363,9 +355,10 @@ export function defineTheme(input) {
363
355
  components = deepMergeComponents(base.components, components);
364
356
  }
365
357
 
366
- // 4. Resolve on-media token overrides (defaults + user overrides)
367
- const __onDark = resolveOnMedia('dark', input.onDark);
368
- const __onLight = resolveOnMedia('light', input.onLight);
358
+ // 4. Resolve on-media token overrides (base's resolved surface, then
359
+ // defaults, then this theme's own overrides)
360
+ const __onDark = resolveOnMedia('dark', input.onDark, base?.__onDark);
361
+ const __onLight = resolveOnMedia('light', input.onLight, base?.__onLight);
369
362
 
370
363
  // 5. Merge icons — input icons override base icons
371
364
  const icons = input.icons && base?.icons ? {
@@ -385,7 +378,10 @@ export function defineTheme(input) {
385
378
  components,
386
379
  icons,
387
380
  indicators,
388
- __inputTokens: input.tokens,
381
+ __inputTokens: base?.__inputTokens || input.tokens ? {
382
+ ...base?.__inputTokens,
383
+ ...input.tokens
384
+ } : undefined,
389
385
  __onDark,
390
386
  __onLight
391
387
  };
@@ -1 +1 @@
1
- {"version":3,"file":"derivedVarRegistry.d.ts","sourceRoot":"","sources":["../../src/theme/derivedVarRegistry.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,iFAAiF;IACjF,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,EAAE,CAsChE,CAAC;AAEF;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,eAAe,EAAE,CAMnB"}
1
+ {"version":3,"file":"derivedVarRegistry.d.ts","sourceRoot":"","sources":["../../src/theme/derivedVarRegistry.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,iFAAiF;IACjF,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,EAAE,CA0ChE,CAAC;AAEF;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,eAAe,EAAE,CAMnB"}
@@ -55,6 +55,13 @@ export const derivedVarRegistry = {
55
55
  property: 'padding',
56
56
  expand: 'container'
57
57
  }],
58
+ 'context-menu': [{
59
+ property: 'borderRadius',
60
+ vars: ['--_dropdown-menu-radius']
61
+ }, {
62
+ property: 'padding',
63
+ vars: ['--_dropdown-menu-padding']
64
+ }],
58
65
  'dropdown-menu': [{
59
66
  property: 'borderRadius',
60
67
  vars: ['--_dropdown-menu-radius']
@@ -1 +1 @@
1
- {"version":3,"file":"expandColorScale.d.ts","sourceRoot":"","sources":["../../src/theme/expandColorScale.ts"],"names":[],"mappings":"AAuCA;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAE3C;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC;CAChC;AAED,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAyCtD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,EACZ,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,MAAM,CAYR;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,CAoI3E"}
1
+ {"version":3,"file":"expandColorScale.d.ts","sourceRoot":"","sources":["../../src/theme/expandColorScale.ts"],"names":[],"mappings":"AAwCA;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAE3C;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC;CAChC;AAED,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAyCtD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,EACZ,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,MAAM,CAYR;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,CAoI3E"}
@@ -28,6 +28,7 @@
28
28
  *
29
29
  * SYNC: When modified, update:
30
30
  * - /packages/core/src/theme/defineTheme.ts
31
+ * - /packages/cli/assets/theme.template.ts (the annotated field reference)
31
32
  */
32
33
 
33
34
  import { contrastRatio } from "./contrast.js";
@@ -16,6 +16,7 @@
16
16
  * SYNC: When modified, update:
17
17
  * - /packages/core/src/theme/expandMotionScale.test.ts
18
18
  * - /packages/core/src/theme/defineTheme.ts
19
+ * - /packages/cli/assets/theme.template.ts (the annotated field reference)
19
20
  */
20
21
  /**
21
22
  * Motion scale configuration.
@@ -1 +1 @@
1
- {"version":3,"file":"expandMotionScale.d.ts","sourceRoot":"","sources":["../../src/theme/expandMotionScale.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AAMH;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,iBAAiB;IAChC,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,MAAM,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qDAAqD;AACrD,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAavD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,iBAAiB,GACxB,iBAAiB,CA0BnB"}
1
+ {"version":3,"file":"expandMotionScale.d.ts","sourceRoot":"","sources":["../../src/theme/expandMotionScale.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;GAmBG;AAMH;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,iBAAiB;IAChC,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,MAAM,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qDAAqD;AACrD,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAavD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,iBAAiB,GACxB,iBAAiB,CA0BnB"}
@@ -18,6 +18,7 @@
18
18
  * SYNC: When modified, update:
19
19
  * - /packages/core/src/theme/expandMotionScale.test.ts
20
20
  * - /packages/core/src/theme/defineTheme.ts
21
+ * - /packages/cli/assets/theme.template.ts (the annotated field reference)
21
22
  */
22
23
 
23
24
  // =============================================================================
@@ -20,6 +20,7 @@
20
20
  * SYNC: When modified, update:
21
21
  * - /packages/core/src/theme/expandRadiusScale.test.ts
22
22
  * - /packages/core/src/theme/defineTheme.ts
23
+ * - /packages/cli/assets/theme.template.ts (the annotated field reference)
23
24
  */
24
25
  /**
25
26
  * Radius scale configuration.
@@ -1 +1 @@
1
- {"version":3,"file":"expandRadiusScale.d.ts","sourceRoot":"","sources":["../../src/theme/expandRadiusScale.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAMH;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,iBAAiB;IAChC,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,yFAAyF;IACzF,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAMvD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,iBAAiB,GACxB,iBAAiB,CAanB"}
1
+ {"version":3,"file":"expandRadiusScale.d.ts","sourceRoot":"","sources":["../../src/theme/expandRadiusScale.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAMH;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,iBAAiB;IAChC,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,yFAAyF;IACzF,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAMvD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,iBAAiB,GACxB,iBAAiB,CAanB"}
@@ -22,6 +22,7 @@
22
22
  * SYNC: When modified, update:
23
23
  * - /packages/core/src/theme/expandRadiusScale.test.ts
24
24
  * - /packages/core/src/theme/defineTheme.ts
25
+ * - /packages/cli/assets/theme.template.ts (the annotated field reference)
25
26
  */
26
27
 
27
28
  // =============================================================================
@@ -42,6 +42,7 @@
42
42
  * SYNC: When modified, update:
43
43
  * - /packages/core/src/theme/expandTypeScale.test.ts
44
44
  * - /packages/core/src/theme/defineTheme.ts
45
+ * - /packages/cli/assets/theme.template.ts (the annotated field reference)
45
46
  */
46
47
  /** Font weight value — either a CSS string or a var() reference. */
47
48
  export type FontWeightValue = string;
@@ -1 +1 @@
1
- {"version":3,"file":"expandTypeScale.d.ts","sourceRoot":"","sources":["../../src/theme/expandTypeScale.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAMH,oEAAoE;AACpE,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC;AAErC;;;;GAIG;AACH,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAC1C,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,eAAe,CAAC,CAC/C,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,OAAO,CACvC,MAAM,CACF,MAAM,GACN,OAAO,GACP,OAAO,GACP,MAAM,GACN,YAAY,GACZ,WAAW,GACX,WAAW,GACX,WAAW,GACX,CAAC,MAAM,GAAG,EAAE,CAAC,EACf,eAAe,CAChB,CACF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,WAAW,eAAe;IAC9B,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,OAAO,CAAC,EAAE;QACR,yEAAyE;QACzE,OAAO,CAAC,EAAE,sBAAsB,CAAC;QACjC,oEAAoE;QACpE,IAAI,CAAC,EAAE,mBAAmB,CAAC;KAC5B,CAAC;CACH;AAED;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAwIrD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,eAAe,CA+CxE;AAiBD;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,eAAe,GACvB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAmDxD"}
1
+ {"version":3,"file":"expandTypeScale.d.ts","sourceRoot":"","sources":["../../src/theme/expandTypeScale.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAMH,oEAAoE;AACpE,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC;AAErC;;;;GAIG;AACH,MAAM,MAAM,sBAAsB,GAAG,OAAO,CAC1C,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,eAAe,CAAC,CAC/C,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,OAAO,CACvC,MAAM,CACF,MAAM,GACN,OAAO,GACP,OAAO,GACP,MAAM,GACN,YAAY,GACZ,WAAW,GACX,WAAW,GACX,WAAW,GACX,CAAC,MAAM,GAAG,EAAE,CAAC,EACf,eAAe,CAChB,CACF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,WAAW,eAAe;IAC9B,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,OAAO,CAAC,EAAE;QACR,yEAAyE;QACzE,OAAO,CAAC,EAAE,sBAAsB,CAAC;QACjC,oEAAoE;QACpE,IAAI,CAAC,EAAE,mBAAmB,CAAC;KAC5B,CAAC;CACH;AAED;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAwIrD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,eAAe,CA+CxE;AAiBD;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,eAAe,GACvB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAmDxD"}
@@ -44,6 +44,7 @@
44
44
  * SYNC: When modified, update:
45
45
  * - /packages/core/src/theme/expandTypeScale.test.ts
46
46
  * - /packages/core/src/theme/defineTheme.ts
47
+ * - /packages/cli/assets/theme.template.ts (the annotated field reference)
47
48
  */
48
49
 
49
50
  // =============================================================================