@delightstack/components 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 +136 -0
- package/SKILL.md +149 -0
- package/bin/agents.js +63 -0
- package/dist/actions/Alert.svelte +202 -0
- package/dist/actions/Alert.svelte.d.ts +36 -0
- package/dist/actions/Alert.svelte.d.ts.map +1 -0
- package/dist/actions/Button.svelte +1450 -0
- package/dist/actions/Button.svelte.d.ts +56 -0
- package/dist/actions/Button.svelte.d.ts.map +1 -0
- package/dist/actions/ButtonGroup.svelte +111 -0
- package/dist/actions/ButtonGroup.svelte.d.ts +41 -0
- package/dist/actions/ButtonGroup.svelte.d.ts.map +1 -0
- package/dist/actions/CommandPalette.svelte +939 -0
- package/dist/actions/CommandPalette.svelte.d.ts +37 -0
- package/dist/actions/CommandPalette.svelte.d.ts.map +1 -0
- package/dist/actions/ContextMenu.svelte +138 -0
- package/dist/actions/ContextMenu.svelte.d.ts +54 -0
- package/dist/actions/ContextMenu.svelte.d.ts.map +1 -0
- package/dist/actions/Modal.svelte +474 -0
- package/dist/actions/Modal.svelte.d.ts +28 -0
- package/dist/actions/Modal.svelte.d.ts.map +1 -0
- package/dist/actions/Popover.svelte +1214 -0
- package/dist/actions/Popover.svelte.d.ts +31 -0
- package/dist/actions/Popover.svelte.d.ts.map +1 -0
- package/dist/actions/Portal.svelte +80 -0
- package/dist/actions/Portal.svelte.d.ts +17 -0
- package/dist/actions/Portal.svelte.d.ts.map +1 -0
- package/dist/actions/ThemeToggle.svelte +345 -0
- package/dist/actions/ThemeToggle.svelte.d.ts +15 -0
- package/dist/actions/ThemeToggle.svelte.d.ts.map +1 -0
- package/dist/actions/index.d.ts +13 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +10 -0
- package/dist/actions/scrollbar.d.ts +48 -0
- package/dist/actions/scrollbar.d.ts.map +1 -0
- package/dist/actions/scrollbar.js +404 -0
- package/dist/display/Accordion.svelte +586 -0
- package/dist/display/Accordion.svelte.d.ts +41 -0
- package/dist/display/Accordion.svelte.d.ts.map +1 -0
- package/dist/display/Avatar.svelte +527 -0
- package/dist/display/Avatar.svelte.d.ts +22 -0
- package/dist/display/Avatar.svelte.d.ts.map +1 -0
- package/dist/display/AvatarGroup.svelte +298 -0
- package/dist/display/AvatarGroup.svelte.d.ts +31 -0
- package/dist/display/AvatarGroup.svelte.d.ts.map +1 -0
- package/dist/display/Calendar.svelte +1366 -0
- package/dist/display/Calendar.svelte.d.ts +58 -0
- package/dist/display/Calendar.svelte.d.ts.map +1 -0
- package/dist/display/Chart.svelte +1426 -0
- package/dist/display/Chart.svelte.d.ts +35 -0
- package/dist/display/Chart.svelte.d.ts.map +1 -0
- package/dist/display/Code.svelte +780 -0
- package/dist/display/Code.svelte.d.ts +19 -0
- package/dist/display/Code.svelte.d.ts.map +1 -0
- package/dist/display/Comparison.svelte +686 -0
- package/dist/display/Comparison.svelte.d.ts +22 -0
- package/dist/display/Comparison.svelte.d.ts.map +1 -0
- package/dist/display/Counter.svelte +285 -0
- package/dist/display/Counter.svelte.d.ts +21 -0
- package/dist/display/Counter.svelte.d.ts.map +1 -0
- package/dist/display/Expand.svelte +48 -0
- package/dist/display/Expand.svelte.d.ts +9 -0
- package/dist/display/Expand.svelte.d.ts.map +1 -0
- package/dist/display/List.svelte +294 -0
- package/dist/display/List.svelte.d.ts +40 -0
- package/dist/display/List.svelte.d.ts.map +1 -0
- package/dist/display/ListContextReset.svelte +19 -0
- package/dist/display/ListContextReset.svelte.d.ts +7 -0
- package/dist/display/ListContextReset.svelte.d.ts.map +1 -0
- package/dist/display/ListItem.svelte +834 -0
- package/dist/display/ListItem.svelte.d.ts +22 -0
- package/dist/display/ListItem.svelte.d.ts.map +1 -0
- package/dist/display/QR.svelte +1193 -0
- package/dist/display/QR.svelte.d.ts +23 -0
- package/dist/display/QR.svelte.d.ts.map +1 -0
- package/dist/display/SplitPane.svelte +744 -0
- package/dist/display/SplitPane.svelte.d.ts +25 -0
- package/dist/display/SplitPane.svelte.d.ts.map +1 -0
- package/dist/display/Stat.svelte +439 -0
- package/dist/display/Stat.svelte.d.ts +24 -0
- package/dist/display/Stat.svelte.d.ts.map +1 -0
- package/dist/display/Table.svelte +4654 -0
- package/dist/display/Table.svelte.d.ts +249 -0
- package/dist/display/Table.svelte.d.ts.map +1 -0
- package/dist/display/TableCellEditor.svelte +935 -0
- package/dist/display/TableCellEditor.svelte.d.ts +58 -0
- package/dist/display/TableCellEditor.svelte.d.ts.map +1 -0
- package/dist/display/Timeline.svelte +1258 -0
- package/dist/display/Timeline.svelte.d.ts +43 -0
- package/dist/display/Timeline.svelte.d.ts.map +1 -0
- package/dist/display/Tree.svelte +1740 -0
- package/dist/display/Tree.svelte.d.ts +74 -0
- package/dist/display/Tree.svelte.d.ts.map +1 -0
- package/dist/display/Typewriter.svelte +338 -0
- package/dist/display/Typewriter.svelte.d.ts +22 -0
- package/dist/display/Typewriter.svelte.d.ts.map +1 -0
- package/dist/display/index.d.ts +24 -0
- package/dist/display/index.d.ts.map +1 -0
- package/dist/display/index.js +18 -0
- package/dist/feedback/Callout.svelte +529 -0
- package/dist/feedback/Callout.svelte.d.ts +24 -0
- package/dist/feedback/Callout.svelte.d.ts.map +1 -0
- package/dist/feedback/Confetti.svelte +631 -0
- package/dist/feedback/Confetti.svelte.d.ts +90 -0
- package/dist/feedback/Confetti.svelte.d.ts.map +1 -0
- package/dist/feedback/Progress.svelte +382 -0
- package/dist/feedback/Progress.svelte.d.ts +25 -0
- package/dist/feedback/Progress.svelte.d.ts.map +1 -0
- package/dist/feedback/Toast.svelte +967 -0
- package/dist/feedback/Toast.svelte.d.ts +54 -0
- package/dist/feedback/Toast.svelte.d.ts.map +1 -0
- package/dist/feedback/index.d.ts +7 -0
- package/dist/feedback/index.d.ts.map +1 -0
- package/dist/feedback/index.js +4 -0
- package/dist/form/Checkbox.svelte +449 -0
- package/dist/form/Checkbox.svelte.d.ts +27 -0
- package/dist/form/Checkbox.svelte.d.ts.map +1 -0
- package/dist/form/Fieldset.svelte +410 -0
- package/dist/form/Fieldset.svelte.d.ts +22 -0
- package/dist/form/Fieldset.svelte.d.ts.map +1 -0
- package/dist/form/FileUpload.svelte +934 -0
- package/dist/form/FileUpload.svelte.d.ts +41 -0
- package/dist/form/FileUpload.svelte.d.ts.map +1 -0
- package/dist/form/Form.svelte +530 -0
- package/dist/form/Form.svelte.d.ts +120 -0
- package/dist/form/Form.svelte.d.ts.map +1 -0
- package/dist/form/Input.svelte +2858 -0
- package/dist/form/Input.svelte.d.ts +66 -0
- package/dist/form/Input.svelte.d.ts.map +1 -0
- package/dist/form/Radio.svelte +507 -0
- package/dist/form/Radio.svelte.d.ts +39 -0
- package/dist/form/Radio.svelte.d.ts.map +1 -0
- package/dist/form/Range.svelte +912 -0
- package/dist/form/Range.svelte.d.ts +33 -0
- package/dist/form/Range.svelte.d.ts.map +1 -0
- package/dist/form/Rating.svelte +429 -0
- package/dist/form/Rating.svelte.d.ts +28 -0
- package/dist/form/Rating.svelte.d.ts.map +1 -0
- package/dist/form/Select.svelte +1933 -0
- package/dist/form/Select.svelte.d.ts +54 -0
- package/dist/form/Select.svelte.d.ts.map +1 -0
- package/dist/form/Toggle.svelte +645 -0
- package/dist/form/Toggle.svelte.d.ts +50 -0
- package/dist/form/Toggle.svelte.d.ts.map +1 -0
- package/dist/form/index.d.ts +15 -0
- package/dist/form/index.d.ts.map +1 -0
- package/dist/form/index.js +10 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/layout/README.md +172 -0
- package/dist/media/Carousel.svelte +2424 -0
- package/dist/media/Carousel.svelte.d.ts +47 -0
- package/dist/media/Carousel.svelte.d.ts.map +1 -0
- package/dist/media/Gallery.svelte +2881 -0
- package/dist/media/Gallery.svelte.d.ts +82 -0
- package/dist/media/Gallery.svelte.d.ts.map +1 -0
- package/dist/media/Image.svelte +389 -0
- package/dist/media/Image.svelte.d.ts +33 -0
- package/dist/media/Image.svelte.d.ts.map +1 -0
- package/dist/media/PDF.svelte +1793 -0
- package/dist/media/PDF.svelte.d.ts +44 -0
- package/dist/media/PDF.svelte.d.ts.map +1 -0
- package/dist/media/Panorama.svelte +1391 -0
- package/dist/media/Panorama.svelte.d.ts +47 -0
- package/dist/media/Panorama.svelte.d.ts.map +1 -0
- package/dist/media/Video.svelte +2501 -0
- package/dist/media/Video.svelte.d.ts +58 -0
- package/dist/media/Video.svelte.d.ts.map +1 -0
- package/dist/media/carousel.d.ts +211 -0
- package/dist/media/carousel.d.ts.map +1 -0
- package/dist/media/carousel.js +408 -0
- package/dist/media/index.d.ts +11 -0
- package/dist/media/index.d.ts.map +1 -0
- package/dist/media/index.js +5 -0
- package/dist/navigation/BottomSheet.svelte +636 -0
- package/dist/navigation/BottomSheet.svelte.d.ts +27 -0
- package/dist/navigation/BottomSheet.svelte.d.ts.map +1 -0
- package/dist/navigation/Breadcrumbs.svelte +611 -0
- package/dist/navigation/Breadcrumbs.svelte.d.ts +28 -0
- package/dist/navigation/Breadcrumbs.svelte.d.ts.map +1 -0
- package/dist/navigation/Pagination.svelte +641 -0
- package/dist/navigation/Pagination.svelte.d.ts +27 -0
- package/dist/navigation/Pagination.svelte.d.ts.map +1 -0
- package/dist/navigation/Steps.svelte +965 -0
- package/dist/navigation/Steps.svelte.d.ts +43 -0
- package/dist/navigation/Steps.svelte.d.ts.map +1 -0
- package/dist/navigation/Tabs.svelte +698 -0
- package/dist/navigation/Tabs.svelte.d.ts +41 -0
- package/dist/navigation/Tabs.svelte.d.ts.map +1 -0
- package/dist/navigation/index.d.ts +8 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +5 -0
- package/package.json +139 -0
|
@@ -0,0 +1,939 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Component } from 'svelte';
|
|
3
|
+
|
|
4
|
+
/** An option/command in the command palette */
|
|
5
|
+
export interface CommandOption {
|
|
6
|
+
/** Unique identifier for the command */
|
|
7
|
+
id: string;
|
|
8
|
+
|
|
9
|
+
/** Display title of the command */
|
|
10
|
+
title: string;
|
|
11
|
+
|
|
12
|
+
/** Optional description shown below the title */
|
|
13
|
+
description?: string;
|
|
14
|
+
|
|
15
|
+
/** Category for grouping commands */
|
|
16
|
+
category?: string;
|
|
17
|
+
|
|
18
|
+
/** Optional icon component */
|
|
19
|
+
icon?: Component;
|
|
20
|
+
|
|
21
|
+
/** Keyboard shortcut keys to display (e.g. ['Ctrl', 'K']) */
|
|
22
|
+
shortcut?: string[];
|
|
23
|
+
|
|
24
|
+
/** Additional search keywords */
|
|
25
|
+
keywords?: string[];
|
|
26
|
+
|
|
27
|
+
/** Whether the command is disabled */
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
|
|
30
|
+
/** Called when the command is selected */
|
|
31
|
+
onselect: () => void | Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Module-level recent commands tracking */
|
|
35
|
+
let recentCommandIds: string[] = $state([]);
|
|
36
|
+
|
|
37
|
+
function trackRecent(id: string, limit: number) {
|
|
38
|
+
recentCommandIds = [id, ...recentCommandIds.filter((r) => r !== id)].slice(0, limit);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Registry of mounted palettes so only one responds to the global shortcut:
|
|
43
|
+
* the topmost open palette, or the most recently mounted one when none are open
|
|
44
|
+
*/
|
|
45
|
+
let paletteRegistry: Array<{ isOpen: () => boolean }> = [];
|
|
46
|
+
|
|
47
|
+
function activePalette() {
|
|
48
|
+
for (let i = paletteRegistry.length - 1; i >= 0; i--) {
|
|
49
|
+
if (paletteRegistry[i].isOpen()) return paletteRegistry[i];
|
|
50
|
+
}
|
|
51
|
+
return paletteRegistry[paletteRegistry.length - 1];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Represents a segment of text with optional highlight */
|
|
55
|
+
interface TextSegment {
|
|
56
|
+
text: string;
|
|
57
|
+
highlighted: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Represents a scored search result */
|
|
61
|
+
interface ScoredCommand {
|
|
62
|
+
command: CommandOption;
|
|
63
|
+
score: number;
|
|
64
|
+
title_segments: TextSegment[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Check if characters of query appear in order within target (fuzzy) */
|
|
68
|
+
function fuzzyMatch(
|
|
69
|
+
query: string,
|
|
70
|
+
target: string,
|
|
71
|
+
): { matched: boolean; score: number; indices: number[] } {
|
|
72
|
+
const lower_query = query.toLowerCase();
|
|
73
|
+
const lower_target = target.toLowerCase();
|
|
74
|
+
const indices: number[] = [];
|
|
75
|
+
let qi = 0;
|
|
76
|
+
for (let ti = 0; ti < lower_target.length && qi < lower_query.length; ti++) {
|
|
77
|
+
if (lower_target[ti] === lower_query[qi]) {
|
|
78
|
+
indices.push(ti);
|
|
79
|
+
qi++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (qi < lower_query.length) return { matched: false, score: 0, indices: [] };
|
|
83
|
+
// Score based on how tight the match is
|
|
84
|
+
const spread = indices.length > 1 ? indices[indices.length - 1] - indices[0] : 0;
|
|
85
|
+
const score = Math.max(1, 10 - spread);
|
|
86
|
+
return { matched: true, score, indices };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Score a single field against a single query word */
|
|
90
|
+
function scoreField(word: string, field: string): { score: number; indices: number[] } {
|
|
91
|
+
const lower_word = word.toLowerCase();
|
|
92
|
+
const lower_field = field.toLowerCase();
|
|
93
|
+
|
|
94
|
+
// Exact match
|
|
95
|
+
if (lower_field === lower_word)
|
|
96
|
+
return { score: 100, indices: Array.from({ length: field.length }, (_, i) => i) };
|
|
97
|
+
|
|
98
|
+
// Starts with
|
|
99
|
+
if (lower_field.startsWith(lower_word)) {
|
|
100
|
+
return { score: 75, indices: Array.from({ length: word.length }, (_, i) => i) };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Word boundary match (matches at the start of a word within the field)
|
|
104
|
+
const word_boundary_regex = new RegExp(
|
|
105
|
+
`(?:^|[\\s\\-_])${escapeRegex(lower_word)}`,
|
|
106
|
+
'i',
|
|
107
|
+
);
|
|
108
|
+
const boundary_match = lower_field.match(word_boundary_regex);
|
|
109
|
+
if (boundary_match) {
|
|
110
|
+
const start =
|
|
111
|
+
boundary_match.index! + (boundary_match[0].length - lower_word.length);
|
|
112
|
+
return {
|
|
113
|
+
score: 60,
|
|
114
|
+
indices: Array.from({ length: word.length }, (_, i) => start + i),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Contains
|
|
119
|
+
const idx = lower_field.indexOf(lower_word);
|
|
120
|
+
if (idx !== -1) {
|
|
121
|
+
return {
|
|
122
|
+
score: 40,
|
|
123
|
+
indices: Array.from({ length: word.length }, (_, i) => idx + i),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Fuzzy
|
|
128
|
+
const fuzzy = fuzzyMatch(word, field);
|
|
129
|
+
if (fuzzy.matched) return { score: fuzzy.score, indices: fuzzy.indices };
|
|
130
|
+
|
|
131
|
+
return { score: 0, indices: [] };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function escapeRegex(str: string): string {
|
|
135
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Build highlighted segments from title and matched character indices */
|
|
139
|
+
function buildSegments(title: string, indices: Set<number>): TextSegment[] {
|
|
140
|
+
if (indices.size === 0) return [{ text: title, highlighted: false }];
|
|
141
|
+
const segments: TextSegment[] = [];
|
|
142
|
+
let current_text = '';
|
|
143
|
+
let current_highlighted = false;
|
|
144
|
+
for (let i = 0; i < title.length; i++) {
|
|
145
|
+
const is_hit = indices.has(i);
|
|
146
|
+
if (is_hit !== current_highlighted && current_text) {
|
|
147
|
+
segments.push({ text: current_text, highlighted: current_highlighted });
|
|
148
|
+
current_text = '';
|
|
149
|
+
}
|
|
150
|
+
current_highlighted = is_hit;
|
|
151
|
+
current_text += title[i];
|
|
152
|
+
}
|
|
153
|
+
if (current_text)
|
|
154
|
+
segments.push({ text: current_text, highlighted: current_highlighted });
|
|
155
|
+
return segments;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Search and score commands against the query */
|
|
159
|
+
function searchCommands(commands: CommandOption[], query: string): ScoredCommand[] {
|
|
160
|
+
const trimmed = query.trim();
|
|
161
|
+
if (!trimmed) return [];
|
|
162
|
+
|
|
163
|
+
const words = trimmed.split(/\s+/).filter(Boolean);
|
|
164
|
+
const results: ScoredCommand[] = [];
|
|
165
|
+
|
|
166
|
+
for (const command of commands) {
|
|
167
|
+
let total_score = 0;
|
|
168
|
+
let all_words_matched = true;
|
|
169
|
+
const title_indices = new Set<number>();
|
|
170
|
+
|
|
171
|
+
for (const word of words) {
|
|
172
|
+
let best_score = 0;
|
|
173
|
+
|
|
174
|
+
// Score against title
|
|
175
|
+
const title_result = scoreField(word, command.title);
|
|
176
|
+
if (title_result.score > best_score) {
|
|
177
|
+
best_score = title_result.score;
|
|
178
|
+
for (const idx of title_result.indices) title_indices.add(idx);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Score against description
|
|
182
|
+
if (command.description) {
|
|
183
|
+
const desc_result = scoreField(word, command.description);
|
|
184
|
+
if (desc_result.score * 0.8 > best_score) {
|
|
185
|
+
best_score = desc_result.score * 0.8;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Score against category
|
|
190
|
+
if (command.category) {
|
|
191
|
+
const cat_result = scoreField(word, command.category);
|
|
192
|
+
if (cat_result.score * 0.6 > best_score) {
|
|
193
|
+
best_score = cat_result.score * 0.6;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Score against keywords
|
|
198
|
+
if (command.keywords) {
|
|
199
|
+
for (const kw of command.keywords) {
|
|
200
|
+
const kw_result = scoreField(word, kw);
|
|
201
|
+
if (kw_result.score * 0.7 > best_score) {
|
|
202
|
+
best_score = kw_result.score * 0.7;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (best_score === 0) {
|
|
208
|
+
all_words_matched = false;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
total_score += best_score;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (all_words_matched && total_score > 0) {
|
|
215
|
+
results.push({
|
|
216
|
+
command,
|
|
217
|
+
score: total_score,
|
|
218
|
+
title_segments: buildSegments(command.title, title_indices),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
results.sort((a, b) => b.score - a.score);
|
|
224
|
+
return results;
|
|
225
|
+
}
|
|
226
|
+
</script>
|
|
227
|
+
|
|
228
|
+
<script lang="ts">
|
|
229
|
+
import { tick } from 'svelte';
|
|
230
|
+
import { fade, scale } from 'svelte/transition';
|
|
231
|
+
import { focusTrap, ripple } from '@delightstack/utilities';
|
|
232
|
+
import { portal } from './Portal.svelte';
|
|
233
|
+
import { scrollbar } from './scrollbar';
|
|
234
|
+
|
|
235
|
+
const propId = $props.id();
|
|
236
|
+
let {
|
|
237
|
+
/** Controls visibility of the command palette */
|
|
238
|
+
open = $bindable(false) as boolean,
|
|
239
|
+
|
|
240
|
+
/** Available commands */
|
|
241
|
+
commands = [] as CommandOption[],
|
|
242
|
+
|
|
243
|
+
/** Input placeholder text */
|
|
244
|
+
placeholder = 'Type a command or search...',
|
|
245
|
+
|
|
246
|
+
/** Maximum number of recent commands to show */
|
|
247
|
+
recent_limit = 5,
|
|
248
|
+
|
|
249
|
+
/** How to group results */
|
|
250
|
+
group_by = 'category' as 'category' | 'none',
|
|
251
|
+
|
|
252
|
+
/** Compact spacing mode */
|
|
253
|
+
dense = false,
|
|
254
|
+
|
|
255
|
+
/** Roomy spacing mode */
|
|
256
|
+
comfortable = false,
|
|
257
|
+
|
|
258
|
+
/** Called when any command is selected */
|
|
259
|
+
onselect = undefined as ((command: CommandOption) => void) | undefined,
|
|
260
|
+
|
|
261
|
+
/** Element ID */
|
|
262
|
+
id = propId,
|
|
263
|
+
|
|
264
|
+
/** Additional CSS classes */
|
|
265
|
+
class: class_name = '',
|
|
266
|
+
} = $props();
|
|
267
|
+
|
|
268
|
+
let query = $state('');
|
|
269
|
+
let selected_index = $state(0);
|
|
270
|
+
// Tracks whether the highlighted row comes from keyboard navigation. When
|
|
271
|
+
// true the cursor row shows a persistent highlight; pointer movement turns
|
|
272
|
+
// it off so the mouse hover (instant-in, fade-out) drives the highlight
|
|
273
|
+
// instead — mirroring the List/ListItem interaction.
|
|
274
|
+
let keyboard_nav = $state(true);
|
|
275
|
+
let is_executing = $state(false);
|
|
276
|
+
let input_el = $state<HTMLInputElement | undefined>(undefined);
|
|
277
|
+
let listbox_el = $state<HTMLElement | undefined>(undefined);
|
|
278
|
+
|
|
279
|
+
const listbox_id = `${id}-listbox`;
|
|
280
|
+
|
|
281
|
+
// Compute search results
|
|
282
|
+
const search_results = $derived(searchCommands(commands, query));
|
|
283
|
+
|
|
284
|
+
// Compute recent commands (when query is empty)
|
|
285
|
+
const recent_commands = $derived.by(() => {
|
|
286
|
+
if (query.trim()) return [];
|
|
287
|
+
return recentCommandIds
|
|
288
|
+
.map((rid) => commands.find((c) => c.id === rid))
|
|
289
|
+
.filter((c): c is CommandOption => c != null)
|
|
290
|
+
.slice(0, recent_limit);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// The flat list of commands currently visible. When there's no query,
|
|
294
|
+
// recent commands are pinned at the top of the full list (not used as
|
|
295
|
+
// a replacement) so users can still browse everything else.
|
|
296
|
+
const visible_commands = $derived.by((): CommandOption[] => {
|
|
297
|
+
if (query.trim()) return search_results.map((r) => r.command);
|
|
298
|
+
if (recent_commands.length > 0) {
|
|
299
|
+
const recent_ids = new Set(recent_commands.map((c) => c.id));
|
|
300
|
+
const rest = commands.filter((c) => !recent_ids.has(c.id));
|
|
301
|
+
return [...recent_commands, ...rest];
|
|
302
|
+
}
|
|
303
|
+
return commands;
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Segments map for highlighting
|
|
307
|
+
const segments_map = $derived.by((): Map<string, TextSegment[]> => {
|
|
308
|
+
const map = new Map<string, TextSegment[]>();
|
|
309
|
+
for (const r of search_results) {
|
|
310
|
+
map.set(r.command.id, r.title_segments);
|
|
311
|
+
}
|
|
312
|
+
return map;
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Grouped commands for display
|
|
316
|
+
interface CommandGroup {
|
|
317
|
+
label: string;
|
|
318
|
+
commands: CommandOption[];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const grouped_commands = $derived.by((): CommandGroup[] => {
|
|
322
|
+
const cmds = visible_commands;
|
|
323
|
+
const has_recent = !query.trim() && recent_commands.length > 0;
|
|
324
|
+
if (group_by === 'none') {
|
|
325
|
+
if (has_recent) {
|
|
326
|
+
const recent_ids = new Set(recent_commands.map((c) => c.id));
|
|
327
|
+
const rest = cmds.filter((c) => !recent_ids.has(c.id));
|
|
328
|
+
const groups: CommandGroup[] = [{ label: 'Recent', commands: recent_commands }];
|
|
329
|
+
if (rest.length) groups.push({ label: '', commands: rest });
|
|
330
|
+
return groups;
|
|
331
|
+
}
|
|
332
|
+
return [{ label: '', commands: cmds }];
|
|
333
|
+
}
|
|
334
|
+
const groups = new Map<string, CommandOption[]>();
|
|
335
|
+
const order: string[] = [];
|
|
336
|
+
// When recents exist, put them in their own pinned group at the top.
|
|
337
|
+
if (has_recent) {
|
|
338
|
+
order.push('__recent__');
|
|
339
|
+
groups.set('__recent__', recent_commands);
|
|
340
|
+
}
|
|
341
|
+
const recent_ids = new Set(recent_commands.map((c) => c.id));
|
|
342
|
+
for (const cmd of cmds) {
|
|
343
|
+
if (has_recent && recent_ids.has(cmd.id)) continue;
|
|
344
|
+
const key = cmd.category || '';
|
|
345
|
+
if (!groups.has(key)) {
|
|
346
|
+
groups.set(key, []);
|
|
347
|
+
order.push(key);
|
|
348
|
+
}
|
|
349
|
+
groups.get(key)!.push(cmd);
|
|
350
|
+
}
|
|
351
|
+
return order.map((key) => ({
|
|
352
|
+
label: key === '__recent__' ? 'Recent' : key,
|
|
353
|
+
commands: groups.get(key)!,
|
|
354
|
+
}));
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// The active descendant ID
|
|
358
|
+
const active_descendant_id = $derived(
|
|
359
|
+
visible_commands[selected_index]
|
|
360
|
+
? `${id}-option-${visible_commands[selected_index].id}`
|
|
361
|
+
: undefined,
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
// Reset selection when results change
|
|
365
|
+
$effect(() => {
|
|
366
|
+
// Subscribe to visible_commands changes
|
|
367
|
+
// oxlint-disable-next-line no-unused-expressions
|
|
368
|
+
visible_commands;
|
|
369
|
+
selected_index = 0;
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Focus input when opened
|
|
373
|
+
$effect(() => {
|
|
374
|
+
if (open) {
|
|
375
|
+
query = '';
|
|
376
|
+
selected_index = 0;
|
|
377
|
+
keyboard_nav = true;
|
|
378
|
+
is_executing = false;
|
|
379
|
+
tick().then(() => input_el?.focus());
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// Global Ctrl/Cmd+K listener (only the active palette in the registry responds)
|
|
384
|
+
$effect(() => {
|
|
385
|
+
const registration = { isOpen: () => open };
|
|
386
|
+
paletteRegistry.push(registration);
|
|
387
|
+
function handleGlobalKeydown(e: KeyboardEvent) {
|
|
388
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
389
|
+
if (activePalette() !== registration) return;
|
|
390
|
+
e.preventDefault();
|
|
391
|
+
open = !open;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
document.addEventListener('keydown', handleGlobalKeydown);
|
|
395
|
+
return () => {
|
|
396
|
+
paletteRegistry = paletteRegistry.filter((r) => r !== registration);
|
|
397
|
+
document.removeEventListener('keydown', handleGlobalKeydown);
|
|
398
|
+
};
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
function close() {
|
|
402
|
+
open = false;
|
|
403
|
+
query = '';
|
|
404
|
+
selected_index = 0;
|
|
405
|
+
is_executing = false;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function scrollSelectedIntoView() {
|
|
409
|
+
tick().then(() => {
|
|
410
|
+
if (!listbox_el) return;
|
|
411
|
+
const active = listbox_el.querySelector(`[id="${active_descendant_id}"]`);
|
|
412
|
+
if (active) active.scrollIntoView({ block: 'nearest' });
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
417
|
+
const count = visible_commands.length;
|
|
418
|
+
if (!count) return;
|
|
419
|
+
|
|
420
|
+
switch (e.key) {
|
|
421
|
+
case 'ArrowDown':
|
|
422
|
+
e.preventDefault();
|
|
423
|
+
keyboard_nav = true;
|
|
424
|
+
selected_index = (selected_index + 1) % count;
|
|
425
|
+
scrollSelectedIntoView();
|
|
426
|
+
break;
|
|
427
|
+
case 'ArrowUp':
|
|
428
|
+
e.preventDefault();
|
|
429
|
+
keyboard_nav = true;
|
|
430
|
+
selected_index = (selected_index - 1 + count) % count;
|
|
431
|
+
scrollSelectedIntoView();
|
|
432
|
+
break;
|
|
433
|
+
case 'Enter':
|
|
434
|
+
e.preventDefault();
|
|
435
|
+
if (
|
|
436
|
+
visible_commands[selected_index] &&
|
|
437
|
+
!visible_commands[selected_index].disabled
|
|
438
|
+
) {
|
|
439
|
+
executeCommand(visible_commands[selected_index]);
|
|
440
|
+
}
|
|
441
|
+
break;
|
|
442
|
+
case 'Escape':
|
|
443
|
+
e.preventDefault();
|
|
444
|
+
close();
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function executeCommand(command: CommandOption, viaPointer = false) {
|
|
450
|
+
if (is_executing || command.disabled) return;
|
|
451
|
+
|
|
452
|
+
trackRecent(command.id, recent_limit);
|
|
453
|
+
onselect?.(command);
|
|
454
|
+
|
|
455
|
+
const result = command.onselect();
|
|
456
|
+
if (result instanceof Promise) {
|
|
457
|
+
is_executing = true;
|
|
458
|
+
try {
|
|
459
|
+
await result;
|
|
460
|
+
close();
|
|
461
|
+
} catch {
|
|
462
|
+
is_executing = false;
|
|
463
|
+
}
|
|
464
|
+
} else if (viaPointer) {
|
|
465
|
+
// Let the click ripple finish animating before the palette unmounts.
|
|
466
|
+
setTimeout(close, 220);
|
|
467
|
+
} else {
|
|
468
|
+
close();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function getSegments(command: CommandOption): TextSegment[] {
|
|
473
|
+
return segments_map.get(command.id) || [{ text: command.title, highlighted: false }];
|
|
474
|
+
}
|
|
475
|
+
</script>
|
|
476
|
+
|
|
477
|
+
{#if open}
|
|
478
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
479
|
+
<div
|
|
480
|
+
class="backdrop"
|
|
481
|
+
role="button"
|
|
482
|
+
tabindex="-1"
|
|
483
|
+
onclick={close}
|
|
484
|
+
in:fade={{ duration: 150 }}
|
|
485
|
+
out:fade={{ duration: 100 }}
|
|
486
|
+
use:portal>
|
|
487
|
+
</div>
|
|
488
|
+
<div
|
|
489
|
+
{id}
|
|
490
|
+
class={['palette', class_name].filter(Boolean).join(' ')}
|
|
491
|
+
class:dense
|
|
492
|
+
class:comfortable
|
|
493
|
+
role="dialog"
|
|
494
|
+
aria-modal="true"
|
|
495
|
+
aria-label="Command palette"
|
|
496
|
+
in:scale={{ duration: 150, start: 0.95, opacity: 0 }}
|
|
497
|
+
out:scale={{ duration: 100, start: 0.95, opacity: 0 }}
|
|
498
|
+
{@attach focusTrap({
|
|
499
|
+
escapeDeactivates: false,
|
|
500
|
+
allowOutsideClick: true,
|
|
501
|
+
returnFocusOnDeactivate: true,
|
|
502
|
+
initialFocus: false,
|
|
503
|
+
})}
|
|
504
|
+
use:portal>
|
|
505
|
+
<div class="input-wrapper">
|
|
506
|
+
<!-- Search icon -->
|
|
507
|
+
<svg
|
|
508
|
+
class="search-icon"
|
|
509
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
510
|
+
viewBox="0 0 24 24"
|
|
511
|
+
width="20"
|
|
512
|
+
height="20"
|
|
513
|
+
fill="none"
|
|
514
|
+
stroke="currentColor"
|
|
515
|
+
stroke-width="2"
|
|
516
|
+
stroke-linecap="round"
|
|
517
|
+
stroke-linejoin="round">
|
|
518
|
+
<circle cx="11" cy="11" r="8" />
|
|
519
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
520
|
+
</svg>
|
|
521
|
+
<input
|
|
522
|
+
bind:this={input_el}
|
|
523
|
+
bind:value={query}
|
|
524
|
+
type="text"
|
|
525
|
+
{placeholder}
|
|
526
|
+
role="combobox"
|
|
527
|
+
autocomplete="off"
|
|
528
|
+
aria-expanded={visible_commands.length > 0}
|
|
529
|
+
aria-controls={listbox_id}
|
|
530
|
+
aria-activedescendant={active_descendant_id}
|
|
531
|
+
aria-autocomplete="list"
|
|
532
|
+
onkeydown={handleKeydown} />
|
|
533
|
+
{#if is_executing}
|
|
534
|
+
<div class="spinner">
|
|
535
|
+
<svg
|
|
536
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
537
|
+
viewBox="0 0 24 24"
|
|
538
|
+
width="18"
|
|
539
|
+
height="18"
|
|
540
|
+
fill="none"
|
|
541
|
+
stroke="currentColor"
|
|
542
|
+
stroke-width="2.5"
|
|
543
|
+
stroke-linecap="round">
|
|
544
|
+
<path d="M12 2a10 10 0 0 1 10 10" />
|
|
545
|
+
</svg>
|
|
546
|
+
</div>
|
|
547
|
+
{/if}
|
|
548
|
+
</div>
|
|
549
|
+
|
|
550
|
+
<div
|
|
551
|
+
bind:this={listbox_el}
|
|
552
|
+
id={listbox_id}
|
|
553
|
+
role="listbox"
|
|
554
|
+
aria-label="Commands"
|
|
555
|
+
class="results"
|
|
556
|
+
{@attach scrollbar()}>
|
|
557
|
+
{#if visible_commands.length === 0}
|
|
558
|
+
<div class="empty">No results found</div>
|
|
559
|
+
{:else}
|
|
560
|
+
{#each grouped_commands as group}
|
|
561
|
+
{#if group.label}
|
|
562
|
+
<div class="group-header" role="presentation">
|
|
563
|
+
{group.label}
|
|
564
|
+
</div>
|
|
565
|
+
{/if}
|
|
566
|
+
{#each group.commands as command, group_i (command.id)}
|
|
567
|
+
{@const flat_index = visible_commands.indexOf(command)}
|
|
568
|
+
{@const is_selected = flat_index === selected_index}
|
|
569
|
+
{@const option_id = `${id}-option-${command.id}`}
|
|
570
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
571
|
+
<div
|
|
572
|
+
id={option_id}
|
|
573
|
+
role="option"
|
|
574
|
+
tabindex="-1"
|
|
575
|
+
aria-selected={is_selected}
|
|
576
|
+
aria-disabled={command.disabled || false}
|
|
577
|
+
class="item"
|
|
578
|
+
class:selected={is_selected && keyboard_nav}
|
|
579
|
+
class:first-in-group={group_i === 0}
|
|
580
|
+
class:last-in-group={group_i === group.commands.length - 1}
|
|
581
|
+
class:disabled={command.disabled}
|
|
582
|
+
onpointerenter={() => {
|
|
583
|
+
keyboard_nav = false;
|
|
584
|
+
selected_index = flat_index;
|
|
585
|
+
}}
|
|
586
|
+
onclick={() => {
|
|
587
|
+
if (!command.disabled) executeCommand(command, true);
|
|
588
|
+
}}
|
|
589
|
+
{@attach ripple({ enabled: !command.disabled, zIndex: 1 })}>
|
|
590
|
+
{#if command.icon}
|
|
591
|
+
<span class="icon">
|
|
592
|
+
<command.icon />
|
|
593
|
+
</span>
|
|
594
|
+
{/if}
|
|
595
|
+
<div class="content">
|
|
596
|
+
<span class="title">
|
|
597
|
+
{#each getSegments(command) as segment}
|
|
598
|
+
{#if segment.highlighted}
|
|
599
|
+
<mark>{segment.text}</mark>
|
|
600
|
+
{:else}
|
|
601
|
+
{segment.text}
|
|
602
|
+
{/if}
|
|
603
|
+
{/each}
|
|
604
|
+
</span>
|
|
605
|
+
{#if command.description}
|
|
606
|
+
<span class="description">{command.description}</span>
|
|
607
|
+
{/if}
|
|
608
|
+
</div>
|
|
609
|
+
{#if command.shortcut && command.shortcut.length > 0}
|
|
610
|
+
<div class="shortcut">
|
|
611
|
+
{#each command.shortcut as key}
|
|
612
|
+
<kbd>{key}</kbd>
|
|
613
|
+
{/each}
|
|
614
|
+
</div>
|
|
615
|
+
{/if}
|
|
616
|
+
</div>
|
|
617
|
+
{/each}
|
|
618
|
+
{/each}
|
|
619
|
+
{/if}
|
|
620
|
+
</div>
|
|
621
|
+
</div>
|
|
622
|
+
{/if}
|
|
623
|
+
|
|
624
|
+
<style>
|
|
625
|
+
.backdrop {
|
|
626
|
+
position: fixed;
|
|
627
|
+
top: 0;
|
|
628
|
+
left: 0;
|
|
629
|
+
right: 0;
|
|
630
|
+
bottom: 0;
|
|
631
|
+
z-index: var(--layer-popover);
|
|
632
|
+
backdrop-filter: blur(8px);
|
|
633
|
+
|
|
634
|
+
&::after {
|
|
635
|
+
content: '';
|
|
636
|
+
position: absolute;
|
|
637
|
+
top: 0;
|
|
638
|
+
left: 0;
|
|
639
|
+
right: 0;
|
|
640
|
+
bottom: 0;
|
|
641
|
+
background-color: var(--color-text);
|
|
642
|
+
opacity: 0.15;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.palette {
|
|
647
|
+
/* Clamp so an over-rounded --radius-xl can't blob this large panel — see --radius-cap. */
|
|
648
|
+
--_radius: min(var(--radius-xl), var(--radius-cap, 40px));
|
|
649
|
+
position: fixed;
|
|
650
|
+
top: 20vh;
|
|
651
|
+
left: 50%;
|
|
652
|
+
transform: translateX(-50%);
|
|
653
|
+
z-index: var(--layer-popover);
|
|
654
|
+
width: min(600px, 90vw);
|
|
655
|
+
background-color: var(--color-bg);
|
|
656
|
+
border-radius: var(--_radius);
|
|
657
|
+
@supports (corner-shape: squircle) {
|
|
658
|
+
corner-shape: squircle;
|
|
659
|
+
border-radius: calc(var(--_radius) * var(--squircle-ratio, 2));
|
|
660
|
+
}
|
|
661
|
+
box-shadow: var(--shadow-lg);
|
|
662
|
+
border: 1px solid var(--color-border);
|
|
663
|
+
display: flex;
|
|
664
|
+
flex-direction: column;
|
|
665
|
+
overflow: hidden;
|
|
666
|
+
&.dense {
|
|
667
|
+
--_radius: calc(var(--radius-lg) * 1.5);
|
|
668
|
+
}
|
|
669
|
+
&.comfortable {
|
|
670
|
+
--_radius: var(--radius-xl);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.input-wrapper {
|
|
675
|
+
display: flex;
|
|
676
|
+
align-items: center;
|
|
677
|
+
padding: 0.75rem 1rem;
|
|
678
|
+
border-bottom: 1px solid var(--color-border);
|
|
679
|
+
gap: 0.75rem;
|
|
680
|
+
|
|
681
|
+
.dense & {
|
|
682
|
+
padding: 0.5rem 0.75rem;
|
|
683
|
+
gap: 0.5rem;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
.comfortable & {
|
|
687
|
+
padding: 1rem 1.25rem;
|
|
688
|
+
gap: 1rem;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.search-icon {
|
|
693
|
+
flex-shrink: 0;
|
|
694
|
+
color: var(--color-text-muted);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
input {
|
|
698
|
+
flex: 1;
|
|
699
|
+
border: none;
|
|
700
|
+
outline: none;
|
|
701
|
+
background: transparent;
|
|
702
|
+
color: var(--color-text);
|
|
703
|
+
font-size: 1rem;
|
|
704
|
+
line-height: 1.5;
|
|
705
|
+
padding: 0;
|
|
706
|
+
|
|
707
|
+
&::placeholder {
|
|
708
|
+
color: var(--color-text-muted);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.dense & {
|
|
712
|
+
font-size: 0.875rem;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
.comfortable & {
|
|
716
|
+
font-size: 1.0625rem;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
.spinner {
|
|
721
|
+
flex-shrink: 0;
|
|
722
|
+
display: flex;
|
|
723
|
+
align-items: center;
|
|
724
|
+
color: var(--color-text-muted);
|
|
725
|
+
animation: spin 0.8s linear infinite;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
@keyframes spin {
|
|
729
|
+
to {
|
|
730
|
+
transform: rotate(360deg);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
.results {
|
|
735
|
+
max-height: 400px;
|
|
736
|
+
overflow-y: auto;
|
|
737
|
+
overscroll-behavior: contain;
|
|
738
|
+
padding: 0.5rem 0;
|
|
739
|
+
|
|
740
|
+
.dense & {
|
|
741
|
+
padding: 0.25rem 0;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
.comfortable & {
|
|
745
|
+
padding: 0.75rem 0;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
.empty {
|
|
750
|
+
padding: 2rem 1rem;
|
|
751
|
+
text-align: center;
|
|
752
|
+
color: var(--color-text-muted);
|
|
753
|
+
font-size: 0.875rem;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
.group-header {
|
|
757
|
+
padding: 0.5rem 1rem 0.25rem;
|
|
758
|
+
font-size: 0.7rem;
|
|
759
|
+
font-weight: 600;
|
|
760
|
+
text-transform: uppercase;
|
|
761
|
+
letter-spacing: 0.05em;
|
|
762
|
+
color: var(--color-text-muted);
|
|
763
|
+
|
|
764
|
+
.dense & {
|
|
765
|
+
padding: 0.375rem 0.75rem 0.125rem;
|
|
766
|
+
font-size: 0.65rem;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
.comfortable & {
|
|
770
|
+
padding: 0.625rem 1.25rem 0.375rem;
|
|
771
|
+
font-size: 0.72rem;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
.item {
|
|
776
|
+
--_radius: var(--radius-lg);
|
|
777
|
+
position: relative;
|
|
778
|
+
display: flex;
|
|
779
|
+
align-items: center;
|
|
780
|
+
padding: 0.6rem 0.85rem;
|
|
781
|
+
margin: 0 0.5rem;
|
|
782
|
+
gap: 0.75rem;
|
|
783
|
+
cursor: pointer;
|
|
784
|
+
/* Square by default; the corners are rounded per group (top item rounded
|
|
785
|
+
* on top, last item rounded on the bottom) so each category reads as a
|
|
786
|
+
* connected block — the same idea as List/ListItem. */
|
|
787
|
+
border-radius: 0;
|
|
788
|
+
overflow: hidden;
|
|
789
|
+
transition: transform 200ms ease;
|
|
790
|
+
|
|
791
|
+
/* Hover/selection highlight. The base overlay fades (300ms); on hover it
|
|
792
|
+
* appears instantly and fades away on leave — matching ListItem. The
|
|
793
|
+
* keyboard cursor (.selected) uses the gentle fade in both directions. */
|
|
794
|
+
&::before {
|
|
795
|
+
content: '';
|
|
796
|
+
position: absolute;
|
|
797
|
+
inset: 0;
|
|
798
|
+
background-color: var(--color-text);
|
|
799
|
+
border-radius: inherit;
|
|
800
|
+
@supports (corner-shape: squircle) {
|
|
801
|
+
corner-shape: inherit;
|
|
802
|
+
}
|
|
803
|
+
opacity: 0;
|
|
804
|
+
transition: opacity 300ms ease;
|
|
805
|
+
pointer-events: none;
|
|
806
|
+
}
|
|
807
|
+
&.selected::before {
|
|
808
|
+
opacity: 0.06;
|
|
809
|
+
}
|
|
810
|
+
&:hover:not(.disabled)::before {
|
|
811
|
+
opacity: 0.06;
|
|
812
|
+
transition-duration: 0ms;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
&.first-in-group {
|
|
816
|
+
border-top-left-radius: var(--_radius, 8px);
|
|
817
|
+
border-top-right-radius: var(--_radius, 8px);
|
|
818
|
+
@supports (corner-shape: squircle) {
|
|
819
|
+
corner-shape: squircle;
|
|
820
|
+
border-top-left-radius: calc(var(--_radius, 8px) * var(--squircle-ratio, 2));
|
|
821
|
+
border-top-right-radius: calc(var(--_radius, 8px) * var(--squircle-ratio, 2));
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
&.last-in-group {
|
|
825
|
+
border-bottom-left-radius: var(--_radius, 8px);
|
|
826
|
+
border-bottom-right-radius: var(--_radius, 8px);
|
|
827
|
+
@supports (corner-shape: squircle) {
|
|
828
|
+
corner-shape: squircle;
|
|
829
|
+
border-bottom-left-radius: calc(var(--_radius, 8px) * var(--squircle-ratio, 2));
|
|
830
|
+
border-bottom-right-radius: calc(var(--_radius, 8px) * var(--squircle-ratio, 2));
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/* Per-item perspective so the press recedes toward the item's own
|
|
835
|
+
* center rather than the list's center (container perspective would
|
|
836
|
+
* share one vanishing point across every row). */
|
|
837
|
+
&:active:not(.disabled) {
|
|
838
|
+
transform: perspective(100px)
|
|
839
|
+
translate3d(0px, 1px, clamp(-10px, calc(0.2em - 12px), -2px));
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
&.disabled {
|
|
843
|
+
opacity: 0.4;
|
|
844
|
+
cursor: not-allowed;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
.dense & {
|
|
848
|
+
padding: 0.45rem 0.7rem;
|
|
849
|
+
gap: 0.5rem;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
.comfortable & {
|
|
853
|
+
padding: 0.85rem 1.1rem;
|
|
854
|
+
gap: 0.85rem;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
.icon {
|
|
858
|
+
flex-shrink: 0;
|
|
859
|
+
display: flex;
|
|
860
|
+
align-items: center;
|
|
861
|
+
justify-content: center;
|
|
862
|
+
width: 1.25rem;
|
|
863
|
+
height: 1.25rem;
|
|
864
|
+
color: var(--color-text-muted);
|
|
865
|
+
|
|
866
|
+
:global(svg) {
|
|
867
|
+
width: 100%;
|
|
868
|
+
height: 100%;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
.content {
|
|
873
|
+
flex: 1;
|
|
874
|
+
min-width: 0;
|
|
875
|
+
display: flex;
|
|
876
|
+
flex-direction: column;
|
|
877
|
+
gap: 0.125rem;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
.title {
|
|
881
|
+
font-size: 0.875rem;
|
|
882
|
+
color: var(--color-text);
|
|
883
|
+
white-space: nowrap;
|
|
884
|
+
overflow: hidden;
|
|
885
|
+
text-overflow: ellipsis;
|
|
886
|
+
|
|
887
|
+
:global(mark) {
|
|
888
|
+
background-color: light-dark(rgba(255, 200, 0, 0.35), rgba(255, 200, 0, 0.25));
|
|
889
|
+
color: inherit;
|
|
890
|
+
border-radius: 2px;
|
|
891
|
+
padding: 0 1px;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
.description {
|
|
896
|
+
font-size: 0.75rem;
|
|
897
|
+
color: var(--color-text-muted);
|
|
898
|
+
white-space: nowrap;
|
|
899
|
+
overflow: hidden;
|
|
900
|
+
text-overflow: ellipsis;
|
|
901
|
+
|
|
902
|
+
.dense & {
|
|
903
|
+
font-size: 0.7rem;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
.comfortable & {
|
|
907
|
+
font-size: 0.78rem;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
.shortcut {
|
|
912
|
+
flex-shrink: 0;
|
|
913
|
+
display: flex;
|
|
914
|
+
align-items: center;
|
|
915
|
+
gap: 0.25rem;
|
|
916
|
+
|
|
917
|
+
kbd {
|
|
918
|
+
display: inline-flex;
|
|
919
|
+
align-items: center;
|
|
920
|
+
justify-content: center;
|
|
921
|
+
min-width: 1.5em;
|
|
922
|
+
height: 1.5em;
|
|
923
|
+
padding: 0 0.35em;
|
|
924
|
+
font-family: inherit;
|
|
925
|
+
font-size: 0.7rem;
|
|
926
|
+
font-weight: 500;
|
|
927
|
+
color: var(--color-text-muted);
|
|
928
|
+
background-color: light-dark(rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.08));
|
|
929
|
+
border: 1px solid var(--color-border);
|
|
930
|
+
border-radius: var(--radius-xl);
|
|
931
|
+
@supports (corner-shape: squircle) {
|
|
932
|
+
corner-shape: squircle;
|
|
933
|
+
border-radius: calc(var(--radius-xl) * var(--squircle-ratio, 2));
|
|
934
|
+
}
|
|
935
|
+
line-height: 1;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
</style>
|