@mrintel/villain-ui 0.3.0 → 0.6.3

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 (128) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +3490 -1296
  3. package/dist/components/buttons/Button.svelte +27 -33
  4. package/dist/components/buttons/Button.svelte.d.ts +4 -1
  5. package/dist/components/buttons/ButtonGroup.svelte +17 -30
  6. package/dist/components/buttons/FloatingActionButton.svelte +20 -44
  7. package/dist/components/buttons/FloatingActionButton.svelte.d.ts +2 -1
  8. package/dist/components/buttons/IconButton.svelte +23 -53
  9. package/dist/components/buttons/IconButton.svelte.d.ts +2 -1
  10. package/dist/components/buttons/LinkButton.svelte +24 -37
  11. package/dist/components/buttons/LinkButton.svelte.d.ts +4 -1
  12. package/dist/components/buttons/buttonClasses.d.ts +5 -0
  13. package/dist/components/buttons/buttonClasses.js +8 -3
  14. package/dist/components/cards/Card.svelte +60 -46
  15. package/dist/components/cards/Card.svelte.d.ts +6 -2
  16. package/dist/components/cards/Container.svelte +17 -33
  17. package/dist/components/cards/Divider.svelte +36 -52
  18. package/dist/components/cards/Divider.svelte.d.ts +2 -0
  19. package/dist/components/cards/Grid.svelte +55 -44
  20. package/dist/components/cards/Panel.svelte +18 -32
  21. package/dist/components/cards/Panel.svelte.d.ts +2 -1
  22. package/dist/components/cards/SectionHeader.svelte +24 -38
  23. package/dist/components/cards/SectionHeader.svelte.d.ts +1 -0
  24. package/dist/components/data/Avatar.svelte +48 -67
  25. package/dist/components/data/Badge.svelte +45 -32
  26. package/dist/components/data/Badge.svelte.d.ts +7 -1
  27. package/dist/components/data/CalendarGrid.svelte +433 -0
  28. package/dist/components/data/CalendarGrid.svelte.d.ts +25 -0
  29. package/dist/components/data/CalendarGrid.types.d.ts +7 -0
  30. package/dist/components/data/CalendarGrid.types.js +1 -0
  31. package/dist/components/data/CodeBlock.svelte +119 -121
  32. package/dist/components/data/CodeBlock.svelte.d.ts +8 -0
  33. package/dist/components/data/List.svelte +87 -64
  34. package/dist/components/data/List.svelte.d.ts +7 -0
  35. package/dist/components/data/Pagination.svelte +121 -123
  36. package/dist/components/data/Pagination.svelte.d.ts +5 -0
  37. package/dist/components/data/Sparkline.svelte +117 -0
  38. package/dist/components/data/Sparkline.svelte.d.ts +43 -0
  39. package/dist/components/data/Stat.svelte +92 -103
  40. package/dist/components/data/Table.svelte +443 -76
  41. package/dist/components/data/Table.svelte.d.ts +23 -2
  42. package/dist/components/data/Table.types.d.ts +14 -0
  43. package/dist/components/data/Table.types.js +1 -0
  44. package/dist/components/data/Tag.svelte +51 -53
  45. package/dist/components/data/Tag.svelte.d.ts +5 -1
  46. package/dist/components/data/index.d.ts +4 -0
  47. package/dist/components/data/index.js +2 -0
  48. package/dist/components/forms/Checkbox.svelte +39 -51
  49. package/dist/components/forms/Checkbox.svelte.d.ts +3 -1
  50. package/dist/components/forms/DatePicker.svelte +61 -0
  51. package/dist/components/forms/DatePicker.svelte.d.ts +15 -0
  52. package/dist/components/forms/DateTimePicker.svelte +63 -0
  53. package/dist/components/forms/DateTimePicker.svelte.d.ts +16 -0
  54. package/dist/components/forms/FileUpload.svelte +136 -164
  55. package/dist/components/forms/FileUpload.svelte.d.ts +1 -0
  56. package/dist/components/forms/Input.svelte +282 -57
  57. package/dist/components/forms/Input.svelte.d.ts +9 -3
  58. package/dist/components/forms/InputGroup.svelte +7 -7
  59. package/dist/components/forms/RadioGroup.svelte +77 -87
  60. package/dist/components/forms/RadioGroup.svelte.d.ts +3 -1
  61. package/dist/components/forms/RangeSlider.svelte +90 -116
  62. package/dist/components/forms/Select.svelte +106 -71
  63. package/dist/components/forms/Select.svelte.d.ts +3 -1
  64. package/dist/components/forms/Switch.svelte +44 -56
  65. package/dist/components/forms/Switch.svelte.d.ts +3 -1
  66. package/dist/components/forms/Textarea.svelte +52 -57
  67. package/dist/components/forms/Textarea.svelte.d.ts +3 -1
  68. package/dist/components/forms/TimePicker.svelte +63 -0
  69. package/dist/components/forms/TimePicker.svelte.d.ts +16 -0
  70. package/dist/components/forms/formClasses.d.ts +3 -0
  71. package/dist/components/forms/formClasses.js +3 -0
  72. package/dist/components/forms/index.d.ts +3 -0
  73. package/dist/components/forms/index.js +3 -0
  74. package/dist/components/navigation/Breadcrumbs.svelte +56 -59
  75. package/dist/components/navigation/Breadcrumbs.svelte.d.ts +1 -0
  76. package/dist/components/navigation/ContextMenu.svelte +133 -83
  77. package/dist/components/navigation/ContextMenu.svelte.d.ts +8 -1
  78. package/dist/components/navigation/DropdownMenu.svelte +139 -80
  79. package/dist/components/navigation/DropdownMenu.svelte.d.ts +8 -1
  80. package/dist/components/navigation/Menu.svelte +72 -48
  81. package/dist/components/navigation/Navbar.svelte +111 -32
  82. package/dist/components/navigation/Navbar.svelte.d.ts +6 -0
  83. package/dist/components/navigation/Sidebar.svelte +236 -35
  84. package/dist/components/navigation/Sidebar.svelte.d.ts +2 -0
  85. package/dist/components/navigation/Tabs.svelte +86 -54
  86. package/dist/components/navigation/Tabs.svelte.d.ts +5 -1
  87. package/dist/components/overlays/Alert.svelte +81 -99
  88. package/dist/components/overlays/Alert.svelte.d.ts +5 -1
  89. package/dist/components/overlays/CommandPalette.svelte +182 -217
  90. package/dist/components/overlays/Drawer.svelte +158 -167
  91. package/dist/components/overlays/Drawer.svelte.d.ts +3 -1
  92. package/dist/components/overlays/Dropdown.svelte +62 -30
  93. package/dist/components/overlays/Dropdown.svelte.d.ts +2 -0
  94. package/dist/components/overlays/Modal.svelte +125 -130
  95. package/dist/components/overlays/Modal.svelte.d.ts +3 -1
  96. package/dist/components/overlays/Popover.svelte +106 -131
  97. package/dist/components/overlays/ProgressBar.svelte +29 -45
  98. package/dist/components/overlays/SkeletonLoader.svelte +66 -82
  99. package/dist/components/overlays/Spinner.svelte +33 -43
  100. package/dist/components/overlays/Toast.svelte +111 -140
  101. package/dist/components/overlays/Toast.svelte.d.ts +3 -0
  102. package/dist/components/overlays/Tooltip.svelte +94 -115
  103. package/dist/components/overlays/Tooltip.svelte.d.ts +3 -1
  104. package/dist/components/typography/Code.svelte +10 -14
  105. package/dist/components/typography/Heading.svelte +15 -22
  106. package/dist/components/typography/Heading.svelte.d.ts +1 -0
  107. package/dist/components/typography/Text.svelte +21 -24
  108. package/dist/components/typography/Text.svelte.d.ts +2 -1
  109. package/dist/components/utilities/Accordion.svelte +54 -67
  110. package/dist/components/utilities/Accordion.svelte.d.ts +4 -1
  111. package/dist/components/utilities/Carousel.svelte +124 -152
  112. package/dist/components/utilities/Collapse.svelte +46 -60
  113. package/dist/components/utilities/Hero.svelte +42 -0
  114. package/dist/components/utilities/Hero.svelte.d.ts +10 -0
  115. package/dist/components/utilities/Portal.svelte +47 -72
  116. package/dist/components/utilities/ScrollArea.svelte +33 -41
  117. package/dist/components/utilities/SystemConsole.svelte +310 -0
  118. package/dist/components/utilities/SystemConsole.svelte.d.ts +20 -0
  119. package/dist/components/utilities/SystemInterface.svelte +726 -0
  120. package/dist/components/utilities/SystemInterface.svelte.d.ts +19 -0
  121. package/dist/components/utilities/index.d.ts +4 -0
  122. package/dist/components/utilities/index.js +3 -0
  123. package/dist/components/utilities/utilities.types.d.ts +46 -0
  124. package/dist/components/utilities/utilities.types.js +4 -0
  125. package/dist/index.d.ts +49 -4
  126. package/dist/index.js +4 -4
  127. package/dist/theme.css +2821 -218
  128. package/package.json +83 -76
@@ -0,0 +1,63 @@
1
+ <script lang="ts">import { createId } from '../../lib/internal/id.js';
2
+ import { baseInputClasses, focusClasses, disabledClasses, } from './formClasses';
3
+ let { value = $bindable(''), min, max, step, placeholder, disabled = false, error = false, label, id = createId('timepicker'), onchange, class: className = '', } = $props();
4
+ const errorClasses = $derived(error ? 'error-state' : '');
5
+ </script>
6
+
7
+ {#if label}
8
+ <div>
9
+ <label for={id} class="text-text-soft text-sm mb-2 block">
10
+ {label}
11
+ </label>
12
+ <input
13
+ type="time"
14
+ {id}
15
+ {placeholder}
16
+ {disabled}
17
+ {min}
18
+ {max}
19
+ {step}
20
+ bind:value
21
+ {onchange}
22
+ class="{baseInputClasses} {focusClasses} {errorClasses} {disabled
23
+ ? disabledClasses
24
+ : ''} {className}"
25
+ />
26
+ </div>
27
+ {:else}
28
+ <input
29
+ type="time"
30
+ {id}
31
+ {placeholder}
32
+ {disabled}
33
+ {min}
34
+ {max}
35
+ {step}
36
+ bind:value
37
+ {onchange}
38
+ class="{baseInputClasses} {focusClasses} {errorClasses} {disabled
39
+ ? disabledClasses
40
+ : ''} {className}"
41
+ />
42
+ {/if}
43
+
44
+ <style>
45
+ input[type='time'] {
46
+ min-height: 3rem;
47
+ cursor: pointer;
48
+ }
49
+
50
+ input[type='time']::-webkit-calendar-picker-indicator {
51
+ cursor: pointer;
52
+ filter: invert(0.7);
53
+ transition: filter 120ms var(--ease-sharp);
54
+ }
55
+
56
+ input[type='time']:hover::-webkit-calendar-picker-indicator {
57
+ filter: invert(1);
58
+ }
59
+
60
+ input[type='time']:disabled::-webkit-calendar-picker-indicator {
61
+ cursor: not-allowed;
62
+ opacity: 0.4;
63
+ }</style>
@@ -0,0 +1,16 @@
1
+ export interface Props {
2
+ value?: string;
3
+ min?: string;
4
+ max?: string;
5
+ step?: number;
6
+ placeholder?: string;
7
+ disabled?: boolean;
8
+ error?: boolean;
9
+ label?: string;
10
+ id?: string;
11
+ onchange?: (event: Event) => void;
12
+ class?: string;
13
+ }
14
+ declare const TimePicker: import("svelte").Component<Props, {}, "value">;
15
+ type TimePicker = ReturnType<typeof TimePicker>;
16
+ export default TimePicker;
@@ -0,0 +1,3 @@
1
+ export declare const baseInputClasses = "glass-panel rounded-[var(--radius-lg)] px-5 py-3.5 font-body text-text placeholder:text-text-muted transition-all duration-300 ease-luxe w-full";
2
+ export declare const focusClasses = "focus:outline-none focus:border-accent focus:accent-glow";
3
+ export declare const disabledClasses = "opacity-50 cursor-not-allowed";
@@ -0,0 +1,3 @@
1
+ export const baseInputClasses = 'glass-panel rounded-[var(--radius-lg)] px-5 py-3.5 font-body text-text placeholder:text-text-muted transition-all duration-300 ease-luxe w-full';
2
+ export const focusClasses = 'focus:outline-none focus:border-accent focus:accent-glow';
3
+ export const disabledClasses = 'opacity-50 cursor-not-allowed';
@@ -7,3 +7,6 @@ export { default as RadioGroup } from './RadioGroup.svelte';
7
7
  export { default as RangeSlider } from './RangeSlider.svelte';
8
8
  export { default as FileUpload } from './FileUpload.svelte';
9
9
  export { default as InputGroup } from './InputGroup.svelte';
10
+ export { default as DatePicker } from './DatePicker.svelte';
11
+ export { default as TimePicker } from './TimePicker.svelte';
12
+ export { default as DateTimePicker } from './DateTimePicker.svelte';
@@ -7,3 +7,6 @@ export { default as RadioGroup } from './RadioGroup.svelte';
7
7
  export { default as RangeSlider } from './RangeSlider.svelte';
8
8
  export { default as FileUpload } from './FileUpload.svelte';
9
9
  export { default as InputGroup } from './InputGroup.svelte';
10
+ export { default as DatePicker } from './DatePicker.svelte';
11
+ export { default as TimePicker } from './TimePicker.svelte';
12
+ export { default as DateTimePicker } from './DateTimePicker.svelte';
@@ -1,59 +1,56 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
-
4
- interface BreadcrumbItem {
5
- label: string;
6
- href?: string;
7
- onclick?: () => void;
8
- }
9
-
10
- interface Props {
11
- items: BreadcrumbItem[];
12
- separator?: string;
13
- separatorIcon?: Snippet;
14
- }
15
-
16
- let {
17
- items,
18
- separator = '/',
19
- separatorIcon
20
- }: Props = $props();
21
- </script>
22
-
23
- <nav role="navigation" aria-label="Breadcrumb">
24
- <ol class="flex items-center gap-2">
25
- {#each items as item, index}
26
- <li class="flex items-center gap-2">
27
- {#if item.href}
28
- <a
29
- href={item.href}
30
- class="text-sm font-[var(--font-body)] text-[var(--color-text-soft)] hover:text-[var(--color-accent)] hover:underline transition-colors duration-200 ease-[var(--ease-luxe)]"
31
- >
32
- {item.label}
33
- </a>
34
- {:else if item.onclick}
35
- <button
36
- onclick={item.onclick}
37
- class="text-sm font-[var(--font-body)] text-[var(--color-text-soft)] hover:text-[var(--color-accent)] hover:underline transition-colors duration-200 ease-[var(--ease-luxe)] cursor-pointer"
38
- >
39
- {item.label}
40
- </button>
41
- {:else}
42
- <span class="text-sm font-[var(--font-body)] {index === items.length - 1 ? 'text-[var(--color-text)]' : 'text-[var(--color-text-soft)]'}">
43
- {item.label}
44
- </span>
45
- {/if}
46
-
47
- {#if index < items.length - 1}
48
- <span class="text-[var(--color-text-muted)] text-sm" aria-hidden="true">
49
- {#if separatorIcon}
50
- {@render separatorIcon()}
51
- {:else}
52
- {separator}
53
- {/if}
54
- </span>
55
- {/if}
56
- </li>
57
- {/each}
58
- </ol>
59
- </nav>
1
+ <script lang="ts">let { items, separator = '/', separatorIcon } = $props();
2
+ export {};
3
+ </script>
4
+
5
+ <nav role="navigation" aria-label="Breadcrumb">
6
+ <ol class="flex items-center gap-2">
7
+ {#each items as item, index}
8
+ <li class="flex items-center gap-2">
9
+ {#if item.href}
10
+ <a
11
+ href={item.href}
12
+ class="flex items-center gap-2 text-sm font-[var(--font-body)] text-[var(--color-text-soft)] hover:text-[var(--color-accent)] hover:underline transition-colors duration-200 ease-[var(--ease-luxe)]"
13
+ >
14
+ {#if item.icon}
15
+ <span class="inline-flex items-center justify-center">
16
+ {@render item.icon()}
17
+ </span>
18
+ {/if}
19
+ {item.label}
20
+ </a>
21
+ {:else if item.onclick}
22
+ <button
23
+ onclick={item.onclick}
24
+ class="flex items-center gap-2 text-sm font-[var(--font-body)] text-[var(--color-text-soft)] hover:text-[var(--color-accent)] hover:underline transition-colors duration-200 ease-[var(--ease-luxe)] cursor-pointer"
25
+ >
26
+ {#if item.icon}
27
+ <span class="inline-flex items-center justify-center">
28
+ {@render item.icon()}
29
+ </span>
30
+ {/if}
31
+ {item.label}
32
+ </button>
33
+ {:else}
34
+ <span class="flex items-center gap-2 text-sm font-[var(--font-body)] {index === items.length - 1 ? 'text-[var(--color-text)]' : 'text-[var(--color-text-soft)]'}">
35
+ {#if item.icon}
36
+ <span class="inline-flex items-center justify-center">
37
+ {@render item.icon()}
38
+ </span>
39
+ {/if}
40
+ {item.label}
41
+ </span>
42
+ {/if}
43
+
44
+ {#if index < items.length - 1}
45
+ <span class="text-[var(--color-text-muted)] text-sm" aria-hidden="true">
46
+ {#if separatorIcon}
47
+ {@render separatorIcon()}
48
+ {:else}
49
+ {separator}
50
+ {/if}
51
+ </span>
52
+ {/if}
53
+ </li>
54
+ {/each}
55
+ </ol>
56
+ </nav>
@@ -3,6 +3,7 @@ interface BreadcrumbItem {
3
3
  label: string;
4
4
  href?: string;
5
5
  onclick?: () => void;
6
+ icon?: Snippet;
6
7
  }
7
8
  interface Props {
8
9
  items: BreadcrumbItem[];
@@ -1,83 +1,133 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
-
4
- interface Props {
5
- open?: boolean;
6
- x?: number;
7
- y?: number;
8
- trigger?: Snippet;
9
- children?: Snippet;
10
- }
11
-
12
- let {
13
- open = $bindable(false),
14
- x = $bindable(0),
15
- y = $bindable(0),
16
- trigger,
17
- children
18
- }: Props = $props();
19
-
20
- let menuElement = $state<HTMLDivElement>();
21
-
22
- function handleContextMenu(event: MouseEvent) {
23
- event.preventDefault();
24
- x = event.clientX;
25
- y = event.clientY;
26
- open = true;
27
-
28
- // Adjust position if menu would overflow viewport
29
- requestAnimationFrame(() => {
30
- if (menuElement) {
31
- const menuRect = menuElement.getBoundingClientRect();
32
- const viewportWidth = window.innerWidth;
33
- const viewportHeight = window.innerHeight;
34
-
35
- if (x + menuRect.width > viewportWidth) {
36
- x = viewportWidth - menuRect.width - 10;
37
- }
38
-
39
- if (y + menuRect.height > viewportHeight) {
40
- y = viewportHeight - menuRect.height - 10;
41
- }
42
- }
43
- });
44
- }
45
-
46
- function handleClickOutside(event: MouseEvent) {
47
- if (menuElement && !menuElement.contains(event.target as Node)) {
48
- open = false;
49
- }
50
- }
51
-
52
- function handleEscape(event: KeyboardEvent) {
53
- if (event.key === 'Escape') {
54
- open = false;
55
- }
56
- }
57
-
58
- $effect(() => {
59
- if (open) {
60
- document.addEventListener('click', handleClickOutside);
61
- document.addEventListener('keydown', handleEscape);
62
-
63
- return () => {
64
- document.removeEventListener('click', handleClickOutside);
65
- document.removeEventListener('keydown', handleEscape);
66
- };
67
- }
68
- });
69
- </script>
70
-
71
- <div oncontextmenu={handleContextMenu} role="presentation" class="contents">
72
- {@render trigger?.()}
73
- </div>
74
-
75
- {#if open}
76
- <div
77
- bind:this={menuElement}
78
- class="fixed z-50 glass-panel rounded-[var(--radius-lg)] shadow-[var(--shadow-deep)] min-w-[12rem] animate-[fade-in_0.15s_var(--ease-luxe)]"
79
- style="left: {x}px; top: {y}px;"
80
- >
81
- {@render children?.()}
82
- </div>
83
- {/if}
1
+ <script lang="ts">import { createId } from '../../lib/internal/id.js';
2
+ let { items, open = $bindable(false), x = $bindable(0), y = $bindable(0), trigger } = $props();
3
+ let menuElement = $state();
4
+ let selectedIndex = $state(0);
5
+ const menuId = createId('context-menu');
6
+ function handleContextMenu(event) {
7
+ event.preventDefault();
8
+ x = event.clientX;
9
+ y = event.clientY;
10
+ open = true;
11
+ selectedIndex = 0;
12
+ // Adjust position if menu would overflow viewport
13
+ requestAnimationFrame(() => {
14
+ if (menuElement) {
15
+ const menuRect = menuElement.getBoundingClientRect();
16
+ const viewportWidth = window.innerWidth;
17
+ const viewportHeight = window.innerHeight;
18
+ if (x + menuRect.width > viewportWidth) {
19
+ x = viewportWidth - menuRect.width - 10;
20
+ }
21
+ if (y + menuRect.height > viewportHeight) {
22
+ y = viewportHeight - menuRect.height - 10;
23
+ }
24
+ }
25
+ });
26
+ }
27
+ function handleItemClick(item) {
28
+ if (item.disabled)
29
+ return;
30
+ item.onclick?.();
31
+ open = false;
32
+ }
33
+ function handleMenuKeyDown(event) {
34
+ // Derive current index from focused element to stay in sync
35
+ const focusedElement = document.activeElement;
36
+ const dataIndex = focusedElement?.getAttribute('data-index');
37
+ if (dataIndex !== null) {
38
+ const currentFocusedIndex = parseInt(dataIndex || '0', 10);
39
+ if (!isNaN(currentFocusedIndex) && currentFocusedIndex >= 0 && currentFocusedIndex < items.length) {
40
+ selectedIndex = currentFocusedIndex;
41
+ }
42
+ }
43
+ const enabledItems = items.filter(item => !item.disabled);
44
+ const currentEnabledIndex = enabledItems.findIndex(item => item.id === items[selectedIndex]?.id);
45
+ let nextIndex = currentEnabledIndex;
46
+ switch (event.key) {
47
+ case 'ArrowDown':
48
+ event.preventDefault();
49
+ nextIndex = currentEnabledIndex < enabledItems.length - 1 ? currentEnabledIndex + 1 : 0;
50
+ selectedIndex = items.findIndex(item => item.id === enabledItems[nextIndex]?.id);
51
+ break;
52
+ case 'ArrowUp':
53
+ event.preventDefault();
54
+ nextIndex = currentEnabledIndex > 0 ? currentEnabledIndex - 1 : enabledItems.length - 1;
55
+ selectedIndex = items.findIndex(item => item.id === enabledItems[nextIndex]?.id);
56
+ break;
57
+ case 'Home':
58
+ event.preventDefault();
59
+ selectedIndex = items.findIndex(item => item.id === enabledItems[0]?.id);
60
+ break;
61
+ case 'End':
62
+ event.preventDefault();
63
+ selectedIndex = items.findIndex(item => item.id === enabledItems[enabledItems.length - 1]?.id);
64
+ break;
65
+ case 'Enter':
66
+ case ' ':
67
+ event.preventDefault();
68
+ if (items[selectedIndex] && !items[selectedIndex].disabled) {
69
+ handleItemClick(items[selectedIndex]);
70
+ }
71
+ break;
72
+ case 'Escape':
73
+ event.preventDefault();
74
+ open = false;
75
+ break;
76
+ }
77
+ }
78
+ function handleClickOutside(event) {
79
+ if (menuElement && !menuElement.contains(event.target)) {
80
+ open = false;
81
+ }
82
+ }
83
+ $effect(() => {
84
+ if (open) {
85
+ document.addEventListener('click', handleClickOutside);
86
+ requestAnimationFrame(() => {
87
+ const firstItem = menuElement?.querySelector('[role="menuitem"]');
88
+ if (firstItem) {
89
+ const dataIndex = firstItem.getAttribute('data-index');
90
+ if (dataIndex !== null) {
91
+ selectedIndex = parseInt(dataIndex, 10);
92
+ }
93
+ firstItem.focus();
94
+ }
95
+ });
96
+ return () => {
97
+ document.removeEventListener('click', handleClickOutside);
98
+ };
99
+ }
100
+ });
101
+ </script>
102
+
103
+ <div oncontextmenu={handleContextMenu} role="presentation" class="contents">
104
+ {@render trigger?.()}
105
+ </div>
106
+
107
+ {#if open}
108
+ <div
109
+ bind:this={menuElement}
110
+ id={menuId}
111
+ role="menu"
112
+ tabindex="-1"
113
+ onkeydown={handleMenuKeyDown}
114
+ class="fixed z-[var(--z-50)] glass-panel rounded-[var(--radius-lg)] shadow-[var(--shadow-deep)] min-w-[12rem] animate-[fade-in_0.15s_var(--ease-luxe)]"
115
+ style="left: {x}px; top: {y}px;"
116
+ >
117
+ {#each items as item, index}
118
+ <button
119
+ role="menuitem"
120
+ data-index={index}
121
+ tabindex={index === selectedIndex ? 0 : -1}
122
+ onclick={() => handleItemClick(item)}
123
+ disabled={item.disabled}
124
+ class="w-full text-left px-4 py-2 text-sm text-text hover:bg-base-3 transition-colors duration-200 flex items-center gap-2 {item.disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}"
125
+ >
126
+ {#if item.icon}
127
+ {@render item.icon()}
128
+ {/if}
129
+ {item.label}
130
+ </button>
131
+ {/each}
132
+ </div>
133
+ {/if}
@@ -1,10 +1,17 @@
1
1
  import type { Snippet } from 'svelte';
2
+ interface MenuItem {
3
+ id: string;
4
+ label: string;
5
+ onclick?: () => void;
6
+ disabled?: boolean;
7
+ icon?: Snippet;
8
+ }
2
9
  interface Props {
10
+ items: MenuItem[];
3
11
  open?: boolean;
4
12
  x?: number;
5
13
  y?: number;
6
14
  trigger?: Snippet;
7
- children?: Snippet;
8
15
  }
9
16
  declare const ContextMenu: import("svelte").Component<Props, {}, "open" | "x" | "y">;
10
17
  type ContextMenu = ReturnType<typeof ContextMenu>;