@insymetri/styleguide 0.1.12 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/IIButton/IIButton.svelte +1 -1
- package/dist/IICalendar/IICalendar.svelte +101 -0
- package/dist/IICalendar/IICalendar.svelte.d.ts +13 -0
- package/dist/IICalendar/IICalendarStories.svelte +60 -0
- package/dist/IICalendar/IICalendarStories.svelte.d.ts +3 -0
- package/dist/IICalendar/index.d.ts +1 -0
- package/dist/IICalendar/index.js +1 -0
- package/dist/IICombobox/IICombobox.svelte +75 -29
- package/dist/IICombobox/IICombobox.svelte.d.ts +6 -0
- package/dist/IICombobox/IIComboboxStories.svelte +97 -0
- package/dist/IICombobox/IIComboboxStories.svelte.d.ts +3 -0
- package/dist/IIDropdownInput/IIDropdownInput.svelte +30 -6
- package/dist/IIDropdownInput/IIDropdownInput.svelte.d.ts +5 -0
- package/dist/IIDropdownInput/IIDropdownInputStories.svelte +88 -0
- package/dist/IIDropdownInput/IIDropdownInputStories.svelte.d.ts +3 -0
- package/dist/IIDropdownMenu/IIDropdownMenu.svelte +90 -23
- package/dist/IIDropdownMenu/IIDropdownMenu.svelte.d.ts +14 -2
- package/dist/IIDropdownMenu/IIDropdownMenuStories.svelte +101 -0
- package/dist/IIDropdownMenu/IIDropdownMenuStories.svelte.d.ts +18 -0
- package/dist/IIIconButton/IIIconButton.svelte +1 -1
- package/dist/IIMultiSelect/IIMultiSelect.svelte +141 -0
- package/dist/IIMultiSelect/IIMultiSelect.svelte.d.ts +20 -0
- package/dist/IIMultiSelect/IIMultiSelectStories.svelte +78 -0
- package/dist/IIMultiSelect/IIMultiSelectStories.svelte.d.ts +3 -0
- package/dist/IIMultiSelect/index.d.ts +1 -0
- package/dist/IIMultiSelect/index.js +1 -0
- package/dist/IIPopover/IIPopover.svelte +48 -0
- package/dist/IIPopover/IIPopover.svelte.d.ts +15 -0
- package/dist/IIPopover/IIPopoverStories.svelte +108 -0
- package/dist/IIPopover/IIPopoverStories.svelte.d.ts +3 -0
- package/dist/IIPopover/index.d.ts +1 -0
- package/dist/IIPopover/index.js +1 -0
- package/dist/IIToggle/IIToggle.svelte +52 -0
- package/dist/IIToggle/IIToggle.svelte.d.ts +15 -0
- package/dist/IIToggle/IIToggleStories.svelte +89 -0
- package/dist/IIToggle/IIToggleStories.svelte.d.ts +3 -0
- package/dist/IIToggle/index.d.ts +1 -0
- package/dist/IIToggle/index.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/style/colors.css +3 -3
- 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-
|
|
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'
|
|
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("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"); 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("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"); 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 @@
|
|
|
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
|
-
|
|
69
|
-
|
|
81
|
+
const filter = filterFn ?? defaultFilter
|
|
82
|
+
if (groups) {
|
|
83
|
+
return filter(allItems, inputValue)
|
|
70
84
|
}
|
|
71
|
-
return
|
|
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 =
|
|
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"
|
|
@@ -114,7 +165,7 @@
|
|
|
114
165
|
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"
|
|
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
|
-
{#
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
{
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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-10 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>
|
|
@@ -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
|
|
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
|
|
@@ -57,7 +65,11 @@
|
|
|
57
65
|
className
|
|
58
66
|
)}
|
|
59
67
|
>
|
|
60
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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">;
|