@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.
- package/dist/dialogs/ContextMenu/ContextMenu.svelte +521 -0
- package/dist/dialogs/ContextMenu/ContextMenu.svelte.d.ts +53 -0
- package/dist/dialogs/ContextMenu/ContextMenu.svelte.d.ts.map +1 -0
- package/dist/dialogs/ContextMenu/index.d.ts +3 -0
- package/dist/dialogs/ContextMenu/index.d.ts.map +1 -0
- package/dist/dialogs/ContextMenu/index.js +1 -0
- package/dist/dialogs/Modal/Modal.svelte +140 -140
- package/dist/dialogs/Modal/Modal.svelte.d.ts.map +1 -1
- package/dist/dialogs/index.d.ts +2 -0
- package/dist/dialogs/index.d.ts.map +1 -1
- package/dist/dialogs/index.js +1 -0
- package/dist/feedback/Alert/Alert.svelte +63 -124
- package/dist/feedback/Alert/Alert.svelte.d.ts +1 -1
- package/dist/feedback/Alert/Alert.svelte.d.ts.map +1 -1
- package/dist/feedback/Spinner/Spinner.svelte +55 -55
- package/dist/feedback/Spinner/Spinner.svelte.d.ts.map +1 -1
- package/dist/forms/Checkbox/Checkbox.svelte +64 -64
- package/dist/forms/Checkbox/Checkbox.svelte.d.ts.map +1 -1
- package/dist/forms/Select/Select.svelte +883 -0
- package/dist/forms/Select/Select.svelte.d.ts +68 -0
- package/dist/forms/Select/Select.svelte.d.ts.map +1 -0
- package/dist/forms/Select/index.d.ts +3 -0
- package/dist/forms/Select/index.d.ts.map +1 -0
- package/dist/forms/Select/index.js +1 -0
- package/dist/forms/TextInput/TextInput.svelte +141 -141
- package/dist/forms/TextInput/TextInput.svelte.d.ts.map +1 -1
- package/dist/forms/index.d.ts +2 -0
- package/dist/forms/index.d.ts.map +1 -1
- package/dist/forms/index.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/layout/Card/Card.svelte +67 -207
- package/dist/layout/Card/Card.svelte.d.ts +3 -9
- package/dist/layout/Card/Card.svelte.d.ts.map +1 -1
- package/dist/layout/Container/Container.svelte +34 -34
- package/dist/layout/Container/Container.svelte.d.ts.map +1 -1
- package/dist/navigation/CommandPalette/CommandPalette.svelte +574 -0
- package/dist/navigation/CommandPalette/CommandPalette.svelte.d.ts +47 -0
- package/dist/navigation/CommandPalette/CommandPalette.svelte.d.ts.map +1 -0
- package/dist/navigation/CommandPalette/index.d.ts +3 -0
- package/dist/navigation/CommandPalette/index.d.ts.map +1 -0
- package/dist/navigation/CommandPalette/index.js +1 -0
- package/dist/navigation/Tabs/Tabs.svelte +100 -101
- package/dist/navigation/Tabs/Tabs.svelte.d.ts.map +1 -1
- package/dist/navigation/index.d.ts +2 -0
- package/dist/navigation/index.d.ts.map +1 -1
- package/dist/navigation/index.js +1 -0
- package/dist/primitives/Badge/Badge.svelte +77 -48
- package/dist/primitives/Badge/Badge.svelte.d.ts +0 -2
- package/dist/primitives/Badge/Badge.svelte.d.ts.map +1 -1
- package/dist/primitives/Button/Button.svelte +86 -60
- package/dist/primitives/Button/Button.svelte.d.ts +1 -1
- package/dist/primitives/Button/Button.svelte.d.ts.map +1 -1
- package/dist/routes/+layout.svelte +1 -1
- package/dist/styles/tokens.css +96 -90
- package/dist/utils/keyboard.js +3 -3
- package/dist/windowing/Window/Window.svelte +602 -0
- package/dist/windowing/Window/Window.svelte.d.ts +65 -0
- package/dist/windowing/Window/Window.svelte.d.ts.map +1 -0
- package/dist/windowing/Window/index.d.ts +2 -0
- package/dist/windowing/Window/index.d.ts.map +1 -0
- package/dist/windowing/Window/index.js +1 -0
- package/dist/windowing/WindowManager/WindowManager.svelte +410 -0
- package/dist/windowing/WindowManager/WindowManager.svelte.d.ts +38 -0
- package/dist/windowing/WindowManager/WindowManager.svelte.d.ts.map +1 -0
- package/dist/windowing/WindowManager/index.d.ts +2 -0
- package/dist/windowing/WindowManager/index.d.ts.map +1 -0
- package/dist/windowing/WindowManager/index.js +1 -0
- package/dist/windowing/index.d.ts +5 -0
- package/dist/windowing/index.d.ts.map +1 -0
- package/dist/windowing/index.js +3 -0
- package/dist/windowing/windowStore.svelte.d.ts +49 -0
- package/dist/windowing/windowStore.svelte.d.ts.map +1 -0
- package/dist/windowing/windowStore.svelte.js +170 -0
- 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">▶</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 @@
|
|
|
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';
|