@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,176 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Roving focus — arrow-key navigation over a group of items.
4
+ *
5
+ * The WAI-ARIA composite-widget pattern: a menu, listbox, tab list or toolbar
6
+ * is *one* tab stop, and arrows move within it. Exactly one item carries
7
+ * `tabindex="0"` at a time; the rest are `-1`. That is what stops a 40-item
8
+ * menu from swallowing 40 presses of Tab.
9
+ *
10
+ * Used by DropdownMenu, ContextMenu, Menubar, Select, Combobox, Command, Tabs
11
+ * and ToggleGroup. The differences between them are configuration, not
12
+ * behaviour: orientation, whether the ends wrap, and whether moving focus also
13
+ * selects.
14
+ *
15
+ * Items are found by querying the container on each keypress rather than being
16
+ * registered up front. Menu contents are reactive — filtered by a Command
17
+ * input, grown by a loaded page — and a registry captured at mount goes stale
18
+ * the moment they change. The query costs a few microseconds per keypress,
19
+ * which is not a budget worth optimising against correctness.
20
+ */
21
+ import { focusSilently, isVisible } from "./focusable.js";
22
+ const DEFAULT_ITEM_SELECTOR = "[data-nebula-item]";
23
+ export function rovingFocus(options) {
24
+ const selector = options.itemSelector ?? DEFAULT_ITEM_SELECTOR;
25
+ const orientation = options.orientation ?? "vertical";
26
+ const loop = options.loop !== false;
27
+ function items() {
28
+ const container = options.container();
29
+ if (container === null)
30
+ return [];
31
+ const found = [];
32
+ for (const node of container.querySelectorAll(selector)) {
33
+ if (!(node instanceof HTMLElement))
34
+ continue;
35
+ // Disabled items stay in the DOM for layout and screen-reader context
36
+ // but must not be landed on — matching how a disabled menu item behaves
37
+ // everywhere else.
38
+ if (node.hasAttribute("data-disabled"))
39
+ continue;
40
+ if (node.getAttribute("aria-disabled") === "true")
41
+ continue;
42
+ if (!isVisible(node))
43
+ continue;
44
+ found.push(node);
45
+ }
46
+ return found;
47
+ }
48
+ /**
49
+ * One item at `tabindex="0"`, everything else at `-1`.
50
+ *
51
+ * The active one is whichever currently holds focus, falling back to the
52
+ * first. The fallback is what makes the group re-enterable by Tab after
53
+ * focus has left it.
54
+ */
55
+ function sync() {
56
+ const list = items();
57
+ const focused = document.activeElement;
58
+ const activeIndex = focused instanceof HTMLElement ? list.indexOf(focused) : -1;
59
+ const active = activeIndex === -1 ? 0 : activeIndex;
60
+ list.forEach((item, index) => {
61
+ item.tabIndex = index === active ? 0 : -1;
62
+ });
63
+ }
64
+ function move(delta) {
65
+ const list = items();
66
+ if (list.length === 0)
67
+ return;
68
+ const focused = document.activeElement;
69
+ const current = focused instanceof HTMLElement ? list.indexOf(focused) : -1;
70
+ const next = step(current, delta, list.length, loop);
71
+ if (next === null)
72
+ return;
73
+ const target = list[next];
74
+ if (target === undefined)
75
+ return;
76
+ for (const item of list)
77
+ item.tabIndex = -1;
78
+ target.tabIndex = 0;
79
+ focusSilently(target);
80
+ options.onFocusChange?.(target);
81
+ }
82
+ function focusEdge(edge) {
83
+ const list = items();
84
+ const target = edge === "first" ? list[0] : list[list.length - 1];
85
+ if (target === undefined)
86
+ return;
87
+ for (const item of list)
88
+ item.tabIndex = -1;
89
+ target.tabIndex = 0;
90
+ focusSilently(target);
91
+ options.onFocusChange?.(target);
92
+ }
93
+ function onKeyDown(event) {
94
+ const container = options.container();
95
+ if (container === null)
96
+ return;
97
+ if (event.target instanceof Node && !container.contains(event.target))
98
+ return;
99
+ // A modifier means the user is talking to the browser, not the widget.
100
+ if (event.altKey || event.ctrlKey || event.metaKey)
101
+ return;
102
+ const direction = directionFor(event.key, orientation);
103
+ if (direction !== 0) {
104
+ event.preventDefault();
105
+ move(direction);
106
+ return;
107
+ }
108
+ if (event.key === "Home") {
109
+ event.preventDefault();
110
+ focusEdge("first");
111
+ return;
112
+ }
113
+ if (event.key === "End") {
114
+ event.preventDefault();
115
+ focusEdge("last");
116
+ return;
117
+ }
118
+ if (event.key === "Enter" || event.key === " ") {
119
+ const active = document.activeElement;
120
+ if (!(active instanceof HTMLElement))
121
+ return;
122
+ if (!items().includes(active))
123
+ return;
124
+ // Space would scroll the page and Enter would submit a surrounding form;
125
+ // inside a composite widget both mean "activate this item".
126
+ event.preventDefault();
127
+ options.onSelect?.(active, event);
128
+ }
129
+ }
130
+ document.addEventListener("keydown", onKeyDown, true);
131
+ let destroyed = false;
132
+ return {
133
+ focusFirst: () => focusEdge("first"),
134
+ focusLast: () => focusEdge("last"),
135
+ items,
136
+ sync,
137
+ destroy() {
138
+ if (destroyed)
139
+ return;
140
+ destroyed = true;
141
+ document.removeEventListener("keydown", onKeyDown, true);
142
+ },
143
+ };
144
+ }
145
+ /** `-1` for previous, `+1` for next, `0` when the key is not ours. */
146
+ function directionFor(key, orientation) {
147
+ const vertical = orientation === "vertical" || orientation === "both";
148
+ const horizontal = orientation === "horizontal" || orientation === "both";
149
+ if (vertical && key === "ArrowDown")
150
+ return 1;
151
+ if (vertical && key === "ArrowUp")
152
+ return -1;
153
+ if (horizontal && key === "ArrowRight")
154
+ return 1;
155
+ if (horizontal && key === "ArrowLeft")
156
+ return -1;
157
+ return 0;
158
+ }
159
+ /**
160
+ * Next index, or `null` when the move is refused at an unlooped end.
161
+ *
162
+ * `current === -1` means focus is not on an item yet — the first arrow press
163
+ * after opening a menu. It enters at the first item going down and the last
164
+ * going up, which is what makes ArrowUp on a fresh menu land on its bottom
165
+ * entry the way every native menu does.
166
+ */
167
+ function step(current, delta, length, loop) {
168
+ if (current === -1)
169
+ return delta > 0 ? 0 : length - 1;
170
+ const next = current + delta;
171
+ if (next >= 0 && next < length)
172
+ return next;
173
+ if (!loop)
174
+ return null;
175
+ return next < 0 ? length - 1 : 0;
176
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Body scroll lock — stop the page scrolling behind a modal surface.
3
+ *
4
+ * Two details separate a working lock from the naive `overflow: hidden`:
5
+ *
6
+ * 1. **Reference counting.** Two modals can be open at once (a confirm dialog
7
+ * over a sheet). The second lock must not re-read the "original" overflow,
8
+ * which is by then already `hidden`, and the first release must not unlock
9
+ * while the second is still up.
10
+ * 2. **Scrollbar compensation.** Hiding overflow removes the scrollbar, the
11
+ * viewport widens by its width, and the whole page jumps sideways as the
12
+ * modal opens. Padding the body by exactly that width holds it still.
13
+ *
14
+ * The saved styles are the *inline* ones, not the computed ones. Restoring a
15
+ * computed value would write a hardcoded padding onto a body that had none,
16
+ * and the layout would stay shifted after the last modal closed.
17
+ */
18
+ /** Lock page scrolling. Returns the matching unlock; safe to call twice. */
19
+ export declare function lockScroll(): () => void;
20
+ /** Is the page currently locked? Exposed for tests. */
21
+ export declare function isScrollLocked(): boolean;
@@ -0,0 +1,75 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Body scroll lock — stop the page scrolling behind a modal surface.
4
+ *
5
+ * Two details separate a working lock from the naive `overflow: hidden`:
6
+ *
7
+ * 1. **Reference counting.** Two modals can be open at once (a confirm dialog
8
+ * over a sheet). The second lock must not re-read the "original" overflow,
9
+ * which is by then already `hidden`, and the first release must not unlock
10
+ * while the second is still up.
11
+ * 2. **Scrollbar compensation.** Hiding overflow removes the scrollbar, the
12
+ * viewport widens by its width, and the whole page jumps sideways as the
13
+ * modal opens. Padding the body by exactly that width holds it still.
14
+ *
15
+ * The saved styles are the *inline* ones, not the computed ones. Restoring a
16
+ * computed value would write a hardcoded padding onto a body that had none,
17
+ * and the layout would stay shifted after the last modal closed.
18
+ */
19
+ let depth = 0;
20
+ let savedOverflow = "";
21
+ let savedPaddingRight = "";
22
+ /** Lock page scrolling. Returns the matching unlock; safe to call twice. */
23
+ export function lockScroll() {
24
+ if (typeof document === "undefined")
25
+ return () => { };
26
+ depth += 1;
27
+ if (depth === 1)
28
+ applyLock();
29
+ let released = false;
30
+ return () => {
31
+ if (released)
32
+ return;
33
+ released = true;
34
+ depth -= 1;
35
+ if (depth === 0)
36
+ removeLock();
37
+ };
38
+ }
39
+ /** Is the page currently locked? Exposed for tests. */
40
+ export function isScrollLocked() {
41
+ return depth > 0;
42
+ }
43
+ function applyLock() {
44
+ const body = document.body;
45
+ savedOverflow = body.style.overflow;
46
+ savedPaddingRight = body.style.paddingRight;
47
+ const gap = scrollbarWidth();
48
+ if (gap > 0) {
49
+ // Add to whatever padding the body already has rather than replacing it.
50
+ const existing = Number.parseFloat(getComputedStyle(body).paddingRight);
51
+ const base = Number.isNaN(existing) ? 0 : existing;
52
+ body.style.paddingRight = `${base + gap}px`;
53
+ }
54
+ body.style.overflow = "hidden";
55
+ }
56
+ function removeLock() {
57
+ const body = document.body;
58
+ body.style.overflow = savedOverflow;
59
+ body.style.paddingRight = savedPaddingRight;
60
+ savedOverflow = "";
61
+ savedPaddingRight = "";
62
+ }
63
+ /**
64
+ * Width of the vertical scrollbar, or 0 when it overlays the content.
65
+ *
66
+ * `innerWidth - documentElement.clientWidth` measures the real gutter, which
67
+ * is 0 on macOS overlay scrollbars and on touch devices. Measuring it beats
68
+ * assuming a value, because assuming produces a *reverse* jump on exactly the
69
+ * platforms where there is no scrollbar to compensate for.
70
+ */
71
+ function scrollbarWidth() {
72
+ if (typeof window === "undefined")
73
+ return 0;
74
+ return Math.max(0, window.innerWidth - document.documentElement.clientWidth);
75
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Type-ahead — jump to an item by typing its first letters.
3
+ *
4
+ * Native `<select>` does it, so a listbox or menu that does not feels broken.
5
+ * The WAI-ARIA practices call for it on listbox, menu, tree and combobox.
6
+ *
7
+ * Two rules carry the whole behaviour, and both come from the native control:
8
+ *
9
+ * - Keys typed within the timeout build one search string, so "st" reaches
10
+ * "Stockholm" rather than stopping at anything beginning with "t".
11
+ * - Repeating a single letter cycles through the items starting with it, which
12
+ * is how you reach the third "S" entry without knowing the rest of its name.
13
+ *
14
+ * Matching starts *after* the currently focused item and wraps, so a match
15
+ * always moves forward — otherwise every search would snap back to the first
16
+ * hit in the list and the cycling rule could never take effect.
17
+ */
18
+ export interface TypeaheadOptions {
19
+ /** Current candidates, in display order. Re-read on every keypress. */
20
+ items: () => readonly HTMLElement[];
21
+ /** The item to search from. Defaults to whatever has focus. */
22
+ current?: () => HTMLElement | null;
23
+ /** A match was found. */
24
+ onMatch: (item: HTMLElement) => void;
25
+ /** Text to match against. Defaults to the item's trimmed text content. */
26
+ textOf?: (item: HTMLElement) => string;
27
+ /** How long keys keep accumulating. Default `1000`ms. */
28
+ resetAfter?: number;
29
+ }
30
+ export interface Typeahead {
31
+ /**
32
+ * Feed a keydown in. Returns `true` when the key was consumed as search
33
+ * input, so the caller can skip its own handling for that press.
34
+ */
35
+ handleKey(event: KeyboardEvent): boolean;
36
+ /** Drop the buffer — call when the surface closes. */
37
+ reset(): void;
38
+ }
39
+ export declare function typeahead(options: TypeaheadOptions): Typeahead;
@@ -0,0 +1,123 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Type-ahead — jump to an item by typing its first letters.
4
+ *
5
+ * Native `<select>` does it, so a listbox or menu that does not feels broken.
6
+ * The WAI-ARIA practices call for it on listbox, menu, tree and combobox.
7
+ *
8
+ * Two rules carry the whole behaviour, and both come from the native control:
9
+ *
10
+ * - Keys typed within the timeout build one search string, so "st" reaches
11
+ * "Stockholm" rather than stopping at anything beginning with "t".
12
+ * - Repeating a single letter cycles through the items starting with it, which
13
+ * is how you reach the third "S" entry without knowing the rest of its name.
14
+ *
15
+ * Matching starts *after* the currently focused item and wraps, so a match
16
+ * always moves forward — otherwise every search would snap back to the first
17
+ * hit in the list and the cycling rule could never take effect.
18
+ */
19
+ const RESET_AFTER_MS = 1000;
20
+ export function typeahead(options) {
21
+ const resetAfter = options.resetAfter ?? RESET_AFTER_MS;
22
+ let buffer = "";
23
+ let timer;
24
+ function reset() {
25
+ buffer = "";
26
+ if (timer !== undefined)
27
+ clearTimeout(timer);
28
+ timer = undefined;
29
+ }
30
+ function restartTimer() {
31
+ if (timer !== undefined)
32
+ clearTimeout(timer);
33
+ timer = setTimeout(reset, resetAfter);
34
+ }
35
+ return {
36
+ reset,
37
+ handleKey(event) {
38
+ if (!isSearchKey(event, buffer.length > 0))
39
+ return false;
40
+ buffer += event.key.toLowerCase();
41
+ restartTimer();
42
+ const items = options.items();
43
+ if (items.length === 0)
44
+ return true;
45
+ // A buffer of one repeated character means "next item starting with it",
46
+ // not "item starting with that string repeated".
47
+ const query = allSameCharacter(buffer) ? (buffer[0] ?? "") : buffer;
48
+ const skipCurrent = allSameCharacter(buffer) && buffer.length > 1;
49
+ const from = startIndex(items, options.current?.() ?? focusedElement());
50
+ const match = findFrom(items, query, from, skipCurrent, options.textOf);
51
+ if (match !== null)
52
+ options.onMatch(match);
53
+ return true;
54
+ },
55
+ };
56
+ }
57
+ /**
58
+ * Is this keypress search input?
59
+ *
60
+ * Single printable characters only. `event.key` is `"a"` for a letter but
61
+ * `"ArrowDown"` or `"Enter"` for a control key, so a length check separates
62
+ * them without a keycode table. Modifiers are excluded because Ctrl+F belongs
63
+ * to the browser — but Shift is not, since it is how you type a capital.
64
+ *
65
+ * Space is the one character that depends on context. On an empty buffer it
66
+ * means "activate the focused item" and must reach the caller untouched; once
67
+ * a search is under way it is a word separator, and swallowing it is what lets
68
+ * "new y" reach "New York".
69
+ */
70
+ function isSearchKey(event, hasBuffer) {
71
+ if (event.ctrlKey || event.metaKey || event.altKey)
72
+ return false;
73
+ if (event.key.length !== 1)
74
+ return false;
75
+ if (event.key === " ")
76
+ return hasBuffer;
77
+ return true;
78
+ }
79
+ function allSameCharacter(value) {
80
+ if (value.length < 2)
81
+ return true;
82
+ const first = value[0];
83
+ for (const character of value) {
84
+ if (character !== first)
85
+ return false;
86
+ }
87
+ return true;
88
+ }
89
+ function focusedElement() {
90
+ if (typeof document === "undefined")
91
+ return null;
92
+ const active = document.activeElement;
93
+ return active instanceof HTMLElement ? active : null;
94
+ }
95
+ function startIndex(items, current) {
96
+ if (current === null)
97
+ return 0;
98
+ const index = items.indexOf(current);
99
+ return index === -1 ? 0 : index;
100
+ }
101
+ /**
102
+ * First item matching `query`, searching forward from `from` and wrapping.
103
+ *
104
+ * `skipCurrent` is the cycling rule: repeating a letter must advance past the
105
+ * item already selected. A fresh search keeps the current item eligible, so
106
+ * typing the first letters of what is already highlighted does not jump away.
107
+ */
108
+ function findFrom(items, query, from, skipCurrent, textOf) {
109
+ if (query === "")
110
+ return null;
111
+ const offset = skipCurrent ? 1 : 0;
112
+ for (let i = 0; i < items.length; i += 1) {
113
+ const item = items[(from + i + offset) % items.length];
114
+ if (item === undefined)
115
+ continue;
116
+ const text = (textOf?.(item) ?? item.textContent ?? "")
117
+ .trim()
118
+ .toLowerCase();
119
+ if (text.startsWith(query))
120
+ return item;
121
+ }
122
+ return null;
123
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * AppShell — sidebar, header, content.
3
+ *
4
+ * The layout behind almost every signed-in page. Shipping it is what makes
5
+ * nebula an atomic-design library rather than a component bag: shadcn stops at
6
+ * organisms and leaves the page skeleton to be rebuilt per app, which is why
7
+ * two pages in the same product end up with different header heights.
8
+ *
9
+ * `h-svh` on the shell and `overflow-auto` on the content column, not on the
10
+ * body. That is what keeps the header and the sidebar fixed while only the
11
+ * content scrolls — pinning them with `position: fixed` instead means
12
+ * hand-maintaining a top padding that matches the header, and it breaks the
13
+ * moment the header wraps to two lines.
14
+ *
15
+ * `svh` rather than `vh` because mobile browsers shrink the viewport as their
16
+ * address bar retracts; `vh` leaves the last hundred pixels of a full-height
17
+ * layout under the browser chrome.
18
+ */
19
+ import { type Slot } from "../lib/children.js";
20
+ import { type Reactive } from "../lib/props.js";
21
+ export interface AppShellProps {
22
+ /** The navigation column — usually a `Sidebar`. */
23
+ sidebar?: Slot;
24
+ /** The top bar, spanning the content column. */
25
+ header?: Slot;
26
+ children?: Slot;
27
+ class?: Reactive<string>;
28
+ contentClass?: Reactive<string>;
29
+ }
30
+ export declare const AppShell: (props?: AppShellProps | undefined) => import("@c9up/aurora").TemplateResult;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * AppShell — sidebar, header, content.
3
+ *
4
+ * The layout behind almost every signed-in page. Shipping it is what makes
5
+ * nebula an atomic-design library rather than a component bag: shadcn stops at
6
+ * organisms and leaves the page skeleton to be rebuilt per app, which is why
7
+ * two pages in the same product end up with different header heights.
8
+ *
9
+ * `h-svh` on the shell and `overflow-auto` on the content column, not on the
10
+ * body. That is what keeps the header and the sidebar fixed while only the
11
+ * content scrolls — pinning them with `position: fixed` instead means
12
+ * hand-maintaining a top padding that matches the header, and it breaks the
13
+ * moment the header wraps to two lines.
14
+ *
15
+ * `svh` rather than `vh` because mobile browsers shrink the viewport as their
16
+ * address bar retracts; `vh` leaves the last hundred pixels of a full-height
17
+ * layout under the browser chrome.
18
+ */
19
+ import { component, html } from "@c9up/aurora";
20
+ import { slot } from "../lib/children.js";
21
+ import { cn } from "../lib/cn.js";
22
+ import { read } from "../lib/props.js";
23
+ export const AppShell = component((props) => {
24
+ return html `<div
25
+ data-slot="app-shell"
26
+ class="${() => cn("bg-background flex h-svh w-full overflow-hidden", read(props.class))}"
27
+ >
28
+ ${slot(props.sidebar)}
29
+ <div data-slot="app-shell-main" class="flex min-w-0 flex-1 flex-col">
30
+ ${props.header === undefined
31
+ ? null
32
+ : html `<header
33
+ data-slot="app-shell-header"
34
+ class="bg-background flex h-14 shrink-0 items-center gap-2 border-b px-4"
35
+ >${slot(props.header)}</header>`}
36
+ <main
37
+ data-slot="app-shell-content"
38
+ class="${() => cn("min-h-0 flex-1 overflow-auto p-4 md:p-6", read(props.contentClass))}"
39
+ >${slot(props.children)}</main>
40
+ </div>
41
+ </div>`;
42
+ });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * AuthLayout — a centred card for sign-in, sign-up and password reset.
3
+ *
4
+ * `min-h-svh` with the card centred by flex, rather than absolute positioning
5
+ * and a translate. Centring by transform breaks as soon as the card grows
6
+ * taller than the viewport — a sign-up form with four fields and a validation
7
+ * error on each — because the top half goes above the fold with nothing to
8
+ * scroll to.
9
+ */
10
+ import type { Child } from "../lib/children.js";
11
+ import { type Slot } from "../lib/children.js";
12
+ import { type Reactive } from "../lib/props.js";
13
+ export interface AuthLayoutProps {
14
+ /** Product name or logo, above the card. */
15
+ brand?: Slot;
16
+ title: Child;
17
+ description?: Child;
18
+ children?: Slot;
19
+ /** Under the card — "Already have an account?" and the like. */
20
+ footer?: Slot;
21
+ class?: Reactive<string>;
22
+ }
23
+ export declare const AuthLayout: (props?: AuthLayoutProps | undefined) => import("@c9up/aurora").TemplateResult;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * AuthLayout — a centred card for sign-in, sign-up and password reset.
3
+ *
4
+ * `min-h-svh` with the card centred by flex, rather than absolute positioning
5
+ * and a translate. Centring by transform breaks as soon as the card grows
6
+ * taller than the viewport — a sign-up form with four fields and a validation
7
+ * error on each — because the top half goes above the fold with nothing to
8
+ * scroll to.
9
+ */
10
+ import { component, html } from "@c9up/aurora";
11
+ import { slot } from "../lib/children.js";
12
+ import { cn } from "../lib/cn.js";
13
+ import { read } from "../lib/props.js";
14
+ export const AuthLayout = component((props) => {
15
+ return html `<div
16
+ data-slot="auth-layout"
17
+ class="${() => cn("bg-muted/40 flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10", read(props.class))}"
18
+ >
19
+ <div class="flex w-full max-w-sm flex-col gap-6">
20
+ ${props.brand === undefined
21
+ ? null
22
+ : html `<div class="flex justify-center">${slot(props.brand)}</div>`}
23
+ <div
24
+ data-slot="auth-card"
25
+ class="bg-card text-card-foreground flex flex-col gap-6 rounded-xl border p-6 shadow-sm"
26
+ >
27
+ <div class="flex flex-col gap-1.5 text-center">
28
+ <h1 class="text-xl leading-none font-semibold">${props.title}</h1>
29
+ ${props.description === undefined
30
+ ? null
31
+ : html `<p class="text-muted-foreground text-sm">${props.description}</p>`}
32
+ </div>
33
+ ${slot(props.children)}
34
+ </div>
35
+ ${props.footer === undefined
36
+ ? null
37
+ : html `<div class="text-muted-foreground text-center text-sm">
38
+ ${slot(props.footer)}
39
+ </div>`}
40
+ </div>
41
+ </div>`;
42
+ });
@@ -0,0 +1,27 @@
1
+ /**
2
+ * SettingsLayout — a section list beside a settings panel.
3
+ *
4
+ * The list is a real `<nav>` of links, not a Tabs component. Settings sections
5
+ * are pages: they need their own URL so a link to "Billing" works, and so the
6
+ * back button returns to the previous section rather than leaving the page
7
+ * entirely. Tabs would give the same look and none of that.
8
+ *
9
+ * The list sits above the panel on narrow screens and beside it from `md` up,
10
+ * which is the one responsive decision this layout has to make.
11
+ */
12
+ import type { Child } from "../lib/children.js";
13
+ import { type Slot } from "../lib/children.js";
14
+ import { type Reactive } from "../lib/props.js";
15
+ export interface SettingsSection {
16
+ label: Child;
17
+ href: string;
18
+ active?: boolean;
19
+ }
20
+ export interface SettingsLayoutProps {
21
+ title: Child;
22
+ description?: Child;
23
+ sections: readonly SettingsSection[];
24
+ children?: Slot;
25
+ class?: Reactive<string>;
26
+ }
27
+ export declare const SettingsLayout: (props?: SettingsLayoutProps | undefined) => import("@c9up/aurora").TemplateResult;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * SettingsLayout — a section list beside a settings panel.
3
+ *
4
+ * The list is a real `<nav>` of links, not a Tabs component. Settings sections
5
+ * are pages: they need their own URL so a link to "Billing" works, and so the
6
+ * back button returns to the previous section rather than leaving the page
7
+ * entirely. Tabs would give the same look and none of that.
8
+ *
9
+ * The list sits above the panel on narrow screens and beside it from `md` up,
10
+ * which is the one responsive decision this layout has to make.
11
+ */
12
+ import { component, html } from "@c9up/aurora";
13
+ import { slot } from "../lib/children.js";
14
+ import { cn } from "../lib/cn.js";
15
+ import { read } from "../lib/props.js";
16
+ export const SettingsLayout = component((props) => {
17
+ return html `<div
18
+ data-slot="settings-layout"
19
+ class="${() => cn("flex flex-col gap-6 p-4 md:p-10", read(props.class))}"
20
+ >
21
+ <div class="flex flex-col gap-1.5">
22
+ <h1 class="text-2xl font-semibold tracking-tight">${props.title}</h1>
23
+ ${props.description === undefined
24
+ ? null
25
+ : html `<p class="text-muted-foreground text-sm">${props.description}</p>`}
26
+ </div>
27
+ <div class="flex flex-col gap-8 md:flex-row md:gap-12">
28
+ <nav
29
+ data-slot="settings-nav"
30
+ aria-label="Settings sections"
31
+ class="flex gap-2 overflow-x-auto md:w-48 md:shrink-0 md:flex-col md:overflow-visible"
32
+ >
33
+ ${props.sections.map((section) => html `<a
34
+ href="${section.href}"
35
+ aria-current="${section.active === true ? "page" : undefined}"
36
+ class="hover:bg-accent hover:text-accent-foreground aria-[current=page]:bg-accent aria-[current=page]:font-medium rounded-md px-3 py-2 text-sm whitespace-nowrap transition-colors"
37
+ >${section.label}</a>`)}
38
+ </nav>
39
+ <div data-slot="settings-panel" class="min-w-0 flex-1">${slot(props.children)}</div>
40
+ </div>
41
+ </div>`;
42
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Templates — page skeletons.
3
+ *
4
+ * Atomic design's fourth level, and the one shadcn does not have: it stops at
5
+ * organisms and ships "blocks" as copy-paste examples instead. The gap shows
6
+ * up as drift — two pages in the same product with different header heights
7
+ * and different content padding, because each was assembled from organisms by
8
+ * hand.
9
+ *
10
+ * A template is layout only. It positions slots and owns no state, no data and
11
+ * no behaviour, which is what keeps it usable for pages it was not written
12
+ * with in mind.
13
+ */
14
+ export { AppShell, type AppShellProps } from "./AppShell.js";
15
+ export { AuthLayout, type AuthLayoutProps } from "./AuthLayout.js";
16
+ export { SettingsLayout, type SettingsLayoutProps, type SettingsSection, } from "./SettingsLayout.js";