@salmexio/ui 0.3.1 → 1.0.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.
Files changed (110) hide show
  1. package/README.md +52 -3
  2. package/dist/dialogs/ContextMenu/ContextMenu.svelte +96 -93
  3. package/dist/dialogs/ContextMenu/ContextMenu.svelte.d.ts +3 -2
  4. package/dist/dialogs/ContextMenu/ContextMenu.svelte.d.ts.map +1 -1
  5. package/dist/dialogs/Modal/Modal.svelte +112 -102
  6. package/dist/dialogs/Modal/Modal.svelte.d.ts +1 -1
  7. package/dist/feedback/Alert/Alert.svelte +115 -221
  8. package/dist/feedback/Alert/Alert.svelte.d.ts +1 -1
  9. package/dist/feedback/ProgressBar/ProgressBar.svelte +246 -0
  10. package/dist/feedback/ProgressBar/ProgressBar.svelte.d.ts +40 -0
  11. package/dist/feedback/ProgressBar/ProgressBar.svelte.d.ts.map +1 -0
  12. package/dist/feedback/ProgressBar/index.d.ts +2 -0
  13. package/dist/feedback/ProgressBar/index.d.ts.map +1 -0
  14. package/dist/feedback/ProgressBar/index.js +1 -0
  15. package/dist/feedback/Skeleton/Skeleton.svelte +153 -0
  16. package/dist/feedback/Skeleton/Skeleton.svelte.d.ts +37 -0
  17. package/dist/feedback/Skeleton/Skeleton.svelte.d.ts.map +1 -0
  18. package/dist/feedback/Skeleton/index.d.ts +2 -0
  19. package/dist/feedback/Skeleton/index.d.ts.map +1 -0
  20. package/dist/feedback/Skeleton/index.js +1 -0
  21. package/dist/feedback/Spinner/Spinner.svelte +88 -154
  22. package/dist/feedback/Spinner/Spinner.svelte.d.ts +5 -3
  23. package/dist/feedback/Spinner/Spinner.svelte.d.ts.map +1 -1
  24. package/dist/feedback/Toast/Toaster.svelte +431 -0
  25. package/dist/feedback/Toast/Toaster.svelte.d.ts +22 -0
  26. package/dist/feedback/Toast/Toaster.svelte.d.ts.map +1 -0
  27. package/dist/feedback/Toast/index.d.ts +4 -0
  28. package/dist/feedback/Toast/index.d.ts.map +1 -0
  29. package/dist/feedback/Toast/index.js +2 -0
  30. package/dist/feedback/Toast/toastStore.d.ts +34 -0
  31. package/dist/feedback/Toast/toastStore.d.ts.map +1 -0
  32. package/dist/feedback/Toast/toastStore.js +43 -0
  33. package/dist/feedback/index.d.ts +4 -0
  34. package/dist/feedback/index.d.ts.map +1 -1
  35. package/dist/feedback/index.js +3 -0
  36. package/dist/forms/Checkbox/Checkbox.svelte +82 -103
  37. package/dist/forms/Checkbox/Checkbox.svelte.d.ts +1 -1
  38. package/dist/forms/Select/Select.svelte +136 -177
  39. package/dist/forms/Select/Select.svelte.d.ts +1 -1
  40. package/dist/forms/Slider/Slider.svelte +356 -0
  41. package/dist/forms/Slider/Slider.svelte.d.ts +50 -0
  42. package/dist/forms/Slider/Slider.svelte.d.ts.map +1 -0
  43. package/dist/forms/Slider/index.d.ts +2 -0
  44. package/dist/forms/Slider/index.d.ts.map +1 -0
  45. package/dist/forms/Slider/index.js +1 -0
  46. package/dist/forms/TextInput/TextInput.svelte +148 -164
  47. package/dist/forms/TextInput/TextInput.svelte.d.ts +1 -1
  48. package/dist/forms/Textarea/Textarea.svelte +615 -0
  49. package/dist/forms/Textarea/Textarea.svelte.d.ts +47 -0
  50. package/dist/forms/Textarea/Textarea.svelte.d.ts.map +1 -0
  51. package/dist/forms/Textarea/index.d.ts +2 -0
  52. package/dist/forms/Textarea/index.d.ts.map +1 -0
  53. package/dist/forms/Textarea/index.js +1 -0
  54. package/dist/forms/Toggle/Toggle.svelte +239 -0
  55. package/dist/forms/Toggle/Toggle.svelte.d.ts +39 -0
  56. package/dist/forms/Toggle/Toggle.svelte.d.ts.map +1 -0
  57. package/dist/forms/Toggle/index.d.ts +2 -0
  58. package/dist/forms/Toggle/index.d.ts.map +1 -0
  59. package/dist/forms/Toggle/index.js +1 -0
  60. package/dist/forms/index.d.ts +3 -0
  61. package/dist/forms/index.d.ts.map +1 -1
  62. package/dist/forms/index.js +3 -0
  63. package/dist/index.d.ts +0 -1
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +0 -1
  66. package/dist/layout/Card/Card.svelte +64 -40
  67. package/dist/layout/Card/Card.svelte.d.ts +1 -1
  68. package/dist/layout/Card/Card.svelte.d.ts.map +1 -1
  69. package/dist/layout/Container/Container.svelte +71 -71
  70. package/dist/layout/Container/Container.svelte.d.ts +2 -2
  71. package/dist/navigation/CommandPalette/CommandPalette.svelte +410 -181
  72. package/dist/navigation/CommandPalette/CommandPalette.svelte.d.ts +8 -3
  73. package/dist/navigation/CommandPalette/CommandPalette.svelte.d.ts.map +1 -1
  74. package/dist/navigation/Tabs/Tabs.svelte +94 -178
  75. package/dist/navigation/Tabs/Tabs.svelte.d.ts +2 -2
  76. package/dist/primitives/Badge/Badge.svelte +85 -223
  77. package/dist/primitives/Badge/Badge.svelte.d.ts +2 -2
  78. package/dist/primitives/Badge/Badge.svelte.d.ts.map +1 -1
  79. package/dist/primitives/Button/Button.svelte +138 -208
  80. package/dist/primitives/Button/Button.svelte.d.ts +3 -3
  81. package/dist/primitives/Button/Button.svelte.d.ts.map +1 -1
  82. package/dist/primitives/Tooltip/Tooltip.svelte +260 -0
  83. package/dist/primitives/Tooltip/Tooltip.svelte.d.ts +36 -0
  84. package/dist/primitives/Tooltip/Tooltip.svelte.d.ts.map +1 -0
  85. package/dist/primitives/Tooltip/index.d.ts +2 -0
  86. package/dist/primitives/Tooltip/index.d.ts.map +1 -0
  87. package/dist/primitives/Tooltip/index.js +1 -0
  88. package/dist/primitives/index.d.ts +1 -0
  89. package/dist/primitives/index.d.ts.map +1 -1
  90. package/dist/primitives/index.js +1 -0
  91. package/dist/styles/tokens.css +200 -259
  92. package/package.json +5 -5
  93. package/dist/windowing/Window/Window.svelte +0 -602
  94. package/dist/windowing/Window/Window.svelte.d.ts +0 -65
  95. package/dist/windowing/Window/Window.svelte.d.ts.map +0 -1
  96. package/dist/windowing/Window/index.d.ts +0 -2
  97. package/dist/windowing/Window/index.d.ts.map +0 -1
  98. package/dist/windowing/Window/index.js +0 -1
  99. package/dist/windowing/WindowManager/WindowManager.svelte +0 -410
  100. package/dist/windowing/WindowManager/WindowManager.svelte.d.ts +0 -38
  101. package/dist/windowing/WindowManager/WindowManager.svelte.d.ts.map +0 -1
  102. package/dist/windowing/WindowManager/index.d.ts +0 -2
  103. package/dist/windowing/WindowManager/index.d.ts.map +0 -1
  104. package/dist/windowing/WindowManager/index.js +0 -1
  105. package/dist/windowing/index.d.ts +0 -5
  106. package/dist/windowing/index.d.ts.map +0 -1
  107. package/dist/windowing/index.js +0 -3
  108. package/dist/windowing/windowStore.svelte.d.ts +0 -49
  109. package/dist/windowing/windowStore.svelte.d.ts.map +0 -1
  110. package/dist/windowing/windowStore.svelte.js +0 -170
@@ -1,8 +1,12 @@
1
1
  <!--
2
2
  @component CommandPalette
3
3
 
4
- Win2K × Basquiat Keyboard-first command launcher with sunken search field,
5
- categorised result list, fuzzy matching, and shortcut display.
4
+ Neo-Brutalist DarkPremium keyboard-first command launcher.
5
+ Glass backdrop, entrance animations, fuzzy search with match highlighting,
6
+ categorised results, shortcut display, live result count announcements.
7
+
8
+ ARIA: dialog containing combobox with listbox. Focus stays on input;
9
+ aria-activedescendant communicates visual focus to screen readers.
6
10
 
7
11
  @example
8
12
  <CommandPalette
@@ -14,12 +18,14 @@
14
18
  />
15
19
  -->
16
20
  <script lang="ts" module>
21
+ import type { Snippet } from 'svelte';
22
+
17
23
  export interface CommandItem {
18
24
  id: string;
19
25
  label: string;
20
26
  shortcut?: string;
21
27
  group?: string;
22
- icon?: string;
28
+ icon?: Snippet;
23
29
  description?: string;
24
30
  disabled?: boolean;
25
31
  action: () => void;
@@ -33,7 +39,7 @@ export interface CommandGroup {
33
39
 
34
40
  <script lang="ts">
35
41
  import { cn } from '../../utils/cn.js';
36
- import { Keys, createFocusTrap } from '../../utils/keyboard.js';
42
+ import { Keys, createFocusTrap, generateId } from '../../utils/keyboard.js';
37
43
  import { onMount, tick } from 'svelte';
38
44
  import type { FocusTrap } from '../../utils/keyboard.js';
39
45
 
@@ -61,63 +67,100 @@ let {
61
67
  testId
62
68
  }: Props = $props();
63
69
 
70
+ const instanceId = generateId('cmd');
71
+ const resultsId = `${instanceId}-results`;
72
+
64
73
  let query = $state('');
65
74
  let activeIndex = $state(0);
66
75
  let inputEl = $state<HTMLInputElement | null>(null);
67
76
  let panelEl = $state<HTMLDivElement | null>(null);
68
77
  let listEl = $state<HTMLDivElement | null>(null);
69
- let backdropEl = $state<HTMLDivElement | null>(null);
70
78
  let wrapperEl = $state<HTMLDivElement | null>(null);
71
79
  let focusTrap: FocusTrap | null = null;
80
+ let previouslyFocused: HTMLElement | null = null;
81
+ let isAnimatingOut = $state(false);
72
82
 
73
- // Fuzzy match: check if query chars appear in order in the label
74
- function fuzzyMatch(text: string, search: string): boolean {
75
- if (!search) return true;
83
+ // Fuzzy match: return matched character indices for highlighting
84
+ function fuzzyMatchIndices(text: string, search: string): number[] | null {
85
+ if (!search) return [];
76
86
  const lower = text.toLowerCase();
77
87
  const searchLower = search.toLowerCase();
88
+ const indices: number[] = [];
78
89
  let si = 0;
79
90
  for (let ti = 0; ti < lower.length && si < searchLower.length; ti++) {
80
- if (lower[ti] === searchLower[si]) si++;
91
+ if (lower[ti] === searchLower[si]) {
92
+ indices.push(ti);
93
+ si++;
94
+ }
81
95
  }
82
- return si === searchLower.length;
96
+ return si === searchLower.length ? indices : null;
83
97
  }
84
98
 
85
- // Score: prefer starts-with and shorter labels
99
+ // Score: prefer starts-with, word-boundary matches, shorter labels
86
100
  function fuzzyScore(text: string, search: string): number {
87
101
  if (!search) return 0;
88
102
  const lower = text.toLowerCase();
89
103
  const searchLower = search.toLowerCase();
90
104
  let score = 0;
91
- if (lower.startsWith(searchLower)) score += 100;
92
- if (lower.includes(searchLower)) score += 50;
105
+
106
+ // Exact prefix highest
107
+ if (lower.startsWith(searchLower)) score += 200;
108
+ // Word boundary matches
109
+ else if (lower.includes(searchLower)) score += 100;
110
+
111
+ // Consecutive matches bonus
112
+ const indices = fuzzyMatchIndices(text, search);
113
+ if (indices && indices.length > 1) {
114
+ let consecutive = 0;
115
+ for (let i = 1; i < indices.length; i++) {
116
+ if (indices[i] === indices[i - 1] + 1) consecutive++;
117
+ }
118
+ score += consecutive * 15;
119
+ }
120
+
121
+ // Word-boundary bonus: match at start of word
122
+ if (indices) {
123
+ for (const idx of indices) {
124
+ if (idx === 0 || text[idx - 1] === ' ' || text[idx - 1] === '-' || text[idx - 1] === '_') {
125
+ score += 10;
126
+ }
127
+ }
128
+ }
129
+
130
+ // Shorter labels rank higher
93
131
  score -= text.length;
94
132
  return score;
95
133
  }
96
134
 
97
- const filteredCommands = $derived<CommandItem[]>(
135
+ const filteredCommands = $derived<(CommandItem & { matchIndices: number[] })[]>(
98
136
  commands
99
- .filter((c) => fuzzyMatch(c.label, query) || (c.description && fuzzyMatch(c.description, query)))
137
+ .map((c) => {
138
+ const labelIndices = fuzzyMatchIndices(c.label, query);
139
+ const descIndices = c.description ? fuzzyMatchIndices(c.description, query) : null;
140
+ if (labelIndices === null && descIndices === null) return null;
141
+ return { ...c, matchIndices: labelIndices ?? [] };
142
+ })
143
+ .filter((c): c is CommandItem & { matchIndices: number[] } => c !== null)
100
144
  .sort((a, b) => fuzzyScore(b.label, query) - fuzzyScore(a.label, query))
101
145
  );
102
146
 
103
147
  // Group filtered results
104
148
  const groupedResults = $derived.by(() => {
105
- const groups = new Map<string, CommandItem[]>();
149
+ const groups = new Map<string, (CommandItem & { matchIndices: number[] })[]>();
106
150
  for (const item of filteredCommands) {
107
151
  const key = item.group ?? '';
108
152
  if (!groups.has(key)) groups.set(key, []);
109
153
  groups.get(key)!.push(item);
110
154
  }
111
- return Array.from(groups.entries()).map(([label, items]) => ({ label, items })) as CommandGroup[];
155
+ return Array.from(groups.entries()).map(([label, items]) => ({ label, items }));
112
156
  });
113
157
 
114
- // Flat list for keyboard navigation
115
- const flatFiltered = $derived(filteredCommands);
116
-
117
158
  function openPalette() {
159
+ previouslyFocused = document.activeElement as HTMLElement;
118
160
  open = true;
119
161
  query = '';
120
162
  activeIndex = 0;
163
+ isAnimatingOut = false;
121
164
  requestAnimationFrame(() => {
122
165
  inputEl?.focus();
123
166
  if (panelEl) {
@@ -128,18 +171,27 @@ function openPalette() {
128
171
  }
129
172
 
130
173
  function closePalette() {
131
- open = false;
132
- query = '';
133
- activeIndex = 0;
174
+ isAnimatingOut = true;
134
175
  focusTrap?.deactivate();
135
176
  focusTrap = null;
136
- onclose?.();
177
+
178
+ // Wait for exit animation
179
+ setTimeout(() => {
180
+ open = false;
181
+ isAnimatingOut = false;
182
+ query = '';
183
+ activeIndex = 0;
184
+ onclose?.();
185
+ previouslyFocused?.focus();
186
+ previouslyFocused = null;
187
+ }, 120);
137
188
  }
138
189
 
139
- function executeItem(item: CommandItem) {
190
+ function executeItem(item: CommandItem & { matchIndices: number[] }) {
140
191
  if (item.disabled) return;
141
192
  closePalette();
142
- item.action();
193
+ // Execute after close animation
194
+ setTimeout(() => item.action(), 130);
143
195
  }
144
196
 
145
197
  function scrollActiveIntoView() {
@@ -147,15 +199,7 @@ function scrollActiveIntoView() {
147
199
  if (!listEl) return;
148
200
  const active = listEl.querySelector('[data-active="true"]') as HTMLElement;
149
201
  if (!active) return;
150
- const listTop = listEl.scrollTop;
151
- const listHeight = listEl.clientHeight;
152
- const elTop = active.offsetTop;
153
- const elHeight = active.offsetHeight;
154
- if (elTop < listTop) {
155
- listEl.scrollTop = elTop;
156
- } else if (elTop + elHeight > listTop + listHeight) {
157
- listEl.scrollTop = elTop + elHeight - listHeight;
158
- }
202
+ active.scrollIntoView({ block: 'nearest' });
159
203
  });
160
204
  }
161
205
 
@@ -163,7 +207,7 @@ function handleKeydown(e: KeyboardEvent) {
163
207
  switch (e.key) {
164
208
  case Keys.ArrowDown:
165
209
  e.preventDefault();
166
- activeIndex = Math.min(activeIndex + 1, flatFiltered.length - 1);
210
+ activeIndex = Math.min(activeIndex + 1, filteredCommands.length - 1);
167
211
  scrollActiveIntoView();
168
212
  break;
169
213
  case Keys.ArrowUp:
@@ -173,7 +217,7 @@ function handleKeydown(e: KeyboardEvent) {
173
217
  break;
174
218
  case Keys.Enter:
175
219
  e.preventDefault();
176
- if (flatFiltered[activeIndex]) executeItem(flatFiltered[activeIndex]);
220
+ if (filteredCommands[activeIndex]) executeItem(filteredCommands[activeIndex]);
177
221
  break;
178
222
  case Keys.Escape:
179
223
  e.preventDefault();
@@ -186,7 +230,7 @@ function handleKeydown(e: KeyboardEvent) {
186
230
  break;
187
231
  case Keys.End:
188
232
  e.preventDefault();
189
- activeIndex = Math.max(flatFiltered.length - 1, 0);
233
+ activeIndex = Math.max(filteredCommands.length - 1, 0);
190
234
  scrollActiveIntoView();
191
235
  break;
192
236
  }
@@ -196,16 +240,12 @@ function handleInput() {
196
240
  activeIndex = 0;
197
241
  }
198
242
 
199
- function handleBackdropClick() {
200
- closePalette();
201
- }
202
-
203
243
  // Watch for external open changes
204
244
  $effect(() => {
205
- if (open) openPalette();
245
+ if (open && !isAnimatingOut) openPalette();
206
246
  });
207
247
 
208
- // Portal: move wrapper to document.body to escape transform/overflow ancestors
248
+ // Portal to document.body
209
249
  $effect(() => {
210
250
  if (wrapperEl && open) {
211
251
  document.body.appendChild(wrapperEl);
@@ -233,7 +273,6 @@ onMount(() => {
233
273
  };
234
274
  });
235
275
 
236
- // Track cumulative index for items within grouped display
237
276
  function getCumulativeIndex(groupIdx: number, itemIdx: number): number {
238
277
  let offset = 0;
239
278
  for (let g = 0; g < groupIdx; g++) {
@@ -241,18 +280,27 @@ function getCumulativeIndex(groupIdx: number, itemIdx: number): number {
241
280
  }
242
281
  return offset + itemIdx;
243
282
  }
283
+
284
+ function getItemId(itemId: string): string {
285
+ return `${instanceId}-item-${itemId}`;
286
+ }
244
287
  </script>
245
288
 
246
289
  {#if open}
247
290
  <div bind:this={wrapperEl}>
248
291
  <!-- Backdrop -->
249
292
  <!-- svelte-ignore a11y_no_static_element_interactions -->
250
- <div bind:this={backdropEl} class="salmex-cmd-backdrop" onclick={handleBackdropClick} onkeydown={() => {}}></div>
293
+ <div
294
+ class={cn('sx-command-backdrop', isAnimatingOut && 'sx-command-backdrop-exit')}
295
+ onclick={() => closePalette()}
296
+ onkeydown={() => {}}
297
+ role="presentation"
298
+ ></div>
251
299
 
252
300
  <!-- Panel -->
253
301
  <div
254
302
  bind:this={panelEl}
255
- class={cn('salmex-cmd', className)}
303
+ class={cn('sx-command', isAnimatingOut && 'sx-command-exit', className)}
256
304
  role="dialog"
257
305
  tabindex="-1"
258
306
  aria-label="Command palette"
@@ -260,9 +308,9 @@ function getCumulativeIndex(groupIdx: number, itemIdx: number): number {
260
308
  onkeydown={handleKeydown}
261
309
  >
262
310
  <!-- Search field -->
263
- <div class="salmex-cmd-search">
264
- <span class="salmex-cmd-search-icon" aria-hidden="true">
265
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2">
311
+ <div class="sx-command-search">
312
+ <span class="sx-command-search-icon" aria-hidden="true">
313
+ <svg width="18" height="18" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
266
314
  <circle cx="6.5" cy="6.5" r="5" />
267
315
  <path d="M10 10L14.5 14.5" />
268
316
  </svg>
@@ -270,38 +318,51 @@ function getCumulativeIndex(groupIdx: number, itemIdx: number): number {
270
318
  <input
271
319
  bind:this={inputEl}
272
320
  type="text"
273
- class="salmex-cmd-input"
321
+ class="sx-command-input"
322
+ role="combobox"
274
323
  {placeholder}
275
324
  bind:value={query}
276
325
  oninput={handleInput}
277
326
  aria-label="Search commands"
278
- aria-controls="cmd-results"
279
- aria-activedescendant={flatFiltered[activeIndex] ? `cmd-item-${flatFiltered[activeIndex].id}` : undefined}
327
+ aria-expanded={filteredCommands.length > 0}
328
+ aria-haspopup="listbox"
329
+ aria-controls={resultsId}
330
+ aria-activedescendant={filteredCommands[activeIndex] ? getItemId(filteredCommands[activeIndex].id) : undefined}
331
+ aria-autocomplete="list"
280
332
  autocomplete="off"
281
333
  spellcheck="false"
282
334
  />
283
- <span class="salmex-cmd-shortcut-hint" aria-hidden="true">ESC</span>
335
+ <kbd class="sx-command-esc" aria-hidden="true">ESC</kbd>
284
336
  </div>
285
337
 
286
338
  <!-- Results -->
287
- <div bind:this={listEl} id="cmd-results" class="salmex-cmd-results" role="listbox">
288
- {#if flatFiltered.length === 0}
289
- <div class="salmex-cmd-empty">No commands found.</div>
339
+ <div bind:this={listEl} id={resultsId} class="sx-command-results" role="listbox" aria-label="Commands">
340
+ {#if filteredCommands.length === 0}
341
+ <div class="sx-command-empty">
342
+ <span class="sx-command-empty-icon" aria-hidden="true">
343
+ <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
344
+ <circle cx="11" cy="11" r="8" />
345
+ <path d="M21 21l-4.35-4.35" />
346
+ <path d="M8 11h6" />
347
+ </svg>
348
+ </span>
349
+ <span class="sx-command-empty-text">No commands match "<strong>{query}</strong>"</span>
350
+ </div>
290
351
  {:else}
291
352
  {#each groupedResults as group, gi}
292
353
  {#if group.label}
293
- <div class="salmex-cmd-group-label">{group.label}</div>
354
+ <div class="sx-command-group-label" role="presentation">{group.label}</div>
294
355
  {/if}
295
356
  {#each group.items as item, ii}
296
357
  {@const globalIdx = getCumulativeIndex(gi, ii)}
297
358
  {@const isActive = globalIdx === activeIndex}
298
359
  <!-- svelte-ignore a11y_no_noninteractive_element_interactions a11y_click_events_have_key_events -->
299
360
  <div
300
- id="cmd-item-{item.id}"
361
+ id={getItemId(item.id)}
301
362
  class={cn(
302
- 'salmex-cmd-item',
303
- isActive && 'salmex-cmd-item-active',
304
- item.disabled && 'salmex-cmd-item-disabled'
363
+ 'sx-command-item',
364
+ isActive && 'sx-command-item-active',
365
+ item.disabled && 'sx-command-item-disabled'
305
366
  )}
306
367
  role="option"
307
368
  tabindex="-1"
@@ -310,19 +371,30 @@ function getCumulativeIndex(groupIdx: number, itemIdx: number): number {
310
371
  data-active={isActive}
311
372
  onmouseenter={() => { activeIndex = globalIdx; }}
312
373
  onclick={() => executeItem(item)}
313
- onkeydown={handleKeydown}
314
374
  >
315
375
  {#if item.icon}
316
- <span class="salmex-cmd-item-icon" aria-hidden="true">{item.icon}</span>
376
+ <span class="sx-command-item-icon" aria-hidden="true">{@render item.icon()}</span>
317
377
  {/if}
318
- <div class="salmex-cmd-item-content">
319
- <span class="salmex-cmd-item-label">{item.label}</span>
378
+ <div class="sx-command-item-content">
379
+ <span class="sx-command-item-label">
380
+ {#if query && item.matchIndices.length > 0}
381
+ {#each item.label.split('') as char, ci}
382
+ {#if item.matchIndices.includes(ci)}
383
+ <mark class="sx-command-match">{char}</mark>
384
+ {:else}
385
+ {char}
386
+ {/if}
387
+ {/each}
388
+ {:else}
389
+ {item.label}
390
+ {/if}
391
+ </span>
320
392
  {#if item.description}
321
- <span class="salmex-cmd-item-desc">{item.description}</span>
393
+ <span class="sx-command-item-desc">{item.description}</span>
322
394
  {/if}
323
395
  </div>
324
396
  {#if item.shortcut}
325
- <span class="salmex-cmd-item-shortcut">{item.shortcut}</span>
397
+ <kbd class="sx-command-item-shortcut">{item.shortcut}</kbd>
326
398
  {/if}
327
399
  </div>
328
400
  {/each}
@@ -330,11 +402,21 @@ function getCumulativeIndex(groupIdx: number, itemIdx: number): number {
330
402
  {/if}
331
403
  </div>
332
404
 
333
- <!-- Footer hint -->
334
- <div class="salmex-cmd-footer">
335
- <span><kbd>&#8593;&#8595;</kbd> navigate</span>
336
- <span><kbd>&#8629;</kbd> select</span>
337
- <span><kbd>esc</kbd> close</span>
405
+ <!-- Live region for screen readers -->
406
+ <div class="sx-sr-only" aria-live="polite" aria-atomic="true">
407
+ {#if query}
408
+ {filteredCommands.length} {filteredCommands.length === 1 ? 'result' : 'results'} available
409
+ {/if}
410
+ </div>
411
+
412
+ <!-- Footer hints -->
413
+ <div class="sx-command-footer">
414
+ <span class="sx-command-footer-hint"><kbd>&uarr;&darr;</kbd> navigate</span>
415
+ <span class="sx-command-footer-hint"><kbd>&crarr;</kbd> select</span>
416
+ <span class="sx-command-footer-hint"><kbd>esc</kbd> close</span>
417
+ {#if filteredCommands.length > 0}
418
+ <span class="sx-command-footer-count">{filteredCommands.length} {filteredCommands.length === 1 ? 'command' : 'commands'}</span>
419
+ {/if}
338
420
  </div>
339
421
  </div>
340
422
  </div>
@@ -342,232 +424,379 @@ function getCumulativeIndex(groupIdx: number, itemIdx: number): number {
342
424
 
343
425
  <style>
344
426
  /* ========================================
345
- BACKDROP
427
+ BACKDROP — Glass blur with entrance/exit
346
428
  ======================================== */
347
- .salmex-cmd-backdrop {
429
+ .sx-command-backdrop {
348
430
  position: fixed;
349
431
  inset: 0;
350
- z-index: var(--salmex-z-modal-backdrop);
351
- background: rgb(0 0 0 / 0.4);
432
+ z-index: var(--sx-z-modal-backdrop);
433
+ background: var(--sx-color-backdrop);
434
+ backdrop-filter: blur(8px);
435
+ -webkit-backdrop-filter: blur(8px);
436
+ animation: sx-cmd-backdrop-in 150ms ease-out both;
437
+ }
438
+
439
+ .sx-command-backdrop-exit {
440
+ animation: sx-cmd-backdrop-out 120ms ease-in both;
441
+ }
442
+
443
+ @keyframes sx-cmd-backdrop-in {
444
+ from { opacity: 0; }
445
+ to { opacity: 1; }
446
+ }
447
+
448
+ @keyframes sx-cmd-backdrop-out {
449
+ from { opacity: 1; }
450
+ to { opacity: 0; }
352
451
  }
353
452
 
354
453
  /* ========================================
355
- PANEL
454
+ PANEL — Elevated surface with entrance/exit
356
455
  ======================================== */
357
- .salmex-cmd {
456
+ .sx-command {
358
457
  position: fixed;
359
- top: 20%;
458
+ top: min(20%, 160px);
360
459
  left: 50%;
361
460
  transform: translateX(-50%);
362
- z-index: var(--salmex-z-modal);
363
- width: min(560px, calc(100vw - 32px));
364
- max-height: 420px;
461
+ z-index: var(--sx-z-modal);
462
+ width: min(600px, calc(100vw - 32px));
463
+ max-height: min(460px, calc(100vh - 120px));
365
464
  display: flex;
366
465
  flex-direction: column;
367
- background: rgb(var(--salmex-window-surface));
368
- border: 3px solid rgb(var(--salmex-border-dark));
466
+ background: var(--sx-color-surface);
467
+ border: 1px solid var(--sx-color-border-strong);
468
+ border-radius: var(--sx-radius-lg);
369
469
  box-shadow:
370
- inset 1px 1px 0 rgb(var(--salmex-button-highlight)),
371
- inset -1px -1px 0 rgb(var(--salmex-button-shadow)),
372
- 8px 8px 0 rgb(0 0 0 / 0.4);
373
- font-family: var(--salmex-font-system);
470
+ var(--sx-shadow-xl),
471
+ 0 0 0 1px rgba(255, 255, 255, 0.04),
472
+ 0 0 60px rgba(0, 0, 0, 0.5);
473
+ font-family: var(--sx-font-body);
374
474
  overflow: hidden;
475
+ animation: sx-cmd-panel-in 200ms var(--sx-ease-out) both;
375
476
  }
376
477
 
377
- :global([data-theme='dark']) .salmex-cmd {
378
- box-shadow:
379
- inset 1px 1px 0 rgb(var(--salmex-button-highlight)),
380
- inset -1px -1px 0 rgb(var(--salmex-button-shadow)),
381
- 8px 8px 0 rgb(0 0 0 / 0.7);
478
+ .sx-command-exit {
479
+ animation: sx-cmd-panel-out 120ms ease-in both;
480
+ }
481
+
482
+ @keyframes sx-cmd-panel-in {
483
+ from {
484
+ opacity: 0;
485
+ transform: translateX(-50%) translateY(-8px) scale(0.98);
486
+ }
487
+ to {
488
+ opacity: 1;
489
+ transform: translateX(-50%) translateY(0) scale(1);
490
+ }
491
+ }
492
+
493
+ @keyframes sx-cmd-panel-out {
494
+ from {
495
+ opacity: 1;
496
+ transform: translateX(-50%) translateY(0) scale(1);
497
+ }
498
+ to {
499
+ opacity: 0;
500
+ transform: translateX(-50%) translateY(4px) scale(0.98);
501
+ }
382
502
  }
383
503
 
384
504
  /* ========================================
385
- SEARCH FIELD — Sunken, prominent
505
+ SEARCH FIELD
386
506
  ======================================== */
387
- .salmex-cmd-search {
507
+ .sx-command-search {
388
508
  display: flex;
389
509
  align-items: center;
390
- gap: var(--salmex-space-3);
391
- padding: var(--salmex-space-4) var(--salmex-space-5);
392
- border-bottom: 1px solid rgb(var(--salmex-border-default));
510
+ gap: var(--sx-space-3);
511
+ padding: var(--sx-space-4) var(--sx-space-5);
512
+ border-bottom: 1px solid var(--sx-color-border);
393
513
  }
394
514
 
395
- .salmex-cmd-search-icon {
515
+ .sx-command-search-icon {
396
516
  flex-shrink: 0;
397
517
  display: flex;
398
- color: rgb(var(--salmex-text-secondary));
518
+ color: var(--sx-color-cyan);
519
+ opacity: 0.7;
399
520
  }
400
521
 
401
- .salmex-cmd-input {
522
+ .sx-command-input {
402
523
  flex: 1;
524
+ min-width: 0;
403
525
  border: none;
404
526
  background: transparent;
405
527
  outline: none;
406
- font-family: var(--salmex-font-system);
407
- font-size: var(--salmex-font-size-md);
408
- font-weight: 600;
409
- color: rgb(var(--salmex-text-primary));
528
+ font-family: var(--sx-font-body);
529
+ font-size: var(--sx-text-base);
530
+ font-weight: 400;
531
+ color: var(--sx-color-text);
532
+ padding: 0;
533
+ line-height: 1.5;
534
+ }
535
+
536
+ .sx-command-input:focus-visible {
537
+ box-shadow: none;
538
+ outline: none;
410
539
  }
411
540
 
412
- .salmex-cmd-input::placeholder {
413
- color: rgb(var(--salmex-text-disabled));
541
+ .sx-command-input::placeholder {
542
+ color: var(--sx-color-text-disabled);
414
543
  }
415
544
 
416
- .salmex-cmd-shortcut-hint {
545
+ .sx-command-esc {
417
546
  flex-shrink: 0;
418
- font-family: var(--salmex-font-mono);
419
- font-size: var(--salmex-font-size-xs);
420
- font-weight: 700;
421
- padding: 2px 6px;
422
- border: 2px solid rgb(var(--salmex-border-default));
423
- background: rgb(var(--salmex-button-face));
424
- color: rgb(var(--salmex-text-secondary));
425
- box-shadow:
426
- inset 1px 1px 0 rgb(var(--salmex-button-highlight)),
427
- inset -1px -1px 0 rgb(var(--salmex-button-shadow));
547
+ font-family: var(--sx-font-mono);
548
+ font-size: var(--sx-text-xs);
549
+ font-weight: 600;
550
+ padding: var(--sx-space-0-5) var(--sx-space-2);
551
+ border: 1px solid var(--sx-color-border-strong);
552
+ border-radius: var(--sx-radius-sm);
553
+ background: var(--sx-color-surface-2);
554
+ color: var(--sx-color-text-disabled);
555
+ line-height: 1.4;
428
556
  }
429
557
 
430
558
  /* ========================================
431
559
  RESULTS LIST
432
560
  ======================================== */
433
- .salmex-cmd-results {
561
+ .sx-command-results {
434
562
  flex: 1;
435
563
  overflow-y: auto;
436
- padding: var(--salmex-space-1) 0;
437
- max-height: 300px;
564
+ overscroll-behavior: contain;
565
+ padding: var(--sx-space-2) 0;
566
+ max-height: 320px;
567
+ scroll-behavior: smooth;
568
+ }
569
+
570
+ .sx-command-results::-webkit-scrollbar {
571
+ width: 4px;
438
572
  }
439
573
 
440
- .salmex-cmd-empty {
441
- padding: var(--salmex-space-6) var(--salmex-space-5);
574
+ .sx-command-results::-webkit-scrollbar-track {
575
+ background: transparent;
576
+ }
577
+
578
+ .sx-command-results::-webkit-scrollbar-thumb {
579
+ background: var(--sx-color-border-strong);
580
+ border-radius: 2px;
581
+ }
582
+
583
+ .sx-command-empty {
584
+ display: flex;
585
+ flex-direction: column;
586
+ align-items: center;
587
+ gap: var(--sx-space-3);
588
+ padding: var(--sx-space-8) var(--sx-space-5);
442
589
  text-align: center;
443
- font-size: var(--salmex-font-size-sm);
444
- color: rgb(var(--salmex-text-disabled));
445
- font-weight: 600;
590
+ }
591
+
592
+ .sx-command-empty-icon {
593
+ color: var(--sx-color-text-disabled);
594
+ opacity: 0.5;
595
+ }
596
+
597
+ .sx-command-empty-text {
598
+ font-size: var(--sx-text-sm);
599
+ color: var(--sx-color-text-disabled);
600
+ font-weight: 400;
601
+ }
602
+
603
+ .sx-command-empty-text strong {
604
+ color: var(--sx-color-text-secondary);
605
+ font-weight: 500;
446
606
  }
447
607
 
448
608
  /* ========================================
449
609
  GROUP LABEL
450
610
  ======================================== */
451
- .salmex-cmd-group-label {
452
- padding: var(--salmex-space-2) var(--salmex-space-5) var(--salmex-space-1);
453
- margin-top: var(--salmex-space-1);
454
- font-family: var(--salmex-font-mono);
455
- font-size: 10px;
611
+ .sx-command-group-label {
612
+ padding: var(--sx-space-3) var(--sx-space-5) var(--sx-space-1);
613
+ margin-top: var(--sx-space-1);
614
+ font-size: var(--sx-text-xs);
456
615
  font-weight: 600;
616
+ letter-spacing: var(--sx-tracking-wider);
457
617
  text-transform: uppercase;
458
- letter-spacing: 0.8px;
459
- color: rgb(var(--salmex-text-disabled));
460
- border-top: 1px solid rgb(var(--salmex-border-light));
618
+ color: var(--sx-color-text-disabled);
461
619
  user-select: none;
462
620
  }
463
621
 
464
- .salmex-cmd-group-label:first-child {
465
- border-top: none;
622
+ .sx-command-group-label:first-child {
466
623
  margin-top: 0;
467
624
  }
468
625
 
469
626
  /* ========================================
470
627
  COMMAND ITEM
471
628
  ======================================== */
472
- .salmex-cmd-item {
629
+ .sx-command-item {
473
630
  display: flex;
474
631
  align-items: center;
475
- gap: var(--salmex-space-3);
476
- padding: var(--salmex-space-2) var(--salmex-space-5);
632
+ gap: var(--sx-space-3);
633
+ padding: var(--sx-space-2-5) var(--sx-space-5);
477
634
  cursor: pointer;
478
635
  user-select: none;
479
- transition: background var(--salmex-transition-fast);
636
+ color: var(--sx-color-text-secondary);
637
+ border-left: 2px solid transparent;
638
+ transition: background 100ms ease, color 100ms ease, border-color 100ms ease;
480
639
  }
481
640
 
482
- .salmex-cmd-item-active {
483
- background: rgb(var(--salmex-electric-blue));
484
- color: rgb(var(--salmex-chalk-white));
641
+ .sx-command-item-active {
642
+ background: var(--sx-color-cyan-hover);
643
+ color: var(--sx-color-text);
644
+ border-left-color: var(--sx-color-cyan);
485
645
  }
486
646
 
487
- :global([data-theme='dark']) .salmex-cmd-item-active {
488
- background: rgb(var(--salmex-primary-light));
489
- color: rgb(var(--salmex-midnight-black));
647
+ .sx-command-item:active:not(.sx-command-item-disabled) {
648
+ background: var(--sx-color-cyan-active);
490
649
  }
491
650
 
492
- .salmex-cmd-item-disabled {
651
+ .sx-command-item-disabled {
493
652
  opacity: 0.4;
494
653
  cursor: not-allowed;
495
654
  }
496
655
 
497
- .salmex-cmd-item-icon {
656
+ .sx-command-item-icon {
498
657
  flex-shrink: 0;
499
658
  width: 20px;
500
- text-align: center;
501
- font-size: 16px;
659
+ height: 20px;
660
+ display: flex;
661
+ align-items: center;
662
+ justify-content: center;
663
+ color: var(--sx-color-text-secondary);
664
+ }
665
+
666
+ .sx-command-item-active .sx-command-item-icon {
667
+ color: var(--sx-color-cyan);
502
668
  }
503
669
 
504
- .salmex-cmd-item-content {
670
+ .sx-command-item-content {
505
671
  flex: 1;
506
672
  min-width: 0;
507
673
  display: flex;
508
674
  flex-direction: column;
675
+ gap: 1px;
509
676
  }
510
677
 
511
- .salmex-cmd-item-label {
512
- font-size: var(--salmex-font-size-sm);
513
- font-weight: 700;
678
+ .sx-command-item-label {
679
+ font-size: var(--sx-text-sm);
680
+ font-weight: 500;
514
681
  overflow: hidden;
515
682
  text-overflow: ellipsis;
516
683
  white-space: nowrap;
684
+ line-height: 1.4;
517
685
  }
518
686
 
519
- .salmex-cmd-item-desc {
520
- font-size: var(--salmex-font-size-xs);
521
- opacity: 0.7;
687
+ .sx-command-item-active .sx-command-item-label {
688
+ font-weight: 600;
689
+ }
690
+
691
+ /* Match highlighting */
692
+ .sx-command-match {
693
+ background: none;
694
+ color: var(--sx-color-cyan);
695
+ font-weight: 700;
696
+ }
697
+
698
+ .sx-command-item-active .sx-command-match {
699
+ color: var(--sx-color-cyan);
700
+ }
701
+
702
+ .sx-command-item-desc {
703
+ font-size: var(--sx-text-xs);
704
+ color: var(--sx-color-text-disabled);
522
705
  overflow: hidden;
523
706
  text-overflow: ellipsis;
524
707
  white-space: nowrap;
708
+ line-height: 1.3;
525
709
  }
526
710
 
527
- .salmex-cmd-item-active .salmex-cmd-item-desc {
528
- opacity: 0.85;
711
+ .sx-command-item-active .sx-command-item-desc {
712
+ color: var(--sx-color-text-secondary);
529
713
  }
530
714
 
531
- .salmex-cmd-item-shortcut {
715
+ .sx-command-item-shortcut {
532
716
  flex-shrink: 0;
533
- font-family: var(--salmex-font-mono);
534
- font-size: var(--salmex-font-size-xs);
535
- font-weight: 600;
536
- opacity: 0.6;
717
+ font-family: var(--sx-font-mono);
718
+ font-size: var(--sx-text-xs);
719
+ font-weight: 500;
720
+ padding: var(--sx-space-0-5) var(--sx-space-2);
721
+ background: var(--sx-color-surface-2);
722
+ border: 1px solid var(--sx-color-border);
723
+ border-radius: var(--sx-radius-sm);
724
+ color: var(--sx-color-text-disabled);
725
+ line-height: 1.3;
537
726
  }
538
727
 
539
- .salmex-cmd-item-active .salmex-cmd-item-shortcut {
540
- opacity: 0.85;
728
+ .sx-command-item-active .sx-command-item-shortcut {
729
+ background: var(--sx-color-surface-3);
730
+ border-color: var(--sx-color-border-strong);
731
+ color: var(--sx-color-text-secondary);
541
732
  }
542
733
 
543
734
  /* ========================================
544
735
  FOOTER
545
736
  ======================================== */
546
- .salmex-cmd-footer {
737
+ .sx-command-footer {
547
738
  display: flex;
548
739
  align-items: center;
549
- gap: var(--salmex-space-5);
550
- padding: var(--salmex-space-2) var(--salmex-space-5);
551
- border-top: 1px solid rgb(var(--salmex-border-default));
552
- font-size: var(--salmex-font-size-xs);
553
- color: rgb(var(--salmex-text-secondary));
554
- background: rgb(var(--salmex-window-surface));
740
+ gap: var(--sx-space-5);
741
+ padding: var(--sx-space-2-5) var(--sx-space-5);
742
+ border-top: 1px solid var(--sx-color-border);
743
+ font-size: var(--sx-text-xs);
744
+ color: var(--sx-color-text-disabled);
745
+ background: var(--sx-color-surface-2);
746
+ border-radius: 0 0 var(--sx-radius-lg) var(--sx-radius-lg);
555
747
  }
556
748
 
557
- .salmex-cmd-footer kbd {
558
- font-family: var(--salmex-font-mono);
559
- font-weight: 700;
560
- padding: 1px 4px;
561
- border: 1px solid rgb(var(--salmex-border-default));
562
- background: rgb(var(--salmex-button-face));
749
+ .sx-command-footer-hint {
750
+ display: flex;
751
+ align-items: center;
752
+ gap: var(--sx-space-1);
753
+ }
754
+
755
+ .sx-command-footer kbd {
756
+ font-family: var(--sx-font-mono);
757
+ font-weight: 600;
758
+ padding: 1px var(--sx-space-1);
759
+ border-radius: var(--sx-radius-sm);
760
+ background: var(--sx-color-surface-3);
761
+ border: 1px solid var(--sx-color-border);
563
762
  font-size: 0.9em;
763
+ color: var(--sx-color-text-disabled);
764
+ line-height: 1.3;
765
+ }
766
+
767
+ .sx-command-footer-count {
768
+ margin-left: auto;
769
+ font-family: var(--sx-font-mono);
770
+ font-weight: 500;
771
+ }
772
+
773
+ /* ========================================
774
+ SCREEN READER ONLY
775
+ ======================================== */
776
+ .sx-sr-only {
777
+ position: absolute;
778
+ width: 1px;
779
+ height: 1px;
780
+ padding: 0;
781
+ margin: -1px;
782
+ overflow: hidden;
783
+ clip: rect(0, 0, 0, 0);
784
+ white-space: nowrap;
785
+ border: 0;
564
786
  }
565
787
 
566
788
  /* ========================================
567
789
  REDUCED MOTION
568
790
  ======================================== */
569
791
  @media (prefers-reduced-motion: reduce) {
570
- .salmex-cmd-item {
792
+ .sx-command,
793
+ .sx-command-exit,
794
+ .sx-command-backdrop,
795
+ .sx-command-backdrop-exit {
796
+ animation: none;
797
+ }
798
+
799
+ .sx-command-item {
571
800
  transition: none;
572
801
  }
573
802
  }