@c9up/nebula 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (327) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +256 -0
  3. package/dist/adapters/css.d.ts +33 -0
  4. package/dist/adapters/css.js +67 -0
  5. package/dist/adapters/index.d.ts +16 -0
  6. package/dist/adapters/index.js +27 -0
  7. package/dist/adapters/tailwind.d.ts +25 -0
  8. package/dist/adapters/tailwind.js +122 -0
  9. package/dist/adapters/types.d.ts +49 -0
  10. package/dist/adapters/types.js +21 -0
  11. package/dist/adapters/unocss.d.ts +17 -0
  12. package/dist/adapters/unocss.js +107 -0
  13. package/dist/atoms/AspectRatio.d.ts +17 -0
  14. package/dist/atoms/AspectRatio.js +19 -0
  15. package/dist/atoms/Avatar.d.ts +23 -0
  16. package/dist/atoms/Avatar.js +46 -0
  17. package/dist/atoms/Badge.d.ts +25 -0
  18. package/dist/atoms/Badge.js +28 -0
  19. package/dist/atoms/Button.d.ts +58 -0
  20. package/dist/atoms/Button.js +67 -0
  21. package/dist/atoms/Checkbox.d.ts +41 -0
  22. package/dist/atoms/Checkbox.js +47 -0
  23. package/dist/atoms/Input.d.ts +36 -0
  24. package/dist/atoms/Input.js +50 -0
  25. package/dist/atoms/Kbd.d.ts +14 -0
  26. package/dist/atoms/Kbd.js +17 -0
  27. package/dist/atoms/Label.d.ts +24 -0
  28. package/dist/atoms/Label.js +26 -0
  29. package/dist/atoms/Marker.d.ts +42 -0
  30. package/dist/atoms/Marker.js +55 -0
  31. package/dist/atoms/NativeSelect.d.ts +43 -0
  32. package/dist/atoms/NativeSelect.js +81 -0
  33. package/dist/atoms/Progress.d.ts +21 -0
  34. package/dist/atoms/Progress.js +48 -0
  35. package/dist/atoms/ScrollArea.d.ts +28 -0
  36. package/dist/atoms/ScrollArea.js +37 -0
  37. package/dist/atoms/Separator.d.ts +16 -0
  38. package/dist/atoms/Separator.js +21 -0
  39. package/dist/atoms/Skeleton.d.ts +13 -0
  40. package/dist/atoms/Skeleton.js +18 -0
  41. package/dist/atoms/Slider.d.ts +30 -0
  42. package/dist/atoms/Slider.js +40 -0
  43. package/dist/atoms/Spinner.d.ts +17 -0
  44. package/dist/atoms/Spinner.js +20 -0
  45. package/dist/atoms/Switch.d.ts +24 -0
  46. package/dist/atoms/Switch.js +41 -0
  47. package/dist/atoms/Textarea.d.ts +28 -0
  48. package/dist/atoms/Textarea.js +36 -0
  49. package/dist/atoms/Toggle.d.ts +38 -0
  50. package/dist/atoms/Toggle.js +45 -0
  51. package/dist/atoms/index.d.ts +32 -0
  52. package/dist/atoms/index.js +32 -0
  53. package/dist/cli/add.d.ts +44 -0
  54. package/dist/cli/add.js +99 -0
  55. package/dist/cli/index.d.ts +10 -0
  56. package/dist/cli/index.js +10 -0
  57. package/dist/cli/registry.d.ts +31 -0
  58. package/dist/cli/registry.js +83 -0
  59. package/dist/config.d.ts +81 -0
  60. package/dist/config.js +43 -0
  61. package/dist/configure.d.ts +34 -0
  62. package/dist/configure.js +85 -0
  63. package/dist/index.d.ts +41 -0
  64. package/dist/index.js +41 -0
  65. package/dist/lib/children.d.ts +28 -0
  66. package/dist/lib/children.js +27 -0
  67. package/dist/lib/cn.d.ts +13 -0
  68. package/dist/lib/cn.js +13 -0
  69. package/dist/lib/cva.d.ts +83 -0
  70. package/dist/lib/cva.js +178 -0
  71. package/dist/lib/icons.d.ts +44 -0
  72. package/dist/lib/icons.js +79 -0
  73. package/dist/lib/id.d.ts +31 -0
  74. package/dist/lib/id.js +41 -0
  75. package/dist/lib/index.d.ts +12 -0
  76. package/dist/lib/index.js +12 -0
  77. package/dist/lib/motion.d.ts +31 -0
  78. package/dist/lib/motion.js +39 -0
  79. package/dist/lib/props.d.ts +48 -0
  80. package/dist/lib/props.js +59 -0
  81. package/dist/lib/styled.d.ts +31 -0
  82. package/dist/lib/styled.js +39 -0
  83. package/dist/molecules/Accordion.d.ts +35 -0
  84. package/dist/molecules/Accordion.js +117 -0
  85. package/dist/molecules/Alert.d.ts +30 -0
  86. package/dist/molecules/Alert.js +35 -0
  87. package/dist/molecules/Attachment.d.ts +39 -0
  88. package/dist/molecules/Attachment.js +103 -0
  89. package/dist/molecules/Breadcrumb.d.ts +29 -0
  90. package/dist/molecules/Breadcrumb.js +73 -0
  91. package/dist/molecules/Bubble.d.ts +56 -0
  92. package/dist/molecules/Bubble.js +92 -0
  93. package/dist/molecules/ButtonGroup.d.ts +23 -0
  94. package/dist/molecules/ButtonGroup.js +30 -0
  95. package/dist/molecules/Card.d.ts +17 -0
  96. package/dist/molecules/Card.js +17 -0
  97. package/dist/molecules/Collapsible.d.ts +40 -0
  98. package/dist/molecules/Collapsible.js +71 -0
  99. package/dist/molecules/Empty.d.ts +16 -0
  100. package/dist/molecules/Empty.js +16 -0
  101. package/dist/molecules/Field.d.ts +54 -0
  102. package/dist/molecules/Field.js +82 -0
  103. package/dist/molecules/InputGroup.d.ts +33 -0
  104. package/dist/molecules/InputGroup.js +35 -0
  105. package/dist/molecules/InputOTP.d.ts +33 -0
  106. package/dist/molecules/InputOTP.js +140 -0
  107. package/dist/molecules/Item.d.ts +41 -0
  108. package/dist/molecules/Item.js +48 -0
  109. package/dist/molecules/Message.d.ts +43 -0
  110. package/dist/molecules/Message.js +82 -0
  111. package/dist/molecules/Pagination.d.ts +41 -0
  112. package/dist/molecules/Pagination.js +144 -0
  113. package/dist/molecules/RadioGroup.d.ts +32 -0
  114. package/dist/molecules/RadioGroup.js +65 -0
  115. package/dist/molecules/Resizable.d.ts +37 -0
  116. package/dist/molecules/Resizable.js +136 -0
  117. package/dist/molecules/Table.d.ts +37 -0
  118. package/dist/molecules/Table.js +78 -0
  119. package/dist/molecules/Tabs.d.ts +37 -0
  120. package/dist/molecules/Tabs.js +109 -0
  121. package/dist/molecules/ToggleGroup.d.ts +37 -0
  122. package/dist/molecules/ToggleGroup.js +68 -0
  123. package/dist/molecules/Typography.d.ts +26 -0
  124. package/dist/molecules/Typography.js +67 -0
  125. package/dist/molecules/index.d.ts +37 -0
  126. package/dist/molecules/index.js +37 -0
  127. package/dist/organisms/AlertDialog.d.ts +40 -0
  128. package/dist/organisms/AlertDialog.js +104 -0
  129. package/dist/organisms/Calendar.d.ts +96 -0
  130. package/dist/organisms/Calendar.js +353 -0
  131. package/dist/organisms/Carousel.d.ts +29 -0
  132. package/dist/organisms/Carousel.js +128 -0
  133. package/dist/organisms/Chart.d.ts +55 -0
  134. package/dist/organisms/Chart.js +189 -0
  135. package/dist/organisms/Combobox.d.ts +41 -0
  136. package/dist/organisms/Combobox.js +102 -0
  137. package/dist/organisms/Command.d.ts +46 -0
  138. package/dist/organisms/Command.js +171 -0
  139. package/dist/organisms/CommandDialog.d.ts +42 -0
  140. package/dist/organisms/CommandDialog.js +116 -0
  141. package/dist/organisms/ContextMenu.d.ts +29 -0
  142. package/dist/organisms/ContextMenu.js +102 -0
  143. package/dist/organisms/DataTable.d.ts +50 -0
  144. package/dist/organisms/DataTable.js +257 -0
  145. package/dist/organisms/DatePicker.d.ts +34 -0
  146. package/dist/organisms/DatePicker.js +115 -0
  147. package/dist/organisms/DateRangePicker.d.ts +34 -0
  148. package/dist/organisms/DateRangePicker.js +122 -0
  149. package/dist/organisms/Dialog.d.ts +50 -0
  150. package/dist/organisms/Dialog.js +116 -0
  151. package/dist/organisms/Drawer.d.ts +40 -0
  152. package/dist/organisms/Drawer.js +131 -0
  153. package/dist/organisms/DropdownMenu.d.ts +30 -0
  154. package/dist/organisms/DropdownMenu.js +91 -0
  155. package/dist/organisms/Form.d.ts +114 -0
  156. package/dist/organisms/Form.js +151 -0
  157. package/dist/organisms/HoverCard.d.ts +30 -0
  158. package/dist/organisms/HoverCard.js +68 -0
  159. package/dist/organisms/Menubar.d.ts +28 -0
  160. package/dist/organisms/Menubar.js +147 -0
  161. package/dist/organisms/MessageScroller.d.ts +36 -0
  162. package/dist/organisms/MessageScroller.js +124 -0
  163. package/dist/organisms/NavigationMenu.d.ts +41 -0
  164. package/dist/organisms/NavigationMenu.js +139 -0
  165. package/dist/organisms/Popover.d.ts +33 -0
  166. package/dist/organisms/Popover.js +58 -0
  167. package/dist/organisms/Questionnaire.d.ts +74 -0
  168. package/dist/organisms/Questionnaire.js +235 -0
  169. package/dist/organisms/Select.d.ts +47 -0
  170. package/dist/organisms/Select.js +250 -0
  171. package/dist/organisms/Sheet.d.ts +34 -0
  172. package/dist/organisms/Sheet.js +107 -0
  173. package/dist/organisms/Sidebar.d.ts +100 -0
  174. package/dist/organisms/Sidebar.js +228 -0
  175. package/dist/organisms/Toaster.d.ts +67 -0
  176. package/dist/organisms/Toaster.js +189 -0
  177. package/dist/organisms/Tooltip.d.ts +39 -0
  178. package/dist/organisms/Tooltip.js +94 -0
  179. package/dist/organisms/index.d.ts +40 -0
  180. package/dist/organisms/index.js +40 -0
  181. package/dist/primitives/controllable.d.ts +38 -0
  182. package/dist/primitives/controllable.js +43 -0
  183. package/dist/primitives/dismissable.d.ts +53 -0
  184. package/dist/primitives/dismissable.js +158 -0
  185. package/dist/primitives/floating.d.ts +116 -0
  186. package/dist/primitives/floating.js +289 -0
  187. package/dist/primitives/floatingSurface.d.ts +56 -0
  188. package/dist/primitives/floatingSurface.js +142 -0
  189. package/dist/primitives/focusTrap.d.ts +47 -0
  190. package/dist/primitives/focusTrap.js +101 -0
  191. package/dist/primitives/focusable.d.ts +53 -0
  192. package/dist/primitives/focusable.js +109 -0
  193. package/dist/primitives/index.d.ts +27 -0
  194. package/dist/primitives/index.js +27 -0
  195. package/dist/primitives/menu.d.ts +102 -0
  196. package/dist/primitives/menu.js +356 -0
  197. package/dist/primitives/modalSurface.d.ts +54 -0
  198. package/dist/primitives/modalSurface.js +146 -0
  199. package/dist/primitives/portal.d.ts +34 -0
  200. package/dist/primitives/portal.js +42 -0
  201. package/dist/primitives/presence.d.ts +45 -0
  202. package/dist/primitives/presence.js +152 -0
  203. package/dist/primitives/rovingFocus.d.ts +47 -0
  204. package/dist/primitives/rovingFocus.js +176 -0
  205. package/dist/primitives/scrollLock.d.ts +21 -0
  206. package/dist/primitives/scrollLock.js +75 -0
  207. package/dist/primitives/typeahead.d.ts +39 -0
  208. package/dist/primitives/typeahead.js +123 -0
  209. package/dist/templates/AppShell.d.ts +30 -0
  210. package/dist/templates/AppShell.js +42 -0
  211. package/dist/templates/AuthLayout.d.ts +23 -0
  212. package/dist/templates/AuthLayout.js +42 -0
  213. package/dist/templates/SettingsLayout.d.ts +27 -0
  214. package/dist/templates/SettingsLayout.js +42 -0
  215. package/dist/templates/index.d.ts +16 -0
  216. package/dist/templates/index.js +16 -0
  217. package/nebula.css +2 -0
  218. package/package.json +92 -0
  219. package/registry.json +1107 -0
  220. package/src/adapters/css.ts +75 -0
  221. package/src/adapters/index.ts +40 -0
  222. package/src/adapters/tailwind.ts +134 -0
  223. package/src/adapters/types.ts +53 -0
  224. package/src/adapters/unocss.ts +116 -0
  225. package/src/atoms/AspectRatio.ts +28 -0
  226. package/src/atoms/Avatar.ts +67 -0
  227. package/src/atoms/Badge.ts +46 -0
  228. package/src/atoms/Button.ts +96 -0
  229. package/src/atoms/Checkbox.ts +76 -0
  230. package/src/atoms/Input.ts +76 -0
  231. package/src/atoms/Kbd.ts +28 -0
  232. package/src/atoms/Label.ts +38 -0
  233. package/src/atoms/Marker.ts +79 -0
  234. package/src/atoms/NativeSelect.ts +115 -0
  235. package/src/atoms/Progress.ts +64 -0
  236. package/src/atoms/ScrollArea.ts +53 -0
  237. package/src/atoms/Separator.ts +36 -0
  238. package/src/atoms/Skeleton.ts +24 -0
  239. package/src/atoms/Slider.ts +65 -0
  240. package/src/atoms/Spinner.ts +28 -0
  241. package/src/atoms/Switch.ts +56 -0
  242. package/src/atoms/Textarea.ts +58 -0
  243. package/src/atoms/Toggle.ts +67 -0
  244. package/src/atoms/index.ts +62 -0
  245. package/src/cli/add.ts +126 -0
  246. package/src/cli/index.ts +22 -0
  247. package/src/cli/registry.ts +116 -0
  248. package/src/config.ts +102 -0
  249. package/src/configure.ts +127 -0
  250. package/src/index.ts +53 -0
  251. package/src/lib/children.ts +43 -0
  252. package/src/lib/cn.ts +14 -0
  253. package/src/lib/cva.ts +249 -0
  254. package/src/lib/icons.ts +138 -0
  255. package/src/lib/id.ts +44 -0
  256. package/src/lib/index.ts +21 -0
  257. package/src/lib/motion.ts +48 -0
  258. package/src/lib/props.ts +83 -0
  259. package/src/lib/styled.ts +60 -0
  260. package/src/molecules/Accordion.ts +161 -0
  261. package/src/molecules/Alert.ts +58 -0
  262. package/src/molecules/Attachment.ts +132 -0
  263. package/src/molecules/Breadcrumb.ts +107 -0
  264. package/src/molecules/Bubble.ts +134 -0
  265. package/src/molecules/ButtonGroup.ts +55 -0
  266. package/src/molecules/Card.ts +42 -0
  267. package/src/molecules/Collapsible.ts +89 -0
  268. package/src/molecules/Empty.ts +43 -0
  269. package/src/molecules/Field.ts +131 -0
  270. package/src/molecules/InputGroup.ts +62 -0
  271. package/src/molecules/InputOTP.ts +166 -0
  272. package/src/molecules/Item.ts +89 -0
  273. package/src/molecules/Message.ts +127 -0
  274. package/src/molecules/Pagination.ts +197 -0
  275. package/src/molecules/RadioGroup.ts +104 -0
  276. package/src/molecules/Resizable.ts +166 -0
  277. package/src/molecules/Table.ts +118 -0
  278. package/src/molecules/Tabs.ts +148 -0
  279. package/src/molecules/ToggleGroup.ts +109 -0
  280. package/src/molecules/Typography.ts +94 -0
  281. package/src/molecules/index.ts +144 -0
  282. package/src/organisms/AlertDialog.ts +133 -0
  283. package/src/organisms/Calendar.ts +470 -0
  284. package/src/organisms/Carousel.ts +161 -0
  285. package/src/organisms/Chart.ts +257 -0
  286. package/src/organisms/Combobox.ts +140 -0
  287. package/src/organisms/Command.ts +211 -0
  288. package/src/organisms/CommandDialog.ts +149 -0
  289. package/src/organisms/ContextMenu.ts +125 -0
  290. package/src/organisms/DataTable.ts +328 -0
  291. package/src/organisms/DatePicker.ts +141 -0
  292. package/src/organisms/DateRangePicker.ts +149 -0
  293. package/src/organisms/Dialog.ts +162 -0
  294. package/src/organisms/Drawer.ts +167 -0
  295. package/src/organisms/DropdownMenu.ts +112 -0
  296. package/src/organisms/Form.ts +217 -0
  297. package/src/organisms/HoverCard.ts +92 -0
  298. package/src/organisms/Menubar.ts +167 -0
  299. package/src/organisms/MessageScroller.ts +145 -0
  300. package/src/organisms/NavigationMenu.ts +184 -0
  301. package/src/organisms/Popover.ts +84 -0
  302. package/src/organisms/Questionnaire.ts +322 -0
  303. package/src/organisms/Select.ts +286 -0
  304. package/src/organisms/Sheet.ts +144 -0
  305. package/src/organisms/Sidebar.ts +336 -0
  306. package/src/organisms/Toaster.ts +246 -0
  307. package/src/organisms/Tooltip.ts +124 -0
  308. package/src/organisms/index.ts +134 -0
  309. package/src/primitives/controllable.ts +64 -0
  310. package/src/primitives/dismissable.ts +195 -0
  311. package/src/primitives/floating.ts +453 -0
  312. package/src/primitives/floatingSurface.ts +209 -0
  313. package/src/primitives/focusTrap.ts +132 -0
  314. package/src/primitives/focusable.ts +108 -0
  315. package/src/primitives/index.ts +88 -0
  316. package/src/primitives/menu.ts +491 -0
  317. package/src/primitives/modalSurface.ts +199 -0
  318. package/src/primitives/portal.ts +61 -0
  319. package/src/primitives/presence.ts +185 -0
  320. package/src/primitives/rovingFocus.ts +212 -0
  321. package/src/primitives/scrollLock.ts +79 -0
  322. package/src/primitives/typeahead.ts +156 -0
  323. package/src/templates/AppShell.ts +56 -0
  324. package/src/templates/AuthLayout.ts +66 -0
  325. package/src/templates/SettingsLayout.ts +63 -0
  326. package/src/templates/index.ts +21 -0
  327. package/theme.css +151 -0
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Which elements can take focus.
3
+ *
4
+ * The focus trap, the roving-focus groups and every overlay that restores
5
+ * focus on close all need the same answer, and getting it slightly different
6
+ * in each place is how keyboard navigation rots. One implementation here.
7
+ *
8
+ * The selector is the standard tabbable set, but a selector alone is not the
9
+ * answer: it matches nodes that are disabled, `inert`, hidden by CSS, or
10
+ * inside a collapsed `<details>`. Those have to be filtered by asking the
11
+ * layout engine, which is why `isVisible` reads `offsetParent` and computed
12
+ * styles rather than trusting the markup.
13
+ */
14
+ /**
15
+ * Is the element rendered and interactive right now?
16
+ *
17
+ * `offsetParent === null` catches `display: none` on the element or any
18
+ * ancestor in one property read, which is much cheaper than walking the tree.
19
+ * It reports null for `position: fixed` elements too, so those fall through to
20
+ * the explicit style checks — that branch is why dialogs and popovers, which
21
+ * are routinely fixed, are not wrongly treated as hidden.
22
+ */
23
+ export declare function isVisible(el: HTMLElement): boolean;
24
+ /**
25
+ * Can this element receive focus via the keyboard?
26
+ *
27
+ * Focusability is decided from the selector match plus the `tabindex`
28
+ * *attribute* — deliberately not the `tabIndex` property. The property is
29
+ * derived: a browser reports `0` for a native `<button>` that never declared
30
+ * one, and DOM implementations disagree about which elements get that
31
+ * treatment. Reading the attribute asks what the markup actually says, and
32
+ * "natively focusable unless it opted out" is then a rule this function states
33
+ * rather than one it inherits from whichever DOM it is running in.
34
+ */
35
+ export declare function isFocusable(el: Element): el is HTMLElement;
36
+ /**
37
+ * Every focusable descendant of `root`, in tab order.
38
+ *
39
+ * Document order is returned as-is. Honouring positive `tabindex` values would
40
+ * mean a second sort, and nebula never emits one — a positive tabindex breaks
41
+ * the tab order of the whole page, so the library treats its presence in host
42
+ * markup as the host's problem rather than reordering around it.
43
+ */
44
+ export declare function focusableWithin(root: ParentNode): HTMLElement[];
45
+ /** First focusable descendant, or `null` when the subtree has none. */
46
+ export declare function firstFocusable(root: ParentNode): HTMLElement | null;
47
+ /**
48
+ * Focus an element without scrolling the page to it.
49
+ *
50
+ * Overlays position themselves; letting the browser scroll to the newly
51
+ * focused node fights that positioning and visibly jumps the page.
52
+ */
53
+ export declare function focusSilently(el: HTMLElement | null | undefined): void;
@@ -0,0 +1,109 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Which elements can take focus.
4
+ *
5
+ * The focus trap, the roving-focus groups and every overlay that restores
6
+ * focus on close all need the same answer, and getting it slightly different
7
+ * in each place is how keyboard navigation rots. One implementation here.
8
+ *
9
+ * The selector is the standard tabbable set, but a selector alone is not the
10
+ * answer: it matches nodes that are disabled, `inert`, hidden by CSS, or
11
+ * inside a collapsed `<details>`. Those have to be filtered by asking the
12
+ * layout engine, which is why `isVisible` reads `offsetParent` and computed
13
+ * styles rather than trusting the markup.
14
+ */
15
+ const FOCUSABLE_SELECTOR = [
16
+ "a[href]",
17
+ "area[href]",
18
+ "button:not([disabled])",
19
+ "input:not([disabled]):not([type='hidden'])",
20
+ "select:not([disabled])",
21
+ "textarea:not([disabled])",
22
+ "iframe",
23
+ "object",
24
+ "embed",
25
+ "audio[controls]",
26
+ "video[controls]",
27
+ "summary",
28
+ "[contenteditable]:not([contenteditable='false'])",
29
+ "[tabindex]",
30
+ ].join(",");
31
+ /**
32
+ * Is the element rendered and interactive right now?
33
+ *
34
+ * `offsetParent === null` catches `display: none` on the element or any
35
+ * ancestor in one property read, which is much cheaper than walking the tree.
36
+ * It reports null for `position: fixed` elements too, so those fall through to
37
+ * the explicit style checks — that branch is why dialogs and popovers, which
38
+ * are routinely fixed, are not wrongly treated as hidden.
39
+ */
40
+ export function isVisible(el) {
41
+ if (el.hasAttribute("inert"))
42
+ return false;
43
+ if (el.closest("[inert]") !== null)
44
+ return false;
45
+ if (el.offsetParent !== null)
46
+ return true;
47
+ const style = getComputedStyle(el);
48
+ if (style.position !== "fixed")
49
+ return false;
50
+ return style.display !== "none" && style.visibility !== "hidden";
51
+ }
52
+ /**
53
+ * Can this element receive focus via the keyboard?
54
+ *
55
+ * Focusability is decided from the selector match plus the `tabindex`
56
+ * *attribute* — deliberately not the `tabIndex` property. The property is
57
+ * derived: a browser reports `0` for a native `<button>` that never declared
58
+ * one, and DOM implementations disagree about which elements get that
59
+ * treatment. Reading the attribute asks what the markup actually says, and
60
+ * "natively focusable unless it opted out" is then a rule this function states
61
+ * rather than one it inherits from whichever DOM it is running in.
62
+ */
63
+ export function isFocusable(el) {
64
+ if (!(el instanceof HTMLElement))
65
+ return false;
66
+ if (!el.matches(FOCUSABLE_SELECTOR))
67
+ return false;
68
+ if (el.hasAttribute("disabled"))
69
+ return false;
70
+ if (el.getAttribute("aria-hidden") === "true")
71
+ return false;
72
+ const declared = el.getAttribute("tabindex");
73
+ if (declared !== null && Number.parseInt(declared, 10) < 0)
74
+ return false;
75
+ return isVisible(el);
76
+ }
77
+ /**
78
+ * Every focusable descendant of `root`, in tab order.
79
+ *
80
+ * Document order is returned as-is. Honouring positive `tabindex` values would
81
+ * mean a second sort, and nebula never emits one — a positive tabindex breaks
82
+ * the tab order of the whole page, so the library treats its presence in host
83
+ * markup as the host's problem rather than reordering around it.
84
+ */
85
+ export function focusableWithin(root) {
86
+ const found = [];
87
+ for (const candidate of root.querySelectorAll(FOCUSABLE_SELECTOR)) {
88
+ if (isFocusable(candidate))
89
+ found.push(candidate);
90
+ }
91
+ return found;
92
+ }
93
+ /** First focusable descendant, or `null` when the subtree has none. */
94
+ export function firstFocusable(root) {
95
+ for (const candidate of root.querySelectorAll(FOCUSABLE_SELECTOR)) {
96
+ if (isFocusable(candidate))
97
+ return candidate;
98
+ }
99
+ return null;
100
+ }
101
+ /**
102
+ * Focus an element without scrolling the page to it.
103
+ *
104
+ * Overlays position themselves; letting the browser scroll to the newly
105
+ * focused node fights that positioning and visibly jumps the page.
106
+ */
107
+ export function focusSilently(el) {
108
+ el?.focus({ preventScroll: true });
109
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Headless primitives — the behaviour layer nebula owns instead of Radix.
3
+ *
4
+ * shadcn/ui is styling on top of Radix. Radix is React-only, so porting the
5
+ * styling alone would leave every interactive component inert. These modules
6
+ * are that missing half, written against Aurora's signals and DOM: focus
7
+ * management, layer dismissal, placement, presence, roving focus, type-ahead
8
+ * and controlled/uncontrolled state.
9
+ *
10
+ * They are deliberately usable on their own. A component the registry does not
11
+ * ship — an app's own combobox, a bespoke overlay — should be able to reach
12
+ * for the same trap and the same layer stack rather than growing a second,
13
+ * subtly different implementation next door.
14
+ */
15
+ export { type Controllable, type ControllableOptions, controllable, } from "./controllable.js";
16
+ export { type DismissableLayer, type DismissableOptions, type DismissReason, dismissable, layerCount, } from "./dismissable.js";
17
+ export { type Align, type AutoPosition, type AutoPositionOptions, autoPosition, type Placement, type Position, type PositionOptions, type Rect, resolvePosition, type Side, type Viewport, } from "./floating.js";
18
+ export { type FloatingSurfaceOptions, floatingSurface, } from "./floatingSurface.js";
19
+ export { firstFocusable, focusableWithin, focusSilently, isFocusable, isVisible, } from "./focusable.js";
20
+ export { type FocusTrap, type FocusTrapOptions, focusTrap, } from "./focusTrap.js";
21
+ export { type MenuAction, type MenuCheckbox, type MenuEntry, type MenuLabel, type MenuRadioGroup, type MenuSeparator, type MenuSubmenu, menuPanel, menuPanelClasses, wireMenu, } from "./menu.js";
22
+ export { type ModalSurfaceOptions, modalSurface } from "./modalSurface.js";
23
+ export { type Portal, type PortalOptions, portal } from "./portal.js";
24
+ export { onExitFinished, type Presence, type PresenceState, presence, } from "./presence.js";
25
+ export { type Orientation, type RovingFocus, type RovingFocusOptions, rovingFocus, } from "./rovingFocus.js";
26
+ export { isScrollLocked, lockScroll } from "./scrollLock.js";
27
+ export { type Typeahead, type TypeaheadOptions, typeahead, } from "./typeahead.js";
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Headless primitives — the behaviour layer nebula owns instead of Radix.
3
+ *
4
+ * shadcn/ui is styling on top of Radix. Radix is React-only, so porting the
5
+ * styling alone would leave every interactive component inert. These modules
6
+ * are that missing half, written against Aurora's signals and DOM: focus
7
+ * management, layer dismissal, placement, presence, roving focus, type-ahead
8
+ * and controlled/uncontrolled state.
9
+ *
10
+ * They are deliberately usable on their own. A component the registry does not
11
+ * ship — an app's own combobox, a bespoke overlay — should be able to reach
12
+ * for the same trap and the same layer stack rather than growing a second,
13
+ * subtly different implementation next door.
14
+ */
15
+ export { controllable, } from "./controllable.js";
16
+ export { dismissable, layerCount, } from "./dismissable.js";
17
+ export { autoPosition, resolvePosition, } from "./floating.js";
18
+ export { floatingSurface, } from "./floatingSurface.js";
19
+ export { firstFocusable, focusableWithin, focusSilently, isFocusable, isVisible, } from "./focusable.js";
20
+ export { focusTrap, } from "./focusTrap.js";
21
+ export { menuPanel, menuPanelClasses, wireMenu, } from "./menu.js";
22
+ export { modalSurface } from "./modalSurface.js";
23
+ export { portal } from "./portal.js";
24
+ export { onExitFinished, presence, } from "./presence.js";
25
+ export { rovingFocus, } from "./rovingFocus.js";
26
+ export { isScrollLocked, lockScroll } from "./scrollLock.js";
27
+ export { typeahead, } from "./typeahead.js";
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The menu panel, shared by every menu-shaped surface.
3
+ *
4
+ * DropdownMenu, ContextMenu and Menubar differ only in what opens them — a
5
+ * button, a right-click, a bar of buttons. The panel itself, its entries, its
6
+ * keyboard model and its submenus are identical, and live here.
7
+ *
8
+ * **Entries are data.** A menu is a list, and describing it as one is what
9
+ * lets the panel enforce things no individual item can see: which radio in a
10
+ * group is selected, where the separators fall, and — the one that matters
11
+ * most — that activating any item closes the whole stack, submenus included.
12
+ *
13
+ * **Submenus are imperative.** `floatingSurface` registers mount and unmount
14
+ * hooks, so it can only be called from a component setup. A submenu opens from
15
+ * inside portalled content, long after setup has run, so it wires `portal`,
16
+ * `autoPosition` and `dismissable` directly. The stack of open submenus is
17
+ * held here and unwound together.
18
+ *
19
+ * The keyboard model is the WAI-ARIA menu pattern: arrows move, Home and End
20
+ * jump, typing seeks, ArrowRight opens a submenu, ArrowLeft closes it, Escape
21
+ * closes one level, and activating an item closes everything.
22
+ */
23
+ import { type TemplateResult } from "@c9up/aurora";
24
+ import type { Child } from "../lib/children.js";
25
+ export interface MenuAction {
26
+ type?: "item";
27
+ label: Child;
28
+ icon?: Child;
29
+ /** Rendered right-aligned. Display only — bind the real shortcut yourself. */
30
+ shortcut?: string;
31
+ disabled?: boolean;
32
+ /** Styles the entry as a destructive action. */
33
+ destructive?: boolean;
34
+ onSelect?: () => void;
35
+ }
36
+ export interface MenuCheckbox {
37
+ type: "checkbox";
38
+ label: Child;
39
+ checked: boolean;
40
+ disabled?: boolean;
41
+ onCheckedChange?: (checked: boolean) => void;
42
+ }
43
+ export interface MenuRadioGroup {
44
+ type: "radio-group";
45
+ value: string;
46
+ options: ReadonlyArray<{
47
+ value: string;
48
+ label: Child;
49
+ disabled?: boolean;
50
+ }>;
51
+ onValueChange?: (value: string) => void;
52
+ }
53
+ export interface MenuLabel {
54
+ type: "label";
55
+ label: Child;
56
+ }
57
+ export interface MenuSeparator {
58
+ type: "separator";
59
+ }
60
+ export interface MenuSubmenu {
61
+ type: "submenu";
62
+ label: Child;
63
+ icon?: Child;
64
+ disabled?: boolean;
65
+ entries: readonly MenuEntry[];
66
+ }
67
+ export type MenuEntry = MenuAction | MenuCheckbox | MenuRadioGroup | MenuLabel | MenuSeparator | MenuSubmenu;
68
+ export declare const menuPanelClasses = "bg-popover text-popover-foreground z-50 min-w-[8rem] max-h-(--nebula-available-height) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md outline-none";
69
+ export interface MenuPanelOptions {
70
+ id: string;
71
+ entries: readonly MenuEntry[];
72
+ /** Close the whole menu stack — called after an item is activated. */
73
+ onCloseAll: () => void;
74
+ labelledBy?: string;
75
+ class?: string;
76
+ }
77
+ /** The panel's markup. Behaviour is attached separately by `wireMenu`. */
78
+ export declare function menuPanel(options: MenuPanelOptions): TemplateResult;
79
+ export interface WireMenuOptions {
80
+ /** Close the whole stack, back to the original trigger. */
81
+ onCloseAll: () => void;
82
+ /** Focus the first item as soon as the panel opens. */
83
+ autoFocusFirst?: boolean;
84
+ /**
85
+ * Close *this* panel and return focus to whatever opened it.
86
+ *
87
+ * Supplied only when wiring a submenu, and it is what makes ArrowLeft and
88
+ * Escape work inside one. A submenu is portalled, so its keydowns never
89
+ * bubble to the parent panel's listener — the parent hands down a way to be
90
+ * called instead. Without it the two keys fall through: ArrowLeft does
91
+ * nothing at all, and Escape reaches the document and closes the entire
92
+ * stack rather than one level.
93
+ */
94
+ onCloseSelf?: () => void;
95
+ }
96
+ /**
97
+ * Attach the keyboard model and submenu handling to a live panel.
98
+ *
99
+ * Returns a teardown that also closes any submenus still open, which is what
100
+ * keeps a stack of three from leaving two behind when the root closes.
101
+ */
102
+ export declare function wireMenu(panel: HTMLElement, options: WireMenuOptions): () => void;
@@ -0,0 +1,356 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * The menu panel, shared by every menu-shaped surface.
4
+ *
5
+ * DropdownMenu, ContextMenu and Menubar differ only in what opens them — a
6
+ * button, a right-click, a bar of buttons. The panel itself, its entries, its
7
+ * keyboard model and its submenus are identical, and live here.
8
+ *
9
+ * **Entries are data.** A menu is a list, and describing it as one is what
10
+ * lets the panel enforce things no individual item can see: which radio in a
11
+ * group is selected, where the separators fall, and — the one that matters
12
+ * most — that activating any item closes the whole stack, submenus included.
13
+ *
14
+ * **Submenus are imperative.** `floatingSurface` registers mount and unmount
15
+ * hooks, so it can only be called from a component setup. A submenu opens from
16
+ * inside portalled content, long after setup has run, so it wires `portal`,
17
+ * `autoPosition` and `dismissable` directly. The stack of open submenus is
18
+ * held here and unwound together.
19
+ *
20
+ * The keyboard model is the WAI-ARIA menu pattern: arrows move, Home and End
21
+ * jump, typing seeks, ArrowRight opens a submenu, ArrowLeft closes it, Escape
22
+ * closes one level, and activating an item closes everything.
23
+ */
24
+ import { html } from "@c9up/aurora";
25
+ import { cn } from "../lib/cn.js";
26
+ import { CheckIcon, ChevronRightIcon, DotIcon } from "../lib/icons.js";
27
+ import { uid } from "../lib/id.js";
28
+ import { zoomInOut } from "../lib/motion.js";
29
+ import { dismissable } from "./dismissable.js";
30
+ import { autoPosition } from "./floating.js";
31
+ import { focusSilently } from "./focusable.js";
32
+ import { portal } from "./portal.js";
33
+ import { rovingFocus } from "./rovingFocus.js";
34
+ import { typeahead } from "./typeahead.js";
35
+ // ─── classes ─────────────────────────────────────────────────────────
36
+ export const menuPanelClasses = "bg-popover text-popover-foreground z-50 min-w-[8rem] max-h-(--nebula-available-height) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md outline-none";
37
+ const itemClasses = "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4";
38
+ const destructiveClasses = "text-destructive focus:bg-destructive/10 focus:text-destructive [&_svg]:text-destructive";
39
+ const indentedItemClasses = `${itemClasses} py-1.5 pr-2 pl-8`;
40
+ /** The panel's markup. Behaviour is attached separately by `wireMenu`. */
41
+ export function menuPanel(options) {
42
+ return html `<div
43
+ data-slot="menu-content"
44
+ id="${options.id}"
45
+ role="menu"
46
+ aria-labelledby="${options.labelledBy}"
47
+ aria-orientation="vertical"
48
+ tabindex="-1"
49
+ class="${cn(menuPanelClasses, zoomInOut, options.class)}"
50
+ >${options.entries.map((entry) => renderEntry(entry, options.onCloseAll))}</div>`;
51
+ }
52
+ function renderEntry(entry, closeAll) {
53
+ if (isSeparator(entry)) {
54
+ return html `<div role="separator" class="bg-border -mx-1 my-1 h-px"></div>`;
55
+ }
56
+ if (isLabel(entry)) {
57
+ return html `<div
58
+ role="presentation"
59
+ class="px-2 py-1.5 text-sm font-medium text-muted-foreground"
60
+ >${entry.label}</div>`;
61
+ }
62
+ if (isCheckbox(entry))
63
+ return renderCheckbox(entry, closeAll);
64
+ if (isRadioGroup(entry))
65
+ return renderRadioGroup(entry, closeAll);
66
+ if (isSubmenu(entry))
67
+ return renderSubmenuTrigger(entry);
68
+ return renderAction(entry, closeAll);
69
+ }
70
+ function renderAction(entry, closeAll) {
71
+ return html `<div
72
+ role="menuitem"
73
+ data-nebula-item
74
+ data-disabled="${entry.disabled === true ? "" : undefined}"
75
+ aria-disabled="${entry.disabled === true ? "true" : undefined}"
76
+ tabindex="-1"
77
+ class="${cn(itemClasses, entry.destructive === true ? destructiveClasses : "")}"
78
+ @click="${() => activate(entry.disabled, entry.onSelect, closeAll)}"
79
+ >
80
+ ${entry.icon}
81
+ <span class="flex-1 truncate">${entry.label}</span>
82
+ ${entry.shortcut === undefined
83
+ ? null
84
+ : html `<span class="text-muted-foreground ml-auto text-xs tracking-widest"
85
+ >${entry.shortcut}</span
86
+ >`}
87
+ </div>`;
88
+ }
89
+ function renderCheckbox(entry, closeAll) {
90
+ return html `<div
91
+ role="menuitemcheckbox"
92
+ data-nebula-item
93
+ aria-checked="${entry.checked ? "true" : "false"}"
94
+ data-disabled="${entry.disabled === true ? "" : undefined}"
95
+ tabindex="-1"
96
+ class="${indentedItemClasses}"
97
+ @click="${() => activate(entry.disabled, () => entry.onCheckedChange?.(!entry.checked), closeAll)}"
98
+ >
99
+ <span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
100
+ ${entry.checked ? CheckIcon({ class: "size-4" }) : null}
101
+ </span>
102
+ ${entry.label}
103
+ </div>`;
104
+ }
105
+ function renderRadioGroup(entry, closeAll) {
106
+ return html `<div role="group">
107
+ ${entry.options.map((option) => html `<div
108
+ role="menuitemradio"
109
+ data-nebula-item
110
+ aria-checked="${option.value === entry.value ? "true" : "false"}"
111
+ data-disabled="${option.disabled === true ? "" : undefined}"
112
+ tabindex="-1"
113
+ class="${indentedItemClasses}"
114
+ @click="${() => activate(option.disabled, () => entry.onValueChange?.(option.value), closeAll)}"
115
+ >
116
+ <span class="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
117
+ ${option.value === entry.value ? DotIcon({ class: "size-2" }) : null}
118
+ </span>
119
+ ${option.label}
120
+ </div>`)}
121
+ </div>`;
122
+ }
123
+ /**
124
+ * A submenu's trigger row.
125
+ *
126
+ * The entries are stashed on the element through a WeakMap rather than an
127
+ * attribute — they contain callbacks and nested structures, and serialising
128
+ * those into the DOM is not possible. `wireMenu` reads them back when the row
129
+ * is opened.
130
+ */
131
+ const submenuEntries = new WeakMap();
132
+ function renderSubmenuTrigger(entry) {
133
+ const id = uid("menu-sub-trigger");
134
+ const row = html `<div
135
+ role="menuitem"
136
+ data-nebula-item
137
+ data-submenu="${id}"
138
+ data-disabled="${entry.disabled === true ? "" : undefined}"
139
+ aria-haspopup="menu"
140
+ aria-expanded="false"
141
+ tabindex="-1"
142
+ class="${itemClasses}"
143
+ >
144
+ ${entry.icon}
145
+ <span class="flex-1 truncate">${entry.label}</span>
146
+ ${ChevronRightIcon({ class: "ml-auto size-4" })}
147
+ </div>`;
148
+ pendingSubmenus.set(id, entry.entries);
149
+ return row;
150
+ }
151
+ /**
152
+ * Entries waiting to be claimed by their element.
153
+ *
154
+ * The trigger's entries are known while the template is built, but the element
155
+ * does not exist until it is mounted. This bridges the two: `wireMenu` moves
156
+ * each set from here onto the real element and clears the id, so nothing is
157
+ * retained after the menu closes.
158
+ */
159
+ const pendingSubmenus = new Map();
160
+ function activate(disabled, onSelect, closeAll) {
161
+ if (disabled === true)
162
+ return;
163
+ onSelect?.();
164
+ closeAll();
165
+ }
166
+ /**
167
+ * Attach the keyboard model and submenu handling to a live panel.
168
+ *
169
+ * Returns a teardown that also closes any submenus still open, which is what
170
+ * keeps a stack of three from leaving two behind when the root closes.
171
+ */
172
+ export function wireMenu(panel, options) {
173
+ claimSubmenuEntries(panel);
174
+ const open = [];
175
+ function closeSubmenusFrom(depth) {
176
+ while (open.length > depth)
177
+ open.pop()?.close();
178
+ }
179
+ const group = rovingFocus({
180
+ container: () => panel,
181
+ itemSelector: "[data-nebula-item]",
182
+ orientation: "vertical",
183
+ // Moving off a submenu's trigger closes it, so the pointer travelling
184
+ // down a menu does not leave a trail of open panels behind it.
185
+ onFocusChange: (item) => {
186
+ if (item.getAttribute("data-submenu") === null)
187
+ closeSubmenusFrom(0);
188
+ },
189
+ onSelect: (item) => {
190
+ if (item.getAttribute("data-submenu") !== null)
191
+ openSubmenu(item);
192
+ else
193
+ item.click();
194
+ },
195
+ });
196
+ group.sync();
197
+ const seek = typeahead({
198
+ items: () => group.items(),
199
+ onMatch: (item) => focusSilently(item),
200
+ });
201
+ function openSubmenu(trigger) {
202
+ const entries = submenuEntries.get(trigger);
203
+ if (entries === undefined || entries.length === 0)
204
+ return;
205
+ closeSubmenusFrom(0);
206
+ const panelId = uid("menu-sub");
207
+ const mount = portal(menuPanel({ id: panelId, entries, onCloseAll: options.onCloseAll }));
208
+ const element = mount.host.firstElementChild;
209
+ if (!(element instanceof HTMLElement)) {
210
+ mount.close();
211
+ return;
212
+ }
213
+ element.setAttribute("data-state", "open");
214
+ trigger.setAttribute("aria-expanded", "true");
215
+ trigger.setAttribute("aria-controls", panelId);
216
+ // Submenus sit beside their parent, overlapping it slightly, which is what
217
+ // lets the pointer travel diagonally into them without crossing a gap.
218
+ const position = autoPosition(trigger, element, {
219
+ placement: "right-start",
220
+ offset: -4,
221
+ });
222
+ const layer = dismissable({
223
+ element: () => element,
224
+ exclude: () => [trigger, panel],
225
+ onDismiss: () => closeSubmenusFrom(0),
226
+ escapeKey: false,
227
+ });
228
+ const inner = wireMenu(element, {
229
+ ...options,
230
+ onCloseSelf: () => {
231
+ closeSubmenusFrom(0);
232
+ focusSilently(trigger);
233
+ },
234
+ });
235
+ const firstItem = element.querySelector("[data-nebula-item]");
236
+ if (firstItem instanceof HTMLElement)
237
+ focusSilently(firstItem);
238
+ open.push({
239
+ close() {
240
+ inner();
241
+ layer.remove();
242
+ position.stop();
243
+ mount.close();
244
+ trigger.setAttribute("aria-expanded", "false");
245
+ trigger.removeAttribute("aria-controls");
246
+ },
247
+ });
248
+ }
249
+ function onKeyDown(event) {
250
+ const active = document.activeElement;
251
+ const inside = active instanceof Node && panel.contains(active);
252
+ if (event.key === "ArrowRight" && inside) {
253
+ if (!(active instanceof HTMLElement))
254
+ return;
255
+ if (active.getAttribute("data-submenu") === null)
256
+ return;
257
+ event.preventDefault();
258
+ event.stopPropagation();
259
+ openSubmenu(active);
260
+ return;
261
+ }
262
+ // One level per press for both keys: a submenu three deep takes three
263
+ // Escapes, not one. Closing a child we opened comes first; with none
264
+ // open, this panel is itself the child and asks its parent to close it.
265
+ // `dismissable` handles the last level, closing the root menu.
266
+ if (event.key === "ArrowLeft" || event.key === "Escape") {
267
+ if (open.length > 0) {
268
+ event.preventDefault();
269
+ event.stopPropagation();
270
+ closeSubmenusFrom(open.length - 1);
271
+ return;
272
+ }
273
+ if (options.onCloseSelf !== undefined) {
274
+ event.preventDefault();
275
+ event.stopPropagation();
276
+ options.onCloseSelf();
277
+ return;
278
+ }
279
+ // A root menu ignores ArrowLeft and lets `dismissable` see Escape.
280
+ if (event.key === "ArrowLeft")
281
+ return;
282
+ }
283
+ if (event.key === "Tab") {
284
+ // Tab out of a menu means "I am done here", not "next item".
285
+ options.onCloseAll();
286
+ return;
287
+ }
288
+ if (inside)
289
+ seek.handleKey(event);
290
+ }
291
+ function onPointerOver(event) {
292
+ const target = event.target;
293
+ if (!(target instanceof Element))
294
+ return;
295
+ const item = target.closest("[data-nebula-item]");
296
+ if (!(item instanceof HTMLElement))
297
+ return;
298
+ if (item.hasAttribute("data-disabled"))
299
+ return;
300
+ focusSilently(item);
301
+ // Hover focuses the row directly rather than going through rovingFocus,
302
+ // so its `onFocusChange` never runs — the close has to be explicit here
303
+ // or the pointer leaves a trail of open panels behind it.
304
+ if (item.getAttribute("data-submenu") !== null)
305
+ openSubmenu(item);
306
+ else
307
+ closeSubmenusFrom(0);
308
+ }
309
+ panel.addEventListener("keydown", onKeyDown);
310
+ panel.addEventListener("pointerover", onPointerOver);
311
+ if (options.autoFocusFirst === true) {
312
+ const first = group.items()[0];
313
+ if (first !== undefined)
314
+ focusSilently(first);
315
+ }
316
+ return () => {
317
+ closeSubmenusFrom(0);
318
+ panel.removeEventListener("keydown", onKeyDown);
319
+ panel.removeEventListener("pointerover", onPointerOver);
320
+ seek.reset();
321
+ group.destroy();
322
+ };
323
+ }
324
+ /** Move each submenu's entries from the build-time map onto its element. */
325
+ function claimSubmenuEntries(panel) {
326
+ for (const node of panel.querySelectorAll("[data-submenu]")) {
327
+ const id = node.getAttribute("data-submenu");
328
+ if (id === null)
329
+ continue;
330
+ const entries = pendingSubmenus.get(id);
331
+ if (entries === undefined)
332
+ continue;
333
+ submenuEntries.set(node, entries);
334
+ pendingSubmenus.delete(id);
335
+ }
336
+ }
337
+ // ─── entry guards ────────────────────────────────────────────────────
338
+ //
339
+ // Discriminating on an optional `type` needs real guards: `entry.type ===
340
+ // "item"` is false for a plain action, which leaves it undiscriminated. Each
341
+ // guard names the shape it accepts so the renderer narrows without a cast.
342
+ function isSeparator(entry) {
343
+ return entry.type === "separator";
344
+ }
345
+ function isLabel(entry) {
346
+ return entry.type === "label";
347
+ }
348
+ function isCheckbox(entry) {
349
+ return entry.type === "checkbox";
350
+ }
351
+ function isRadioGroup(entry) {
352
+ return entry.type === "radio-group";
353
+ }
354
+ function isSubmenu(entry) {
355
+ return entry.type === "submenu";
356
+ }