@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,246 @@
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
+
25
+ import { component, html, signal } from "@c9up/aurora";
26
+ import type { Child } from "../lib/children.js";
27
+ import { cn } from "../lib/cn.js";
28
+ import { XIcon } from "../lib/icons.js";
29
+ import { fadeInOut } from "../lib/motion.js";
30
+ import { type Reactive, read } from "../lib/props.js";
31
+
32
+ export type ToastVariant = "default" | "success" | "error" | "warning";
33
+
34
+ export interface ToastOptions {
35
+ title: Child;
36
+ description?: Child;
37
+ variant?: ToastVariant;
38
+ /** Milliseconds on screen. `0` keeps it until dismissed. */
39
+ duration?: number;
40
+ action?: { label: string; onClick: () => void };
41
+ }
42
+
43
+ interface ActiveToast extends ToastOptions {
44
+ readonly id: number;
45
+ readonly variant: ToastVariant;
46
+ }
47
+
48
+ const DEFAULT_DURATION_MS = 5000;
49
+
50
+ const queue = signal<readonly ActiveToast[]>([]);
51
+ const timers = new Map<number, ReturnType<typeof setTimeout>>();
52
+ /** When each toast is due to disappear, as an epoch timestamp. */
53
+ const deadlines = new Map<number, number>();
54
+ /** Time each toast had left when the pointer entered the region. */
55
+ const remainders = new Map<number, number>();
56
+ let nextId = 0;
57
+
58
+ function dismiss(id: number): void {
59
+ clearTimer(id);
60
+ deadlines.delete(id);
61
+ remainders.delete(id);
62
+ queue(queue().filter((entry) => entry.id !== id));
63
+ }
64
+
65
+ function clearTimer(id: number): void {
66
+ const timer = timers.get(id);
67
+ if (timer !== undefined) clearTimeout(timer);
68
+ timers.delete(id);
69
+ }
70
+
71
+ function schedule(id: number, duration: number): void {
72
+ // A duration of zero means "stays until dismissed", so no timer and no
73
+ // deadline — which is also what keeps pause/resume from resurrecting one.
74
+ if (duration <= 0) return;
75
+ clearTimer(id);
76
+ timers.set(
77
+ id,
78
+ setTimeout(() => dismiss(id), duration),
79
+ );
80
+ deadlines.set(id, Date.now() + duration);
81
+ }
82
+
83
+ /** Hold every countdown while the pointer is over the region. */
84
+ function pauseAll(): void {
85
+ const now = Date.now();
86
+ for (const [id, deadline] of deadlines) {
87
+ clearTimer(id);
88
+ remainders.set(id, Math.max(deadline - now, 0));
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Resume every held countdown.
94
+ *
95
+ * A minimum is applied: a toast the pointer rested on until its time ran out
96
+ * would otherwise vanish the instant the pointer left, which reads as the
97
+ * hover having dismissed it.
98
+ */
99
+ function resumeAll(minimumMs: number): void {
100
+ for (const [id, remaining] of remainders) {
101
+ schedule(id, Math.max(remaining, minimumMs));
102
+ }
103
+ remainders.clear();
104
+ }
105
+
106
+ function show(options: ToastOptions): number {
107
+ nextId += 1;
108
+ const id = nextId;
109
+ const entry: ActiveToast = {
110
+ ...options,
111
+ id,
112
+ variant: options.variant ?? "default",
113
+ };
114
+ queue([...queue(), entry]);
115
+ schedule(id, options.duration ?? DEFAULT_DURATION_MS);
116
+ return id;
117
+ }
118
+
119
+ /**
120
+ * Raise a toast. Returns its id, which `dismiss` accepts.
121
+ *
122
+ * Callable from anywhere — no component, no context, no handle.
123
+ */
124
+ export const toast = {
125
+ show,
126
+ dismiss,
127
+ success: (title: Child, description?: Child): number =>
128
+ show({ title, description, variant: "success" }),
129
+ error: (title: Child, description?: Child): number =>
130
+ // Errors stay twice as long: the user has to read them, and often act.
131
+ show({
132
+ title,
133
+ description,
134
+ variant: "error",
135
+ duration: DEFAULT_DURATION_MS * 2,
136
+ }),
137
+ warning: (title: Child, description?: Child): number =>
138
+ show({ title, description, variant: "warning" }),
139
+ /** Clear everything — on navigation, say. */
140
+ clear(): void {
141
+ for (const timer of timers.values()) clearTimeout(timer);
142
+ timers.clear();
143
+ deadlines.clear();
144
+ remainders.clear();
145
+ queue([]);
146
+ },
147
+ };
148
+
149
+ const variantClasses: Record<ToastVariant, string> = {
150
+ default: "bg-popover text-popover-foreground border",
151
+ success:
152
+ "bg-popover text-popover-foreground border-l-4 border-l-primary border",
153
+ error: "bg-popover text-destructive border-l-4 border-l-destructive border",
154
+ warning:
155
+ "bg-popover text-popover-foreground border-l-4 border-l-chart-4 border",
156
+ };
157
+
158
+ export interface ToasterProps {
159
+ /** Which corner. Default `"bottom-right"`. */
160
+ position?: "top-right" | "top-left" | "bottom-right" | "bottom-left";
161
+ class?: Reactive<string>;
162
+ }
163
+
164
+ const positionClasses: Record<NonNullable<ToasterProps["position"]>, string> = {
165
+ "top-right": "top-0 right-0 flex-col",
166
+ "top-left": "top-0 left-0 flex-col",
167
+ "bottom-right": "bottom-0 right-0 flex-col-reverse",
168
+ "bottom-left": "bottom-0 left-0 flex-col-reverse",
169
+ };
170
+
171
+ /**
172
+ * Mount once, near the root of the app.
173
+ *
174
+ * `data-nebula-live` marks it as exempt from the `aria-hidden` sweep a modal
175
+ * performs over its siblings — a toast raised while a dialog is open still has
176
+ * to be announced.
177
+ */
178
+ export const Toaster = component<ToasterProps>((props) => {
179
+ const position = props.position ?? "bottom-right";
180
+
181
+ return html`<div
182
+ data-slot="toaster"
183
+ data-nebula-live
184
+ class="${() =>
185
+ cn(
186
+ "pointer-events-none fixed z-100 flex max-h-screen w-full p-4 sm:max-w-sm",
187
+ positionClasses[position],
188
+ read(props.class),
189
+ )}"
190
+ @pointerenter="${pauseAll}"
191
+ @pointerleave="${() => resumeAll(1000)}"
192
+ >
193
+ <div role="status" aria-live="polite" aria-atomic="false" class="contents">
194
+ ${() =>
195
+ queue()
196
+ .filter((entry) => entry.variant !== "error")
197
+ .map(renderToast)}
198
+ </div>
199
+ <div role="alert" aria-live="assertive" aria-atomic="false" class="contents">
200
+ ${() =>
201
+ queue()
202
+ .filter((entry) => entry.variant === "error")
203
+ .map(renderToast)}
204
+ </div>
205
+ </div>`;
206
+ });
207
+
208
+ function renderToast(entry: ActiveToast): Child {
209
+ return html`<div
210
+ data-slot="toast"
211
+ data-variant="${entry.variant}"
212
+ data-state="open"
213
+ class="${cn(
214
+ "pointer-events-auto mt-2 flex w-full items-start gap-3 rounded-md p-4 shadow-lg",
215
+ variantClasses[entry.variant],
216
+ fadeInOut,
217
+ )}"
218
+ >
219
+ <div class="flex-1 text-sm">
220
+ <div class="font-medium">${entry.title}</div>
221
+ ${
222
+ entry.description === undefined
223
+ ? null
224
+ : html`<div class="text-muted-foreground mt-1">${entry.description}</div>`
225
+ }
226
+ </div>
227
+ ${
228
+ entry.action === undefined
229
+ ? null
230
+ : html`<button
231
+ type="button"
232
+ class="text-sm font-medium underline-offset-4 hover:underline"
233
+ @click="${() => {
234
+ entry.action?.onClick();
235
+ dismiss(entry.id);
236
+ }}"
237
+ >${entry.action.label}</button>`
238
+ }
239
+ <button
240
+ type="button"
241
+ aria-label="Dismiss notification"
242
+ class="opacity-60 transition-opacity hover:opacity-100"
243
+ @click="${() => dismiss(entry.id)}"
244
+ >${XIcon({ class: "size-4" })}</button>
245
+ </div>`;
246
+ }
@@ -0,0 +1,124 @@
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
+
25
+ import { component, html, onUnmount, signal } from "@c9up/aurora";
26
+ import type { Child } from "../lib/children.js";
27
+ import { type Slot, slot } from "../lib/children.js";
28
+ import { cn } from "../lib/cn.js";
29
+ import { uid } from "../lib/id.js";
30
+ import { fadeInOut } from "../lib/motion.js";
31
+ import { type Reactive, read } from "../lib/props.js";
32
+ import type { Placement } from "../primitives/floating.js";
33
+ import { floatingSurface } from "../primitives/floatingSurface.js";
34
+
35
+ const OPEN_DELAY_MS = 500;
36
+ const CLOSE_DELAY_MS = 150;
37
+ /** How long after one closes that the next opens instantly. */
38
+ const GRACE_MS = 300;
39
+
40
+ /** Shared across every tooltip on the page — see the "no delay" rule above. */
41
+ let lastClosedAt = 0;
42
+
43
+ export interface TooltipProps {
44
+ /** The control being described. */
45
+ trigger?: Slot;
46
+ /** The tooltip text. Keep it to a phrase. */
47
+ content?: Child;
48
+ placement?: Placement;
49
+ openDelay?: number;
50
+ closeDelay?: number;
51
+ class?: Reactive<string>;
52
+ contentClass?: Reactive<string>;
53
+ }
54
+
55
+ export const Tooltip = component<TooltipProps>((props) => {
56
+ const triggerId = uid("tooltip-trigger");
57
+ const contentId = uid("tooltip-content");
58
+ const open = signal(false);
59
+
60
+ let timer: ReturnType<typeof setTimeout> | undefined;
61
+
62
+ function clear(): void {
63
+ if (timer !== undefined) clearTimeout(timer);
64
+ timer = undefined;
65
+ }
66
+
67
+ function scheduleOpen(immediate: boolean): void {
68
+ clear();
69
+ const withinGrace = Date.now() - lastClosedAt < GRACE_MS;
70
+ const delay =
71
+ immediate || withinGrace ? 0 : (props.openDelay ?? OPEN_DELAY_MS);
72
+ if (delay === 0) {
73
+ open(true);
74
+ return;
75
+ }
76
+ timer = setTimeout(() => open(true), delay);
77
+ }
78
+
79
+ function scheduleClose(): void {
80
+ clear();
81
+ timer = setTimeout(() => {
82
+ open(false);
83
+ lastClosedAt = Date.now();
84
+ }, props.closeDelay ?? CLOSE_DELAY_MS);
85
+ }
86
+
87
+ onUnmount(clear);
88
+
89
+ floatingSurface({
90
+ anchor: () => document.getElementById(triggerId),
91
+ open: () => open(),
92
+ onClose: () => {
93
+ clear();
94
+ open(false);
95
+ },
96
+ placement: props.placement ?? "top",
97
+ offset: 6,
98
+ outsidePointer: false,
99
+ content: () =>
100
+ html`<div
101
+ data-slot="tooltip-content"
102
+ id="${contentId}"
103
+ role="tooltip"
104
+ class="${cn(
105
+ "bg-primary text-primary-foreground z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance",
106
+ fadeInOut,
107
+ read(props.contentClass),
108
+ )}"
109
+ @pointerenter="${clear}"
110
+ @pointerleave="${scheduleClose}"
111
+ >${props.content}</div>`,
112
+ });
113
+
114
+ return html`<span
115
+ data-slot="tooltip-trigger"
116
+ id="${triggerId}"
117
+ aria-describedby="${() => (open() ? contentId : undefined)}"
118
+ class="${() => cn("inline-flex", read(props.class))}"
119
+ @pointerenter="${() => scheduleOpen(false)}"
120
+ @pointerleave="${scheduleClose}"
121
+ @focusin="${() => scheduleOpen(true)}"
122
+ @focusout="${scheduleClose}"
123
+ >${slot(props.trigger)}</span>`;
124
+ });
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Organisms — components that escape their own DOM position.
3
+ *
4
+ * The line that separates these from molecules: an organism portals, traps
5
+ * focus, floats against an anchor, or coordinates several molecules into one
6
+ * working unit. Dialog is an organism though its markup is simpler than
7
+ * Table's, because it renders into `<body>` and takes over the page while it
8
+ * is open.
9
+ *
10
+ * Almost all of them are assembled from two shared surfaces —
11
+ * `floatingSurface` for the anchored overlays, `modalSurface` for the modal
12
+ * ones — so the parts that are easy to get wrong (teardown order, focus
13
+ * restoration, hiding the page from assistive technology) exist once.
14
+ */
15
+
16
+ export { AlertDialog, type AlertDialogProps } from "./AlertDialog.js";
17
+ export {
18
+ addDays,
19
+ addMonths,
20
+ Calendar,
21
+ type CalendarProps,
22
+ type DateRange,
23
+ isSameDay,
24
+ monthGrid,
25
+ startOfDay,
26
+ } from "./Calendar.js";
27
+ export { Carousel, type CarouselProps } from "./Carousel.js";
28
+ export {
29
+ Chart,
30
+ type ChartDatum,
31
+ type ChartProps,
32
+ type ChartSeries,
33
+ valueRange,
34
+ } from "./Chart.js";
35
+ export {
36
+ Combobox,
37
+ type ComboboxOption,
38
+ type ComboboxProps,
39
+ } from "./Combobox.js";
40
+ export {
41
+ Command,
42
+ type CommandItem,
43
+ type CommandProps,
44
+ defaultFilter,
45
+ } from "./Command.js";
46
+ export { CommandDialog, type CommandDialogProps } from "./CommandDialog.js";
47
+ export { ContextMenu, type ContextMenuProps } from "./ContextMenu.js";
48
+ export { type Column, DataTable, type DataTableProps } from "./DataTable.js";
49
+ export { DatePicker, type DatePickerProps, toISODate } from "./DatePicker.js";
50
+ export {
51
+ DateRangePicker,
52
+ type DateRangePickerProps,
53
+ } from "./DateRangePicker.js";
54
+ export {
55
+ Dialog,
56
+ type DialogProps,
57
+ dialogBackdropClasses,
58
+ dialogPanelClasses,
59
+ useDialog,
60
+ } from "./Dialog.js";
61
+ export { Drawer, type DrawerProps } from "./Drawer.js";
62
+ export { DropdownMenu, type DropdownMenuProps } from "./DropdownMenu.js";
63
+ export {
64
+ bind,
65
+ type FieldBinding,
66
+ Form,
67
+ type FormProps,
68
+ SubmitButton,
69
+ type SubmitButtonProps,
70
+ TextAreaField,
71
+ type TextAreaFieldProps,
72
+ TextField,
73
+ type TextFieldProps,
74
+ } from "./Form.js";
75
+ export { HoverCard, type HoverCardProps } from "./HoverCard.js";
76
+ export { Menubar, type MenubarMenu, type MenubarProps } from "./Menubar.js";
77
+ export {
78
+ MessageScroller,
79
+ type MessageScrollerProps,
80
+ } from "./MessageScroller.js";
81
+ export {
82
+ type NavigationItem,
83
+ type NavigationLink,
84
+ NavigationMenu,
85
+ type NavigationMenuProps,
86
+ } from "./NavigationMenu.js";
87
+ export {
88
+ Popover,
89
+ type PopoverProps,
90
+ popoverContentClasses,
91
+ } from "./Popover.js";
92
+ export {
93
+ type Answer,
94
+ type Answers,
95
+ type FreeformQuestion,
96
+ isAnswered,
97
+ type MultipleChoiceQuestion,
98
+ type Question,
99
+ Questionnaire,
100
+ type QuestionnaireProps,
101
+ type QuestionOption,
102
+ type SingleChoiceQuestion,
103
+ } from "./Questionnaire.js";
104
+ export {
105
+ Select,
106
+ type SelectOption,
107
+ type SelectProps,
108
+ selectTriggerClasses,
109
+ } from "./Select.js";
110
+ export { Sheet, type SheetProps } from "./Sheet.js";
111
+ export {
112
+ Sidebar,
113
+ SidebarGroup,
114
+ SidebarGroupLabel,
115
+ SidebarMenu,
116
+ SidebarMenuAction,
117
+ SidebarMenuItem,
118
+ type SidebarMenuItemProps,
119
+ SidebarMenuSub,
120
+ SidebarMenuSubItem,
121
+ type SidebarProps,
122
+ SidebarTrigger,
123
+ type SidebarTriggerProps,
124
+ sidebarCollapsed,
125
+ sidebarState,
126
+ } from "./Sidebar.js";
127
+ export {
128
+ Toaster,
129
+ type ToasterProps,
130
+ type ToastOptions,
131
+ type ToastVariant,
132
+ toast,
133
+ } from "./Toaster.js";
134
+ export { Tooltip, type TooltipProps } from "./Tooltip.js";
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Controlled / uncontrolled state.
3
+ *
4
+ * Every stateful shadcn component takes the same trio: `open` (the caller
5
+ * owns the state), `defaultOpen` (the component owns it) and `onOpenChange`
6
+ * (told either way). Radix folds that into `useControllableState`; this is the
7
+ * Aurora equivalent, and the reason a Dialog can be opened either by its own
8
+ * trigger or by a signal somewhere else in the app without changing shape.
9
+ *
10
+ * // uncontrolled — nebula holds the state
11
+ * Dialog({ defaultOpen: false })
12
+ *
13
+ * // controlled — the app holds it, nebula only asks
14
+ * const open = signal(false)
15
+ * Dialog({ open, onOpenChange: open })
16
+ *
17
+ * The rule is one line: a `value` that reads as anything other than
18
+ * `undefined` wins over the internal signal, on every read. Deciding once at
19
+ * setup instead would freeze a component whose prop only becomes defined after
20
+ * an async load.
21
+ */
22
+
23
+ import { memo, type ReadSignal, signal } from "@c9up/aurora";
24
+ import { type Reactive, read } from "../lib/props.js";
25
+
26
+ export interface ControllableOptions<T> {
27
+ /** The caller's value. Present means controlled. */
28
+ value?: Reactive<T | undefined>;
29
+ /** Starting value when uncontrolled. */
30
+ initial: T;
31
+ /** Notified on every change, controlled or not. */
32
+ onChange?: (next: T) => void;
33
+ }
34
+
35
+ export interface Controllable<T> {
36
+ /** Current value — the caller's when controlled, ours otherwise. */
37
+ readonly current: ReadSignal<T>;
38
+ /** Request a change. Always notifies; only writes internally when uncontrolled. */
39
+ set(next: T): void;
40
+ }
41
+
42
+ export function controllable<T>(
43
+ options: ControllableOptions<T>,
44
+ ): Controllable<T> {
45
+ const internal = signal(options.initial);
46
+
47
+ const current = memo(() => {
48
+ if (options.value === undefined) return internal();
49
+ const external = read(options.value);
50
+ return external === undefined ? internal() : external;
51
+ });
52
+
53
+ return {
54
+ current,
55
+ set(next: T): void {
56
+ // Written even when controlled. The caller may ignore `onChange` and
57
+ // leave its own state put — a "cancel" on a confirm dialog does exactly
58
+ // that — and the internal value has to stay in step for the moment the
59
+ // caller stops controlling it.
60
+ internal(next);
61
+ options.onChange?.(next);
62
+ },
63
+ };
64
+ }