@aiaiai-pt/design-system 0.3.3 → 0.3.5
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/components/Alert.svelte +3 -3
- package/components/BottomNavItem.svelte +2 -2
- package/components/Button.svelte +2 -2
- package/components/CardGrid.svelte +104 -0
- package/components/Checkbox.svelte +2 -2
- package/components/Combobox.svelte +1 -1
- package/components/CommandPalette.svelte +605 -0
- package/components/ConditionTable.svelte +12 -1
- package/components/FileUploadItem.svelte +3 -3
- package/components/FilterBar.svelte +255 -0
- package/components/Input.svelte +3 -3
- package/components/LogViewer.svelte +410 -0
- package/components/Modal.svelte +2 -2
- package/components/PageContainer.svelte +74 -0
- package/components/SearchInput.svelte +473 -0
- package/components/Select.svelte +14 -2
- package/components/SidebarItem.svelte +2 -2
- package/components/Stepper.svelte +2 -2
- package/components/index.js +7 -0
- package/package.json +1 -1
- package/tokens/base.css +8 -0
- package/tokens/components.css +131 -9
- package/tokens/semantic.css +17 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component CommandPalette
|
|
3
|
+
|
|
4
|
+
Modal search + command launcher triggered by keyboard shortcut.
|
|
5
|
+
Supports built-in fuzzy scoring, grouped results, keyboard navigation,
|
|
6
|
+
and both declarative (sections prop) and composable (children) APIs.
|
|
7
|
+
Consumes --palette-* tokens from components.css.
|
|
8
|
+
|
|
9
|
+
@example Declarative
|
|
10
|
+
<CommandPalette
|
|
11
|
+
bind:open={paletteOpen}
|
|
12
|
+
trigger="mod+k"
|
|
13
|
+
placeholder="Type a command or search..."
|
|
14
|
+
sections={[
|
|
15
|
+
{ heading: 'Recent', items: [
|
|
16
|
+
{ value: 'pipeline-1', label: 'Customer ETL', onselect: () => goto('/pipelines/1') },
|
|
17
|
+
]},
|
|
18
|
+
{ heading: 'Actions', items: [
|
|
19
|
+
{ value: 'new-pipeline', label: 'Create pipeline', shortcut: '⌘N', onselect: createPipeline },
|
|
20
|
+
]},
|
|
21
|
+
]}
|
|
22
|
+
/>
|
|
23
|
+
|
|
24
|
+
@example Composable
|
|
25
|
+
<CommandPalette.Root bind:open={paletteOpen} trigger="mod+k">
|
|
26
|
+
<CommandPalette.Input placeholder="Search..." />
|
|
27
|
+
<CommandPalette.List>
|
|
28
|
+
<CommandPalette.Group heading="Recent">
|
|
29
|
+
<CommandPalette.Item value="pipeline-1" onselect={...}>Customer ETL</CommandPalette.Item>
|
|
30
|
+
</CommandPalette.Group>
|
|
31
|
+
<CommandPalette.Empty>No results found</CommandPalette.Empty>
|
|
32
|
+
</CommandPalette.List>
|
|
33
|
+
</CommandPalette.Root>
|
|
34
|
+
-->
|
|
35
|
+
<script module>
|
|
36
|
+
let _paletteUid = 0;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Simple fuzzy score: returns 0-1 for how well query matches text.
|
|
40
|
+
* 0 = no match, 1 = exact prefix match. Based on command-score heuristics.
|
|
41
|
+
* @param {string} text
|
|
42
|
+
* @param {string} query
|
|
43
|
+
* @returns {number}
|
|
44
|
+
*/
|
|
45
|
+
export function commandScore(text, query) {
|
|
46
|
+
if (!query) return 1;
|
|
47
|
+
const lower = text.toLowerCase();
|
|
48
|
+
const q = query.toLowerCase();
|
|
49
|
+
|
|
50
|
+
// Exact prefix
|
|
51
|
+
if (lower.startsWith(q)) return 1;
|
|
52
|
+
|
|
53
|
+
// Contains as substring
|
|
54
|
+
const idx = lower.indexOf(q);
|
|
55
|
+
if (idx >= 0) {
|
|
56
|
+
// Prefer word boundary matches
|
|
57
|
+
if (lower[idx - 1] === ' ' || lower[idx - 1] === '-' || lower[idx - 1] === '/') {
|
|
58
|
+
return 0.8;
|
|
59
|
+
}
|
|
60
|
+
return 0.6;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Character-by-character fuzzy match
|
|
64
|
+
let qi = 0;
|
|
65
|
+
let matched = 0;
|
|
66
|
+
for (let i = 0; i < lower.length && qi < q.length; i++) {
|
|
67
|
+
if (lower[i] === q[qi]) {
|
|
68
|
+
matched++;
|
|
69
|
+
qi++;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (qi < q.length) return 0; // Not all query chars found
|
|
74
|
+
return (matched / text.length) * 0.4;
|
|
75
|
+
}
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<script>
|
|
79
|
+
/**
|
|
80
|
+
* @typedef {{ value: string, label: string, description?: string, shortcut?: string, keywords?: string[], disabled?: boolean, onselect?: (value: string) => void }} PaletteItem
|
|
81
|
+
* @typedef {{ heading: string, items: PaletteItem[] }} PaletteSection
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
let {
|
|
85
|
+
/** @type {boolean} */
|
|
86
|
+
open = $bindable(false),
|
|
87
|
+
/** @type {string} Keyboard shortcut trigger. "mod" = ⌘ on Mac, Ctrl on Windows. */
|
|
88
|
+
trigger = 'mod+k',
|
|
89
|
+
/** @type {string} */
|
|
90
|
+
placeholder = 'Type a command or search...',
|
|
91
|
+
/** @type {PaletteSection[]} Declarative sections and items */
|
|
92
|
+
sections = [],
|
|
93
|
+
/** @type {boolean} Use built-in fuzzy scoring */
|
|
94
|
+
shouldFilter = true,
|
|
95
|
+
/** @type {boolean} Arrow keys wrap around the list */
|
|
96
|
+
loop = true,
|
|
97
|
+
/** @type {((open: boolean) => void) | undefined} */
|
|
98
|
+
onopenchange = undefined,
|
|
99
|
+
/** @type {string} */
|
|
100
|
+
class: className = '',
|
|
101
|
+
/** @type {import('svelte').Snippet | undefined} */
|
|
102
|
+
children = undefined,
|
|
103
|
+
...rest
|
|
104
|
+
} = $props();
|
|
105
|
+
|
|
106
|
+
const paletteId = `palette-${_paletteUid++}`;
|
|
107
|
+
const inputId = `${paletteId}-input`;
|
|
108
|
+
|
|
109
|
+
let query = $state('');
|
|
110
|
+
let activeIndex = $state(0);
|
|
111
|
+
/** @type {HTMLInputElement | undefined} */
|
|
112
|
+
let inputEl = $state();
|
|
113
|
+
/** @type {HTMLElement | undefined} */
|
|
114
|
+
let listEl = $state();
|
|
115
|
+
|
|
116
|
+
// Parse trigger shortcut
|
|
117
|
+
const triggerParts = $derived(trigger.split('+'));
|
|
118
|
+
const triggerKey = $derived(triggerParts[triggerParts.length - 1].toLowerCase());
|
|
119
|
+
const triggerMod = $derived(triggerParts.includes('mod'));
|
|
120
|
+
const triggerShift = $derived(triggerParts.includes('shift'));
|
|
121
|
+
const triggerAlt = $derived(triggerParts.includes('alt'));
|
|
122
|
+
|
|
123
|
+
// Filter items by query
|
|
124
|
+
const filteredSections = $derived.by(() => {
|
|
125
|
+
if (!shouldFilter || !query) return sections;
|
|
126
|
+
|
|
127
|
+
return sections
|
|
128
|
+
.map(section => {
|
|
129
|
+
const scored = section.items
|
|
130
|
+
.map(item => {
|
|
131
|
+
const textScore = commandScore(item.label, query);
|
|
132
|
+
const descScore = item.description ? commandScore(item.description, query) * 0.5 : 0;
|
|
133
|
+
const kwScore = item.keywords
|
|
134
|
+
? Math.max(...item.keywords.map(kw => commandScore(kw, query))) * 0.7
|
|
135
|
+
: 0;
|
|
136
|
+
return { item, score: Math.max(textScore, descScore, kwScore) };
|
|
137
|
+
})
|
|
138
|
+
.filter(({ score }) => score > 0)
|
|
139
|
+
.sort((a, b) => b.score - a.score);
|
|
140
|
+
|
|
141
|
+
return { heading: section.heading, items: scored.map(s => s.item) };
|
|
142
|
+
})
|
|
143
|
+
.filter(section => section.items.length > 0);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Flat list of all visible items for keyboard navigation
|
|
147
|
+
const flatItems = $derived(filteredSections.flatMap(s => s.items));
|
|
148
|
+
const totalItems = $derived(flatItems.length);
|
|
149
|
+
const isEmpty = $derived(query.length > 0 && totalItems === 0);
|
|
150
|
+
|
|
151
|
+
// Reset active index when results change
|
|
152
|
+
$effect(() => {
|
|
153
|
+
// Access flatItems to create dependency
|
|
154
|
+
flatItems;
|
|
155
|
+
activeIndex = 0;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
function openPalette() {
|
|
159
|
+
open = true;
|
|
160
|
+
query = '';
|
|
161
|
+
activeIndex = 0;
|
|
162
|
+
onopenchange?.(true);
|
|
163
|
+
requestAnimationFrame(() => inputEl?.focus());
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function closePalette() {
|
|
167
|
+
open = false;
|
|
168
|
+
query = '';
|
|
169
|
+
onopenchange?.(false);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function selectItem(/** @type {PaletteItem} */ item) {
|
|
173
|
+
if (item.disabled) return;
|
|
174
|
+
closePalette();
|
|
175
|
+
item.onselect?.(item.value);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** @param {KeyboardEvent} e */
|
|
179
|
+
function handleInputKeydown(e) {
|
|
180
|
+
switch (e.key) {
|
|
181
|
+
case 'ArrowDown':
|
|
182
|
+
e.preventDefault();
|
|
183
|
+
if (loop) {
|
|
184
|
+
activeIndex = (activeIndex + 1) % Math.max(totalItems, 1);
|
|
185
|
+
} else {
|
|
186
|
+
activeIndex = Math.min(activeIndex + 1, totalItems - 1);
|
|
187
|
+
}
|
|
188
|
+
scrollActiveIntoView();
|
|
189
|
+
break;
|
|
190
|
+
case 'ArrowUp':
|
|
191
|
+
e.preventDefault();
|
|
192
|
+
if (loop) {
|
|
193
|
+
activeIndex = activeIndex <= 0 ? Math.max(totalItems - 1, 0) : activeIndex - 1;
|
|
194
|
+
} else {
|
|
195
|
+
activeIndex = Math.max(activeIndex - 1, 0);
|
|
196
|
+
}
|
|
197
|
+
scrollActiveIntoView();
|
|
198
|
+
break;
|
|
199
|
+
case 'Enter':
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
if (activeIndex >= 0 && activeIndex < totalItems) {
|
|
202
|
+
selectItem(flatItems[activeIndex]);
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
case 'Escape':
|
|
206
|
+
e.preventDefault();
|
|
207
|
+
closePalette();
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function scrollActiveIntoView() {
|
|
213
|
+
requestAnimationFrame(() => {
|
|
214
|
+
const el = listEl?.querySelector(`[data-palette-index="${activeIndex}"]`);
|
|
215
|
+
el?.scrollIntoView({ block: 'nearest' });
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Highlight matching text
|
|
221
|
+
* @param {string} text
|
|
222
|
+
* @param {string} q
|
|
223
|
+
* @returns {{ before: string, match: string, after: string } | null}
|
|
224
|
+
*/
|
|
225
|
+
function getHighlight(text, q) {
|
|
226
|
+
if (!q) return null;
|
|
227
|
+
const lower = text.toLowerCase();
|
|
228
|
+
const idx = lower.indexOf(q.toLowerCase());
|
|
229
|
+
if (idx === -1) return null;
|
|
230
|
+
return {
|
|
231
|
+
before: text.slice(0, idx),
|
|
232
|
+
match: text.slice(idx, idx + q.length),
|
|
233
|
+
after: text.slice(idx + q.length),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Global keyboard shortcut listener
|
|
238
|
+
$effect(() => {
|
|
239
|
+
/** @param {KeyboardEvent} e */
|
|
240
|
+
function handleGlobalKeydown(e) {
|
|
241
|
+
const modMatch = triggerMod ? (e.metaKey || e.ctrlKey) : true;
|
|
242
|
+
const shiftMatch = triggerShift ? e.shiftKey : !e.shiftKey;
|
|
243
|
+
const altMatch = triggerAlt ? e.altKey : !e.altKey;
|
|
244
|
+
|
|
245
|
+
if (modMatch && shiftMatch && altMatch && e.key.toLowerCase() === triggerKey) {
|
|
246
|
+
e.preventDefault();
|
|
247
|
+
if (open) {
|
|
248
|
+
closePalette();
|
|
249
|
+
} else {
|
|
250
|
+
openPalette();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
document.addEventListener('keydown', handleGlobalKeydown);
|
|
256
|
+
return () => document.removeEventListener('keydown', handleGlobalKeydown);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Precomputed index map: item → global index (M6 fix)
|
|
260
|
+
const itemIndexMap = $derived(new Map(flatItems.map((item, i) => [item, i])));
|
|
261
|
+
|
|
262
|
+
// Focus trap + scroll lock when open (C1 fix)
|
|
263
|
+
/** @type {HTMLElement | undefined} */
|
|
264
|
+
let paletteEl = $state();
|
|
265
|
+
const FOCUSABLE = 'input, button, [tabindex]:not([tabindex="-1"])';
|
|
266
|
+
|
|
267
|
+
$effect(() => {
|
|
268
|
+
if (!open || !paletteEl) return;
|
|
269
|
+
document.body.style.overflow = 'hidden';
|
|
270
|
+
|
|
271
|
+
/** @param {KeyboardEvent} e */
|
|
272
|
+
function handleTrapKeydown(e) {
|
|
273
|
+
if (e.key !== 'Tab') return;
|
|
274
|
+
const focusable = /** @type {NodeListOf<HTMLElement>} */ (paletteEl?.querySelectorAll(FOCUSABLE));
|
|
275
|
+
if (!focusable?.length) return;
|
|
276
|
+
|
|
277
|
+
const first = focusable[0];
|
|
278
|
+
const last = focusable[focusable.length - 1];
|
|
279
|
+
|
|
280
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
281
|
+
e.preventDefault();
|
|
282
|
+
last.focus();
|
|
283
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
284
|
+
e.preventDefault();
|
|
285
|
+
first.focus();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
document.addEventListener('keydown', handleTrapKeydown);
|
|
290
|
+
return () => {
|
|
291
|
+
document.removeEventListener('keydown', handleTrapKeydown);
|
|
292
|
+
document.body.style.overflow = '';
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
</script>
|
|
296
|
+
|
|
297
|
+
{#if open}
|
|
298
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
299
|
+
<div
|
|
300
|
+
class="palette-backdrop"
|
|
301
|
+
onclick={closePalette}
|
|
302
|
+
aria-hidden="true"
|
|
303
|
+
role="presentation"
|
|
304
|
+
></div>
|
|
305
|
+
|
|
306
|
+
<div class="palette-container">
|
|
307
|
+
<div
|
|
308
|
+
bind:this={paletteEl}
|
|
309
|
+
class="palette {className}"
|
|
310
|
+
role="dialog"
|
|
311
|
+
aria-modal="true"
|
|
312
|
+
aria-label="Command palette"
|
|
313
|
+
{...rest}
|
|
314
|
+
>
|
|
315
|
+
{#if children}
|
|
316
|
+
{@render children()}
|
|
317
|
+
{:else}
|
|
318
|
+
<!-- Declarative API -->
|
|
319
|
+
<div class="palette-input-wrap">
|
|
320
|
+
<span class="palette-input-icon" aria-hidden="true">
|
|
321
|
+
<svg class="palette-icon" viewBox="0 0 256 256" fill="none">
|
|
322
|
+
<circle cx="115.5" cy="115.5" r="67.5" stroke="currentColor" stroke-width="16" fill="none"/>
|
|
323
|
+
<line x1="164.2" y1="164.2" x2="224" y2="224" stroke="currentColor" stroke-width="16" stroke-linecap="round"/>
|
|
324
|
+
</svg>
|
|
325
|
+
</span>
|
|
326
|
+
<input
|
|
327
|
+
bind:this={inputEl}
|
|
328
|
+
id={inputId}
|
|
329
|
+
type="text"
|
|
330
|
+
class="palette-input"
|
|
331
|
+
{placeholder}
|
|
332
|
+
bind:value={query}
|
|
333
|
+
onkeydown={handleInputKeydown}
|
|
334
|
+
autocomplete="off"
|
|
335
|
+
spellcheck="false"
|
|
336
|
+
role="combobox"
|
|
337
|
+
aria-expanded={totalItems > 0}
|
|
338
|
+
aria-controls="{paletteId}-list"
|
|
339
|
+
aria-activedescendant={totalItems > 0 ? `${paletteId}-item-${activeIndex}` : undefined}
|
|
340
|
+
/>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
<div
|
|
344
|
+
bind:this={listEl}
|
|
345
|
+
id="{paletteId}-list"
|
|
346
|
+
class="palette-list"
|
|
347
|
+
role="listbox"
|
|
348
|
+
aria-label="Command palette results"
|
|
349
|
+
>
|
|
350
|
+
{#if isEmpty}
|
|
351
|
+
<div class="palette-empty" role="status">
|
|
352
|
+
No results for "{query}"
|
|
353
|
+
</div>
|
|
354
|
+
{:else}
|
|
355
|
+
{#each filteredSections as section}
|
|
356
|
+
<div class="palette-group" role="group" aria-label={section.heading}>
|
|
357
|
+
<div class="palette-group-heading">{section.heading}</div>
|
|
358
|
+
{#each section.items as item}
|
|
359
|
+
{@const globalIndex = itemIndexMap.get(item) ?? -1}
|
|
360
|
+
{@const isActive = globalIndex === activeIndex}
|
|
361
|
+
{@const hl = getHighlight(item.label, query)}
|
|
362
|
+
<div
|
|
363
|
+
id="{paletteId}-item-{globalIndex}"
|
|
364
|
+
class="palette-item"
|
|
365
|
+
class:palette-item-active={isActive}
|
|
366
|
+
class:palette-item-disabled={item.disabled}
|
|
367
|
+
role="option"
|
|
368
|
+
aria-selected={isActive}
|
|
369
|
+
aria-disabled={item.disabled || undefined}
|
|
370
|
+
data-palette-index={globalIndex}
|
|
371
|
+
onmousedown={(e) => { if (!item.disabled) e.preventDefault(); selectItem(item); }}
|
|
372
|
+
onmouseenter={() => { activeIndex = globalIndex; }}
|
|
373
|
+
>
|
|
374
|
+
<div class="palette-item-content">
|
|
375
|
+
<span class="palette-item-label">
|
|
376
|
+
{#if hl}
|
|
377
|
+
{hl.before}<mark class="palette-highlight">{hl.match}</mark>{hl.after}
|
|
378
|
+
{:else}
|
|
379
|
+
{item.label}
|
|
380
|
+
{/if}
|
|
381
|
+
</span>
|
|
382
|
+
{#if item.description}
|
|
383
|
+
<span class="palette-item-description">{item.description}</span>
|
|
384
|
+
{/if}
|
|
385
|
+
</div>
|
|
386
|
+
{#if item.shortcut}
|
|
387
|
+
<kbd class="palette-shortcut">{item.shortcut}</kbd>
|
|
388
|
+
{/if}
|
|
389
|
+
</div>
|
|
390
|
+
{/each}
|
|
391
|
+
</div>
|
|
392
|
+
{/each}
|
|
393
|
+
{/if}
|
|
394
|
+
</div>
|
|
395
|
+
{/if}
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
{/if}
|
|
399
|
+
|
|
400
|
+
<style>
|
|
401
|
+
/* ─── Backdrop ─── */
|
|
402
|
+
.palette-backdrop {
|
|
403
|
+
position: fixed;
|
|
404
|
+
inset: 0;
|
|
405
|
+
background: var(--palette-backdrop);
|
|
406
|
+
z-index: 60;
|
|
407
|
+
animation: palette-fade-in var(--palette-transition);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.palette-container {
|
|
411
|
+
position: fixed;
|
|
412
|
+
inset: 0;
|
|
413
|
+
z-index: 61;
|
|
414
|
+
display: flex;
|
|
415
|
+
align-items: flex-start;
|
|
416
|
+
justify-content: center;
|
|
417
|
+
padding-top: var(--palette-top-offset);
|
|
418
|
+
pointer-events: none;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/* ─── Dialog ─── */
|
|
422
|
+
.palette {
|
|
423
|
+
pointer-events: auto;
|
|
424
|
+
width: var(--palette-width);
|
|
425
|
+
max-width: calc(100vw - var(--space-xl));
|
|
426
|
+
max-height: var(--palette-max-height);
|
|
427
|
+
background: var(--palette-bg);
|
|
428
|
+
border-radius: var(--palette-radius);
|
|
429
|
+
box-shadow: var(--palette-shadow);
|
|
430
|
+
display: flex;
|
|
431
|
+
flex-direction: column;
|
|
432
|
+
overflow: hidden;
|
|
433
|
+
animation: palette-scale-in var(--palette-transition);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/* ─── Input ─── */
|
|
437
|
+
.palette-input-wrap {
|
|
438
|
+
position: relative;
|
|
439
|
+
display: flex;
|
|
440
|
+
align-items: center;
|
|
441
|
+
border-bottom: var(--palette-input-border);
|
|
442
|
+
flex-shrink: 0;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.palette-input-icon {
|
|
446
|
+
position: absolute;
|
|
447
|
+
left: var(--space-md);
|
|
448
|
+
display: flex;
|
|
449
|
+
color: var(--search-icon-color);
|
|
450
|
+
pointer-events: none;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.palette-icon {
|
|
454
|
+
width: var(--search-icon-size-md);
|
|
455
|
+
height: var(--search-icon-size-md);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.palette-input {
|
|
459
|
+
width: 100%;
|
|
460
|
+
height: var(--palette-input-height);
|
|
461
|
+
padding: var(--palette-input-padding);
|
|
462
|
+
padding-left: calc(var(--space-md) + var(--search-icon-size-md) + var(--space-sm));
|
|
463
|
+
font-family: var(--palette-input-font);
|
|
464
|
+
font-size: var(--palette-input-size);
|
|
465
|
+
color: var(--color-text);
|
|
466
|
+
background: transparent;
|
|
467
|
+
border: none;
|
|
468
|
+
outline: none;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.palette-input::placeholder {
|
|
472
|
+
color: var(--input-placeholder);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/* ─── List ─── */
|
|
476
|
+
.palette-list {
|
|
477
|
+
flex: 1;
|
|
478
|
+
overflow-y: auto;
|
|
479
|
+
padding: var(--palette-list-padding);
|
|
480
|
+
max-height: var(--palette-list-max-height);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/* ─── Group ─── */
|
|
484
|
+
.palette-group-heading {
|
|
485
|
+
font-family: var(--palette-group-font);
|
|
486
|
+
font-size: var(--palette-group-size);
|
|
487
|
+
letter-spacing: var(--palette-group-tracking);
|
|
488
|
+
color: var(--palette-group-color);
|
|
489
|
+
padding: var(--palette-group-padding);
|
|
490
|
+
text-transform: uppercase;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/* ─── Item ─── */
|
|
494
|
+
.palette-item {
|
|
495
|
+
display: flex;
|
|
496
|
+
align-items: center;
|
|
497
|
+
justify-content: space-between;
|
|
498
|
+
gap: var(--space-sm);
|
|
499
|
+
padding: var(--palette-item-padding);
|
|
500
|
+
border-radius: var(--palette-item-radius);
|
|
501
|
+
cursor: pointer;
|
|
502
|
+
transition: background var(--duration-instant) var(--easing-default);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.palette-item:hover,
|
|
506
|
+
.palette-item-active {
|
|
507
|
+
background: var(--palette-item-hover-bg);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.palette-item-active {
|
|
511
|
+
background: var(--palette-item-active-bg);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.palette-item-disabled {
|
|
515
|
+
opacity: 0.5;
|
|
516
|
+
cursor: not-allowed;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.palette-item-content {
|
|
520
|
+
display: flex;
|
|
521
|
+
flex-direction: column;
|
|
522
|
+
gap: var(--space-2xs);
|
|
523
|
+
min-width: 0;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.palette-item-label {
|
|
527
|
+
font-family: var(--input-font);
|
|
528
|
+
font-size: var(--input-font-size);
|
|
529
|
+
color: var(--color-text);
|
|
530
|
+
white-space: nowrap;
|
|
531
|
+
overflow: hidden;
|
|
532
|
+
text-overflow: ellipsis;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.palette-item-description {
|
|
536
|
+
font-family: var(--type-caption-font);
|
|
537
|
+
font-size: var(--type-caption-size);
|
|
538
|
+
color: var(--color-text-muted);
|
|
539
|
+
white-space: nowrap;
|
|
540
|
+
overflow: hidden;
|
|
541
|
+
text-overflow: ellipsis;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.palette-highlight {
|
|
545
|
+
background: transparent;
|
|
546
|
+
color: var(--palette-highlight-color);
|
|
547
|
+
font-weight: var(--type-overline-weight);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/* ─── Shortcut badge ─── */
|
|
551
|
+
.palette-shortcut {
|
|
552
|
+
flex-shrink: 0;
|
|
553
|
+
font-family: var(--palette-shortcut-font);
|
|
554
|
+
font-size: var(--palette-shortcut-size);
|
|
555
|
+
color: var(--palette-shortcut-color);
|
|
556
|
+
background: var(--palette-shortcut-bg);
|
|
557
|
+
border-radius: var(--palette-shortcut-radius);
|
|
558
|
+
padding: var(--palette-shortcut-padding);
|
|
559
|
+
border: var(--elevation-border);
|
|
560
|
+
line-height: 1;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/* ─── Empty state ─── */
|
|
564
|
+
.palette-empty {
|
|
565
|
+
padding: var(--space-xl) var(--space-md);
|
|
566
|
+
text-align: center;
|
|
567
|
+
font-family: var(--palette-empty-font);
|
|
568
|
+
font-size: var(--palette-empty-size);
|
|
569
|
+
color: var(--palette-empty-color);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/* ─── Animations ─── */
|
|
573
|
+
@keyframes palette-fade-in {
|
|
574
|
+
from { opacity: 0; }
|
|
575
|
+
to { opacity: 1; }
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
@keyframes palette-scale-in {
|
|
579
|
+
from { opacity: 0; transform: scale(0.96) translateY(-8px); }
|
|
580
|
+
to { opacity: 1; transform: scale(1) translateY(0); }
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
@media (prefers-reduced-motion: reduce) {
|
|
584
|
+
.palette,
|
|
585
|
+
.palette-backdrop {
|
|
586
|
+
animation: none;
|
|
587
|
+
}
|
|
588
|
+
.palette-item {
|
|
589
|
+
transition: none;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/* ─── Mobile ─── */
|
|
594
|
+
@media (max-width: 640px) {
|
|
595
|
+
.palette-container {
|
|
596
|
+
padding-top: var(--space-sm);
|
|
597
|
+
align-items: flex-start;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
.palette {
|
|
601
|
+
max-width: calc(100vw - var(--space-md));
|
|
602
|
+
max-height: calc(100vh - var(--space-lg));
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
</style>
|
|
@@ -18,11 +18,13 @@
|
|
|
18
18
|
<script>
|
|
19
19
|
/**
|
|
20
20
|
* @typedef {{ value: string, label: string }} SelectOption
|
|
21
|
-
* @typedef {{
|
|
21
|
+
* @typedef {{ value: string, label: string, group?: string, description?: string }} ComboboxItem
|
|
22
|
+
* @typedef {{ key: string, label: string, type: 'text' | 'select' | 'combobox', width?: string, options?: SelectOption[], items?: ComboboxItem[], placeholder?: string }} ColumnDef
|
|
22
23
|
*/
|
|
23
24
|
|
|
24
25
|
import Input from './Input.svelte';
|
|
25
26
|
import Select from './Select.svelte';
|
|
27
|
+
import Combobox from './Combobox.svelte';
|
|
26
28
|
import Button from './Button.svelte';
|
|
27
29
|
|
|
28
30
|
let {
|
|
@@ -109,6 +111,15 @@
|
|
|
109
111
|
{disabled}
|
|
110
112
|
onchange={(e) => updateCell(rowIndex, col.key, e.target.value)}
|
|
111
113
|
/>
|
|
114
|
+
{:else if col.type === 'combobox'}
|
|
115
|
+
<Combobox
|
|
116
|
+
size="sm"
|
|
117
|
+
value={row[col.key] ?? ''}
|
|
118
|
+
items={col.items ?? []}
|
|
119
|
+
placeholder={col.placeholder}
|
|
120
|
+
{disabled}
|
|
121
|
+
onchange={(v) => updateCell(rowIndex, col.key, v)}
|
|
122
|
+
/>
|
|
112
123
|
{:else}
|
|
113
124
|
<Input
|
|
114
125
|
size="sm"
|
|
@@ -175,13 +175,13 @@
|
|
|
175
175
|
all: unset;
|
|
176
176
|
cursor: pointer;
|
|
177
177
|
flex-shrink: 0;
|
|
178
|
-
width:
|
|
179
|
-
height:
|
|
178
|
+
width: var(--icon-size-md);
|
|
179
|
+
height: var(--icon-size-md);
|
|
180
180
|
display: flex;
|
|
181
181
|
align-items: center;
|
|
182
182
|
justify-content: center;
|
|
183
183
|
color: var(--color-text-muted);
|
|
184
|
-
font-size:
|
|
184
|
+
font-size: var(--icon-size-sm);
|
|
185
185
|
line-height: 1;
|
|
186
186
|
border-radius: var(--radius-sm);
|
|
187
187
|
}
|