@insymetri/styleguide 0.1.35 → 0.1.37

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 (39) 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/colors.css +1 -1
  18. package/dist/style/tailwind/animations.js +1 -1
  19. package/dist/style/tailwind/shadows.d.ts +1 -0
  20. package/dist/style/tailwind/shadows.js +3 -1
  21. package/dist/utils/menu/MenuItemContent.svelte +21 -0
  22. package/dist/utils/menu/MenuItemContent.svelte.d.ts +9 -0
  23. package/dist/utils/menu/MenuNoResults.svelte +4 -0
  24. package/dist/utils/menu/MenuNoResults.svelte.d.ts +18 -0
  25. package/dist/utils/menu/MenuSearchInput.svelte +25 -0
  26. package/dist/utils/menu/MenuSearchInput.svelte.d.ts +9 -0
  27. package/dist/utils/menu/index.d.ts +8 -0
  28. package/dist/utils/menu/index.js +8 -0
  29. package/dist/utils/menu/menu-search.svelte.d.ts +53 -0
  30. package/dist/utils/menu/menu-search.svelte.js +138 -0
  31. package/dist/utils/menu/menu-styles.d.ts +16 -0
  32. package/dist/utils/menu/menu-styles.js +19 -0
  33. package/dist/utils/menu/use-click-outside.d.ts +9 -0
  34. package/dist/utils/menu/use-click-outside.js +45 -0
  35. package/dist/utils/menu/use-floating.svelte.d.ts +13 -0
  36. package/dist/utils/menu/use-floating.svelte.js +55 -0
  37. package/dist/utils/menu/use-portal.d.ts +3 -0
  38. package/dist/utils/menu/use-portal.js +8 -0
  39. package/package.json +8 -1
@@ -1,39 +1,27 @@
1
1
  <script lang="ts">
2
2
  import type {Snippet} from 'svelte'
3
- import {DropdownMenu} from 'bits-ui'
4
3
  import {cn} from '../utils/cn'
5
4
  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
- }
14
-
15
- type SeparatorEntry = {
16
- type: 'separator'
17
- }
18
-
19
- type GroupEntry = {
20
- type: 'group'
21
- heading?: string
22
- items: Item[]
23
- }
24
-
25
- type MenuEntry = Item | SeparatorEntry | GroupEntry
5
+ import {createMenuSearch, isItem, isGroup, isSub, isSeparator, menuStyles, menuItemClass, type MenuItem, type MenuEntry, type SubEntry} from '../utils/menu'
6
+ import {useFloating, portal, clickOutside} from '../utils/menu'
7
+ import MenuItemContent from '../utils/menu/MenuItemContent.svelte'
8
+ import MenuSearchInput from '../utils/menu/MenuSearchInput.svelte'
9
+ import MenuNoResults from '../utils/menu/MenuNoResults.svelte'
10
+ import IIDropdownMenuSub from './IIDropdownMenuSub.svelte'
11
+ import IIDropdownMenuSubSimple from './IIDropdownMenuSubSimple.svelte'
26
12
 
27
13
  type Props = {
28
14
  items: MenuEntry[]
29
15
  onSelect: (value: string) => void
30
16
  open?: boolean
31
17
  children?: Snippet
32
- renderItem?: Snippet<[Item]>
18
+ renderItem?: Snippet<[MenuItem]>
33
19
  side?: 'top' | 'right' | 'bottom' | 'left'
34
20
  align?: 'start' | 'center' | 'end'
35
21
  collisionPadding?: number
36
22
  triggerClass?: string
23
+ searchable?: boolean
24
+ searchPlaceholder?: string
37
25
  class?: string
38
26
  }
39
27
 
@@ -47,98 +35,299 @@
47
35
  align = 'end',
48
36
  collisionPadding = 8,
49
37
  triggerClass,
38
+ searchable = false,
39
+ searchPlaceholder = 'Search...',
50
40
  class: className,
51
41
  }: Props = $props()
52
42
 
43
+ let triggerEl = $state<HTMLElement | null>(null)
44
+ let floatingEl = $state<HTMLElement | null>(null)
45
+ let openSubIndex = $state<number | null>(null)
46
+ let subTriggerEls: Record<number, HTMLElement | null> = {}
47
+
48
+ const placement = $derived(align === 'center' ? side : `${side}-${align}` as const)
49
+
50
+ useFloating({
51
+ reference: () => triggerEl,
52
+ floating: () => floatingEl,
53
+ placement: placement as any,
54
+ offset: 4,
55
+ shift: {padding: collisionPadding},
56
+ })
57
+
58
+ const search = createMenuSearch({
59
+ getItems: () => items,
60
+ onSelect: handleSelect,
61
+ })
62
+
63
+ $effect(() => {
64
+ if (open && searchable) {
65
+ search.resetAndFocus()
66
+ }
67
+ })
68
+
69
+ // Focus first item when menu opens (non-searchable)
70
+ $effect(() => {
71
+ if (open && floatingEl && !searchable) {
72
+ requestAnimationFrame(() => {
73
+ const first = floatingEl?.querySelector<HTMLElement>('[role="menuitem"]:not([data-disabled])')
74
+ first?.focus()
75
+ })
76
+ }
77
+ })
78
+
79
+ function toggle() {
80
+ if (open) {
81
+ close()
82
+ } else {
83
+ openSubIndex = null
84
+ open = true
85
+ }
86
+ }
87
+
88
+ function close() {
89
+ open = false
90
+ openSubIndex = null
91
+ triggerEl?.focus()
92
+ }
93
+
53
94
  function handleSelect(value: string) {
54
95
  onSelect(value)
55
- open = false
96
+ close()
97
+ }
98
+
99
+ function getMenuItems(): HTMLElement[] {
100
+ if (!floatingEl) return []
101
+ return Array.from(floatingEl.querySelectorAll<HTMLElement>(':scope > [role="menuitem"]:not([data-disabled]), :scope > [role="group"] > [role="menuitem"]:not([data-disabled])'))
102
+ }
103
+
104
+ function focusItem(items: HTMLElement[], currentIndex: number, direction: 1 | -1) {
105
+ if (items.length === 0) return
106
+ const nextIndex = direction === 1
107
+ ? (currentIndex >= items.length - 1 ? 0 : currentIndex + 1)
108
+ : (currentIndex <= 0 ? items.length - 1 : currentIndex - 1)
109
+ items[nextIndex]?.focus()
56
110
  }
57
111
 
58
- function isSeparator(entry: MenuEntry): entry is SeparatorEntry {
59
- return 'type' in entry && entry.type === 'separator'
112
+ function openAndFocusSub(trigger: HTMLElement) {
113
+ trigger.click()
114
+ requestAnimationFrame(() => {
115
+ const subContent = document.querySelector('[data-menu-sub-content]')
116
+ const input = subContent?.querySelector('input') as HTMLInputElement
117
+ if (input) {
118
+ input.focus()
119
+ } else {
120
+ const firstItem = subContent?.querySelector('[role="menuitem"]:not([data-disabled])') as HTMLElement
121
+ firstItem?.focus()
122
+ }
123
+ })
60
124
  }
61
125
 
62
- function isGroup(entry: MenuEntry): entry is GroupEntry {
63
- return 'type' in entry && entry.type === 'group'
126
+ function handleMenuKeydown(e: KeyboardEvent) {
127
+ if (searchable) return // search input handles its own keyboard nav
128
+
129
+ const menuItems = getMenuItems()
130
+ const currentIndex = menuItems.indexOf(document.activeElement as HTMLElement)
131
+
132
+ switch (e.key) {
133
+ case 'ArrowDown':
134
+ e.preventDefault()
135
+ focusItem(menuItems, currentIndex, 1)
136
+ break
137
+ case 'ArrowUp':
138
+ e.preventDefault()
139
+ focusItem(menuItems, currentIndex, -1)
140
+ break
141
+ case 'Enter':
142
+ case ' ': {
143
+ e.preventDefault()
144
+ const focused = document.activeElement as HTMLElement
145
+ if (focused?.hasAttribute('aria-haspopup')) {
146
+ openAndFocusSub(focused)
147
+ } else {
148
+ focused?.click()
149
+ }
150
+ break
151
+ }
152
+ case 'ArrowRight': {
153
+ const focused = document.activeElement as HTMLElement
154
+ if (focused?.hasAttribute('aria-haspopup')) {
155
+ e.preventDefault()
156
+ openAndFocusSub(focused)
157
+ }
158
+ break
159
+ }
160
+ case 'Escape':
161
+ e.preventDefault()
162
+ close()
163
+ break
164
+ }
64
165
  }
65
166
 
66
- function isItem(entry: MenuEntry): entry is Item {
67
- return !('type' in entry)
167
+ function getSubIndex(entries: MenuEntry[], target: SubEntry): number {
168
+ let idx = 0
169
+ for (const e of entries) {
170
+ if (e === target) return idx
171
+ if (isSub(e)) idx++
172
+ if (isGroup(e)) {
173
+ for (const gi of e.items) {
174
+ if (gi === target) return idx
175
+ if (isSub(gi)) idx++
176
+ }
177
+ }
178
+ }
179
+ return -1
68
180
  }
181
+
182
+ const defaultTriggerClass = '[all:unset] cursor-default inline-flex items-center justify-center p-4 rounded-4 text-secondary transition-all duration-fast hover:bg-background hover:text-body data-[state=open]:bg-background data-[state=open]:text-body motion-reduce:transition-none'
69
183
  </script>
70
184
 
71
- {#snippet itemContent(item: Item)}
185
+ {#snippet itemContent(item: MenuItem)}
72
186
  {#if renderItem}
73
187
  {@render renderItem(item)}
74
188
  {:else}
75
- {#if item.icon}
76
- <div class="w-16 h-16 flex items-center justify-center shrink-0 [&_svg]:w-16 [&_svg]:h-16">
77
- {@render item.icon()}
78
- </div>
79
- {/if}
80
- <span class="flex-1">{item.label}</span>
189
+ <MenuItemContent icon={item.icon} label={item.label} shortcut={item.shortcut} />
81
190
  {/if}
82
191
  {/snippet}
83
192
 
84
- {#snippet menuItem(item: Item)}
85
- <DropdownMenu.Item
86
- disabled={item.disabled}
87
- class={cn(
88
- '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',
89
- item.variant === 'destructive'
90
- ? 'text-error hover:bg-error-bg data-[highlighted]:bg-error-bg data-[highlighted]:outline-none'
91
- : 'text-dropdown-item hover:bg-dropdown-item-hover data-[highlighted]:bg-dropdown-item-hover data-[highlighted]:outline-none'
92
- )}
93
- onSelect={() => handleSelect(item.value)}
193
+ {#snippet menuItem(item: MenuItem)}
194
+ {@const index = searchable ? search.getItemIndex(item) : -1}
195
+ <div
196
+ role="menuitem"
197
+ tabindex="-1"
198
+ data-disabled={item.disabled ? '' : undefined}
199
+ class={menuItemClass({variant: item.variant, searchable, isHighlighted: index === search.highlightedIndex})}
200
+ data-search-item={searchable ? '' : undefined}
201
+ onclick={() => handleSelect(item.value)}
202
+ onfocus={searchable ? search.refocusInput : undefined}
203
+ onpointermove={searchable ? () => search.setHighlight(index) : undefined}
204
+ onpointerenter={!searchable ? (e) => {
205
+ openSubIndex = null
206
+ ;(e.currentTarget as HTMLElement).focus()
207
+ } : undefined}
94
208
  >
95
209
  {@render itemContent(item)}
96
- </DropdownMenu.Item>
210
+ </div>
97
211
  {/snippet}
98
212
 
99
- <DropdownMenu.Root bind:open>
100
- <DropdownMenu.Trigger
101
- class={cn(
102
- children && triggerClass
103
- ? triggerClass
104
- : '[all:unset] cursor-default inline-flex items-center justify-center p-4 rounded-4 text-secondary transition-all duration-fast hover:bg-background hover:text-body data-[state=open]:bg-background data-[state=open]:text-body motion-reduce:transition-none',
105
- !children && !triggerClass && ''
106
- )}
213
+ {#snippet subMenu(entry: SubEntry)}
214
+ {@const subIdx = getSubIndex(items, entry)}
215
+ {@const isOpen = openSubIndex === subIdx}
216
+ <div
217
+ role="menuitem"
218
+ tabindex="-1"
219
+ aria-haspopup="menu"
220
+ aria-expanded={isOpen}
221
+ data-state={isOpen ? 'open' : 'closed'}
222
+ data-disabled={entry.disabled ? '' : undefined}
223
+ class={cn(menuStyles.item, menuStyles.itemDefault)}
224
+ bind:this={subTriggerEls[subIdx]}
225
+ onpointerenter={(e) => {
226
+ openSubIndex = subIdx
227
+ ;(e.currentTarget as HTMLElement).focus()
228
+ }}
229
+ onpointerleave={(e) => e.stopImmediatePropagation()}
230
+ onclick={() => {
231
+ openSubIndex = isOpen ? null : subIdx
232
+ }}
107
233
  >
108
- {#if children}
109
- {@render children()}
234
+ <MenuItemContent icon={entry.icon} label={entry.label} shortcut={entry.shortcut} />
235
+ <IIIcon iconName="caret-right" class="w-12 h-12 text-tertiary shrink-0" />
236
+ </div>
237
+ {#if isOpen}
238
+ {#if entry.searchable}
239
+ <IIDropdownMenuSub
240
+ items={entry.items}
241
+ onSelect={handleSelect}
242
+ onClose={() => {
243
+ openSubIndex = null
244
+ subTriggerEls[subIdx]?.focus()
245
+ }}
246
+ triggerEl={subTriggerEls[subIdx]}
247
+ searchPlaceholder={entry.searchPlaceholder}
248
+ renderItemContent={itemContent}
249
+ />
110
250
  {:else}
111
- <IIIcon iconName="dots-three-vertical" />
251
+ <IIDropdownMenuSubSimple
252
+ items={entry.items}
253
+ onSelect={handleSelect}
254
+ onClose={() => {
255
+ openSubIndex = null
256
+ subTriggerEls[subIdx]?.focus()
257
+ }}
258
+ triggerEl={subTriggerEls[subIdx]}
259
+ renderItemContent={itemContent}
260
+ renderSubMenu={subMenu}
261
+ />
112
262
  {/if}
113
- </DropdownMenu.Trigger>
114
- <DropdownMenu.Portal>
115
- <DropdownMenu.Content
116
- {side}
117
- {align}
118
- {collisionPadding}
119
- class={cn(
120
- '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',
121
- className
122
- )}
123
- >
124
- {#each items as entry, i (i)}
125
- {#if isSeparator(entry)}
126
- <DropdownMenu.Separator class="h-1 bg-muted mx-4 my-4" />
127
- {:else if isGroup(entry)}
128
- <DropdownMenu.Group>
129
- {#if entry.heading}
130
- <DropdownMenu.GroupHeading class="text-tiny-emphasis text-secondary px-12 py-4 uppercase select-none">
131
- {entry.heading}
132
- </DropdownMenu.GroupHeading>
133
- {/if}
134
- {#each entry.items as item (item.value)}
135
- {@render menuItem(item)}
136
- {/each}
137
- </DropdownMenu.Group>
138
- {:else if isItem(entry)}
139
- {@render menuItem(entry)}
263
+ {/if}
264
+ {/snippet}
265
+
266
+ {#snippet menuEntries(entries: MenuEntry[])}
267
+ {#each entries as entry, i (i)}
268
+ {#if isSeparator(entry)}
269
+ <div role="separator" class={menuStyles.separator}></div>
270
+ {:else if isGroup(entry)}
271
+ {@const headingId = `dd-group-${i}`}
272
+ <div role="group" aria-labelledby={entry.heading ? headingId : undefined}>
273
+ {#if entry.heading}
274
+ <div id={headingId} role="presentation" class={menuStyles.groupHeading}>
275
+ {entry.heading}
276
+ </div>
140
277
  {/if}
141
- {/each}
142
- </DropdownMenu.Content>
143
- </DropdownMenu.Portal>
144
- </DropdownMenu.Root>
278
+ {#each entry.items as groupItem (isItem(groupItem) ? groupItem.value : groupItem.label)}
279
+ {#if isSub(groupItem)}
280
+ {@render subMenu(groupItem)}
281
+ {:else if isItem(groupItem)}
282
+ {@render menuItem(groupItem)}
283
+ {/if}
284
+ {/each}
285
+ </div>
286
+ {:else if isSub(entry)}
287
+ {@render subMenu(entry)}
288
+ {:else if isItem(entry)}
289
+ {@render menuItem(entry)}
290
+ {/if}
291
+ {/each}
292
+ {/snippet}
293
+
294
+ <button
295
+ bind:this={triggerEl}
296
+ type="button"
297
+ aria-haspopup="menu"
298
+ aria-expanded={open}
299
+ data-state={open ? 'open' : 'closed'}
300
+ class={cn(children && triggerClass ? triggerClass : defaultTriggerClass)}
301
+ onclick={toggle}
302
+ >
303
+ {#if children}
304
+ {@render children()}
305
+ {:else}
306
+ <IIIcon iconName="dots-three-vertical" />
307
+ {/if}
308
+ </button>
309
+
310
+ {#if open}
311
+ <div use:portal use:clickOutside={{onClose: close}}>
312
+ <div
313
+ bind:this={floatingEl}
314
+ role="menu"
315
+ data-menu-content
316
+ class={cn(menuStyles.content, className)}
317
+ onkeydown={handleMenuKeydown}
318
+ >
319
+ {#if searchable}
320
+ <MenuSearchInput
321
+ bind:inputEl={search.inputEl}
322
+ bind:searchQuery={search.searchQuery}
323
+ placeholder={searchPlaceholder}
324
+ onkeydown={(e) => search.handleKeydown(e, '[data-menu-content]')}
325
+ />
326
+ {/if}
327
+ {@render menuEntries(searchable ? search.filteredItems : items)}
328
+ {#if searchable && search.filteredItems.length === 0}
329
+ <MenuNoResults />
330
+ {/if}
331
+ </div>
332
+ </div>
333
+ {/if}
@@ -1,30 +1,17 @@
1
1
  import type { Snippet } from 'svelte';
2
- type Item = {
3
- label: string;
4
- value: string;
5
- icon?: Snippet;
6
- disabled?: boolean;
7
- variant?: 'default' | 'destructive';
8
- };
9
- type SeparatorEntry = {
10
- type: 'separator';
11
- };
12
- type GroupEntry = {
13
- type: 'group';
14
- heading?: string;
15
- items: Item[];
16
- };
17
- type MenuEntry = Item | SeparatorEntry | GroupEntry;
2
+ import { type MenuItem, type MenuEntry } from '../utils/menu';
18
3
  type Props = {
19
4
  items: MenuEntry[];
20
5
  onSelect: (value: string) => void;
21
6
  open?: boolean;
22
7
  children?: Snippet;
23
- renderItem?: Snippet<[Item]>;
8
+ renderItem?: Snippet<[MenuItem]>;
24
9
  side?: 'top' | 'right' | 'bottom' | 'left';
25
10
  align?: 'start' | 'center' | 'end';
26
11
  collisionPadding?: number;
27
12
  triggerClass?: string;
13
+ searchable?: boolean;
14
+ searchPlaceholder?: string;
28
15
  class?: string;
29
16
  };
30
17
  declare const IIDropdownMenu: import("svelte").Component<Props, {}, "open">;
@@ -75,6 +75,26 @@
75
75
  </IIDropdownMenu>
76
76
  </section>
77
77
 
78
+ <!-- Searchable -->
79
+ <section>
80
+ <h2 class="text-default-emphasis text-primary mb-8">Searchable</h2>
81
+ <p class="text-small text-secondary mb-12">A search input at the top filters menu items as you type.</p>
82
+ <IIDropdownMenu
83
+ items={[
84
+ {label: 'Edit', value: 'edit'},
85
+ {label: 'Duplicate', value: 'duplicate'},
86
+ {label: 'Archive', value: 'archive'},
87
+ {label: 'Export', value: 'export'},
88
+ {label: 'Share', value: 'share'},
89
+ {label: 'Print', value: 'print'},
90
+ {type: 'separator'},
91
+ {label: 'Delete', value: 'delete', variant: 'destructive'},
92
+ ]}
93
+ onSelect={handleSelect}
94
+ searchable
95
+ />
96
+ </section>
97
+
78
98
  <!-- Mixed Entries -->
79
99
  <section>
80
100
  <h2 class="text-default-emphasis text-primary mb-8">Mixed: Items + Separators + Groups</h2>
@@ -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 = `dd-sub-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 IIDropdownMenuSub: import("svelte").Component<Props, {}, "">;
12
+ type IIDropdownMenuSub = ReturnType<typeof IIDropdownMenuSub>;
13
+ export default IIDropdownMenuSub;