twintrinsic 0.0.1
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/LICENSE +674 -0
- package/README.md +150 -0
- package/dist/App/App.svelte +54 -0
- package/dist/App/App.svelte.d.ts +65 -0
- package/dist/Section.svelte +25 -0
- package/dist/Section.svelte.d.ts +34 -0
- package/dist/actions/clickOutside.d.ts +9 -0
- package/dist/actions/clickOutside.js +19 -0
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/components/Accordion/Accordion.svelte +75 -0
- package/dist/components/Accordion/Accordion.svelte.d.ts +39 -0
- package/dist/components/Accordion/AccordionItem.svelte +150 -0
- package/dist/components/Accordion/AccordionItem.svelte.d.ts +30 -0
- package/dist/components/App/App.story.md +8 -0
- package/dist/components/App/App.story.svelte +170 -0
- package/dist/components/App/App.story.svelte.d.ts +22 -0
- package/dist/components/App/App.svelte +77 -0
- package/dist/components/App/App.svelte.d.ts +66 -0
- package/dist/components/App/Split.svelte +346 -0
- package/dist/components/App/Split.svelte.d.ts +54 -0
- package/dist/components/App/index.d.ts +2 -0
- package/dist/components/App/index.js +3 -0
- package/dist/components/AppHeader/AppHeader.svelte +439 -0
- package/dist/components/AppHeader/AppHeader.svelte.d.ts +24 -0
- package/dist/components/Avatar/Avatar.svelte +300 -0
- package/dist/components/Avatar/Avatar.svelte.d.ts +48 -0
- package/dist/components/Avatar/AvatarGroup.svelte +185 -0
- package/dist/components/Avatar/AvatarGroup.svelte.d.ts +46 -0
- package/dist/components/Badge/Badge.svelte +186 -0
- package/dist/components/Badge/Badge.svelte.d.ts +51 -0
- package/dist/components/BottomBar/BottomBar.svelte +146 -0
- package/dist/components/BottomBar/BottomBar.svelte.d.ts +38 -0
- package/dist/components/Breadcrumb/Breadcrumb.svelte +77 -0
- package/dist/components/Breadcrumb/Breadcrumb.svelte.d.ts +42 -0
- package/dist/components/Breadcrumb/BreadcrumbItem.svelte +171 -0
- package/dist/components/Breadcrumb/BreadcrumbItem.svelte.d.ts +38 -0
- package/dist/components/Button/Button.svelte +252 -0
- package/dist/components/Button/Button.svelte.d.ts +80 -0
- package/dist/components/Button/ButtonGroup.svelte +127 -0
- package/dist/components/Button/ButtonGroup.svelte.d.ts +44 -0
- package/dist/components/Card/Card.svelte +152 -0
- package/dist/components/Card/Card.svelte.d.ts +55 -0
- package/dist/components/Carousel/Carousel.svelte +461 -0
- package/dist/components/Carousel/Carousel.svelte.d.ts +79 -0
- package/dist/components/Carousel/CarouselItem.svelte +149 -0
- package/dist/components/Carousel/CarouselItem.svelte.d.ts +35 -0
- package/dist/components/Chip/Chip.svelte +288 -0
- package/dist/components/Chip/Chip.svelte.d.ts +71 -0
- package/dist/components/Chip/ChipGroup.svelte +190 -0
- package/dist/components/Chip/ChipGroup.svelte.d.ts +71 -0
- package/dist/components/CodeBlock/CodeBlock.svelte +356 -0
- package/dist/components/CodeBlock/CodeBlock.svelte.d.ts +44 -0
- package/dist/components/CodeBlock/index.d.ts +1 -0
- package/dist/components/CodeBlock/index.js +1 -0
- package/dist/components/CodeBlockSpeed/CodeBlockSpeed.svelte +145 -0
- package/dist/components/CodeBlockSpeed/CodeBlockSpeed.svelte.d.ts +44 -0
- package/dist/components/CodeEditor/CodeEditor.svelte +229 -0
- package/dist/components/CodeEditor/CodeEditor.svelte.d.ts +23 -0
- package/dist/components/Combobox/Combobox.svelte +279 -0
- package/dist/components/Combobox/Combobox.svelte.d.ts +34 -0
- package/dist/components/Container/Container.svelte +45 -0
- package/dist/components/Container/Container.svelte.d.ts +36 -0
- package/dist/components/DataTable/DataTable.svelte +879 -0
- package/dist/components/DataTable/DataTable.svelte.d.ts +102 -0
- package/dist/components/Form/AutoComplete.svelte +357 -0
- package/dist/components/Form/AutoComplete.svelte.d.ts +73 -0
- package/dist/components/Form/Calendar.svelte +429 -0
- package/dist/components/Form/Calendar.svelte.d.ts +53 -0
- package/dist/components/Form/Checkbox.svelte +196 -0
- package/dist/components/Form/Checkbox.svelte.d.ts +50 -0
- package/dist/components/Form/ColorPicker.svelte +396 -0
- package/dist/components/Form/ColorPicker.svelte.d.ts +43 -0
- package/dist/components/Form/Combobox.svelte +645 -0
- package/dist/components/Form/Combobox.svelte.d.ts +93 -0
- package/dist/components/Form/Dropdown.svelte +773 -0
- package/dist/components/Form/Dropdown.svelte.d.ts +81 -0
- package/dist/components/Form/FileUpload.svelte +796 -0
- package/dist/components/Form/FileUpload.svelte.d.ts +78 -0
- package/dist/components/Form/FloatLabel.svelte +245 -0
- package/dist/components/Form/FloatLabel.svelte.d.ts +44 -0
- package/dist/components/Form/Form.svelte +281 -0
- package/dist/components/Form/Form.svelte.d.ts +54 -0
- package/dist/components/Form/FormField.svelte +218 -0
- package/dist/components/Form/FormField.svelte.d.ts +47 -0
- package/dist/components/Form/Input.svelte +340 -0
- package/dist/components/Form/Input.svelte.d.ts +79 -0
- package/dist/components/Form/InputSwitch.svelte +189 -0
- package/dist/components/Form/InputSwitch.svelte.d.ts +46 -0
- package/dist/components/Form/InvalidState.svelte +97 -0
- package/dist/components/Form/InvalidState.svelte.d.ts +37 -0
- package/dist/components/Form/Knob.svelte +537 -0
- package/dist/components/Form/Knob.svelte.d.ts +78 -0
- package/dist/components/Form/ListInput.svelte +469 -0
- package/dist/components/Form/ListInput.svelte.d.ts +70 -0
- package/dist/components/Form/Listbox.svelte +513 -0
- package/dist/components/Form/Listbox.svelte.d.ts +74 -0
- package/dist/components/Form/NumberInput.svelte +452 -0
- package/dist/components/Form/NumberInput.svelte.d.ts +82 -0
- package/dist/components/Form/Radio.svelte +192 -0
- package/dist/components/Form/Radio.svelte.d.ts +53 -0
- package/dist/components/Form/RadioGroup.svelte +155 -0
- package/dist/components/Form/RadioGroup.svelte.d.ts +48 -0
- package/dist/components/Form/Rating.svelte +380 -0
- package/dist/components/Form/Rating.svelte.d.ts +64 -0
- package/dist/components/Form/Select.svelte +436 -0
- package/dist/components/Form/Select.svelte.d.ts +49 -0
- package/dist/components/Form/SelectGroup.svelte +34 -0
- package/dist/components/Form/SelectGroup.svelte.d.ts +33 -0
- package/dist/components/Form/Slider.svelte +622 -0
- package/dist/components/Form/Slider.svelte.d.ts +73 -0
- package/dist/components/Form/Switch.svelte +192 -0
- package/dist/components/Form/Switch.svelte.d.ts +46 -0
- package/dist/components/Form/TextInput.svelte +274 -0
- package/dist/components/Form/TextInput.svelte.d.ts +74 -0
- package/dist/components/Form/Textarea.svelte +207 -0
- package/dist/components/Form/Textarea.svelte.d.ts +62 -0
- package/dist/components/Icon/Icon.svelte +140 -0
- package/dist/components/Icon/Icon.svelte.d.ts +25 -0
- package/dist/components/Icon/index.d.ts +1 -0
- package/dist/components/Icon/index.js +1 -0
- package/dist/components/Lazy/Lazy.svelte +158 -0
- package/dist/components/Lazy/Lazy.svelte.d.ts +42 -0
- package/dist/components/Masonry/Masonry.svelte +299 -0
- package/dist/components/Masonry/Masonry.svelte.d.ts +55 -0
- package/dist/components/Menu/Menu/Menu.svelte +65 -0
- package/dist/components/Menu/Menu/Menu.svelte.d.ts +17 -0
- package/dist/components/Menu/Menu/MenuItem.svelte +90 -0
- package/dist/components/Menu/Menu/MenuItem.svelte.d.ts +27 -0
- package/dist/components/Modal/Modal.svelte +334 -0
- package/dist/components/Modal/Modal.svelte.d.ts +55 -0
- package/dist/components/Panel/Card.svelte +141 -0
- package/dist/components/Panel/Card.svelte.d.ts +52 -0
- package/dist/components/Panel/Hero/Hero.story.md +9 -0
- package/dist/components/Panel/Hero/Hero.story.svelte +49 -0
- package/dist/components/Panel/Hero/Hero.story.svelte.d.ts +21 -0
- package/dist/components/Panel/Hero/Hero.svelte +24 -0
- package/dist/components/Panel/Hero/Hero.svelte.d.ts +32 -0
- package/dist/components/Panel/LazyPanel.svelte +110 -0
- package/dist/components/Panel/LazyPanel.svelte.d.ts +46 -0
- package/dist/components/Panel/Panel.svelte +205 -0
- package/dist/components/Panel/Panel.svelte.d.ts +23 -0
- package/dist/components/Progress/Progress.svelte +220 -0
- package/dist/components/Progress/Progress.svelte.d.ts +61 -0
- package/dist/components/Separator/Separator.svelte +109 -0
- package/dist/components/Separator/Separator.svelte.d.ts +35 -0
- package/dist/components/Sidebar/Sidebar.svelte +213 -0
- package/dist/components/Sidebar/Sidebar.svelte.d.ts +60 -0
- package/dist/components/Skeleton/Skeleton.svelte +170 -0
- package/dist/components/Skeleton/Skeleton.svelte.d.ts +48 -0
- package/dist/components/Stepper/Stepper.svelte +111 -0
- package/dist/components/Stepper/Stepper.svelte.d.ts +54 -0
- package/dist/components/Stepper/StepperStep.svelte +369 -0
- package/dist/components/Stepper/StepperStep.svelte.d.ts +63 -0
- package/dist/components/Table/Table.svelte +167 -0
- package/dist/components/Table/Table.svelte.d.ts +56 -0
- package/dist/components/Table/TableBody.svelte +41 -0
- package/dist/components/Table/TableBody.svelte.d.ts +33 -0
- package/dist/components/Table/TableCell.svelte +76 -0
- package/dist/components/Table/TableCell.svelte.d.ts +36 -0
- package/dist/components/Table/TableHead.svelte +41 -0
- package/dist/components/Table/TableHead.svelte.d.ts +32 -0
- package/dist/components/Table/TableHeader.svelte +148 -0
- package/dist/components/Table/TableHeader.svelte.d.ts +42 -0
- package/dist/components/Table/TableRow.svelte +99 -0
- package/dist/components/Table/TableRow.svelte.d.ts +40 -0
- package/dist/components/Tabs/Tab.svelte +145 -0
- package/dist/components/Tabs/Tab.svelte.d.ts +36 -0
- package/dist/components/Tabs/TabList.svelte +60 -0
- package/dist/components/Tabs/TabList.svelte.d.ts +32 -0
- package/dist/components/Tabs/TabPanel.svelte +118 -0
- package/dist/components/Tabs/TabPanel.svelte.d.ts +38 -0
- package/dist/components/Tabs/Tabs.svelte +287 -0
- package/dist/components/Tabs/Tabs.svelte.d.ts +50 -0
- package/dist/components/Tag/Tag.svelte +260 -0
- package/dist/components/Tag/Tag.svelte.d.ts +54 -0
- package/dist/components/Tag/TagGroup.svelte +147 -0
- package/dist/components/Tag/TagGroup.svelte.d.ts +62 -0
- package/dist/components/ThemeToggle/ThemeToggle.svelte +93 -0
- package/dist/components/ThemeToggle/ThemeToggle.svelte.d.ts +12 -0
- package/dist/components/Timeline/Timeline.svelte +144 -0
- package/dist/components/Timeline/Timeline.svelte.d.ts +48 -0
- package/dist/components/Timeline/TimelineItem.svelte +391 -0
- package/dist/components/Timeline/TimelineItem.svelte.d.ts +63 -0
- package/dist/components/Toast/Toast.svelte +313 -0
- package/dist/components/Toast/Toast.svelte.d.ts +44 -0
- package/dist/components/Toast/toastStore.d.ts +40 -0
- package/dist/components/Toast/toastStore.js +293 -0
- package/dist/components/Tooltip/Tooltip.svelte +282 -0
- package/dist/components/Tooltip/Tooltip.svelte.d.ts +55 -0
- package/dist/components/Tree/Tree.svelte +129 -0
- package/dist/components/Tree/Tree.svelte.d.ts +61 -0
- package/dist/components/Tree/TreeNode.svelte +332 -0
- package/dist/components/Tree/TreeNode.svelte.d.ts +55 -0
- package/dist/components/icons/TwintrinsicLogo.svelte +73 -0
- package/dist/components/icons/TwintrinsicLogo.svelte.d.ts +17 -0
- package/dist/components/icons/twintrinsic-source.svg +73 -0
- package/dist/components/icons/twintrinsic.svg +38 -0
- package/dist/docs/EventsTable.svelte +86 -0
- package/dist/docs/EventsTable.svelte.d.ts +27 -0
- package/dist/docs/PropsTable.svelte +103 -0
- package/dist/docs/PropsTable.svelte.d.ts +28 -0
- package/dist/docs/index.d.ts +2 -0
- package/dist/docs/index.js +2 -0
- package/dist/helpers/detectLanguage.d.ts +6 -0
- package/dist/helpers/detectLanguage.js +60 -0
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/index.js +1 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +94 -0
- package/dist/twintrinsic.css +347 -0
- package/package.json +98 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
Listbox - A component for selecting one or multiple options from a list.
|
|
4
|
+
Provides accessible keyboard navigation, filtering, and customization options.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
```svelte
|
|
8
|
+
<Listbox
|
|
9
|
+
name="users"
|
|
10
|
+
options={users}
|
|
11
|
+
optionLabel="name"
|
|
12
|
+
optionValue="id"
|
|
13
|
+
/>
|
|
14
|
+
|
|
15
|
+
<Listbox
|
|
16
|
+
name="colors"
|
|
17
|
+
options={colors}
|
|
18
|
+
multiple
|
|
19
|
+
filter
|
|
20
|
+
/>
|
|
21
|
+
|
|
22
|
+
<FormField label="Select a category">
|
|
23
|
+
<Listbox
|
|
24
|
+
name="category"
|
|
25
|
+
options={categories}
|
|
26
|
+
optionIcon="icon"
|
|
27
|
+
/>
|
|
28
|
+
</FormField>
|
|
29
|
+
```
|
|
30
|
+
-->
|
|
31
|
+
<script>
|
|
32
|
+
import { getContext, onMount } from "svelte"
|
|
33
|
+
|
|
34
|
+
const {
|
|
35
|
+
/** @type {string} - Additional CSS classes */
|
|
36
|
+
class: className = "",
|
|
37
|
+
|
|
38
|
+
/** @type {string} - HTML id for accessibility */
|
|
39
|
+
id = crypto.randomUUID(),
|
|
40
|
+
|
|
41
|
+
/** @type {string} - Input name */
|
|
42
|
+
name,
|
|
43
|
+
|
|
44
|
+
/** @type {Array} - Options to display */
|
|
45
|
+
options = [],
|
|
46
|
+
|
|
47
|
+
/** @type {any} - Selected value(s) */
|
|
48
|
+
value,
|
|
49
|
+
|
|
50
|
+
/** @type {boolean} - Whether multiple selection is allowed */
|
|
51
|
+
multiple = false,
|
|
52
|
+
|
|
53
|
+
/** @type {string} - Property name for option label */
|
|
54
|
+
optionLabel = "label",
|
|
55
|
+
|
|
56
|
+
/** @type {string} - Property name for option value */
|
|
57
|
+
optionValue = "value",
|
|
58
|
+
|
|
59
|
+
/** @type {string} - Property name for option icon */
|
|
60
|
+
optionIcon,
|
|
61
|
+
|
|
62
|
+
/** @type {boolean} - Whether the listbox is disabled */
|
|
63
|
+
disabled = false,
|
|
64
|
+
|
|
65
|
+
/** @type {boolean} - Whether the listbox is required */
|
|
66
|
+
required = false,
|
|
67
|
+
|
|
68
|
+
/** @type {boolean} - Whether to filter options by typing */
|
|
69
|
+
filter = false,
|
|
70
|
+
|
|
71
|
+
/** @type {string} - Placeholder for filter input */
|
|
72
|
+
filterPlaceholder = "Search...",
|
|
73
|
+
|
|
74
|
+
/** @type {number} - Maximum height of the listbox */
|
|
75
|
+
maxHeight = 300,
|
|
76
|
+
|
|
77
|
+
/** @type {boolean} - Whether to show a checkbox for multiple selection */
|
|
78
|
+
showCheckbox = true,
|
|
79
|
+
|
|
80
|
+
/** @type {string} - ARIA label for accessibility */
|
|
81
|
+
ariaLabel,
|
|
82
|
+
|
|
83
|
+
/** @type {(event: CustomEvent) => void} - Change event handler */
|
|
84
|
+
onchange,
|
|
85
|
+
/** @type {(event: CustomEvent) => void} - Filter event handler */
|
|
86
|
+
onfilter,
|
|
87
|
+
} = $props()
|
|
88
|
+
|
|
89
|
+
// Get form context if available
|
|
90
|
+
const formContext = getContext("form")
|
|
91
|
+
|
|
92
|
+
// Component state
|
|
93
|
+
let selectedValues = $state(multiple ? [] : null)
|
|
94
|
+
let filterValue = $state("")
|
|
95
|
+
let highlightedIndex = $state(0)
|
|
96
|
+
let listboxElement = $state()
|
|
97
|
+
let filterInputElement = $state()
|
|
98
|
+
|
|
99
|
+
// Register with form if available
|
|
100
|
+
let fieldApi = $state()
|
|
101
|
+
|
|
102
|
+
$effect(() => {
|
|
103
|
+
if (formContext && name) {
|
|
104
|
+
fieldApi = formContext.registerField(name, value)
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// Update value when form field changes
|
|
109
|
+
$effect(() => {
|
|
110
|
+
if (fieldApi) {
|
|
111
|
+
const formValue = fieldApi.getValue()
|
|
112
|
+
if (formValue !== undefined && JSON.stringify(formValue) !== JSON.stringify(selectedValues)) {
|
|
113
|
+
selectedValues = formValue
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// Initialize selected values from prop
|
|
119
|
+
$effect(() => {
|
|
120
|
+
if (value !== undefined) {
|
|
121
|
+
selectedValues = value
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Gets the display label for an option
|
|
127
|
+
* @param {Object|string} option - Option to get label for
|
|
128
|
+
* @returns {string} - Display label
|
|
129
|
+
*/
|
|
130
|
+
function getOptionLabel(option) {
|
|
131
|
+
if (!option) return ""
|
|
132
|
+
|
|
133
|
+
if (typeof option === "object") {
|
|
134
|
+
return option[optionLabel] || ""
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return option.toString()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Gets the value for an option
|
|
142
|
+
* @param {Object|string} option - Option to get value for
|
|
143
|
+
* @returns {any} - Option value
|
|
144
|
+
*/
|
|
145
|
+
function getOptionValue(option) {
|
|
146
|
+
if (!option) return null
|
|
147
|
+
|
|
148
|
+
if (typeof option === "object") {
|
|
149
|
+
return option[optionValue]
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return option
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Gets the icon for an option
|
|
157
|
+
* @param {Object} option - Option to get icon for
|
|
158
|
+
* @returns {string} - Icon HTML
|
|
159
|
+
*/
|
|
160
|
+
function getOptionIcon(option) {
|
|
161
|
+
if (!option || !optionIcon || typeof option !== "object") return null
|
|
162
|
+
|
|
163
|
+
return option[optionIcon]
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Checks if an option is selected
|
|
168
|
+
* @param {Object|string} option - Option to check
|
|
169
|
+
* @returns {boolean} - Whether the option is selected
|
|
170
|
+
*/
|
|
171
|
+
function isOptionSelected(option) {
|
|
172
|
+
const value = getOptionValue(option)
|
|
173
|
+
|
|
174
|
+
if (multiple) {
|
|
175
|
+
return (
|
|
176
|
+
Array.isArray(selectedValues) &&
|
|
177
|
+
selectedValues.some((v) => (typeof v === "object" ? v[optionValue] === value : v === value))
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
selectedValues === value ||
|
|
183
|
+
(typeof selectedValues === "object" && selectedValues && selectedValues[optionValue] === value)
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Filters options based on input value
|
|
189
|
+
* @returns {Array} - Filtered options
|
|
190
|
+
*/
|
|
191
|
+
function filterOptions() {
|
|
192
|
+
if (!filter || !filterValue) return options
|
|
193
|
+
|
|
194
|
+
return options.filter((option) => {
|
|
195
|
+
const label = getOptionLabel(option).toLowerCase()
|
|
196
|
+
return label.includes(filterValue.toLowerCase())
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Selects an option
|
|
202
|
+
* @param {Object|string} option - Option to select
|
|
203
|
+
*/
|
|
204
|
+
function selectOption(option) {
|
|
205
|
+
if (disabled) return
|
|
206
|
+
|
|
207
|
+
const value = getOptionValue(option)
|
|
208
|
+
|
|
209
|
+
if (multiple) {
|
|
210
|
+
if (isOptionSelected(option)) {
|
|
211
|
+
// Remove from selection
|
|
212
|
+
selectedValues = Array.isArray(selectedValues)
|
|
213
|
+
? selectedValues.filter((v) =>
|
|
214
|
+
typeof v === "object" ? v[optionValue] !== value : v !== value
|
|
215
|
+
)
|
|
216
|
+
: []
|
|
217
|
+
} else {
|
|
218
|
+
// Add to selection
|
|
219
|
+
selectedValues = Array.isArray(selectedValues) ? [...selectedValues, value] : [value]
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
// Single selection
|
|
223
|
+
selectedValues = value
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Update form field if available
|
|
227
|
+
if (fieldApi) {
|
|
228
|
+
fieldApi.setValue(selectedValues)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
onchange?.(new CustomEvent("change", { detail: { value: selectedValues } }))
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Handles keydown events
|
|
236
|
+
* @param {KeyboardEvent} event - Keydown event
|
|
237
|
+
*/
|
|
238
|
+
function handleKeydown(event) {
|
|
239
|
+
if (disabled) return
|
|
240
|
+
|
|
241
|
+
const filteredOptions = filterOptions()
|
|
242
|
+
|
|
243
|
+
switch (event.key) {
|
|
244
|
+
case "ArrowDown":
|
|
245
|
+
event.preventDefault()
|
|
246
|
+
highlightedIndex = (highlightedIndex + 1) % filteredOptions.length
|
|
247
|
+
scrollOptionIntoView()
|
|
248
|
+
break
|
|
249
|
+
|
|
250
|
+
case "ArrowUp":
|
|
251
|
+
event.preventDefault()
|
|
252
|
+
highlightedIndex = (highlightedIndex - 1 + filteredOptions.length) % filteredOptions.length
|
|
253
|
+
scrollOptionIntoView()
|
|
254
|
+
break
|
|
255
|
+
|
|
256
|
+
case "Enter":
|
|
257
|
+
case " ":
|
|
258
|
+
if (!filter || event.target !== filterInputElement) {
|
|
259
|
+
event.preventDefault()
|
|
260
|
+
if (filteredOptions[highlightedIndex]) {
|
|
261
|
+
selectOption(filteredOptions[highlightedIndex])
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
break
|
|
265
|
+
|
|
266
|
+
case "Home":
|
|
267
|
+
event.preventDefault()
|
|
268
|
+
highlightedIndex = 0
|
|
269
|
+
scrollOptionIntoView()
|
|
270
|
+
break
|
|
271
|
+
|
|
272
|
+
case "End":
|
|
273
|
+
event.preventDefault()
|
|
274
|
+
highlightedIndex = filteredOptions.length - 1
|
|
275
|
+
scrollOptionIntoView()
|
|
276
|
+
break
|
|
277
|
+
|
|
278
|
+
case "Tab":
|
|
279
|
+
// Allow normal tab behavior
|
|
280
|
+
break
|
|
281
|
+
|
|
282
|
+
default:
|
|
283
|
+
// If filter is not enabled, use type-ahead
|
|
284
|
+
if (!filter && event.key.length === 1) {
|
|
285
|
+
const char = event.key.toLowerCase()
|
|
286
|
+
const matchingIndex = filteredOptions.findIndex(
|
|
287
|
+
(option, index) =>
|
|
288
|
+
index > highlightedIndex && getOptionLabel(option).toLowerCase().startsWith(char)
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
if (matchingIndex !== -1) {
|
|
292
|
+
highlightedIndex = matchingIndex
|
|
293
|
+
scrollOptionIntoView()
|
|
294
|
+
} else {
|
|
295
|
+
// Try from the beginning
|
|
296
|
+
const firstMatchingIndex = filteredOptions.findIndex((option) =>
|
|
297
|
+
getOptionLabel(option).toLowerCase().startsWith(char)
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
if (firstMatchingIndex !== -1) {
|
|
301
|
+
highlightedIndex = firstMatchingIndex
|
|
302
|
+
scrollOptionIntoView()
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
break
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Scrolls the highlighted option into view
|
|
312
|
+
*/
|
|
313
|
+
function scrollOptionIntoView() {
|
|
314
|
+
if (!listboxElement) return
|
|
315
|
+
|
|
316
|
+
const highlightedOption = listboxElement.querySelector(`[data-index="${highlightedIndex}"]`)
|
|
317
|
+
if (highlightedOption) {
|
|
318
|
+
highlightedOption.scrollIntoView({ block: "nearest" })
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Handles filter input
|
|
324
|
+
* @param {Event} event - Input event
|
|
325
|
+
*/
|
|
326
|
+
function handleFilterInput(event) {
|
|
327
|
+
filterValue = event.target.value
|
|
328
|
+
highlightedIndex = 0
|
|
329
|
+
|
|
330
|
+
onfilter?.(new CustomEvent("filter", { detail: { filter: filterValue } }))
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Focus the listbox on mount
|
|
334
|
+
onMount(() => {
|
|
335
|
+
if (filter && filterInputElement) {
|
|
336
|
+
filterInputElement.focus()
|
|
337
|
+
} else if (listboxElement) {
|
|
338
|
+
listboxElement.focus()
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
// Computed filtered options
|
|
343
|
+
const filteredOptions = $derived(filterOptions())
|
|
344
|
+
</script>
|
|
345
|
+
|
|
346
|
+
<div class="listbox-container {className}">
|
|
347
|
+
{#if filter}
|
|
348
|
+
<div class="listbox-filter">
|
|
349
|
+
<input
|
|
350
|
+
type="text"
|
|
351
|
+
class="listbox-filter-input"
|
|
352
|
+
placeholder={filterPlaceholder}
|
|
353
|
+
value={filterValue}
|
|
354
|
+
oninput={handleFilterInput}
|
|
355
|
+
onkeydown={handleKeydown}
|
|
356
|
+
bind:this={filterInputElement}
|
|
357
|
+
{disabled}
|
|
358
|
+
aria-controls={`${id}-listbox`}
|
|
359
|
+
/>
|
|
360
|
+
</div>
|
|
361
|
+
{/if}
|
|
362
|
+
|
|
363
|
+
<div
|
|
364
|
+
id="{id}-listbox"
|
|
365
|
+
class="listbox"
|
|
366
|
+
style="max-height: {maxHeight}px;"
|
|
367
|
+
tabindex={disabled ? undefined : 0}
|
|
368
|
+
role="listbox"
|
|
369
|
+
aria-multiselectable={multiple ? 'true' : undefined}
|
|
370
|
+
aria-label={ariaLabel || name}
|
|
371
|
+
aria-disabled={disabled ? 'true' : undefined}
|
|
372
|
+
onkeydown={handleKeydown}
|
|
373
|
+
bind:this={listboxElement}
|
|
374
|
+
>
|
|
375
|
+
{#if filteredOptions.length > 0}
|
|
376
|
+
<ul class="listbox-options">
|
|
377
|
+
{#each filteredOptions as option, index}
|
|
378
|
+
{@const isHighlighted = index === highlightedIndex}
|
|
379
|
+
{@const isSelected = isOptionSelected(option)}
|
|
380
|
+
|
|
381
|
+
<li
|
|
382
|
+
class="
|
|
383
|
+
listbox-option
|
|
384
|
+
{isHighlighted ? 'listbox-option-highlighted' : ''}
|
|
385
|
+
{isSelected ? 'listbox-option-selected' : ''}
|
|
386
|
+
"
|
|
387
|
+
role="option"
|
|
388
|
+
aria-selected={isSelected ? 'true' : 'false'}
|
|
389
|
+
data-index={index}
|
|
390
|
+
onclick={() => selectOption(option)}
|
|
391
|
+
onmouseenter={() => highlightedIndex = index}
|
|
392
|
+
>
|
|
393
|
+
<div class="listbox-option-content">
|
|
394
|
+
{#if multiple && showCheckbox}
|
|
395
|
+
<div class="listbox-checkbox">
|
|
396
|
+
{#if isSelected}
|
|
397
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
398
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
|
399
|
+
</svg>
|
|
400
|
+
{/if}
|
|
401
|
+
</div>
|
|
402
|
+
{/if}
|
|
403
|
+
|
|
404
|
+
{#if optionIcon && getOptionIcon(option)}
|
|
405
|
+
<div class="listbox-option-icon">
|
|
406
|
+
{@html getOptionIcon(option)}
|
|
407
|
+
</div>
|
|
408
|
+
{/if}
|
|
409
|
+
|
|
410
|
+
<div class="listbox-option-label">
|
|
411
|
+
{getOptionLabel(option)}
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
</li>
|
|
415
|
+
{/each}
|
|
416
|
+
</ul>
|
|
417
|
+
{:else}
|
|
418
|
+
<div class="listbox-empty">No options available</div>
|
|
419
|
+
{/if}
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
<!-- Hidden input for form submission -->
|
|
423
|
+
<input
|
|
424
|
+
type="hidden"
|
|
425
|
+
{id}
|
|
426
|
+
{name}
|
|
427
|
+
value={JSON.stringify(selectedValues)}
|
|
428
|
+
{required}
|
|
429
|
+
{disabled}
|
|
430
|
+
/>
|
|
431
|
+
</div>
|
|
432
|
+
|
|
433
|
+
<style>
|
|
434
|
+
@reference "../../twintrinsic.css";
|
|
435
|
+
|
|
436
|
+
.listbox-container {
|
|
437
|
+
@apply w-full;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.listbox-filter {
|
|
441
|
+
@apply mb-2;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.listbox-filter-input {
|
|
445
|
+
@apply w-full px-3 py-2;
|
|
446
|
+
@apply bg-background dark:bg-background;
|
|
447
|
+
@apply border border-border dark:border-border rounded-md;
|
|
448
|
+
@apply text-text dark:text-text placeholder:text-muted dark:placeholder:text-muted;
|
|
449
|
+
@apply focus:outline-none focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400 focus:border-primary-500 dark:focus:border-primary-400;
|
|
450
|
+
@apply disabled:opacity-50 disabled:cursor-not-allowed;
|
|
451
|
+
@apply transition-colors duration-200;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.listbox {
|
|
455
|
+
@apply w-full overflow-y-auto;
|
|
456
|
+
@apply bg-background dark:bg-background;
|
|
457
|
+
@apply border border-border dark:border-border rounded-md;
|
|
458
|
+
@apply focus:outline-none focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400;
|
|
459
|
+
@apply transition-colors duration-200;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.listbox[aria-disabled="true"] {
|
|
463
|
+
@apply opacity-50 cursor-not-allowed;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.listbox-options {
|
|
467
|
+
@apply py-1;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.listbox-option {
|
|
471
|
+
@apply px-3 py-2 cursor-pointer;
|
|
472
|
+
@apply text-text dark:text-text;
|
|
473
|
+
@apply hover:bg-hover dark:hover:bg-hover;
|
|
474
|
+
@apply transition-colors duration-150;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.listbox-option-content {
|
|
478
|
+
@apply flex items-center gap-2;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.listbox-option-highlighted {
|
|
482
|
+
@apply bg-hover dark:bg-hover;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.listbox-option-selected {
|
|
486
|
+
@apply bg-primary-50 dark:bg-primary-900/20;
|
|
487
|
+
@apply text-primary-700 dark:text-primary-300;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.listbox-checkbox {
|
|
491
|
+
@apply w-4 h-4 flex-shrink-0;
|
|
492
|
+
@apply border border-border dark:border-border rounded;
|
|
493
|
+
@apply flex items-center justify-center;
|
|
494
|
+
@apply bg-background dark:bg-background;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.listbox-option-selected .listbox-checkbox {
|
|
498
|
+
@apply bg-primary-500 dark:bg-primary-400 border-primary-500 dark:border-primary-400;
|
|
499
|
+
@apply text-white dark:text-white;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.listbox-option-icon {
|
|
503
|
+
@apply flex-shrink-0 w-5 h-5;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.listbox-option-label {
|
|
507
|
+
@apply flex-grow truncate;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.listbox-empty {
|
|
511
|
+
@apply px-3 py-4 text-muted dark:text-muted text-center;
|
|
512
|
+
}
|
|
513
|
+
</style>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export default Listbox;
|
|
2
|
+
type Listbox = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Listbox - A component for selecting one or multiple options from a list.
|
|
8
|
+
* Provides accessible keyboard navigation, filtering, and customization options.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* ```svelte
|
|
12
|
+
* <Listbox
|
|
13
|
+
* name="users"
|
|
14
|
+
* options={users}
|
|
15
|
+
* optionLabel="name"
|
|
16
|
+
* optionValue="id"
|
|
17
|
+
* />
|
|
18
|
+
*
|
|
19
|
+
* <Listbox
|
|
20
|
+
* name="colors"
|
|
21
|
+
* options={colors}
|
|
22
|
+
* multiple
|
|
23
|
+
* filter
|
|
24
|
+
* />
|
|
25
|
+
*
|
|
26
|
+
* <FormField label="Select a category">
|
|
27
|
+
* <Listbox
|
|
28
|
+
* name="category"
|
|
29
|
+
* options={categories}
|
|
30
|
+
* optionIcon="icon"
|
|
31
|
+
* />
|
|
32
|
+
* </FormField>
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
declare const Listbox: import("svelte").Component<{
|
|
36
|
+
class?: string;
|
|
37
|
+
id?: any;
|
|
38
|
+
name: any;
|
|
39
|
+
options?: any[];
|
|
40
|
+
value: any;
|
|
41
|
+
multiple?: boolean;
|
|
42
|
+
optionLabel?: string;
|
|
43
|
+
optionValue?: string;
|
|
44
|
+
optionIcon: any;
|
|
45
|
+
disabled?: boolean;
|
|
46
|
+
required?: boolean;
|
|
47
|
+
filter?: boolean;
|
|
48
|
+
filterPlaceholder?: string;
|
|
49
|
+
maxHeight?: number;
|
|
50
|
+
showCheckbox?: boolean;
|
|
51
|
+
ariaLabel: any;
|
|
52
|
+
onchange: any;
|
|
53
|
+
onfilter: any;
|
|
54
|
+
}, {}, "">;
|
|
55
|
+
type $$ComponentProps = {
|
|
56
|
+
class?: string;
|
|
57
|
+
id?: any;
|
|
58
|
+
name: any;
|
|
59
|
+
options?: any[];
|
|
60
|
+
value: any;
|
|
61
|
+
multiple?: boolean;
|
|
62
|
+
optionLabel?: string;
|
|
63
|
+
optionValue?: string;
|
|
64
|
+
optionIcon: any;
|
|
65
|
+
disabled?: boolean;
|
|
66
|
+
required?: boolean;
|
|
67
|
+
filter?: boolean;
|
|
68
|
+
filterPlaceholder?: string;
|
|
69
|
+
maxHeight?: number;
|
|
70
|
+
showCheckbox?: boolean;
|
|
71
|
+
ariaLabel: any;
|
|
72
|
+
onchange: any;
|
|
73
|
+
onfilter: any;
|
|
74
|
+
};
|