@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,116 @@
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 { component, html, onMount, onUnmount } from "@c9up/aurora";
24
+ import { cn } from "../lib/cn.js";
25
+ import { uid } from "../lib/id.js";
26
+ import { fadeInOut, zoomInOut } from "../lib/motion.js";
27
+ import { read } from "../lib/props.js";
28
+ import { controllable } from "../primitives/controllable.js";
29
+ import { modalSurface } from "../primitives/modalSurface.js";
30
+ import { Command } from "./Command.js";
31
+ import { dialogBackdropClasses } from "./Dialog.js";
32
+ export const CommandDialog = component((props) => {
33
+ const panelId = uid("command-dialog-panel");
34
+ const titleId = uid("command-dialog-title");
35
+ const descriptionId = uid("command-dialog-description");
36
+ const state = controllable({
37
+ value: props.open,
38
+ initial: props.defaultOpen ?? false,
39
+ onChange: props.onOpenChange,
40
+ });
41
+ /**
42
+ * Toggle, not open — the same keystroke has to close it.
43
+ *
44
+ * Captured at the document, so a menu or popover that is already open does
45
+ * not consume the key on its way up. `metaKey || ctrlKey` covers both
46
+ * platforms without sniffing the user agent.
47
+ */
48
+ function onKeyDown(event) {
49
+ if (props.shortcut === undefined)
50
+ return;
51
+ if (event.key.toLowerCase() !== props.shortcut.toLowerCase())
52
+ return;
53
+ if (!event.metaKey && !event.ctrlKey)
54
+ return;
55
+ event.preventDefault();
56
+ state.set(!state.current());
57
+ }
58
+ onMount(() => {
59
+ if (props.shortcut === undefined)
60
+ return;
61
+ document.addEventListener("keydown", onKeyDown, true);
62
+ });
63
+ onUnmount(() => document.removeEventListener("keydown", onKeyDown, true));
64
+ function close() {
65
+ state.set(false);
66
+ }
67
+ function runAndClose(item) {
68
+ props.onSelect?.(item);
69
+ // Closing after the action, not before: a handler that opens another
70
+ // surface should not have this one tearing down focus underneath it.
71
+ close();
72
+ }
73
+ modalSurface({
74
+ open: () => state.current(),
75
+ onClose: close,
76
+ panel: (root) => root.querySelector(`#${CSS.escape(panelId)}`),
77
+ // The search field, not the first focusable — the whole point of the
78
+ // palette is that you can start typing immediately.
79
+ initialFocus: (panel) => panel.querySelector("input"),
80
+ content: () => html `<div
81
+ data-slot="command-dialog-overlay"
82
+ class="${cn("fixed inset-0 z-50", fadeInOut)}"
83
+ >
84
+ <div class="${dialogBackdropClasses}"></div>
85
+ <div
86
+ data-slot="command-dialog"
87
+ id="${panelId}"
88
+ role="dialog"
89
+ aria-modal="true"
90
+ aria-labelledby="${titleId}"
91
+ aria-describedby="${props.description === undefined ? undefined : descriptionId}"
92
+ tabindex="-1"
93
+ class="${cn("bg-popover fixed top-[20%] left-1/2 z-50 w-full max-w-lg -translate-x-1/2 overflow-hidden rounded-lg border shadow-lg outline-none", zoomInOut, read(props.contentClass))}"
94
+ >
95
+ <h2
96
+ id="${titleId}"
97
+ class="${cn("px-4 pt-4 text-sm font-medium", props.showTitle === true ? "" : "sr-only")}"
98
+ >${props.title ?? "Command palette"}</h2>
99
+ ${props.description === undefined
100
+ ? null
101
+ : html `<p id="${descriptionId}" class="sr-only">${props.description}</p>`}
102
+ ${Command({
103
+ items: props.items,
104
+ placeholder: props.placeholder,
105
+ emptyMessage: props.emptyMessage,
106
+ filter: props.filter,
107
+ onSelect: runAndClose,
108
+ })}
109
+ </div>
110
+ </div>`,
111
+ });
112
+ // The palette has no trigger of its own — it is opened by the shortcut or
113
+ // by the `open` prop. Something still has to sit in the tree for the
114
+ // component to mount into; the surface itself lives in a portal.
115
+ return html `<span data-slot="command-dialog-root" hidden></span>`;
116
+ });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * ContextMenu — a menu opened by right-clicking a region.
3
+ *
4
+ * Same panel as DropdownMenu; the difference is the anchor. There is no
5
+ * trigger element to position against — the menu belongs at the pointer, which
6
+ * could be anywhere inside the region.
7
+ *
8
+ * So a zero-size element is parked at the click coordinates and used as the
9
+ * anchor. It is a real element on purpose: `autoPosition` measures its rect,
10
+ * and that rect flips and shifts exactly like a button's would, which is what
11
+ * gets a menu opened near the bottom of the window to open upwards instead of
12
+ * off screen.
13
+ *
14
+ * Long-press opens it on touch, where there is no right button. The 500ms
15
+ * threshold matches the platform convention, and any movement cancels it so a
16
+ * scroll gesture does not surface a menu.
17
+ */
18
+ import { type Slot } from "../lib/children.js";
19
+ import { type Reactive } from "../lib/props.js";
20
+ import { type MenuEntry } from "../primitives/menu.js";
21
+ export interface ContextMenuProps {
22
+ /** The region that answers to right-click. */
23
+ children?: Slot;
24
+ entries: readonly MenuEntry[];
25
+ onOpenChange?: (open: boolean) => void;
26
+ class?: Reactive<string>;
27
+ contentClass?: Reactive<string>;
28
+ }
29
+ export declare const ContextMenu: (props?: ContextMenuProps | undefined) => import("@c9up/aurora").TemplateResult;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * ContextMenu — a menu opened by right-clicking a region.
3
+ *
4
+ * Same panel as DropdownMenu; the difference is the anchor. There is no
5
+ * trigger element to position against — the menu belongs at the pointer, which
6
+ * could be anywhere inside the region.
7
+ *
8
+ * So a zero-size element is parked at the click coordinates and used as the
9
+ * anchor. It is a real element on purpose: `autoPosition` measures its rect,
10
+ * and that rect flips and shifts exactly like a button's would, which is what
11
+ * gets a menu opened near the bottom of the window to open upwards instead of
12
+ * off screen.
13
+ *
14
+ * Long-press opens it on touch, where there is no right button. The 500ms
15
+ * threshold matches the platform convention, and any movement cancels it so a
16
+ * scroll gesture does not surface a menu.
17
+ */
18
+ import { component, html, onUnmount, signal } from "@c9up/aurora";
19
+ import { slot } from "../lib/children.js";
20
+ import { cn } from "../lib/cn.js";
21
+ import { uid } from "../lib/id.js";
22
+ import { read } from "../lib/props.js";
23
+ import { floatingSurface } from "../primitives/floatingSurface.js";
24
+ import { menuPanel, wireMenu } from "../primitives/menu.js";
25
+ const LONG_PRESS_MS = 500;
26
+ export const ContextMenu = component((props) => {
27
+ const regionId = uid("context-menu-region");
28
+ const anchorId = uid("context-menu-anchor");
29
+ const contentId = uid("context-menu-content");
30
+ const open = signal(false);
31
+ let unwire = null;
32
+ let pressTimer;
33
+ function setOpen(next) {
34
+ open(next);
35
+ props.onOpenChange?.(next);
36
+ }
37
+ function moveAnchorTo(x, y) {
38
+ const anchor = document.getElementById(anchorId);
39
+ if (anchor === null)
40
+ return;
41
+ anchor.style.left = `${x}px`;
42
+ anchor.style.top = `${y}px`;
43
+ }
44
+ function openAt(x, y) {
45
+ moveAnchorTo(x, y);
46
+ setOpen(true);
47
+ }
48
+ function onContextMenu(event) {
49
+ event.preventDefault();
50
+ openAt(event.clientX, event.clientY);
51
+ }
52
+ function onPointerDown(event) {
53
+ if (event.pointerType !== "touch")
54
+ return;
55
+ cancelPress();
56
+ pressTimer = setTimeout(() => openAt(event.clientX, event.clientY), LONG_PRESS_MS);
57
+ }
58
+ function cancelPress() {
59
+ if (pressTimer !== undefined)
60
+ clearTimeout(pressTimer);
61
+ pressTimer = undefined;
62
+ }
63
+ onUnmount(cancelPress);
64
+ floatingSurface({
65
+ anchor: () => document.getElementById(anchorId),
66
+ open: () => open(),
67
+ onClose: () => setOpen(false),
68
+ placement: "bottom-start",
69
+ offset: 2,
70
+ content: () => menuPanel({
71
+ id: contentId,
72
+ entries: props.entries,
73
+ onCloseAll: () => setOpen(false),
74
+ class: read(props.contentClass),
75
+ }),
76
+ onOpened: (panel) => {
77
+ unwire = wireMenu(panel, { onCloseAll: () => setOpen(false) });
78
+ panel.focus({ preventScroll: true });
79
+ },
80
+ onClosed: () => {
81
+ unwire?.();
82
+ unwire = null;
83
+ },
84
+ });
85
+ return html `<div
86
+ data-slot="context-menu"
87
+ id="${regionId}"
88
+ class="${() => cn(read(props.class))}"
89
+ @contextmenu="${onContextMenu}"
90
+ @pointerdown="${onPointerDown}"
91
+ @pointerup="${cancelPress}"
92
+ @pointermove="${cancelPress}"
93
+ @pointercancel="${cancelPress}"
94
+ >
95
+ ${slot(props.children)}
96
+ <span
97
+ id="${anchorId}"
98
+ aria-hidden="true"
99
+ class="pointer-events-none fixed size-0"
100
+ ></span>
101
+ </div>`;
102
+ });
@@ -0,0 +1,50 @@
1
+ /**
2
+ * DataTable — a table with sorting, filtering, paging and row selection.
3
+ *
4
+ * shadcn wraps `@tanstack/react-table`, a headless engine with column
5
+ * grouping, virtualisation, pinning and faceted filters. nebula implements the
6
+ * four features its own examples actually use, over the `Table` molecule.
7
+ *
8
+ * The scope line is drawn where the data does. Everything here operates on an
9
+ * in-memory array: sorting compares values, filtering scans rows, paging
10
+ * slices. That is the right answer up to a few thousand rows and the wrong one
11
+ * beyond, where the work belongs on the server — so rather than growing a
12
+ * half-server-side mode, the component stays client-side and honest about it.
13
+ *
14
+ * A generic function rather than `component<P>`: `component` needs a concrete
15
+ * prop type, and `Row` has to stay open so `cell` receives the caller's own
16
+ * row type instead of a record of unknowns.
17
+ *
18
+ * `aria-sort` on the sorted column is the part hand-rolled tables always miss.
19
+ * The little chevron says which column is sorted to anyone who can see it; the
20
+ * attribute is what says it to everyone else.
21
+ */
22
+ import { type TemplateResult } from "@c9up/aurora";
23
+ import type { Child } from "../lib/children.js";
24
+ export interface Column<Row> {
25
+ key: string;
26
+ header: Child;
27
+ /** Renders the cell. Defaults to the row's property of the same key. */
28
+ cell?: (row: Row) => Child;
29
+ sortable?: boolean;
30
+ /** What to compare when sorting. Defaults to the rendered property. */
31
+ sortValue?: (row: Row) => string | number;
32
+ class?: string;
33
+ }
34
+ export interface DataTableProps<Row> {
35
+ columns: readonly Column<Row>[];
36
+ rows: readonly Row[];
37
+ /** Stable identity per row — used for selection and as the render key. */
38
+ rowKey: (row: Row) => string;
39
+ /** Rows per page. Omit to show everything. */
40
+ pageSize?: number;
41
+ /** Show a filter box. Provide `filterMatch` to say what it searches. */
42
+ filterPlaceholder?: string;
43
+ filterMatch?: (row: Row, query: string) => boolean;
44
+ /** Add a checkbox column. */
45
+ selectable?: boolean;
46
+ onSelectionChange?: (keys: readonly string[]) => void;
47
+ emptyMessage?: Child;
48
+ class?: string;
49
+ }
50
+ export declare function DataTable<Row>(props: DataTableProps<Row>): TemplateResult;
@@ -0,0 +1,257 @@
1
+ /**
2
+ * DataTable — a table with sorting, filtering, paging and row selection.
3
+ *
4
+ * shadcn wraps `@tanstack/react-table`, a headless engine with column
5
+ * grouping, virtualisation, pinning and faceted filters. nebula implements the
6
+ * four features its own examples actually use, over the `Table` molecule.
7
+ *
8
+ * The scope line is drawn where the data does. Everything here operates on an
9
+ * in-memory array: sorting compares values, filtering scans rows, paging
10
+ * slices. That is the right answer up to a few thousand rows and the wrong one
11
+ * beyond, where the work belongs on the server — so rather than growing a
12
+ * half-server-side mode, the component stays client-side and honest about it.
13
+ *
14
+ * A generic function rather than `component<P>`: `component` needs a concrete
15
+ * prop type, and `Row` has to stay open so `cell` receives the caller's own
16
+ * row type instead of a record of unknowns.
17
+ *
18
+ * `aria-sort` on the sorted column is the part hand-rolled tables always miss.
19
+ * The little chevron says which column is sorted to anyone who can see it; the
20
+ * attribute is what says it to everyone else.
21
+ */
22
+ import { html, signal } from "@c9up/aurora";
23
+ import { Checkbox } from "../atoms/Checkbox.js";
24
+ import { Input } from "../atoms/Input.js";
25
+ import { cn } from "../lib/cn.js";
26
+ import { ChevronDownIcon, ChevronsUpDownIcon, ChevronUpIcon, } from "../lib/icons.js";
27
+ import { Pagination } from "../molecules/Pagination.js";
28
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../molecules/Table.js";
29
+ export function DataTable(props) {
30
+ const sortKey = signal(undefined);
31
+ const sortDirection = signal("asc");
32
+ const query = signal("");
33
+ const page = signal(1);
34
+ const selected = signal([]);
35
+ function readCell(row, column) {
36
+ if (column.cell !== undefined)
37
+ return column.cell(row);
38
+ return propertyOf(row, column.key);
39
+ }
40
+ function sortableValue(row, column) {
41
+ if (column.sortValue !== undefined)
42
+ return column.sortValue(row);
43
+ const value = propertyOf(row, column.key);
44
+ return typeof value === "number" ? value : String(value ?? "");
45
+ }
46
+ function filtered() {
47
+ const needle = query().trim().toLowerCase();
48
+ if (needle === "" || props.filterMatch === undefined)
49
+ return props.rows;
50
+ return props.rows.filter((row) => props.filterMatch?.(row, needle) === true);
51
+ }
52
+ function sorted() {
53
+ const key = sortKey();
54
+ const rows = filtered();
55
+ if (key === undefined)
56
+ return rows;
57
+ const column = props.columns.find((entry) => entry.key === key);
58
+ if (column === undefined)
59
+ return rows;
60
+ const direction = sortDirection() === "asc" ? 1 : -1;
61
+ // A copy: sorting the caller's array in place would reorder their data as
62
+ // a side effect of rendering it.
63
+ return [...rows].sort((left, right) => {
64
+ const a = sortableValue(left, column);
65
+ const b = sortableValue(right, column);
66
+ if (typeof a === "number" && typeof b === "number")
67
+ return (a - b) * direction;
68
+ return String(a).localeCompare(String(b)) * direction;
69
+ });
70
+ }
71
+ function pageCount() {
72
+ if (props.pageSize === undefined)
73
+ return 1;
74
+ return Math.max(1, Math.ceil(sorted().length / props.pageSize));
75
+ }
76
+ function visible() {
77
+ const rows = sorted();
78
+ if (props.pageSize === undefined)
79
+ return rows;
80
+ // Clamp: filtering down to fewer pages while on the last one would
81
+ // otherwise show an empty table with no way back.
82
+ const current = Math.min(page(), pageCount());
83
+ const from = (current - 1) * props.pageSize;
84
+ return rows.slice(from, from + props.pageSize);
85
+ }
86
+ function toggleSort(column) {
87
+ if (column.sortable === false)
88
+ return;
89
+ if (sortKey() === column.key) {
90
+ sortDirection(sortDirection() === "asc" ? "desc" : "asc");
91
+ return;
92
+ }
93
+ sortKey(column.key);
94
+ sortDirection("asc");
95
+ }
96
+ function setSelection(keys) {
97
+ selected(keys);
98
+ props.onSelectionChange?.(keys);
99
+ }
100
+ function toggleRow(key) {
101
+ const current = selected();
102
+ setSelection(current.includes(key)
103
+ ? current.filter((entry) => entry !== key)
104
+ : [...current, key]);
105
+ }
106
+ /**
107
+ * The header checkbox acts on the *visible* page, not the whole dataset.
108
+ *
109
+ * Selecting rows the user cannot see — and may have filtered away on
110
+ * purpose — is the behaviour behind every "I deleted more than I meant to"
111
+ * story about a bulk action.
112
+ */
113
+ function toggleAllVisible() {
114
+ const keys = visible().map(props.rowKey);
115
+ const allSelected = keys.every((key) => selected().includes(key));
116
+ if (allSelected) {
117
+ setSelection(selected().filter((key) => !keys.includes(key)));
118
+ }
119
+ else {
120
+ const merged = new Set([...selected(), ...keys]);
121
+ setSelection([...merged]);
122
+ }
123
+ }
124
+ function ariaSortFor(column) {
125
+ if (column.sortable === false)
126
+ return undefined;
127
+ if (sortKey() !== column.key)
128
+ return "none";
129
+ return sortDirection() === "asc" ? "ascending" : "descending";
130
+ }
131
+ function sortIcon(column) {
132
+ if (column.sortable === false)
133
+ return null;
134
+ if (sortKey() !== column.key) {
135
+ return ChevronsUpDownIcon({ class: "ml-2 size-3.5 opacity-50" });
136
+ }
137
+ return sortDirection() === "asc"
138
+ ? ChevronUpIcon({ class: "ml-2 size-3.5" })
139
+ : ChevronDownIcon({ class: "ml-2 size-3.5" });
140
+ }
141
+ const columnCount = props.columns.length + (props.selectable === true ? 1 : 0);
142
+ return html `<div data-slot="data-table" class="${cn("flex flex-col gap-4", props.class)}">
143
+ ${props.filterMatch === undefined
144
+ ? null
145
+ : Input({
146
+ type: "search",
147
+ placeholder: props.filterPlaceholder ?? "Filter…",
148
+ class: "max-w-sm",
149
+ onInput: (value) => {
150
+ query(value);
151
+ page(1);
152
+ },
153
+ })}
154
+ ${Table({
155
+ children: [
156
+ TableHeader({
157
+ children: TableRow({
158
+ children: [
159
+ props.selectable === true
160
+ ? TableHead({
161
+ class: "w-10",
162
+ children: Checkbox({
163
+ label: "Select all rows on this page",
164
+ checked: () => {
165
+ const keys = visible().map(props.rowKey);
166
+ return (keys.length > 0 &&
167
+ keys.every((key) => selected().includes(key)));
168
+ },
169
+ indeterminate: () => {
170
+ const keys = visible().map(props.rowKey);
171
+ const chosen = keys.filter((key) => selected().includes(key)).length;
172
+ return chosen > 0 && chosen < keys.length;
173
+ },
174
+ onCheckedChange: toggleAllVisible,
175
+ }),
176
+ })
177
+ : null,
178
+ ...props.columns.map((column) => TableHead({
179
+ class: cn(column.sortable === false
180
+ ? ""
181
+ : "cursor-pointer select-none", column.class),
182
+ sort: () => ariaSortFor(column),
183
+ onClick: () => toggleSort(column),
184
+ children: html `<span class="inline-flex items-center"
185
+ >${column.header}${() => sortIcon(column)}</span
186
+ >`,
187
+ })),
188
+ ],
189
+ }),
190
+ }),
191
+ TableBody({
192
+ children: () => {
193
+ const rows = visible();
194
+ if (rows.length === 0) {
195
+ return TableRow({
196
+ children: TableCell({
197
+ colspan: columnCount,
198
+ class: "h-24 text-center",
199
+ children: props.emptyMessage ?? "No results.",
200
+ }),
201
+ });
202
+ }
203
+ return rows.map((row) => renderRow(row));
204
+ },
205
+ }),
206
+ ],
207
+ })}
208
+ ${props.pageSize === undefined
209
+ ? null
210
+ : Pagination({
211
+ page: () => Math.min(page(), pageCount()),
212
+ pageCount,
213
+ onPageChange: (next) => page(next),
214
+ })}
215
+ </div>`;
216
+ function renderRow(row) {
217
+ const key = props.rowKey(row);
218
+ return TableRow({
219
+ selected: () => selected().includes(key),
220
+ children: [
221
+ props.selectable === true
222
+ ? TableCell({
223
+ children: Checkbox({
224
+ label: `Select row ${key}`,
225
+ checked: () => selected().includes(key),
226
+ onCheckedChange: () => toggleRow(key),
227
+ }),
228
+ })
229
+ : null,
230
+ ...props.columns.map((column) => TableCell({ class: column.class, children: readCell(row, column) })),
231
+ ],
232
+ });
233
+ }
234
+ }
235
+ /**
236
+ * Read a property by name from a row of unknown shape.
237
+ *
238
+ * `Row` is the caller's own type and may be an interface, which cannot be
239
+ * indexed by an arbitrary string. The guard turns the lookup into a checked
240
+ * read that yields `undefined` for a missing key — a blank cell — instead of
241
+ * an assertion that it exists.
242
+ */
243
+ function propertyOf(row, key) {
244
+ if (typeof row !== "object" || row === null)
245
+ return null;
246
+ if (!(key in row))
247
+ return null;
248
+ const value = Reflect.get(row, key);
249
+ if (value === null || value === undefined)
250
+ return null;
251
+ if (typeof value === "string" ||
252
+ typeof value === "number" ||
253
+ typeof value === "boolean") {
254
+ return value;
255
+ }
256
+ return String(value);
257
+ }
@@ -0,0 +1,34 @@
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 { type Reactive } from "../lib/props.js";
19
+ export interface DatePickerProps {
20
+ name?: string;
21
+ value?: Reactive<Date | undefined>;
22
+ defaultValue?: Date;
23
+ placeholder?: string;
24
+ min?: Date;
25
+ max?: Date;
26
+ disabled?: Reactive<boolean>;
27
+ invalid?: Reactive<boolean>;
28
+ locale?: string;
29
+ class?: Reactive<string>;
30
+ onValueChange?: (date: Date) => void;
31
+ }
32
+ /** `YYYY-MM-DD` from a local date, without a timezone round-trip. */
33
+ export declare function toISODate(date: Date): string;
34
+ export declare const DatePicker: (props?: DatePickerProps | undefined) => import("@c9up/aurora").TemplateResult;