@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,289 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Floating placement — where a popover, menu, select or tooltip goes.
4
+ *
5
+ * This is the piece Radix delegates to `@floating-ui/dom`. nebula computes it
6
+ * instead, because the alternative is a runtime dependency in a workspace
7
+ * whose stated position is that `cn` was worth reimplementing rather than
8
+ * pulling in clsx and tailwind-merge.
9
+ *
10
+ * Scope is chosen to make that defensible. Floating UI is a general middleware
11
+ * pipeline; this is the four behaviours the shadcn component set actually
12
+ * uses — `offset`, `flip`, `shift`, and arrow centring — plus the available
13
+ * height a scrollable menu needs for its `max-height`. No middleware
14
+ * abstraction, no virtual elements, no `autoPlacement`.
15
+ *
16
+ * The geometry lives in `resolvePosition`, a pure function over rectangles.
17
+ * Everything that touches the DOM — measuring, writing styles, watching for
18
+ * scroll — is in `autoPosition` around it. That split is what lets the flip
19
+ * and shift rules be tested exhaustively without a browser, which matters
20
+ * because they are where placement bugs actually live.
21
+ *
22
+ * Coordinates are viewport-relative and meant for `position: fixed`. Fixed
23
+ * positioning is what lets a portalled surface ignore every transformed or
24
+ * clipping ancestor between it and the root; an absolutely positioned popover
25
+ * has to find an offset parent, and gets it wrong the moment one appears.
26
+ */
27
+ const DEFAULT_OFFSET = 4;
28
+ const DEFAULT_PADDING = 8;
29
+ /**
30
+ * Compute where the surface goes. Pure — no DOM, no side effects.
31
+ *
32
+ * Order matters and matches Floating UI's: offset, then flip, then shift.
33
+ * Shifting before flipping would slide a surface halfway off the anchor to
34
+ * make room on a side that should have been abandoned entirely.
35
+ */
36
+ export function resolvePosition(anchor, floating, viewport, options = {}) {
37
+ const offset = options.offset ?? DEFAULT_OFFSET;
38
+ const padding = options.padding ?? DEFAULT_PADDING;
39
+ const requested = options.placement ?? "bottom";
40
+ const [physicalSide, physicalAlign] = splitPlacement(requested);
41
+ const [requestedSide, align] = options.rtl === true
42
+ ? mirrorForRtl(physicalSide, physicalAlign)
43
+ : [physicalSide, physicalAlign];
44
+ const side = options.flip === false
45
+ ? requestedSide
46
+ : bestSide(requestedSide, anchor, floating, viewport, offset, padding);
47
+ let { x, y } = coordsFor(side, align, anchor, floating, offset);
48
+ if (options.shift !== false) {
49
+ if (isVertical(side)) {
50
+ x = clamp(x, padding, viewport.width - floating.width - padding);
51
+ }
52
+ else {
53
+ y = clamp(y, padding, viewport.height - floating.height - padding);
54
+ }
55
+ }
56
+ const position = {
57
+ x,
58
+ y,
59
+ placement: align === "center" ? side : `${side}-${align}`,
60
+ side,
61
+ align,
62
+ availableHeight: spaceOn(side, anchor, viewport, offset, padding),
63
+ };
64
+ if (options.arrowSize === undefined)
65
+ return position;
66
+ return {
67
+ ...position,
68
+ arrow: arrowOffset(side, anchor, floating, { x, y }, options.arrowSize, padding),
69
+ };
70
+ }
71
+ // ─── geometry ────────────────────────────────────────────────────────
72
+ function splitPlacement(placement) {
73
+ const dash = placement.indexOf("-");
74
+ if (dash === -1)
75
+ return [toSide(placement), "center"];
76
+ return [toSide(placement.slice(0, dash)), toAlign(placement.slice(dash + 1))];
77
+ }
78
+ /**
79
+ * Narrow a substring back to `Side` / `Align`.
80
+ *
81
+ * `splitPlacement` slices a string the type system already proved is a valid
82
+ * `Placement`, but slicing erases that. A guard restores it without a cast,
83
+ * and the fallback keeps a hand-built placement string from crashing the
84
+ * layout — it lands on the default instead.
85
+ */
86
+ function toSide(value) {
87
+ return value === "top" || value === "right" || value === "left"
88
+ ? value
89
+ : "bottom";
90
+ }
91
+ function toAlign(value) {
92
+ return value === "start" || value === "end" ? value : "center";
93
+ }
94
+ function isVertical(side) {
95
+ return side === "top" || side === "bottom";
96
+ }
97
+ /**
98
+ * Flip a placement across the vertical axis.
99
+ *
100
+ * Which half moves depends on the side. For `left` / `right` the *side* is the
101
+ * horizontal thing, so it swaps and the alignment (top/bottom) is untouched.
102
+ * For `top` / `bottom` the side is unaffected and it is the *alignment* that
103
+ * refers to a horizontal edge, so that swaps instead. Mirroring both would put
104
+ * a `bottom-start` menu back where it started.
105
+ */
106
+ function mirrorForRtl(side, align) {
107
+ if (!isVertical(side))
108
+ return [opposite(side), align];
109
+ if (align === "center")
110
+ return [side, align];
111
+ return [side, align === "start" ? "end" : "start"];
112
+ }
113
+ function opposite(side) {
114
+ if (side === "top")
115
+ return "bottom";
116
+ if (side === "bottom")
117
+ return "top";
118
+ if (side === "left")
119
+ return "right";
120
+ return "left";
121
+ }
122
+ /** Top-left corner of the surface for a given side and alignment. */
123
+ function coordsFor(side, align, anchor, floating, offset) {
124
+ if (isVertical(side)) {
125
+ const y = side === "bottom"
126
+ ? anchor.y + anchor.height + offset
127
+ : anchor.y - floating.height - offset;
128
+ return {
129
+ x: alignedStart(anchor.x, anchor.width, floating.width, align),
130
+ y,
131
+ };
132
+ }
133
+ const x = side === "right"
134
+ ? anchor.x + anchor.width + offset
135
+ : anchor.x - floating.width - offset;
136
+ return {
137
+ x,
138
+ y: alignedStart(anchor.y, anchor.height, floating.height, align),
139
+ };
140
+ }
141
+ /** Where the surface starts along the cross axis, for one alignment. */
142
+ function alignedStart(anchorStart, anchorSize, floatingSize, align) {
143
+ if (align === "start")
144
+ return anchorStart;
145
+ if (align === "end")
146
+ return anchorStart + anchorSize - floatingSize;
147
+ return anchorStart + (anchorSize - floatingSize) / 2;
148
+ }
149
+ /** Room between the anchor and the viewport edge on one side. */
150
+ function spaceOn(side, anchor, viewport, offset, padding) {
151
+ const raw = side === "top"
152
+ ? anchor.y
153
+ : side === "bottom"
154
+ ? viewport.height - (anchor.y + anchor.height)
155
+ : side === "left"
156
+ ? anchor.x
157
+ : viewport.width - (anchor.x + anchor.width);
158
+ return Math.max(0, raw - offset - padding);
159
+ }
160
+ /**
161
+ * Keep the requested side unless the opposite one is genuinely better.
162
+ *
163
+ * "Better" is not "has more room": flipping a menu that overflows by two
164
+ * pixels is more disruptive than letting it shift. So the opposite side has to
165
+ * both fit and beat the requested one. When neither fits, the roomier side
166
+ * wins and `availableHeight` lets the surface scroll inside it.
167
+ */
168
+ function bestSide(requested, anchor, floating, viewport, offset, padding) {
169
+ const needed = isVertical(requested) ? floating.height : floating.width;
170
+ const here = spaceOn(requested, anchor, viewport, offset, padding);
171
+ if (here >= needed)
172
+ return requested;
173
+ const other = opposite(requested);
174
+ const there = spaceOn(other, anchor, viewport, offset, padding);
175
+ if (there >= needed)
176
+ return other;
177
+ return there > here ? other : requested;
178
+ }
179
+ /**
180
+ * Centre the arrow on the anchor, then pull it back off the corners.
181
+ *
182
+ * The arrow tracks the *anchor*, not the surface: once shift has slid the
183
+ * surface sideways, an arrow centred on the surface points at empty space.
184
+ * The clamp keeps it clear of the rounded corners, where it would otherwise
185
+ * poke out of the border radius.
186
+ */
187
+ function arrowOffset(side, anchor, floating, coords, arrowSize, padding) {
188
+ if (isVertical(side)) {
189
+ const centre = anchor.x + anchor.width / 2 - coords.x - arrowSize / 2;
190
+ const limit = floating.width - arrowSize - padding;
191
+ return { x: clamp(centre, padding, Math.max(padding, limit)) };
192
+ }
193
+ const centre = anchor.y + anchor.height / 2 - coords.y - arrowSize / 2;
194
+ const limit = floating.height - arrowSize - padding;
195
+ return { y: clamp(centre, padding, Math.max(padding, limit)) };
196
+ }
197
+ function clamp(value, min, max) {
198
+ // `max` can fall below `min` when the surface is wider than the viewport.
199
+ // Pinning to `min` keeps the leading edge visible, which is the readable
200
+ // half; honouring `max` instead would push the start off screen.
201
+ if (max < min)
202
+ return min;
203
+ return Math.min(Math.max(value, min), max);
204
+ }
205
+ /**
206
+ * Position `floating` against `anchor` and keep it there.
207
+ *
208
+ * Updates run on scroll (captured, so ancestor scroll containers are covered
209
+ * without walking the tree to find them) and on resize, plus whenever either
210
+ * element changes size where `ResizeObserver` exists. A menu whose content
211
+ * loads in and grows would otherwise stay positioned for its old height.
212
+ */
213
+ export function autoPosition(anchor, floating, options = {}) {
214
+ function update() {
215
+ const anchorRect = anchor.getBoundingClientRect();
216
+ // Read per update, not once: a `dir` attribute can be toggled at runtime
217
+ // by a language switcher, and a surface open across that switch has to
218
+ // move with it.
219
+ const rtl = options.rtl ?? isRightToLeft(anchor);
220
+ if (options.matchWidth === true) {
221
+ floating.style.width = `${anchorRect.width}px`;
222
+ }
223
+ const floatingRect = floating.getBoundingClientRect();
224
+ const position = resolvePosition(anchorRect, floatingRect, { width: window.innerWidth, height: window.innerHeight }, { ...options, rtl });
225
+ floating.style.position = "fixed";
226
+ floating.style.left = `${Math.round(position.x)}px`;
227
+ floating.style.top = `${Math.round(position.y)}px`;
228
+ // Exposed as custom properties so the *stylesheet* decides what to do with
229
+ // them. shadcn's menus read the available height through
230
+ // `max-h-(--nebula-available-height)`, which keeps the scroll decision in
231
+ // CSS rather than hardcoding a pixel max-height from script.
232
+ floating.style.setProperty("--nebula-available-height", `${Math.round(position.availableHeight)}px`);
233
+ floating.style.setProperty("--nebula-anchor-width", `${Math.round(anchorRect.width)}px`);
234
+ floating.setAttribute("data-side", position.side);
235
+ floating.setAttribute("data-align", position.align);
236
+ const arrowEl = options.arrow?.();
237
+ if (arrowEl && position.arrow) {
238
+ const { x, y } = position.arrow;
239
+ arrowEl.style.position = "absolute";
240
+ arrowEl.style.left = x === undefined ? "" : `${Math.round(x)}px`;
241
+ arrowEl.style.top = y === undefined ? "" : `${Math.round(y)}px`;
242
+ }
243
+ options.onPositioned?.(position);
244
+ }
245
+ update();
246
+ const onScrollOrResize = () => update();
247
+ window.addEventListener("scroll", onScrollOrResize, true);
248
+ window.addEventListener("resize", onScrollOrResize);
249
+ const observer = observeSizes([anchor, floating], update);
250
+ let stopped = false;
251
+ return {
252
+ update,
253
+ stop() {
254
+ if (stopped)
255
+ return;
256
+ stopped = true;
257
+ window.removeEventListener("scroll", onScrollOrResize, true);
258
+ window.removeEventListener("resize", onScrollOrResize);
259
+ observer?.disconnect();
260
+ },
261
+ };
262
+ }
263
+ /**
264
+ * Is this element laid out right to left?
265
+ *
266
+ * The computed style rather than the `dir` attribute, so an element inheriting
267
+ * the direction from `<html dir="rtl">` is answered correctly without every
268
+ * anchor having to carry the attribute itself.
269
+ */
270
+ function isRightToLeft(element) {
271
+ if (typeof getComputedStyle !== "function")
272
+ return false;
273
+ return getComputedStyle(element).direction === "rtl";
274
+ }
275
+ /**
276
+ * Watch elements for size changes, where the platform supports it.
277
+ *
278
+ * happy-dom (nebula's unit-test environment) has no `ResizeObserver`, and the
279
+ * scroll and resize listeners already cover the common cases, so its absence
280
+ * degrades rather than throws.
281
+ */
282
+ function observeSizes(elements, onResize) {
283
+ if (typeof ResizeObserver !== "function")
284
+ return null;
285
+ const observer = new ResizeObserver(onResize);
286
+ for (const element of elements)
287
+ observer.observe(element);
288
+ return observer;
289
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Floating surface — the whole lifecycle of a popover-shaped overlay.
3
+ *
4
+ * Popover, Tooltip, HoverCard, DropdownMenu, ContextMenu, Menubar, Select and
5
+ * Combobox are the same machine with different contents and different opening
6
+ * gestures. That machine is: portal out of the tree, position against an
7
+ * anchor, register as a dismissable layer, optionally trap focus, and on close
8
+ * let the exit animation finish before removing anything.
9
+ *
10
+ * Five primitives in a fixed order, and the order is the part that is easy to
11
+ * get wrong. The layer must register *after* the content exists, or its
12
+ * containment check has nothing to test against and the opening click closes
13
+ * it again. The focus trap must run after the first position pass, or it
14
+ * focuses a surface still sitting at (0,0) and the browser scrolls to it.
15
+ *
16
+ * Written once here so eight components cannot each get that order slightly
17
+ * different.
18
+ */
19
+ import { type TemplateResult } from "@c9up/aurora";
20
+ import { type DismissReason } from "./dismissable.js";
21
+ import { type Placement } from "./floating.js";
22
+ export interface FloatingSurfaceOptions {
23
+ /** What the surface is positioned against — usually the trigger. */
24
+ anchor: () => HTMLElement | null;
25
+ /** Should the surface be showing? Read reactively. */
26
+ open: () => boolean;
27
+ /** The surface wants to close. Flip whatever drives `open`. */
28
+ onClose: (reason: DismissReason) => void;
29
+ /** The surface's markup. Built fresh on each open. */
30
+ content: () => TemplateResult;
31
+ placement?: Placement;
32
+ offset?: number;
33
+ /** Size the surface to the anchor. Select and Combobox want this. */
34
+ matchWidth?: boolean;
35
+ /** Keep keyboard focus inside while open. */
36
+ trapFocus?: boolean;
37
+ /** Move focus into the surface on open. Implied by `trapFocus`. */
38
+ autoFocus?: boolean;
39
+ /** Where focus goes first. Defaults to the first focusable element. */
40
+ initialFocus?: (content: HTMLElement) => HTMLElement | null;
41
+ escapeKey?: boolean;
42
+ outsidePointer?: boolean;
43
+ /** Default `false` — a surface that opened on hover should not steal focus. */
44
+ outsideFocus?: boolean;
45
+ /** The surface is live and positioned. Wire item handlers here. */
46
+ onOpened?: (content: HTMLElement) => void;
47
+ /** The surface is about to be removed. */
48
+ onClosed?: () => void;
49
+ }
50
+ /**
51
+ * Wire a floating surface into the surrounding component.
52
+ *
53
+ * Call from a component setup. It registers its own mount and unmount hooks,
54
+ * so there is nothing to dispose by hand.
55
+ */
56
+ export declare function floatingSurface(options: FloatingSurfaceOptions): void;
@@ -0,0 +1,142 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Floating surface — the whole lifecycle of a popover-shaped overlay.
4
+ *
5
+ * Popover, Tooltip, HoverCard, DropdownMenu, ContextMenu, Menubar, Select and
6
+ * Combobox are the same machine with different contents and different opening
7
+ * gestures. That machine is: portal out of the tree, position against an
8
+ * anchor, register as a dismissable layer, optionally trap focus, and on close
9
+ * let the exit animation finish before removing anything.
10
+ *
11
+ * Five primitives in a fixed order, and the order is the part that is easy to
12
+ * get wrong. The layer must register *after* the content exists, or its
13
+ * containment check has nothing to test against and the opening click closes
14
+ * it again. The focus trap must run after the first position pass, or it
15
+ * focuses a surface still sitting at (0,0) and the browser scrolls to it.
16
+ *
17
+ * Written once here so eight components cannot each get that order slightly
18
+ * different.
19
+ */
20
+ import { effect, onMount } from "@c9up/aurora";
21
+ import { dismissable, } from "./dismissable.js";
22
+ import { autoPosition } from "./floating.js";
23
+ import { firstFocusable, focusSilently } from "./focusable.js";
24
+ import { focusTrap } from "./focusTrap.js";
25
+ import { portal } from "./portal.js";
26
+ import { onExitFinished } from "./presence.js";
27
+ /**
28
+ * Wire a floating surface into the surrounding component.
29
+ *
30
+ * Call from a component setup. It registers its own mount and unmount hooks,
31
+ * so there is nothing to dispose by hand.
32
+ */
33
+ export function floatingSurface(options) {
34
+ let live = null;
35
+ function show() {
36
+ if (live !== null) {
37
+ // Already open and mid-exit: cancel the teardown and reuse the node
38
+ // rather than stacking a second copy on top of the one fading out.
39
+ live.cancelExit?.();
40
+ live.cancelExit = null;
41
+ live.element.setAttribute("data-state", "open");
42
+ return;
43
+ }
44
+ const anchor = options.anchor();
45
+ if (anchor === null)
46
+ return;
47
+ const mount = portal(options.content());
48
+ const element = mount.host.firstElementChild;
49
+ if (!(element instanceof HTMLElement)) {
50
+ mount.close();
51
+ return;
52
+ }
53
+ element.setAttribute("data-state", "open");
54
+ const position = autoPosition(anchor, element, {
55
+ placement: options.placement,
56
+ offset: options.offset,
57
+ matchWidth: options.matchWidth,
58
+ });
59
+ const layer = dismissable({
60
+ element: () => element,
61
+ exclude: () => [anchor],
62
+ onDismiss: options.onClose,
63
+ escapeKey: options.escapeKey,
64
+ outsidePointer: options.outsidePointer,
65
+ outsideFocus: options.outsideFocus ?? false,
66
+ });
67
+ let trap = null;
68
+ if (options.trapFocus === true) {
69
+ trap = focusTrap(element, {
70
+ initialFocus: () => options.initialFocus?.(element) ?? null,
71
+ });
72
+ }
73
+ else if (options.autoFocus === true) {
74
+ focusSilently(options.initialFocus?.(element) ?? firstFocusable(element));
75
+ }
76
+ live = { mount, anchor, element, position, layer, trap, cancelExit: null };
77
+ options.onOpened?.(element);
78
+ }
79
+ function hide() {
80
+ const current = live;
81
+ if (current === null)
82
+ return;
83
+ // Detach behaviour first, then animate out. A surface that is fading
84
+ // should not still be answering Escape or repositioning itself.
85
+ current.layer.remove();
86
+ current.position.stop();
87
+ current.trap?.release();
88
+ returnFocusToAnchor(current.element, current.anchor);
89
+ current.element.setAttribute("data-state", "closed");
90
+ options.onClosed?.();
91
+ current.cancelExit = onExitFinished(current.element, () => {
92
+ current.mount.close();
93
+ if (live === current)
94
+ live = null;
95
+ });
96
+ }
97
+ /**
98
+ * Hand focus back to the anchor when the surface held it.
99
+ *
100
+ * Dismissal by keyboard leaves focus inside a subtree that is about to be
101
+ * removed; without this it falls to `<body>` and the next Tab restarts at
102
+ * the top of the page. `dismissable` handles Escape from a *captured*
103
+ * document listener and stops propagation, so a component cannot do this in
104
+ * its own keydown handler — the event never reaches it.
105
+ *
106
+ * Guarded on containment, so a dismissal caused by clicking somewhere else
107
+ * does not steal focus back from wherever the user just put it. A trapped
108
+ * surface has already restored focus by the time this runs, and the guard
109
+ * makes it a no-op there too.
110
+ */
111
+ function returnFocusToAnchor(surface, anchor) {
112
+ const active = document.activeElement;
113
+ if (!(active instanceof Node) || !surface.contains(active))
114
+ return;
115
+ focusSilently(anchor);
116
+ }
117
+ function teardown() {
118
+ const current = live;
119
+ if (current === null)
120
+ return;
121
+ live = null;
122
+ current.cancelExit?.();
123
+ current.layer.remove();
124
+ current.position.stop();
125
+ current.trap?.release();
126
+ current.mount.close();
127
+ }
128
+ onMount(() => {
129
+ const stop = effect(() => {
130
+ if (options.open())
131
+ show();
132
+ else
133
+ hide();
134
+ });
135
+ // The component is going away, so the exit animation has nobody left to
136
+ // play for — drop the node immediately rather than orphaning it in body.
137
+ return () => {
138
+ stop();
139
+ teardown();
140
+ };
141
+ });
142
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Focus trap — keyboard focus cannot leave a subtree while it is active.
3
+ *
4
+ * Required by every modal surface (Dialog, AlertDialog, Sheet, Drawer). The
5
+ * WAI-ARIA dialog pattern is specific about it: Tab from the last focusable
6
+ * element wraps to the first, Shift+Tab from the first wraps to the last, and
7
+ * focus returns to whatever opened the dialog when it closes.
8
+ *
9
+ * Two mechanisms, deliberately both:
10
+ *
11
+ * - A `keydown` handler implements the wrap. This is the path that runs for
12
+ * ordinary keyboard use, and it is the only one that can put focus on the
13
+ * *correct* end of the trap.
14
+ * - A `focusin` handler on the document catches everything else — a click on
15
+ * the page behind, a screen reader jumping by landmark, a browser find bar
16
+ * handing focus back somewhere else. It pulls focus back into the trap.
17
+ *
18
+ * The keydown handler alone leaks; the focusin handler alone produces the
19
+ * classic bug where Tab escapes and is yanked back a frame later, which reads
20
+ * as a flicker and confuses assistive technology.
21
+ */
22
+ export interface FocusTrapOptions {
23
+ /**
24
+ * Where focus goes when the trap activates. Defaults to the first focusable
25
+ * descendant, then the container itself.
26
+ */
27
+ initialFocus?: () => HTMLElement | null;
28
+ /**
29
+ * Where focus goes when it releases. Defaults to whatever was focused when
30
+ * the trap activated — which is nearly always the trigger.
31
+ */
32
+ returnFocus?: () => HTMLElement | null;
33
+ /** Skip moving focus on activation. For surfaces that focus themselves. */
34
+ skipInitialFocus?: boolean;
35
+ }
36
+ export interface FocusTrap {
37
+ /** Tear the trap down and restore focus. Safe to call twice. */
38
+ release(): void;
39
+ }
40
+ /**
41
+ * Trap focus inside `container` until the returned handle is released.
42
+ *
43
+ * The container is made focusable with `tabindex="-1"` if it is not already,
44
+ * so an empty surface still has somewhere to put focus. Without it, focus
45
+ * would fall to `<body>` and the next Tab would land outside the trap.
46
+ */
47
+ export declare function focusTrap(container: HTMLElement, options?: FocusTrapOptions): FocusTrap;
@@ -0,0 +1,101 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Focus trap — keyboard focus cannot leave a subtree while it is active.
4
+ *
5
+ * Required by every modal surface (Dialog, AlertDialog, Sheet, Drawer). The
6
+ * WAI-ARIA dialog pattern is specific about it: Tab from the last focusable
7
+ * element wraps to the first, Shift+Tab from the first wraps to the last, and
8
+ * focus returns to whatever opened the dialog when it closes.
9
+ *
10
+ * Two mechanisms, deliberately both:
11
+ *
12
+ * - A `keydown` handler implements the wrap. This is the path that runs for
13
+ * ordinary keyboard use, and it is the only one that can put focus on the
14
+ * *correct* end of the trap.
15
+ * - A `focusin` handler on the document catches everything else — a click on
16
+ * the page behind, a screen reader jumping by landmark, a browser find bar
17
+ * handing focus back somewhere else. It pulls focus back into the trap.
18
+ *
19
+ * The keydown handler alone leaks; the focusin handler alone produces the
20
+ * classic bug where Tab escapes and is yanked back a frame later, which reads
21
+ * as a flicker and confuses assistive technology.
22
+ */
23
+ import { firstFocusable, focusableWithin, focusSilently } from "./focusable.js";
24
+ /**
25
+ * Trap focus inside `container` until the returned handle is released.
26
+ *
27
+ * The container is made focusable with `tabindex="-1"` if it is not already,
28
+ * so an empty surface still has somewhere to put focus. Without it, focus
29
+ * would fall to `<body>` and the next Tab would land outside the trap.
30
+ */
31
+ export function focusTrap(container, options = {}) {
32
+ const previouslyFocused = activeElement();
33
+ if (!container.hasAttribute("tabindex")) {
34
+ container.setAttribute("tabindex", "-1");
35
+ }
36
+ if (!options.skipInitialFocus) {
37
+ const target = options.initialFocus?.() ?? firstFocusable(container) ?? container;
38
+ focusSilently(target);
39
+ }
40
+ function onKeyDown(event) {
41
+ if (event.key !== "Tab")
42
+ return;
43
+ const focusables = focusableWithin(container);
44
+ if (focusables.length === 0) {
45
+ // Nothing to cycle through: hold focus on the container so Tab cannot
46
+ // walk out into the page behind.
47
+ event.preventDefault();
48
+ focusSilently(container);
49
+ return;
50
+ }
51
+ const first = focusables[0];
52
+ const last = focusables[focusables.length - 1];
53
+ const current = activeElement();
54
+ if (event.shiftKey && (current === first || current === container)) {
55
+ event.preventDefault();
56
+ focusSilently(last);
57
+ }
58
+ else if (!event.shiftKey && current === last) {
59
+ event.preventDefault();
60
+ focusSilently(first);
61
+ }
62
+ }
63
+ /**
64
+ * Pull focus back when it lands outside by any route other than Tab.
65
+ *
66
+ * Guarded on `contains`, and only ever moves focus *into* the trap, so it
67
+ * cannot fight a nested trap: an inner dialog's container is a descendant
68
+ * of the outer one, so the outer handler sees the focus as already inside.
69
+ */
70
+ function onFocusIn(event) {
71
+ const target = event.target;
72
+ if (!(target instanceof Node))
73
+ return;
74
+ if (container.contains(target))
75
+ return;
76
+ focusSilently(firstFocusable(container) ?? container);
77
+ }
78
+ document.addEventListener("keydown", onKeyDown, true);
79
+ document.addEventListener("focusin", onFocusIn, true);
80
+ let released = false;
81
+ return {
82
+ release() {
83
+ if (released)
84
+ return;
85
+ released = true;
86
+ document.removeEventListener("keydown", onKeyDown, true);
87
+ document.removeEventListener("focusin", onFocusIn, true);
88
+ const target = options.returnFocus?.() ?? previouslyFocused;
89
+ // Only restore focus if it is still inside the trap. If something else
90
+ // has already claimed it — a second dialog, a toast action — stealing it
91
+ // back would be the wrong call.
92
+ if (container.contains(activeElement()))
93
+ focusSilently(target);
94
+ },
95
+ };
96
+ }
97
+ /** The focused element, narrowed to something we can call `.focus()` on. */
98
+ function activeElement() {
99
+ const active = document.activeElement;
100
+ return active instanceof HTMLElement ? active : null;
101
+ }