@insymetri/styleguide 0.1.13 → 0.1.15

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 (45) hide show
  1. package/dist/IIButton/IIButton.svelte +1 -1
  2. package/dist/IICalendar/IICalendar.svelte +101 -0
  3. package/dist/IICalendar/IICalendar.svelte.d.ts +13 -0
  4. package/dist/IICalendar/IICalendarStories.svelte +60 -0
  5. package/dist/IICalendar/IICalendarStories.svelte.d.ts +3 -0
  6. package/dist/IICalendar/index.d.ts +1 -0
  7. package/dist/IICalendar/index.js +1 -0
  8. package/dist/IICombobox/IICombobox.svelte +76 -30
  9. package/dist/IICombobox/IICombobox.svelte.d.ts +6 -0
  10. package/dist/IICombobox/IIComboboxStories.svelte +97 -0
  11. package/dist/IICombobox/IIComboboxStories.svelte.d.ts +3 -0
  12. package/dist/IIDatePicker/IIDatePicker.svelte +1 -1
  13. package/dist/IIDropdownInput/IIDropdownInput.svelte +31 -7
  14. package/dist/IIDropdownInput/IIDropdownInput.svelte.d.ts +5 -0
  15. package/dist/IIDropdownInput/IIDropdownInputStories.svelte +88 -0
  16. package/dist/IIDropdownInput/IIDropdownInputStories.svelte.d.ts +3 -0
  17. package/dist/IIDropdownMenu/IIDropdownMenu.svelte +90 -23
  18. package/dist/IIDropdownMenu/IIDropdownMenu.svelte.d.ts +14 -2
  19. package/dist/IIDropdownMenu/IIDropdownMenuStories.svelte +101 -0
  20. package/dist/IIDropdownMenu/IIDropdownMenuStories.svelte.d.ts +18 -0
  21. package/dist/IIIconButton/IIIconButton.svelte +1 -1
  22. package/dist/IIInput/IIInput.svelte +2 -2
  23. package/dist/IIMultiSelect/IIMultiSelect.svelte +141 -0
  24. package/dist/IIMultiSelect/IIMultiSelect.svelte.d.ts +20 -0
  25. package/dist/IIMultiSelect/IIMultiSelectStories.svelte +78 -0
  26. package/dist/IIMultiSelect/IIMultiSelectStories.svelte.d.ts +3 -0
  27. package/dist/IIMultiSelect/index.d.ts +1 -0
  28. package/dist/IIMultiSelect/index.js +1 -0
  29. package/dist/IIPopover/IIPopover.svelte +48 -0
  30. package/dist/IIPopover/IIPopover.svelte.d.ts +15 -0
  31. package/dist/IIPopover/IIPopoverStories.svelte +108 -0
  32. package/dist/IIPopover/IIPopoverStories.svelte.d.ts +3 -0
  33. package/dist/IIPopover/index.d.ts +1 -0
  34. package/dist/IIPopover/index.js +1 -0
  35. package/dist/IITabs/IITabs.svelte +1 -1
  36. package/dist/IITextarea/IITextarea.svelte +1 -1
  37. package/dist/IIToggle/IIToggle.svelte +52 -0
  38. package/dist/IIToggle/IIToggle.svelte.d.ts +15 -0
  39. package/dist/IIToggle/IIToggleStories.svelte +89 -0
  40. package/dist/IIToggle/IIToggleStories.svelte.d.ts +3 -0
  41. package/dist/IIToggle/index.d.ts +1 -0
  42. package/dist/IIToggle/index.js +1 -0
  43. package/dist/index.d.ts +4 -0
  44. package/dist/index.js +4 -0
  45. package/package.json +1 -1
@@ -37,7 +37,7 @@
37
37
  const isDisabled = $derived(disabled || loading)
38
38
 
39
39
  const baseClasses =
40
- 'inline-flex items-center justify-center gap-8 whitespace-nowrap rounded-10 cursor-default transition-colors duration-base ease-in-out no-underline disabled:cursor-not-allowed'
40
+ 'inline-flex items-center justify-center gap-8 whitespace-nowrap rounded-8 cursor-default transition-colors duration-base ease-in-out no-underline disabled:cursor-not-allowed'
41
41
 
42
42
  const variantClasses = {
43
43
  primary:
@@ -0,0 +1,101 @@
1
+ <script lang="ts">
2
+ import {Calendar} from 'bits-ui'
3
+ import type {DateValue} from '@internationalized/date'
4
+ import {cn} from '../utils/cn'
5
+
6
+ type Props = {
7
+ value: DateValue | undefined
8
+ onValueChange?: (value: DateValue | undefined) => void
9
+ minValue?: DateValue
10
+ maxValue?: DateValue
11
+ isDateDisabled?: (date: DateValue) => boolean
12
+ disabled?: boolean
13
+ class?: string
14
+ }
15
+
16
+ let {
17
+ value = $bindable(),
18
+ onValueChange,
19
+ minValue,
20
+ maxValue,
21
+ isDateDisabled,
22
+ disabled = false,
23
+ class: className,
24
+ }: Props = $props()
25
+ </script>
26
+
27
+ <div class={cn('min-w-280', className)}>
28
+ <Calendar.Root
29
+ type="single"
30
+ bind:value
31
+ {onValueChange}
32
+ {minValue}
33
+ {maxValue}
34
+ {isDateDisabled}
35
+ {disabled}
36
+ >
37
+ {#snippet children({months, weekdays})}
38
+ <div class="flex items-center justify-between gap-16 mb-12">
39
+ <Calendar.MonthSelect aria-label="Select month" monthFormat="long">
40
+ {#snippet child({props, monthItems, selectedMonthItem})}
41
+ <select
42
+ {...props}
43
+ class="appearance-none bg-transparent border-0 py-4 pr-16 pl-0 text-small-emphasis text-body cursor-pointer focus:outline-none flex-1"
44
+ style="background-image: url(&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E&quot;); background-repeat: no-repeat; background-position: right center"
45
+ >
46
+ {#each monthItems as month (month.value)}
47
+ <option value={month.value} selected={month.value === selectedMonthItem.value}>
48
+ {month.label}
49
+ </option>
50
+ {/each}
51
+ </select>
52
+ {/snippet}
53
+ </Calendar.MonthSelect>
54
+ <Calendar.YearSelect aria-label="Select year">
55
+ {#snippet child({props, yearItems, selectedYearItem})}
56
+ <select
57
+ {...props}
58
+ class="appearance-none bg-transparent border-0 py-4 pr-16 pl-0 text-small-emphasis text-body cursor-pointer focus:outline-none"
59
+ style="background-image: url(&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E&quot;); background-repeat: no-repeat; background-position: right center"
60
+ >
61
+ {#each yearItems as year (year.value)}
62
+ <option value={year.value} selected={year.value === selectedYearItem.value}>
63
+ {year.label}
64
+ </option>
65
+ {/each}
66
+ </select>
67
+ {/snippet}
68
+ </Calendar.YearSelect>
69
+ </div>
70
+
71
+ {#each months as month (month.value.toString())}
72
+ <Calendar.Grid class="w-full border-collapse">
73
+ <Calendar.GridHead>
74
+ <Calendar.GridRow class="flex w-full">
75
+ {#each weekdays as day, i (`day-${i}`)}
76
+ <Calendar.HeadCell class="flex-1 text-center text-tiny-emphasis text-secondary p-4 uppercase">
77
+ {day.slice(0, 2)}
78
+ </Calendar.HeadCell>
79
+ {/each}
80
+ </Calendar.GridRow>
81
+ </Calendar.GridHead>
82
+ <Calendar.GridBody>
83
+ {#each month.weeks as weekDates, weekIndex (weekIndex)}
84
+ <Calendar.GridRow class="flex w-full">
85
+ {#each weekDates as date (date.toString())}
86
+ <Calendar.Cell {date} month={month.value} class="flex-1 aspect-square p-2">
87
+ <Calendar.Day
88
+ class="[all:unset] flex items-center justify-center w-full h-full rounded-4 text-small text-body cursor-default transition-all duration-fast hover:bg-gray-100 data-[selected]:bg-primary data-[selected]:text-inverse data-[selected]:font-semibold data-[today]:border data-[today]:border-primary data-[outside-month]:text-tertiary data-[outside-month]:opacity-50 data-[disabled]:text-tertiary data-[disabled]:cursor-not-allowed data-[disabled]:opacity-30 motion-reduce:transition-none"
89
+ >
90
+ {date.day}
91
+ </Calendar.Day>
92
+ </Calendar.Cell>
93
+ {/each}
94
+ </Calendar.GridRow>
95
+ {/each}
96
+ </Calendar.GridBody>
97
+ </Calendar.Grid>
98
+ {/each}
99
+ {/snippet}
100
+ </Calendar.Root>
101
+ </div>
@@ -0,0 +1,13 @@
1
+ import type { DateValue } from '@internationalized/date';
2
+ type Props = {
3
+ value: DateValue | undefined;
4
+ onValueChange?: (value: DateValue | undefined) => void;
5
+ minValue?: DateValue;
6
+ maxValue?: DateValue;
7
+ isDateDisabled?: (date: DateValue) => boolean;
8
+ disabled?: boolean;
9
+ class?: string;
10
+ };
11
+ declare const IICalendar: import("svelte").Component<Props, {}, "value">;
12
+ type IICalendar = ReturnType<typeof IICalendar>;
13
+ export default IICalendar;
@@ -0,0 +1,60 @@
1
+ <script lang="ts">
2
+ import IICalendar from './IICalendar.svelte'
3
+ import {CalendarDate, today, getLocalTimeZone} from '@internationalized/date'
4
+
5
+ let basicValue = $state<import('@internationalized/date').DateValue | undefined>(undefined)
6
+ let presetValue = $state<import('@internationalized/date').DateValue | undefined>(new CalendarDate(2026, 3, 17))
7
+ let constrainedValue = $state<import('@internationalized/date').DateValue | undefined>(undefined)
8
+
9
+ const todayDate = today(getLocalTimeZone())
10
+ const minDate = todayDate
11
+ const maxDate = todayDate.add({months: 3})
12
+
13
+ function isWeekend(date: import('@internationalized/date').DateValue) {
14
+ const dayOfWeek = date.toDate(getLocalTimeZone()).getDay()
15
+ return dayOfWeek === 0 || dayOfWeek === 6
16
+ }
17
+ </script>
18
+
19
+ <div class="flex flex-col gap-32">
20
+ <!-- Basic -->
21
+ <section>
22
+ <h2 class="text-default-emphasis text-primary mb-8">Basic Calendar</h2>
23
+ <p class="text-small text-secondary mb-12">Standalone calendar with no constraints.</p>
24
+ <div class="bg-surface border border-primary rounded-10 shadow-dropdown p-12 inline-block">
25
+ <IICalendar bind:value={basicValue} />
26
+ </div>
27
+ {#if basicValue}
28
+ <p class="text-small text-secondary mt-8">Selected: {basicValue.toString()}</p>
29
+ {/if}
30
+ </section>
31
+
32
+ <!-- With Preset Value -->
33
+ <section>
34
+ <h2 class="text-default-emphasis text-primary mb-8">With Preset Value</h2>
35
+ <p class="text-small text-secondary mb-12">Calendar initialized to March 17, 2026.</p>
36
+ <div class="bg-surface border border-primary rounded-10 shadow-dropdown p-12 inline-block">
37
+ <IICalendar bind:value={presetValue} />
38
+ </div>
39
+ </section>
40
+
41
+ <!-- Constrained Range -->
42
+ <section>
43
+ <h2 class="text-default-emphasis text-primary mb-8">Date Constraints</h2>
44
+ <p class="text-small text-secondary mb-12">Only allows dates from today to 3 months ahead, weekdays only.</p>
45
+ <div class="bg-surface border border-primary rounded-10 shadow-dropdown p-12 inline-block">
46
+ <IICalendar bind:value={constrainedValue} minValue={minDate} maxValue={maxDate} isDateDisabled={isWeekend} />
47
+ </div>
48
+ {#if constrainedValue}
49
+ <p class="text-small text-secondary mt-8">Selected: {constrainedValue.toString()}</p>
50
+ {/if}
51
+ </section>
52
+
53
+ <!-- Disabled -->
54
+ <section>
55
+ <h2 class="text-default-emphasis text-primary mb-8">Disabled</h2>
56
+ <div class="bg-surface border border-primary rounded-10 shadow-dropdown p-12 inline-block">
57
+ <IICalendar value={new CalendarDate(2026, 3, 17)} disabled />
58
+ </div>
59
+ </section>
60
+ </div>
@@ -0,0 +1,3 @@
1
+ declare const IICalendarStories: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type IICalendarStories = ReturnType<typeof IICalendarStories>;
3
+ export default IICalendarStories;
@@ -0,0 +1 @@
1
+ export { default as IICalendar } from './IICalendar.svelte';
@@ -0,0 +1 @@
1
+ export { default as IICalendar } from './IICalendar.svelte';
@@ -11,11 +11,17 @@
11
11
  disabled?: boolean
12
12
  }
13
13
 
14
+ type Group<T> = {
15
+ heading: string
16
+ items: T[]
17
+ }
18
+
14
19
  type Props<T> = {
15
20
  items: T[]
16
21
  value?: string
17
22
  placeholder?: string
18
23
  disabled?: boolean
24
+ loading?: boolean
19
25
  onSelect?: (item: T) => void
20
26
  // Function to extract value from item (required for custom types)
21
27
  getItemValue: (item: T) => string
@@ -27,6 +33,8 @@
27
33
  renderItem?: Snippet<[item: T, selected: boolean]>
28
34
  // Content shown inside dropdown when there are no items (e.g. loading/no results)
29
35
  emptyContent?: Snippet
36
+ // Grouped items (alternative to flat items)
37
+ groups?: Group<T>[]
30
38
  // CSS classes
31
39
  class?: string
32
40
  contentClass?: string
@@ -37,12 +45,14 @@
37
45
  value = $bindable(''),
38
46
  placeholder = 'Select...',
39
47
  disabled = false,
48
+ loading = false,
40
49
  onSelect,
41
50
  getItemValue,
42
51
  getItemDisabled,
43
52
  filterFn,
44
53
  renderItem,
45
54
  emptyContent,
55
+ groups,
46
56
  class: className,
47
57
  contentClass,
48
58
  }: Props<T> = $props()
@@ -50,6 +60,9 @@
50
60
  let inputValue = $state('')
51
61
  let open = $state(false)
52
62
 
63
+ // When groups provided, derive flat items from groups
64
+ const allItems = $derived(groups ? groups.flatMap(g => g.items) : items)
65
+
53
66
  // Default filter for simple items
54
67
  function defaultFilter(items: T[], inputValue: string): T[] {
55
68
  if (!inputValue) return items
@@ -65,16 +78,29 @@
65
78
  }
66
79
 
67
80
  const filteredItems = $derived.by(() => {
68
- if (filterFn) {
69
- return filterFn(items, inputValue)
81
+ const filter = filterFn ?? defaultFilter
82
+ if (groups) {
83
+ return filter(allItems, inputValue)
70
84
  }
71
- return defaultFilter(items, inputValue)
85
+ return filter(items, inputValue)
86
+ })
87
+
88
+ // For grouped rendering, filter each group's items
89
+ const filteredGroups = $derived.by(() => {
90
+ if (!groups) return undefined
91
+ const filter = filterFn ?? defaultFilter
92
+ return groups
93
+ .map(g => ({
94
+ ...g,
95
+ items: filter(g.items, inputValue),
96
+ }))
97
+ .filter(g => g.items.length > 0)
72
98
  })
73
99
 
74
100
  function handleValueChange(newValue: string | undefined) {
75
101
  value = newValue || ''
76
102
  if (newValue && onSelect) {
77
- const selectedItem = items.find(item => getItemValue(item) === newValue)
103
+ const selectedItem = allItems.find(item => getItemValue(item) === newValue)
78
104
  if (selectedItem) {
79
105
  onSelect(selectedItem)
80
106
  }
@@ -98,6 +124,31 @@
98
124
  }
99
125
  </script>
100
126
 
127
+ {#snippet comboboxItem(item: T, i: number)}
128
+ <Combobox.Item
129
+ value={getItemValue(item)}
130
+ label=""
131
+ disabled={getItemDisabled?.(item) ?? false}
132
+ class="flex items-center gap-8 py-6 px-12 text-small text-dropdown-item rounded-6 cursor-default transition-all duration-fast outline-none hover:bg-dropdown-item-hover data-[highlighted]:bg-dropdown-item-hover data-[selected]:bg-dropdown-item-selected data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed motion-reduce:transition-none"
133
+ >
134
+ {#snippet children({selected})}
135
+ {#if renderItem}
136
+ {@render renderItem(item, selected)}
137
+ {:else if isSimpleItem(item)}
138
+ <span class="flex-1">{item.label}</span>
139
+ {#if selected}
140
+ <IIIcon iconName="check" class="w-18 h-18 shrink-0 ml-auto" />
141
+ {/if}
142
+ {:else}
143
+ <span class="flex-1">{getItemValue(item)}</span>
144
+ {#if selected}
145
+ <IIIcon iconName="check" class="w-18 h-18 shrink-0 ml-auto" />
146
+ {/if}
147
+ {/if}
148
+ {/snippet}
149
+ </Combobox.Item>
150
+ {/snippet}
151
+
101
152
  <div class={cn('block w-full', className)}>
102
153
  <Combobox.Root
103
154
  type="single"
@@ -111,10 +162,10 @@
111
162
  {placeholder}
112
163
  {disabled}
113
164
  oninput={handleInput}
114
- class="w-full box-border py-5 px-12 text-small text-input-text bg-input-bg border border-input-border rounded-10 transition-all duration-fast outline-none placeholder:text-input-placeholder hover:border-input-border-hover focus:border-input-border-hover disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-input-bg-disabled motion-reduce:transition-none"
165
+ class="w-full box-border py-5 px-12 text-small text-input-text bg-input-bg border border-input-border rounded-8 transition-all duration-fast outline-none placeholder:text-input-placeholder hover:border-input-border-hover focus:border-input-border-hover disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-input-bg-disabled motion-reduce:transition-none"
115
166
  />
116
167
  <Combobox.Portal>
117
- {#if filteredItems.length > 0 || emptyContent}
168
+ {#if filteredItems.length > 0 || emptyContent || loading}
118
169
  <Combobox.Content
119
170
  class={cn(
120
171
  'bg-dropdown-bg border border-dropdown-border rounded-10 shadow-dropdown p-4 max-h-300 overflow-y-auto z-16',
@@ -124,33 +175,28 @@
124
175
  sideOffset={2}
125
176
  align="start"
126
177
  >
127
- {#each filteredItems as item, i (i + '-' + getItemValue(item))}
128
- <Combobox.Item
129
- value={getItemValue(item)}
130
- label=""
131
- disabled={getItemDisabled?.(item) ?? false}
132
- class="flex items-center gap-8 py-6 px-12 text-small text-dropdown-item rounded-6 cursor-default transition-all duration-fast outline-none hover:bg-dropdown-item-hover data-[highlighted]:bg-dropdown-item-hover data-[selected]:bg-dropdown-item-selected data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed motion-reduce:transition-none"
133
- >
134
- {#snippet children({selected})}
135
- {#if renderItem}
136
- {@render renderItem(item, selected)}
137
- {:else if isSimpleItem(item)}
138
- <span class="flex-1">{item.label}</span>
139
- {#if selected}
140
- <IIIcon iconName="check" class="w-18 h-18 shrink-0 ml-auto" />
141
- {/if}
142
- {:else}
143
- <span class="flex-1">{getItemValue(item)}</span>
144
- {#if selected}
145
- <IIIcon iconName="check" class="w-18 h-18 shrink-0 ml-auto" />
146
- {/if}
147
- {/if}
148
- {/snippet}
149
- </Combobox.Item>
150
- {/each}
178
+ {#if filteredGroups}
179
+ {#each filteredGroups as group, gi (gi)}
180
+ <div class="text-tiny-emphasis text-secondary px-12 py-4 uppercase select-none">
181
+ {group.heading}
182
+ </div>
183
+ {#each group.items as item, i (i + '-' + getItemValue(item))}
184
+ {@render comboboxItem(item, i)}
185
+ {/each}
186
+ {/each}
187
+ {:else}
188
+ {#each filteredItems as item, i (i + '-' + getItemValue(item))}
189
+ {@render comboboxItem(item, i)}
190
+ {/each}
191
+ {/if}
151
192
  {#if filteredItems.length === 0 && emptyContent}
152
193
  {@render emptyContent()}
153
194
  {/if}
195
+ {#if loading}
196
+ <div class="flex items-center justify-center py-8">
197
+ <span class="w-16 h-16 border-2 border-current border-r-transparent rounded-full animate-spin text-secondary"></span>
198
+ </div>
199
+ {/if}
154
200
  </Combobox.Content>
155
201
  {/if}
156
202
  </Combobox.Portal>
@@ -1,15 +1,21 @@
1
1
  import type { Snippet } from 'svelte';
2
+ type Group<T> = {
3
+ heading: string;
4
+ items: T[];
5
+ };
2
6
  type Props<T> = {
3
7
  items: T[];
4
8
  value?: string;
5
9
  placeholder?: string;
6
10
  disabled?: boolean;
11
+ loading?: boolean;
7
12
  onSelect?: (item: T) => void;
8
13
  getItemValue: (item: T) => string;
9
14
  getItemDisabled?: (item: T) => boolean;
10
15
  filterFn?: (items: T[], inputValue: string) => T[];
11
16
  renderItem?: Snippet<[item: T, selected: boolean]>;
12
17
  emptyContent?: Snippet;
18
+ groups?: Group<T>[];
13
19
  class?: string;
14
20
  contentClass?: string;
15
21
  };
@@ -0,0 +1,97 @@
1
+ <script lang="ts">
2
+ import IICombobox from './IICombobox.svelte'
3
+
4
+ type SimpleItem = {
5
+ label: string
6
+ value: string
7
+ disabled?: boolean
8
+ }
9
+
10
+ const getItemValue = (item: SimpleItem) => item.value
11
+
12
+ let groupedValue = $state('')
13
+ let loadingValue = $state('')
14
+ let loading = $state(false)
15
+
16
+ const teamGroups = [
17
+ {
18
+ heading: 'Engineering',
19
+ items: [
20
+ {label: 'Alice Chen', value: 'alice'},
21
+ {label: 'Bob Smith', value: 'bob'},
22
+ {label: 'Carol Wu', value: 'carol'},
23
+ ],
24
+ },
25
+ {
26
+ heading: 'Design',
27
+ items: [
28
+ {label: 'Diana Patel', value: 'diana'},
29
+ {label: 'Evan Kim', value: 'evan'},
30
+ ],
31
+ },
32
+ {
33
+ heading: 'Product',
34
+ items: [
35
+ {label: 'Frank Lee', value: 'frank'},
36
+ {label: 'Grace Ng', value: 'grace'},
37
+ ],
38
+ },
39
+ ]
40
+
41
+ const asyncItems: SimpleItem[] = [
42
+ {label: 'California', value: 'CA'},
43
+ {label: 'New York', value: 'NY'},
44
+ {label: 'Texas', value: 'TX'},
45
+ {label: 'Florida', value: 'FL'},
46
+ ]
47
+
48
+ function simulateLoading() {
49
+ loading = true
50
+ setTimeout(() => {
51
+ loading = false
52
+ }, 2000)
53
+ }
54
+ </script>
55
+
56
+ <div class="flex flex-col gap-32">
57
+ <!-- Grouped Items -->
58
+ <section>
59
+ <h2 class="text-default-emphasis text-primary mb-8">Grouped Items</h2>
60
+ <p class="text-small text-secondary mb-12">Items organized by team with group headings. Groups with no matching items are hidden during filtering.</p>
61
+ <div class="w-280">
62
+ <IICombobox
63
+ items={[]}
64
+ groups={teamGroups}
65
+ bind:value={groupedValue}
66
+ placeholder="Search team members..."
67
+ {getItemValue}
68
+ />
69
+ </div>
70
+ {#if groupedValue}
71
+ <p class="text-small text-secondary mt-8">Selected: {groupedValue}</p>
72
+ {/if}
73
+ </section>
74
+
75
+ <!-- Loading State -->
76
+ <section>
77
+ <h2 class="text-default-emphasis text-primary mb-8">Loading State</h2>
78
+ <p class="text-small text-secondary mb-12">Shows a spinner at the bottom of the dropdown while loading. Click "Simulate Load" then open the combobox.</p>
79
+ <div class="flex items-center gap-12">
80
+ <div class="w-280">
81
+ <IICombobox
82
+ items={asyncItems}
83
+ bind:value={loadingValue}
84
+ placeholder="Select a state..."
85
+ {getItemValue}
86
+ {loading}
87
+ />
88
+ </div>
89
+ <button
90
+ class="px-12 py-5 rounded-8 border border-button-secondary text-small text-button-secondary hover:border-button-secondary-hover cursor-default"
91
+ onclick={simulateLoading}
92
+ >
93
+ Simulate Load
94
+ </button>
95
+ </div>
96
+ </section>
97
+ </div>
@@ -0,0 +1,3 @@
1
+ declare const IIComboboxStories: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type IIComboboxStories = ReturnType<typeof IIComboboxStories>;
3
+ export default IIComboboxStories;
@@ -18,7 +18,7 @@
18
18
  {/if}
19
19
  <div class="relative">
20
20
  <DatePicker.Input
21
- class="flex items-center gap-4 py-5 px-12 border border-strong rounded-10 text-small text-gray-800 bg-surface transition-all duration-fast [&:has(:focus)]:border-primary [&:has(:focus)]:ring-3 [&:has(:focus)]:ring-primary"
21
+ class="flex items-center gap-4 py-5 px-12 border border-strong rounded-8 text-small text-gray-800 bg-surface transition-all duration-fast [&:has(:focus)]:border-primary [&:has(:focus)]:ring-3 [&:has(:focus)]:ring-primary"
22
22
  >
23
23
  {#snippet children({segments})}
24
24
  {#each segments as { part, value: segValue }, i (`${part}-${i}`)}
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import type {Snippet} from 'svelte'
2
3
  import {DropdownMenu} from 'bits-ui'
3
4
  import {IIIcon} from '../IIIcon'
4
5
  import {cn} from '../utils/cn'
@@ -6,6 +7,8 @@
6
7
  type Item = {
7
8
  label: string
8
9
  value: string
10
+ icon?: Snippet
11
+ disabled?: boolean
9
12
  }
10
13
 
11
14
  type Props = {
@@ -15,6 +18,8 @@
15
18
  disabled?: boolean
16
19
  onSelect?: (value: string) => void
17
20
  matchTriggerWidth?: boolean
21
+ renderItem?: Snippet<[item: Item, selected: boolean]>
22
+ renderSelected?: Snippet<[item: Item]>
18
23
  class?: string
19
24
  }
20
25
 
@@ -25,6 +30,8 @@
25
30
  disabled = false,
26
31
  onSelect,
27
32
  matchTriggerWidth = false,
33
+ renderItem,
34
+ renderSelected,
28
35
  class: className,
29
36
  }: Props = $props()
30
37
 
@@ -32,7 +39,8 @@
32
39
  let triggerEl = $state<HTMLElement | null>(null)
33
40
  let triggerWidth = $state<number | undefined>(undefined)
34
41
 
35
- const selectedLabel = $derived(items.find(i => i.value === value)?.label ?? placeholder)
42
+ const selectedItem = $derived(items.find(i => i.value === value))
43
+ const selectedLabel = $derived(selectedItem?.label ?? placeholder)
36
44
 
37
45
  function handleSelect(item: Item) {
38
46
  value = item.value
@@ -52,12 +60,16 @@
52
60
  bind:ref={triggerEl}
53
61
  {disabled}
54
62
  class={cn(
55
- 'flex items-center justify-between gap-4 py-5 pl-12 pr-8 border rounded-10 bg-button-secondary cursor-default text-small text-button-secondary box-border appearance-none font-inherit outline-none transition-colors duration-base ease-in-out hover:text-button-secondary-hover hover:border-button-secondary-hover focus:border-accent focus:ring-3 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed',
63
+ 'flex items-center justify-between gap-4 py-5 pl-12 pr-8 border rounded-8 bg-button-secondary cursor-default text-small text-button-secondary box-border appearance-none font-inherit outline-none transition-colors duration-base ease-in-out hover:text-button-secondary-hover hover:border-button-secondary-hover focus:border-accent focus:ring-3 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed',
56
64
  open ? 'border-button-secondary-hover' : 'border-button-secondary',
57
65
  className
58
66
  )}
59
67
  >
60
- <span class="text-small">{selectedLabel}</span>
68
+ {#if renderSelected && selectedItem}
69
+ {@render renderSelected(selectedItem)}
70
+ {:else}
71
+ <span class="text-small">{selectedLabel}</span>
72
+ {/if}
61
73
  <IIIcon iconName="caret-down" class="w-14 h-14 shrink-0" />
62
74
  </DropdownMenu.Trigger>
63
75
  <DropdownMenu.Portal>
@@ -72,15 +84,27 @@
72
84
  <div {...props}>
73
85
  {#each items as item (item.value)}
74
86
  <DropdownMenu.Item
87
+ disabled={item.disabled}
75
88
  class={cn(
76
- 'flex items-center justify-between gap-12 px-12 py-6 rounded-6 text-small text-dropdown-item cursor-default outline-none data-[highlighted]:bg-dropdown-item-hover',
89
+ 'flex items-center justify-between gap-12 px-12 py-6 rounded-6 text-small text-dropdown-item cursor-default outline-none data-[highlighted]:bg-dropdown-item-hover data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed',
77
90
  value === item.value && 'text-dropdown-item-selected'
78
91
  )}
79
92
  onSelect={() => handleSelect(item)}
80
93
  >
81
- <span>{item.label}</span>
82
- {#if value === item.value}
83
- <IIIcon iconName="check" class="w-14 h-14 text-accent shrink-0" />
94
+ {#if renderItem}
95
+ {@render renderItem(item, value === item.value)}
96
+ {:else}
97
+ <span class="flex items-center gap-8">
98
+ {#if item.icon}
99
+ <span class="w-16 h-16 flex items-center justify-center shrink-0 [&_svg]:w-16 [&_svg]:h-16">
100
+ {@render item.icon()}
101
+ </span>
102
+ {/if}
103
+ {item.label}
104
+ </span>
105
+ {#if value === item.value}
106
+ <IIIcon iconName="check" class="w-14 h-14 text-accent shrink-0" />
107
+ {/if}
84
108
  {/if}
85
109
  </DropdownMenu.Item>
86
110
  {/each}
@@ -1,6 +1,9 @@
1
+ import type { Snippet } from 'svelte';
1
2
  type Item = {
2
3
  label: string;
3
4
  value: string;
5
+ icon?: Snippet;
6
+ disabled?: boolean;
4
7
  };
5
8
  type Props = {
6
9
  items: Item[];
@@ -9,6 +12,8 @@ type Props = {
9
12
  disabled?: boolean;
10
13
  onSelect?: (value: string) => void;
11
14
  matchTriggerWidth?: boolean;
15
+ renderItem?: Snippet<[item: Item, selected: boolean]>;
16
+ renderSelected?: Snippet<[item: Item]>;
12
17
  class?: string;
13
18
  };
14
19
  declare const IIDropdownInput: import("svelte").Component<Props, {}, "value">;