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