@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,149 @@
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
+
24
+ import { component, html, onMount, onUnmount } from "@c9up/aurora";
25
+ import type { Child } from "../lib/children.js";
26
+ import { cn } from "../lib/cn.js";
27
+ import { uid } from "../lib/id.js";
28
+ import { fadeInOut, zoomInOut } from "../lib/motion.js";
29
+ import { type Reactive, read } from "../lib/props.js";
30
+ import { controllable } from "../primitives/controllable.js";
31
+ import { modalSurface } from "../primitives/modalSurface.js";
32
+ import { Command, type CommandItem, type CommandProps } from "./Command.js";
33
+ import { dialogBackdropClasses } from "./Dialog.js";
34
+
35
+ export interface CommandDialogProps extends Omit<CommandProps, "class"> {
36
+ /** Announced on open. Hidden visually unless `showTitle`. */
37
+ title?: Child;
38
+ description?: Child;
39
+ showTitle?: boolean;
40
+ open?: Reactive<boolean>;
41
+ defaultOpen?: boolean;
42
+ onOpenChange?: (open: boolean) => void;
43
+ /**
44
+ * A single character that opens the palette with the platform's command
45
+ * modifier — `"k"` gives ⌘K on macOS and Ctrl+K elsewhere. Omit for a
46
+ * palette opened only from your own code.
47
+ */
48
+ shortcut?: string;
49
+ contentClass?: Reactive<string>;
50
+ }
51
+
52
+ export const CommandDialog = component<CommandDialogProps>((props) => {
53
+ const panelId = uid("command-dialog-panel");
54
+ const titleId = uid("command-dialog-title");
55
+ const descriptionId = uid("command-dialog-description");
56
+
57
+ const state = controllable<boolean>({
58
+ value: props.open,
59
+ initial: props.defaultOpen ?? false,
60
+ onChange: props.onOpenChange,
61
+ });
62
+
63
+ /**
64
+ * Toggle, not open — the same keystroke has to close it.
65
+ *
66
+ * Captured at the document, so a menu or popover that is already open does
67
+ * not consume the key on its way up. `metaKey || ctrlKey` covers both
68
+ * platforms without sniffing the user agent.
69
+ */
70
+ function onKeyDown(event: KeyboardEvent): void {
71
+ if (props.shortcut === undefined) return;
72
+ if (event.key.toLowerCase() !== props.shortcut.toLowerCase()) return;
73
+ if (!event.metaKey && !event.ctrlKey) return;
74
+ event.preventDefault();
75
+ state.set(!state.current());
76
+ }
77
+
78
+ onMount(() => {
79
+ if (props.shortcut === undefined) return;
80
+ document.addEventListener("keydown", onKeyDown, true);
81
+ });
82
+ onUnmount(() => document.removeEventListener("keydown", onKeyDown, true));
83
+
84
+ function close(): void {
85
+ state.set(false);
86
+ }
87
+
88
+ function runAndClose(item: CommandItem): void {
89
+ props.onSelect?.(item);
90
+ // Closing after the action, not before: a handler that opens another
91
+ // surface should not have this one tearing down focus underneath it.
92
+ close();
93
+ }
94
+
95
+ modalSurface({
96
+ open: () => state.current(),
97
+ onClose: close,
98
+ panel: (root) => root.querySelector(`#${CSS.escape(panelId)}`),
99
+ // The search field, not the first focusable — the whole point of the
100
+ // palette is that you can start typing immediately.
101
+ initialFocus: (panel) => panel.querySelector("input"),
102
+ content: () =>
103
+ html`<div
104
+ data-slot="command-dialog-overlay"
105
+ class="${cn("fixed inset-0 z-50", fadeInOut)}"
106
+ >
107
+ <div class="${dialogBackdropClasses}"></div>
108
+ <div
109
+ data-slot="command-dialog"
110
+ id="${panelId}"
111
+ role="dialog"
112
+ aria-modal="true"
113
+ aria-labelledby="${titleId}"
114
+ aria-describedby="${props.description === undefined ? undefined : descriptionId}"
115
+ tabindex="-1"
116
+ class="${cn(
117
+ "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",
118
+ zoomInOut,
119
+ read(props.contentClass),
120
+ )}"
121
+ >
122
+ <h2
123
+ id="${titleId}"
124
+ class="${cn(
125
+ "px-4 pt-4 text-sm font-medium",
126
+ props.showTitle === true ? "" : "sr-only",
127
+ )}"
128
+ >${props.title ?? "Command palette"}</h2>
129
+ ${
130
+ props.description === undefined
131
+ ? null
132
+ : html`<p id="${descriptionId}" class="sr-only">${props.description}</p>`
133
+ }
134
+ ${Command({
135
+ items: props.items,
136
+ placeholder: props.placeholder,
137
+ emptyMessage: props.emptyMessage,
138
+ filter: props.filter,
139
+ onSelect: runAndClose,
140
+ })}
141
+ </div>
142
+ </div>`,
143
+ });
144
+
145
+ // The palette has no trigger of its own — it is opened by the shortcut or
146
+ // by the `open` prop. Something still has to sit in the tree for the
147
+ // component to mount into; the surface itself lives in a portal.
148
+ return html`<span data-slot="command-dialog-root" hidden></span>`;
149
+ });
@@ -0,0 +1,125 @@
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
+
19
+ import { component, html, onUnmount, signal } from "@c9up/aurora";
20
+ import { type Slot, slot } from "../lib/children.js";
21
+ import { cn } from "../lib/cn.js";
22
+ import { uid } from "../lib/id.js";
23
+ import { type Reactive, read } from "../lib/props.js";
24
+ import { floatingSurface } from "../primitives/floatingSurface.js";
25
+ import { type MenuEntry, menuPanel, wireMenu } from "../primitives/menu.js";
26
+
27
+ const LONG_PRESS_MS = 500;
28
+
29
+ export interface ContextMenuProps {
30
+ /** The region that answers to right-click. */
31
+ children?: Slot;
32
+ entries: readonly MenuEntry[];
33
+ onOpenChange?: (open: boolean) => void;
34
+ class?: Reactive<string>;
35
+ contentClass?: Reactive<string>;
36
+ }
37
+
38
+ export const ContextMenu = component<ContextMenuProps>((props) => {
39
+ const regionId = uid("context-menu-region");
40
+ const anchorId = uid("context-menu-anchor");
41
+ const contentId = uid("context-menu-content");
42
+ const open = signal(false);
43
+
44
+ let unwire: (() => void) | null = null;
45
+ let pressTimer: ReturnType<typeof setTimeout> | undefined;
46
+
47
+ function setOpen(next: boolean): void {
48
+ open(next);
49
+ props.onOpenChange?.(next);
50
+ }
51
+
52
+ function moveAnchorTo(x: number, y: number): void {
53
+ const anchor = document.getElementById(anchorId);
54
+ if (anchor === null) return;
55
+ anchor.style.left = `${x}px`;
56
+ anchor.style.top = `${y}px`;
57
+ }
58
+
59
+ function openAt(x: number, y: number): void {
60
+ moveAnchorTo(x, y);
61
+ setOpen(true);
62
+ }
63
+
64
+ function onContextMenu(event: MouseEvent): void {
65
+ event.preventDefault();
66
+ openAt(event.clientX, event.clientY);
67
+ }
68
+
69
+ function onPointerDown(event: PointerEvent): void {
70
+ if (event.pointerType !== "touch") return;
71
+ cancelPress();
72
+ pressTimer = setTimeout(
73
+ () => openAt(event.clientX, event.clientY),
74
+ LONG_PRESS_MS,
75
+ );
76
+ }
77
+
78
+ function cancelPress(): void {
79
+ if (pressTimer !== undefined) clearTimeout(pressTimer);
80
+ pressTimer = undefined;
81
+ }
82
+
83
+ onUnmount(cancelPress);
84
+
85
+ floatingSurface({
86
+ anchor: () => document.getElementById(anchorId),
87
+ open: () => open(),
88
+ onClose: () => setOpen(false),
89
+ placement: "bottom-start",
90
+ offset: 2,
91
+ content: () =>
92
+ menuPanel({
93
+ id: contentId,
94
+ entries: props.entries,
95
+ onCloseAll: () => setOpen(false),
96
+ class: read(props.contentClass),
97
+ }),
98
+ onOpened: (panel) => {
99
+ unwire = wireMenu(panel, { onCloseAll: () => setOpen(false) });
100
+ panel.focus({ preventScroll: true });
101
+ },
102
+ onClosed: () => {
103
+ unwire?.();
104
+ unwire = null;
105
+ },
106
+ });
107
+
108
+ return html`<div
109
+ data-slot="context-menu"
110
+ id="${regionId}"
111
+ class="${() => cn(read(props.class))}"
112
+ @contextmenu="${onContextMenu}"
113
+ @pointerdown="${onPointerDown}"
114
+ @pointerup="${cancelPress}"
115
+ @pointermove="${cancelPress}"
116
+ @pointercancel="${cancelPress}"
117
+ >
118
+ ${slot(props.children)}
119
+ <span
120
+ id="${anchorId}"
121
+ aria-hidden="true"
122
+ class="pointer-events-none fixed size-0"
123
+ ></span>
124
+ </div>`;
125
+ });
@@ -0,0 +1,328 @@
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
+
23
+ import { html, signal, type TemplateResult } from "@c9up/aurora";
24
+ import { Checkbox } from "../atoms/Checkbox.js";
25
+ import { Input } from "../atoms/Input.js";
26
+ import type { Child } from "../lib/children.js";
27
+ import { cn } from "../lib/cn.js";
28
+ import {
29
+ ChevronDownIcon,
30
+ ChevronsUpDownIcon,
31
+ ChevronUpIcon,
32
+ } from "../lib/icons.js";
33
+ import { Pagination } from "../molecules/Pagination.js";
34
+ import {
35
+ Table,
36
+ TableBody,
37
+ TableCell,
38
+ TableHead,
39
+ TableHeader,
40
+ TableRow,
41
+ } from "../molecules/Table.js";
42
+
43
+ export interface Column<Row> {
44
+ key: string;
45
+ header: Child;
46
+ /** Renders the cell. Defaults to the row's property of the same key. */
47
+ cell?: (row: Row) => Child;
48
+ sortable?: boolean;
49
+ /** What to compare when sorting. Defaults to the rendered property. */
50
+ sortValue?: (row: Row) => string | number;
51
+ class?: string;
52
+ }
53
+
54
+ export interface DataTableProps<Row> {
55
+ columns: readonly Column<Row>[];
56
+ rows: readonly Row[];
57
+ /** Stable identity per row — used for selection and as the render key. */
58
+ rowKey: (row: Row) => string;
59
+ /** Rows per page. Omit to show everything. */
60
+ pageSize?: number;
61
+ /** Show a filter box. Provide `filterMatch` to say what it searches. */
62
+ filterPlaceholder?: string;
63
+ filterMatch?: (row: Row, query: string) => boolean;
64
+ /** Add a checkbox column. */
65
+ selectable?: boolean;
66
+ onSelectionChange?: (keys: readonly string[]) => void;
67
+ emptyMessage?: Child;
68
+ class?: string;
69
+ }
70
+
71
+ type SortDirection = "asc" | "desc";
72
+
73
+ export function DataTable<Row>(props: DataTableProps<Row>): TemplateResult {
74
+ const sortKey = signal<string | undefined>(undefined);
75
+ const sortDirection = signal<SortDirection>("asc");
76
+ const query = signal("");
77
+ const page = signal(1);
78
+ const selected = signal<readonly string[]>([]);
79
+
80
+ function readCell(row: Row, column: Column<Row>): Child {
81
+ if (column.cell !== undefined) return column.cell(row);
82
+ return propertyOf(row, column.key);
83
+ }
84
+
85
+ function sortableValue(row: Row, column: Column<Row>): string | number {
86
+ if (column.sortValue !== undefined) return column.sortValue(row);
87
+ const value = propertyOf(row, column.key);
88
+ return typeof value === "number" ? value : String(value ?? "");
89
+ }
90
+
91
+ function filtered(): readonly Row[] {
92
+ const needle = query().trim().toLowerCase();
93
+ if (needle === "" || props.filterMatch === undefined) return props.rows;
94
+ return props.rows.filter(
95
+ (row) => props.filterMatch?.(row, needle) === true,
96
+ );
97
+ }
98
+
99
+ function sorted(): readonly Row[] {
100
+ const key = sortKey();
101
+ const rows = filtered();
102
+ if (key === undefined) return rows;
103
+
104
+ const column = props.columns.find((entry) => entry.key === key);
105
+ if (column === undefined) return rows;
106
+
107
+ const direction = sortDirection() === "asc" ? 1 : -1;
108
+ // A copy: sorting the caller's array in place would reorder their data as
109
+ // a side effect of rendering it.
110
+ return [...rows].sort((left, right) => {
111
+ const a = sortableValue(left, column);
112
+ const b = sortableValue(right, column);
113
+ if (typeof a === "number" && typeof b === "number")
114
+ return (a - b) * direction;
115
+ return String(a).localeCompare(String(b)) * direction;
116
+ });
117
+ }
118
+
119
+ function pageCount(): number {
120
+ if (props.pageSize === undefined) return 1;
121
+ return Math.max(1, Math.ceil(sorted().length / props.pageSize));
122
+ }
123
+
124
+ function visible(): readonly Row[] {
125
+ const rows = sorted();
126
+ if (props.pageSize === undefined) return rows;
127
+ // Clamp: filtering down to fewer pages while on the last one would
128
+ // otherwise show an empty table with no way back.
129
+ const current = Math.min(page(), pageCount());
130
+ const from = (current - 1) * props.pageSize;
131
+ return rows.slice(from, from + props.pageSize);
132
+ }
133
+
134
+ function toggleSort(column: Column<Row>): void {
135
+ if (column.sortable === false) return;
136
+ if (sortKey() === column.key) {
137
+ sortDirection(sortDirection() === "asc" ? "desc" : "asc");
138
+ return;
139
+ }
140
+ sortKey(column.key);
141
+ sortDirection("asc");
142
+ }
143
+
144
+ function setSelection(keys: readonly string[]): void {
145
+ selected(keys);
146
+ props.onSelectionChange?.(keys);
147
+ }
148
+
149
+ function toggleRow(key: string): void {
150
+ const current = selected();
151
+ setSelection(
152
+ current.includes(key)
153
+ ? current.filter((entry) => entry !== key)
154
+ : [...current, key],
155
+ );
156
+ }
157
+
158
+ /**
159
+ * The header checkbox acts on the *visible* page, not the whole dataset.
160
+ *
161
+ * Selecting rows the user cannot see — and may have filtered away on
162
+ * purpose — is the behaviour behind every "I deleted more than I meant to"
163
+ * story about a bulk action.
164
+ */
165
+ function toggleAllVisible(): void {
166
+ const keys = visible().map(props.rowKey);
167
+ const allSelected = keys.every((key) => selected().includes(key));
168
+ if (allSelected) {
169
+ setSelection(selected().filter((key) => !keys.includes(key)));
170
+ } else {
171
+ const merged = new Set([...selected(), ...keys]);
172
+ setSelection([...merged]);
173
+ }
174
+ }
175
+
176
+ function ariaSortFor(
177
+ column: Column<Row>,
178
+ ): "ascending" | "descending" | "none" | undefined {
179
+ if (column.sortable === false) return undefined;
180
+ if (sortKey() !== column.key) return "none";
181
+ return sortDirection() === "asc" ? "ascending" : "descending";
182
+ }
183
+
184
+ function sortIcon(column: Column<Row>): Child {
185
+ if (column.sortable === false) return null;
186
+ if (sortKey() !== column.key) {
187
+ return ChevronsUpDownIcon({ class: "ml-2 size-3.5 opacity-50" });
188
+ }
189
+ return sortDirection() === "asc"
190
+ ? ChevronUpIcon({ class: "ml-2 size-3.5" })
191
+ : ChevronDownIcon({ class: "ml-2 size-3.5" });
192
+ }
193
+
194
+ const columnCount =
195
+ props.columns.length + (props.selectable === true ? 1 : 0);
196
+
197
+ return html`<div data-slot="data-table" class="${cn("flex flex-col gap-4", props.class)}">
198
+ ${
199
+ props.filterMatch === undefined
200
+ ? null
201
+ : Input({
202
+ type: "search",
203
+ placeholder: props.filterPlaceholder ?? "Filter…",
204
+ class: "max-w-sm",
205
+ onInput: (value) => {
206
+ query(value);
207
+ page(1);
208
+ },
209
+ })
210
+ }
211
+ ${Table({
212
+ children: [
213
+ TableHeader({
214
+ children: TableRow({
215
+ children: [
216
+ props.selectable === true
217
+ ? TableHead({
218
+ class: "w-10",
219
+ children: Checkbox({
220
+ label: "Select all rows on this page",
221
+ checked: () => {
222
+ const keys = visible().map(props.rowKey);
223
+ return (
224
+ keys.length > 0 &&
225
+ keys.every((key) => selected().includes(key))
226
+ );
227
+ },
228
+ indeterminate: () => {
229
+ const keys = visible().map(props.rowKey);
230
+ const chosen = keys.filter((key) =>
231
+ selected().includes(key),
232
+ ).length;
233
+ return chosen > 0 && chosen < keys.length;
234
+ },
235
+ onCheckedChange: toggleAllVisible,
236
+ }),
237
+ })
238
+ : null,
239
+ ...props.columns.map((column) =>
240
+ TableHead({
241
+ class: cn(
242
+ column.sortable === false
243
+ ? ""
244
+ : "cursor-pointer select-none",
245
+ column.class,
246
+ ),
247
+ sort: () => ariaSortFor(column),
248
+ onClick: () => toggleSort(column),
249
+ children: html`<span class="inline-flex items-center"
250
+ >${column.header}${() => sortIcon(column)}</span
251
+ >`,
252
+ }),
253
+ ),
254
+ ],
255
+ }),
256
+ }),
257
+ TableBody({
258
+ children: () => {
259
+ const rows = visible();
260
+ if (rows.length === 0) {
261
+ return TableRow({
262
+ children: TableCell({
263
+ colspan: columnCount,
264
+ class: "h-24 text-center",
265
+ children: props.emptyMessage ?? "No results.",
266
+ }),
267
+ });
268
+ }
269
+ return rows.map((row) => renderRow(row));
270
+ },
271
+ }),
272
+ ],
273
+ })}
274
+ ${
275
+ props.pageSize === undefined
276
+ ? null
277
+ : Pagination({
278
+ page: () => Math.min(page(), pageCount()),
279
+ pageCount,
280
+ onPageChange: (next) => page(next),
281
+ })
282
+ }
283
+ </div>`;
284
+
285
+ function renderRow(row: Row): Child {
286
+ const key = props.rowKey(row);
287
+ return TableRow({
288
+ selected: () => selected().includes(key),
289
+ children: [
290
+ props.selectable === true
291
+ ? TableCell({
292
+ children: Checkbox({
293
+ label: `Select row ${key}`,
294
+ checked: () => selected().includes(key),
295
+ onCheckedChange: () => toggleRow(key),
296
+ }),
297
+ })
298
+ : null,
299
+ ...props.columns.map((column) =>
300
+ TableCell({ class: column.class, children: readCell(row, column) }),
301
+ ),
302
+ ],
303
+ });
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Read a property by name from a row of unknown shape.
309
+ *
310
+ * `Row` is the caller's own type and may be an interface, which cannot be
311
+ * indexed by an arbitrary string. The guard turns the lookup into a checked
312
+ * read that yields `undefined` for a missing key — a blank cell — instead of
313
+ * an assertion that it exists.
314
+ */
315
+ function propertyOf(row: unknown, key: string): Child {
316
+ if (typeof row !== "object" || row === null) return null;
317
+ if (!(key in row)) return null;
318
+ const value = Reflect.get(row, key);
319
+ if (value === null || value === undefined) return null;
320
+ if (
321
+ typeof value === "string" ||
322
+ typeof value === "number" ||
323
+ typeof value === "boolean"
324
+ ) {
325
+ return value;
326
+ }
327
+ return String(value);
328
+ }