@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,91 @@
1
+ /**
2
+ * DropdownMenu — a menu opened from a button.
3
+ *
4
+ * The panel, its entries and its keyboard model all come from `./menu.js`;
5
+ * this component contributes the trigger and the open/close state.
6
+ *
7
+ * Two behaviours are specific to a button-opened menu:
8
+ *
9
+ * - Opening with the keyboard focuses the first item; opening with the pointer
10
+ * does not. A keyboard user has no other way in, while pre-highlighting an
11
+ * item for a mouse user suggests it is about to be chosen.
12
+ * - ArrowDown on the trigger opens the menu *and* enters it, which is the
13
+ * behaviour of every native menu button and the one users try first.
14
+ */
15
+ import { component, html } from "@c9up/aurora";
16
+ import { slot } from "../lib/children.js";
17
+ import { cn } from "../lib/cn.js";
18
+ import { uid } from "../lib/id.js";
19
+ import { read } from "../lib/props.js";
20
+ import { controllable } from "../primitives/controllable.js";
21
+ import { floatingSurface } from "../primitives/floatingSurface.js";
22
+ import { menuPanel, wireMenu } from "../primitives/menu.js";
23
+ export const DropdownMenu = component((props) => {
24
+ const triggerId = uid("dropdown-trigger");
25
+ const contentId = uid("dropdown-content");
26
+ const state = controllable({
27
+ value: props.open,
28
+ initial: props.defaultOpen ?? false,
29
+ onChange: props.onOpenChange,
30
+ });
31
+ // Set by the trigger's key handler just before opening, and consumed by
32
+ // `onOpened`. The panel does not exist yet at the moment of the keypress, so
33
+ // the intent has to survive the gap.
34
+ let enterOnOpen = false;
35
+ let unwire = null;
36
+ function close() {
37
+ state.set(false);
38
+ }
39
+ floatingSurface({
40
+ anchor: () => document.getElementById(triggerId),
41
+ open: () => state.current(),
42
+ onClose: close,
43
+ placement: props.placement ?? "bottom-start",
44
+ offset: 4,
45
+ content: () => menuPanel({
46
+ id: contentId,
47
+ entries: props.entries,
48
+ onCloseAll: close,
49
+ labelledBy: triggerId,
50
+ class: read(props.contentClass),
51
+ }),
52
+ onOpened: (panel) => {
53
+ const enterItems = enterOnOpen;
54
+ enterOnOpen = false;
55
+ unwire = wireMenu(panel, {
56
+ onCloseAll: close,
57
+ autoFocusFirst: enterItems,
58
+ });
59
+ // The panel takes focus only when no item did — it needs to hold focus
60
+ // so Escape and the arrows reach it rather than the page behind, but
61
+ // focusing it unconditionally steals focus straight back from the item
62
+ // `autoFocusFirst` just landed on, and opening with the keyboard would
63
+ // never reach the first entry.
64
+ if (!enterItems)
65
+ panel.focus({ preventScroll: true });
66
+ },
67
+ onClosed: () => {
68
+ unwire?.();
69
+ unwire = null;
70
+ },
71
+ });
72
+ function onTriggerKeyDown(event) {
73
+ if (event.key !== "ArrowDown" && event.key !== "Enter" && event.key !== " ")
74
+ return;
75
+ event.preventDefault();
76
+ enterOnOpen = true;
77
+ state.set(true);
78
+ }
79
+ return html `<button
80
+ type="button"
81
+ data-slot="dropdown-menu-trigger"
82
+ id="${triggerId}"
83
+ aria-haspopup="menu"
84
+ aria-expanded="${() => (state.current() ? "true" : "false")}"
85
+ aria-controls="${() => (state.current() ? contentId : undefined)}"
86
+ data-state="${() => (state.current() ? "open" : "closed")}"
87
+ class="${() => cn(read(props.triggerClass))}"
88
+ @click="${() => state.set(!state.current())}"
89
+ @keydown="${onTriggerKeyDown}"
90
+ >${slot(props.trigger)}</button>`;
91
+ });
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Form — binding between Aurora's form controller and nebula's field markup.
3
+ *
4
+ * shadcn's Form is a set of adapters over `react-hook-form`, and none of that
5
+ * layer is needed here: Aurora already ships `form()`, a reactive controller
6
+ * with values, per-field errors, touched state, submit-in-flight and optional
7
+ * rune/zod-shaped validation. Reimplementing it would be a second source of
8
+ * truth for the same thing.
9
+ *
10
+ * So this file is only the join. It wires one field to one `Field` plus one
11
+ * control, and takes care of the three things that otherwise get forgotten:
12
+ * `aria-invalid` when the field has an error, `aria-describedby` pointing at
13
+ * the error element, and marking the field touched on blur so an error appears
14
+ * after leaving a field rather than while typing in it.
15
+ *
16
+ * const login = form({
17
+ * initial: { email: "", password: "" },
18
+ * validate: (values) => (values.email === "" ? { email: "Required" } : {}),
19
+ * submit: (values) => http.post("/login", values),
20
+ * })
21
+ *
22
+ * Form({
23
+ * form: login,
24
+ * children: [
25
+ * TextField({ bind: bind(login, "email"), label: "Email", type: "email" }),
26
+ * TextField({ bind: bind(login, "password"), label: "Password", type: "password" }),
27
+ * SubmitButton({ form: login, label: "Sign in" }),
28
+ * ],
29
+ * })
30
+ */
31
+ import type { Form as AuroraForm, FormField } from "@c9up/aurora";
32
+ import { html } from "@c9up/aurora";
33
+ import type { Child, Slot } from "../lib/children.js";
34
+ import { type Reactive } from "../lib/props.js";
35
+ /**
36
+ * One field, with a setter that already knows which key it writes to.
37
+ *
38
+ * This shape exists so the input components are not generic over the form.
39
+ * Inside a function generic in `T`, TypeScript cannot prove a `string` is
40
+ * assignable to `T[K]`, and the usual escape is a cast. `bind()` is called at
41
+ * a *concrete* call site, where `K` is a literal and `T[K]` resolves to a real
42
+ * type — so `bind(login, "email")` is a `FieldBinding<string>` and handing it
43
+ * to a numeric input is a compile error rather than a runtime surprise.
44
+ */
45
+ export interface FieldBinding<V> {
46
+ readonly name: string;
47
+ readonly field: FormField<V>;
48
+ set(value: V): void;
49
+ }
50
+ /** Bind one key of a form. Call it at the field's own call site. */
51
+ export declare function bind<T, K extends keyof T & string>(form: AuroraForm<T>, name: K): FieldBinding<T[K]>;
52
+ export interface FormProps<T> {
53
+ form: AuroraForm<T>;
54
+ children?: Slot;
55
+ class?: Reactive<string>;
56
+ }
57
+ /**
58
+ * The `<form>` element itself.
59
+ *
60
+ * `novalidate` on purpose. The browser's own validation bubbles cannot be
61
+ * styled, appear one at a time, and vanish on scroll — and they would fire
62
+ * before the controller's validation ever runs, so the user would see two
63
+ * different error presentations for the same field.
64
+ */
65
+ export declare function Form<T>(props: FormProps<T>): ReturnType<typeof html>;
66
+ interface FieldShellProps {
67
+ bind: FieldBinding<string>;
68
+ label: Child;
69
+ description?: Child;
70
+ placeholder?: string;
71
+ required?: boolean;
72
+ disabled?: Reactive<boolean>;
73
+ }
74
+ export interface TextFieldProps extends FieldShellProps {
75
+ type?: string;
76
+ autocomplete?: string;
77
+ }
78
+ /**
79
+ * A labelled text input bound to one field.
80
+ *
81
+ * The error is shown only once the field has been touched, which is what stops
82
+ * a pristine form opening with "Required" under every empty box.
83
+ *
84
+ * When errors actually appear is Aurora's decision, not this file's, and it is
85
+ * worth knowing: `form()` populates them on `validate()` — which `handleSubmit`
86
+ * calls — and clears a field's error as soon as it is written to. So the
87
+ * sequence is: submit, see every problem at once, and watch each one disappear
88
+ * as it is fixed. Typing into an untouched field does not raise an error, and
89
+ * `markTouched` on blur does not validate; it only lifts the gate for an error
90
+ * that validation has already found.
91
+ */
92
+ export declare const TextField: (props?: TextFieldProps | undefined) => import("@c9up/aurora").TemplateResult;
93
+ export interface TextAreaFieldProps extends FieldShellProps {
94
+ rows?: number;
95
+ }
96
+ /** The same, with a multi-line control. */
97
+ export declare const TextAreaField: (props?: TextAreaFieldProps | undefined) => import("@c9up/aurora").TemplateResult;
98
+ export interface SubmitButtonProps<T> {
99
+ form: AuroraForm<T>;
100
+ label: Child;
101
+ /** Replaces the label while the submit is in flight. */
102
+ pendingLabel?: Child;
103
+ class?: Reactive<string>;
104
+ }
105
+ /**
106
+ * A submit button that disables itself while the submit is in flight.
107
+ *
108
+ * Disabled on `submitting` only — never on `!valid`. A submit button that is
109
+ * dead until the form is correct gives the user nothing to press and no
110
+ * explanation; pressing it and seeing every error at once is how a form tells
111
+ * you what is wrong.
112
+ */
113
+ export declare function SubmitButton<T>(props: SubmitButtonProps<T>): ReturnType<typeof html>;
114
+ export {};
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Form — binding between Aurora's form controller and nebula's field markup.
3
+ *
4
+ * shadcn's Form is a set of adapters over `react-hook-form`, and none of that
5
+ * layer is needed here: Aurora already ships `form()`, a reactive controller
6
+ * with values, per-field errors, touched state, submit-in-flight and optional
7
+ * rune/zod-shaped validation. Reimplementing it would be a second source of
8
+ * truth for the same thing.
9
+ *
10
+ * So this file is only the join. It wires one field to one `Field` plus one
11
+ * control, and takes care of the three things that otherwise get forgotten:
12
+ * `aria-invalid` when the field has an error, `aria-describedby` pointing at
13
+ * the error element, and marking the field touched on blur so an error appears
14
+ * after leaving a field rather than while typing in it.
15
+ *
16
+ * const login = form({
17
+ * initial: { email: "", password: "" },
18
+ * validate: (values) => (values.email === "" ? { email: "Required" } : {}),
19
+ * submit: (values) => http.post("/login", values),
20
+ * })
21
+ *
22
+ * Form({
23
+ * form: login,
24
+ * children: [
25
+ * TextField({ bind: bind(login, "email"), label: "Email", type: "email" }),
26
+ * TextField({ bind: bind(login, "password"), label: "Password", type: "password" }),
27
+ * SubmitButton({ form: login, label: "Sign in" }),
28
+ * ],
29
+ * })
30
+ */
31
+ import { component, html } from "@c9up/aurora";
32
+ import { Button } from "../atoms/Button.js";
33
+ import { Input } from "../atoms/Input.js";
34
+ import { Spinner } from "../atoms/Spinner.js";
35
+ import { Textarea } from "../atoms/Textarea.js";
36
+ import { slot } from "../lib/children.js";
37
+ import { cn } from "../lib/cn.js";
38
+ import { read } from "../lib/props.js";
39
+ import { Field, fieldIds } from "../molecules/Field.js";
40
+ /** Bind one key of a form. Call it at the field's own call site. */
41
+ export function bind(form, name) {
42
+ return {
43
+ name,
44
+ field: form.field(name),
45
+ set: (value) => form.set(name, value),
46
+ };
47
+ }
48
+ /**
49
+ * The `<form>` element itself.
50
+ *
51
+ * `novalidate` on purpose. The browser's own validation bubbles cannot be
52
+ * styled, appear one at a time, and vanish on scroll — and they would fire
53
+ * before the controller's validation ever runs, so the user would see two
54
+ * different error presentations for the same field.
55
+ */
56
+ export function Form(props) {
57
+ return html `<form
58
+ data-slot="form"
59
+ novalidate
60
+ class="${() => cn("flex flex-col gap-6", read(props.class))}"
61
+ @submit="${(event) => {
62
+ void props.form.handleSubmit(event);
63
+ }}"
64
+ >${slot(props.children)}</form>`;
65
+ }
66
+ /**
67
+ * A labelled text input bound to one field.
68
+ *
69
+ * The error is shown only once the field has been touched, which is what stops
70
+ * a pristine form opening with "Required" under every empty box.
71
+ *
72
+ * When errors actually appear is Aurora's decision, not this file's, and it is
73
+ * worth knowing: `form()` populates them on `validate()` — which `handleSubmit`
74
+ * calls — and clears a field's error as soon as it is written to. So the
75
+ * sequence is: submit, see every problem at once, and watch each one disappear
76
+ * as it is fixed. Typing into an untouched field does not raise an error, and
77
+ * `markTouched` on blur does not validate; it only lifts the gate for an error
78
+ * that validation has already found.
79
+ */
80
+ export const TextField = component((props) => {
81
+ const ids = fieldIds();
82
+ const { field, set, name } = props.bind;
83
+ const visibleError = () => field.touched() ? (field.error() ?? undefined) : undefined;
84
+ return Field({
85
+ ids,
86
+ label: props.label,
87
+ description: props.description,
88
+ error: visibleError,
89
+ required: props.required,
90
+ disabled: props.disabled,
91
+ children: Input({
92
+ id: ids.control,
93
+ name,
94
+ type: props.type ?? "text",
95
+ placeholder: props.placeholder,
96
+ autocomplete: props.autocomplete,
97
+ required: props.required,
98
+ disabled: props.disabled,
99
+ invalid: () => visibleError() !== undefined,
100
+ describedBy: ids.describedBy,
101
+ value: () => field.value(),
102
+ onInput: set,
103
+ onBlur: () => field.markTouched(),
104
+ }),
105
+ });
106
+ });
107
+ /** The same, with a multi-line control. */
108
+ export const TextAreaField = component((props) => {
109
+ const ids = fieldIds();
110
+ const { field, set, name } = props.bind;
111
+ const visibleError = () => field.touched() ? (field.error() ?? undefined) : undefined;
112
+ return Field({
113
+ ids,
114
+ label: props.label,
115
+ description: props.description,
116
+ error: visibleError,
117
+ required: props.required,
118
+ disabled: props.disabled,
119
+ children: Textarea({
120
+ id: ids.control,
121
+ name,
122
+ rows: props.rows,
123
+ placeholder: props.placeholder,
124
+ required: props.required,
125
+ disabled: props.disabled,
126
+ invalid: () => visibleError() !== undefined,
127
+ describedBy: ids.describedBy,
128
+ value: () => field.value(),
129
+ onInput: set,
130
+ onBlur: () => field.markTouched(),
131
+ }),
132
+ });
133
+ });
134
+ /**
135
+ * A submit button that disables itself while the submit is in flight.
136
+ *
137
+ * Disabled on `submitting` only — never on `!valid`. A submit button that is
138
+ * dead until the form is correct gives the user nothing to press and no
139
+ * explanation; pressing it and seeing every error at once is how a form tells
140
+ * you what is wrong.
141
+ */
142
+ export function SubmitButton(props) {
143
+ return Button({
144
+ type: "submit",
145
+ disabled: () => props.form.submitting(),
146
+ class: props.class,
147
+ children: () => props.form.submitting()
148
+ ? [Spinner({ class: "mr-2" }), props.pendingLabel ?? props.label]
149
+ : props.label,
150
+ });
151
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * HoverCard — a rich preview shown on hover.
3
+ *
4
+ * A Tooltip carries a phrase; a HoverCard carries content — an avatar, a
5
+ * summary, a link. That difference drives every behavioural difference between
6
+ * the two.
7
+ *
8
+ * It opens more slowly, because the payload is heavier and a card flashing
9
+ * past is worse than a tooltip flashing past. It closes more slowly, because
10
+ * the user is expected to move the pointer *into* it. And it is never the only
11
+ * route to what it contains: pointer-only content is unreachable by keyboard
12
+ * and touch, so a hover card must preview something a click already reaches.
13
+ *
14
+ * `role="dialog"` rather than `tooltip`, since it holds interactive content —
15
+ * a tooltip's contents are announced as a description, which is wrong for
16
+ * something containing a link.
17
+ */
18
+ import { type Slot } from "../lib/children.js";
19
+ import { type Reactive } from "../lib/props.js";
20
+ import type { Placement } from "../primitives/floating.js";
21
+ export interface HoverCardProps {
22
+ trigger?: Slot;
23
+ children?: Slot;
24
+ placement?: Placement;
25
+ openDelay?: number;
26
+ closeDelay?: number;
27
+ class?: Reactive<string>;
28
+ contentClass?: Reactive<string>;
29
+ }
30
+ export declare const HoverCard: (props?: HoverCardProps | undefined) => import("@c9up/aurora").TemplateResult;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * HoverCard — a rich preview shown on hover.
3
+ *
4
+ * A Tooltip carries a phrase; a HoverCard carries content — an avatar, a
5
+ * summary, a link. That difference drives every behavioural difference between
6
+ * the two.
7
+ *
8
+ * It opens more slowly, because the payload is heavier and a card flashing
9
+ * past is worse than a tooltip flashing past. It closes more slowly, because
10
+ * the user is expected to move the pointer *into* it. And it is never the only
11
+ * route to what it contains: pointer-only content is unreachable by keyboard
12
+ * and touch, so a hover card must preview something a click already reaches.
13
+ *
14
+ * `role="dialog"` rather than `tooltip`, since it holds interactive content —
15
+ * a tooltip's contents are announced as a description, which is wrong for
16
+ * something containing a link.
17
+ */
18
+ import { component, html, onUnmount, signal } from "@c9up/aurora";
19
+ import { slot } from "../lib/children.js";
20
+ import { cn } from "../lib/cn.js";
21
+ import { uid } from "../lib/id.js";
22
+ import { zoomInOut } from "../lib/motion.js";
23
+ import { read } from "../lib/props.js";
24
+ import { floatingSurface } from "../primitives/floatingSurface.js";
25
+ const OPEN_DELAY_MS = 700;
26
+ const CLOSE_DELAY_MS = 300;
27
+ export const HoverCard = component((props) => {
28
+ const triggerId = uid("hover-card-trigger");
29
+ const contentId = uid("hover-card-content");
30
+ const open = signal(false);
31
+ let timer;
32
+ function clear() {
33
+ if (timer !== undefined)
34
+ clearTimeout(timer);
35
+ timer = undefined;
36
+ }
37
+ function schedule(next, delay) {
38
+ clear();
39
+ timer = setTimeout(() => open(next), delay);
40
+ }
41
+ onUnmount(clear);
42
+ floatingSurface({
43
+ anchor: () => document.getElementById(triggerId),
44
+ open: () => open(),
45
+ onClose: () => {
46
+ clear();
47
+ open(false);
48
+ },
49
+ placement: props.placement ?? "bottom",
50
+ offset: 8,
51
+ content: () => html `<div
52
+ data-slot="hover-card-content"
53
+ id="${contentId}"
54
+ role="dialog"
55
+ aria-labelledby="${triggerId}"
56
+ class="${cn("bg-popover text-popover-foreground z-50 w-64 rounded-md border p-4 shadow-md outline-none", zoomInOut, read(props.contentClass))}"
57
+ @pointerenter="${clear}"
58
+ @pointerleave="${() => schedule(false, props.closeDelay ?? CLOSE_DELAY_MS)}"
59
+ >${slot(props.children)}</div>`,
60
+ });
61
+ return html `<span
62
+ data-slot="hover-card-trigger"
63
+ id="${triggerId}"
64
+ class="${() => cn("inline-flex", read(props.class))}"
65
+ @pointerenter="${() => schedule(true, props.openDelay ?? OPEN_DELAY_MS)}"
66
+ @pointerleave="${() => schedule(false, props.closeDelay ?? CLOSE_DELAY_MS)}"
67
+ >${slot(props.trigger)}</span>`;
68
+ });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Menubar — an application menu bar.
3
+ *
4
+ * A row of menu buttons where at most one menu is open. The behaviour that
5
+ * makes it a menubar rather than a row of independent dropdowns is the
6
+ * handover: once a menu is open, moving the pointer onto a neighbouring button
7
+ * switches to it without a click, and ArrowLeft/ArrowRight do the same from
8
+ * the keyboard. That is how every desktop menu bar behaves, and a row of
9
+ * dropdowns that each need their own click is the thing users notice.
10
+ *
11
+ * One `floatingSurface` for the bar, not one per menu. The open menu is an
12
+ * index, so switching moves the same surface to a new anchor instead of
13
+ * closing one and opening another — which would flash, and would lose the
14
+ * exit animation into the entry animation.
15
+ */
16
+ import type { Child } from "../lib/children.js";
17
+ import { type Reactive } from "../lib/props.js";
18
+ import { type MenuEntry } from "../primitives/menu.js";
19
+ export interface MenubarMenu {
20
+ label: Child;
21
+ entries: readonly MenuEntry[];
22
+ disabled?: boolean;
23
+ }
24
+ export interface MenubarProps {
25
+ menus: readonly MenubarMenu[];
26
+ class?: Reactive<string>;
27
+ }
28
+ export declare const Menubar: (props?: MenubarProps | undefined) => import("@c9up/aurora").TemplateResult;
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Menubar — an application menu bar.
3
+ *
4
+ * A row of menu buttons where at most one menu is open. The behaviour that
5
+ * makes it a menubar rather than a row of independent dropdowns is the
6
+ * handover: once a menu is open, moving the pointer onto a neighbouring button
7
+ * switches to it without a click, and ArrowLeft/ArrowRight do the same from
8
+ * the keyboard. That is how every desktop menu bar behaves, and a row of
9
+ * dropdowns that each need their own click is the thing users notice.
10
+ *
11
+ * One `floatingSurface` for the bar, not one per menu. The open menu is an
12
+ * index, so switching moves the same surface to a new anchor instead of
13
+ * closing one and opening another — which would flash, and would lose the
14
+ * exit animation into the entry animation.
15
+ */
16
+ import { component, html, signal } from "@c9up/aurora";
17
+ import { cn } from "../lib/cn.js";
18
+ import { uid } from "../lib/id.js";
19
+ import { read } from "../lib/props.js";
20
+ import { floatingSurface } from "../primitives/floatingSurface.js";
21
+ import { menuPanel, wireMenu } from "../primitives/menu.js";
22
+ export const Menubar = component((props) => {
23
+ const contentId = uid("menubar-content");
24
+ const triggerIds = props.menus.map(() => uid("menubar-trigger"));
25
+ /** Index of the open menu, or `-1`. */
26
+ const openIndex = signal(-1);
27
+ let unwire = null;
28
+ let focusFirstOnOpen = false;
29
+ function close() {
30
+ openIndex(-1);
31
+ }
32
+ function openAt(index, withKeyboard) {
33
+ if (props.menus[index]?.disabled === true)
34
+ return;
35
+ focusFirstOnOpen = withKeyboard;
36
+ openIndex(index);
37
+ }
38
+ function move(delta) {
39
+ const count = props.menus.length;
40
+ if (count === 0)
41
+ return;
42
+ const from = openIndex();
43
+ if (from === -1)
44
+ return;
45
+ openAt((from + delta + count) % count, true);
46
+ }
47
+ floatingSurface({
48
+ anchor: () => {
49
+ const id = triggerIds[openIndex()];
50
+ return id === undefined ? null : document.getElementById(id);
51
+ },
52
+ open: () => openIndex() !== -1,
53
+ onClose: close,
54
+ placement: "bottom-start",
55
+ offset: 4,
56
+ content: () => menuPanel({
57
+ id: contentId,
58
+ entries: props.menus[openIndex()]?.entries ?? [],
59
+ onCloseAll: close,
60
+ labelledBy: triggerIds[openIndex()],
61
+ }),
62
+ onOpened: (panel) => {
63
+ const enterItems = focusFirstOnOpen;
64
+ focusFirstOnOpen = false;
65
+ unwire = wireMenu(panel, {
66
+ onCloseAll: close,
67
+ autoFocusFirst: enterItems,
68
+ });
69
+ // Only when no item took focus. Focusing the panel unconditionally
70
+ // steals focus back from the item `autoFocusFirst` just landed on, and
71
+ // opening a menu with the keyboard would never reach its first entry.
72
+ if (!enterItems)
73
+ panel.focus({ preventScroll: true });
74
+ // Left/right walk the bar from inside the open panel. `menu.js` claims
75
+ // ArrowLeft for closing a submenu and stops propagation when it does,
76
+ // so this only fires at the top level.
77
+ panel.addEventListener("keydown", onPanelKeyDown);
78
+ },
79
+ onClosed: () => {
80
+ unwire?.();
81
+ unwire = null;
82
+ },
83
+ });
84
+ function onPanelKeyDown(event) {
85
+ if (event.key === "ArrowRight") {
86
+ event.preventDefault();
87
+ move(1);
88
+ }
89
+ else if (event.key === "ArrowLeft") {
90
+ event.preventDefault();
91
+ move(-1);
92
+ }
93
+ }
94
+ return html `<div
95
+ data-slot="menubar"
96
+ role="menubar"
97
+ class="${() => cn("bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs", read(props.class))}"
98
+ >
99
+ ${props.menus.map((menu, index) => renderTrigger(menu, index))}
100
+ </div>`;
101
+ function renderTrigger(menu, index) {
102
+ return html `<button
103
+ type="button"
104
+ role="menuitem"
105
+ data-slot="menubar-trigger"
106
+ id="${triggerIds[index]}"
107
+ aria-haspopup="menu"
108
+ aria-expanded="${() => (openIndex() === index ? "true" : "false")}"
109
+ aria-controls="${() => (openIndex() === index ? contentId : undefined)}"
110
+ data-state="${() => (openIndex() === index ? "open" : "closed")}"
111
+ ?disabled="${menu.disabled === true}"
112
+ class="focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex select-none items-center rounded-sm px-2 py-1 text-sm font-medium outline-none disabled:pointer-events-none disabled:opacity-50"
113
+ @click="${() => (openIndex() === index ? close() : openAt(index, false))}"
114
+ @pointerenter="${() => {
115
+ // Hover switches menus only while one is already open — otherwise
116
+ // the bar would spring menus at a pointer merely passing over it.
117
+ if (openIndex() !== -1)
118
+ openAt(index, false);
119
+ }}"
120
+ @keydown="${(event) => {
121
+ if (event.key === "ArrowDown" ||
122
+ event.key === "Enter" ||
123
+ event.key === " ") {
124
+ event.preventDefault();
125
+ openAt(index, true);
126
+ }
127
+ else if (event.key === "ArrowRight") {
128
+ event.preventDefault();
129
+ focusTrigger(index + 1);
130
+ }
131
+ else if (event.key === "ArrowLeft") {
132
+ event.preventDefault();
133
+ focusTrigger(index - 1);
134
+ }
135
+ }}"
136
+ >${menu.label}</button>`;
137
+ }
138
+ function focusTrigger(index) {
139
+ const count = props.menus.length;
140
+ if (count === 0)
141
+ return;
142
+ const id = triggerIds[(index + count) % count];
143
+ if (id === undefined)
144
+ return;
145
+ document.getElementById(id)?.focus({ preventScroll: true });
146
+ }
147
+ });
@@ -0,0 +1,36 @@
1
+ /**
2
+ * MessageScroller — a conversation that stays pinned to the newest message.
3
+ *
4
+ * The whole component is one rule and its exception: **follow the bottom, but
5
+ * never yank the reader.**
6
+ *
7
+ * A chat log that always scrolls to the end is unusable the moment you scroll
8
+ * up to re-read something and a new message arrives. One that never scrolls is
9
+ * unusable in the ordinary case. So the container remembers whether the reader
10
+ * was already at the bottom when the content grew: if they were, it follows; if
11
+ * they had scrolled away, it holds position and offers a button instead.
12
+ *
13
+ * Growth is detected with a `ResizeObserver` on the inner wrapper rather than
14
+ * by counting children. An image finishing its load, a code block wrapping, a
15
+ * streamed reply growing token by token — none of those change the child count,
16
+ * and all of them move the bottom.
17
+ *
18
+ * `atBottom` carries a tolerance because a scroll position is fractional on a
19
+ * scaled display: `scrollTop + clientHeight` lands a fraction short of
20
+ * `scrollHeight` at the true bottom, and an exact comparison would decide the
21
+ * reader had scrolled away when they had not.
22
+ *
23
+ * `role="log"` with `aria-live="polite"`: new turns are announced as they
24
+ * arrive, without interrupting whatever is being read.
25
+ */
26
+ import { type Slot } from "../lib/children.js";
27
+ import { type Reactive } from "../lib/props.js";
28
+ export interface MessageScrollerProps {
29
+ children?: Slot;
30
+ /** Announced as the log's name. */
31
+ label?: string;
32
+ /** Label for the catch-up button. Default `"Jump to latest"`. */
33
+ jumpLabel?: string;
34
+ class?: Reactive<string>;
35
+ }
36
+ export declare const MessageScroller: (props?: MessageScrollerProps | undefined) => import("@c9up/aurora").TemplateResult;