@salmexio/ui 0.4.0 → 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 +97 -94
  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 -116
  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 +86 -151
  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 -104
  37. package/dist/forms/Checkbox/Checkbox.svelte.d.ts +1 -1
  38. package/dist/forms/Select/Select.svelte +137 -179
  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 +151 -167
  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 -39
  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 +407 -189
  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 +95 -181
  75. package/dist/navigation/Tabs/Tabs.svelte.d.ts +2 -2
  76. package/dist/primitives/Badge/Badge.svelte +83 -220
  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 +144 -195
  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 +197 -265
  92. package/package.json +5 -5
  93. package/dist/windowing/Window/Window.svelte +0 -637
  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 -425
  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,243 +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: 2px solid rgb(var(--salmex-border-dark));
369
- border-radius: var(--salmex-radius-lg);
466
+ background: var(--sx-color-surface);
467
+ border: 1px solid var(--sx-color-border-strong);
468
+ border-radius: var(--sx-radius-lg);
370
469
  box-shadow:
371
- inset 1px 1px 0 rgb(var(--salmex-button-highlight)),
372
- inset -1px -1px 0 rgb(var(--salmex-button-shadow)),
373
- 6px 6px 4px rgb(0 0 0 / 0.35);
374
- 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);
375
474
  overflow: hidden;
475
+ animation: sx-cmd-panel-in 200ms var(--sx-ease-out) both;
376
476
  }
377
477
 
378
- :global([data-theme='dark']) .salmex-cmd {
379
- box-shadow:
380
- inset 1px 1px 0 rgb(var(--salmex-button-highlight)),
381
- inset -1px -1px 0 rgb(var(--salmex-button-shadow)),
382
- 6px 6px 4px rgb(0 0 0 / 0.6);
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
+ }
383
502
  }
384
503
 
385
504
  /* ========================================
386
- SEARCH FIELD — Sunken, prominent
505
+ SEARCH FIELD
387
506
  ======================================== */
388
- .salmex-cmd-search {
507
+ .sx-command-search {
389
508
  display: flex;
390
509
  align-items: center;
391
- gap: var(--salmex-space-3);
392
- padding: var(--salmex-space-4) var(--salmex-space-5);
393
- border-bottom: 1px solid rgb(var(--salmex-border-default));
394
- background: rgb(var(--salmex-button-face));
395
- box-shadow:
396
- inset 1px 1px 0 rgb(var(--salmex-button-shadow)),
397
- inset -1px -1px 0 rgb(var(--salmex-button-highlight));
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);
398
513
  }
399
514
 
400
- .salmex-cmd-search-icon {
515
+ .sx-command-search-icon {
401
516
  flex-shrink: 0;
402
517
  display: flex;
403
- color: rgb(var(--salmex-text-secondary));
518
+ color: var(--sx-color-cyan);
519
+ opacity: 0.7;
404
520
  }
405
521
 
406
- .salmex-cmd-input {
522
+ .sx-command-input {
407
523
  flex: 1;
408
524
  min-width: 0;
409
525
  border: none;
410
526
  background: transparent;
411
527
  outline: none;
412
- font-family: var(--salmex-font-system);
413
- font-size: var(--salmex-font-size-base);
414
- color: rgb(var(--salmex-text-primary));
415
- padding: var(--salmex-space-2) 0;
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;
416
534
  }
417
535
 
418
- .salmex-cmd-input:focus-visible {
536
+ .sx-command-input:focus-visible {
419
537
  box-shadow: none;
538
+ outline: none;
420
539
  }
421
540
 
422
- .salmex-cmd-input::placeholder {
423
- color: rgb(var(--salmex-text-disabled));
541
+ .sx-command-input::placeholder {
542
+ color: var(--sx-color-text-disabled);
424
543
  }
425
544
 
426
- .salmex-cmd-shortcut-hint {
545
+ .sx-command-esc {
427
546
  flex-shrink: 0;
428
- font-family: var(--salmex-font-mono);
429
- font-size: var(--salmex-font-size-xs);
430
- font-weight: 700;
431
- padding: 2px 6px;
432
- border: 1px solid rgb(var(--salmex-border-default));
433
- border-radius: var(--salmex-radius-sm);
434
- background: rgb(var(--salmex-button-face));
435
- color: rgb(var(--salmex-text-secondary));
436
- box-shadow:
437
- inset 1px 1px 0 rgb(var(--salmex-button-highlight)),
438
- 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;
439
556
  }
440
557
 
441
558
  /* ========================================
442
559
  RESULTS LIST
443
560
  ======================================== */
444
- .salmex-cmd-results {
561
+ .sx-command-results {
445
562
  flex: 1;
446
563
  overflow-y: auto;
447
- padding: var(--salmex-space-1) 0;
448
- max-height: 300px;
564
+ overscroll-behavior: contain;
565
+ padding: var(--sx-space-2) 0;
566
+ max-height: 320px;
567
+ scroll-behavior: smooth;
449
568
  }
450
569
 
451
- .salmex-cmd-empty {
452
- padding: var(--salmex-space-6) var(--salmex-space-5);
570
+ .sx-command-results::-webkit-scrollbar {
571
+ width: 4px;
572
+ }
573
+
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);
453
589
  text-align: center;
454
- font-size: var(--salmex-font-size-sm);
455
- color: rgb(var(--salmex-text-disabled));
456
- 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;
457
606
  }
458
607
 
459
608
  /* ========================================
460
609
  GROUP LABEL
461
610
  ======================================== */
462
- .salmex-cmd-group-label {
463
- padding: var(--salmex-space-2) var(--salmex-space-5) var(--salmex-space-1);
464
- margin-top: var(--salmex-space-1);
465
- font-family: var(--salmex-font-mono);
466
- 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);
467
615
  font-weight: 600;
468
- letter-spacing: 0.8px;
469
- color: rgb(var(--salmex-text-disabled));
470
- border-top: 1px solid rgb(var(--salmex-border-light));
616
+ letter-spacing: var(--sx-tracking-wider);
617
+ text-transform: uppercase;
618
+ color: var(--sx-color-text-disabled);
471
619
  user-select: none;
472
620
  }
473
621
 
474
- .salmex-cmd-group-label:first-child {
475
- border-top: none;
622
+ .sx-command-group-label:first-child {
476
623
  margin-top: 0;
477
624
  }
478
625
 
479
626
  /* ========================================
480
627
  COMMAND ITEM
481
628
  ======================================== */
482
- .salmex-cmd-item {
629
+ .sx-command-item {
483
630
  display: flex;
484
631
  align-items: center;
485
- gap: var(--salmex-space-3);
486
- 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);
487
634
  cursor: pointer;
488
635
  user-select: none;
489
- 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;
490
639
  }
491
640
 
492
- .salmex-cmd-item-active {
493
- background: rgb(var(--salmex-electric-blue));
494
- 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);
495
645
  }
496
646
 
497
- :global([data-theme='dark']) .salmex-cmd-item-active {
498
- background: rgb(var(--salmex-primary-light));
499
- color: rgb(var(--salmex-midnight-black));
647
+ .sx-command-item:active:not(.sx-command-item-disabled) {
648
+ background: var(--sx-color-cyan-active);
500
649
  }
501
650
 
502
- .salmex-cmd-item-disabled {
651
+ .sx-command-item-disabled {
503
652
  opacity: 0.4;
504
653
  cursor: not-allowed;
505
654
  }
506
655
 
507
- .salmex-cmd-item-icon {
656
+ .sx-command-item-icon {
508
657
  flex-shrink: 0;
509
658
  width: 20px;
510
- text-align: center;
511
- 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);
512
668
  }
513
669
 
514
- .salmex-cmd-item-content {
670
+ .sx-command-item-content {
515
671
  flex: 1;
516
672
  min-width: 0;
517
673
  display: flex;
518
674
  flex-direction: column;
675
+ gap: 1px;
519
676
  }
520
677
 
521
- .salmex-cmd-item-label {
522
- font-size: var(--salmex-font-size-sm);
523
- font-weight: 700;
678
+ .sx-command-item-label {
679
+ font-size: var(--sx-text-sm);
680
+ font-weight: 500;
524
681
  overflow: hidden;
525
682
  text-overflow: ellipsis;
526
683
  white-space: nowrap;
684
+ line-height: 1.4;
527
685
  }
528
686
 
529
- .salmex-cmd-item-desc {
530
- font-size: var(--salmex-font-size-xs);
531
- 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);
532
705
  overflow: hidden;
533
706
  text-overflow: ellipsis;
534
707
  white-space: nowrap;
708
+ line-height: 1.3;
535
709
  }
536
710
 
537
- .salmex-cmd-item-active .salmex-cmd-item-desc {
538
- opacity: 0.85;
711
+ .sx-command-item-active .sx-command-item-desc {
712
+ color: var(--sx-color-text-secondary);
539
713
  }
540
714
 
541
- .salmex-cmd-item-shortcut {
715
+ .sx-command-item-shortcut {
542
716
  flex-shrink: 0;
543
- font-family: var(--salmex-font-mono);
544
- font-size: var(--salmex-font-size-xs);
545
- font-weight: 600;
546
- 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;
547
726
  }
548
727
 
549
- .salmex-cmd-item-active .salmex-cmd-item-shortcut {
550
- 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);
551
732
  }
552
733
 
553
734
  /* ========================================
554
735
  FOOTER
555
736
  ======================================== */
556
- .salmex-cmd-footer {
737
+ .sx-command-footer {
557
738
  display: flex;
558
739
  align-items: center;
559
- gap: var(--salmex-space-5);
560
- padding: var(--salmex-space-2) var(--salmex-space-5);
561
- border-top: 1px solid rgb(var(--salmex-border-default));
562
- font-size: var(--salmex-font-size-xs);
563
- color: rgb(var(--salmex-text-secondary));
564
- 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);
565
747
  }
566
748
 
567
- .salmex-cmd-footer kbd {
568
- font-family: var(--salmex-font-mono);
569
- font-weight: 700;
570
- padding: 1px 4px;
571
- border: 1px solid rgb(var(--salmex-border-default));
572
- border-radius: var(--salmex-radius-sm);
573
- 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);
574
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;
575
786
  }
576
787
 
577
788
  /* ========================================
578
789
  REDUCED MOTION
579
790
  ======================================== */
580
791
  @media (prefers-reduced-motion: reduce) {
581
- .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 {
582
800
  transition: none;
583
801
  }
584
802
  }