@salmexio/ui 0.1.1 → 0.3.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 (76) hide show
  1. package/dist/dialogs/ContextMenu/ContextMenu.svelte +521 -0
  2. package/dist/dialogs/ContextMenu/ContextMenu.svelte.d.ts +53 -0
  3. package/dist/dialogs/ContextMenu/ContextMenu.svelte.d.ts.map +1 -0
  4. package/dist/dialogs/ContextMenu/index.d.ts +3 -0
  5. package/dist/dialogs/ContextMenu/index.d.ts.map +1 -0
  6. package/dist/dialogs/ContextMenu/index.js +1 -0
  7. package/dist/dialogs/Modal/Modal.svelte +140 -140
  8. package/dist/dialogs/Modal/Modal.svelte.d.ts.map +1 -1
  9. package/dist/dialogs/index.d.ts +2 -0
  10. package/dist/dialogs/index.d.ts.map +1 -1
  11. package/dist/dialogs/index.js +1 -0
  12. package/dist/feedback/Alert/Alert.svelte +63 -124
  13. package/dist/feedback/Alert/Alert.svelte.d.ts +1 -1
  14. package/dist/feedback/Alert/Alert.svelte.d.ts.map +1 -1
  15. package/dist/feedback/Spinner/Spinner.svelte +55 -55
  16. package/dist/feedback/Spinner/Spinner.svelte.d.ts.map +1 -1
  17. package/dist/forms/Checkbox/Checkbox.svelte +64 -64
  18. package/dist/forms/Checkbox/Checkbox.svelte.d.ts.map +1 -1
  19. package/dist/forms/Select/Select.svelte +883 -0
  20. package/dist/forms/Select/Select.svelte.d.ts +68 -0
  21. package/dist/forms/Select/Select.svelte.d.ts.map +1 -0
  22. package/dist/forms/Select/index.d.ts +3 -0
  23. package/dist/forms/Select/index.d.ts.map +1 -0
  24. package/dist/forms/Select/index.js +1 -0
  25. package/dist/forms/TextInput/TextInput.svelte +141 -141
  26. package/dist/forms/TextInput/TextInput.svelte.d.ts.map +1 -1
  27. package/dist/forms/index.d.ts +2 -0
  28. package/dist/forms/index.d.ts.map +1 -1
  29. package/dist/forms/index.js +1 -0
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1 -0
  33. package/dist/layout/Card/Card.svelte +67 -207
  34. package/dist/layout/Card/Card.svelte.d.ts +3 -9
  35. package/dist/layout/Card/Card.svelte.d.ts.map +1 -1
  36. package/dist/layout/Container/Container.svelte +34 -34
  37. package/dist/layout/Container/Container.svelte.d.ts.map +1 -1
  38. package/dist/navigation/CommandPalette/CommandPalette.svelte +574 -0
  39. package/dist/navigation/CommandPalette/CommandPalette.svelte.d.ts +47 -0
  40. package/dist/navigation/CommandPalette/CommandPalette.svelte.d.ts.map +1 -0
  41. package/dist/navigation/CommandPalette/index.d.ts +3 -0
  42. package/dist/navigation/CommandPalette/index.d.ts.map +1 -0
  43. package/dist/navigation/CommandPalette/index.js +1 -0
  44. package/dist/navigation/Tabs/Tabs.svelte +100 -101
  45. package/dist/navigation/Tabs/Tabs.svelte.d.ts.map +1 -1
  46. package/dist/navigation/index.d.ts +2 -0
  47. package/dist/navigation/index.d.ts.map +1 -1
  48. package/dist/navigation/index.js +1 -0
  49. package/dist/primitives/Badge/Badge.svelte +77 -48
  50. package/dist/primitives/Badge/Badge.svelte.d.ts +0 -2
  51. package/dist/primitives/Badge/Badge.svelte.d.ts.map +1 -1
  52. package/dist/primitives/Button/Button.svelte +86 -60
  53. package/dist/primitives/Button/Button.svelte.d.ts +1 -1
  54. package/dist/primitives/Button/Button.svelte.d.ts.map +1 -1
  55. package/dist/routes/+layout.svelte +1 -1
  56. package/dist/styles/tokens.css +96 -90
  57. package/dist/utils/keyboard.js +3 -3
  58. package/dist/windowing/Window/Window.svelte +602 -0
  59. package/dist/windowing/Window/Window.svelte.d.ts +65 -0
  60. package/dist/windowing/Window/Window.svelte.d.ts.map +1 -0
  61. package/dist/windowing/Window/index.d.ts +2 -0
  62. package/dist/windowing/Window/index.d.ts.map +1 -0
  63. package/dist/windowing/Window/index.js +1 -0
  64. package/dist/windowing/WindowManager/WindowManager.svelte +410 -0
  65. package/dist/windowing/WindowManager/WindowManager.svelte.d.ts +38 -0
  66. package/dist/windowing/WindowManager/WindowManager.svelte.d.ts.map +1 -0
  67. package/dist/windowing/WindowManager/index.d.ts +2 -0
  68. package/dist/windowing/WindowManager/index.d.ts.map +1 -0
  69. package/dist/windowing/WindowManager/index.js +1 -0
  70. package/dist/windowing/index.d.ts +5 -0
  71. package/dist/windowing/index.d.ts.map +1 -0
  72. package/dist/windowing/index.js +3 -0
  73. package/dist/windowing/windowStore.svelte.d.ts +49 -0
  74. package/dist/windowing/windowStore.svelte.d.ts.map +1 -0
  75. package/dist/windowing/windowStore.svelte.js +170 -0
  76. package/package.json +1 -1
@@ -0,0 +1,521 @@
1
+ <!--
2
+ @component ContextMenu
3
+
4
+ Win2K × Basquiat — Right-click context menu with raised panel, keyboard navigation,
5
+ separators, section headers, submenus, and keyboard shortcut hints.
6
+
7
+ @example
8
+ <ContextMenu
9
+ items={[
10
+ { type: 'item', label: 'Cut', shortcut: 'Ctrl+X', action: handleCut },
11
+ { type: 'item', label: 'Copy', shortcut: 'Ctrl+C', action: handleCopy },
12
+ { type: 'separator' },
13
+ { type: 'item', label: 'Paste', shortcut: 'Ctrl+V', action: handlePaste },
14
+ ]}
15
+ bind:this={menu}
16
+ />
17
+ <div oncontextmenu={(e) => { e.preventDefault(); menu.open(e.clientX, e.clientY); }}>
18
+ Right click here
19
+ </div>
20
+ -->
21
+ <script lang="ts" module>
22
+ export interface MenuItem {
23
+ type: 'item';
24
+ label: string;
25
+ shortcut?: string;
26
+ disabled?: boolean;
27
+ icon?: string;
28
+ action?: () => void;
29
+ children?: MenuItemOrSeparator[];
30
+ }
31
+
32
+ export interface MenuSeparator {
33
+ type: 'separator';
34
+ }
35
+
36
+ export interface MenuGroup {
37
+ type: 'group';
38
+ label: string;
39
+ items: MenuItemOrSeparator[];
40
+ }
41
+
42
+ export type MenuItemOrSeparator = MenuItem | MenuSeparator | MenuGroup;
43
+ </script>
44
+
45
+ <script lang="ts">
46
+ import { cn } from '../../utils/cn.js';
47
+ import { Keys } from '../../utils/keyboard.js';
48
+ import { onMount, tick } from 'svelte';
49
+
50
+ interface Props {
51
+ /** Menu items */
52
+ items: MenuItemOrSeparator[];
53
+ /** Additional CSS class */
54
+ class?: string;
55
+ /** Test ID */
56
+ testId?: string;
57
+ }
58
+
59
+ let {
60
+ items,
61
+ class: className = '',
62
+ testId
63
+ }: Props = $props();
64
+
65
+ let isOpen = $state(false);
66
+ let posX = $state(0);
67
+ let posY = $state(0);
68
+ let activeIndex = $state(-1);
69
+ let menuEl = $state<HTMLDivElement | null>(null);
70
+ let openSubmenuIndex = $state(-1);
71
+ let submenuPosX = $state(0);
72
+ let submenuPosY = $state(0);
73
+ let submenuActiveIndex = $state(-1);
74
+
75
+ // Flatten to only actionable items for keyboard nav indices
76
+ const actionableIndices = $derived(
77
+ items.map((item, i) => ({ i, actionable: item.type === 'item' || item.type === 'group' }))
78
+ .filter((x) => x.actionable)
79
+ .map((x) => x.i)
80
+ );
81
+
82
+ export function open(x: number, y: number) {
83
+ posX = x;
84
+ posY = y;
85
+ isOpen = true;
86
+ activeIndex = -1;
87
+ openSubmenuIndex = -1;
88
+ // Clamping and focus are handled by the portal $effect
89
+ }
90
+
91
+ export function close() {
92
+ isOpen = false;
93
+ activeIndex = -1;
94
+ openSubmenuIndex = -1;
95
+ }
96
+
97
+ function navigateUp() {
98
+ const pos = actionableIndices.indexOf(activeIndex);
99
+ if (pos > 0) activeIndex = actionableIndices[pos - 1];
100
+ else activeIndex = actionableIndices[actionableIndices.length - 1];
101
+ openSubmenuIndex = -1;
102
+ }
103
+
104
+ function navigateDown() {
105
+ const pos = actionableIndices.indexOf(activeIndex);
106
+ if (pos < actionableIndices.length - 1) activeIndex = actionableIndices[pos + 1];
107
+ else activeIndex = actionableIndices[0];
108
+ openSubmenuIndex = -1;
109
+ }
110
+
111
+ function activateItem(item: MenuItemOrSeparator) {
112
+ if (item.type !== 'item' || item.disabled) return;
113
+ if (item.children && item.children.length > 0) {
114
+ openSubmenu(items.indexOf(item));
115
+ return;
116
+ }
117
+ item.action?.();
118
+ close();
119
+ }
120
+
121
+ function openSubmenu(index: number) {
122
+ const item = items[index];
123
+ if (item.type !== 'item' || !item.children?.length) return;
124
+ openSubmenuIndex = index;
125
+ submenuActiveIndex = -1;
126
+ // Position submenu to the right of the item
127
+ requestAnimationFrame(() => {
128
+ if (!menuEl) return;
129
+ const itemEl = menuEl.querySelector(`[data-index="${index}"]`) as HTMLElement;
130
+ if (!itemEl) return;
131
+ const itemRect = itemEl.getBoundingClientRect();
132
+ const menuRect = menuEl.getBoundingClientRect();
133
+ submenuPosX = menuRect.width - 4;
134
+ submenuPosY = itemRect.top - menuRect.top;
135
+ });
136
+ }
137
+
138
+ function handleKeydown(e: KeyboardEvent) {
139
+ if (openSubmenuIndex >= 0) {
140
+ handleSubmenuKeydown(e);
141
+ return;
142
+ }
143
+ switch (e.key) {
144
+ case Keys.ArrowDown:
145
+ e.preventDefault();
146
+ navigateDown();
147
+ break;
148
+ case Keys.ArrowUp:
149
+ e.preventDefault();
150
+ navigateUp();
151
+ break;
152
+ case Keys.Enter:
153
+ case Keys.Space:
154
+ e.preventDefault();
155
+ if (activeIndex >= 0) activateItem(items[activeIndex]);
156
+ break;
157
+ case Keys.ArrowRight:
158
+ e.preventDefault();
159
+ if (activeIndex >= 0) {
160
+ const item = items[activeIndex];
161
+ if (item.type === 'item' && item.children?.length) openSubmenu(activeIndex);
162
+ }
163
+ break;
164
+ case Keys.Escape:
165
+ e.preventDefault();
166
+ close();
167
+ break;
168
+ case Keys.Home:
169
+ e.preventDefault();
170
+ activeIndex = actionableIndices[0] ?? -1;
171
+ break;
172
+ case Keys.End:
173
+ e.preventDefault();
174
+ activeIndex = actionableIndices[actionableIndices.length - 1] ?? -1;
175
+ break;
176
+ }
177
+ }
178
+
179
+ function handleSubmenuKeydown(e: KeyboardEvent) {
180
+ const parentItem = items[openSubmenuIndex];
181
+ if (parentItem.type !== 'item' || !parentItem.children) return;
182
+ const subItems = parentItem.children.filter((i) => i.type === 'item') as MenuItem[];
183
+ const subActionable = parentItem.children.map((item, i) => ({ i, actionable: item.type === 'item' })).filter((x) => x.actionable).map((x) => x.i);
184
+
185
+ switch (e.key) {
186
+ case Keys.ArrowDown:
187
+ e.preventDefault();
188
+ {
189
+ const pos = subActionable.indexOf(submenuActiveIndex);
190
+ submenuActiveIndex = pos < subActionable.length - 1 ? subActionable[pos + 1] : subActionable[0];
191
+ }
192
+ break;
193
+ case Keys.ArrowUp:
194
+ e.preventDefault();
195
+ {
196
+ const pos = subActionable.indexOf(submenuActiveIndex);
197
+ submenuActiveIndex = pos > 0 ? subActionable[pos - 1] : subActionable[subActionable.length - 1];
198
+ }
199
+ break;
200
+ case Keys.Enter:
201
+ case Keys.Space:
202
+ e.preventDefault();
203
+ if (submenuActiveIndex >= 0) {
204
+ const subItem = parentItem.children[submenuActiveIndex];
205
+ if (subItem.type === 'item' && !subItem.disabled) {
206
+ subItem.action?.();
207
+ close();
208
+ }
209
+ }
210
+ break;
211
+ case Keys.ArrowLeft:
212
+ case Keys.Escape:
213
+ e.preventDefault();
214
+ openSubmenuIndex = -1;
215
+ submenuActiveIndex = -1;
216
+ break;
217
+ }
218
+ }
219
+
220
+ function handleClickOutside(e: MouseEvent) {
221
+ const target = e.target as Node;
222
+ if (menuEl && !menuEl.contains(target)) close();
223
+ }
224
+
225
+ // Portal: move menu DOM node to document.body to escape transform/overflow ancestors
226
+ $effect(() => {
227
+ if (menuEl && isOpen) {
228
+ document.body.appendChild(menuEl);
229
+ // Re-clamp after portal move
230
+ tick().then(() => {
231
+ if (!menuEl) return;
232
+ const rect = menuEl.getBoundingClientRect();
233
+ if (rect.right > window.innerWidth) posX = window.innerWidth - rect.width - 4;
234
+ if (rect.bottom > window.innerHeight) posY = window.innerHeight - rect.height - 4;
235
+ if (posX < 0) posX = 4;
236
+ if (posY < 0) posY = 4;
237
+ menuEl.focus();
238
+ });
239
+ return () => {
240
+ if (menuEl?.parentNode === document.body) {
241
+ document.body.removeChild(menuEl);
242
+ }
243
+ };
244
+ }
245
+ });
246
+
247
+ onMount(() => {
248
+ document.addEventListener('mousedown', handleClickOutside);
249
+ return () => {
250
+ document.removeEventListener('mousedown', handleClickOutside);
251
+ if (menuEl?.parentNode === document.body) {
252
+ document.body.removeChild(menuEl);
253
+ }
254
+ };
255
+ });
256
+ </script>
257
+
258
+ {#if isOpen}
259
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions a11y_click_events_have_key_events -->
260
+ <div
261
+ bind:this={menuEl}
262
+ class={cn('salmex-ctx', className)}
263
+ style="left: {posX}px; top: {posY}px;"
264
+ role="menu"
265
+ tabindex="-1"
266
+ data-testid={testId}
267
+ onkeydown={handleKeydown}
268
+ >
269
+ {#each items as item, i}
270
+ {#if item.type === 'separator'}
271
+ <div class="salmex-ctx-separator" role="separator"></div>
272
+ {:else if item.type === 'group'}
273
+ <div class="salmex-ctx-group-label">{item.label}</div>
274
+ {#each item.items as groupItem}
275
+ {#if groupItem.type === 'item'}
276
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions a11y_click_events_have_key_events -->
277
+ <div
278
+ class={cn(
279
+ 'salmex-ctx-item',
280
+ groupItem.disabled && 'salmex-ctx-item-disabled'
281
+ )}
282
+ role="menuitem"
283
+ tabindex="-1"
284
+ aria-disabled={groupItem.disabled || undefined}
285
+ onclick={() => { if (!groupItem.disabled) { groupItem.action?.(); close(); } }}
286
+ onkeydown={handleKeydown}
287
+ onmouseenter={() => { if (!groupItem.disabled) { activeIndex = -1; openSubmenuIndex = -1; } }}
288
+ >
289
+ {#if groupItem.icon}
290
+ <span class="salmex-ctx-icon" aria-hidden="true">{groupItem.icon}</span>
291
+ {:else}
292
+ <span class="salmex-ctx-icon-spacer"></span>
293
+ {/if}
294
+ <span class="salmex-ctx-label">{groupItem.label}</span>
295
+ {#if groupItem.shortcut}
296
+ <span class="salmex-ctx-shortcut">{groupItem.shortcut}</span>
297
+ {/if}
298
+ </div>
299
+ {:else if groupItem.type === 'separator'}
300
+ <div class="salmex-ctx-separator" role="separator"></div>
301
+ {/if}
302
+ {/each}
303
+ {:else if item.type === 'item'}
304
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions a11y_click_events_have_key_events -->
305
+ <div
306
+ class={cn(
307
+ 'salmex-ctx-item',
308
+ i === activeIndex && 'salmex-ctx-item-active',
309
+ item.disabled && 'salmex-ctx-item-disabled'
310
+ )}
311
+ role="menuitem"
312
+ tabindex="-1"
313
+ aria-disabled={item.disabled || undefined}
314
+ data-index={i}
315
+ onmouseenter={() => { if (!item.disabled) { activeIndex = i; if (item.children?.length) openSubmenu(i); else openSubmenuIndex = -1; } }}
316
+ onclick={() => activateItem(item)}
317
+ onkeydown={handleKeydown}
318
+ >
319
+ {#if item.icon}
320
+ <span class="salmex-ctx-icon" aria-hidden="true">{item.icon}</span>
321
+ {:else}
322
+ <span class="salmex-ctx-icon-spacer"></span>
323
+ {/if}
324
+ <span class="salmex-ctx-label">{item.label}</span>
325
+ {#if item.children?.length}
326
+ <span class="salmex-ctx-submenu-arrow" aria-hidden="true">&#9654;</span>
327
+ {:else if item.shortcut}
328
+ <span class="salmex-ctx-shortcut">{item.shortcut}</span>
329
+ {/if}
330
+ </div>
331
+
332
+ <!-- Submenu -->
333
+ {#if openSubmenuIndex === i && item.children?.length}
334
+ <div
335
+ class="salmex-ctx salmex-ctx-submenu"
336
+ style="left: {submenuPosX}px; top: {submenuPosY}px;"
337
+ role="menu"
338
+ >
339
+ {#each item.children as subItem, si}
340
+ {#if subItem.type === 'separator'}
341
+ <div class="salmex-ctx-separator" role="separator"></div>
342
+ {:else if subItem.type === 'item'}
343
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions a11y_click_events_have_key_events -->
344
+ <div
345
+ class={cn(
346
+ 'salmex-ctx-item',
347
+ si === submenuActiveIndex && 'salmex-ctx-item-active',
348
+ subItem.disabled && 'salmex-ctx-item-disabled'
349
+ )}
350
+ role="menuitem"
351
+ tabindex="-1"
352
+ aria-disabled={subItem.disabled || undefined}
353
+ onmouseenter={() => { if (!subItem.disabled) submenuActiveIndex = si; }}
354
+ onclick={() => { if (!subItem.disabled) { subItem.action?.(); close(); } }}
355
+ onkeydown={handleKeydown}
356
+ >
357
+ {#if subItem.icon}
358
+ <span class="salmex-ctx-icon" aria-hidden="true">{subItem.icon}</span>
359
+ {:else}
360
+ <span class="salmex-ctx-icon-spacer"></span>
361
+ {/if}
362
+ <span class="salmex-ctx-label">{subItem.label}</span>
363
+ {#if subItem.shortcut}
364
+ <span class="salmex-ctx-shortcut">{subItem.shortcut}</span>
365
+ {/if}
366
+ </div>
367
+ {/if}
368
+ {/each}
369
+ </div>
370
+ {/if}
371
+ {/if}
372
+ {/each}
373
+ </div>
374
+ {/if}
375
+
376
+ <style>
377
+ /* ========================================
378
+ MENU PANEL — Raised, bold shadow
379
+ ======================================== */
380
+ .salmex-ctx {
381
+ position: fixed;
382
+ z-index: var(--salmex-z-popover);
383
+ min-width: 180px;
384
+ max-width: 320px;
385
+ background: rgb(var(--salmex-bg-primary));
386
+ border: 3px solid rgb(var(--salmex-border-dark));
387
+ box-shadow:
388
+ inset 1px 1px 0 rgb(var(--salmex-button-highlight)),
389
+ inset -1px -1px 0 rgb(var(--salmex-button-shadow)),
390
+ 5px 5px 0 rgb(0 0 0 / 0.35);
391
+ padding: var(--salmex-space-1) 0;
392
+ outline: none;
393
+ font-family: var(--salmex-font-system);
394
+ }
395
+
396
+ :global([data-theme='dark']) .salmex-ctx {
397
+ box-shadow:
398
+ inset 1px 1px 0 rgb(var(--salmex-button-highlight)),
399
+ inset -1px -1px 0 rgb(var(--salmex-button-shadow)),
400
+ 5px 5px 0 rgb(0 0 0 / 0.7);
401
+ }
402
+
403
+ /* ========================================
404
+ SUBMENU — Positioned relative to parent
405
+ ======================================== */
406
+ .salmex-ctx-submenu {
407
+ position: absolute;
408
+ }
409
+
410
+ /* ========================================
411
+ MENU ITEM
412
+ ======================================== */
413
+ .salmex-ctx-item {
414
+ display: flex;
415
+ align-items: center;
416
+ gap: var(--salmex-space-2);
417
+ padding: var(--salmex-space-2) var(--salmex-space-4);
418
+ font-size: var(--salmex-font-size-sm);
419
+ font-weight: 600;
420
+ color: rgb(var(--salmex-text-primary));
421
+ cursor: pointer;
422
+ user-select: none;
423
+ white-space: nowrap;
424
+ transition: background var(--salmex-transition-fast);
425
+ }
426
+
427
+ .salmex-ctx-item-active {
428
+ background: rgb(var(--salmex-electric-blue));
429
+ color: rgb(var(--salmex-chalk-white));
430
+ }
431
+
432
+ :global([data-theme='dark']) .salmex-ctx-item-active {
433
+ background: rgb(var(--salmex-primary-light));
434
+ color: rgb(var(--salmex-midnight-black));
435
+ }
436
+
437
+ .salmex-ctx-item-disabled {
438
+ opacity: 0.4;
439
+ cursor: not-allowed;
440
+ }
441
+
442
+ /* ========================================
443
+ ICON
444
+ ======================================== */
445
+ .salmex-ctx-icon {
446
+ flex-shrink: 0;
447
+ width: 16px;
448
+ text-align: center;
449
+ font-size: 14px;
450
+ }
451
+
452
+ .salmex-ctx-icon-spacer {
453
+ flex-shrink: 0;
454
+ width: 16px;
455
+ }
456
+
457
+ /* ========================================
458
+ LABEL & SHORTCUT
459
+ ======================================== */
460
+ .salmex-ctx-label {
461
+ flex: 1;
462
+ }
463
+
464
+ .salmex-ctx-shortcut {
465
+ margin-left: var(--salmex-space-6);
466
+ font-size: var(--salmex-font-size-xs);
467
+ font-family: var(--salmex-font-mono);
468
+ font-weight: 600;
469
+ opacity: 0.6;
470
+ text-align: right;
471
+ }
472
+
473
+ .salmex-ctx-item-active .salmex-ctx-shortcut {
474
+ opacity: 0.85;
475
+ }
476
+
477
+ /* ========================================
478
+ SUBMENU ARROW
479
+ ======================================== */
480
+ .salmex-ctx-submenu-arrow {
481
+ margin-left: var(--salmex-space-4);
482
+ font-size: 8px;
483
+ opacity: 0.7;
484
+ }
485
+
486
+ .salmex-ctx-item-active .salmex-ctx-submenu-arrow {
487
+ opacity: 1;
488
+ }
489
+
490
+ /* ========================================
491
+ SEPARATOR
492
+ ======================================== */
493
+ .salmex-ctx-separator {
494
+ height: 0;
495
+ margin: var(--salmex-space-1) var(--salmex-space-2);
496
+ border-top: 1px solid rgb(var(--salmex-button-shadow));
497
+ border-bottom: 1px solid rgb(var(--salmex-button-highlight));
498
+ }
499
+
500
+ /* ========================================
501
+ GROUP LABEL
502
+ ======================================== */
503
+ .salmex-ctx-group-label {
504
+ padding: var(--salmex-space-2) var(--salmex-space-4) var(--salmex-space-1);
505
+ font-size: var(--salmex-font-size-xs);
506
+ font-weight: 700;
507
+ text-transform: uppercase;
508
+ letter-spacing: 0.4px;
509
+ color: rgb(var(--salmex-text-secondary));
510
+ user-select: none;
511
+ }
512
+
513
+ /* ========================================
514
+ REDUCED MOTION
515
+ ======================================== */
516
+ @media (prefers-reduced-motion: reduce) {
517
+ .salmex-ctx-item {
518
+ transition: none;
519
+ }
520
+ }
521
+ </style>
@@ -0,0 +1,53 @@
1
+ export interface MenuItem {
2
+ type: 'item';
3
+ label: string;
4
+ shortcut?: string;
5
+ disabled?: boolean;
6
+ icon?: string;
7
+ action?: () => void;
8
+ children?: MenuItemOrSeparator[];
9
+ }
10
+ export interface MenuSeparator {
11
+ type: 'separator';
12
+ }
13
+ export interface MenuGroup {
14
+ type: 'group';
15
+ label: string;
16
+ items: MenuItemOrSeparator[];
17
+ }
18
+ export type MenuItemOrSeparator = MenuItem | MenuSeparator | MenuGroup;
19
+ interface Props {
20
+ /** Menu items */
21
+ items: MenuItemOrSeparator[];
22
+ /** Additional CSS class */
23
+ class?: string;
24
+ /** Test ID */
25
+ testId?: string;
26
+ }
27
+ /**
28
+ * ContextMenu
29
+ *
30
+ * Win2K × Basquiat — Right-click context menu with raised panel, keyboard navigation,
31
+ * separators, section headers, submenus, and keyboard shortcut hints.
32
+ *
33
+ * @example
34
+ * <ContextMenu
35
+ * items={[
36
+ * { type: 'item', label: 'Cut', shortcut: 'Ctrl+X', action: handleCut },
37
+ * { type: 'item', label: 'Copy', shortcut: 'Ctrl+C', action: handleCopy },
38
+ * { type: 'separator' },
39
+ * { type: 'item', label: 'Paste', shortcut: 'Ctrl+V', action: handlePaste },
40
+ * ]}
41
+ * bind:this={menu}
42
+ * />
43
+ * <div oncontextmenu={(e) => { e.preventDefault(); menu.open(e.clientX, e.clientY); }}>
44
+ * Right click here
45
+ * </div>
46
+ */
47
+ declare const ContextMenu: import("svelte").Component<Props, {
48
+ open: (x: number, y: number) => void;
49
+ close: () => void;
50
+ }, "">;
51
+ type ContextMenu = ReturnType<typeof ContextMenu>;
52
+ export default ContextMenu;
53
+ //# sourceMappingURL=ContextMenu.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ContextMenu.svelte.d.ts","sourceRoot":"","sources":["../../../src/dialogs/ContextMenu/ContextMenu.svelte.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,QAAQ;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,CAAC,EAAE,mBAAmB,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,WAAW,CAAC;CAClB;AAED,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,mBAAmB,EAAE,CAAC;CAC7B;AAED,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,aAAa,GAAG,SAAS,CAAC;AAQvE,UAAU,KAAK;IACd,iBAAiB;IACjB,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAC7B,2BAA2B;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAsSD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,QAAA,MAAM,WAAW;cA5RC,MAAM,KAAK,MAAM;;MA4RsB,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { default as ContextMenu } from './ContextMenu.svelte';
2
+ export type { MenuItem, MenuSeparator, MenuGroup, MenuItemOrSeparator } from './ContextMenu.svelte';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/dialogs/ContextMenu/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,SAAS,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1 @@
1
+ export { default as ContextMenu } from './ContextMenu.svelte';