@blurrow/ui 0.2.0 → 0.2.1
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/README.md +48 -36
- package/dist/Accordion.d.ts +17 -0
- package/dist/Accordion.js +138 -0
- package/dist/Accordion.js.map +1 -0
- package/dist/Alert.d.ts +13 -0
- package/dist/Alert.js +69 -0
- package/dist/Alert.js.map +1 -0
- package/dist/AuthorCard.d.ts +12 -0
- package/dist/AuthorCard.js +57 -0
- package/dist/AuthorCard.js.map +1 -0
- package/dist/Avatar.d.ts +21 -0
- package/dist/Avatar.js +96 -0
- package/dist/Avatar.js.map +1 -0
- package/dist/Badge.d.ts +12 -0
- package/dist/Badge.js +46 -0
- package/dist/Badge.js.map +1 -0
- package/dist/Banner.d.ts +15 -0
- package/dist/Banner.js +68 -0
- package/dist/Banner.js.map +1 -0
- package/dist/Breadcrumb.d.ts +13 -0
- package/dist/Breadcrumb.js +45 -0
- package/dist/Breadcrumb.js.map +1 -0
- package/dist/Button.d.ts +19 -0
- package/dist/Button.js +282 -0
- package/dist/Button.js.map +1 -0
- package/dist/ButtonGroup.d.ts +19 -0
- package/dist/ButtonGroup.js +62 -0
- package/dist/ButtonGroup.js.map +1 -0
- package/dist/Card.d.ts +40 -0
- package/dist/Card.js +175 -0
- package/dist/Card.js.map +1 -0
- package/dist/CardShell.d.ts +18 -0
- package/dist/CardShell.js +58 -0
- package/dist/CardShell.js.map +1 -0
- package/dist/Carousel.d.ts +22 -0
- package/dist/Carousel.js +247 -0
- package/dist/Carousel.js.map +1 -0
- package/dist/Checkbox.d.ts +15 -0
- package/dist/Checkbox.js +84 -0
- package/dist/Checkbox.js.map +1 -0
- package/dist/CloseButton.d.ts +12 -0
- package/dist/CloseButton.js +63 -0
- package/dist/CloseButton.js.map +1 -0
- package/dist/CodeBlock.d.ts +12 -0
- package/dist/CodeBlock.js +221 -0
- package/dist/CodeBlock.js.map +1 -0
- package/dist/ColorPicker.d.ts +24 -0
- package/dist/ColorPicker.js +366 -0
- package/dist/ColorPicker.js.map +1 -0
- package/dist/Combobox.d.ts +34 -0
- package/dist/Combobox.js +255 -0
- package/dist/Combobox.js.map +1 -0
- package/dist/CommandPalette.d.ts +25 -0
- package/dist/CommandPalette.js +304 -0
- package/dist/CommandPalette.js.map +1 -0
- package/dist/ContextMenu.d.ts +16 -0
- package/dist/ContextMenu.js +209 -0
- package/dist/ContextMenu.js.map +1 -0
- package/dist/DataList.d.ts +15 -0
- package/dist/DataList.js +42 -0
- package/dist/DataList.js.map +1 -0
- package/dist/DatePicker.d.ts +65 -0
- package/dist/DatePicker.js +391 -0
- package/dist/DatePicker.js.map +1 -0
- package/dist/DateRangePicker.d.ts +34 -0
- package/dist/DateRangePicker.js +180 -0
- package/dist/DateRangePicker.js.map +1 -0
- package/dist/Drawer.d.ts +17 -0
- package/dist/Drawer.js +132 -0
- package/dist/Drawer.js.map +1 -0
- package/dist/DropdownMenu.d.ts +23 -0
- package/dist/DropdownMenu.js +185 -0
- package/dist/DropdownMenu.js.map +1 -0
- package/dist/EmptyState.d.ts +13 -0
- package/dist/EmptyState.js +55 -0
- package/dist/EmptyState.js.map +1 -0
- package/dist/FieldMessage.d.ts +26 -0
- package/dist/FieldMessage.js +95 -0
- package/dist/FieldMessage.js.map +1 -0
- package/dist/FileUpload.d.ts +32 -0
- package/dist/FileUpload.js +253 -0
- package/dist/FileUpload.js.map +1 -0
- package/dist/Floating.d.ts +32 -0
- package/dist/Floating.js +95 -0
- package/dist/Floating.js.map +1 -0
- package/dist/HoverCard.d.ts +18 -0
- package/dist/HoverCard.js +90 -0
- package/dist/HoverCard.js.map +1 -0
- package/dist/Input.d.ts +17 -0
- package/dist/Input.js +96 -0
- package/dist/Input.js.map +1 -0
- package/dist/Kbd.d.ts +9 -0
- package/dist/Kbd.js +24 -0
- package/dist/Kbd.js.map +1 -0
- package/dist/Modal.d.ts +15 -0
- package/dist/Modal.js +109 -0
- package/dist/Modal.js.map +1 -0
- package/dist/NumberInput.d.ts +23 -0
- package/dist/NumberInput.js +162 -0
- package/dist/NumberInput.js.map +1 -0
- package/dist/OtpInput.d.ts +26 -0
- package/dist/OtpInput.js +173 -0
- package/dist/OtpInput.js.map +1 -0
- package/dist/Pagination.d.ts +11 -0
- package/dist/Pagination.js +148 -0
- package/dist/Pagination.js.map +1 -0
- package/dist/Popover.d.ts +13 -0
- package/dist/Popover.js +98 -0
- package/dist/Popover.js.map +1 -0
- package/dist/Progress.d.ts +12 -0
- package/dist/Progress.js +61 -0
- package/dist/Progress.js.map +1 -0
- package/dist/QuoteCard.d.ts +13 -0
- package/dist/QuoteCard.js +92 -0
- package/dist/QuoteCard.js.map +1 -0
- package/dist/Radio.d.ts +19 -0
- package/dist/Radio.js +161 -0
- package/dist/Radio.js.map +1 -0
- package/dist/RadioCards.d.ts +27 -0
- package/dist/RadioCards.js +205 -0
- package/dist/RadioCards.js.map +1 -0
- package/dist/ReactionBar.d.ts +15 -0
- package/dist/ReactionBar.js +92 -0
- package/dist/ReactionBar.js.map +1 -0
- package/dist/ReadingProgress.d.ts +8 -0
- package/dist/ReadingProgress.js +51 -0
- package/dist/ReadingProgress.js.map +1 -0
- package/dist/ReadingTime.d.ts +9 -0
- package/dist/ReadingTime.js +39 -0
- package/dist/ReadingTime.js.map +1 -0
- package/dist/Select.d.ts +20 -0
- package/dist/Select.js +244 -0
- package/dist/Select.js.map +1 -0
- package/dist/Separator.d.ts +14 -0
- package/dist/Separator.js +33 -0
- package/dist/Separator.js.map +1 -0
- package/dist/ShareButtons.d.ts +10 -0
- package/dist/ShareButtons.js +107 -0
- package/dist/ShareButtons.js.map +1 -0
- package/dist/Sidebar.d.ts +30 -0
- package/dist/Sidebar.js +140 -0
- package/dist/Sidebar.js.map +1 -0
- package/dist/Skeleton.d.ts +15 -0
- package/dist/Skeleton.js +67 -0
- package/dist/Skeleton.js.map +1 -0
- package/dist/Slider.d.ts +27 -0
- package/dist/Slider.js +182 -0
- package/dist/Slider.js.map +1 -0
- package/dist/Spinner.d.ts +12 -0
- package/dist/Spinner.js +24 -0
- package/dist/Spinner.js.map +1 -0
- package/dist/SpotlightCard.d.ts +17 -0
- package/dist/SpotlightCard.js +87 -0
- package/dist/SpotlightCard.js.map +1 -0
- package/dist/Stat.d.ts +19 -0
- package/dist/Stat.js +71 -0
- package/dist/Stat.js.map +1 -0
- package/dist/StatCard.d.ts +24 -0
- package/dist/StatCard.js +212 -0
- package/dist/StatCard.js.map +1 -0
- package/dist/Steps.d.ts +14 -0
- package/dist/Steps.js +94 -0
- package/dist/Steps.js.map +1 -0
- package/dist/Table.d.ts +21 -0
- package/dist/Table.js +93 -0
- package/dist/Table.js.map +1 -0
- package/dist/TableOfContents.d.ts +17 -0
- package/dist/TableOfContents.js +92 -0
- package/dist/TableOfContents.js.map +1 -0
- package/dist/Tabs.d.ts +17 -0
- package/dist/Tabs.js +123 -0
- package/dist/Tabs.js.map +1 -0
- package/dist/TagInput.d.ts +16 -0
- package/dist/TagInput.js +169 -0
- package/dist/TagInput.js.map +1 -0
- package/dist/Textarea.d.ts +13 -0
- package/dist/Textarea.js +74 -0
- package/dist/Textarea.js.map +1 -0
- package/dist/TimePicker.d.ts +28 -0
- package/dist/TimePicker.js +264 -0
- package/dist/TimePicker.js.map +1 -0
- package/dist/Timeline.d.ts +19 -0
- package/dist/Timeline.js +76 -0
- package/dist/Timeline.js.map +1 -0
- package/dist/Toast.d.ts +18 -0
- package/dist/Toast.js +318 -0
- package/dist/Toast.js.map +1 -0
- package/dist/Toggle.d.ts +13 -0
- package/dist/Toggle.js +84 -0
- package/dist/Toggle.js.map +1 -0
- package/dist/Tooltip.d.ts +13 -0
- package/dist/Tooltip.js +138 -0
- package/dist/Tooltip.js.map +1 -0
- package/dist/cli.js +172 -5
- package/dist/index.d.ts +68 -631
- package/dist/index.js +67 -5558
- package/dist/index.js.map +1 -1
- package/dist/registry/Accordion.tsx +6 -6
- package/dist/registry/Alert.tsx +13 -10
- package/dist/registry/AuthorCard.tsx +7 -7
- package/dist/registry/Avatar.tsx +5 -5
- package/dist/registry/Badge.tsx +12 -15
- package/dist/registry/Banner.tsx +15 -18
- package/dist/registry/Breadcrumb.tsx +3 -3
- package/dist/registry/Button.tsx +58 -69
- package/dist/registry/ButtonGroup.tsx +9 -9
- package/dist/registry/Card.tsx +15 -15
- package/dist/registry/CardShell.tsx +6 -6
- package/dist/registry/Carousel.tsx +19 -15
- package/dist/registry/Checkbox.tsx +14 -9
- package/dist/registry/CloseButton.tsx +2 -2
- package/dist/registry/CodeBlock.tsx +15 -15
- package/dist/registry/ColorPicker.tsx +404 -0
- package/dist/registry/Combobox.tsx +318 -0
- package/dist/registry/CommandPalette.tsx +31 -24
- package/dist/registry/ContextMenu.tsx +241 -0
- package/dist/registry/DataList.tsx +4 -4
- package/dist/registry/DatePicker.tsx +494 -0
- package/dist/registry/DateRangePicker.tsx +232 -0
- package/dist/registry/Drawer.tsx +17 -15
- package/dist/registry/DropdownMenu.tsx +39 -77
- package/dist/registry/EmptyState.tsx +8 -8
- package/dist/registry/FieldMessage.tsx +123 -0
- package/dist/registry/FileUpload.tsx +318 -0
- package/dist/registry/Floating.tsx +134 -0
- package/dist/registry/HoverCard.tsx +113 -0
- package/dist/registry/Input.tsx +54 -43
- package/dist/registry/Kbd.tsx +4 -5
- package/dist/registry/Modal.tsx +17 -15
- package/dist/registry/NumberInput.tsx +183 -0
- package/dist/registry/OtpInput.tsx +219 -0
- package/dist/registry/Pagination.tsx +15 -18
- package/dist/registry/Popover.tsx +40 -45
- package/dist/registry/Progress.tsx +4 -4
- package/dist/registry/QuoteCard.tsx +7 -7
- package/dist/registry/Radio.tsx +24 -21
- package/dist/registry/RadioCards.tsx +247 -0
- package/dist/registry/ReactionBar.tsx +5 -5
- package/dist/registry/ReadingProgress.tsx +2 -2
- package/dist/registry/ReadingTime.tsx +2 -2
- package/dist/registry/Select.tsx +28 -53
- package/dist/registry/Separator.tsx +52 -0
- package/dist/registry/ShareButtons.tsx +2 -2
- package/dist/registry/Sidebar.tsx +18 -18
- package/dist/registry/Skeleton.tsx +1 -1
- package/dist/registry/Slider.tsx +229 -0
- package/dist/registry/Spinner.tsx +32 -0
- package/dist/registry/SpotlightCard.tsx +6 -6
- package/dist/registry/Stat.tsx +4 -4
- package/dist/registry/StatCard.tsx +19 -19
- package/dist/registry/Steps.tsx +7 -7
- package/dist/registry/Table.tsx +12 -10
- package/dist/registry/TableOfContents.tsx +8 -8
- package/dist/registry/Tabs.tsx +6 -6
- package/dist/registry/TagInput.tsx +9 -9
- package/dist/registry/Textarea.tsx +5 -5
- package/dist/registry/TimePicker.tsx +315 -0
- package/dist/registry/Timeline.tsx +14 -14
- package/dist/registry/Toast.tsx +15 -12
- package/dist/registry/Toggle.tsx +7 -10
- package/dist/registry/Tooltip.tsx +21 -73
- package/dist/registry/styles.css +868 -62
- package/dist/registry/useFloatingPanel.ts +112 -0
- package/dist/styles.css +868 -62
- package/dist/useFloatingPanel.d.ts +24 -0
- package/dist/useFloatingPanel.js +89 -0
- package/dist/useFloatingPanel.js.map +1 -0
- package/dist/useModalBehavior.d.ts +15 -0
- package/dist/useModalBehavior.js +57 -0
- package/dist/useModalBehavior.js.map +1 -0
- package/package.json +1 -1
package/dist/Combobox.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// ../../components/ui/Combobox.tsx
|
|
4
|
+
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { Portal } from "./Floating.js";
|
|
6
|
+
import { FieldLabel, FieldMessage } from "./FieldMessage.js";
|
|
7
|
+
import { useFloatingPanel } from "./useFloatingPanel.js";
|
|
8
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
var defaultFilter = (o, q) => {
|
|
10
|
+
const hay = [o.label, o.description ?? "", ...o.keywords ?? []].join(" ").toLowerCase();
|
|
11
|
+
return q.toLowerCase().split(/\s+/).filter(Boolean).every((word) => hay.includes(word));
|
|
12
|
+
};
|
|
13
|
+
function Highlight({ text, query }) {
|
|
14
|
+
const q = query.trim().toLowerCase();
|
|
15
|
+
const at = q ? text.toLowerCase().indexOf(q) : -1;
|
|
16
|
+
if (at < 0) return /* @__PURE__ */ jsx(Fragment, { children: text });
|
|
17
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
18
|
+
text.slice(0, at),
|
|
19
|
+
/* @__PURE__ */ jsx("mark", { className: "bg-transparent font-semibold text-ink", children: text.slice(at, at + q.length) }),
|
|
20
|
+
text.slice(at + q.length)
|
|
21
|
+
] });
|
|
22
|
+
}
|
|
23
|
+
function Combobox({
|
|
24
|
+
options,
|
|
25
|
+
value,
|
|
26
|
+
defaultValue = null,
|
|
27
|
+
onChange,
|
|
28
|
+
placeholder = "search\u2026",
|
|
29
|
+
label,
|
|
30
|
+
hint,
|
|
31
|
+
error,
|
|
32
|
+
emptyText = "nothing matches",
|
|
33
|
+
filter = defaultFilter,
|
|
34
|
+
clearable = false,
|
|
35
|
+
disabled = false,
|
|
36
|
+
fullWidth = false,
|
|
37
|
+
name,
|
|
38
|
+
className = ""
|
|
39
|
+
}) {
|
|
40
|
+
const uid = useId();
|
|
41
|
+
const ref = useRef(null);
|
|
42
|
+
const inputRef = useRef(null);
|
|
43
|
+
const listRef = useRef(null);
|
|
44
|
+
const [internal, setInternal] = useState(defaultValue);
|
|
45
|
+
const selectedValue = value !== void 0 ? value : internal;
|
|
46
|
+
const selected = options.find((o) => o.value === selectedValue) ?? null;
|
|
47
|
+
const [query, setQuery] = useState(null);
|
|
48
|
+
const [active, setActive] = useState(0);
|
|
49
|
+
const { open, mounted, show, close, setFloating, panelStyle } = useFloatingPanel(ref, {
|
|
50
|
+
matchWidth: true,
|
|
51
|
+
onEscape: () => setQuery(null)
|
|
52
|
+
});
|
|
53
|
+
const messageId = error || hint ? `${uid}-message` : void 0;
|
|
54
|
+
const matches = useMemo(
|
|
55
|
+
() => query ? options.filter((o) => filter(o, query)) : options,
|
|
56
|
+
[options, query, filter]
|
|
57
|
+
);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!open) return;
|
|
60
|
+
listRef.current?.querySelector(`[data-index="${active}"]`)?.scrollIntoView({ block: "nearest" });
|
|
61
|
+
}, [open, active]);
|
|
62
|
+
const pick = (o) => {
|
|
63
|
+
if (o?.disabled) return;
|
|
64
|
+
setInternal(o?.value ?? null);
|
|
65
|
+
onChange?.(o?.value ?? null);
|
|
66
|
+
setQuery(null);
|
|
67
|
+
close();
|
|
68
|
+
};
|
|
69
|
+
const openList = () => {
|
|
70
|
+
if (open || disabled) return;
|
|
71
|
+
const i = matches.findIndex((o) => o.value === selectedValue);
|
|
72
|
+
setActive(Math.max(0, i));
|
|
73
|
+
show();
|
|
74
|
+
};
|
|
75
|
+
const step = (dir) => {
|
|
76
|
+
if (!matches.length) return;
|
|
77
|
+
let i = active;
|
|
78
|
+
for (let n = 0; n < matches.length; n++) {
|
|
79
|
+
i = (i + dir + matches.length) % matches.length;
|
|
80
|
+
if (!matches[i].disabled) break;
|
|
81
|
+
}
|
|
82
|
+
setActive(i);
|
|
83
|
+
};
|
|
84
|
+
const onKeyDown = (e) => {
|
|
85
|
+
switch (e.key) {
|
|
86
|
+
case "ArrowDown":
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
if (!open) openList();
|
|
89
|
+
else step(1);
|
|
90
|
+
break;
|
|
91
|
+
case "ArrowUp":
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
if (!open) openList();
|
|
94
|
+
else step(-1);
|
|
95
|
+
break;
|
|
96
|
+
case "Enter":
|
|
97
|
+
if (open && matches[active]) {
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
pick(matches[active]);
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
case "Tab":
|
|
103
|
+
if (open) close();
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const shownText = query ?? selected?.label ?? "";
|
|
108
|
+
const showClear = clearable && !!selected && !disabled;
|
|
109
|
+
return /* @__PURE__ */ jsxs("div", { className: ["group/field flex flex-col gap-1.5", fullWidth ? "w-full" : ""].join(" "), children: [
|
|
110
|
+
label && /* @__PURE__ */ jsx(FieldLabel, { id: `${uid}-label`, htmlFor: `${uid}-input`, children: label }),
|
|
111
|
+
/* @__PURE__ */ jsxs("div", { ref, className: ["group/combo relative isolate", fullWidth ? "w-full" : "w-60", className].join(" "), children: [
|
|
112
|
+
/* @__PURE__ */ jsx(
|
|
113
|
+
"input",
|
|
114
|
+
{
|
|
115
|
+
ref: inputRef,
|
|
116
|
+
id: `${uid}-input`,
|
|
117
|
+
role: "combobox",
|
|
118
|
+
"aria-expanded": open,
|
|
119
|
+
"aria-controls": `${uid}-list`,
|
|
120
|
+
"aria-autocomplete": "list",
|
|
121
|
+
"aria-activedescendant": open && matches[active] ? `${uid}-opt-${active}` : void 0,
|
|
122
|
+
"aria-invalid": error ? true : void 0,
|
|
123
|
+
"aria-describedby": messageId,
|
|
124
|
+
autoComplete: "off",
|
|
125
|
+
spellCheck: false,
|
|
126
|
+
disabled,
|
|
127
|
+
placeholder: selected?.label ?? placeholder,
|
|
128
|
+
value: shownText,
|
|
129
|
+
onChange: (e) => {
|
|
130
|
+
setQuery(e.target.value);
|
|
131
|
+
setActive(0);
|
|
132
|
+
if (!open) show();
|
|
133
|
+
},
|
|
134
|
+
onClick: openList,
|
|
135
|
+
onFocus: (e) => e.target.select(),
|
|
136
|
+
onKeyDown,
|
|
137
|
+
onBlur: () => {
|
|
138
|
+
setQuery(null);
|
|
139
|
+
if (open) close();
|
|
140
|
+
},
|
|
141
|
+
"data-open": open || void 0,
|
|
142
|
+
className: [
|
|
143
|
+
"field relative z-10 w-full rounded-xl py-3 pl-4 text-[13.5px] font-medium text-ink caret-ink",
|
|
144
|
+
"placeholder:font-normal placeholder:text-ink-faint focus:placeholder:text-ink-ghost",
|
|
145
|
+
"disabled:opacity-40 disabled:cursor-not-allowed",
|
|
146
|
+
open ? "cursor-text" : "cursor-pointer",
|
|
147
|
+
showClear ? "pr-[60px]" : "pr-10"
|
|
148
|
+
].join(" ")
|
|
149
|
+
}
|
|
150
|
+
),
|
|
151
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "pointer-events-none absolute inset-0 z-15 overflow-hidden rounded-xl", children: /* @__PURE__ */ jsx(
|
|
152
|
+
"span",
|
|
153
|
+
{
|
|
154
|
+
className: "absolute inset-y-0 -left-[30%] block w-[30%] opacity-0 group-focus-within/combo:animate-input-shimmer",
|
|
155
|
+
style: { background: "linear-gradient(90deg, transparent 0%, var(--shimmer) 50%, transparent 100%)" }
|
|
156
|
+
}
|
|
157
|
+
) }),
|
|
158
|
+
showClear && /* @__PURE__ */ jsx(
|
|
159
|
+
"button",
|
|
160
|
+
{
|
|
161
|
+
type: "button",
|
|
162
|
+
"aria-label": "clear",
|
|
163
|
+
tabIndex: -1,
|
|
164
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
165
|
+
onClick: () => pick(null),
|
|
166
|
+
className: [
|
|
167
|
+
"absolute right-9 top-1/2 z-20 grid h-[18px] w-[18px] -translate-y-1/2 cursor-pointer place-items-center rounded-full",
|
|
168
|
+
"text-ink-faint transition-[opacity,color,background-color] duration-150 hover:bg-surface-active hover:text-ink",
|
|
169
|
+
"opacity-0 group-hover/combo:opacity-100 group-focus-within/combo:opacity-100"
|
|
170
|
+
].join(" "),
|
|
171
|
+
children: /* @__PURE__ */ jsx("svg", { "aria-hidden": true, width: "9", height: "9", fill: "none", stroke: "currentColor", strokeWidth: "2.75", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", d: "M6 6l12 12M18 6L6 18" }) })
|
|
172
|
+
}
|
|
173
|
+
),
|
|
174
|
+
/* @__PURE__ */ jsx(
|
|
175
|
+
"svg",
|
|
176
|
+
{
|
|
177
|
+
"aria-hidden": true,
|
|
178
|
+
width: "14",
|
|
179
|
+
height: "14",
|
|
180
|
+
fill: "none",
|
|
181
|
+
stroke: "currentColor",
|
|
182
|
+
strokeWidth: "2",
|
|
183
|
+
viewBox: "0 0 24 24",
|
|
184
|
+
className: [
|
|
185
|
+
"pointer-events-none absolute right-4 top-1/2 z-20 -mt-[7px] transition-[transform,color] duration-300 ease-[var(--ease-spring)]",
|
|
186
|
+
open ? "rotate-180 text-ink" : "text-ink-faint group-hover/combo:text-ink-label"
|
|
187
|
+
].join(" "),
|
|
188
|
+
children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" })
|
|
189
|
+
}
|
|
190
|
+
),
|
|
191
|
+
name && /* @__PURE__ */ jsx("input", { type: "hidden", name, value: selectedValue ?? "" })
|
|
192
|
+
] }),
|
|
193
|
+
mounted && /* @__PURE__ */ jsx(Portal, { children: /* @__PURE__ */ jsx(
|
|
194
|
+
"div",
|
|
195
|
+
{
|
|
196
|
+
ref: setFloating,
|
|
197
|
+
className: "rounded-[16px] p-1.5",
|
|
198
|
+
style: { ...panelStyle, maxWidth: "min(420px, calc(100vw - 16px))" },
|
|
199
|
+
children: /* @__PURE__ */ jsxs(
|
|
200
|
+
"div",
|
|
201
|
+
{
|
|
202
|
+
ref: listRef,
|
|
203
|
+
id: `${uid}-list`,
|
|
204
|
+
role: "listbox",
|
|
205
|
+
"aria-labelledby": label ? `${uid}-label` : void 0,
|
|
206
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
207
|
+
className: "flex max-h-72 flex-col gap-0.5 overflow-y-auto rounded-[11px] bg-panel-face p-1 scroll-thin",
|
|
208
|
+
style: { boxShadow: "var(--panel-face-shadow)" },
|
|
209
|
+
children: [
|
|
210
|
+
matches.length === 0 && /* @__PURE__ */ jsx("div", { role: "presentation", className: "px-2.5 py-2 text-[13px] text-ink-faint", children: emptyText }),
|
|
211
|
+
matches.map((o, i) => {
|
|
212
|
+
const isSel = o.value === selectedValue;
|
|
213
|
+
const isActive = i === active;
|
|
214
|
+
return /* @__PURE__ */ jsxs(
|
|
215
|
+
"div",
|
|
216
|
+
{
|
|
217
|
+
id: `${uid}-opt-${i}`,
|
|
218
|
+
"data-index": i,
|
|
219
|
+
role: "option",
|
|
220
|
+
"aria-selected": isSel,
|
|
221
|
+
"aria-disabled": o.disabled || void 0,
|
|
222
|
+
onClick: () => pick(o),
|
|
223
|
+
onMouseMove: () => !o.disabled && setActive(i),
|
|
224
|
+
className: [
|
|
225
|
+
"flex w-full items-center justify-between gap-3 rounded-[8px] px-2.5 py-2 text-[13px] transition-colors duration-100",
|
|
226
|
+
o.disabled ? "cursor-not-allowed opacity-40" : "cursor-pointer",
|
|
227
|
+
isSel ? "font-medium" : "",
|
|
228
|
+
// one ink per state, as in Select
|
|
229
|
+
isSel || isActive ? "text-ink" : "text-ink-body",
|
|
230
|
+
isActive ? "bg-surface-active/80" : ""
|
|
231
|
+
].join(" "),
|
|
232
|
+
style: { animation: `select-option-in 300ms var(--ease-out-soft) ${40 + Math.min(i, 8) * 22}ms both` },
|
|
233
|
+
children: [
|
|
234
|
+
/* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col gap-px", children: [
|
|
235
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: /* @__PURE__ */ jsx(Highlight, { text: o.label, query: query ?? "" }) }),
|
|
236
|
+
o.description && /* @__PURE__ */ jsx("span", { className: "truncate text-[11.5px] font-normal text-ink-faint", children: o.description })
|
|
237
|
+
] }),
|
|
238
|
+
isSel && /* @__PURE__ */ jsx("svg", { "aria-hidden": true, className: "shrink-0 text-ink", width: "13", height: "13", fill: "none", stroke: "currentColor", strokeWidth: "2.5", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 13l4 4L19 7" }) })
|
|
239
|
+
]
|
|
240
|
+
},
|
|
241
|
+
o.value
|
|
242
|
+
);
|
|
243
|
+
})
|
|
244
|
+
]
|
|
245
|
+
}
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
) }),
|
|
249
|
+
/* @__PURE__ */ jsx(FieldMessage, { id: messageId, error, hint })
|
|
250
|
+
] });
|
|
251
|
+
}
|
|
252
|
+
export {
|
|
253
|
+
Combobox
|
|
254
|
+
};
|
|
255
|
+
//# sourceMappingURL=Combobox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../components/ui/Combobox.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { Portal } from \"./Floating\";\nimport { FieldLabel, FieldMessage } from \"./FieldMessage\";\nimport { useFloatingPanel } from \"./useFloatingPanel\";\n\nexport interface ComboboxOption {\n value: string;\n label: string;\n /** A second, quieter line under the label */\n description?: string;\n /** Extra words to match on that aren't shown */\n keywords?: string[];\n disabled?: boolean;\n}\n\ninterface ComboboxProps {\n options: ComboboxOption[];\n value?: string | null;\n defaultValue?: string | null;\n onChange?: (value: string | null) => void;\n placeholder?: string;\n label?: string;\n hint?: string;\n error?: string;\n /** Shown when nothing matches */\n emptyText?: string;\n /** Replace the default \"contains\" match */\n filter?: (option: ComboboxOption, query: string) => boolean;\n /** Show a key that empties the field */\n clearable?: boolean;\n disabled?: boolean;\n fullWidth?: boolean;\n name?: string;\n className?: string;\n}\n\nconst defaultFilter = (o: ComboboxOption, q: string) => {\n const hay = [o.label, o.description ?? \"\", ...(o.keywords ?? [])].join(\" \").toLowerCase();\n return q\n .toLowerCase()\n .split(/\\s+/)\n .filter(Boolean)\n .every((word) => hay.includes(word));\n};\n\n/* Bolds the part of the label that matched what was typed */\nfunction Highlight({ text, query }: { text: string; query: string }) {\n const q = query.trim().toLowerCase();\n const at = q ? text.toLowerCase().indexOf(q) : -1;\n if (at < 0) return <>{text}</>;\n return (\n <>\n {text.slice(0, at)}\n <mark className=\"bg-transparent font-semibold text-ink\">{text.slice(at, at + q.length)}</mark>\n {text.slice(at + q.length)}\n </>\n );\n}\n\n/* A Select you can type into. The field filters the list as you go,\n arrows walk the matches, Enter picks one, and leaving the field puts\n the chosen label back if what you typed didn't match anything. */\nexport function Combobox({\n options,\n value,\n defaultValue = null,\n onChange,\n placeholder = \"search…\",\n label,\n hint,\n error,\n emptyText = \"nothing matches\",\n filter = defaultFilter,\n clearable = false,\n disabled = false,\n fullWidth = false,\n name,\n className = \"\",\n}: ComboboxProps) {\n const uid = useId();\n const ref = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const listRef = useRef<HTMLDivElement>(null);\n const [internal, setInternal] = useState<string | null>(defaultValue);\n const selectedValue = value !== undefined ? value : internal;\n const selected = options.find((o) => o.value === selectedValue) ?? null;\n const [query, setQuery] = useState<string | null>(null); // null: not typing, show the selection\n const [active, setActive] = useState(0);\n const { open, mounted, show, close, setFloating, panelStyle } = useFloatingPanel(ref, {\n matchWidth: true,\n onEscape: () => setQuery(null),\n });\n const messageId = error || hint ? `${uid}-message` : undefined;\n\n const matches = useMemo(\n () => (query ? options.filter((o) => filter(o, query)) : options),\n [options, query, filter],\n );\n\n useEffect(() => {\n if (!open) return;\n listRef.current?.querySelector(`[data-index=\"${active}\"]`)?.scrollIntoView({ block: \"nearest\" });\n }, [open, active]);\n\n const pick = (o: ComboboxOption | null) => {\n if (o?.disabled) return;\n setInternal(o?.value ?? null);\n onChange?.(o?.value ?? null);\n setQuery(null);\n close();\n };\n\n const openList = () => {\n if (open || disabled) return;\n const i = matches.findIndex((o) => o.value === selectedValue);\n setActive(Math.max(0, i));\n show();\n };\n\n const step = (dir: 1 | -1) => {\n if (!matches.length) return;\n let i = active;\n for (let n = 0; n < matches.length; n++) {\n i = (i + dir + matches.length) % matches.length;\n if (!matches[i].disabled) break;\n }\n setActive(i);\n };\n\n const onKeyDown = (e: React.KeyboardEvent) => {\n switch (e.key) {\n case \"ArrowDown\":\n e.preventDefault();\n if (!open) openList();\n else step(1);\n break;\n case \"ArrowUp\":\n e.preventDefault();\n if (!open) openList();\n else step(-1);\n break;\n case \"Enter\":\n if (open && matches[active]) {\n e.preventDefault();\n pick(matches[active]);\n }\n break;\n case \"Tab\":\n if (open) close();\n break;\n }\n };\n\n const shownText = query ?? selected?.label ?? \"\";\n const showClear = clearable && !!selected && !disabled;\n\n return (\n <div className={[\"group/field flex flex-col gap-1.5\", fullWidth ? \"w-full\" : \"\"].join(\" \")}>\n {label && (\n <FieldLabel id={`${uid}-label`} htmlFor={`${uid}-input`}>\n {label}\n </FieldLabel>\n )}\n {/* Closed, it reads exactly like Select: the chosen label and a\n chevron. Focus it and it turns into a field you type into. */}\n <div ref={ref} className={[\"group/combo relative isolate\", fullWidth ? \"w-full\" : \"w-60\", className].join(\" \")}>\n <input\n ref={inputRef}\n id={`${uid}-input`}\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={`${uid}-list`}\n aria-autocomplete=\"list\"\n aria-activedescendant={open && matches[active] ? `${uid}-opt-${active}` : undefined}\n aria-invalid={error ? true : undefined}\n aria-describedby={messageId}\n autoComplete=\"off\"\n spellCheck={false}\n disabled={disabled}\n placeholder={selected?.label ?? placeholder}\n value={shownText}\n onChange={(e) => {\n setQuery(e.target.value);\n setActive(0);\n if (!open) show();\n }}\n onClick={openList}\n // typing replaces the chosen label instead of adding to it\n onFocus={(e) => e.target.select()}\n onKeyDown={onKeyDown}\n onBlur={() => {\n setQuery(null);\n if (open) close();\n }}\n data-open={open || undefined}\n className={[\n \"field relative z-10 w-full rounded-xl py-3 pl-4 text-[13.5px] font-medium text-ink caret-ink\",\n \"placeholder:font-normal placeholder:text-ink-faint focus:placeholder:text-ink-ghost\",\n \"disabled:opacity-40 disabled:cursor-not-allowed\",\n open ? \"cursor-text\" : \"cursor-pointer\",\n showClear ? \"pr-[60px]\" : \"pr-10\",\n ].join(\" \")}\n />\n\n {/* Input's sweep of light as the field comes up */}\n <span aria-hidden className=\"pointer-events-none absolute inset-0 z-15 overflow-hidden rounded-xl\">\n <span\n className=\"absolute inset-y-0 -left-[30%] block w-[30%] opacity-0 group-focus-within/combo:animate-input-shimmer\"\n style={{ background: \"linear-gradient(90deg, transparent 0%, var(--shimmer) 50%, transparent 100%)\" }}\n />\n </span>\n\n {showClear && (\n <button\n type=\"button\"\n aria-label=\"clear\"\n tabIndex={-1}\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => pick(null)}\n className={[\n \"absolute right-9 top-1/2 z-20 grid h-[18px] w-[18px] -translate-y-1/2 cursor-pointer place-items-center rounded-full\",\n \"text-ink-faint transition-[opacity,color,background-color] duration-150 hover:bg-surface-active hover:text-ink\",\n \"opacity-0 group-hover/combo:opacity-100 group-focus-within/combo:opacity-100\",\n ].join(\" \")}\n >\n <svg aria-hidden width=\"9\" height=\"9\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.75\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" d=\"M6 6l12 12M18 6L6 18\" />\n </svg>\n </button>\n )}\n\n <svg\n aria-hidden\n width=\"14\"\n height=\"14\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n viewBox=\"0 0 24 24\"\n className={[\n \"pointer-events-none absolute right-4 top-1/2 z-20 -mt-[7px] transition-[transform,color] duration-300 ease-[var(--ease-spring)]\",\n open ? \"rotate-180 text-ink\" : \"text-ink-faint group-hover/combo:text-ink-label\",\n ].join(\" \")}\n >\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M19 9l-7 7-7-7\" />\n </svg>\n {name && <input type=\"hidden\" name={name} value={selectedValue ?? \"\"} />}\n </div>\n\n {mounted && (\n <Portal>\n <div\n ref={setFloating}\n className=\"rounded-[16px] p-1.5\"\n style={{ ...panelStyle, maxWidth: \"min(420px, calc(100vw - 16px))\" }}\n >\n <div\n ref={listRef}\n id={`${uid}-list`}\n role=\"listbox\"\n aria-labelledby={label ? `${uid}-label` : undefined}\n // keeps focus in the field, even when the scrollbar is dragged\n onMouseDown={(e) => e.preventDefault()}\n className=\"flex max-h-72 flex-col gap-0.5 overflow-y-auto rounded-[11px] bg-panel-face p-1 scroll-thin\"\n style={{ boxShadow: \"var(--panel-face-shadow)\" }}\n >\n {matches.length === 0 && (\n <div role=\"presentation\" className=\"px-2.5 py-2 text-[13px] text-ink-faint\">\n {emptyText}\n </div>\n )}\n {matches.map((o, i) => {\n const isSel = o.value === selectedValue;\n const isActive = i === active;\n return (\n <div\n key={o.value}\n id={`${uid}-opt-${i}`}\n data-index={i}\n role=\"option\"\n aria-selected={isSel}\n aria-disabled={o.disabled || undefined}\n onClick={() => pick(o)}\n onMouseMove={() => !o.disabled && setActive(i)}\n className={[\n \"flex w-full items-center justify-between gap-3 rounded-[8px] px-2.5 py-2 text-[13px] transition-colors duration-100\",\n o.disabled ? \"cursor-not-allowed opacity-40\" : \"cursor-pointer\",\n isSel ? \"font-medium\" : \"\",\n // one ink per state, as in Select\n isSel || isActive ? \"text-ink\" : \"text-ink-body\",\n isActive ? \"bg-surface-active/80\" : \"\",\n ].join(\" \")}\n style={{ animation: `select-option-in 300ms var(--ease-out-soft) ${40 + Math.min(i, 8) * 22}ms both` }}\n >\n <span className=\"flex min-w-0 flex-col gap-px\">\n <span className=\"truncate\">\n <Highlight text={o.label} query={query ?? \"\"} />\n </span>\n {o.description && <span className=\"truncate text-[11.5px] font-normal text-ink-faint\">{o.description}</span>}\n </span>\n {isSel && (\n <svg aria-hidden className=\"shrink-0 text-ink\" width=\"13\" height=\"13\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M5 13l4 4L19 7\" />\n </svg>\n )}\n </div>\n );\n })}\n </div>\n </div>\n </Portal>\n )}\n <FieldMessage id={messageId} error={error} hint={hint} />\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
interface CommandItem {
|
|
5
|
+
key: string;
|
|
6
|
+
label: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
icon?: React.ReactNode;
|
|
9
|
+
shortcut?: string;
|
|
10
|
+
onSelect?: () => void;
|
|
11
|
+
}
|
|
12
|
+
interface CommandSection {
|
|
13
|
+
title: string;
|
|
14
|
+
items: CommandItem[];
|
|
15
|
+
}
|
|
16
|
+
interface CommandPaletteProps {
|
|
17
|
+
open: boolean;
|
|
18
|
+
onClose: () => void;
|
|
19
|
+
sections: CommandSection[];
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
declare function CommandPalette({ open, onClose, sections, placeholder, className, }: CommandPaletteProps): react_jsx_runtime.JSX.Element | null;
|
|
24
|
+
|
|
25
|
+
export { CommandPalette };
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// ../../components/ui/CommandPalette.tsx
|
|
4
|
+
import {
|
|
5
|
+
useEffect,
|
|
6
|
+
useLayoutEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
useMemo,
|
|
10
|
+
useCallback,
|
|
11
|
+
useId
|
|
12
|
+
} from "react";
|
|
13
|
+
import { Portal } from "./Floating.js";
|
|
14
|
+
import { Kbd } from "./Kbd.js";
|
|
15
|
+
import { useModalBehavior } from "./useModalBehavior.js";
|
|
16
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
17
|
+
var CLOSE_MS = 240;
|
|
18
|
+
function CommandPalette({
|
|
19
|
+
open,
|
|
20
|
+
onClose,
|
|
21
|
+
sections,
|
|
22
|
+
placeholder = "search commands\u2026",
|
|
23
|
+
className = ""
|
|
24
|
+
}) {
|
|
25
|
+
const [mounted, setMounted] = useState(false);
|
|
26
|
+
const [visible, setVisible] = useState(false);
|
|
27
|
+
const [query, setQuery] = useState("");
|
|
28
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
29
|
+
const dialogRef = useRef(null);
|
|
30
|
+
const inputRef = useRef(null);
|
|
31
|
+
const listRef = useRef(null);
|
|
32
|
+
const uid = useId();
|
|
33
|
+
const filtered = useMemo(() => {
|
|
34
|
+
const q = query.toLowerCase().trim();
|
|
35
|
+
if (!q) return sections;
|
|
36
|
+
return sections.map((section) => ({
|
|
37
|
+
...section,
|
|
38
|
+
items: section.items.filter(
|
|
39
|
+
(item) => item.label.toLowerCase().includes(q) || item.description?.toLowerCase().includes(q)
|
|
40
|
+
)
|
|
41
|
+
})).filter((section) => section.items.length > 0);
|
|
42
|
+
}, [sections, query]);
|
|
43
|
+
const flatItems = useMemo(
|
|
44
|
+
() => filtered.flatMap((s) => s.items),
|
|
45
|
+
[filtered]
|
|
46
|
+
);
|
|
47
|
+
const [wasOpen, setWasOpen] = useState(false);
|
|
48
|
+
if (open !== wasOpen) {
|
|
49
|
+
setWasOpen(open);
|
|
50
|
+
if (open) {
|
|
51
|
+
setMounted(true);
|
|
52
|
+
setQuery("");
|
|
53
|
+
setActiveIndex(0);
|
|
54
|
+
} else {
|
|
55
|
+
setVisible(false);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (open || !mounted) return;
|
|
60
|
+
const t = setTimeout(() => setMounted(false), CLOSE_MS);
|
|
61
|
+
return () => clearTimeout(t);
|
|
62
|
+
}, [open, mounted]);
|
|
63
|
+
useLayoutEffect(() => {
|
|
64
|
+
if (!mounted || !open || visible) return;
|
|
65
|
+
const el = dialogRef.current;
|
|
66
|
+
if (el) {
|
|
67
|
+
void el.getBoundingClientRect();
|
|
68
|
+
}
|
|
69
|
+
const id = requestAnimationFrame(() => {
|
|
70
|
+
setVisible(true);
|
|
71
|
+
});
|
|
72
|
+
return () => cancelAnimationFrame(id);
|
|
73
|
+
}, [mounted, open, visible]);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
setActiveIndex(0);
|
|
76
|
+
}, [query]);
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!listRef.current) return;
|
|
79
|
+
const activeEl = listRef.current.querySelector(
|
|
80
|
+
`[data-command-index="${activeIndex}"]`
|
|
81
|
+
);
|
|
82
|
+
if (activeEl) {
|
|
83
|
+
activeEl.scrollIntoView({ block: "nearest" });
|
|
84
|
+
}
|
|
85
|
+
}, [activeIndex]);
|
|
86
|
+
const handleSelect = useCallback(
|
|
87
|
+
(item) => {
|
|
88
|
+
item.onSelect?.();
|
|
89
|
+
onClose();
|
|
90
|
+
},
|
|
91
|
+
[onClose]
|
|
92
|
+
);
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!open) return;
|
|
95
|
+
const onKey = (e) => {
|
|
96
|
+
switch (e.key) {
|
|
97
|
+
case "ArrowDown":
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
setActiveIndex((i) => i + 1 >= flatItems.length ? 0 : i + 1);
|
|
100
|
+
break;
|
|
101
|
+
case "ArrowUp":
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
setActiveIndex(
|
|
104
|
+
(i) => i - 1 < 0 ? Math.max(flatItems.length - 1, 0) : i - 1
|
|
105
|
+
);
|
|
106
|
+
break;
|
|
107
|
+
case "Enter":
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
if (flatItems[activeIndex]) {
|
|
110
|
+
handleSelect(flatItems[activeIndex]);
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
document.addEventListener("keydown", onKey);
|
|
116
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
117
|
+
}, [open, onClose, flatItems, activeIndex, handleSelect]);
|
|
118
|
+
useModalBehavior({ open, ready: visible, ref: dialogRef, onClose, initialFocus: inputRef });
|
|
119
|
+
if (!mounted) return null;
|
|
120
|
+
let itemIndex = 0;
|
|
121
|
+
return /* @__PURE__ */ jsx(Portal, { children: /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-90 flex items-start justify-center pt-[min(20vh,140px)] px-4", children: [
|
|
122
|
+
/* @__PURE__ */ jsx(
|
|
123
|
+
"div",
|
|
124
|
+
{
|
|
125
|
+
className: [
|
|
126
|
+
"absolute inset-0 backdrop-blur-[6px]",
|
|
127
|
+
"transition-opacity ease-out",
|
|
128
|
+
visible ? "opacity-100 duration-300" : "opacity-0 duration-200"
|
|
129
|
+
].join(" "),
|
|
130
|
+
style: { background: "var(--scrim)" },
|
|
131
|
+
"aria-hidden": true,
|
|
132
|
+
onClick: onClose
|
|
133
|
+
}
|
|
134
|
+
),
|
|
135
|
+
/* @__PURE__ */ jsx(
|
|
136
|
+
"div",
|
|
137
|
+
{
|
|
138
|
+
ref: dialogRef,
|
|
139
|
+
role: "dialog",
|
|
140
|
+
"aria-modal": "true",
|
|
141
|
+
"aria-label": "Command palette",
|
|
142
|
+
className: [
|
|
143
|
+
"relative z-10 w-full max-w-lg rounded-[20px] p-1.5 overflow-hidden",
|
|
144
|
+
"transition-[opacity,transform,scale,translate] ease-[var(--ease-spring)]",
|
|
145
|
+
visible ? "opacity-100 scale-100 translate-y-0 duration-300" : "opacity-0 scale-[0.94] translate-y-3 duration-200",
|
|
146
|
+
className
|
|
147
|
+
].filter(Boolean).join(" "),
|
|
148
|
+
style: { background: "var(--panel-bezel)", boxShadow: "var(--modal-shadow)" },
|
|
149
|
+
children: /* @__PURE__ */ jsxs(
|
|
150
|
+
"div",
|
|
151
|
+
{
|
|
152
|
+
className: "bg-panel-face rounded-[14px] overflow-hidden",
|
|
153
|
+
style: { boxShadow: "var(--panel-face-shadow)" },
|
|
154
|
+
children: [
|
|
155
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-4 py-3.5", children: [
|
|
156
|
+
/* @__PURE__ */ jsx(
|
|
157
|
+
"svg",
|
|
158
|
+
{
|
|
159
|
+
className: "shrink-0 text-ink-faint",
|
|
160
|
+
width: "14",
|
|
161
|
+
height: "14",
|
|
162
|
+
fill: "none",
|
|
163
|
+
stroke: "currentColor",
|
|
164
|
+
strokeWidth: "1.75",
|
|
165
|
+
viewBox: "0 0 24 24",
|
|
166
|
+
children: /* @__PURE__ */ jsx(
|
|
167
|
+
"path",
|
|
168
|
+
{
|
|
169
|
+
strokeLinecap: "round",
|
|
170
|
+
strokeLinejoin: "round",
|
|
171
|
+
d: "M21 21l-4.35-4.35M11 19a8 8 0 100-16 8 8 0 000 16z"
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
),
|
|
176
|
+
/* @__PURE__ */ jsx(
|
|
177
|
+
"input",
|
|
178
|
+
{
|
|
179
|
+
ref: inputRef,
|
|
180
|
+
type: "text",
|
|
181
|
+
value: query,
|
|
182
|
+
onChange: (e) => setQuery(e.target.value),
|
|
183
|
+
placeholder,
|
|
184
|
+
role: "combobox",
|
|
185
|
+
"aria-expanded": "true",
|
|
186
|
+
"aria-controls": `${uid}-list`,
|
|
187
|
+
"aria-activedescendant": flatItems[activeIndex] ? `${uid}-item-${activeIndex}` : void 0,
|
|
188
|
+
"aria-autocomplete": "list",
|
|
189
|
+
className: "flex-1 min-w-0 text-[14.5px] font-medium text-ink placeholder:text-ink-faint placeholder:font-normal bg-transparent border-none outline-none"
|
|
190
|
+
}
|
|
191
|
+
),
|
|
192
|
+
/* @__PURE__ */ jsx(Kbd, { className: "shrink-0", children: "esc" })
|
|
193
|
+
] }),
|
|
194
|
+
/* @__PURE__ */ jsx("div", { className: "border-b border-line" }),
|
|
195
|
+
/* @__PURE__ */ jsx(
|
|
196
|
+
"div",
|
|
197
|
+
{
|
|
198
|
+
ref: listRef,
|
|
199
|
+
id: `${uid}-list`,
|
|
200
|
+
role: "listbox",
|
|
201
|
+
className: "max-h-[340px] overflow-y-auto scroll-thin py-2 px-2",
|
|
202
|
+
children: filtered.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "py-10 flex flex-col items-center gap-1 text-center", children: [
|
|
203
|
+
/* @__PURE__ */ jsxs("p", { className: "text-[13px] font-medium text-ink-label", children: [
|
|
204
|
+
"nothing for \u201C",
|
|
205
|
+
query,
|
|
206
|
+
"\u201D"
|
|
207
|
+
] }),
|
|
208
|
+
/* @__PURE__ */ jsx("p", { className: "text-[12px] text-ink-faint", children: "try a shorter or different word." })
|
|
209
|
+
] }) : filtered.map((section, si) => /* @__PURE__ */ jsxs("div", { role: "group", "aria-label": section.title, className: si > 0 ? "mt-3" : "", children: [
|
|
210
|
+
/* @__PURE__ */ jsx("div", { "aria-hidden": true, className: "px-3 py-1.5 text-[11.5px] font-medium text-ink-faint lowercase select-none", children: section.title }),
|
|
211
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-0.5", children: section.items.map((item) => {
|
|
212
|
+
const currentIndex = itemIndex++;
|
|
213
|
+
const isActive = currentIndex === activeIndex;
|
|
214
|
+
return /* @__PURE__ */ jsxs(
|
|
215
|
+
"div",
|
|
216
|
+
{
|
|
217
|
+
id: `${uid}-item-${currentIndex}`,
|
|
218
|
+
role: "option",
|
|
219
|
+
"aria-selected": isActive,
|
|
220
|
+
"data-command-index": currentIndex,
|
|
221
|
+
onClick: () => handleSelect(item),
|
|
222
|
+
onMouseMove: () => setActiveIndex(currentIndex),
|
|
223
|
+
className: [
|
|
224
|
+
"group/cmd flex items-center gap-3 w-full text-left px-3 py-2 rounded-[10px] cursor-pointer",
|
|
225
|
+
"transition-[background,box-shadow] duration-100",
|
|
226
|
+
isActive ? "bg-surface-hover" : ""
|
|
227
|
+
].join(" "),
|
|
228
|
+
style: { boxShadow: isActive ? "var(--panel-face-shadow)" : void 0 },
|
|
229
|
+
children: [
|
|
230
|
+
item.icon && /* @__PURE__ */ jsx(
|
|
231
|
+
"span",
|
|
232
|
+
{
|
|
233
|
+
className: [
|
|
234
|
+
"shrink-0 w-7 h-7 rounded-[8px] flex items-center justify-center transition-colors duration-100",
|
|
235
|
+
isActive ? "text-ink" : "text-ink-muted"
|
|
236
|
+
].join(" "),
|
|
237
|
+
style: { background: "var(--face)", boxShadow: "var(--face-shadow)" },
|
|
238
|
+
children: item.icon
|
|
239
|
+
}
|
|
240
|
+
),
|
|
241
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0 flex flex-col", children: [
|
|
242
|
+
/* @__PURE__ */ jsx("span", { className: "text-[13px] font-medium text-ink truncate", children: item.label }),
|
|
243
|
+
item.description && /* @__PURE__ */ jsx("span", { className: "text-[12px] text-ink-muted truncate", children: item.description })
|
|
244
|
+
] }),
|
|
245
|
+
item.shortcut && /* @__PURE__ */ jsx(Kbd, { className: "shrink-0", children: item.shortcut }),
|
|
246
|
+
!item.shortcut && /* @__PURE__ */ jsx(
|
|
247
|
+
"svg",
|
|
248
|
+
{
|
|
249
|
+
"aria-hidden": true,
|
|
250
|
+
width: "12",
|
|
251
|
+
height: "12",
|
|
252
|
+
fill: "none",
|
|
253
|
+
stroke: "currentColor",
|
|
254
|
+
strokeWidth: "2",
|
|
255
|
+
viewBox: "0 0 24 24",
|
|
256
|
+
className: [
|
|
257
|
+
"shrink-0 text-ink-faint transition-all duration-150",
|
|
258
|
+
isActive ? "opacity-100 translate-x-0" : "opacity-0 -translate-x-1"
|
|
259
|
+
].join(" "),
|
|
260
|
+
children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M9 5l7 7-7 7" })
|
|
261
|
+
}
|
|
262
|
+
)
|
|
263
|
+
]
|
|
264
|
+
},
|
|
265
|
+
item.key
|
|
266
|
+
);
|
|
267
|
+
}) })
|
|
268
|
+
] }, section.title))
|
|
269
|
+
}
|
|
270
|
+
),
|
|
271
|
+
/* @__PURE__ */ jsxs(
|
|
272
|
+
"div",
|
|
273
|
+
{
|
|
274
|
+
className: "flex items-center gap-4 px-4 py-2.5 border-t border-line text-[11.5px] text-ink-muted select-none",
|
|
275
|
+
style: { background: "var(--palette-tray)" },
|
|
276
|
+
children: [
|
|
277
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5", children: [
|
|
278
|
+
/* @__PURE__ */ jsx(Kbd, { children: "\u2191" }),
|
|
279
|
+
/* @__PURE__ */ jsx(Kbd, { children: "\u2193" }),
|
|
280
|
+
"Navigate"
|
|
281
|
+
] }),
|
|
282
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5", children: [
|
|
283
|
+
/* @__PURE__ */ jsx(Kbd, { children: "\u21B5" }),
|
|
284
|
+
"Open"
|
|
285
|
+
] }),
|
|
286
|
+
/* @__PURE__ */ jsxs("span", { className: "ml-auto tabular-nums text-ink-faint", children: [
|
|
287
|
+
flatItems.length,
|
|
288
|
+
" ",
|
|
289
|
+
flatItems.length === 1 ? "result" : "results"
|
|
290
|
+
] })
|
|
291
|
+
]
|
|
292
|
+
}
|
|
293
|
+
)
|
|
294
|
+
]
|
|
295
|
+
}
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
)
|
|
299
|
+
] }) });
|
|
300
|
+
}
|
|
301
|
+
export {
|
|
302
|
+
CommandPalette
|
|
303
|
+
};
|
|
304
|
+
//# sourceMappingURL=CommandPalette.js.map
|