@insymetri/styleguide 0.1.35 → 0.1.36

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 (38) hide show
  1. package/dist/IIContextMenu/IIContextMenu.svelte +223 -98
  2. package/dist/IIContextMenu/IIContextMenu.svelte.d.ts +13 -0
  3. package/dist/IIContextMenu/IIContextMenuStories.svelte +8 -0
  4. package/dist/IIContextMenu/IIContextMenuSub.svelte +131 -0
  5. package/dist/IIContextMenu/IIContextMenuSub.svelte.d.ts +13 -0
  6. package/dist/IIContextMenu/IIContextMenuSubSimple.svelte +137 -0
  7. package/dist/IIContextMenu/IIContextMenuSubSimple.svelte.d.ts +13 -0
  8. package/dist/IIDropdownInput/IIDropdownInput.svelte +161 -60
  9. package/dist/IIDropdownInput/IIDropdownInput.svelte.d.ts +6 -9
  10. package/dist/IIDropdownMenu/IIDropdownMenu.svelte +278 -89
  11. package/dist/IIDropdownMenu/IIDropdownMenu.svelte.d.ts +4 -17
  12. package/dist/IIDropdownMenu/IIDropdownMenuStories.svelte +20 -0
  13. package/dist/IIDropdownMenu/IIDropdownMenuSub.svelte +131 -0
  14. package/dist/IIDropdownMenu/IIDropdownMenuSub.svelte.d.ts +13 -0
  15. package/dist/IIDropdownMenu/IIDropdownMenuSubSimple.svelte +134 -0
  16. package/dist/IIDropdownMenu/IIDropdownMenuSubSimple.svelte.d.ts +13 -0
  17. package/dist/style/tailwind/animations.js +1 -1
  18. package/dist/style/tailwind/shadows.d.ts +1 -0
  19. package/dist/style/tailwind/shadows.js +3 -1
  20. package/dist/utils/menu/MenuItemContent.svelte +21 -0
  21. package/dist/utils/menu/MenuItemContent.svelte.d.ts +9 -0
  22. package/dist/utils/menu/MenuNoResults.svelte +4 -0
  23. package/dist/utils/menu/MenuNoResults.svelte.d.ts +18 -0
  24. package/dist/utils/menu/MenuSearchInput.svelte +25 -0
  25. package/dist/utils/menu/MenuSearchInput.svelte.d.ts +9 -0
  26. package/dist/utils/menu/index.d.ts +8 -0
  27. package/dist/utils/menu/index.js +8 -0
  28. package/dist/utils/menu/menu-search.svelte.d.ts +53 -0
  29. package/dist/utils/menu/menu-search.svelte.js +138 -0
  30. package/dist/utils/menu/menu-styles.d.ts +16 -0
  31. package/dist/utils/menu/menu-styles.js +19 -0
  32. package/dist/utils/menu/use-click-outside.d.ts +9 -0
  33. package/dist/utils/menu/use-click-outside.js +45 -0
  34. package/dist/utils/menu/use-floating.svelte.d.ts +13 -0
  35. package/dist/utils/menu/use-floating.svelte.js +55 -0
  36. package/dist/utils/menu/use-portal.d.ts +3 -0
  37. package/dist/utils/menu/use-portal.js +8 -0
  38. package/package.json +8 -1
@@ -1,45 +1,20 @@
1
1
  <script lang="ts">
2
2
  import type {Snippet} from 'svelte'
3
- import {ContextMenu} from 'bits-ui'
3
+ import type {VirtualElement} from '@floating-ui/dom'
4
4
  import {cn} from '../utils/cn'
5
5
  import {IIIcon} from '../IIIcon'
6
-
7
- type Item = {
8
- label: string
9
- value: string
10
- icon?: Snippet
11
- disabled?: boolean
12
- variant?: 'default' | 'destructive'
13
- shortcut?: string
14
- }
15
-
16
- type SeparatorEntry = {
17
- type: 'separator'
18
- }
19
-
20
- type GroupEntry = {
21
- type: 'group'
22
- heading?: string
23
- items: (Item | SubEntry)[]
24
- }
25
-
26
- type SubEntry = {
27
- type: 'sub'
28
- label: string
29
- icon?: Snippet
30
- disabled?: boolean
31
- shortcut?: string
32
- items: MenuEntry[]
33
- }
34
-
35
- type MenuEntry = Item | SeparatorEntry | GroupEntry | SubEntry
6
+ import {isItem, isGroup, isSub, isSeparator, menuStyles, menuItemClass, type MenuItem, type MenuEntry, type SubEntry} from '../utils/menu'
7
+ import {useFloating, portal, clickOutside} from '../utils/menu'
8
+ import MenuItemContent from '../utils/menu/MenuItemContent.svelte'
9
+ import IIContextMenuSub from './IIContextMenuSub.svelte'
10
+ import IIContextMenuSubSimple from './IIContextMenuSubSimple.svelte'
36
11
 
37
12
  type Props = {
38
13
  items: MenuEntry[]
39
14
  onSelect: (value: string) => void
40
15
  open?: boolean
41
16
  children: Snippet
42
- renderItem?: Snippet<[Item]>
17
+ renderItem?: Snippet<[MenuItem]>
43
18
  class?: string
44
19
  }
45
20
 
@@ -52,103 +27,245 @@
52
27
  class: className,
53
28
  }: Props = $props()
54
29
 
30
+ let virtualRef = $state<VirtualElement | null>(null)
31
+ let floatingEl = $state<HTMLElement | null>(null)
32
+ let previousFocus = $state<HTMLElement | null>(null)
33
+ let openSubIndex = $state<number | null>(null)
34
+ let subTriggerEls: Record<number, HTMLElement | null> = {}
35
+
36
+ useFloating({
37
+ reference: () => virtualRef,
38
+ floating: () => floatingEl,
39
+ placement: 'bottom-start',
40
+ offset: 4,
41
+ shift: {padding: 8},
42
+ })
43
+
44
+ function handleContextMenu(e: MouseEvent) {
45
+ e.preventDefault()
46
+ previousFocus = document.activeElement as HTMLElement
47
+ openSubIndex = null
48
+ virtualRef = {
49
+ getBoundingClientRect: () => ({
50
+ x: e.clientX,
51
+ y: e.clientY,
52
+ width: 0,
53
+ height: 0,
54
+ top: e.clientY,
55
+ left: e.clientX,
56
+ right: e.clientX,
57
+ bottom: e.clientY,
58
+ }),
59
+ }
60
+ open = true
61
+ }
62
+
63
+ function close() {
64
+ open = false
65
+ openSubIndex = null
66
+ previousFocus?.focus()
67
+ }
68
+
55
69
  function handleSelect(value: string) {
56
70
  onSelect(value)
57
- open = false
71
+ close()
58
72
  }
59
73
 
60
- function isSeparator(entry: MenuEntry): entry is SeparatorEntry {
61
- return 'type' in entry && entry.type === 'separator'
74
+ // Focus first item when menu opens
75
+ $effect(() => {
76
+ if (open && floatingEl) {
77
+ requestAnimationFrame(() => {
78
+ const first = floatingEl?.querySelector<HTMLElement>('[role="menuitem"]:not([data-disabled])')
79
+ first?.focus()
80
+ })
81
+ }
82
+ })
83
+
84
+ function getMenuItems(): HTMLElement[] {
85
+ if (!floatingEl) return []
86
+ return Array.from(floatingEl.querySelectorAll<HTMLElement>(':scope > [role="menuitem"]:not([data-disabled]), :scope > [role="group"] > [role="menuitem"]:not([data-disabled])'))
62
87
  }
63
88
 
64
- function isGroup(entry: MenuEntry): entry is GroupEntry {
65
- return 'type' in entry && entry.type === 'group'
89
+ function focusItem(items: HTMLElement[], currentIndex: number, direction: 1 | -1) {
90
+ if (items.length === 0) return
91
+ const nextIndex = direction === 1
92
+ ? (currentIndex >= items.length - 1 ? 0 : currentIndex + 1)
93
+ : (currentIndex <= 0 ? items.length - 1 : currentIndex - 1)
94
+ items[nextIndex]?.focus()
66
95
  }
67
96
 
68
- function isSub(entry: MenuEntry): entry is SubEntry {
69
- return 'type' in entry && entry.type === 'sub'
97
+ function openAndFocusSub(trigger: HTMLElement) {
98
+ trigger.click()
99
+ requestAnimationFrame(() => {
100
+ const subContent = document.querySelector('[data-menu-sub-content]')
101
+ const input = subContent?.querySelector('input') as HTMLInputElement
102
+ if (input) {
103
+ input.focus()
104
+ } else {
105
+ const firstItem = subContent?.querySelector('[role="menuitem"]:not([data-disabled])') as HTMLElement
106
+ firstItem?.focus()
107
+ }
108
+ })
70
109
  }
71
110
 
72
- function isItem(entry: MenuEntry): entry is Item {
73
- return !('type' in entry)
111
+ function handleMenuKeydown(e: KeyboardEvent) {
112
+ const menuItems = getMenuItems()
113
+ const currentIndex = menuItems.indexOf(document.activeElement as HTMLElement)
114
+
115
+ switch (e.key) {
116
+ case 'ArrowDown':
117
+ e.preventDefault()
118
+ focusItem(menuItems, currentIndex, 1)
119
+ break
120
+ case 'ArrowUp':
121
+ e.preventDefault()
122
+ focusItem(menuItems, currentIndex, -1)
123
+ break
124
+ case 'Tab':
125
+ e.preventDefault()
126
+ e.stopImmediatePropagation()
127
+ focusItem(menuItems, currentIndex, e.shiftKey ? -1 : 1)
128
+ break
129
+ case 'Enter':
130
+ case ' ': {
131
+ e.preventDefault()
132
+ const focused = document.activeElement as HTMLElement
133
+ if (focused?.hasAttribute('aria-haspopup')) {
134
+ openAndFocusSub(focused)
135
+ } else {
136
+ focused?.click()
137
+ }
138
+ break
139
+ }
140
+ case 'ArrowRight': {
141
+ const focused = document.activeElement as HTMLElement
142
+ if (focused?.hasAttribute('aria-haspopup')) {
143
+ e.preventDefault()
144
+ openAndFocusSub(focused)
145
+ }
146
+ break
147
+ }
148
+ case 'Escape':
149
+ e.preventDefault()
150
+ close()
151
+ break
152
+ }
74
153
  }
75
154
 
76
- const itemClass = 'flex items-center gap-8 px-12 py-8 rounded-4 text-small cursor-default select-none outline-none data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed data-[disabled]:pointer-events-none motion-reduce:transition-none'
77
- const itemDefaultClass = 'text-dropdown-item hover:bg-dropdown-item-hover data-[highlighted]:bg-dropdown-item-hover data-[highlighted]:outline-none'
78
- const itemDestructiveClass = 'text-error hover:bg-error-bg data-[highlighted]:bg-error-bg data-[highlighted]:outline-none'
79
- const contentClass = 'min-w-48 max-h-300 overflow-y-auto bg-dropdown-bg border border-dropdown-border rounded-10 shadow-dropdown p-4 z-12 animate-slide-in motion-reduce:animate-none'
155
+ // Deterministic sub-index: walks the items tree and counts subs until it finds the target
156
+ function getSubIndex(entries: MenuEntry[], target: SubEntry): number {
157
+ let idx = 0
158
+ for (const e of entries) {
159
+ if (e === target) return idx
160
+ if (isSub(e)) idx++
161
+ if (isGroup(e)) {
162
+ for (const gi of e.items) {
163
+ if (gi === target) return idx
164
+ if (isSub(gi)) idx++
165
+ }
166
+ }
167
+ }
168
+ return -1
169
+ }
80
170
  </script>
81
171
 
82
- {#snippet itemContent(item: Item)}
172
+ {#snippet itemContent(item: MenuItem)}
83
173
  {#if renderItem}
84
174
  {@render renderItem(item)}
85
175
  {:else}
86
- {#if item.icon}
87
- <div class="w-16 h-16 flex items-center justify-center shrink-0 [&_svg]:w-16 [&_svg]:h-16">
88
- {@render item.icon()}
89
- </div>
90
- {/if}
91
- <span class="flex-1">{item.label}</span>
92
- {#if item.shortcut}
93
- <span class="text-tiny text-tertiary ml-8">{item.shortcut}</span>
94
- {/if}
176
+ <MenuItemContent icon={item.icon} label={item.label} shortcut={item.shortcut} />
95
177
  {/if}
96
178
  {/snippet}
97
179
 
98
- {#snippet menuItem(item: Item)}
99
- <ContextMenu.Item
100
- disabled={item.disabled}
101
- class={cn(itemClass, item.variant === 'destructive' ? itemDestructiveClass : itemDefaultClass)}
102
- onSelect={() => handleSelect(item.value)}
180
+ {#snippet menuItem(item: MenuItem)}
181
+ <div
182
+ role="menuitem"
183
+ tabindex="-1"
184
+ data-disabled={item.disabled ? '' : undefined}
185
+ class={menuItemClass({variant: item.variant})}
186
+ onclick={() => handleSelect(item.value)}
187
+ onpointerenter={(e) => {
188
+ openSubIndex = null
189
+ ;(e.currentTarget as HTMLElement).focus()
190
+ }}
103
191
  >
104
192
  {@render itemContent(item)}
105
- </ContextMenu.Item>
193
+ </div>
106
194
  {/snippet}
107
195
 
108
196
  {#snippet subMenu(entry: SubEntry)}
109
- <ContextMenu.Sub>
110
- <ContextMenu.SubTrigger
111
- disabled={entry.disabled}
112
- class={cn(itemClass, itemDefaultClass)}
113
- >
114
- {#if entry.icon}
115
- <div class="w-16 h-16 flex items-center justify-center shrink-0 [&_svg]:w-16 [&_svg]:h-16">
116
- {@render entry.icon()}
117
- </div>
118
- {/if}
119
- <span class="flex-1">{entry.label}</span>
120
- {#if entry.shortcut}
121
- <span class="text-tiny text-tertiary ml-8">{entry.shortcut}</span>
122
- {/if}
123
- <IIIcon iconName="caret-right" class="w-12 h-12 text-tertiary shrink-0" />
124
- </ContextMenu.SubTrigger>
125
- <ContextMenu.Portal>
126
- <ContextMenu.SubContent class={contentClass}>
127
- {@render menuEntries(entry.items)}
128
- </ContextMenu.SubContent>
129
- </ContextMenu.Portal>
130
- </ContextMenu.Sub>
197
+ {@const subIdx = getSubIndex(items, entry)}
198
+ {@const isOpen = openSubIndex === subIdx}
199
+ <div
200
+ role="menuitem"
201
+ tabindex="-1"
202
+ aria-haspopup="menu"
203
+ aria-expanded={isOpen}
204
+ data-state={isOpen ? 'open' : 'closed'}
205
+ data-disabled={entry.disabled ? '' : undefined}
206
+ class={cn(menuStyles.item, menuStyles.itemDefault)}
207
+ bind:this={subTriggerEls[subIdx]}
208
+ onpointerenter={(e) => {
209
+ openSubIndex = subIdx
210
+ ;(e.currentTarget as HTMLElement).focus()
211
+ }}
212
+ onpointerleave={(e) => e.stopImmediatePropagation()}
213
+ onclick={() => {
214
+ openSubIndex = isOpen ? null : subIdx
215
+ }}
216
+ >
217
+ <MenuItemContent icon={entry.icon} label={entry.label} shortcut={entry.shortcut} />
218
+ <IIIcon iconName="caret-right" class="w-12 h-12 text-tertiary shrink-0" />
219
+ </div>
220
+ {#if isOpen}
221
+ {#if entry.searchable}
222
+ <IIContextMenuSub
223
+ items={entry.items}
224
+ onSelect={handleSelect}
225
+ onClose={() => {
226
+ openSubIndex = null
227
+ subTriggerEls[subIdx]?.focus()
228
+ }}
229
+ triggerEl={subTriggerEls[subIdx]}
230
+ searchPlaceholder={entry.searchPlaceholder}
231
+ renderItemContent={itemContent}
232
+ />
233
+ {:else}
234
+ <IIContextMenuSubSimple
235
+ items={entry.items}
236
+ onSelect={handleSelect}
237
+ onClose={() => {
238
+ openSubIndex = null
239
+ subTriggerEls[subIdx]?.focus()
240
+ }}
241
+ triggerEl={subTriggerEls[subIdx]}
242
+ renderItemContent={itemContent}
243
+ renderSubMenu={subMenu}
244
+ />
245
+ {/if}
246
+ {/if}
131
247
  {/snippet}
132
248
 
133
249
  {#snippet menuEntries(entries: MenuEntry[])}
134
250
  {#each entries as entry, i (i)}
135
251
  {#if isSeparator(entry)}
136
- <ContextMenu.Separator class="h-1 bg-muted mx-4 my-4" />
252
+ <div role="separator" class={menuStyles.separator}></div>
137
253
  {:else if isGroup(entry)}
138
- <ContextMenu.Group>
254
+ {@const headingId = `ctx-group-${i}`}
255
+ <div role="group" aria-labelledby={entry.heading ? headingId : undefined}>
139
256
  {#if entry.heading}
140
- <ContextMenu.GroupHeading class="text-tiny-emphasis text-secondary px-12 py-4 uppercase select-none">
257
+ <div id={headingId} role="presentation" class={menuStyles.groupHeading}>
141
258
  {entry.heading}
142
- </ContextMenu.GroupHeading>
259
+ </div>
143
260
  {/if}
144
261
  {#each entry.items as groupItem (isItem(groupItem) ? groupItem.value : groupItem.label)}
145
262
  {#if isSub(groupItem)}
146
263
  {@render subMenu(groupItem)}
147
- {:else}
264
+ {:else if isItem(groupItem)}
148
265
  {@render menuItem(groupItem)}
149
266
  {/if}
150
267
  {/each}
151
- </ContextMenu.Group>
268
+ </div>
152
269
  {:else if isSub(entry)}
153
270
  {@render subMenu(entry)}
154
271
  {:else if isItem(entry)}
@@ -157,13 +274,21 @@
157
274
  {/each}
158
275
  {/snippet}
159
276
 
160
- <ContextMenu.Root bind:open>
161
- <ContextMenu.Trigger class="inline-block">
162
- {@render children()}
163
- </ContextMenu.Trigger>
164
- <ContextMenu.Portal>
165
- <ContextMenu.Content class={cn(contentClass, className)}>
277
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
278
+ <div class="inline-block" oncontextmenu={handleContextMenu}>
279
+ {@render children()}
280
+ </div>
281
+
282
+ {#if open}
283
+ <div use:portal use:clickOutside={{onClose: close}}>
284
+ <div
285
+ bind:this={floatingEl}
286
+ role="menu"
287
+ data-menu-content
288
+ class={cn(menuStyles.content, className)}
289
+ onkeydown={handleMenuKeydown}
290
+ >
166
291
  {@render menuEntries(items)}
167
- </ContextMenu.Content>
168
- </ContextMenu.Portal>
169
- </ContextMenu.Root>
292
+ </div>
293
+ </div>
294
+ {/if}
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { type MenuItem, type MenuEntry } from '../utils/menu';
3
+ type Props = {
4
+ items: MenuEntry[];
5
+ onSelect: (value: string) => void;
6
+ open?: boolean;
7
+ children: Snippet;
8
+ renderItem?: Snippet<[MenuItem]>;
9
+ class?: string;
10
+ };
11
+ declare const IIContextMenu: import("svelte").Component<Props, {}, "open">;
12
+ type IIContextMenu = ReturnType<typeof IIContextMenu>;
13
+ export default IIContextMenu;
@@ -68,6 +68,8 @@
68
68
  label: 'Status',
69
69
  icon: statusIcon,
70
70
  shortcut: 'S',
71
+ searchable: true,
72
+ searchPlaceholder: 'Set status...',
71
73
  items: [
72
74
  {label: 'To Do', value: 'status-todo', icon: circleIcon},
73
75
  {label: 'In Progress', value: 'status-progress', icon: clockIcon},
@@ -79,6 +81,8 @@
79
81
  label: 'Assignee',
80
82
  icon: userIcon,
81
83
  shortcut: 'A',
84
+ searchable: true,
85
+ searchPlaceholder: 'Assign to...',
82
86
  items: [
83
87
  {label: 'No assignee', value: 'assign-none', icon: userIcon},
84
88
  {type: 'separator'},
@@ -97,6 +101,8 @@
97
101
  label: 'Priority',
98
102
  icon: warningIcon,
99
103
  shortcut: 'P',
104
+ searchable: true,
105
+ searchPlaceholder: 'Set priority...',
100
106
  items: [
101
107
  {label: 'Urgent', value: 'priority-urgent'},
102
108
  {label: 'High', value: 'priority-high'},
@@ -110,6 +116,8 @@
110
116
  label: 'Labels',
111
117
  icon: listIcon,
112
118
  shortcut: 'L',
119
+ searchable: true,
120
+ searchPlaceholder: 'Add label...',
113
121
  items: [
114
122
  {label: 'Bug', value: 'label-bug'},
115
123
  {label: 'Feature', value: 'label-feature'},
@@ -0,0 +1,131 @@
1
+ <script lang="ts">
2
+ import type {Snippet} from 'svelte'
3
+ import {createMenuSearch, isItem, isGroup, isSeparator, type MenuItem, type MenuEntry} from '../utils/menu'
4
+ import {menuStyles, menuItemClass} from '../utils/menu'
5
+ import {useFloating, portal} from '../utils/menu'
6
+ import MenuSearchInput from '../utils/menu/MenuSearchInput.svelte'
7
+ import MenuNoResults from '../utils/menu/MenuNoResults.svelte'
8
+
9
+ type Props = {
10
+ items: MenuEntry[]
11
+ onSelect: (value: string) => void
12
+ onClose: () => void
13
+ triggerEl: HTMLElement | null
14
+ searchPlaceholder?: string
15
+ renderItemContent: Snippet<[MenuItem]>
16
+ }
17
+
18
+ let {
19
+ items,
20
+ onSelect,
21
+ onClose,
22
+ triggerEl,
23
+ searchPlaceholder = 'Search...',
24
+ renderItemContent,
25
+ }: Props = $props()
26
+
27
+ const search = createMenuSearch({
28
+ getItems: () => items,
29
+ onSelect,
30
+ passthroughKeys: ['ArrowLeft'],
31
+ })
32
+
33
+ let floatingEl = $state<HTMLElement | null>(null)
34
+
35
+ useFloating({
36
+ reference: () => triggerEl,
37
+ floating: () => floatingEl,
38
+ placement: 'right-start',
39
+ offset: 4,
40
+ })
41
+
42
+ // Reset search state on mount but don't focus — focus transfers on ArrowRight or pointer enter
43
+ $effect(() => {
44
+ if (floatingEl) {
45
+ search.reset()
46
+ }
47
+ })
48
+
49
+ function activate() {
50
+ search.resetAndFocus()
51
+ }
52
+
53
+ function handleKeydown(e: KeyboardEvent) {
54
+ if (e.key === 'ArrowLeft' || e.key === 'Escape') {
55
+ e.preventDefault()
56
+ onClose()
57
+ return
58
+ }
59
+ search.handleKeydown(e, '[data-menu-sub-content]')
60
+ }
61
+ </script>
62
+
63
+ <div use:portal>
64
+ <div
65
+ bind:this={floatingEl}
66
+ role="menu"
67
+ data-menu-content
68
+ data-menu-sub-content
69
+ class={menuStyles.searchableSubContent}
70
+ onpointerleave={(e) => {
71
+ e.stopImmediatePropagation()
72
+ search.inputEl?.blur()
73
+ }}
74
+ onpointerenter={() => {
75
+ activate()
76
+ }}
77
+ >
78
+ <MenuSearchInput
79
+ bind:inputEl={search.inputEl}
80
+ bind:searchQuery={search.searchQuery}
81
+ placeholder={searchPlaceholder}
82
+ onkeydown={handleKeydown}
83
+ />
84
+ <div class={menuStyles.scrollableItems}>
85
+ {#each search.filteredItems as entry, i (i)}
86
+ {#if isSeparator(entry)}
87
+ <div role="separator" class={menuStyles.separator}></div>
88
+ {:else if isGroup(entry)}
89
+ {@const headingId = `menu-group-${i}`}
90
+ <div role="group" aria-labelledby={entry.heading ? headingId : undefined}>
91
+ {#if entry.heading}
92
+ <div id={headingId} role="presentation" class={menuStyles.groupHeading}>
93
+ {entry.heading}
94
+ </div>
95
+ {/if}
96
+ {#each entry.items.filter(isItem) as item (item.value)}
97
+ {@const index = search.getItemIndex(item)}
98
+ <div
99
+ role="menuitem"
100
+ tabindex="-1"
101
+ data-disabled={item.disabled ? '' : undefined}
102
+ class={menuItemClass({variant: item.variant, searchable: true, isHighlighted: index === search.highlightedIndex})}
103
+ data-search-item=""
104
+ onclick={() => onSelect(item.value)}
105
+ onpointermove={() => search.setHighlight(index)}
106
+ >
107
+ {@render renderItemContent(item)}
108
+ </div>
109
+ {/each}
110
+ </div>
111
+ {:else if isItem(entry)}
112
+ {@const index = search.getItemIndex(entry)}
113
+ <div
114
+ role="menuitem"
115
+ tabindex="-1"
116
+ data-disabled={entry.disabled ? '' : undefined}
117
+ class={menuItemClass({variant: entry.variant, searchable: true, isHighlighted: index === search.highlightedIndex})}
118
+ data-search-item=""
119
+ onclick={() => onSelect(entry.value)}
120
+ onpointermove={() => search.setHighlight(index)}
121
+ >
122
+ {@render renderItemContent(entry)}
123
+ </div>
124
+ {/if}
125
+ {/each}
126
+ {#if search.filteredItems.length === 0}
127
+ <MenuNoResults />
128
+ {/if}
129
+ </div>
130
+ </div>
131
+ </div>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { type MenuItem, type MenuEntry } from '../utils/menu';
3
+ type Props = {
4
+ items: MenuEntry[];
5
+ onSelect: (value: string) => void;
6
+ onClose: () => void;
7
+ triggerEl: HTMLElement | null;
8
+ searchPlaceholder?: string;
9
+ renderItemContent: Snippet<[MenuItem]>;
10
+ };
11
+ declare const IIContextMenuSub: import("svelte").Component<Props, {}, "">;
12
+ type IIContextMenuSub = ReturnType<typeof IIContextMenuSub>;
13
+ export default IIContextMenuSub;