@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,353 @@
1
+ /**
2
+ * Calendar — a month grid for picking a date.
3
+ *
4
+ * shadcn wraps `react-day-picker`, which brings `date-fns` with it. nebula
5
+ * uses `Date` and `Intl`, which cover everything this needs: month lengths and
6
+ * leap years come from `Date`, and weekday and month names come from
7
+ * `Intl.DateTimeFormat` in the user's locale for free — which is more than a
8
+ * hardcoded English array would give.
9
+ *
10
+ * Dates are handled at local midnight throughout. A calendar cell means "this
11
+ * day", not "this instant", and mixing the two is how a date picker ends up
12
+ * off by one for users west of UTC: `new Date("2026-03-14")` parses as UTC
13
+ * midnight, which is the 13th in New York.
14
+ *
15
+ * The keyboard model is the WAI-ARIA grid pattern, and it is what makes the
16
+ * control usable at all without a pointer: arrows move a day, PageUp/PageDown
17
+ * move a month, Home and End jump to the ends of the week. Only one cell is
18
+ * ever tabbable — the focused day — so Tab leaves the grid instead of walking
19
+ * through thirty-one buttons.
20
+ */
21
+ import { component, html, signal } from "@c9up/aurora";
22
+ import { buttonVariants } from "../atoms/Button.js";
23
+ import { cn } from "../lib/cn.js";
24
+ import { ChevronLeftIcon, ChevronRightIcon } from "../lib/icons.js";
25
+ import { uid } from "../lib/id.js";
26
+ import { read } from "../lib/props.js";
27
+ // ─── date helpers ────────────────────────────────────────────────────
28
+ /** Local midnight of the given day — the canonical form for a calendar date. */
29
+ export function startOfDay(date) {
30
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
31
+ }
32
+ export function isSameDay(a, b) {
33
+ return (a.getFullYear() === b.getFullYear() &&
34
+ a.getMonth() === b.getMonth() &&
35
+ a.getDate() === b.getDate());
36
+ }
37
+ export function addDays(date, days) {
38
+ // Day-of-month arithmetic through the Date constructor, which normalises
39
+ // overflow — the 32nd of March becomes the 1st of April, leap years and
40
+ // month lengths included. Adding milliseconds would not: a day is not
41
+ // always 86 400 000 ms across a daylight-saving boundary.
42
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);
43
+ }
44
+ export function addMonths(date, months) {
45
+ const target = new Date(date.getFullYear(), date.getMonth() + months, 1);
46
+ // Clamp the day: 31 January plus one month is 28 February, not 3 March.
47
+ const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
48
+ return new Date(target.getFullYear(), target.getMonth(), Math.min(date.getDate(), lastDay));
49
+ }
50
+ /**
51
+ * The six-week grid covering a month.
52
+ *
53
+ * Always six weeks, always 42 cells, even when five would do. A grid that
54
+ * changes height between months makes the whole popover jump as you page
55
+ * through it.
56
+ */
57
+ export function monthGrid(month, weekStartsOn) {
58
+ const first = new Date(month.getFullYear(), month.getMonth(), 1);
59
+ const lead = (first.getDay() - weekStartsOn + 7) % 7;
60
+ const start = addDays(first, -lead);
61
+ return Array.from({ length: 42 }, (_unused, index) => addDays(start, index));
62
+ }
63
+ function hasWeekInfo(value) {
64
+ return "getWeekInfo" in value && typeof value.getWeekInfo === "function";
65
+ }
66
+ /** The locale's first day of the week, or Monday where it cannot be read. */
67
+ function localeWeekStart(locale) {
68
+ const resolved = new Intl.Locale(locale ?? navigator.language);
69
+ if (!hasWeekInfo(resolved))
70
+ return 1;
71
+ // `firstDay` is 1–7 with 7 for Sunday; the Date API uses 0–6 with 0 Sunday.
72
+ return resolved.getWeekInfo().firstDay % 7;
73
+ }
74
+ /**
75
+ * Place a day relative to a span.
76
+ *
77
+ * `end` is passed in rather than read off the range because it may be the
78
+ * *preview* end — wherever the pointer is, before the second click. Keeping
79
+ * that decision at the call site is what lets this stay a pure function over
80
+ * three dates, which is the part worth testing exhaustively.
81
+ */
82
+ export function rangePosition(date, range, end) {
83
+ if (range === undefined)
84
+ return "none";
85
+ if (end === undefined) {
86
+ return isSameDay(date, range.from) ? "single" : "none";
87
+ }
88
+ // Ordered, because the preview end can sit before the fixed start while the
89
+ // pointer sweeps backwards.
90
+ const [lo, hi] = range.from <= end ? [range.from, end] : [end, range.from];
91
+ // A one-day span is both ends at once, and squaring either side of it would
92
+ // leave a lone cell with a flat edge against nothing.
93
+ if (isSameDay(lo, hi))
94
+ return isSameDay(date, lo) ? "single" : "none";
95
+ if (isSameDay(date, lo))
96
+ return "start";
97
+ if (isSameDay(date, hi))
98
+ return "end";
99
+ return date > lo && date < hi ? "middle" : "none";
100
+ }
101
+ /**
102
+ * The span after clicking `date`.
103
+ *
104
+ * A click before the fixed start restarts the span rather than producing an
105
+ * inverted one. Silently swapping the ends would be the other option, and it
106
+ * is worse: the user who clicked the 3rd after the 10th almost always meant to
107
+ * start again, not to select the week between them.
108
+ */
109
+ export function nextRange(current, date) {
110
+ if (current === undefined ||
111
+ current.to !== undefined ||
112
+ date < current.from) {
113
+ return { from: date };
114
+ }
115
+ return { from: current.from, to: date };
116
+ }
117
+ // ─── component ───────────────────────────────────────────────────────
118
+ export const Calendar = component((props) => {
119
+ const gridId = uid("calendar-grid");
120
+ const labelId = uid("calendar-label");
121
+ const locale = props.locale;
122
+ const weekStartsOn = props.weekStartsOn ?? safeWeekStart(locale);
123
+ const isRange = props.mode === "range";
124
+ const today = startOfDay(new Date());
125
+ const initial = props.defaultValue ??
126
+ props.defaultRange?.from ??
127
+ props.defaultMonth ??
128
+ today;
129
+ const month = signal(new Date(initial.getFullYear(), initial.getMonth(), 1));
130
+ /** The day the keyboard is on. Only this cell is tabbable. */
131
+ const cursor = signal(startOfDay(initial));
132
+ const selected = () => read(props.value) ?? props.defaultValue;
133
+ /** The span being built, when the caller does not control it. */
134
+ const draft = signal(props.defaultRange);
135
+ /** The day under the pointer, for the preview half of an open range. */
136
+ const hovered = signal(undefined);
137
+ const activeRange = () => read(props.range) ?? draft();
138
+ /**
139
+ * The far end of the span as currently drawn.
140
+ *
141
+ * While only one end is fixed, that is wherever the pointer is — which is
142
+ * what makes the range visibly follow the cursor before the second click.
143
+ * Keyboard users get no preview, and that is fine: for them the span
144
+ * appears on Enter, and a preview tied to the roving cursor would announce
145
+ * a selection that has not happened.
146
+ */
147
+ function rangeEnd() {
148
+ const current = activeRange();
149
+ if (current === undefined)
150
+ return undefined;
151
+ return current.to ?? hovered();
152
+ }
153
+ function rangePositionOf(date) {
154
+ const current = activeRange();
155
+ if (current === undefined)
156
+ return "none";
157
+ const end = rangeEnd();
158
+ if (end === undefined) {
159
+ return isSameDay(date, current.from) ? "single" : "none";
160
+ }
161
+ // Ordered, because the preview end can sit before the fixed start while
162
+ // the pointer sweeps backwards.
163
+ const [lo, hi] = current.from <= end ? [current.from, end] : [end, current.from];
164
+ // A one-day span is both ends at once, and squaring either side of it
165
+ // would leave a lone cell with a flat edge against nothing.
166
+ if (isSameDay(lo, hi))
167
+ return isSameDay(date, lo) ? "single" : "none";
168
+ if (isSameDay(date, lo))
169
+ return "start";
170
+ if (isSameDay(date, hi))
171
+ return "end";
172
+ return date > lo && date < hi ? "middle" : "none";
173
+ }
174
+ const monthLabel = new Intl.DateTimeFormat(locale, {
175
+ month: "long",
176
+ year: "numeric",
177
+ });
178
+ const weekdayLabel = new Intl.DateTimeFormat(locale, { weekday: "short" });
179
+ const fullDate = new Intl.DateTimeFormat(locale, { dateStyle: "full" });
180
+ function isDisabled(date) {
181
+ if (props.min !== undefined && date < startOfDay(props.min))
182
+ return true;
183
+ if (props.max !== undefined && date > startOfDay(props.max))
184
+ return true;
185
+ return props.disabled?.(date) === true;
186
+ }
187
+ /**
188
+ * Show the month containing `date`, if it is not already showing.
189
+ *
190
+ * The guard is load-bearing. `month` is a signal the whole grid is derived
191
+ * from, so writing a fresh `Date` for the month already on screen rebuilds
192
+ * all 42 cells — destroying the one the user just clicked, and with it the
193
+ * focus that was on it.
194
+ */
195
+ function showMonthOf(date) {
196
+ const current = month();
197
+ if (current.getFullYear() === date.getFullYear() &&
198
+ current.getMonth() === date.getMonth()) {
199
+ return;
200
+ }
201
+ month(new Date(date.getFullYear(), date.getMonth(), 1));
202
+ }
203
+ function choose(date) {
204
+ if (isDisabled(date))
205
+ return;
206
+ cursor(date);
207
+ showMonthOf(date);
208
+ if (isRange)
209
+ chooseInRange(date);
210
+ else
211
+ props.onValueChange?.(date);
212
+ }
213
+ function chooseInRange(date) {
214
+ const next = nextRange(activeRange(), date);
215
+ draft(next);
216
+ // Drop the preview: it belonged to the span that just closed, and leaving
217
+ // it would paint a band to wherever the pointer happens to rest.
218
+ hovered(undefined);
219
+ props.onRangeChange?.(next);
220
+ }
221
+ function moveCursor(next) {
222
+ cursor(next);
223
+ // Follow the cursor across a month boundary, otherwise arrowing off the
224
+ // end of the grid moves focus to a cell that is not on screen.
225
+ if (next.getMonth() !== month().getMonth() ||
226
+ next.getFullYear() !== month().getFullYear()) {
227
+ month(new Date(next.getFullYear(), next.getMonth(), 1));
228
+ }
229
+ focusCursor();
230
+ }
231
+ function focusCursor() {
232
+ // After the signal write, so the new cell exists and carries tabindex 0.
233
+ queueMicrotask(() => {
234
+ const grid = document.getElementById(gridId);
235
+ grid
236
+ ?.querySelector("[data-cursor]")
237
+ ?.focus({ preventScroll: true });
238
+ });
239
+ }
240
+ function onKeyDown(event) {
241
+ const from = cursor();
242
+ const moves = {
243
+ ArrowLeft: () => addDays(from, -1),
244
+ ArrowRight: () => addDays(from, 1),
245
+ ArrowUp: () => addDays(from, -7),
246
+ ArrowDown: () => addDays(from, 7),
247
+ PageUp: () => addMonths(from, -1),
248
+ PageDown: () => addMonths(from, 1),
249
+ Home: () => addDays(from, -((from.getDay() - weekStartsOn + 7) % 7)),
250
+ End: () => addDays(from, 6 - ((from.getDay() - weekStartsOn + 7) % 7)),
251
+ };
252
+ const move = moves[event.key];
253
+ if (move !== undefined) {
254
+ event.preventDefault();
255
+ moveCursor(move());
256
+ return;
257
+ }
258
+ if (event.key === "Enter" || event.key === " ") {
259
+ event.preventDefault();
260
+ choose(from);
261
+ }
262
+ }
263
+ const weekdayNames = Array.from({ length: 7 }, (_unused, index) => weekdayLabel.format(new Date(2024, 0, 7 + ((weekStartsOn + index) % 7))));
264
+ return html `<div
265
+ data-slot="calendar"
266
+ class="${() => cn("bg-background w-fit rounded-md p-3", read(props.class))}"
267
+ >
268
+ <div class="flex items-center justify-between pb-4">
269
+ <button
270
+ type="button"
271
+ aria-label="Previous month"
272
+ class="${buttonVariants({ variant: "outline", size: "icon", class: "size-7" })}"
273
+ @click="${() => month(addMonths(month(), -1))}"
274
+ >${ChevronLeftIcon({ class: "size-4" })}</button>
275
+ <div id="${labelId}" aria-live="polite" class="text-sm font-medium">
276
+ ${() => monthLabel.format(month())}
277
+ </div>
278
+ <button
279
+ type="button"
280
+ aria-label="Next month"
281
+ class="${buttonVariants({ variant: "outline", size: "icon", class: "size-7" })}"
282
+ @click="${() => month(addMonths(month(), 1))}"
283
+ >${ChevronRightIcon({ class: "size-4" })}</button>
284
+ </div>
285
+ <div
286
+ role="grid"
287
+ id="${gridId}"
288
+ aria-labelledby="${labelId}"
289
+ class="grid grid-cols-7 gap-1"
290
+ @keydown="${onKeyDown}"
291
+ >
292
+ ${weekdayNames.map((name) => html `<div
293
+ role="columnheader"
294
+ aria-label="${name}"
295
+ class="text-muted-foreground flex size-8 items-center justify-center text-[0.8rem] font-normal"
296
+ >${name}</div>`)}
297
+ ${() => monthGrid(month(), weekStartsOn).map(renderDay)}
298
+ </div>
299
+ </div>`;
300
+ function renderDay(date) {
301
+ const outside = date.getMonth() !== month().getMonth();
302
+ const position = () => isRange ? rangePositionOf(date) : "none";
303
+ const chosen = () => {
304
+ if (isRange)
305
+ return position() !== "none";
306
+ const current = selected();
307
+ return current !== undefined && isSameDay(current, date);
308
+ };
309
+ const onCursor = () => isSameDay(cursor(), date);
310
+ const disabled = isDisabled(date);
311
+ return html `<button
312
+ type="button"
313
+ role="gridcell"
314
+ data-slot="calendar-day"
315
+ data-cursor="${() => (onCursor() ? "" : undefined)}"
316
+ data-outside="${outside ? "" : undefined}"
317
+ data-today="${isSameDay(date, today) ? "" : undefined}"
318
+ data-range="${() => {
319
+ const at = position();
320
+ return at === "none" ? undefined : at;
321
+ }}"
322
+ aria-label="${fullDate.format(date)}"
323
+ aria-selected="${() => (chosen() ? "true" : "false")}"
324
+ aria-current="${isSameDay(date, today) ? "date" : undefined}"
325
+ tabindex="${() => (onCursor() ? 0 : -1)}"
326
+ ?disabled="${disabled}"
327
+ class="${() => cn("flex size-8 items-center justify-center rounded-md p-0 text-sm font-normal transition-colors outline-none", "hover:bg-accent hover:text-accent-foreground focus-visible:ring-ring/50 focus-visible:ring-[3px]", "aria-selected:bg-primary aria-selected:text-primary-foreground aria-selected:hover:bg-primary", "data-[today]:bg-accent data-[today]:aria-selected:bg-primary",
328
+ // The interior of a span is a flat band: square, and in accent
329
+ // rather than primary, so the two ends stay the emphasis.
330
+ "data-[range=middle]:bg-accent data-[range=middle]:text-accent-foreground data-[range=middle]:rounded-none", "data-[range=start]:rounded-r-none data-[range=end]:rounded-l-none", outside ? "text-muted-foreground opacity-50" : "", disabled ? "pointer-events-none opacity-30" : "")}"
331
+ @click="${() => choose(date)}"
332
+ @pointerenter="${() => {
333
+ if (isRange && !disabled)
334
+ hovered(date);
335
+ }}"
336
+ >${date.getDate()}</button>`;
337
+ }
338
+ });
339
+ /**
340
+ * `Intl.Locale.getWeekInfo` is not available everywhere yet, and neither is
341
+ * `navigator` on the server. Monday is the fallback — the ISO week start, and
342
+ * the right answer for most of the world.
343
+ */
344
+ function safeWeekStart(locale) {
345
+ try {
346
+ if (typeof navigator === "undefined" && locale === undefined)
347
+ return 1;
348
+ return localeWeekStart(locale);
349
+ }
350
+ catch {
351
+ return 1;
352
+ }
353
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Carousel — a horizontally scrolling strip of slides.
3
+ *
4
+ * shadcn wraps `embla-carousel`, which animates a transform with its own
5
+ * requestAnimationFrame loop and reimplements drag physics on top. nebula uses
6
+ * CSS scroll-snap and `scrollBy`.
7
+ *
8
+ * That is not only smaller, it is better behaved. Native scrolling brings
9
+ * touch momentum, trackpad gestures, the platform's own overscroll, and
10
+ * keyboard scrolling — all things a transform-based carousel has to rebuild
11
+ * and usually gets partly wrong. `scroll-behavior: smooth` handles the
12
+ * animation, and `prefers-reduced-motion` turns it off without a branch here.
13
+ *
14
+ * The strip is a `region` with `aria-roledescription="carousel"`, and slides
15
+ * are `group`s labelled "N of M". A screen reader user otherwise has no way to
16
+ * tell how much is off-screen — the visual affordance of a strip running off
17
+ * the edge carries none of that.
18
+ */
19
+ import type { Child } from "../lib/children.js";
20
+ import { type Reactive } from "../lib/props.js";
21
+ export interface CarouselProps {
22
+ slides: readonly Child[];
23
+ /** Announced before the slides. */
24
+ label?: string;
25
+ orientation?: "horizontal" | "vertical";
26
+ class?: Reactive<string>;
27
+ slideClass?: Reactive<string>;
28
+ }
29
+ export declare const Carousel: (props?: CarouselProps | undefined) => import("@c9up/aurora").TemplateResult;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Carousel — a horizontally scrolling strip of slides.
3
+ *
4
+ * shadcn wraps `embla-carousel`, which animates a transform with its own
5
+ * requestAnimationFrame loop and reimplements drag physics on top. nebula uses
6
+ * CSS scroll-snap and `scrollBy`.
7
+ *
8
+ * That is not only smaller, it is better behaved. Native scrolling brings
9
+ * touch momentum, trackpad gestures, the platform's own overscroll, and
10
+ * keyboard scrolling — all things a transform-based carousel has to rebuild
11
+ * and usually gets partly wrong. `scroll-behavior: smooth` handles the
12
+ * animation, and `prefers-reduced-motion` turns it off without a branch here.
13
+ *
14
+ * The strip is a `region` with `aria-roledescription="carousel"`, and slides
15
+ * are `group`s labelled "N of M". A screen reader user otherwise has no way to
16
+ * tell how much is off-screen — the visual affordance of a strip running off
17
+ * the edge carries none of that.
18
+ */
19
+ import { component, html, onMount, onUnmount, signal } from "@c9up/aurora";
20
+ import { buttonVariants } from "../atoms/Button.js";
21
+ import { cn } from "../lib/cn.js";
22
+ import { ArrowLeftIcon, ArrowRightIcon } from "../lib/icons.js";
23
+ import { uid } from "../lib/id.js";
24
+ import { read } from "../lib/props.js";
25
+ export const Carousel = component((props) => {
26
+ const trackId = uid("carousel-track");
27
+ const horizontal = props.orientation !== "vertical";
28
+ const atStart = signal(true);
29
+ const atEnd = signal(false);
30
+ function track() {
31
+ return document.getElementById(trackId);
32
+ }
33
+ /**
34
+ * Recompute whether either end is reached.
35
+ *
36
+ * The one-pixel tolerance is load-bearing: with fractional device pixel
37
+ * ratios, `scrollLeft` at the far end lands a fraction short of
38
+ * `scrollWidth - clientWidth`, and an exact comparison leaves the "next"
39
+ * button enabled on a carousel that cannot scroll any further.
40
+ */
41
+ function syncEdges() {
42
+ const element = track();
43
+ if (element === null)
44
+ return;
45
+ const position = horizontal ? element.scrollLeft : element.scrollTop;
46
+ const total = horizontal ? element.scrollWidth : element.scrollHeight;
47
+ const visible = horizontal ? element.clientWidth : element.clientHeight;
48
+ atStart(position <= 1);
49
+ atEnd(position + visible >= total - 1);
50
+ }
51
+ function scrollByPage(direction) {
52
+ const element = track();
53
+ if (element === null)
54
+ return;
55
+ const distance = (horizontal ? element.clientWidth : element.clientHeight) * direction;
56
+ element.scrollBy(horizontal
57
+ ? { left: distance, behavior: "smooth" }
58
+ : { top: distance, behavior: "smooth" });
59
+ }
60
+ function onKeyDown(event) {
61
+ const back = horizontal ? "ArrowLeft" : "ArrowUp";
62
+ const forward = horizontal ? "ArrowRight" : "ArrowDown";
63
+ if (event.key === back) {
64
+ event.preventDefault();
65
+ scrollByPage(-1);
66
+ }
67
+ else if (event.key === forward) {
68
+ event.preventDefault();
69
+ scrollByPage(1);
70
+ }
71
+ }
72
+ onMount(() => {
73
+ syncEdges();
74
+ const element = track();
75
+ element?.addEventListener("scroll", syncEdges, { passive: true });
76
+ window.addEventListener("resize", syncEdges);
77
+ });
78
+ onUnmount(() => {
79
+ track()?.removeEventListener("scroll", syncEdges);
80
+ window.removeEventListener("resize", syncEdges);
81
+ });
82
+ const count = props.slides.length;
83
+ return html `<div
84
+ data-slot="carousel"
85
+ role="region"
86
+ aria-roledescription="carousel"
87
+ aria-label="${props.label ?? "Carousel"}"
88
+ class="${() => cn("relative", read(props.class))}"
89
+ @keydown="${onKeyDown}"
90
+ >
91
+ <div
92
+ data-slot="carousel-track"
93
+ id="${trackId}"
94
+ tabindex="0"
95
+ class="${cn("flex snap-mandatory gap-4 overflow-auto scroll-smooth outline-none motion-reduce:scroll-auto", horizontal
96
+ ? "snap-x flex-row [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
97
+ : "snap-y max-h-96 flex-col [scrollbar-width:none] [&::-webkit-scrollbar]:hidden")}"
98
+ >
99
+ ${props.slides.map((child, index) => html `<div
100
+ data-slot="carousel-slide"
101
+ role="group"
102
+ aria-roledescription="slide"
103
+ aria-label="${`${index + 1} of ${count}`}"
104
+ class="${() => cn("min-w-0 shrink-0 grow-0 basis-full snap-start", read(props.slideClass))}"
105
+ >${child}</div>`)}
106
+ </div>
107
+ <button
108
+ type="button"
109
+ data-slot="carousel-previous"
110
+ aria-label="Previous slide"
111
+ ?disabled="${() => atStart()}"
112
+ class="${cn(buttonVariants({ variant: "outline", size: "icon" }), "absolute size-8 rounded-full", horizontal
113
+ ? "top-1/2 -left-12 -translate-y-1/2"
114
+ : "-top-12 left-1/2 -translate-x-1/2 rotate-90")}"
115
+ @click="${() => scrollByPage(-1)}"
116
+ >${ArrowLeftIcon({ class: "size-4" })}</button>
117
+ <button
118
+ type="button"
119
+ data-slot="carousel-next"
120
+ aria-label="Next slide"
121
+ ?disabled="${() => atEnd()}"
122
+ class="${cn(buttonVariants({ variant: "outline", size: "icon" }), "absolute size-8 rounded-full", horizontal
123
+ ? "top-1/2 -right-12 -translate-y-1/2"
124
+ : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90")}"
125
+ @click="${() => scrollByPage(1)}"
126
+ >${ArrowRightIcon({ class: "size-4" })}</button>
127
+ </div>`;
128
+ });
@@ -0,0 +1,55 @@
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 { type Reactive } from "../lib/props.js";
23
+ export interface ChartSeries {
24
+ key: string;
25
+ label: string;
26
+ /** Any CSS colour. Defaults to `--chart-1`…`--chart-5` by position. */
27
+ color?: string;
28
+ }
29
+ /** One row: a category plus one numeric value per series key. */
30
+ export type ChartDatum = Record<string, string | number>;
31
+ export interface ChartProps {
32
+ data: readonly ChartDatum[];
33
+ series: readonly ChartSeries[];
34
+ /** Key in each datum holding the category label. */
35
+ categoryKey: string;
36
+ type?: "line" | "area" | "bar";
37
+ height?: number;
38
+ /** Announced as the chart's name, and used as the data table's caption. */
39
+ label: string;
40
+ /** Draw horizontal gridlines. Default `true`. */
41
+ grid?: boolean;
42
+ class?: Reactive<string>;
43
+ }
44
+ /**
45
+ * The value range the Y axis covers.
46
+ *
47
+ * Anchored at zero whenever the data is non-negative. A bar chart whose axis
48
+ * starts at the minimum value exaggerates every difference — the classic
49
+ * misleading chart — and it is not a choice worth exposing as an option.
50
+ */
51
+ export declare function valueRange(data: readonly ChartDatum[], series: readonly ChartSeries[]): {
52
+ min: number;
53
+ max: number;
54
+ };
55
+ export declare const Chart: (props?: ChartProps | undefined) => import("@c9up/aurora").TemplateResult;