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,469 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
ListInput - A component for handling multiple input values displayed as "chips".
|
|
4
|
+
Allows adding, removing, and editing multiple values with keyboard navigation.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
```svelte
|
|
8
|
+
<ListInput
|
|
9
|
+
name="tags"
|
|
10
|
+
placeholder="Add tag..."
|
|
11
|
+
values={['react', 'svelte']}
|
|
12
|
+
onchange={handleTagsChange}
|
|
13
|
+
/>
|
|
14
|
+
|
|
15
|
+
<FormField label="Recipients">
|
|
16
|
+
<ListInput
|
|
17
|
+
name="recipients"
|
|
18
|
+
placeholder="Add email..."
|
|
19
|
+
validator={(value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)}
|
|
20
|
+
errorMessage="Please enter a valid email address"
|
|
21
|
+
/>
|
|
22
|
+
</FormField>
|
|
23
|
+
```
|
|
24
|
+
-->
|
|
25
|
+
<script>
|
|
26
|
+
import { getContext } from "svelte"
|
|
27
|
+
|
|
28
|
+
const {
|
|
29
|
+
/** @type {string} - Additional CSS classes */
|
|
30
|
+
class: className = "",
|
|
31
|
+
|
|
32
|
+
/** @type {string} - HTML id for accessibility */
|
|
33
|
+
id = crypto.randomUUID(),
|
|
34
|
+
|
|
35
|
+
/** @type {string} - Input name */
|
|
36
|
+
name,
|
|
37
|
+
|
|
38
|
+
/** @type {string} - Placeholder text */
|
|
39
|
+
placeholder = "Add item...",
|
|
40
|
+
|
|
41
|
+
/** @type {string[]} - Array of values */
|
|
42
|
+
values = [],
|
|
43
|
+
|
|
44
|
+
/** @type {boolean} - Whether the input is disabled */
|
|
45
|
+
disabled = false,
|
|
46
|
+
|
|
47
|
+
/** @type {boolean} - Whether the input is required */
|
|
48
|
+
required = false,
|
|
49
|
+
|
|
50
|
+
/** @type {boolean} - Whether to allow duplicates */
|
|
51
|
+
allowDuplicates = false,
|
|
52
|
+
|
|
53
|
+
/** @type {string} - Character(s) that trigger adding a new item */
|
|
54
|
+
addOnKeys = "Enter,Tab,Comma",
|
|
55
|
+
|
|
56
|
+
/** @type {string} - Separator for pasting multiple values */
|
|
57
|
+
pasteSeparator = ",",
|
|
58
|
+
|
|
59
|
+
/** @type {number} - Maximum number of items */
|
|
60
|
+
maxItems,
|
|
61
|
+
|
|
62
|
+
/** @type {Function} - Validator function for new values */
|
|
63
|
+
validator,
|
|
64
|
+
|
|
65
|
+
/** @type {string} - Error message for invalid values */
|
|
66
|
+
errorMessage = "Invalid value",
|
|
67
|
+
|
|
68
|
+
/** @type {string} - ARIA label for the input */
|
|
69
|
+
ariaLabel,
|
|
70
|
+
|
|
71
|
+
/** @type {(event: CustomEvent) => void} - Change event handler */
|
|
72
|
+
onchange,
|
|
73
|
+
/** @type {(event: CustomEvent) => void} - Add event handler */
|
|
74
|
+
onadd,
|
|
75
|
+
/** @type {(event: CustomEvent) => void} - Remove event handler */
|
|
76
|
+
onremove,
|
|
77
|
+
/** @type {(event: CustomEvent) => void} - Focus event handler */
|
|
78
|
+
onfocus,
|
|
79
|
+
/** @type {(event: CustomEvent) => void} - Blur event handler */
|
|
80
|
+
onblur,
|
|
81
|
+
} = $props()
|
|
82
|
+
|
|
83
|
+
// Get form context if available
|
|
84
|
+
const formContext = getContext("form")
|
|
85
|
+
|
|
86
|
+
// Input state
|
|
87
|
+
let inputValue = $state("")
|
|
88
|
+
let itemValues = $state([...values])
|
|
89
|
+
let focusedIndex = $state(-1)
|
|
90
|
+
let inputEl
|
|
91
|
+
let isInvalid = $state(false)
|
|
92
|
+
let validationMessage = $state("")
|
|
93
|
+
|
|
94
|
+
// Register with form if available
|
|
95
|
+
let fieldApi = $state()
|
|
96
|
+
|
|
97
|
+
$effect(() => {
|
|
98
|
+
if (formContext && name) {
|
|
99
|
+
fieldApi = formContext.registerField(name, values)
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// Update values when form field changes
|
|
104
|
+
$effect(() => {
|
|
105
|
+
if (fieldApi) {
|
|
106
|
+
const formValue = fieldApi.getValue()
|
|
107
|
+
if (formValue !== undefined && JSON.stringify(formValue) !== JSON.stringify(itemValues)) {
|
|
108
|
+
itemValues = [...formValue]
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// Update internal values when prop changes
|
|
114
|
+
$effect(() => {
|
|
115
|
+
if (JSON.stringify(values) !== JSON.stringify(itemValues)) {
|
|
116
|
+
itemValues = [...values]
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Validates a value
|
|
122
|
+
* @param {string} value - Value to validate
|
|
123
|
+
* @returns {boolean} - Whether the value is valid
|
|
124
|
+
*/
|
|
125
|
+
function validateValue(value) {
|
|
126
|
+
if (!value.trim()) return false
|
|
127
|
+
|
|
128
|
+
if (!allowDuplicates && itemValues.includes(value.trim())) {
|
|
129
|
+
validationMessage = "Duplicate value"
|
|
130
|
+
return false
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (maxItems && itemValues.length >= maxItems) {
|
|
134
|
+
validationMessage = `Maximum of ${maxItems} items allowed`
|
|
135
|
+
return false
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (validator) {
|
|
139
|
+
const isValid = validator(value.trim())
|
|
140
|
+
if (!isValid) {
|
|
141
|
+
validationMessage = errorMessage
|
|
142
|
+
}
|
|
143
|
+
return isValid
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return true
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Adds a new item
|
|
151
|
+
* @param {string} value - Value to add
|
|
152
|
+
*/
|
|
153
|
+
function addItem(value) {
|
|
154
|
+
const trimmedValue = value.trim()
|
|
155
|
+
|
|
156
|
+
if (!validateValue(trimmedValue)) {
|
|
157
|
+
isInvalid = true
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
isInvalid = false
|
|
162
|
+
validationMessage = ""
|
|
163
|
+
|
|
164
|
+
// Add the item
|
|
165
|
+
itemValues = [...itemValues, trimmedValue]
|
|
166
|
+
inputValue = ""
|
|
167
|
+
|
|
168
|
+
// Update form field if available
|
|
169
|
+
if (fieldApi) {
|
|
170
|
+
fieldApi.setValue(itemValues)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
onchange?.(new CustomEvent("change", { detail: { values: itemValues } }))
|
|
174
|
+
onadd?.(new CustomEvent("add", { detail: { value: trimmedValue } }))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Removes an item
|
|
179
|
+
* @param {number} index - Index of the item to remove
|
|
180
|
+
*/
|
|
181
|
+
function removeItem(index) {
|
|
182
|
+
if (index < 0 || index >= itemValues.length) return
|
|
183
|
+
|
|
184
|
+
const removedValue = itemValues[index]
|
|
185
|
+
itemValues = itemValues.filter((_, i) => i !== index)
|
|
186
|
+
|
|
187
|
+
// Update form field if available
|
|
188
|
+
if (fieldApi) {
|
|
189
|
+
fieldApi.setValue(itemValues)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Update focused index
|
|
193
|
+
if (focusedIndex >= itemValues.length) {
|
|
194
|
+
focusedIndex = itemValues.length - 1
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
onchange?.(new CustomEvent("change", { detail: { values: itemValues } }))
|
|
198
|
+
onremove?.(new CustomEvent("remove", { detail: { value: removedValue, index } }))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Handles input keydown
|
|
203
|
+
* @param {KeyboardEvent} event - Keydown event
|
|
204
|
+
*/
|
|
205
|
+
function handleKeydown(event) {
|
|
206
|
+
// Get add trigger keys
|
|
207
|
+
const triggerKeys = addOnKeys.split(",").map((k) => k.trim().toLowerCase())
|
|
208
|
+
|
|
209
|
+
// Check if key should trigger adding an item
|
|
210
|
+
const shouldAddItem =
|
|
211
|
+
(triggerKeys.includes("enter") && event.key === "Enter") ||
|
|
212
|
+
(triggerKeys.includes("tab") && event.key === "Tab") ||
|
|
213
|
+
(triggerKeys.includes("comma") && event.key === ",") ||
|
|
214
|
+
(triggerKeys.includes("space") && event.key === " ")
|
|
215
|
+
|
|
216
|
+
if (shouldAddItem && inputValue.trim()) {
|
|
217
|
+
event.preventDefault()
|
|
218
|
+
addItem(inputValue)
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Handle backspace to remove last item
|
|
223
|
+
if (event.key === "Backspace" && !inputValue && itemValues.length > 0) {
|
|
224
|
+
if (focusedIndex === -1) {
|
|
225
|
+
// Focus the last item first
|
|
226
|
+
focusedIndex = itemValues.length - 1
|
|
227
|
+
} else {
|
|
228
|
+
// Remove the focused item
|
|
229
|
+
removeItem(focusedIndex)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Handle arrow keys for navigation
|
|
234
|
+
if (event.key === "ArrowLeft" && focusedIndex === -1 && !inputValue && itemValues.length > 0) {
|
|
235
|
+
focusedIndex = itemValues.length - 1
|
|
236
|
+
} else if (event.key === "ArrowLeft" && focusedIndex > 0) {
|
|
237
|
+
focusedIndex--
|
|
238
|
+
} else if (event.key === "ArrowRight" && focusedIndex < itemValues.length - 1) {
|
|
239
|
+
focusedIndex++
|
|
240
|
+
} else if (event.key === "ArrowRight" && focusedIndex === itemValues.length - 1) {
|
|
241
|
+
focusedIndex = -1
|
|
242
|
+
inputEl?.focus()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Handle Escape to blur
|
|
246
|
+
if (event.key === "Escape") {
|
|
247
|
+
inputEl?.blur()
|
|
248
|
+
focusedIndex = -1
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Handles input change
|
|
254
|
+
* @param {Event} event - Input event
|
|
255
|
+
*/
|
|
256
|
+
function handleInput(event) {
|
|
257
|
+
inputValue = event.target.value
|
|
258
|
+
|
|
259
|
+
// Clear validation state when typing
|
|
260
|
+
if (isInvalid) {
|
|
261
|
+
isInvalid = false
|
|
262
|
+
validationMessage = ""
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// If the last character is a comma, add the item
|
|
266
|
+
if (inputValue.endsWith(",") && addOnKeys.includes("Comma")) {
|
|
267
|
+
const valueToAdd = inputValue.slice(0, -1)
|
|
268
|
+
if (valueToAdd.trim()) {
|
|
269
|
+
addItem(valueToAdd)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
dispatch("input", { value: inputValue })
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Handles paste event
|
|
278
|
+
* @param {ClipboardEvent} event - Paste event
|
|
279
|
+
*/
|
|
280
|
+
function handlePaste(event) {
|
|
281
|
+
if (!pasteSeparator) return
|
|
282
|
+
|
|
283
|
+
const pastedText = event.clipboardData?.getData("text") || ""
|
|
284
|
+
if (!pastedText.includes(pasteSeparator)) return
|
|
285
|
+
|
|
286
|
+
// Prevent default paste
|
|
287
|
+
event.preventDefault()
|
|
288
|
+
|
|
289
|
+
// Split pasted text and add items
|
|
290
|
+
const pastedValues = pastedText
|
|
291
|
+
.split(pasteSeparator)
|
|
292
|
+
.map((v) => v.trim())
|
|
293
|
+
.filter((v) => v)
|
|
294
|
+
|
|
295
|
+
for (const value of pastedValues) {
|
|
296
|
+
addItem(value)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Handles chip click
|
|
302
|
+
* @param {number} index - Chip index
|
|
303
|
+
*/
|
|
304
|
+
function handleChipClick(index) {
|
|
305
|
+
focusedIndex = index
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Handles input focus
|
|
310
|
+
*/
|
|
311
|
+
function handleFocus() {
|
|
312
|
+
focusedIndex = -1
|
|
313
|
+
dispatch("focus")
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Handles input blur
|
|
318
|
+
*/
|
|
319
|
+
function handleBlur(event) {
|
|
320
|
+
// Add current value if not empty and not clicking on a chip
|
|
321
|
+
if (inputValue.trim() && !event.relatedTarget?.classList.contains("list-input-chip")) {
|
|
322
|
+
addItem(inputValue)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Reset focused index after a short delay
|
|
326
|
+
// This allows chip click events to fire first
|
|
327
|
+
setTimeout(() => {
|
|
328
|
+
focusedIndex = -1
|
|
329
|
+
}, 100)
|
|
330
|
+
|
|
331
|
+
dispatch("blur")
|
|
332
|
+
}
|
|
333
|
+
</script>
|
|
334
|
+
|
|
335
|
+
<div
|
|
336
|
+
class="
|
|
337
|
+
list-input
|
|
338
|
+
{isInvalid ? 'list-input-invalid' : ''}
|
|
339
|
+
{disabled ? 'list-input-disabled' : ''}
|
|
340
|
+
{className}
|
|
341
|
+
"
|
|
342
|
+
>
|
|
343
|
+
<div
|
|
344
|
+
class="list-input-container"
|
|
345
|
+
onclick={() => inputEl?.focus()}
|
|
346
|
+
>
|
|
347
|
+
<!-- Chips -->
|
|
348
|
+
{#each itemValues as value, index}
|
|
349
|
+
<div
|
|
350
|
+
class="list-input-chip {focusedIndex === index ? 'list-input-chip-focused' : ''}"
|
|
351
|
+
tabindex="0"
|
|
352
|
+
onclick={() => handleChipClick(index)}
|
|
353
|
+
onkeydown={(e) => {
|
|
354
|
+
if (e.key === 'Backspace' || e.key === 'Delete') {
|
|
355
|
+
removeItem(index);
|
|
356
|
+
inputEl?.focus();
|
|
357
|
+
}
|
|
358
|
+
}}
|
|
359
|
+
>
|
|
360
|
+
<span class="list-input-chip-text">{value}</span>
|
|
361
|
+
<button
|
|
362
|
+
type="button"
|
|
363
|
+
class="list-input-chip-remove"
|
|
364
|
+
aria-label={`Remove ${value}`}
|
|
365
|
+
tabindex="-1"
|
|
366
|
+
onclick={(e) => {
|
|
367
|
+
e.stopPropagation();
|
|
368
|
+
e.preventDefault();
|
|
369
|
+
removeItem(index);
|
|
370
|
+
inputEl?.focus();
|
|
371
|
+
}}
|
|
372
|
+
>
|
|
373
|
+
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
374
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
375
|
+
</svg>
|
|
376
|
+
</button>
|
|
377
|
+
</div>
|
|
378
|
+
{/each}
|
|
379
|
+
|
|
380
|
+
<!-- Input -->
|
|
381
|
+
<input
|
|
382
|
+
{id}
|
|
383
|
+
{name}
|
|
384
|
+
type="text"
|
|
385
|
+
class="list-input-field"
|
|
386
|
+
{placeholder}
|
|
387
|
+
value={inputValue}
|
|
388
|
+
disabled={disabled || (maxItems && itemValues.length >= maxItems)}
|
|
389
|
+
aria-label={ariaLabel || name}
|
|
390
|
+
aria-invalid={isInvalid ? 'true' : undefined}
|
|
391
|
+
bind:this={inputEl}
|
|
392
|
+
oninput={handleInput}
|
|
393
|
+
onkeydown={handleKeydown}
|
|
394
|
+
onfocus={handleFocus}
|
|
395
|
+
onblur={handleBlur}
|
|
396
|
+
onpaste={handlePaste}
|
|
397
|
+
/>
|
|
398
|
+
</div>
|
|
399
|
+
|
|
400
|
+
{#if isInvalid && validationMessage}
|
|
401
|
+
<div class="list-input-error" role="alert">
|
|
402
|
+
{validationMessage}
|
|
403
|
+
</div>
|
|
404
|
+
{/if}
|
|
405
|
+
|
|
406
|
+
<!-- Hidden input for form submission -->
|
|
407
|
+
<input
|
|
408
|
+
type="hidden"
|
|
409
|
+
{name}
|
|
410
|
+
value={JSON.stringify(itemValues)}
|
|
411
|
+
{required}
|
|
412
|
+
{disabled}
|
|
413
|
+
/>
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
<style>
|
|
417
|
+
@reference "../../twintrinsic.css";
|
|
418
|
+
|
|
419
|
+
.list-input {
|
|
420
|
+
@apply w-full;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.list-input-container {
|
|
424
|
+
@apply flex flex-wrap items-center gap-2 p-2;
|
|
425
|
+
@apply bg-background dark:bg-background;
|
|
426
|
+
@apply border border-border dark:border-border rounded-md;
|
|
427
|
+
@apply focus-within:ring-2 focus-within:ring-primary-500 dark:focus-within:ring-primary-400 focus-within:border-primary-500 dark:focus-within:border-primary-400;
|
|
428
|
+
@apply transition-colors duration-200;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.list-input-field {
|
|
432
|
+
@apply flex-grow min-w-[80px] bg-transparent border-none outline-none p-1;
|
|
433
|
+
@apply text-text dark:text-text placeholder:text-muted dark:placeholder:text-muted;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.list-input-chip {
|
|
437
|
+
@apply flex items-center gap-1 px-2 py-1 rounded-md;
|
|
438
|
+
@apply bg-surface dark:bg-surface text-text dark:text-text;
|
|
439
|
+
@apply border border-border dark:border-border;
|
|
440
|
+
@apply focus:outline-none focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.list-input-chip-focused {
|
|
444
|
+
@apply bg-primary-100 dark:bg-primary-900 border-primary-300 dark:border-primary-700;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.list-input-chip-text {
|
|
448
|
+
@apply text-sm;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.list-input-chip-remove {
|
|
452
|
+
@apply p-0.5 rounded-full;
|
|
453
|
+
@apply text-muted dark:text-muted;
|
|
454
|
+
@apply hover:bg-hover dark:hover:bg-hover hover:text-text dark:hover:text-text;
|
|
455
|
+
@apply focus:outline-none focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.list-input-error {
|
|
459
|
+
@apply mt-1 text-xs text-error-600 dark:text-error-400;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.list-input-invalid .list-input-container {
|
|
463
|
+
@apply border-error-500 dark:border-error-400 focus-within:ring-error-500 dark:focus-within:ring-error-400 focus-within:border-error-500 dark:focus-within:border-error-400;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.list-input-disabled {
|
|
467
|
+
@apply opacity-60 pointer-events-none;
|
|
468
|
+
}
|
|
469
|
+
</style>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export default ListInput;
|
|
2
|
+
type ListInput = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* ListInput - A component for handling multiple input values displayed as "chips".
|
|
8
|
+
* Allows adding, removing, and editing multiple values with keyboard navigation.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* ```svelte
|
|
12
|
+
* <ListInput
|
|
13
|
+
* name="tags"
|
|
14
|
+
* placeholder="Add tag..."
|
|
15
|
+
* values={['react', 'svelte']}
|
|
16
|
+
* onchange={handleTagsChange}
|
|
17
|
+
* />
|
|
18
|
+
*
|
|
19
|
+
* <FormField label="Recipients">
|
|
20
|
+
* <ListInput
|
|
21
|
+
* name="recipients"
|
|
22
|
+
* placeholder="Add email..."
|
|
23
|
+
* validator={(value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)}
|
|
24
|
+
* errorMessage="Please enter a valid email address"
|
|
25
|
+
* />
|
|
26
|
+
* </FormField>
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
declare const ListInput: import("svelte").Component<{
|
|
30
|
+
class?: string;
|
|
31
|
+
id?: any;
|
|
32
|
+
name: any;
|
|
33
|
+
placeholder?: string;
|
|
34
|
+
values?: any[];
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
required?: boolean;
|
|
37
|
+
allowDuplicates?: boolean;
|
|
38
|
+
addOnKeys?: string;
|
|
39
|
+
pasteSeparator?: string;
|
|
40
|
+
maxItems: any;
|
|
41
|
+
validator: any;
|
|
42
|
+
errorMessage?: string;
|
|
43
|
+
ariaLabel: any;
|
|
44
|
+
onchange: any;
|
|
45
|
+
onadd: any;
|
|
46
|
+
onremove: any;
|
|
47
|
+
onfocus: any;
|
|
48
|
+
onblur: any;
|
|
49
|
+
}, {}, "">;
|
|
50
|
+
type $$ComponentProps = {
|
|
51
|
+
class?: string;
|
|
52
|
+
id?: any;
|
|
53
|
+
name: any;
|
|
54
|
+
placeholder?: string;
|
|
55
|
+
values?: any[];
|
|
56
|
+
disabled?: boolean;
|
|
57
|
+
required?: boolean;
|
|
58
|
+
allowDuplicates?: boolean;
|
|
59
|
+
addOnKeys?: string;
|
|
60
|
+
pasteSeparator?: string;
|
|
61
|
+
maxItems: any;
|
|
62
|
+
validator: any;
|
|
63
|
+
errorMessage?: string;
|
|
64
|
+
ariaLabel: any;
|
|
65
|
+
onchange: any;
|
|
66
|
+
onadd: any;
|
|
67
|
+
onremove: any;
|
|
68
|
+
onfocus: any;
|
|
69
|
+
onblur: any;
|
|
70
|
+
};
|