@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,189 @@
1
+ /**
2
+ * Chart — line, area and bar charts drawn as inline SVG.
3
+ *
4
+ * shadcn's Chart is a theming wrapper around Recharts. nebula cannot follow it
5
+ * there — Recharts is React — and a zero-dependency port of a full charting
6
+ * library is not a thing to promise. So the scope is stated plainly: three
7
+ * chart types over one categorical axis and one numeric axis, which is what
8
+ * the shadcn chart examples are and what dashboards overwhelmingly need.
9
+ *
10
+ * Anything beyond that — stacked bars with negative values, dual axes, brushes,
11
+ * candlesticks — is a charting library's job, and an app needing one should
12
+ * install one rather than have this component grow into a bad copy.
13
+ *
14
+ * The colours come from `--chart-1` … `--chart-5`, so a chart re-themes with
15
+ * the rest of the design system instead of carrying its own palette.
16
+ *
17
+ * An SVG chart is invisible to a screen reader whatever ARIA is put on it, so
18
+ * the same data is emitted as a real `<table>`, visually hidden. That is the
19
+ * only approach that actually works: the reader gets rows and values it can
20
+ * navigate, not a described picture.
21
+ */
22
+ import { component, html } from "@c9up/aurora";
23
+ import { cn } from "../lib/cn.js";
24
+ import { uid } from "../lib/id.js";
25
+ import { read } from "../lib/props.js";
26
+ const VIEWBOX_WIDTH = 600;
27
+ const PADDING = { top: 8, right: 8, bottom: 24, left: 40 };
28
+ function colorFor(series, index) {
29
+ return series.color ?? `var(--chart-${(index % 5) + 1})`;
30
+ }
31
+ function numberAt(datum, key) {
32
+ const value = datum[key];
33
+ return typeof value === "number" ? value : 0;
34
+ }
35
+ function categoryAt(datum, key) {
36
+ const value = datum[key];
37
+ return typeof value === "string" ? value : String(value ?? "");
38
+ }
39
+ /**
40
+ * The value range the Y axis covers.
41
+ *
42
+ * Anchored at zero whenever the data is non-negative. A bar chart whose axis
43
+ * starts at the minimum value exaggerates every difference — the classic
44
+ * misleading chart — and it is not a choice worth exposing as an option.
45
+ */
46
+ export function valueRange(data, series) {
47
+ let min = 0;
48
+ let max = 0;
49
+ for (const datum of data) {
50
+ for (const entry of series) {
51
+ const value = numberAt(datum, entry.key);
52
+ if (value < min)
53
+ min = value;
54
+ if (value > max)
55
+ max = value;
56
+ }
57
+ }
58
+ // A flat all-zero dataset would divide by zero when scaling.
59
+ if (min === max)
60
+ return { min, max: max + 1 };
61
+ return { min, max };
62
+ }
63
+ export const Chart = component((props) => {
64
+ const titleId = uid("chart-title");
65
+ const height = props.height ?? 240;
66
+ const type = props.type ?? "line";
67
+ const { min, max } = valueRange(props.data, props.series);
68
+ const plotWidth = VIEWBOX_WIDTH - PADDING.left - PADDING.right;
69
+ const plotHeight = height - PADDING.top - PADDING.bottom;
70
+ const y = (value) => PADDING.top + plotHeight - ((value - min) / (max - min)) * plotHeight;
71
+ /** Centre of the slot for index `i`, so points sit above their labels. */
72
+ const x = (index) => {
73
+ const count = Math.max(props.data.length, 1);
74
+ const step = plotWidth / count;
75
+ return PADDING.left + step * index + step / 2;
76
+ };
77
+ const gridLines = [0, 0.25, 0.5, 0.75, 1].map((fraction) => PADDING.top + plotHeight * fraction);
78
+ function linePath(series) {
79
+ return props.data
80
+ .map((datum, index) => `${index === 0 ? "M" : "L"}${x(index)},${y(numberAt(datum, series.key))}`)
81
+ .join(" ");
82
+ }
83
+ function areaPath(series) {
84
+ if (props.data.length === 0)
85
+ return "";
86
+ const baseline = y(Math.max(min, 0));
87
+ return `${linePath(series)} L${x(props.data.length - 1)},${baseline} L${x(0)},${baseline} Z`;
88
+ }
89
+ return html `<figure
90
+ data-slot="chart"
91
+ class="${() => cn("w-full", read(props.class))}"
92
+ >
93
+ <svg
94
+ viewBox="${`0 0 ${VIEWBOX_WIDTH} ${height}`}"
95
+ role="img"
96
+ aria-labelledby="${titleId}"
97
+ preserveAspectRatio="none"
98
+ class="h-auto w-full overflow-visible"
99
+ >
100
+ <title id="${titleId}">${props.label}</title>
101
+ ${props.grid === false
102
+ ? null
103
+ : gridLines.map((position) => html `<line
104
+ x1="${PADDING.left}"
105
+ x2="${VIEWBOX_WIDTH - PADDING.right}"
106
+ y1="${position}"
107
+ y2="${position}"
108
+ stroke="var(--border)"
109
+ stroke-width="1"
110
+ />`)}
111
+ ${props.series.map((series, seriesIndex) => type === "bar"
112
+ ? renderBars(series, seriesIndex)
113
+ : renderCurve(series, seriesIndex))}
114
+ ${props.data.map((datum, index) => html `<text
115
+ x="${x(index)}"
116
+ y="${height - 6}"
117
+ text-anchor="middle"
118
+ font-size="10"
119
+ fill="var(--muted-foreground)"
120
+ >${categoryAt(datum, props.categoryKey)}</text>`)}
121
+ </svg>
122
+ <figcaption class="sr-only">${renderDataTable()}</figcaption>
123
+ <div data-slot="chart-legend" class="mt-2 flex flex-wrap justify-center gap-4 text-xs">
124
+ ${props.series.map((series, index) => html `<span class="flex items-center gap-1.5">
125
+ <span
126
+ aria-hidden="true"
127
+ class="size-2 shrink-0 rounded-[2px]"
128
+ style="${`background:${colorFor(series, index)}`}"
129
+ ></span>
130
+ <span class="text-muted-foreground">${series.label}</span>
131
+ </span>`)}
132
+ </div>
133
+ </figure>`;
134
+ function renderCurve(series, index) {
135
+ const color = colorFor(series, index);
136
+ return html `<g>
137
+ ${type === "area"
138
+ ? html `<path d="${areaPath(series)}" fill="${color}" fill-opacity="0.15" />`
139
+ : null}
140
+ <path
141
+ d="${linePath(series)}"
142
+ fill="none"
143
+ stroke="${color}"
144
+ stroke-width="2"
145
+ stroke-linecap="round"
146
+ stroke-linejoin="round"
147
+ />
148
+ </g>`;
149
+ }
150
+ function renderBars(series, seriesIndex) {
151
+ const count = Math.max(props.data.length, 1);
152
+ const slot = plotWidth / count;
153
+ // Share each slot between the series so grouped bars sit side by side,
154
+ // with a quarter of the slot left as the gap between groups.
155
+ const barWidth = (slot * 0.75) / props.series.length;
156
+ const baseline = y(Math.max(min, 0));
157
+ return html `<g fill="${colorFor(series, seriesIndex)}">
158
+ ${props.data.map((datum, index) => {
159
+ const top = y(numberAt(datum, series.key));
160
+ const left = PADDING.left + slot * index + slot * 0.125 + barWidth * seriesIndex;
161
+ return html `<rect
162
+ x="${left}"
163
+ y="${Math.min(top, baseline)}"
164
+ width="${barWidth}"
165
+ height="${Math.abs(baseline - top)}"
166
+ rx="2"
167
+ />`;
168
+ })}
169
+ </g>`;
170
+ }
171
+ /** The same numbers as a real table, for anything that cannot see the SVG. */
172
+ function renderDataTable() {
173
+ return html `<table>
174
+ <caption>${props.label}</caption>
175
+ <thead>
176
+ <tr>
177
+ <th scope="col">${props.categoryKey}</th>
178
+ ${props.series.map((series) => html `<th scope="col">${series.label}</th>`)}
179
+ </tr>
180
+ </thead>
181
+ <tbody>
182
+ ${props.data.map((datum) => html `<tr>
183
+ <th scope="row">${categoryAt(datum, props.categoryKey)}</th>
184
+ ${props.series.map((series) => html `<td>${numberAt(datum, series.key)}</td>`)}
185
+ </tr>`)}
186
+ </tbody>
187
+ </table>`;
188
+ }
189
+ });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Combobox — a select you can type into.
3
+ *
4
+ * Structurally a Select whose panel holds a search field, which is exactly how
5
+ * shadcn builds it (a Popover wrapping a Command). nebula does the same, so
6
+ * the filtering, the highlight and the keyboard model are `Command`'s and are
7
+ * not written twice.
8
+ *
9
+ * The one addition over Command is selection: a combobox remembers what was
10
+ * picked, shows it on the trigger, and posts it with the form. Command runs an
11
+ * action and forgets.
12
+ *
13
+ * Focus moves into the panel's search field on open — that is the point of the
14
+ * control — and returns to the trigger on close, so the tab order does not
15
+ * jump to the top of the page after choosing.
16
+ */
17
+ import { type Reactive } from "../lib/props.js";
18
+ export interface ComboboxOption {
19
+ value: string;
20
+ label: string;
21
+ keywords?: readonly string[];
22
+ disabled?: boolean;
23
+ group?: string;
24
+ }
25
+ export interface ComboboxProps {
26
+ options: readonly ComboboxOption[];
27
+ name?: string;
28
+ value?: Reactive<string | undefined>;
29
+ defaultValue?: string;
30
+ /** Trigger text when nothing is chosen. */
31
+ placeholder?: string;
32
+ /** Placeholder inside the search field. */
33
+ searchPlaceholder?: string;
34
+ emptyMessage?: string;
35
+ disabled?: Reactive<boolean>;
36
+ invalid?: Reactive<boolean>;
37
+ class?: Reactive<string>;
38
+ contentClass?: Reactive<string>;
39
+ onValueChange?: (value: string) => void;
40
+ }
41
+ export declare const Combobox: (props?: ComboboxProps | undefined) => import("@c9up/aurora").TemplateResult;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Combobox — a select you can type into.
3
+ *
4
+ * Structurally a Select whose panel holds a search field, which is exactly how
5
+ * shadcn builds it (a Popover wrapping a Command). nebula does the same, so
6
+ * the filtering, the highlight and the keyboard model are `Command`'s and are
7
+ * not written twice.
8
+ *
9
+ * The one addition over Command is selection: a combobox remembers what was
10
+ * picked, shows it on the trigger, and posts it with the form. Command runs an
11
+ * action and forgets.
12
+ *
13
+ * Focus moves into the panel's search field on open — that is the point of the
14
+ * control — and returns to the trigger on close, so the tab order does not
15
+ * jump to the top of the page after choosing.
16
+ */
17
+ import { component, html, signal } from "@c9up/aurora";
18
+ import { cn } from "../lib/cn.js";
19
+ import { ChevronsUpDownIcon } from "../lib/icons.js";
20
+ import { uid } from "../lib/id.js";
21
+ import { zoomInOut } from "../lib/motion.js";
22
+ import { read, readOr } from "../lib/props.js";
23
+ import { controllable } from "../primitives/controllable.js";
24
+ import { floatingSurface } from "../primitives/floatingSurface.js";
25
+ import { focusSilently } from "../primitives/focusable.js";
26
+ import { Command } from "./Command.js";
27
+ import { selectTriggerClasses } from "./Select.js";
28
+ export const Combobox = component((props) => {
29
+ const triggerId = uid("combobox-trigger");
30
+ const contentId = uid("combobox-content");
31
+ const open = signal(false);
32
+ const selection = controllable({
33
+ value: props.value,
34
+ initial: props.defaultValue,
35
+ onChange: (next) => {
36
+ if (next !== undefined)
37
+ props.onValueChange?.(next);
38
+ },
39
+ });
40
+ function labelFor(value) {
41
+ return props.options.find((option) => option.value === value)?.label;
42
+ }
43
+ const items = () => props.options.map((option) => ({
44
+ value: option.value,
45
+ label: option.label,
46
+ keywords: option.keywords,
47
+ disabled: option.disabled,
48
+ group: option.group,
49
+ }));
50
+ function choose(item) {
51
+ selection.set(item.value);
52
+ open(false);
53
+ focusSilently(document.getElementById(triggerId));
54
+ }
55
+ floatingSurface({
56
+ anchor: () => document.getElementById(triggerId),
57
+ open: () => open(),
58
+ onClose: () => open(false),
59
+ placement: "bottom-start",
60
+ offset: 4,
61
+ matchWidth: true,
62
+ trapFocus: true,
63
+ // Focus the search field, not the first result — the user opened this to
64
+ // type. `firstFocusable` would land on whichever element comes first in
65
+ // the DOM, which is only the input by accident.
66
+ initialFocus: (content) => content.querySelector("input"),
67
+ content: () => html `<div
68
+ data-slot="combobox-content"
69
+ id="${contentId}"
70
+ class="${cn("bg-popover text-popover-foreground z-50 min-w-(--nebula-anchor-width) overflow-hidden rounded-md border shadow-md", zoomInOut, read(props.contentClass))}"
71
+ >
72
+ ${Command({
73
+ items: items(),
74
+ placeholder: props.searchPlaceholder ?? "Search…",
75
+ emptyMessage: props.emptyMessage ?? "No results found.",
76
+ onSelect: choose,
77
+ })}
78
+ </div>`,
79
+ });
80
+ return html `<div data-slot="combobox" class="${() => cn("inline-flex", read(props.class))}">
81
+ <button
82
+ type="button"
83
+ data-slot="combobox-trigger"
84
+ id="${triggerId}"
85
+ role="combobox"
86
+ aria-haspopup="listbox"
87
+ aria-expanded="${() => (open() ? "true" : "false")}"
88
+ aria-controls="${() => (open() ? contentId : undefined)}"
89
+ aria-invalid="${() => (read(props.invalid) === true ? "true" : undefined)}"
90
+ data-placeholder="${() => (selection.current() === undefined ? "" : undefined)}"
91
+ ?disabled="${() => readOr(props.disabled, false)}"
92
+ class="${() => cn(selectTriggerClasses, "justify-between")}"
93
+ @click="${() => open(!open())}"
94
+ >
95
+ <span class="truncate"
96
+ >${() => labelFor(selection.current()) ?? props.placeholder ?? "Select…"}</span
97
+ >
98
+ ${ChevronsUpDownIcon({ class: "size-4 opacity-50" })}
99
+ </button>
100
+ <input type="hidden" name="${props.name}" .value="${() => selection.current() ?? ""}" />
101
+ </div>`;
102
+ });
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Command — a searchable list of actions.
3
+ *
4
+ * The palette pattern: type to filter, arrows to move, Enter to run. Used on
5
+ * its own — inside a Popover, a Sheet, or a page — while `CommandDialog` wraps
6
+ * this same component in a modal for the full-screen palette.
7
+ *
8
+ * The filter is the interesting decision. `cmdk`, which shadcn wraps, scores
9
+ * matches with a fuzzy algorithm; nebula matches on substring, plus an
10
+ * optional `keywords` list per item. Fuzzy matching is impressive on a demo
11
+ * and frustrating in use — it surfaces items the user did not type anything
12
+ * resembling, and the ranking is impossible to predict. Substring plus
13
+ * keywords is predictable, and "s" reaching "Settings" is a keyword, not an
14
+ * algorithm.
15
+ *
16
+ * Focus stays in the search input the whole time; the highlighted item is
17
+ * named with `aria-activedescendant`. Moving DOM focus onto the list would
18
+ * take it out of the input and the next keystroke would go nowhere.
19
+ */
20
+ import type { Child } from "../lib/children.js";
21
+ import { type Reactive } from "../lib/props.js";
22
+ export interface CommandItem {
23
+ /** Stable key. */
24
+ value: string;
25
+ label: string;
26
+ icon?: Child;
27
+ /** Extra terms that should match this item. */
28
+ keywords?: readonly string[];
29
+ shortcut?: string;
30
+ disabled?: boolean;
31
+ group?: string;
32
+ onSelect?: () => void;
33
+ }
34
+ export interface CommandProps {
35
+ items: readonly CommandItem[];
36
+ placeholder?: string;
37
+ /** Shown when the filter matches nothing. */
38
+ emptyMessage?: Child;
39
+ /** Replace the built-in substring filter. */
40
+ filter?: (item: CommandItem, query: string) => boolean;
41
+ class?: Reactive<string>;
42
+ onSelect?: (item: CommandItem) => void;
43
+ }
44
+ /** Substring over the label, then over each keyword. */
45
+ export declare function defaultFilter(item: CommandItem, query: string): boolean;
46
+ export declare const Command: (props?: CommandProps | undefined) => import("@c9up/aurora").TemplateResult;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Command — a searchable list of actions.
3
+ *
4
+ * The palette pattern: type to filter, arrows to move, Enter to run. Used on
5
+ * its own — inside a Popover, a Sheet, or a page — while `CommandDialog` wraps
6
+ * this same component in a modal for the full-screen palette.
7
+ *
8
+ * The filter is the interesting decision. `cmdk`, which shadcn wraps, scores
9
+ * matches with a fuzzy algorithm; nebula matches on substring, plus an
10
+ * optional `keywords` list per item. Fuzzy matching is impressive on a demo
11
+ * and frustrating in use — it surfaces items the user did not type anything
12
+ * resembling, and the ranking is impossible to predict. Substring plus
13
+ * keywords is predictable, and "s" reaching "Settings" is a keyword, not an
14
+ * algorithm.
15
+ *
16
+ * Focus stays in the search input the whole time; the highlighted item is
17
+ * named with `aria-activedescendant`. Moving DOM focus onto the list would
18
+ * take it out of the input and the next keystroke would go nowhere.
19
+ */
20
+ import { component, html, signal } from "@c9up/aurora";
21
+ import { cn } from "../lib/cn.js";
22
+ import { SearchIcon } from "../lib/icons.js";
23
+ import { uid } from "../lib/id.js";
24
+ import { read } from "../lib/props.js";
25
+ /** Substring over the label, then over each keyword. */
26
+ export function defaultFilter(item, query) {
27
+ if (query === "")
28
+ return true;
29
+ const needle = query.toLowerCase();
30
+ if (item.label.toLowerCase().includes(needle))
31
+ return true;
32
+ return (item.keywords ?? []).some((keyword) => keyword.toLowerCase().includes(needle));
33
+ }
34
+ export const Command = component((props) => {
35
+ const inputId = uid("command-input");
36
+ const listId = uid("command-list");
37
+ const itemIds = new Map();
38
+ for (const item of props.items)
39
+ itemIds.set(item.value, uid("command-item"));
40
+ const query = signal("");
41
+ const active = signal(undefined);
42
+ function matches() {
43
+ const filter = props.filter ?? defaultFilter;
44
+ return props.items.filter((item) => item.disabled !== true && filter(item, query()));
45
+ }
46
+ function run(item) {
47
+ if (item.disabled === true)
48
+ return;
49
+ item.onSelect?.();
50
+ props.onSelect?.(item);
51
+ }
52
+ function move(delta) {
53
+ const list = matches();
54
+ if (list.length === 0)
55
+ return;
56
+ const from = list.findIndex((item) => item.value === active());
57
+ const next = from === -1 ? (delta > 0 ? 0 : list.length - 1) : from + delta;
58
+ // Wrapping is right here, unlike in a menu: a palette is a search result
59
+ // list and the user is scanning it, not navigating a fixed structure.
60
+ const target = list[(next + list.length) % list.length];
61
+ if (target !== undefined) {
62
+ active(target.value);
63
+ scrollIntoView(target.value);
64
+ }
65
+ }
66
+ function scrollIntoView(value) {
67
+ const id = itemIds.get(value);
68
+ if (id === undefined)
69
+ return;
70
+ document.getElementById(id)?.scrollIntoView({ block: "nearest" });
71
+ }
72
+ function onKeyDown(event) {
73
+ if (event.key === "ArrowDown") {
74
+ event.preventDefault();
75
+ move(1);
76
+ }
77
+ else if (event.key === "ArrowUp") {
78
+ event.preventDefault();
79
+ move(-1);
80
+ }
81
+ else if (event.key === "Enter") {
82
+ event.preventDefault();
83
+ const current = matches().find((item) => item.value === active());
84
+ if (current !== undefined)
85
+ run(current);
86
+ }
87
+ }
88
+ function onInput(event) {
89
+ const target = event.target;
90
+ if (!(target instanceof HTMLInputElement))
91
+ return;
92
+ query(target.value);
93
+ // Re-point the highlight at the first result: leaving it on an item the
94
+ // new query filtered out means Enter runs something invisible.
95
+ active(matches()[0]?.value);
96
+ }
97
+ return html `<div
98
+ data-slot="command"
99
+ class="${() => cn("bg-popover text-popover-foreground flex size-full flex-col overflow-hidden rounded-md", read(props.class))}"
100
+ >
101
+ <div data-slot="command-input-wrapper" class="flex h-9 items-center gap-2 border-b px-3">
102
+ ${SearchIcon({ class: "size-4 shrink-0 opacity-50" })}
103
+ <input
104
+ data-slot="command-input"
105
+ id="${inputId}"
106
+ type="text"
107
+ role="combobox"
108
+ autocomplete="off"
109
+ aria-expanded="true"
110
+ aria-controls="${listId}"
111
+ aria-activedescendant="${() => {
112
+ const current = active();
113
+ return current === undefined ? undefined : itemIds.get(current);
114
+ }}"
115
+ placeholder="${props.placeholder ?? "Type a command or search…"}"
116
+ class="placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50"
117
+ @input="${onInput}"
118
+ @keydown="${onKeyDown}"
119
+ />
120
+ </div>
121
+ <div
122
+ data-slot="command-list"
123
+ id="${listId}"
124
+ role="listbox"
125
+ class="max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto p-1"
126
+ >
127
+ ${() => renderResults()}
128
+ </div>
129
+ </div>`;
130
+ function renderResults() {
131
+ const list = matches();
132
+ if (list.length === 0) {
133
+ return html `<div
134
+ data-slot="command-empty"
135
+ role="presentation"
136
+ class="py-6 text-center text-sm"
137
+ >${props.emptyMessage ?? "No results found."}</div>`;
138
+ }
139
+ let lastGroup;
140
+ return list.map((item) => {
141
+ const heading = item.group !== undefined && item.group !== lastGroup
142
+ ? html `<div
143
+ role="presentation"
144
+ class="text-muted-foreground px-2 py-1.5 text-xs font-medium"
145
+ >${item.group}</div>`
146
+ : null;
147
+ lastGroup = item.group;
148
+ return [heading, renderItem(item)];
149
+ });
150
+ }
151
+ function renderItem(item) {
152
+ return html `<div
153
+ role="option"
154
+ data-slot="command-item"
155
+ id="${itemIds.get(item.value)}"
156
+ aria-selected="${() => (active() === item.value ? "true" : "false")}"
157
+ data-active="${() => (active() === item.value ? "" : undefined)}"
158
+ class="relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[active]:bg-accent data-[active]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4"
159
+ @click="${() => run(item)}"
160
+ @pointerenter="${() => active(item.value)}"
161
+ >
162
+ ${item.icon}
163
+ <span class="flex-1 truncate">${item.label}</span>
164
+ ${item.shortcut === undefined
165
+ ? null
166
+ : html `<span class="text-muted-foreground ml-auto text-xs tracking-widest"
167
+ >${item.shortcut}</span
168
+ >`}
169
+ </div>`;
170
+ }
171
+ });
@@ -0,0 +1,42 @@
1
+ /**
2
+ * CommandDialog — the command palette, full screen.
3
+ *
4
+ * `Command` on its own is a list you place somewhere. This is the form people
5
+ * mean by "command palette": a modal over the page, opened by a keyboard
6
+ * shortcut, dismissed by Escape or a click outside.
7
+ *
8
+ * An organism composing another organism — `Command` inside `modalSurface`.
9
+ * Nothing about the palette's behaviour is re-implemented here; the filter,
10
+ * the highlight and the keyboard model stay in `Command`, and this file adds
11
+ * only the modal shell and the shortcut.
12
+ *
13
+ * The shortcut is opt-in but built in, because binding it correctly is
14
+ * fiddlier than it looks: it has to be a capturing document listener (a menu
15
+ * or a dialog already open would otherwise swallow it), it has to preventDefault
16
+ * (⌘K is "focus the search bar" in several browsers), and it has to toggle
17
+ * rather than open, so the same keystroke closes it.
18
+ *
19
+ * The title is present but visually hidden. A dialog with no accessible name
20
+ * is announced as just "dialog"; giving it one costs nothing and the palette's
21
+ * own search field is the visible affordance.
22
+ */
23
+ import type { Child } from "../lib/children.js";
24
+ import { type Reactive } from "../lib/props.js";
25
+ import { type CommandProps } from "./Command.js";
26
+ export interface CommandDialogProps extends Omit<CommandProps, "class"> {
27
+ /** Announced on open. Hidden visually unless `showTitle`. */
28
+ title?: Child;
29
+ description?: Child;
30
+ showTitle?: boolean;
31
+ open?: Reactive<boolean>;
32
+ defaultOpen?: boolean;
33
+ onOpenChange?: (open: boolean) => void;
34
+ /**
35
+ * A single character that opens the palette with the platform's command
36
+ * modifier — `"k"` gives ⌘K on macOS and Ctrl+K elsewhere. Omit for a
37
+ * palette opened only from your own code.
38
+ */
39
+ shortcut?: string;
40
+ contentClass?: Reactive<string>;
41
+ }
42
+ export declare const CommandDialog: (props?: CommandDialogProps | undefined) => import("@c9up/aurora").TemplateResult;