@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,166 @@
1
+ /**
2
+ * Resizable — two panes with a draggable divider.
3
+ *
4
+ * `react-resizable-panels`, which shadcn wraps, supports arbitrarily nested
5
+ * groups with persisted layouts and collapse-to-zero. nebula ships the case
6
+ * that covers almost every use of it: two panes, one handle. Nesting two of
7
+ * these gets a three-pane layout, which is where the demand stops.
8
+ *
9
+ * The split is one number — the first pane's percentage — driving both panes
10
+ * through `flex-basis`. One source of truth means the two can never disagree,
11
+ * which is the failure mode of tracking a size per pane.
12
+ *
13
+ * Pointer capture on the handle is what keeps the drag alive when the pointer
14
+ * outruns it, which it will: the handle is a few pixels wide and the pointer
15
+ * moves faster than layout. Without capture the drag drops the moment the
16
+ * cursor leaves.
17
+ *
18
+ * The handle is also a real `separator` with `aria-valuenow`, and arrow keys
19
+ * move it. A resizable layout reachable only by dragging is unusable by
20
+ * keyboard, and this is the whole of what it takes to fix that.
21
+ */
22
+
23
+ import { component, html, signal } from "@c9up/aurora";
24
+ import { type Slot, slot } from "../lib/children.js";
25
+ import { cn } from "../lib/cn.js";
26
+ import { GripVerticalIcon } from "../lib/icons.js";
27
+ import { uid } from "../lib/id.js";
28
+ import { type Reactive, read } from "../lib/props.js";
29
+
30
+ export interface ResizableProps {
31
+ first?: Slot;
32
+ second?: Slot;
33
+ direction?: "horizontal" | "vertical";
34
+ /** First pane's share at first render, in percent. Default `50`. */
35
+ defaultSize?: number;
36
+ minSize?: number;
37
+ maxSize?: number;
38
+ /** Show the grip dots on the handle. */
39
+ withHandle?: boolean;
40
+ class?: Reactive<string>;
41
+ onResize?: (percent: number) => void;
42
+ }
43
+
44
+ const KEYBOARD_STEP = 5;
45
+
46
+ export const Resizable = component<ResizableProps>((props) => {
47
+ const horizontal = props.direction !== "vertical";
48
+ const min = props.minSize ?? 10;
49
+ const max = props.maxSize ?? 90;
50
+ const size = signal(clamp(props.defaultSize ?? 50, min, max));
51
+ const rootId = uid("resizable");
52
+
53
+ function setSize(percent: number): void {
54
+ const next = clamp(percent, min, max);
55
+ size(next);
56
+ props.onResize?.(next);
57
+ }
58
+
59
+ /**
60
+ * Convert a pointer position into a percentage of the group.
61
+ *
62
+ * Measured against the group's own rect on every move rather than a rect
63
+ * cached at drag start: the window can be resized mid-drag, and a stale rect
64
+ * makes the divider drift away from the pointer.
65
+ */
66
+ function percentAt(event: PointerEvent): number | null {
67
+ const root = document.getElementById(rootId);
68
+ if (root === null) return null;
69
+ const rect = root.getBoundingClientRect();
70
+ const span = horizontal ? rect.width : rect.height;
71
+ if (span === 0) return null;
72
+ const offset = horizontal
73
+ ? event.clientX - rect.left
74
+ : event.clientY - rect.top;
75
+ return (offset / span) * 100;
76
+ }
77
+
78
+ function onPointerDown(event: PointerEvent): void {
79
+ const target = event.currentTarget;
80
+ if (!(target instanceof HTMLElement)) return;
81
+ event.preventDefault();
82
+ target.setPointerCapture(event.pointerId);
83
+
84
+ const onMove = (move: PointerEvent): void => {
85
+ const percent = percentAt(move);
86
+ if (percent !== null) setSize(percent);
87
+ };
88
+ const onUp = (): void => {
89
+ target.releasePointerCapture(event.pointerId);
90
+ target.removeEventListener("pointermove", onMove);
91
+ target.removeEventListener("pointerup", onUp);
92
+ target.removeEventListener("pointercancel", onUp);
93
+ };
94
+
95
+ target.addEventListener("pointermove", onMove);
96
+ target.addEventListener("pointerup", onUp);
97
+ target.addEventListener("pointercancel", onUp);
98
+ }
99
+
100
+ function onKeyDown(event: KeyboardEvent): void {
101
+ const back = horizontal ? "ArrowLeft" : "ArrowUp";
102
+ const forward = horizontal ? "ArrowRight" : "ArrowDown";
103
+
104
+ if (event.key === back) {
105
+ event.preventDefault();
106
+ setSize(size() - KEYBOARD_STEP);
107
+ } else if (event.key === forward) {
108
+ event.preventDefault();
109
+ setSize(size() + KEYBOARD_STEP);
110
+ } else if (event.key === "Home") {
111
+ event.preventDefault();
112
+ setSize(min);
113
+ } else if (event.key === "End") {
114
+ event.preventDefault();
115
+ setSize(max);
116
+ }
117
+ }
118
+
119
+ return html`<div
120
+ data-slot="resizable-group"
121
+ id="${rootId}"
122
+ data-direction="${horizontal ? "horizontal" : "vertical"}"
123
+ class="${() =>
124
+ cn(
125
+ "flex h-full w-full",
126
+ horizontal ? "flex-row" : "flex-col",
127
+ read(props.class),
128
+ )}"
129
+ >
130
+ <div
131
+ data-slot="resizable-panel"
132
+ class="overflow-hidden"
133
+ style="${() => `flex: 0 0 ${size()}%`}"
134
+ >${slot(props.first)}</div>
135
+ <div
136
+ data-slot="resizable-handle"
137
+ role="separator"
138
+ tabindex="0"
139
+ aria-orientation="${horizontal ? "vertical" : "horizontal"}"
140
+ aria-valuemin="${min}"
141
+ aria-valuemax="${max}"
142
+ aria-valuenow="${() => Math.round(size())}"
143
+ aria-label="Resize panes"
144
+ class="${cn(
145
+ "bg-border relative flex shrink-0 items-center justify-center outline-none",
146
+ "focus-visible:ring-ring focus-visible:ring-1 focus-visible:ring-offset-1",
147
+ horizontal ? "w-px cursor-col-resize" : "h-px cursor-row-resize",
148
+ )}"
149
+ @pointerdown="${onPointerDown}"
150
+ @keydown="${onKeyDown}"
151
+ >
152
+ ${
153
+ props.withHandle === true
154
+ ? html`<span
155
+ class="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border"
156
+ >${GripVerticalIcon({ class: "size-2.5" })}</span>`
157
+ : null
158
+ }
159
+ </div>
160
+ <div data-slot="resizable-panel" class="flex-1 overflow-hidden">${slot(props.second)}</div>
161
+ </div>`;
162
+ });
163
+
164
+ function clamp(value: number, min: number, max: number): number {
165
+ return Math.min(Math.max(value, min), max);
166
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Table — the styled primitives for tabular data.
3
+ *
4
+ * Real table elements, not a grid of divs. A `<table>` gives screen readers
5
+ * row and column context for free ("row 3, Amount, 42"), and reproducing that
6
+ * over divs means hand-maintaining `role="grid"`, `aria-rowindex` and
7
+ * `aria-colindex` on every cell. The container's `overflow-x-auto` handles
8
+ * narrow viewports without touching the semantics.
9
+ *
10
+ * These are the presentation layer only. `DataTable` in `organisms/` adds
11
+ * sorting, selection and pagination on top of them.
12
+ */
13
+
14
+ import { component, html } from "@c9up/aurora";
15
+ import { slot } from "../lib/children.js";
16
+ import { cn } from "../lib/cn.js";
17
+ import { type Reactive, read } from "../lib/props.js";
18
+ import type { StyledProps } from "../lib/styled.js";
19
+
20
+ export type TableProps = StyledProps;
21
+
22
+ export interface TableCellProps extends StyledProps {
23
+ colspan?: number;
24
+ rowspan?: number;
25
+ /** `col` for a column header, `row` for a row header. */
26
+ scope?: "col" | "row";
27
+ /** Reflected as `aria-sort` — set it on the column currently sorted. */
28
+ sort?: Reactive<"ascending" | "descending" | "none" | undefined>;
29
+ onClick?: (event: MouseEvent) => void;
30
+ }
31
+
32
+ export const Table = component<TableProps>((props) => {
33
+ return html`<div
34
+ data-slot="table-container"
35
+ class="relative w-full overflow-x-auto"
36
+ >
37
+ <table
38
+ data-slot="table"
39
+ class="${() => cn("w-full caption-bottom text-sm", read(props.class))}"
40
+ >${slot(props.children)}</table>
41
+ </div>`;
42
+ });
43
+
44
+ export const TableHeader = component<TableProps>((props) => {
45
+ return html`<thead
46
+ data-slot="table-header"
47
+ class="${() => cn("[&_tr]:border-b", read(props.class))}"
48
+ >${slot(props.children)}</thead>`;
49
+ });
50
+
51
+ export const TableBody = component<TableProps>((props) => {
52
+ return html`<tbody
53
+ data-slot="table-body"
54
+ class="${() => cn("[&_tr:last-child]:border-0", read(props.class))}"
55
+ >${slot(props.children)}</tbody>`;
56
+ });
57
+
58
+ export const TableFooter = component<TableProps>((props) => {
59
+ return html`<tfoot
60
+ data-slot="table-footer"
61
+ class="${() => cn("bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", read(props.class))}"
62
+ >${slot(props.children)}</tfoot>`;
63
+ });
64
+
65
+ export interface TableRowProps extends StyledProps {
66
+ /** Reflected as `data-state="selected"`, which the row styling keys off. */
67
+ selected?: Reactive<boolean>;
68
+ onClick?: (event: MouseEvent) => void;
69
+ }
70
+
71
+ export const TableRow = component<TableRowProps>((props) => {
72
+ return html`<tr
73
+ data-slot="table-row"
74
+ data-state="${() => (read(props.selected) === true ? "selected" : undefined)}"
75
+ class="${() =>
76
+ cn(
77
+ "hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
78
+ read(props.class),
79
+ )}"
80
+ @click="${props.onClick}"
81
+ >${slot(props.children)}</tr>`;
82
+ });
83
+
84
+ export const TableHead = component<TableCellProps>((props) => {
85
+ return html`<th
86
+ data-slot="table-head"
87
+ scope="${props.scope ?? "col"}"
88
+ colspan="${props.colspan}"
89
+ rowspan="${props.rowspan}"
90
+ aria-sort="${() => read(props.sort)}"
91
+ class="${() =>
92
+ cn(
93
+ "text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0",
94
+ read(props.class),
95
+ )}"
96
+ @click="${props.onClick}"
97
+ >${slot(props.children)}</th>`;
98
+ });
99
+
100
+ export const TableCell = component<TableCellProps>((props) => {
101
+ return html`<td
102
+ data-slot="table-cell"
103
+ colspan="${props.colspan}"
104
+ rowspan="${props.rowspan}"
105
+ class="${() =>
106
+ cn(
107
+ "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
108
+ read(props.class),
109
+ )}"
110
+ >${slot(props.children)}</td>`;
111
+ });
112
+
113
+ export const TableCaption = component<TableProps>((props) => {
114
+ return html`<caption
115
+ data-slot="table-caption"
116
+ class="${() => cn("text-muted-foreground mt-4 text-sm", read(props.class))}"
117
+ >${slot(props.children)}</caption>`;
118
+ });
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Tabs — one panel visible at a time.
3
+ *
4
+ * The WAI-ARIA tabs pattern in full: the tab list is a single tab stop with
5
+ * arrows moving between tabs, each tab points at its panel with
6
+ * `aria-controls`, and each panel points back with `aria-labelledby`.
7
+ *
8
+ * Activation is automatic — moving to a tab selects it — which is the right
9
+ * default when panels are already rendered and switching costs nothing. The
10
+ * `activateOnFocus: false` escape hatch exists for panels that fetch on
11
+ * display, where arrowing past three tabs would fire three requests.
12
+ *
13
+ * Hidden panels stay mounted so a form or a scroll position inside one
14
+ * survives a trip to another tab. `hidden` keeps them out of the layout and
15
+ * the accessibility tree, which is the behaviour `display: none` would give
16
+ * without also removing them from the DOM.
17
+ */
18
+
19
+ import { component, html, onMount, onUnmount, signal } from "@c9up/aurora";
20
+ import type { Child, Slot } from "../lib/children.js";
21
+ import { slot } from "../lib/children.js";
22
+ import { cn } from "../lib/cn.js";
23
+ import { uid } from "../lib/id.js";
24
+ import { type Reactive, read } from "../lib/props.js";
25
+ import { type RovingFocus, rovingFocus } from "../primitives/rovingFocus.js";
26
+
27
+ export interface TabItem {
28
+ value: string;
29
+ label: Child;
30
+ content: Slot;
31
+ disabled?: boolean;
32
+ }
33
+
34
+ export interface TabsProps {
35
+ items: readonly TabItem[];
36
+ /** Selected at first render. Defaults to the first enabled tab. */
37
+ defaultValue?: string;
38
+ orientation?: "horizontal" | "vertical";
39
+ /** Selecting follows focus. Default `true`. */
40
+ activateOnFocus?: boolean;
41
+ onValueChange?: (value: string) => void;
42
+ class?: Reactive<string>;
43
+ listClass?: Reactive<string>;
44
+ }
45
+
46
+ export const Tabs = component<TabsProps>((props) => {
47
+ const orientation = props.orientation ?? "horizontal";
48
+ const first = props.items.find((item) => item.disabled !== true);
49
+ const active = signal(props.defaultValue ?? first?.value ?? "");
50
+ const listId = uid("tabs-list");
51
+
52
+ // Ids are minted once per tab, up front, because a tab and its panel each
53
+ // need to name the other — one of them has to know both before rendering.
54
+ const ids = new Map<string, { tab: string; panel: string }>();
55
+ for (const item of props.items) {
56
+ ids.set(item.value, { tab: uid("tab"), panel: uid("tab-panel") });
57
+ }
58
+
59
+ function select(value: string): void {
60
+ if (active() === value) return;
61
+ active(value);
62
+ props.onValueChange?.(value);
63
+ }
64
+
65
+ let group: RovingFocus | undefined;
66
+ onMount(() => {
67
+ group = rovingFocus({
68
+ container: () => document.getElementById(listId),
69
+ itemSelector: "[role='tab']",
70
+ orientation,
71
+ onFocusChange: (item) => {
72
+ if (props.activateOnFocus === false) return;
73
+ const value = item.getAttribute("data-value");
74
+ if (value !== null) select(value);
75
+ },
76
+ onSelect: (item) => {
77
+ const value = item.getAttribute("data-value");
78
+ if (value !== null) select(value);
79
+ },
80
+ });
81
+ group.sync();
82
+ });
83
+ onUnmount(() => group?.destroy());
84
+
85
+ return html`<div
86
+ data-slot="tabs"
87
+ data-orientation="${orientation}"
88
+ class="${() => cn("flex gap-2", orientation === "vertical" ? "flex-row" : "flex-col", read(props.class))}"
89
+ >
90
+ <div
91
+ role="tablist"
92
+ data-slot="tabs-list"
93
+ id="${listId}"
94
+ aria-orientation="${orientation}"
95
+ class="${() =>
96
+ cn(
97
+ "bg-muted text-muted-foreground inline-flex w-fit items-center justify-center rounded-lg p-[3px]",
98
+ orientation === "vertical" ? "flex-col" : "h-9",
99
+ read(props.listClass),
100
+ )}"
101
+ >
102
+ ${props.items.map((item) => renderTab(item, ids, active, select))}
103
+ </div>
104
+ ${props.items.map((item) => renderPanel(item, ids, active))}
105
+ </div>`;
106
+ });
107
+
108
+ function renderTab(
109
+ item: TabItem,
110
+ ids: ReadonlyMap<string, { tab: string; panel: string }>,
111
+ active: () => string,
112
+ select: (value: string) => void,
113
+ ): Child {
114
+ const id = ids.get(item.value);
115
+ const selected = (): boolean => active() === item.value;
116
+
117
+ return html`<button
118
+ type="button"
119
+ role="tab"
120
+ data-slot="tabs-trigger"
121
+ data-value="${item.value}"
122
+ data-state="${() => (selected() ? "active" : "inactive")}"
123
+ data-disabled="${item.disabled === true ? "" : undefined}"
124
+ id="${id?.tab}"
125
+ aria-selected="${() => (selected() ? "true" : "false")}"
126
+ aria-controls="${id?.panel}"
127
+ ?disabled="${item.disabled === true}"
128
+ class="data-[state=active]:bg-background data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0"
129
+ @click="${() => select(item.value)}"
130
+ >${item.label}</button>`;
131
+ }
132
+
133
+ function renderPanel(
134
+ item: TabItem,
135
+ ids: ReadonlyMap<string, { tab: string; panel: string }>,
136
+ active: () => string,
137
+ ): Child {
138
+ const id = ids.get(item.value);
139
+ return html`<div
140
+ role="tabpanel"
141
+ data-slot="tabs-content"
142
+ id="${id?.panel}"
143
+ aria-labelledby="${id?.tab}"
144
+ tabindex="0"
145
+ ?hidden="${() => active() !== item.value}"
146
+ class="flex-1 outline-none"
147
+ >${slot(item.content)}</div>`;
148
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * ToggleGroup — a row of toggles acting as one control.
3
+ *
4
+ * `type` decides both the behaviour and the semantics, and they have to move
5
+ * together. `"single"` is a radio group: one value, `role="radio"` on each
6
+ * button, and picking one clears the rest. `"multiple"` is a set of
7
+ * independent toggle buttons, each with `aria-pressed`.
8
+ *
9
+ * Getting that pairing wrong is the usual bug here — a single-select group
10
+ * announcing each option as "pressed" tells a screen-reader user they can turn
11
+ * several on at once.
12
+ *
13
+ * The classes come from `Toggle`'s own `cva`, so a grouped button and a
14
+ * standalone one cannot drift apart. Only the corner rounding is overridden,
15
+ * to weld the row into one shape.
16
+ */
17
+
18
+ import { component, html, signal } from "@c9up/aurora";
19
+ import { type ToggleVariants, toggleVariants } from "../atoms/Toggle.js";
20
+ import type { Child } from "../lib/children.js";
21
+ import { cn } from "../lib/cn.js";
22
+ import { type Reactive, read, readOr } from "../lib/props.js";
23
+
24
+ export interface ToggleGroupItem {
25
+ value: string;
26
+ label: Child;
27
+ /** Required when the label is an icon alone. */
28
+ ariaLabel?: string;
29
+ disabled?: boolean;
30
+ }
31
+
32
+ export interface ToggleGroupProps {
33
+ items: readonly ToggleGroupItem[];
34
+ type?: "single" | "multiple";
35
+ defaultValue?: string | readonly string[];
36
+ variant?: Reactive<ToggleVariants["variant"]>;
37
+ size?: Reactive<ToggleVariants["size"]>;
38
+ disabled?: Reactive<boolean>;
39
+ class?: Reactive<string>;
40
+ onValueChange?: (value: readonly string[]) => void;
41
+ }
42
+
43
+ export const ToggleGroup = component<ToggleGroupProps>((props) => {
44
+ const type = props.type ?? "single";
45
+ const selected = signal<readonly string[]>(
46
+ props.defaultValue === undefined
47
+ ? []
48
+ : typeof props.defaultValue === "string"
49
+ ? [props.defaultValue]
50
+ : props.defaultValue,
51
+ );
52
+
53
+ function toggle(value: string): void {
54
+ const current = selected();
55
+ const next =
56
+ type === "single"
57
+ ? current.includes(value)
58
+ ? []
59
+ : [value]
60
+ : current.includes(value)
61
+ ? current.filter((entry) => entry !== value)
62
+ : [...current, value];
63
+ selected(next);
64
+ props.onValueChange?.(next);
65
+ }
66
+
67
+ return html`<div
68
+ data-slot="toggle-group"
69
+ role="${type === "single" ? "radiogroup" : "group"}"
70
+ class="${() =>
71
+ cn(
72
+ "group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
73
+ read(props.class),
74
+ )}"
75
+ data-variant="${() => readOr(props.variant, "default")}"
76
+ >
77
+ ${props.items.map((item) => renderItem(item, props, type, selected, toggle))}
78
+ </div>`;
79
+ });
80
+
81
+ function renderItem(
82
+ item: ToggleGroupItem,
83
+ props: ToggleGroupProps,
84
+ type: "single" | "multiple",
85
+ selected: () => readonly string[],
86
+ toggle: (value: string) => void,
87
+ ): Child {
88
+ const on = (): boolean => selected().includes(item.value);
89
+
90
+ return html`<button
91
+ type="button"
92
+ data-slot="toggle-group-item"
93
+ data-state="${() => (on() ? "on" : "off")}"
94
+ data-value="${item.value}"
95
+ role="${type === "single" ? "radio" : undefined}"
96
+ aria-checked="${() => (type === "single" ? (on() ? "true" : "false") : undefined)}"
97
+ aria-pressed="${() => (type === "multiple" ? (on() ? "true" : "false") : undefined)}"
98
+ aria-label="${item.ariaLabel}"
99
+ ?disabled="${() => item.disabled === true || readOr(props.disabled, false)}"
100
+ class="${() =>
101
+ toggleVariants({
102
+ variant: read(props.variant),
103
+ size: read(props.size),
104
+ class:
105
+ "min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l aria-checked:bg-accent aria-checked:text-accent-foreground",
106
+ })}"
107
+ @click="${() => toggle(item.value)}"
108
+ >${item.label}</button>`;
109
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Typography — prose styles for content nebula does not otherwise own.
3
+ *
4
+ * shadcn documents these as copy-paste snippets rather than shipping them as
5
+ * components, which leaves every app to re-derive the same scale. They are
6
+ * cheap to ship and they are what keeps a marketing page, a changelog and an
7
+ * empty state reading as one product.
8
+ *
9
+ * `text-balance` on headings and `text-pretty` on body copy do the work here:
10
+ * they stop a two-word orphan on the last line of a heading and a single word
11
+ * ending a paragraph, which is most of what separates typeset text from text.
12
+ */
13
+
14
+ import { component, html } from "@c9up/aurora";
15
+ import { slot } from "../lib/children.js";
16
+ import { cn } from "../lib/cn.js";
17
+ import { read } from "../lib/props.js";
18
+ import { type StyledProps, styledDiv } from "../lib/styled.js";
19
+
20
+ export type TypographyProps = StyledProps;
21
+
22
+ export const H1 = component<TypographyProps>((props) => {
23
+ return html`<h1
24
+ id="${props.id}"
25
+ class="${() =>
26
+ cn(
27
+ "scroll-m-20 text-4xl font-extrabold tracking-tight text-balance",
28
+ read(props.class),
29
+ )}"
30
+ >${slot(props.children)}</h1>`;
31
+ });
32
+
33
+ export const H2 = component<TypographyProps>((props) => {
34
+ return html`<h2
35
+ id="${props.id}"
36
+ class="${() =>
37
+ cn(
38
+ "scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight first:mt-0",
39
+ read(props.class),
40
+ )}"
41
+ >${slot(props.children)}</h2>`;
42
+ });
43
+
44
+ export const H3 = component<TypographyProps>((props) => {
45
+ return html`<h3
46
+ id="${props.id}"
47
+ class="${() => cn("scroll-m-20 text-2xl font-semibold tracking-tight", read(props.class))}"
48
+ >${slot(props.children)}</h3>`;
49
+ });
50
+
51
+ export const H4 = component<TypographyProps>((props) => {
52
+ return html`<h4
53
+ id="${props.id}"
54
+ class="${() => cn("scroll-m-20 text-xl font-semibold tracking-tight", read(props.class))}"
55
+ >${slot(props.children)}</h4>`;
56
+ });
57
+
58
+ export const P = component<TypographyProps>((props) => {
59
+ return html`<p
60
+ id="${props.id}"
61
+ class="${() => cn("leading-7 text-pretty [&:not(:first-child)]:mt-6", read(props.class))}"
62
+ >${slot(props.children)}</p>`;
63
+ });
64
+
65
+ export const Blockquote = component<TypographyProps>((props) => {
66
+ return html`<blockquote
67
+ id="${props.id}"
68
+ class="${() => cn("mt-6 border-l-2 pl-6 italic", read(props.class))}"
69
+ >${slot(props.children)}</blockquote>`;
70
+ });
71
+
72
+ export const InlineCode = component<TypographyProps>((props) => {
73
+ return html`<code
74
+ class="${() =>
75
+ cn(
76
+ "bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold",
77
+ read(props.class),
78
+ )}"
79
+ >${slot(props.children)}</code>`;
80
+ });
81
+
82
+ export const List = component<TypographyProps>((props) => {
83
+ return html`<ul
84
+ class="${() => cn("my-6 ml-6 list-disc [&>li]:mt-2", read(props.class))}"
85
+ >${slot(props.children)}</ul>`;
86
+ });
87
+
88
+ export const Lead = styledDiv(
89
+ "lead",
90
+ "text-muted-foreground text-xl text-pretty",
91
+ );
92
+ export const Large = styledDiv("large", "text-lg font-semibold");
93
+ export const Small = styledDiv("small", "text-sm leading-none font-medium");
94
+ export const Muted = styledDiv("muted", "text-muted-foreground text-sm");