@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,115 @@
1
+ /**
2
+ * DatePicker — a Calendar in a popover, behind a button.
3
+ *
4
+ * shadcn composes this out of Popover, Button and Calendar rather than
5
+ * shipping it; nebula ships it, because the composition has three details that
6
+ * are easy to get wrong and pointless to rediscover per project.
7
+ *
8
+ * The popover closes on selection — a date picker that stays open after a
9
+ * choice leaves the user hunting for the way out. Focus returns to the
10
+ * trigger, so the tab order does not restart at the top of the page. And the
11
+ * trigger reads the chosen date through `Intl.DateTimeFormat`, so it renders
12
+ * in the user's locale rather than an American ordering everyone else has to
13
+ * decode.
14
+ *
15
+ * A hidden input carries the value as ISO `YYYY-MM-DD`, which is what a server
16
+ * wants — never the localised string on the button.
17
+ */
18
+ import { component, html, signal } from "@c9up/aurora";
19
+ import { buttonVariants } from "../atoms/Button.js";
20
+ import { cn } from "../lib/cn.js";
21
+ import { CalendarIcon } from "../lib/icons.js";
22
+ import { uid } from "../lib/id.js";
23
+ import { zoomInOut } from "../lib/motion.js";
24
+ import { read, readOr } from "../lib/props.js";
25
+ import { controllable } from "../primitives/controllable.js";
26
+ import { floatingSurface } from "../primitives/floatingSurface.js";
27
+ import { focusSilently } from "../primitives/focusable.js";
28
+ import { Calendar, startOfDay } from "./Calendar.js";
29
+ /** `YYYY-MM-DD` from a local date, without a timezone round-trip. */
30
+ export function toISODate(date) {
31
+ const month = String(date.getMonth() + 1).padStart(2, "0");
32
+ const day = String(date.getDate()).padStart(2, "0");
33
+ return `${date.getFullYear()}-${month}-${day}`;
34
+ }
35
+ export const DatePicker = component((props) => {
36
+ const triggerId = uid("date-picker-trigger");
37
+ const contentId = uid("date-picker-content");
38
+ const open = signal(false);
39
+ const selection = controllable({
40
+ value: props.value,
41
+ initial: props.defaultValue,
42
+ onChange: (next) => {
43
+ if (next !== undefined)
44
+ props.onValueChange?.(next);
45
+ },
46
+ });
47
+ const format = new Intl.DateTimeFormat(props.locale, { dateStyle: "medium" });
48
+ function choose(date) {
49
+ selection.set(startOfDay(date));
50
+ open(false);
51
+ focusSilently(document.getElementById(triggerId));
52
+ }
53
+ floatingSurface({
54
+ anchor: () => document.getElementById(triggerId),
55
+ open: () => open(),
56
+ onClose: () => open(false),
57
+ placement: "bottom-start",
58
+ offset: 4,
59
+ trapFocus: true,
60
+ // The cursor cell, not the first focusable — landing on the previous-month
61
+ // arrow means the first arrow key pages the calendar instead of moving a
62
+ // day, which is not what a keyboard user reaching a date grid expects.
63
+ initialFocus: (content) => content.querySelector("[data-cursor]"),
64
+ content: () => html `<div
65
+ data-slot="date-picker-content"
66
+ id="${contentId}"
67
+ role="dialog"
68
+ aria-label="Choose a date"
69
+ class="${cn("bg-popover text-popover-foreground z-50 rounded-md border p-0 shadow-md outline-none", zoomInOut)}"
70
+ >
71
+ ${Calendar({
72
+ value: () => selection.current(),
73
+ defaultMonth: selection.current(),
74
+ min: props.min,
75
+ max: props.max,
76
+ locale: props.locale,
77
+ onValueChange: choose,
78
+ })}
79
+ </div>`,
80
+ });
81
+ return html `<div data-slot="date-picker" class="${() => cn("inline-flex", read(props.class))}">
82
+ <button
83
+ type="button"
84
+ id="${triggerId}"
85
+ data-slot="date-picker-trigger"
86
+ aria-haspopup="dialog"
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="${buttonVariants({
93
+ variant: "outline",
94
+ class: "w-full justify-start gap-2 font-normal data-[placeholder]:text-muted-foreground",
95
+ })}"
96
+ @click="${() => open(!open())}"
97
+ >
98
+ ${CalendarIcon({ class: "size-4" })}
99
+ <span>${() => {
100
+ const current = selection.current();
101
+ return current === undefined
102
+ ? (props.placeholder ?? "Pick a date")
103
+ : format.format(current);
104
+ }}</span>
105
+ </button>
106
+ <input
107
+ type="hidden"
108
+ name="${props.name}"
109
+ .value="${() => {
110
+ const current = selection.current();
111
+ return current === undefined ? "" : toISODate(current);
112
+ }}"
113
+ />
114
+ </div>`;
115
+ });
@@ -0,0 +1,34 @@
1
+ /**
2
+ * DateRangePicker — a Calendar in range mode, behind a button.
3
+ *
4
+ * The same shell as `DatePicker`, and deliberately so: the two differ in what
5
+ * they collect, not in how they behave. Both close on completion, both return
6
+ * focus to the trigger, both format through `Intl` and both post ISO dates.
7
+ *
8
+ * The one behavioural difference is when to close. A single date is done in
9
+ * one click; a range is not done until the second, so the popover stays open
10
+ * between them. Closing on the first click and reopening for the second is the
11
+ * usual bug here, and it makes the control feel broken.
12
+ *
13
+ * Two hidden inputs rather than one comma-joined field. A server parsing
14
+ * `from`/`to` separately never has to split a string, and an empty `to` is
15
+ * unambiguous.
16
+ */
17
+ import { type Reactive } from "../lib/props.js";
18
+ import { type DateRange } from "./Calendar.js";
19
+ export interface DateRangePickerProps {
20
+ /** Base name for the hidden inputs — `"stay"` posts `stay_from` / `stay_to`. */
21
+ name?: string;
22
+ value?: Reactive<DateRange | undefined>;
23
+ defaultValue?: DateRange;
24
+ placeholder?: string;
25
+ min?: Date;
26
+ max?: Date;
27
+ disabled?: Reactive<boolean>;
28
+ invalid?: Reactive<boolean>;
29
+ locale?: string;
30
+ class?: Reactive<string>;
31
+ /** Fires on both clicks — `to` is absent after the first. */
32
+ onValueChange?: (range: DateRange) => void;
33
+ }
34
+ export declare const DateRangePicker: (props?: DateRangePickerProps | undefined) => import("@c9up/aurora").TemplateResult;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * DateRangePicker — a Calendar in range mode, behind a button.
3
+ *
4
+ * The same shell as `DatePicker`, and deliberately so: the two differ in what
5
+ * they collect, not in how they behave. Both close on completion, both return
6
+ * focus to the trigger, both format through `Intl` and both post ISO dates.
7
+ *
8
+ * The one behavioural difference is when to close. A single date is done in
9
+ * one click; a range is not done until the second, so the popover stays open
10
+ * between them. Closing on the first click and reopening for the second is the
11
+ * usual bug here, and it makes the control feel broken.
12
+ *
13
+ * Two hidden inputs rather than one comma-joined field. A server parsing
14
+ * `from`/`to` separately never has to split a string, and an empty `to` is
15
+ * unambiguous.
16
+ */
17
+ import { component, html, signal } from "@c9up/aurora";
18
+ import { buttonVariants } from "../atoms/Button.js";
19
+ import { cn } from "../lib/cn.js";
20
+ import { CalendarIcon } from "../lib/icons.js";
21
+ import { uid } from "../lib/id.js";
22
+ import { zoomInOut } from "../lib/motion.js";
23
+ import { read, readOr } from "../lib/props.js";
24
+ import { controllable } from "../primitives/controllable.js";
25
+ import { floatingSurface } from "../primitives/floatingSurface.js";
26
+ import { focusSilently } from "../primitives/focusable.js";
27
+ import { Calendar, startOfDay } from "./Calendar.js";
28
+ import { toISODate } from "./DatePicker.js";
29
+ export const DateRangePicker = component((props) => {
30
+ const triggerId = uid("date-range-trigger");
31
+ const contentId = uid("date-range-content");
32
+ const open = signal(false);
33
+ const selection = controllable({
34
+ value: props.value,
35
+ initial: props.defaultValue,
36
+ onChange: (next) => {
37
+ if (next !== undefined)
38
+ props.onValueChange?.(next);
39
+ },
40
+ });
41
+ const format = new Intl.DateTimeFormat(props.locale, { dateStyle: "medium" });
42
+ function pick(range) {
43
+ selection.set({
44
+ from: startOfDay(range.from),
45
+ to: range.to === undefined ? undefined : startOfDay(range.to),
46
+ });
47
+ // Only a completed span closes the popover.
48
+ if (range.to === undefined)
49
+ return;
50
+ open(false);
51
+ focusSilently(document.getElementById(triggerId));
52
+ }
53
+ function label() {
54
+ const current = selection.current();
55
+ if (current === undefined)
56
+ return props.placeholder ?? "Pick a date range";
57
+ if (current.to === undefined)
58
+ return `${format.format(current.from)} — …`;
59
+ return `${format.format(current.from)} — ${format.format(current.to)}`;
60
+ }
61
+ floatingSurface({
62
+ anchor: () => document.getElementById(triggerId),
63
+ open: () => open(),
64
+ onClose: () => open(false),
65
+ placement: "bottom-start",
66
+ offset: 4,
67
+ trapFocus: true,
68
+ initialFocus: (content) => content.querySelector("[data-cursor]"),
69
+ content: () => html `<div
70
+ data-slot="date-range-picker-content"
71
+ id="${contentId}"
72
+ role="dialog"
73
+ aria-label="Choose a date range"
74
+ class="${cn("bg-popover text-popover-foreground z-50 rounded-md border p-0 shadow-md outline-none", zoomInOut)}"
75
+ >
76
+ ${Calendar({
77
+ mode: "range",
78
+ range: () => selection.current(),
79
+ defaultMonth: selection.current()?.from,
80
+ min: props.min,
81
+ max: props.max,
82
+ locale: props.locale,
83
+ onRangeChange: pick,
84
+ })}
85
+ </div>`,
86
+ });
87
+ const isoOf = (date) => date === undefined ? "" : toISODate(date);
88
+ return html `<div
89
+ data-slot="date-range-picker"
90
+ class="${() => cn("inline-flex", read(props.class))}"
91
+ >
92
+ <button
93
+ type="button"
94
+ id="${triggerId}"
95
+ data-slot="date-range-picker-trigger"
96
+ aria-haspopup="dialog"
97
+ aria-expanded="${() => (open() ? "true" : "false")}"
98
+ aria-controls="${() => (open() ? contentId : undefined)}"
99
+ aria-invalid="${() => (read(props.invalid) === true ? "true" : undefined)}"
100
+ data-placeholder="${() => (selection.current() === undefined ? "" : undefined)}"
101
+ ?disabled="${() => readOr(props.disabled, false)}"
102
+ class="${buttonVariants({
103
+ variant: "outline",
104
+ class: "w-full justify-start gap-2 font-normal data-[placeholder]:text-muted-foreground",
105
+ })}"
106
+ @click="${() => open(!open())}"
107
+ >
108
+ ${CalendarIcon({ class: "size-4" })}
109
+ <span>${label}</span>
110
+ </button>
111
+ <input
112
+ type="hidden"
113
+ name="${props.name === undefined ? undefined : `${props.name}_from`}"
114
+ .value="${() => isoOf(selection.current()?.from)}"
115
+ />
116
+ <input
117
+ type="hidden"
118
+ name="${props.name === undefined ? undefined : `${props.name}_to`}"
119
+ .value="${() => isoOf(selection.current()?.to)}"
120
+ />
121
+ </div>`;
122
+ });
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Dialog — a modal window over the page.
3
+ *
4
+ * `modalSurface` supplies the contract (trap, scroll lock, backdrop dismissal,
5
+ * hiding the page from assistive technology); this component supplies the
6
+ * markup and the trigger.
7
+ *
8
+ * The `panel` callback matters more than it looks. The overlay's root spans
9
+ * the viewport so the backdrop can, which means containment has to be answered
10
+ * against the panel — check the root and every click on the page counts as
11
+ * inside, and clicking the backdrop silently stops closing the dialog.
12
+ *
13
+ * `aria-labelledby` points at the title and `aria-describedby` at the
14
+ * description, so a screen reader announces what the dialog is for on open. A
15
+ * dialog with neither announces only "dialog", which is why `title` is
16
+ * required rather than optional — `srOnlyTitle` is there for designs that show
17
+ * no visible heading.
18
+ */
19
+ import type { Child } from "../lib/children.js";
20
+ import { type Slot } from "../lib/children.js";
21
+ import { type Reactive } from "../lib/props.js";
22
+ export declare const dialogBackdropClasses = "fixed inset-0 z-50 bg-black/50";
23
+ export declare const dialogPanelClasses = "bg-background fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg border p-6 shadow-lg outline-none sm:max-w-lg";
24
+ export interface DialogProps {
25
+ /** Rendered inside the trigger button. Omit to drive `open` yourself. */
26
+ trigger?: Slot;
27
+ /** Announced on open. Hide it visually with `srOnlyTitle`. */
28
+ title: Child;
29
+ description?: Child;
30
+ children?: Slot;
31
+ /** Actions, laid out bottom-right. */
32
+ footer?: Slot;
33
+ open?: Reactive<boolean>;
34
+ defaultOpen?: boolean;
35
+ onOpenChange?: (open: boolean) => void;
36
+ /** Keep the title for screen readers but out of the layout. */
37
+ srOnlyTitle?: boolean;
38
+ /** Hide the corner close button. Escape and the backdrop still work. */
39
+ hideCloseButton?: boolean;
40
+ class?: Reactive<string>;
41
+ triggerClass?: Reactive<string>;
42
+ contentClass?: Reactive<string>;
43
+ }
44
+ export declare const Dialog: (props?: DialogProps | undefined) => import("@c9up/aurora").TemplateResult;
45
+ /** A dialog driven purely by a signal, with no trigger of its own. */
46
+ export declare function useDialog(initial?: boolean): {
47
+ open: () => boolean;
48
+ show(): void;
49
+ hide(): void;
50
+ };
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Dialog — a modal window over the page.
3
+ *
4
+ * `modalSurface` supplies the contract (trap, scroll lock, backdrop dismissal,
5
+ * hiding the page from assistive technology); this component supplies the
6
+ * markup and the trigger.
7
+ *
8
+ * The `panel` callback matters more than it looks. The overlay's root spans
9
+ * the viewport so the backdrop can, which means containment has to be answered
10
+ * against the panel — check the root and every click on the page counts as
11
+ * inside, and clicking the backdrop silently stops closing the dialog.
12
+ *
13
+ * `aria-labelledby` points at the title and `aria-describedby` at the
14
+ * description, so a screen reader announces what the dialog is for on open. A
15
+ * dialog with neither announces only "dialog", which is why `title` is
16
+ * required rather than optional — `srOnlyTitle` is there for designs that show
17
+ * no visible heading.
18
+ */
19
+ import { component, html, signal } from "@c9up/aurora";
20
+ import { slot } from "../lib/children.js";
21
+ import { cn } from "../lib/cn.js";
22
+ import { XIcon } from "../lib/icons.js";
23
+ import { uid } from "../lib/id.js";
24
+ import { fadeInOut, zoomInOut } from "../lib/motion.js";
25
+ import { read } from "../lib/props.js";
26
+ import { controllable } from "../primitives/controllable.js";
27
+ import { modalSurface } from "../primitives/modalSurface.js";
28
+ export const dialogBackdropClasses = "fixed inset-0 z-50 bg-black/50";
29
+ export const dialogPanelClasses = "bg-background fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg border p-6 shadow-lg outline-none sm:max-w-lg";
30
+ export const Dialog = component((props) => {
31
+ const triggerId = uid("dialog-trigger");
32
+ const panelId = uid("dialog-panel");
33
+ const titleId = uid("dialog-title");
34
+ const descriptionId = uid("dialog-description");
35
+ const state = controllable({
36
+ value: props.open,
37
+ initial: props.defaultOpen ?? false,
38
+ onChange: props.onOpenChange,
39
+ });
40
+ modalSurface({
41
+ open: () => state.current(),
42
+ onClose: () => state.set(false),
43
+ panel: (root) => root.querySelector(`#${CSS.escape(panelId)}`),
44
+ returnFocus: () => document.getElementById(triggerId),
45
+ content: () => html `<div data-slot="dialog-overlay" class="${cn("fixed inset-0 z-50", fadeInOut)}">
46
+ <div data-slot="dialog-backdrop" class="${dialogBackdropClasses}"></div>
47
+ <div
48
+ data-slot="dialog-content"
49
+ id="${panelId}"
50
+ role="dialog"
51
+ aria-modal="true"
52
+ aria-labelledby="${titleId}"
53
+ aria-describedby="${props.description === undefined ? undefined : descriptionId}"
54
+ tabindex="-1"
55
+ class="${cn(dialogPanelClasses, zoomInOut, read(props.contentClass))}"
56
+ >
57
+ <div data-slot="dialog-header" class="flex flex-col gap-2 text-center sm:text-left">
58
+ <h2
59
+ id="${titleId}"
60
+ data-slot="dialog-title"
61
+ class="${cn("text-lg leading-none font-semibold", props.srOnlyTitle === true ? "sr-only" : "")}"
62
+ >${props.title}</h2>
63
+ ${props.description === undefined
64
+ ? null
65
+ : html `<p
66
+ id="${descriptionId}"
67
+ data-slot="dialog-description"
68
+ class="text-muted-foreground text-sm"
69
+ >${props.description}</p>`}
70
+ </div>
71
+ ${slot(props.children)}
72
+ ${props.footer === undefined
73
+ ? null
74
+ : html `<div
75
+ data-slot="dialog-footer"
76
+ class="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"
77
+ >${slot(props.footer)}</div>`}
78
+ ${props.hideCloseButton === true
79
+ ? null
80
+ : html `<button
81
+ type="button"
82
+ data-slot="dialog-close"
83
+ class="ring-offset-background focus-visible:ring-ring absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
84
+ @click="${() => state.set(false)}"
85
+ >
86
+ ${XIcon({ class: "size-4" })}
87
+ <span class="sr-only">Close</span>
88
+ </button>`}
89
+ </div>
90
+ </div>`,
91
+ });
92
+ if (props.trigger === undefined) {
93
+ // No trigger: the dialog is driven entirely by the `open` prop. Something
94
+ // still has to be in the tree for the component to mount into, and the
95
+ // surface itself lives in a portal.
96
+ return html `<span data-slot="dialog" hidden></span>`;
97
+ }
98
+ return html `<button
99
+ type="button"
100
+ data-slot="dialog-trigger"
101
+ id="${triggerId}"
102
+ aria-haspopup="dialog"
103
+ aria-expanded="${() => (state.current() ? "true" : "false")}"
104
+ class="${() => cn(read(props.triggerClass))}"
105
+ @click="${() => state.set(true)}"
106
+ >${slot(props.trigger)}</button>`;
107
+ });
108
+ /** A dialog driven purely by a signal, with no trigger of its own. */
109
+ export function useDialog(initial = false) {
110
+ const open = signal(initial);
111
+ return {
112
+ open: () => open(),
113
+ show: () => open(true),
114
+ hide: () => open(false),
115
+ };
116
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Drawer — a bottom sheet you can swipe away.
3
+ *
4
+ * A Sheet from the bottom edge, plus the one gesture that makes it feel native
5
+ * on a phone: drag the panel down to dismiss it.
6
+ *
7
+ * The drag rules are what separate this from a panel that merely follows the
8
+ * finger:
9
+ *
10
+ * - Downward only. Dragging up would peel the panel off its edge and leave a
11
+ * gap under it.
12
+ * - Released past a threshold, it closes; short of it, it springs back. A
13
+ * distance threshold alone punishes a fast flick, so velocity counts too —
14
+ * a quick short drag dismisses, which is what the gesture means.
15
+ * - The transition is disabled during the drag and restored on release.
16
+ * Leaving it on makes the panel lag the finger by its own duration, which
17
+ * reads as a broken gesture rather than a smooth one.
18
+ *
19
+ * The grab handle is decorative. Everything the drag does, the close button,
20
+ * Escape and the backdrop already do — a gesture must never be the only way
21
+ * out of a modal surface.
22
+ */
23
+ import type { Child } from "../lib/children.js";
24
+ import { type Slot } from "../lib/children.js";
25
+ import { type Reactive } from "../lib/props.js";
26
+ export interface DrawerProps {
27
+ trigger?: Slot;
28
+ title: Child;
29
+ description?: Child;
30
+ children?: Slot;
31
+ footer?: Slot;
32
+ open?: Reactive<boolean>;
33
+ defaultOpen?: boolean;
34
+ onOpenChange?: (open: boolean) => void;
35
+ /** Turn off drag-to-dismiss — for a drawer holding a scrollable list. */
36
+ disableDrag?: boolean;
37
+ triggerClass?: Reactive<string>;
38
+ contentClass?: Reactive<string>;
39
+ }
40
+ export declare const Drawer: (props?: DrawerProps | undefined) => import("@c9up/aurora").TemplateResult;
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Drawer — a bottom sheet you can swipe away.
3
+ *
4
+ * A Sheet from the bottom edge, plus the one gesture that makes it feel native
5
+ * on a phone: drag the panel down to dismiss it.
6
+ *
7
+ * The drag rules are what separate this from a panel that merely follows the
8
+ * finger:
9
+ *
10
+ * - Downward only. Dragging up would peel the panel off its edge and leave a
11
+ * gap under it.
12
+ * - Released past a threshold, it closes; short of it, it springs back. A
13
+ * distance threshold alone punishes a fast flick, so velocity counts too —
14
+ * a quick short drag dismisses, which is what the gesture means.
15
+ * - The transition is disabled during the drag and restored on release.
16
+ * Leaving it on makes the panel lag the finger by its own duration, which
17
+ * reads as a broken gesture rather than a smooth one.
18
+ *
19
+ * The grab handle is decorative. Everything the drag does, the close button,
20
+ * Escape and the backdrop already do — a gesture must never be the only way
21
+ * out of a modal surface.
22
+ */
23
+ import { component, html } from "@c9up/aurora";
24
+ import { slot } from "../lib/children.js";
25
+ import { cn } from "../lib/cn.js";
26
+ import { uid } from "../lib/id.js";
27
+ import { fadeInOut, slideFrom } from "../lib/motion.js";
28
+ import { read } from "../lib/props.js";
29
+ import { controllable } from "../primitives/controllable.js";
30
+ import { modalSurface } from "../primitives/modalSurface.js";
31
+ import { dialogBackdropClasses } from "./Dialog.js";
32
+ /** Drag this far down and release to dismiss, in px. */
33
+ const DISMISS_DISTANCE = 120;
34
+ /** Or release faster than this, in px per millisecond. */
35
+ const DISMISS_VELOCITY = 0.5;
36
+ export const Drawer = component((props) => {
37
+ const triggerId = uid("drawer-trigger");
38
+ const panelId = uid("drawer-panel");
39
+ const titleId = uid("drawer-title");
40
+ const descriptionId = uid("drawer-description");
41
+ const state = controllable({
42
+ value: props.open,
43
+ initial: props.defaultOpen ?? false,
44
+ onChange: props.onOpenChange,
45
+ });
46
+ function onPointerDown(event) {
47
+ if (props.disableDrag === true)
48
+ return;
49
+ const panel = document.getElementById(panelId);
50
+ if (panel === null)
51
+ return;
52
+ const startY = event.clientY;
53
+ const startedAt = performance.now();
54
+ let offset = 0;
55
+ panel.setPointerCapture(event.pointerId);
56
+ panel.style.transition = "none";
57
+ const onMove = (move) => {
58
+ offset = Math.max(0, move.clientY - startY);
59
+ panel.style.transform = `translateY(${offset}px)`;
60
+ };
61
+ const onUp = () => {
62
+ panel.releasePointerCapture(event.pointerId);
63
+ panel.removeEventListener("pointermove", onMove);
64
+ panel.removeEventListener("pointerup", onUp);
65
+ panel.removeEventListener("pointercancel", onUp);
66
+ // Restore the transition before deciding, so both outcomes animate.
67
+ panel.style.transition = "";
68
+ const velocity = offset / Math.max(1, performance.now() - startedAt);
69
+ if (offset > DISMISS_DISTANCE || velocity > DISMISS_VELOCITY) {
70
+ panel.style.transform = "";
71
+ state.set(false);
72
+ }
73
+ else {
74
+ panel.style.transform = "";
75
+ }
76
+ };
77
+ panel.addEventListener("pointermove", onMove);
78
+ panel.addEventListener("pointerup", onUp);
79
+ panel.addEventListener("pointercancel", onUp);
80
+ }
81
+ modalSurface({
82
+ open: () => state.current(),
83
+ onClose: () => state.set(false),
84
+ panel: (root) => root.querySelector(`#${CSS.escape(panelId)}`),
85
+ returnFocus: () => document.getElementById(triggerId),
86
+ content: () => html `<div data-slot="drawer-overlay" class="${cn("fixed inset-0 z-50", fadeInOut)}">
87
+ <div class="${dialogBackdropClasses}"></div>
88
+ <div
89
+ data-slot="drawer-content"
90
+ id="${panelId}"
91
+ role="dialog"
92
+ aria-modal="true"
93
+ aria-labelledby="${titleId}"
94
+ aria-describedby="${props.description === undefined ? undefined : descriptionId}"
95
+ tabindex="-1"
96
+ class="${cn("bg-background fixed inset-x-0 bottom-0 z-50 mt-24 flex max-h-[80vh] touch-none flex-col gap-4 rounded-t-lg border-t p-6 shadow-lg outline-none", slideFrom("bottom"), read(props.contentClass))}"
97
+ @pointerdown="${onPointerDown}"
98
+ >
99
+ <div
100
+ data-slot="drawer-handle"
101
+ aria-hidden="true"
102
+ class="bg-muted mx-auto h-1.5 w-12 shrink-0 rounded-full"
103
+ ></div>
104
+ <div class="flex flex-col gap-1.5 text-center sm:text-left">
105
+ <h2 id="${titleId}" class="text-foreground font-semibold">${props.title}</h2>
106
+ ${props.description === undefined
107
+ ? null
108
+ : html `<p id="${descriptionId}" class="text-muted-foreground text-sm">
109
+ ${props.description}
110
+ </p>`}
111
+ </div>
112
+ <div class="flex-1 overflow-y-auto">${slot(props.children)}</div>
113
+ ${props.footer === undefined
114
+ ? null
115
+ : html `<div class="mt-auto flex flex-col gap-2">${slot(props.footer)}</div>`}
116
+ </div>
117
+ </div>`,
118
+ });
119
+ if (props.trigger === undefined) {
120
+ return html `<span data-slot="drawer" hidden></span>`;
121
+ }
122
+ return html `<button
123
+ type="button"
124
+ data-slot="drawer-trigger"
125
+ id="${triggerId}"
126
+ aria-haspopup="dialog"
127
+ aria-expanded="${() => (state.current() ? "true" : "false")}"
128
+ class="${() => cn(read(props.triggerClass))}"
129
+ @click="${() => state.set(true)}"
130
+ >${slot(props.trigger)}</button>`;
131
+ });
@@ -0,0 +1,30 @@
1
+ /**
2
+ * DropdownMenu — a menu opened from a button.
3
+ *
4
+ * The panel, its entries and its keyboard model all come from `./menu.js`;
5
+ * this component contributes the trigger and the open/close state.
6
+ *
7
+ * Two behaviours are specific to a button-opened menu:
8
+ *
9
+ * - Opening with the keyboard focuses the first item; opening with the pointer
10
+ * does not. A keyboard user has no other way in, while pre-highlighting an
11
+ * item for a mouse user suggests it is about to be chosen.
12
+ * - ArrowDown on the trigger opens the menu *and* enters it, which is the
13
+ * behaviour of every native menu button and the one users try first.
14
+ */
15
+ import { type Slot } from "../lib/children.js";
16
+ import { type Reactive } from "../lib/props.js";
17
+ import type { Placement } from "../primitives/floating.js";
18
+ import { type MenuEntry } from "../primitives/menu.js";
19
+ export interface DropdownMenuProps {
20
+ trigger?: Slot;
21
+ entries: readonly MenuEntry[];
22
+ open?: Reactive<boolean>;
23
+ defaultOpen?: boolean;
24
+ onOpenChange?: (open: boolean) => void;
25
+ placement?: Placement;
26
+ class?: Reactive<string>;
27
+ triggerClass?: Reactive<string>;
28
+ contentClass?: Reactive<string>;
29
+ }
30
+ export declare const DropdownMenu: (props?: DropdownMenuProps | undefined) => import("@c9up/aurora").TemplateResult;