@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,257 @@
|
|
|
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
|
+
|
|
23
|
+
import { component, html } from "@c9up/aurora";
|
|
24
|
+
import { cn } from "../lib/cn.js";
|
|
25
|
+
import { uid } from "../lib/id.js";
|
|
26
|
+
import { type Reactive, read } from "../lib/props.js";
|
|
27
|
+
|
|
28
|
+
export interface ChartSeries {
|
|
29
|
+
key: string;
|
|
30
|
+
label: string;
|
|
31
|
+
/** Any CSS colour. Defaults to `--chart-1`…`--chart-5` by position. */
|
|
32
|
+
color?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** One row: a category plus one numeric value per series key. */
|
|
36
|
+
export type ChartDatum = Record<string, string | number>;
|
|
37
|
+
|
|
38
|
+
export interface ChartProps {
|
|
39
|
+
data: readonly ChartDatum[];
|
|
40
|
+
series: readonly ChartSeries[];
|
|
41
|
+
/** Key in each datum holding the category label. */
|
|
42
|
+
categoryKey: string;
|
|
43
|
+
type?: "line" | "area" | "bar";
|
|
44
|
+
height?: number;
|
|
45
|
+
/** Announced as the chart's name, and used as the data table's caption. */
|
|
46
|
+
label: string;
|
|
47
|
+
/** Draw horizontal gridlines. Default `true`. */
|
|
48
|
+
grid?: boolean;
|
|
49
|
+
class?: Reactive<string>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const VIEWBOX_WIDTH = 600;
|
|
53
|
+
const PADDING = { top: 8, right: 8, bottom: 24, left: 40 };
|
|
54
|
+
|
|
55
|
+
function colorFor(series: ChartSeries, index: number): string {
|
|
56
|
+
return series.color ?? `var(--chart-${(index % 5) + 1})`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function numberAt(datum: ChartDatum, key: string): number {
|
|
60
|
+
const value = datum[key];
|
|
61
|
+
return typeof value === "number" ? value : 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function categoryAt(datum: ChartDatum, key: string): string {
|
|
65
|
+
const value = datum[key];
|
|
66
|
+
return typeof value === "string" ? value : String(value ?? "");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The value range the Y axis covers.
|
|
71
|
+
*
|
|
72
|
+
* Anchored at zero whenever the data is non-negative. A bar chart whose axis
|
|
73
|
+
* starts at the minimum value exaggerates every difference — the classic
|
|
74
|
+
* misleading chart — and it is not a choice worth exposing as an option.
|
|
75
|
+
*/
|
|
76
|
+
export function valueRange(
|
|
77
|
+
data: readonly ChartDatum[],
|
|
78
|
+
series: readonly ChartSeries[],
|
|
79
|
+
): { min: number; max: number } {
|
|
80
|
+
let min = 0;
|
|
81
|
+
let max = 0;
|
|
82
|
+
for (const datum of data) {
|
|
83
|
+
for (const entry of series) {
|
|
84
|
+
const value = numberAt(datum, entry.key);
|
|
85
|
+
if (value < min) min = value;
|
|
86
|
+
if (value > max) max = value;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// A flat all-zero dataset would divide by zero when scaling.
|
|
90
|
+
if (min === max) return { min, max: max + 1 };
|
|
91
|
+
return { min, max };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const Chart = component<ChartProps>((props) => {
|
|
95
|
+
const titleId = uid("chart-title");
|
|
96
|
+
const height = props.height ?? 240;
|
|
97
|
+
const type = props.type ?? "line";
|
|
98
|
+
const { min, max } = valueRange(props.data, props.series);
|
|
99
|
+
|
|
100
|
+
const plotWidth = VIEWBOX_WIDTH - PADDING.left - PADDING.right;
|
|
101
|
+
const plotHeight = height - PADDING.top - PADDING.bottom;
|
|
102
|
+
|
|
103
|
+
const y = (value: number): number =>
|
|
104
|
+
PADDING.top + plotHeight - ((value - min) / (max - min)) * plotHeight;
|
|
105
|
+
|
|
106
|
+
/** Centre of the slot for index `i`, so points sit above their labels. */
|
|
107
|
+
const x = (index: number): number => {
|
|
108
|
+
const count = Math.max(props.data.length, 1);
|
|
109
|
+
const step = plotWidth / count;
|
|
110
|
+
return PADDING.left + step * index + step / 2;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const gridLines = [0, 0.25, 0.5, 0.75, 1].map(
|
|
114
|
+
(fraction) => PADDING.top + plotHeight * fraction,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
function linePath(series: ChartSeries): string {
|
|
118
|
+
return props.data
|
|
119
|
+
.map(
|
|
120
|
+
(datum, index) =>
|
|
121
|
+
`${index === 0 ? "M" : "L"}${x(index)},${y(numberAt(datum, series.key))}`,
|
|
122
|
+
)
|
|
123
|
+
.join(" ");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function areaPath(series: ChartSeries): string {
|
|
127
|
+
if (props.data.length === 0) return "";
|
|
128
|
+
const baseline = y(Math.max(min, 0));
|
|
129
|
+
return `${linePath(series)} L${x(props.data.length - 1)},${baseline} L${x(0)},${baseline} Z`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return html`<figure
|
|
133
|
+
data-slot="chart"
|
|
134
|
+
class="${() => cn("w-full", read(props.class))}"
|
|
135
|
+
>
|
|
136
|
+
<svg
|
|
137
|
+
viewBox="${`0 0 ${VIEWBOX_WIDTH} ${height}`}"
|
|
138
|
+
role="img"
|
|
139
|
+
aria-labelledby="${titleId}"
|
|
140
|
+
preserveAspectRatio="none"
|
|
141
|
+
class="h-auto w-full overflow-visible"
|
|
142
|
+
>
|
|
143
|
+
<title id="${titleId}">${props.label}</title>
|
|
144
|
+
${
|
|
145
|
+
props.grid === false
|
|
146
|
+
? null
|
|
147
|
+
: gridLines.map(
|
|
148
|
+
(position) => html`<line
|
|
149
|
+
x1="${PADDING.left}"
|
|
150
|
+
x2="${VIEWBOX_WIDTH - PADDING.right}"
|
|
151
|
+
y1="${position}"
|
|
152
|
+
y2="${position}"
|
|
153
|
+
stroke="var(--border)"
|
|
154
|
+
stroke-width="1"
|
|
155
|
+
/>`,
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
${props.series.map((series, seriesIndex) =>
|
|
159
|
+
type === "bar"
|
|
160
|
+
? renderBars(series, seriesIndex)
|
|
161
|
+
: renderCurve(series, seriesIndex),
|
|
162
|
+
)}
|
|
163
|
+
${props.data.map(
|
|
164
|
+
(datum, index) => html`<text
|
|
165
|
+
x="${x(index)}"
|
|
166
|
+
y="${height - 6}"
|
|
167
|
+
text-anchor="middle"
|
|
168
|
+
font-size="10"
|
|
169
|
+
fill="var(--muted-foreground)"
|
|
170
|
+
>${categoryAt(datum, props.categoryKey)}</text>`,
|
|
171
|
+
)}
|
|
172
|
+
</svg>
|
|
173
|
+
<figcaption class="sr-only">${renderDataTable()}</figcaption>
|
|
174
|
+
<div data-slot="chart-legend" class="mt-2 flex flex-wrap justify-center gap-4 text-xs">
|
|
175
|
+
${props.series.map(
|
|
176
|
+
(series, index) => html`<span class="flex items-center gap-1.5">
|
|
177
|
+
<span
|
|
178
|
+
aria-hidden="true"
|
|
179
|
+
class="size-2 shrink-0 rounded-[2px]"
|
|
180
|
+
style="${`background:${colorFor(series, index)}`}"
|
|
181
|
+
></span>
|
|
182
|
+
<span class="text-muted-foreground">${series.label}</span>
|
|
183
|
+
</span>`,
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
</figure>`;
|
|
187
|
+
|
|
188
|
+
function renderCurve(
|
|
189
|
+
series: ChartSeries,
|
|
190
|
+
index: number,
|
|
191
|
+
): ReturnType<typeof html> {
|
|
192
|
+
const color = colorFor(series, index);
|
|
193
|
+
return html`<g>
|
|
194
|
+
${
|
|
195
|
+
type === "area"
|
|
196
|
+
? html`<path d="${areaPath(series)}" fill="${color}" fill-opacity="0.15" />`
|
|
197
|
+
: null
|
|
198
|
+
}
|
|
199
|
+
<path
|
|
200
|
+
d="${linePath(series)}"
|
|
201
|
+
fill="none"
|
|
202
|
+
stroke="${color}"
|
|
203
|
+
stroke-width="2"
|
|
204
|
+
stroke-linecap="round"
|
|
205
|
+
stroke-linejoin="round"
|
|
206
|
+
/>
|
|
207
|
+
</g>`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function renderBars(
|
|
211
|
+
series: ChartSeries,
|
|
212
|
+
seriesIndex: number,
|
|
213
|
+
): ReturnType<typeof html> {
|
|
214
|
+
const count = Math.max(props.data.length, 1);
|
|
215
|
+
const slot = plotWidth / count;
|
|
216
|
+
// Share each slot between the series so grouped bars sit side by side,
|
|
217
|
+
// with a quarter of the slot left as the gap between groups.
|
|
218
|
+
const barWidth = (slot * 0.75) / props.series.length;
|
|
219
|
+
const baseline = y(Math.max(min, 0));
|
|
220
|
+
|
|
221
|
+
return html`<g fill="${colorFor(series, seriesIndex)}">
|
|
222
|
+
${props.data.map((datum, index) => {
|
|
223
|
+
const top = y(numberAt(datum, series.key));
|
|
224
|
+
const left =
|
|
225
|
+
PADDING.left + slot * index + slot * 0.125 + barWidth * seriesIndex;
|
|
226
|
+
return html`<rect
|
|
227
|
+
x="${left}"
|
|
228
|
+
y="${Math.min(top, baseline)}"
|
|
229
|
+
width="${barWidth}"
|
|
230
|
+
height="${Math.abs(baseline - top)}"
|
|
231
|
+
rx="2"
|
|
232
|
+
/>`;
|
|
233
|
+
})}
|
|
234
|
+
</g>`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The same numbers as a real table, for anything that cannot see the SVG. */
|
|
238
|
+
function renderDataTable(): ReturnType<typeof html> {
|
|
239
|
+
return html`<table>
|
|
240
|
+
<caption>${props.label}</caption>
|
|
241
|
+
<thead>
|
|
242
|
+
<tr>
|
|
243
|
+
<th scope="col">${props.categoryKey}</th>
|
|
244
|
+
${props.series.map((series) => html`<th scope="col">${series.label}</th>`)}
|
|
245
|
+
</tr>
|
|
246
|
+
</thead>
|
|
247
|
+
<tbody>
|
|
248
|
+
${props.data.map(
|
|
249
|
+
(datum) => html`<tr>
|
|
250
|
+
<th scope="row">${categoryAt(datum, props.categoryKey)}</th>
|
|
251
|
+
${props.series.map((series) => html`<td>${numberAt(datum, series.key)}</td>`)}
|
|
252
|
+
</tr>`,
|
|
253
|
+
)}
|
|
254
|
+
</tbody>
|
|
255
|
+
</table>`;
|
|
256
|
+
}
|
|
257
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
|
|
18
|
+
import { component, html, signal } from "@c9up/aurora";
|
|
19
|
+
import { cn } from "../lib/cn.js";
|
|
20
|
+
import { ChevronsUpDownIcon } from "../lib/icons.js";
|
|
21
|
+
import { uid } from "../lib/id.js";
|
|
22
|
+
import { zoomInOut } from "../lib/motion.js";
|
|
23
|
+
import { type Reactive, read, readOr } from "../lib/props.js";
|
|
24
|
+
import { controllable } from "../primitives/controllable.js";
|
|
25
|
+
import { floatingSurface } from "../primitives/floatingSurface.js";
|
|
26
|
+
import { focusSilently } from "../primitives/focusable.js";
|
|
27
|
+
import { Command, type CommandItem } from "./Command.js";
|
|
28
|
+
import { selectTriggerClasses } from "./Select.js";
|
|
29
|
+
|
|
30
|
+
export interface ComboboxOption {
|
|
31
|
+
value: string;
|
|
32
|
+
label: string;
|
|
33
|
+
keywords?: readonly string[];
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
group?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ComboboxProps {
|
|
39
|
+
options: readonly ComboboxOption[];
|
|
40
|
+
name?: string;
|
|
41
|
+
value?: Reactive<string | undefined>;
|
|
42
|
+
defaultValue?: string;
|
|
43
|
+
/** Trigger text when nothing is chosen. */
|
|
44
|
+
placeholder?: string;
|
|
45
|
+
/** Placeholder inside the search field. */
|
|
46
|
+
searchPlaceholder?: string;
|
|
47
|
+
emptyMessage?: string;
|
|
48
|
+
disabled?: Reactive<boolean>;
|
|
49
|
+
invalid?: Reactive<boolean>;
|
|
50
|
+
class?: Reactive<string>;
|
|
51
|
+
contentClass?: Reactive<string>;
|
|
52
|
+
onValueChange?: (value: string) => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const Combobox = component<ComboboxProps>((props) => {
|
|
56
|
+
const triggerId = uid("combobox-trigger");
|
|
57
|
+
const contentId = uid("combobox-content");
|
|
58
|
+
const open = signal(false);
|
|
59
|
+
|
|
60
|
+
const selection = controllable<string | undefined>({
|
|
61
|
+
value: props.value,
|
|
62
|
+
initial: props.defaultValue,
|
|
63
|
+
onChange: (next) => {
|
|
64
|
+
if (next !== undefined) props.onValueChange?.(next);
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
function labelFor(value: string | undefined): string | undefined {
|
|
69
|
+
return props.options.find((option) => option.value === value)?.label;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const items = (): readonly CommandItem[] =>
|
|
73
|
+
props.options.map((option) => ({
|
|
74
|
+
value: option.value,
|
|
75
|
+
label: option.label,
|
|
76
|
+
keywords: option.keywords,
|
|
77
|
+
disabled: option.disabled,
|
|
78
|
+
group: option.group,
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
function choose(item: CommandItem): void {
|
|
82
|
+
selection.set(item.value);
|
|
83
|
+
open(false);
|
|
84
|
+
focusSilently(document.getElementById(triggerId));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
floatingSurface({
|
|
88
|
+
anchor: () => document.getElementById(triggerId),
|
|
89
|
+
open: () => open(),
|
|
90
|
+
onClose: () => open(false),
|
|
91
|
+
placement: "bottom-start",
|
|
92
|
+
offset: 4,
|
|
93
|
+
matchWidth: true,
|
|
94
|
+
trapFocus: true,
|
|
95
|
+
// Focus the search field, not the first result — the user opened this to
|
|
96
|
+
// type. `firstFocusable` would land on whichever element comes first in
|
|
97
|
+
// the DOM, which is only the input by accident.
|
|
98
|
+
initialFocus: (content) => content.querySelector("input"),
|
|
99
|
+
content: () =>
|
|
100
|
+
html`<div
|
|
101
|
+
data-slot="combobox-content"
|
|
102
|
+
id="${contentId}"
|
|
103
|
+
class="${cn(
|
|
104
|
+
"bg-popover text-popover-foreground z-50 min-w-(--nebula-anchor-width) overflow-hidden rounded-md border shadow-md",
|
|
105
|
+
zoomInOut,
|
|
106
|
+
read(props.contentClass),
|
|
107
|
+
)}"
|
|
108
|
+
>
|
|
109
|
+
${Command({
|
|
110
|
+
items: items(),
|
|
111
|
+
placeholder: props.searchPlaceholder ?? "Search…",
|
|
112
|
+
emptyMessage: props.emptyMessage ?? "No results found.",
|
|
113
|
+
onSelect: choose,
|
|
114
|
+
})}
|
|
115
|
+
</div>`,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return html`<div data-slot="combobox" class="${() => cn("inline-flex", read(props.class))}">
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
data-slot="combobox-trigger"
|
|
122
|
+
id="${triggerId}"
|
|
123
|
+
role="combobox"
|
|
124
|
+
aria-haspopup="listbox"
|
|
125
|
+
aria-expanded="${() => (open() ? "true" : "false")}"
|
|
126
|
+
aria-controls="${() => (open() ? contentId : undefined)}"
|
|
127
|
+
aria-invalid="${() => (read(props.invalid) === true ? "true" : undefined)}"
|
|
128
|
+
data-placeholder="${() => (selection.current() === undefined ? "" : undefined)}"
|
|
129
|
+
?disabled="${() => readOr(props.disabled, false)}"
|
|
130
|
+
class="${() => cn(selectTriggerClasses, "justify-between")}"
|
|
131
|
+
@click="${() => open(!open())}"
|
|
132
|
+
>
|
|
133
|
+
<span class="truncate"
|
|
134
|
+
>${() => labelFor(selection.current()) ?? props.placeholder ?? "Select…"}</span
|
|
135
|
+
>
|
|
136
|
+
${ChevronsUpDownIcon({ class: "size-4 opacity-50" })}
|
|
137
|
+
</button>
|
|
138
|
+
<input type="hidden" name="${props.name}" .value="${() => selection.current() ?? ""}" />
|
|
139
|
+
</div>`;
|
|
140
|
+
});
|
|
@@ -0,0 +1,211 @@
|
|
|
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
|
+
|
|
21
|
+
import { component, html, signal } from "@c9up/aurora";
|
|
22
|
+
import type { Child } from "../lib/children.js";
|
|
23
|
+
import { cn } from "../lib/cn.js";
|
|
24
|
+
import { SearchIcon } from "../lib/icons.js";
|
|
25
|
+
import { uid } from "../lib/id.js";
|
|
26
|
+
import { type Reactive, read } from "../lib/props.js";
|
|
27
|
+
|
|
28
|
+
export interface CommandItem {
|
|
29
|
+
/** Stable key. */
|
|
30
|
+
value: string;
|
|
31
|
+
label: string;
|
|
32
|
+
icon?: Child;
|
|
33
|
+
/** Extra terms that should match this item. */
|
|
34
|
+
keywords?: readonly string[];
|
|
35
|
+
shortcut?: string;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
group?: string;
|
|
38
|
+
onSelect?: () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface CommandProps {
|
|
42
|
+
items: readonly CommandItem[];
|
|
43
|
+
placeholder?: string;
|
|
44
|
+
/** Shown when the filter matches nothing. */
|
|
45
|
+
emptyMessage?: Child;
|
|
46
|
+
/** Replace the built-in substring filter. */
|
|
47
|
+
filter?: (item: CommandItem, query: string) => boolean;
|
|
48
|
+
class?: Reactive<string>;
|
|
49
|
+
onSelect?: (item: CommandItem) => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Substring over the label, then over each keyword. */
|
|
53
|
+
export function defaultFilter(item: CommandItem, query: string): boolean {
|
|
54
|
+
if (query === "") return true;
|
|
55
|
+
const needle = query.toLowerCase();
|
|
56
|
+
if (item.label.toLowerCase().includes(needle)) return true;
|
|
57
|
+
return (item.keywords ?? []).some((keyword) =>
|
|
58
|
+
keyword.toLowerCase().includes(needle),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const Command = component<CommandProps>((props) => {
|
|
63
|
+
const inputId = uid("command-input");
|
|
64
|
+
const listId = uid("command-list");
|
|
65
|
+
const itemIds = new Map<string, string>();
|
|
66
|
+
for (const item of props.items) itemIds.set(item.value, uid("command-item"));
|
|
67
|
+
|
|
68
|
+
const query = signal("");
|
|
69
|
+
const active = signal<string | undefined>(undefined);
|
|
70
|
+
|
|
71
|
+
function matches(): readonly CommandItem[] {
|
|
72
|
+
const filter = props.filter ?? defaultFilter;
|
|
73
|
+
return props.items.filter(
|
|
74
|
+
(item) => item.disabled !== true && filter(item, query()),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function run(item: CommandItem): void {
|
|
79
|
+
if (item.disabled === true) return;
|
|
80
|
+
item.onSelect?.();
|
|
81
|
+
props.onSelect?.(item);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function move(delta: number): void {
|
|
85
|
+
const list = matches();
|
|
86
|
+
if (list.length === 0) return;
|
|
87
|
+
const from = list.findIndex((item) => item.value === active());
|
|
88
|
+
const next = from === -1 ? (delta > 0 ? 0 : list.length - 1) : from + delta;
|
|
89
|
+
// Wrapping is right here, unlike in a menu: a palette is a search result
|
|
90
|
+
// list and the user is scanning it, not navigating a fixed structure.
|
|
91
|
+
const target = list[(next + list.length) % list.length];
|
|
92
|
+
if (target !== undefined) {
|
|
93
|
+
active(target.value);
|
|
94
|
+
scrollIntoView(target.value);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function scrollIntoView(value: string): void {
|
|
99
|
+
const id = itemIds.get(value);
|
|
100
|
+
if (id === undefined) return;
|
|
101
|
+
document.getElementById(id)?.scrollIntoView({ block: "nearest" });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function onKeyDown(event: KeyboardEvent): void {
|
|
105
|
+
if (event.key === "ArrowDown") {
|
|
106
|
+
event.preventDefault();
|
|
107
|
+
move(1);
|
|
108
|
+
} else if (event.key === "ArrowUp") {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
move(-1);
|
|
111
|
+
} else if (event.key === "Enter") {
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
const current = matches().find((item) => item.value === active());
|
|
114
|
+
if (current !== undefined) run(current);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function onInput(event: Event): void {
|
|
119
|
+
const target = event.target;
|
|
120
|
+
if (!(target instanceof HTMLInputElement)) return;
|
|
121
|
+
query(target.value);
|
|
122
|
+
// Re-point the highlight at the first result: leaving it on an item the
|
|
123
|
+
// new query filtered out means Enter runs something invisible.
|
|
124
|
+
active(matches()[0]?.value);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return html`<div
|
|
128
|
+
data-slot="command"
|
|
129
|
+
class="${() =>
|
|
130
|
+
cn(
|
|
131
|
+
"bg-popover text-popover-foreground flex size-full flex-col overflow-hidden rounded-md",
|
|
132
|
+
read(props.class),
|
|
133
|
+
)}"
|
|
134
|
+
>
|
|
135
|
+
<div data-slot="command-input-wrapper" class="flex h-9 items-center gap-2 border-b px-3">
|
|
136
|
+
${SearchIcon({ class: "size-4 shrink-0 opacity-50" })}
|
|
137
|
+
<input
|
|
138
|
+
data-slot="command-input"
|
|
139
|
+
id="${inputId}"
|
|
140
|
+
type="text"
|
|
141
|
+
role="combobox"
|
|
142
|
+
autocomplete="off"
|
|
143
|
+
aria-expanded="true"
|
|
144
|
+
aria-controls="${listId}"
|
|
145
|
+
aria-activedescendant="${() => {
|
|
146
|
+
const current = active();
|
|
147
|
+
return current === undefined ? undefined : itemIds.get(current);
|
|
148
|
+
}}"
|
|
149
|
+
placeholder="${props.placeholder ?? "Type a command or search…"}"
|
|
150
|
+
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"
|
|
151
|
+
@input="${onInput}"
|
|
152
|
+
@keydown="${onKeyDown}"
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
<div
|
|
156
|
+
data-slot="command-list"
|
|
157
|
+
id="${listId}"
|
|
158
|
+
role="listbox"
|
|
159
|
+
class="max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto p-1"
|
|
160
|
+
>
|
|
161
|
+
${() => renderResults()}
|
|
162
|
+
</div>
|
|
163
|
+
</div>`;
|
|
164
|
+
|
|
165
|
+
function renderResults(): Child {
|
|
166
|
+
const list = matches();
|
|
167
|
+
if (list.length === 0) {
|
|
168
|
+
return html`<div
|
|
169
|
+
data-slot="command-empty"
|
|
170
|
+
role="presentation"
|
|
171
|
+
class="py-6 text-center text-sm"
|
|
172
|
+
>${props.emptyMessage ?? "No results found."}</div>`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let lastGroup: string | undefined;
|
|
176
|
+
return list.map((item) => {
|
|
177
|
+
const heading =
|
|
178
|
+
item.group !== undefined && item.group !== lastGroup
|
|
179
|
+
? html`<div
|
|
180
|
+
role="presentation"
|
|
181
|
+
class="text-muted-foreground px-2 py-1.5 text-xs font-medium"
|
|
182
|
+
>${item.group}</div>`
|
|
183
|
+
: null;
|
|
184
|
+
lastGroup = item.group;
|
|
185
|
+
return [heading, renderItem(item)];
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function renderItem(item: CommandItem): Child {
|
|
190
|
+
return html`<div
|
|
191
|
+
role="option"
|
|
192
|
+
data-slot="command-item"
|
|
193
|
+
id="${itemIds.get(item.value)}"
|
|
194
|
+
aria-selected="${() => (active() === item.value ? "true" : "false")}"
|
|
195
|
+
data-active="${() => (active() === item.value ? "" : undefined)}"
|
|
196
|
+
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"
|
|
197
|
+
@click="${() => run(item)}"
|
|
198
|
+
@pointerenter="${() => active(item.value)}"
|
|
199
|
+
>
|
|
200
|
+
${item.icon}
|
|
201
|
+
<span class="flex-1 truncate">${item.label}</span>
|
|
202
|
+
${
|
|
203
|
+
item.shortcut === undefined
|
|
204
|
+
? null
|
|
205
|
+
: html`<span class="text-muted-foreground ml-auto text-xs tracking-widest"
|
|
206
|
+
>${item.shortcut}</span
|
|
207
|
+
>`
|
|
208
|
+
}
|
|
209
|
+
</div>`;
|
|
210
|
+
}
|
|
211
|
+
});
|