@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,100 @@
1
+ /**
2
+ * Sidebar — the application shell's navigation column.
3
+ *
4
+ * Two presentations from one definition, because a sidebar is not a component
5
+ * so much as a layout decision that changes with the viewport:
6
+ *
7
+ * - On a wide screen it is a column beside the content, collapsible to a strip
8
+ * of icons or away entirely.
9
+ * - On a narrow one it is a Sheet. A 16rem column on a phone leaves nothing
10
+ * for the content, and every attempt to keep it visible ends up as a drawer
11
+ * anyway.
12
+ *
13
+ * The collapsed state is persisted in a cookie rather than `localStorage`. The
14
+ * server renders the shell, and only a cookie is readable there — with
15
+ * `localStorage` the sidebar renders expanded, then snaps shut once the client
16
+ * hydrates, which is visible on every page load.
17
+ *
18
+ * `cmd/ctrl + B` toggles it, the convention from every editor that has one.
19
+ */
20
+ import { type Signal } from "@c9up/aurora";
21
+ import type { Child, Slot } from "../lib/children.js";
22
+ import { type Reactive } from "../lib/props.js";
23
+ import { type StyledProps } from "../lib/styled.js";
24
+ export declare function sidebarState(defaultOpen?: boolean): Signal<boolean>;
25
+ /** Is the sidebar collapsed to its icon rail right now? */
26
+ export declare function sidebarCollapsed(): boolean;
27
+ export interface SidebarProps {
28
+ children?: Slot;
29
+ header?: Slot;
30
+ footer?: Slot;
31
+ /** Which edge. Default `"left"`. */
32
+ side?: "left" | "right";
33
+ /** What collapsing does: shrink to icons, or disappear. Default `"icon"`. */
34
+ collapsible?: "icon" | "offcanvas";
35
+ /** Starting state. Read from the cookie when there is one. */
36
+ defaultOpen?: boolean;
37
+ /** Announced as the navigation landmark's name. */
38
+ label?: string;
39
+ class?: Reactive<string>;
40
+ }
41
+ export declare const Sidebar: (props?: SidebarProps | undefined) => import("@c9up/aurora").TemplateResult;
42
+ export interface SidebarTriggerProps {
43
+ onToggle: () => void;
44
+ class?: Reactive<string>;
45
+ }
46
+ /**
47
+ * The button that opens and closes the sidebar.
48
+ *
49
+ * Kept apart from `Sidebar` because it belongs in the page header, not in the
50
+ * panel — a trigger inside an off-canvas sidebar disappears with it, and there
51
+ * is then no way back.
52
+ */
53
+ export declare const SidebarTrigger: (props?: SidebarTriggerProps | undefined) => import("@c9up/aurora").TemplateResult;
54
+ export declare const SidebarGroup: (props?: StyledProps) => import("@c9up/aurora").TemplateResult;
55
+ export declare const SidebarGroupLabel: (props?: StyledProps) => import("@c9up/aurora").TemplateResult;
56
+ export declare const SidebarMenu: (props?: StyledProps) => import("@c9up/aurora").TemplateResult;
57
+ export interface SidebarMenuItemProps {
58
+ label: Child;
59
+ href?: string;
60
+ icon?: Child;
61
+ active?: Reactive<boolean>;
62
+ /**
63
+ * Shown as a tooltip while the sidebar is collapsed to icons.
64
+ *
65
+ * Only then: an entry whose label is already on screen does not need the
66
+ * same words repeated on hover. Without it a collapsed rail is a column of
67
+ * unlabelled glyphs, so give one to every entry that has an icon.
68
+ */
69
+ tooltip?: string;
70
+ /** A trailing control — the count of unread items, a status dot. */
71
+ badge?: Child;
72
+ onClick?: () => void;
73
+ }
74
+ /**
75
+ * One navigation entry.
76
+ *
77
+ * `aria-current="page"` on the active entry, not just a highlight class. The
78
+ * highlight tells a sighted user where they are; without the attribute nobody
79
+ * else is told at all.
80
+ */
81
+ export declare const SidebarMenuItem: (props?: SidebarMenuItemProps | undefined) => import("@c9up/aurora").TemplateResult;
82
+ /**
83
+ * A second control on a menu row — a "more" menu, a remove button.
84
+ *
85
+ * Absolutely positioned rather than a flex sibling, so it can overlap a long
86
+ * label instead of squeezing it. It disappears with the rail: there is no room
87
+ * for two controls in an icon-width column, and the row's own action is the
88
+ * one that matters.
89
+ */
90
+ export declare const SidebarMenuAction: (props?: SidebarMenuItemProps | undefined) => import("@c9up/aurora").TemplateResult;
91
+ /**
92
+ * A nested list under a menu entry.
93
+ *
94
+ * A real `<ul>`, so the nesting is structure a screen reader can announce
95
+ * rather than an indent. Hidden entirely when collapsed — an indented tree in
96
+ * an icon-width rail is unreadable, and the parent entry is still reachable.
97
+ */
98
+ export declare const SidebarMenuSub: (props?: StyledProps | undefined) => import("@c9up/aurora").TemplateResult;
99
+ /** One row of a nested list. Wrap a `SidebarMenuItem` in it. */
100
+ export declare const SidebarMenuSubItem: (props?: StyledProps) => import("@c9up/aurora").TemplateResult;
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Sidebar — the application shell's navigation column.
3
+ *
4
+ * Two presentations from one definition, because a sidebar is not a component
5
+ * so much as a layout decision that changes with the viewport:
6
+ *
7
+ * - On a wide screen it is a column beside the content, collapsible to a strip
8
+ * of icons or away entirely.
9
+ * - On a narrow one it is a Sheet. A 16rem column on a phone leaves nothing
10
+ * for the content, and every attempt to keep it visible ends up as a drawer
11
+ * anyway.
12
+ *
13
+ * The collapsed state is persisted in a cookie rather than `localStorage`. The
14
+ * server renders the shell, and only a cookie is readable there — with
15
+ * `localStorage` the sidebar renders expanded, then snaps shut once the client
16
+ * hydrates, which is visible on every page load.
17
+ *
18
+ * `cmd/ctrl + B` toggles it, the convention from every editor that has one.
19
+ */
20
+ import { booleanCookie, component, cookieState, html, onMount, onUnmount, signal, } from "@c9up/aurora";
21
+ import { Button } from "../atoms/Button.js";
22
+ import { slot } from "../lib/children.js";
23
+ import { cn } from "../lib/cn.js";
24
+ import { PanelLeftIcon } from "../lib/icons.js";
25
+ import { read } from "../lib/props.js";
26
+ import { styledDiv } from "../lib/styled.js";
27
+ import { Sheet } from "./Sheet.js";
28
+ import { Tooltip } from "./Tooltip.js";
29
+ const COOKIE_NAME = "nebula:sidebar";
30
+ /**
31
+ * The open/collapsed state, shared by every part of the sidebar.
32
+ *
33
+ * shadcn publishes this through a `SidebarProvider` and React context, so that
34
+ * a menu item three levels down can know the sidebar is collapsed and show a
35
+ * tooltip instead of a label. Aurora has no context, and the honest
36
+ * replacement is a module-level signal: an app has one sidebar, so one signal
37
+ * is the whole of what the provider was carrying.
38
+ *
39
+ * Created on first use rather than at import, because `cookieState` reads the
40
+ * cookie immediately and this module is imported during SSR too.
41
+ */
42
+ let sharedOpen;
43
+ export function sidebarState(defaultOpen = true) {
44
+ sharedOpen ??= cookieState(COOKIE_NAME, defaultOpen, booleanCookie, {
45
+ path: "/",
46
+ maxAge: COOKIE_MAX_AGE_DAYS * 24 * 60 * 60,
47
+ sameSite: "lax",
48
+ });
49
+ return sharedOpen;
50
+ }
51
+ /** Is the sidebar collapsed to its icon rail right now? */
52
+ export function sidebarCollapsed() {
53
+ return !sidebarState()();
54
+ }
55
+ const COOKIE_MAX_AGE_DAYS = 365;
56
+ /** Below this width the sidebar becomes a Sheet. Matches Tailwind's `md`. */
57
+ const MOBILE_BREAKPOINT = 768;
58
+ export const Sidebar = component((props) => {
59
+ const side = props.side ?? "left";
60
+ const collapsible = props.collapsible ?? "icon";
61
+ // The shared signal, so the menu items collapse in step with the panel.
62
+ const open = sidebarState(props.defaultOpen ?? true);
63
+ const mobile = signal(false);
64
+ function toggle() {
65
+ open(!open());
66
+ }
67
+ function onKeyDown(event) {
68
+ if (event.key !== "b" && event.key !== "B")
69
+ return;
70
+ if (!event.metaKey && !event.ctrlKey)
71
+ return;
72
+ event.preventDefault();
73
+ toggle();
74
+ }
75
+ // `matchMedia` rather than a resize listener: it fires only when the
76
+ // breakpoint is actually crossed, not on every pixel of a window drag.
77
+ let media;
78
+ function syncMobile() {
79
+ mobile(media?.matches === true);
80
+ }
81
+ onMount(() => {
82
+ media = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
83
+ syncMobile();
84
+ media.addEventListener("change", syncMobile);
85
+ document.addEventListener("keydown", onKeyDown);
86
+ });
87
+ onUnmount(() => {
88
+ media?.removeEventListener("change", syncMobile);
89
+ document.removeEventListener("keydown", onKeyDown);
90
+ });
91
+ const body = () => html `<div data-slot="sidebar-inner" class="flex h-full w-full flex-col">
92
+ ${props.header === undefined
93
+ ? null
94
+ : html `<div data-slot="sidebar-header" class="flex flex-col gap-2 p-2">
95
+ ${slot(props.header)}
96
+ </div>`}
97
+ <div data-slot="sidebar-body" class="min-h-0 flex-1 overflow-auto p-2">
98
+ ${slot(props.children)}
99
+ </div>
100
+ ${props.footer === undefined
101
+ ? null
102
+ : html `<div data-slot="sidebar-footer" class="flex flex-col gap-2 p-2">
103
+ ${slot(props.footer)}
104
+ </div>`}
105
+ </div>`;
106
+ return html `<div data-slot="sidebar-root">
107
+ ${() => mobile()
108
+ ? Sheet({
109
+ title: props.label ?? "Navigation",
110
+ srOnlyTitle: true,
111
+ side,
112
+ open: () => open(),
113
+ onOpenChange: (next) => open(next),
114
+ children: body(),
115
+ contentClass: "bg-sidebar text-sidebar-foreground w-[18rem] p-0",
116
+ })
117
+ : html `<nav
118
+ data-slot="sidebar"
119
+ data-state="${() => (open() ? "expanded" : "collapsed")}"
120
+ data-side="${side}"
121
+ data-collapsible="${collapsible}"
122
+ aria-label="${props.label ?? "Sidebar"}"
123
+ class="${() => cn("bg-sidebar text-sidebar-foreground h-svh shrink-0 overflow-hidden transition-[width] duration-200 ease-linear motion-reduce:transition-none", side === "left" ? "border-r" : "border-l", open()
124
+ ? "w-(--sidebar-width,16rem)"
125
+ : collapsible === "icon"
126
+ ? "w-(--sidebar-width-icon,3rem)"
127
+ : "w-0 border-0", read(props.class))}"
128
+ >${body()}</nav>`}
129
+ </div>`;
130
+ });
131
+ /**
132
+ * The button that opens and closes the sidebar.
133
+ *
134
+ * Kept apart from `Sidebar` because it belongs in the page header, not in the
135
+ * panel — a trigger inside an off-canvas sidebar disappears with it, and there
136
+ * is then no way back.
137
+ */
138
+ export const SidebarTrigger = component((props) => {
139
+ return Button({
140
+ variant: "ghost",
141
+ size: "icon",
142
+ label: "Toggle sidebar",
143
+ class: props.class,
144
+ onClick: props.onToggle,
145
+ children: PanelLeftIcon({ class: "size-4" }),
146
+ });
147
+ });
148
+ export const SidebarGroup = styledDiv("sidebar-group", "flex w-full flex-col gap-1 p-2");
149
+ export const SidebarGroupLabel = styledDiv("sidebar-group-label", "text-sidebar-foreground/70 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium");
150
+ export const SidebarMenu = styledDiv("sidebar-menu", "flex w-full min-w-0 flex-col gap-1");
151
+ /**
152
+ * One navigation entry.
153
+ *
154
+ * `aria-current="page"` on the active entry, not just a highlight class. The
155
+ * highlight tells a sighted user where they are; without the attribute nobody
156
+ * else is told at all.
157
+ */
158
+ export const SidebarMenuItem = component((props) => {
159
+ const classes = "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground aria-[current=page]:bg-sidebar-accent aria-[current=page]:font-medium flex h-8 w-full items-center gap-2 overflow-hidden rounded-md px-2 text-left text-sm outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring [&>svg]:size-4 [&>svg]:shrink-0";
160
+ /**
161
+ * The label and badge fold away with the rail, the icon does not.
162
+ *
163
+ * `sr-only` rather than `hidden`: the entry keeps its accessible name when
164
+ * collapsed, so a screen reader still reads "Settings" off a column that
165
+ * shows only a cog.
166
+ */
167
+ const foldable = () => (sidebarCollapsed() ? "sr-only" : "truncate");
168
+ const body = html `${props.icon}<span class="${foldable}">${props.label}</span>${() => props.badge === undefined || sidebarCollapsed()
169
+ ? null
170
+ : html `<span
171
+ data-slot="sidebar-menu-badge"
172
+ class="text-sidebar-foreground/70 ml-auto flex h-5 min-w-5 shrink-0 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums"
173
+ >${props.badge}</span>`}`;
174
+ const entry = props.href !== undefined
175
+ ? html `<a
176
+ data-slot="sidebar-menu-item"
177
+ href="${props.href}"
178
+ aria-current="${() => (read(props.active) === true ? "page" : undefined)}"
179
+ class="${classes}"
180
+ >${body}</a>`
181
+ : html `<button
182
+ type="button"
183
+ data-slot="sidebar-menu-item"
184
+ aria-current="${() => (read(props.active) === true ? "page" : undefined)}"
185
+ class="${classes}"
186
+ @click="${props.onClick}"
187
+ >${body}</button>`;
188
+ if (props.tooltip === undefined)
189
+ return entry;
190
+ // Wrapped once, not toggled: swapping the wrapper on collapse would remount
191
+ // the entry and lose focus mid-keyboard-navigation. The tooltip decides for
192
+ // itself whether to open, which is the cheaper half to make conditional.
193
+ return html `${() => sidebarCollapsed()
194
+ ? Tooltip({ trigger: entry, content: props.tooltip, placement: "right" })
195
+ : entry}`;
196
+ });
197
+ /**
198
+ * A second control on a menu row — a "more" menu, a remove button.
199
+ *
200
+ * Absolutely positioned rather than a flex sibling, so it can overlap a long
201
+ * label instead of squeezing it. It disappears with the rail: there is no room
202
+ * for two controls in an icon-width column, and the row's own action is the
203
+ * one that matters.
204
+ */
205
+ export const SidebarMenuAction = component((props) => {
206
+ return html `<button
207
+ type="button"
208
+ data-slot="sidebar-menu-action"
209
+ aria-label="${props.tooltip}"
210
+ class="${() => cn("text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-1 right-1 flex aspect-square w-5 items-center justify-center rounded-md outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring [&>svg]:size-4 [&>svg]:shrink-0", sidebarCollapsed() ? "hidden" : "")}"
211
+ @click="${props.onClick}"
212
+ >${props.icon ?? props.label}</button>`;
213
+ });
214
+ /**
215
+ * A nested list under a menu entry.
216
+ *
217
+ * A real `<ul>`, so the nesting is structure a screen reader can announce
218
+ * rather than an indent. Hidden entirely when collapsed — an indented tree in
219
+ * an icon-width rail is unreadable, and the parent entry is still reachable.
220
+ */
221
+ export const SidebarMenuSub = component((props) => {
222
+ return html `<ul
223
+ data-slot="sidebar-menu-sub"
224
+ class="${() => cn("border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5", sidebarCollapsed() ? "hidden" : "", read(props.class))}"
225
+ >${slot(props.children)}</ul>`;
226
+ });
227
+ /** One row of a nested list. Wrap a `SidebarMenuItem` in it. */
228
+ export const SidebarMenuSubItem = styledDiv("sidebar-menu-sub-item", "relative flex min-w-0 items-center");
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Toaster — transient notifications.
3
+ *
4
+ * shadcn delegates this to `sonner`. nebula keeps it in-house, and the shape
5
+ * is the same: one region mounted once near the app root, and a `toast()`
6
+ * function callable from anywhere with no reference to it.
7
+ *
8
+ * That decoupling is the whole design. A queue in module state, a signal the
9
+ * region subscribes to, and `toast.error(…)` works from a route handler's
10
+ * error branch without threading a handle down to it.
11
+ *
12
+ * The accessibility rules are stricter than they look, and the region is
13
+ * static so they can be satisfied:
14
+ *
15
+ * - The live region must exist **before** the toast does. A live region
16
+ * inserted with content already in it is not announced by most screen
17
+ * readers, which is why the region mounts empty and stays.
18
+ * - `polite` for ordinary toasts, `assertive` for errors. Everything
19
+ * interrupting the user is the fastest way to make a screen reader
20
+ * unusable — so there are two regions, and each toast picks one.
21
+ * - Hovering pauses the timers. A toast that vanishes while being read is the
22
+ * single most common complaint about the pattern.
23
+ */
24
+ import type { Child } from "../lib/children.js";
25
+ import { type Reactive } from "../lib/props.js";
26
+ export type ToastVariant = "default" | "success" | "error" | "warning";
27
+ export interface ToastOptions {
28
+ title: Child;
29
+ description?: Child;
30
+ variant?: ToastVariant;
31
+ /** Milliseconds on screen. `0` keeps it until dismissed. */
32
+ duration?: number;
33
+ action?: {
34
+ label: string;
35
+ onClick: () => void;
36
+ };
37
+ }
38
+ declare function dismiss(id: number): void;
39
+ declare function show(options: ToastOptions): number;
40
+ /**
41
+ * Raise a toast. Returns its id, which `dismiss` accepts.
42
+ *
43
+ * Callable from anywhere — no component, no context, no handle.
44
+ */
45
+ export declare const toast: {
46
+ show: typeof show;
47
+ dismiss: typeof dismiss;
48
+ success: (title: Child, description?: Child) => number;
49
+ error: (title: Child, description?: Child) => number;
50
+ warning: (title: Child, description?: Child) => number;
51
+ /** Clear everything — on navigation, say. */
52
+ clear(): void;
53
+ };
54
+ export interface ToasterProps {
55
+ /** Which corner. Default `"bottom-right"`. */
56
+ position?: "top-right" | "top-left" | "bottom-right" | "bottom-left";
57
+ class?: Reactive<string>;
58
+ }
59
+ /**
60
+ * Mount once, near the root of the app.
61
+ *
62
+ * `data-nebula-live` marks it as exempt from the `aria-hidden` sweep a modal
63
+ * performs over its siblings — a toast raised while a dialog is open still has
64
+ * to be announced.
65
+ */
66
+ export declare const Toaster: (props?: ToasterProps | undefined) => import("@c9up/aurora").TemplateResult;
67
+ export {};
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Toaster — transient notifications.
3
+ *
4
+ * shadcn delegates this to `sonner`. nebula keeps it in-house, and the shape
5
+ * is the same: one region mounted once near the app root, and a `toast()`
6
+ * function callable from anywhere with no reference to it.
7
+ *
8
+ * That decoupling is the whole design. A queue in module state, a signal the
9
+ * region subscribes to, and `toast.error(…)` works from a route handler's
10
+ * error branch without threading a handle down to it.
11
+ *
12
+ * The accessibility rules are stricter than they look, and the region is
13
+ * static so they can be satisfied:
14
+ *
15
+ * - The live region must exist **before** the toast does. A live region
16
+ * inserted with content already in it is not announced by most screen
17
+ * readers, which is why the region mounts empty and stays.
18
+ * - `polite` for ordinary toasts, `assertive` for errors. Everything
19
+ * interrupting the user is the fastest way to make a screen reader
20
+ * unusable — so there are two regions, and each toast picks one.
21
+ * - Hovering pauses the timers. A toast that vanishes while being read is the
22
+ * single most common complaint about the pattern.
23
+ */
24
+ import { component, html, signal } from "@c9up/aurora";
25
+ import { cn } from "../lib/cn.js";
26
+ import { XIcon } from "../lib/icons.js";
27
+ import { fadeInOut } from "../lib/motion.js";
28
+ import { read } from "../lib/props.js";
29
+ const DEFAULT_DURATION_MS = 5000;
30
+ const queue = signal([]);
31
+ const timers = new Map();
32
+ /** When each toast is due to disappear, as an epoch timestamp. */
33
+ const deadlines = new Map();
34
+ /** Time each toast had left when the pointer entered the region. */
35
+ const remainders = new Map();
36
+ let nextId = 0;
37
+ function dismiss(id) {
38
+ clearTimer(id);
39
+ deadlines.delete(id);
40
+ remainders.delete(id);
41
+ queue(queue().filter((entry) => entry.id !== id));
42
+ }
43
+ function clearTimer(id) {
44
+ const timer = timers.get(id);
45
+ if (timer !== undefined)
46
+ clearTimeout(timer);
47
+ timers.delete(id);
48
+ }
49
+ function schedule(id, duration) {
50
+ // A duration of zero means "stays until dismissed", so no timer and no
51
+ // deadline — which is also what keeps pause/resume from resurrecting one.
52
+ if (duration <= 0)
53
+ return;
54
+ clearTimer(id);
55
+ timers.set(id, setTimeout(() => dismiss(id), duration));
56
+ deadlines.set(id, Date.now() + duration);
57
+ }
58
+ /** Hold every countdown while the pointer is over the region. */
59
+ function pauseAll() {
60
+ const now = Date.now();
61
+ for (const [id, deadline] of deadlines) {
62
+ clearTimer(id);
63
+ remainders.set(id, Math.max(deadline - now, 0));
64
+ }
65
+ }
66
+ /**
67
+ * Resume every held countdown.
68
+ *
69
+ * A minimum is applied: a toast the pointer rested on until its time ran out
70
+ * would otherwise vanish the instant the pointer left, which reads as the
71
+ * hover having dismissed it.
72
+ */
73
+ function resumeAll(minimumMs) {
74
+ for (const [id, remaining] of remainders) {
75
+ schedule(id, Math.max(remaining, minimumMs));
76
+ }
77
+ remainders.clear();
78
+ }
79
+ function show(options) {
80
+ nextId += 1;
81
+ const id = nextId;
82
+ const entry = {
83
+ ...options,
84
+ id,
85
+ variant: options.variant ?? "default",
86
+ };
87
+ queue([...queue(), entry]);
88
+ schedule(id, options.duration ?? DEFAULT_DURATION_MS);
89
+ return id;
90
+ }
91
+ /**
92
+ * Raise a toast. Returns its id, which `dismiss` accepts.
93
+ *
94
+ * Callable from anywhere — no component, no context, no handle.
95
+ */
96
+ export const toast = {
97
+ show,
98
+ dismiss,
99
+ success: (title, description) => show({ title, description, variant: "success" }),
100
+ error: (title, description) =>
101
+ // Errors stay twice as long: the user has to read them, and often act.
102
+ show({
103
+ title,
104
+ description,
105
+ variant: "error",
106
+ duration: DEFAULT_DURATION_MS * 2,
107
+ }),
108
+ warning: (title, description) => show({ title, description, variant: "warning" }),
109
+ /** Clear everything — on navigation, say. */
110
+ clear() {
111
+ for (const timer of timers.values())
112
+ clearTimeout(timer);
113
+ timers.clear();
114
+ deadlines.clear();
115
+ remainders.clear();
116
+ queue([]);
117
+ },
118
+ };
119
+ const variantClasses = {
120
+ default: "bg-popover text-popover-foreground border",
121
+ success: "bg-popover text-popover-foreground border-l-4 border-l-primary border",
122
+ error: "bg-popover text-destructive border-l-4 border-l-destructive border",
123
+ warning: "bg-popover text-popover-foreground border-l-4 border-l-chart-4 border",
124
+ };
125
+ const positionClasses = {
126
+ "top-right": "top-0 right-0 flex-col",
127
+ "top-left": "top-0 left-0 flex-col",
128
+ "bottom-right": "bottom-0 right-0 flex-col-reverse",
129
+ "bottom-left": "bottom-0 left-0 flex-col-reverse",
130
+ };
131
+ /**
132
+ * Mount once, near the root of the app.
133
+ *
134
+ * `data-nebula-live` marks it as exempt from the `aria-hidden` sweep a modal
135
+ * performs over its siblings — a toast raised while a dialog is open still has
136
+ * to be announced.
137
+ */
138
+ export const Toaster = component((props) => {
139
+ const position = props.position ?? "bottom-right";
140
+ return html `<div
141
+ data-slot="toaster"
142
+ data-nebula-live
143
+ class="${() => cn("pointer-events-none fixed z-100 flex max-h-screen w-full p-4 sm:max-w-sm", positionClasses[position], read(props.class))}"
144
+ @pointerenter="${pauseAll}"
145
+ @pointerleave="${() => resumeAll(1000)}"
146
+ >
147
+ <div role="status" aria-live="polite" aria-atomic="false" class="contents">
148
+ ${() => queue()
149
+ .filter((entry) => entry.variant !== "error")
150
+ .map(renderToast)}
151
+ </div>
152
+ <div role="alert" aria-live="assertive" aria-atomic="false" class="contents">
153
+ ${() => queue()
154
+ .filter((entry) => entry.variant === "error")
155
+ .map(renderToast)}
156
+ </div>
157
+ </div>`;
158
+ });
159
+ function renderToast(entry) {
160
+ return html `<div
161
+ data-slot="toast"
162
+ data-variant="${entry.variant}"
163
+ data-state="open"
164
+ class="${cn("pointer-events-auto mt-2 flex w-full items-start gap-3 rounded-md p-4 shadow-lg", variantClasses[entry.variant], fadeInOut)}"
165
+ >
166
+ <div class="flex-1 text-sm">
167
+ <div class="font-medium">${entry.title}</div>
168
+ ${entry.description === undefined
169
+ ? null
170
+ : html `<div class="text-muted-foreground mt-1">${entry.description}</div>`}
171
+ </div>
172
+ ${entry.action === undefined
173
+ ? null
174
+ : html `<button
175
+ type="button"
176
+ class="text-sm font-medium underline-offset-4 hover:underline"
177
+ @click="${() => {
178
+ entry.action?.onClick();
179
+ dismiss(entry.id);
180
+ }}"
181
+ >${entry.action.label}</button>`}
182
+ <button
183
+ type="button"
184
+ aria-label="Dismiss notification"
185
+ class="opacity-60 transition-opacity hover:opacity-100"
186
+ @click="${() => dismiss(entry.id)}"
187
+ >${XIcon({ class: "size-4" })}</button>
188
+ </div>`;
189
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Tooltip — a label that appears on hover or focus.
3
+ *
4
+ * Three rules separate a usable tooltip from an irritating one, and all three
5
+ * are about time rather than appearance:
6
+ *
7
+ * - An **open delay**, so moving the pointer across a toolbar does not fire
8
+ * six tooltips on the way past.
9
+ * - A **close delay**, so travelling from the trigger to the tooltip — which a
10
+ * user does to read a long one, or click a link inside — does not dismiss it
11
+ * halfway.
12
+ * - **No delay between neighbours.** Once one tooltip is showing, the next one
13
+ * appears instantly; the user has clearly decided to browse the toolbar, and
14
+ * re-serving the delay each time makes the interface feel stuck. That is why
15
+ * `lastClosedAt` is module state and not per-component.
16
+ *
17
+ * Focus opens it with no delay at all. A keyboard user landing on a control
18
+ * has already committed to it, and a delay just looks like lag.
19
+ *
20
+ * `role="tooltip"` plus `aria-describedby` — never `aria-labelledby`. A
21
+ * tooltip supplements a control's name; it is not the name. A button labelled
22
+ * only by its tooltip is unlabelled to anything that does not hover.
23
+ */
24
+ import type { Child } from "../lib/children.js";
25
+ import { type Slot } from "../lib/children.js";
26
+ import { type Reactive } from "../lib/props.js";
27
+ import type { Placement } from "../primitives/floating.js";
28
+ export interface TooltipProps {
29
+ /** The control being described. */
30
+ trigger?: Slot;
31
+ /** The tooltip text. Keep it to a phrase. */
32
+ content?: Child;
33
+ placement?: Placement;
34
+ openDelay?: number;
35
+ closeDelay?: number;
36
+ class?: Reactive<string>;
37
+ contentClass?: Reactive<string>;
38
+ }
39
+ export declare const Tooltip: (props?: TooltipProps | undefined) => import("@c9up/aurora").TemplateResult;