@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,54 @@
1
+ /**
2
+ * Modal surface — the lifecycle of a dialog-shaped overlay.
3
+ *
4
+ * Dialog, AlertDialog, Sheet and Drawer differ only in where they come from
5
+ * and how they animate. What they share is the modal contract, and every part
6
+ * of it is a thing an app gets wrong when it builds one by hand:
7
+ *
8
+ * - A backdrop, so the page behind reads as unavailable.
9
+ * - Focus trapped inside, and returned to the trigger on close.
10
+ * - Page scroll locked, without the sideways jump when the scrollbar goes.
11
+ * - Escape closes; a click on the backdrop closes, unless the surface is
12
+ * asking a question that must be answered (AlertDialog).
13
+ * - The rest of the page hidden from assistive technology, so a screen reader
14
+ * cannot wander out of the dialog while sighted users cannot.
15
+ *
16
+ * That last one is the one almost always missed. Trapping *keyboard* focus
17
+ * does nothing for a reader navigating by heading or landmark — it will read
18
+ * straight through the page behind the overlay. `aria-hidden` on the siblings
19
+ * of the portal is what actually makes the dialog modal.
20
+ */
21
+ import { type TemplateResult } from "@c9up/aurora";
22
+ import { type DismissReason } from "./dismissable.js";
23
+ export interface ModalSurfaceOptions {
24
+ open: () => boolean;
25
+ onClose: (reason: DismissReason) => void;
26
+ /** The whole overlay: backdrop and panel. */
27
+ content: () => TemplateResult;
28
+ /**
29
+ * The panel within the content, when the content's root is a full-screen
30
+ * wrapper.
31
+ *
32
+ * Containment is answered against this, not the root. An overlay's root
33
+ * usually spans the viewport so the backdrop can, and a root-based check
34
+ * would report every click on the page as "inside" — outside-click dismissal
35
+ * would silently never fire. The focus trap needs the same distinction:
36
+ * trapping to the root would let Tab reach the backdrop.
37
+ *
38
+ * Defaults to the root, which is right for an overlay that is only a panel.
39
+ */
40
+ panel?: (root: HTMLElement) => HTMLElement | null;
41
+ /** Where focus lands. Defaults to the first focusable element. */
42
+ initialFocus?: (content: HTMLElement) => HTMLElement | null;
43
+ /** Where focus returns. Defaults to whatever was focused before opening. */
44
+ returnFocus?: () => HTMLElement | null;
45
+ /** A click outside closes. `false` for a decision that must be made. */
46
+ dismissOnOutside?: boolean;
47
+ /** Escape closes. `false` for the same reason. */
48
+ dismissOnEscape?: boolean;
49
+ /** Hold page scroll. Default `true`. */
50
+ lockScroll?: boolean;
51
+ onOpened?: (content: HTMLElement) => void;
52
+ onClosed?: () => void;
53
+ }
54
+ export declare function modalSurface(options: ModalSurfaceOptions): void;
@@ -0,0 +1,146 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Modal surface — the lifecycle of a dialog-shaped overlay.
4
+ *
5
+ * Dialog, AlertDialog, Sheet and Drawer differ only in where they come from
6
+ * and how they animate. What they share is the modal contract, and every part
7
+ * of it is a thing an app gets wrong when it builds one by hand:
8
+ *
9
+ * - A backdrop, so the page behind reads as unavailable.
10
+ * - Focus trapped inside, and returned to the trigger on close.
11
+ * - Page scroll locked, without the sideways jump when the scrollbar goes.
12
+ * - Escape closes; a click on the backdrop closes, unless the surface is
13
+ * asking a question that must be answered (AlertDialog).
14
+ * - The rest of the page hidden from assistive technology, so a screen reader
15
+ * cannot wander out of the dialog while sighted users cannot.
16
+ *
17
+ * That last one is the one almost always missed. Trapping *keyboard* focus
18
+ * does nothing for a reader navigating by heading or landmark — it will read
19
+ * straight through the page behind the overlay. `aria-hidden` on the siblings
20
+ * of the portal is what actually makes the dialog modal.
21
+ */
22
+ import { effect, onMount } from "@c9up/aurora";
23
+ import { dismissable, } from "./dismissable.js";
24
+ import { focusTrap } from "./focusTrap.js";
25
+ import { portal } from "./portal.js";
26
+ import { onExitFinished } from "./presence.js";
27
+ import { lockScroll } from "./scrollLock.js";
28
+ export function modalSurface(options) {
29
+ let live = null;
30
+ function show() {
31
+ if (live !== null) {
32
+ live.cancelExit?.();
33
+ live.cancelExit = null;
34
+ live.element.setAttribute("data-state", "open");
35
+ return;
36
+ }
37
+ const mount = portal(options.content());
38
+ const element = mount.host.firstElementChild;
39
+ if (!(element instanceof HTMLElement)) {
40
+ mount.close();
41
+ return;
42
+ }
43
+ element.setAttribute("data-state", "open");
44
+ const panel = options.panel?.(element) ?? element;
45
+ const unlock = options.lockScroll === false ? () => { } : lockScroll();
46
+ const restoreSiblings = hideSiblingsFrom(mount.host);
47
+ const trap = focusTrap(panel, {
48
+ initialFocus: () => options.initialFocus?.(panel) ?? null,
49
+ returnFocus: options.returnFocus,
50
+ });
51
+ const layer = dismissable({
52
+ element: () => panel,
53
+ onDismiss: options.onClose,
54
+ escapeKey: options.dismissOnEscape !== false,
55
+ outsidePointer: options.dismissOnOutside !== false,
56
+ outsideFocus: false,
57
+ });
58
+ live = {
59
+ mount,
60
+ element,
61
+ layer,
62
+ trap,
63
+ unlock,
64
+ restoreSiblings,
65
+ cancelExit: null,
66
+ };
67
+ options.onOpened?.(panel);
68
+ }
69
+ function hide() {
70
+ const current = live;
71
+ if (current === null)
72
+ return;
73
+ current.layer.remove();
74
+ current.trap.release();
75
+ current.restoreSiblings();
76
+ current.element.setAttribute("data-state", "closed");
77
+ options.onClosed?.();
78
+ // Scroll stays locked through the exit animation. Releasing it first
79
+ // lets the page jump back under a panel that is still sliding away.
80
+ current.cancelExit = onExitFinished(current.element, () => {
81
+ current.unlock();
82
+ current.mount.close();
83
+ if (live === current)
84
+ live = null;
85
+ });
86
+ }
87
+ function teardown() {
88
+ const current = live;
89
+ if (current === null)
90
+ return;
91
+ live = null;
92
+ current.cancelExit?.();
93
+ current.layer.remove();
94
+ current.trap.release();
95
+ current.restoreSiblings();
96
+ current.unlock();
97
+ current.mount.close();
98
+ }
99
+ onMount(() => {
100
+ const stop = effect(() => {
101
+ if (options.open())
102
+ show();
103
+ else
104
+ hide();
105
+ });
106
+ return () => {
107
+ stop();
108
+ teardown();
109
+ };
110
+ });
111
+ }
112
+ /**
113
+ * Hide everything except the overlay from assistive technology.
114
+ *
115
+ * Marks each sibling of the portal host, and restores exactly what was there
116
+ * before — an element that was *already* `aria-hidden` for its own reasons
117
+ * must stay that way after the dialog closes, so the previous value is saved
118
+ * rather than the attribute simply removed.
119
+ *
120
+ * Only siblings of the host, not the whole page: a second modal opening over
121
+ * the first must not hide it, and it will not, because it appends its own host
122
+ * after the first one and only touches what is beside it.
123
+ */
124
+ function hideSiblingsFrom(host) {
125
+ const parent = host.parentElement;
126
+ if (parent === null)
127
+ return () => { };
128
+ const saved = [];
129
+ for (const sibling of parent.children) {
130
+ if (sibling === host)
131
+ continue;
132
+ // A live region announcing outside the dialog (a toast) is deliberate.
133
+ if (sibling.hasAttribute("data-nebula-live"))
134
+ continue;
135
+ saved.push([sibling, sibling.getAttribute("aria-hidden")]);
136
+ sibling.setAttribute("aria-hidden", "true");
137
+ }
138
+ return () => {
139
+ for (const [element, previous] of saved) {
140
+ if (previous === null)
141
+ element.removeAttribute("aria-hidden");
142
+ else
143
+ element.setAttribute("aria-hidden", previous);
144
+ }
145
+ };
146
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Portal — mount a template outside the component's own DOM position.
3
+ *
4
+ * Overlays have to escape their parent. A Popover rendered where it is
5
+ * declared inherits every `overflow: hidden`, `transform` and `z-index` stack
6
+ * between it and the root, and any one of those will clip it or bury it. The
7
+ * fix is universal: render into `<body>` and position with fixed coordinates.
8
+ *
9
+ * Aurora's `render()` already mounts a `TemplateResult` anywhere and hands
10
+ * back a disposer, so a portal is that call plus a host element to hold the
11
+ * content and the discipline to tear both down together.
12
+ */
13
+ import { type TemplateResult } from "@c9up/aurora";
14
+ export interface PortalOptions {
15
+ /** Where to mount. Defaults to `document.body`. */
16
+ container?: () => Element | null;
17
+ /** Extra classes for the host element. */
18
+ class?: string;
19
+ }
20
+ export interface Portal {
21
+ /** The element holding the portalled content. */
22
+ readonly host: HTMLElement;
23
+ /** Unmount the content and remove the host. Safe to call twice. */
24
+ close(): void;
25
+ }
26
+ /**
27
+ * Mount `content` into a fresh host element appended to the container.
28
+ *
29
+ * A host element rather than mounting straight into `<body>`: it gives the
30
+ * content one node to be removed as a unit, keeps sibling portals from
31
+ * interleaving, and gives the layer a place to hang `data-nebula-portal` so a
32
+ * test — or a developer in the inspector — can see what put it there.
33
+ */
34
+ export declare function portal(content: TemplateResult, options?: PortalOptions): Portal;
@@ -0,0 +1,42 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Portal — mount a template outside the component's own DOM position.
4
+ *
5
+ * Overlays have to escape their parent. A Popover rendered where it is
6
+ * declared inherits every `overflow: hidden`, `transform` and `z-index` stack
7
+ * between it and the root, and any one of those will clip it or bury it. The
8
+ * fix is universal: render into `<body>` and position with fixed coordinates.
9
+ *
10
+ * Aurora's `render()` already mounts a `TemplateResult` anywhere and hands
11
+ * back a disposer, so a portal is that call plus a host element to hold the
12
+ * content and the discipline to tear both down together.
13
+ */
14
+ import { render } from "@c9up/aurora";
15
+ /**
16
+ * Mount `content` into a fresh host element appended to the container.
17
+ *
18
+ * A host element rather than mounting straight into `<body>`: it gives the
19
+ * content one node to be removed as a unit, keeps sibling portals from
20
+ * interleaving, and gives the layer a place to hang `data-nebula-portal` so a
21
+ * test — or a developer in the inspector — can see what put it there.
22
+ */
23
+ export function portal(content, options = {}) {
24
+ const container = options.container?.() ?? document.body;
25
+ const host = document.createElement("div");
26
+ host.setAttribute("data-nebula-portal", "");
27
+ if (options.class !== undefined)
28
+ host.className = options.class;
29
+ container.appendChild(host);
30
+ const dispose = render(content, host);
31
+ let closed = false;
32
+ return {
33
+ host,
34
+ close() {
35
+ if (closed)
36
+ return;
37
+ closed = true;
38
+ dispose();
39
+ host.remove();
40
+ },
41
+ };
42
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Presence — keep a node mounted until its exit animation has finished.
3
+ *
4
+ * Closing an overlay by removing it from the DOM cancels any exit animation:
5
+ * the node is gone before the first frame plays. Radix solves this with a
6
+ * `Presence` wrapper, and shadcn's markup depends on it — the `data-[state=
7
+ * closed]:animate-out` utilities on every overlay assume something is holding
8
+ * the node on screen while that animation runs.
9
+ *
10
+ * `presence()` owns a `state` signal that leads the DOM: it flips to `closed`
11
+ * immediately so the exit animation can start, and only reports `mounted`
12
+ * false once the animation ends. A surface with no animation unmounts on the
13
+ * next tick, so the abstraction costs nothing when unused.
14
+ */
15
+ import { type ReadSignal } from "@c9up/aurora";
16
+ export type PresenceState = "open" | "closed";
17
+ export interface Presence {
18
+ /** Should the node be in the DOM right now? */
19
+ readonly mounted: ReadSignal<boolean>;
20
+ /** The value for `data-state` — drives the enter/exit utility classes. */
21
+ readonly state: ReadSignal<PresenceState>;
22
+ /** Show the node. Cancels a pending unmount. */
23
+ open(): void;
24
+ /** Start the exit animation. Unmount follows when it finishes. */
25
+ close(): void;
26
+ /**
27
+ * Hand over the element once it is live. Presence watches it for the end of
28
+ * the exit animation; until it is given one, close() unmounts immediately.
29
+ */
30
+ attach(element: HTMLElement | null): void;
31
+ /** Drop listeners and timers. */
32
+ dispose(): void;
33
+ }
34
+ export declare function presence(initiallyOpen?: boolean): Presence;
35
+ /**
36
+ * Run `done` once the element's exit animation finishes, or immediately when
37
+ * there is none. Returns a cancel function.
38
+ *
39
+ * The portalled surfaces do not use `presence()` — they own their own mounting
40
+ * through `portal()`, so a second `mounted` signal would just be a shadow of
41
+ * the portal's own lifetime. What they do need is this: the answer to "may I
42
+ * remove the node yet". Call it *after* flipping `data-state` to `closed`, so
43
+ * the computed style already reflects the closing rules.
44
+ */
45
+ export declare function onExitFinished(element: HTMLElement, done: () => void): () => void;
@@ -0,0 +1,152 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Presence — keep a node mounted until its exit animation has finished.
4
+ *
5
+ * Closing an overlay by removing it from the DOM cancels any exit animation:
6
+ * the node is gone before the first frame plays. Radix solves this with a
7
+ * `Presence` wrapper, and shadcn's markup depends on it — the `data-[state=
8
+ * closed]:animate-out` utilities on every overlay assume something is holding
9
+ * the node on screen while that animation runs.
10
+ *
11
+ * `presence()` owns a `state` signal that leads the DOM: it flips to `closed`
12
+ * immediately so the exit animation can start, and only reports `mounted`
13
+ * false once the animation ends. A surface with no animation unmounts on the
14
+ * next tick, so the abstraction costs nothing when unused.
15
+ */
16
+ import { signal } from "@c9up/aurora";
17
+ export function presence(initiallyOpen = false) {
18
+ const mounted = signal(initiallyOpen);
19
+ const state = signal(initiallyOpen ? "open" : "closed");
20
+ let element = null;
21
+ let pendingUnmount = false;
22
+ function finishClose() {
23
+ if (!pendingUnmount)
24
+ return;
25
+ pendingUnmount = false;
26
+ mounted(false);
27
+ }
28
+ /**
29
+ * The animation is only ours if it played on the surface itself.
30
+ *
31
+ * Events bubble, so a child's animation would otherwise unmount the parent
32
+ * mid-flight — a spinner inside a closing dialog is enough to trigger it.
33
+ */
34
+ function onAnimationEnd(event) {
35
+ if (event.target !== element)
36
+ return;
37
+ finishClose();
38
+ }
39
+ function detach() {
40
+ if (element === null)
41
+ return;
42
+ element.removeEventListener("animationend", onAnimationEnd);
43
+ element.removeEventListener("animationcancel", onAnimationEnd);
44
+ element.removeEventListener("transitionend", onAnimationEnd);
45
+ element.removeEventListener("transitioncancel", onAnimationEnd);
46
+ element = null;
47
+ }
48
+ return {
49
+ mounted,
50
+ state,
51
+ open() {
52
+ pendingUnmount = false;
53
+ mounted(true);
54
+ state("open");
55
+ },
56
+ close() {
57
+ if (!mounted())
58
+ return;
59
+ state("closed");
60
+ // No element yet means nothing can be animating — unmount now rather
61
+ // than waiting for an event that will never arrive.
62
+ if (element === null) {
63
+ mounted(false);
64
+ return;
65
+ }
66
+ pendingUnmount = true;
67
+ if (!isAnimating(element))
68
+ finishClose();
69
+ },
70
+ attach(next) {
71
+ detach();
72
+ element = next;
73
+ if (element === null)
74
+ return;
75
+ element.addEventListener("animationend", onAnimationEnd);
76
+ element.addEventListener("animationcancel", onAnimationEnd);
77
+ element.addEventListener("transitionend", onAnimationEnd);
78
+ element.addEventListener("transitioncancel", onAnimationEnd);
79
+ },
80
+ dispose() {
81
+ pendingUnmount = false;
82
+ detach();
83
+ },
84
+ };
85
+ }
86
+ /**
87
+ * Run `done` once the element's exit animation finishes, or immediately when
88
+ * there is none. Returns a cancel function.
89
+ *
90
+ * The portalled surfaces do not use `presence()` — they own their own mounting
91
+ * through `portal()`, so a second `mounted` signal would just be a shadow of
92
+ * the portal's own lifetime. What they do need is this: the answer to "may I
93
+ * remove the node yet". Call it *after* flipping `data-state` to `closed`, so
94
+ * the computed style already reflects the closing rules.
95
+ */
96
+ export function onExitFinished(element, done) {
97
+ if (!isAnimating(element)) {
98
+ done();
99
+ return () => { };
100
+ }
101
+ function finish(event) {
102
+ // Bubbled events from children would cut the parent's exit short.
103
+ if (event.target !== element)
104
+ return;
105
+ cancel();
106
+ done();
107
+ }
108
+ function cancel() {
109
+ element.removeEventListener("animationend", finish);
110
+ element.removeEventListener("animationcancel", finish);
111
+ element.removeEventListener("transitionend", finish);
112
+ element.removeEventListener("transitioncancel", finish);
113
+ }
114
+ element.addEventListener("animationend", finish);
115
+ element.addEventListener("animationcancel", finish);
116
+ element.addEventListener("transitionend", finish);
117
+ element.addEventListener("transitioncancel", finish);
118
+ return cancel;
119
+ }
120
+ /**
121
+ * Is an exit animation actually running?
122
+ *
123
+ * Read after `data-state` has flipped, so the computed style already reflects
124
+ * the closing rules. `animationName: none` and a zero transition duration both
125
+ * mean there is nothing to wait for, and waiting anyway would strand the node
126
+ * in the DOM forever — the failure mode this check exists to prevent.
127
+ */
128
+ function isAnimating(element) {
129
+ if (typeof getComputedStyle !== "function")
130
+ return false;
131
+ const style = getComputedStyle(element);
132
+ const hasAnimation = style.animationName !== "" && style.animationName !== "none";
133
+ if (hasAnimation)
134
+ return true;
135
+ return parseDuration(style.transitionDuration) > 0;
136
+ }
137
+ /** Longest duration in a comma-separated CSS time list, in milliseconds. */
138
+ function parseDuration(value) {
139
+ let longest = 0;
140
+ for (const part of value.split(",")) {
141
+ const trimmed = part.trim();
142
+ if (trimmed === "")
143
+ continue;
144
+ const numeric = Number.parseFloat(trimmed);
145
+ if (Number.isNaN(numeric))
146
+ continue;
147
+ const ms = trimmed.endsWith("ms") ? numeric : numeric * 1000;
148
+ if (ms > longest)
149
+ longest = ms;
150
+ }
151
+ return longest;
152
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Roving focus — arrow-key navigation over a group of items.
3
+ *
4
+ * The WAI-ARIA composite-widget pattern: a menu, listbox, tab list or toolbar
5
+ * is *one* tab stop, and arrows move within it. Exactly one item carries
6
+ * `tabindex="0"` at a time; the rest are `-1`. That is what stops a 40-item
7
+ * menu from swallowing 40 presses of Tab.
8
+ *
9
+ * Used by DropdownMenu, ContextMenu, Menubar, Select, Combobox, Command, Tabs
10
+ * and ToggleGroup. The differences between them are configuration, not
11
+ * behaviour: orientation, whether the ends wrap, and whether moving focus also
12
+ * selects.
13
+ *
14
+ * Items are found by querying the container on each keypress rather than being
15
+ * registered up front. Menu contents are reactive — filtered by a Command
16
+ * input, grown by a loaded page — and a registry captured at mount goes stale
17
+ * the moment they change. The query costs a few microseconds per keypress,
18
+ * which is not a budget worth optimising against correctness.
19
+ */
20
+ export type Orientation = "vertical" | "horizontal" | "both";
21
+ export interface RovingFocusOptions {
22
+ /** The group element. Read lazily — it mounts after the group registers. */
23
+ container: () => HTMLElement | null;
24
+ /** Which descendants are navigable. Default: nebula's own item marker. */
25
+ itemSelector?: string;
26
+ /** Which arrows move focus. Default `"vertical"`. */
27
+ orientation?: Orientation;
28
+ /** Wrap from last to first and back. Default `true`. */
29
+ loop?: boolean;
30
+ /** Enter / Space on the focused item. */
31
+ onSelect?: (item: HTMLElement, event: KeyboardEvent) => void;
32
+ /** Focus moved to a new item — used by menus that highlight on navigate. */
33
+ onFocusChange?: (item: HTMLElement) => void;
34
+ }
35
+ export interface RovingFocus {
36
+ /** Focus the first enabled item. */
37
+ focusFirst(): void;
38
+ /** Focus the last enabled item. */
39
+ focusLast(): void;
40
+ /** The navigable items, in document order. */
41
+ items(): HTMLElement[];
42
+ /** Re-apply `tabindex` after the item list changes. */
43
+ sync(): void;
44
+ /** Detach the key handler. Safe to call twice. */
45
+ destroy(): void;
46
+ }
47
+ export declare function rovingFocus(options: RovingFocusOptions): RovingFocus;