@placeholderco/placeholder-ui 1.0.3 → 1.0.6
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 +26 -26
- package/README.md +179 -179
- package/dist/display/Alert.svelte +179 -179
- package/dist/display/Avatar.svelte +166 -166
- package/dist/display/LinkCollection.svelte +161 -161
- package/dist/display/Paper.svelte +118 -118
- package/dist/form/Autocomplete.svelte +223 -191
- package/dist/form/Autocomplete.svelte.d.ts +3 -1
- package/dist/form/AutocompleteMulti.svelte +356 -0
- package/dist/form/AutocompleteMulti.svelte.d.ts +28 -0
- package/dist/form/Checkbox.svelte +201 -201
- package/dist/form/Chips.svelte +128 -128
- package/dist/form/ComboBox.svelte +158 -158
- package/dist/form/ComboBox.svelte.d.ts +1 -1
- package/dist/form/ComboBoxItemBuilder.svelte +460 -460
- package/dist/form/ComboBoxMulti.svelte +197 -197
- package/dist/form/ComboBoxMulti.svelte.d.ts +1 -1
- package/dist/form/CronBuilder.svelte +693 -693
- package/dist/form/DatePicker.svelte +672 -672
- package/dist/form/DateTimePicker.svelte +712 -712
- package/dist/form/FileInput.svelte +235 -235
- package/dist/form/FormGroup.svelte +68 -68
- package/dist/form/Number.svelte +238 -238
- package/dist/form/PasswordInput.svelte +252 -252
- package/dist/form/RadioGroup.svelte +210 -210
- package/dist/form/Rating.svelte +235 -235
- package/dist/form/SegmentedControl.svelte +149 -149
- package/dist/form/Select.svelte +590 -590
- package/dist/form/Select.svelte.d.ts +1 -1
- package/dist/form/SelectMulti.svelte +613 -613
- package/dist/form/SelectMulti.svelte.d.ts +1 -1
- package/dist/form/Slider.svelte +358 -358
- package/dist/form/Switch.svelte +147 -147
- package/dist/form/TextArea.svelte +148 -148
- package/dist/form/Textbox.svelte +228 -228
- package/dist/form/TimePicker.svelte +267 -267
- package/dist/icon/Icon.svelte +52 -52
- package/dist/icon/alert-octagon.svg +5 -5
- package/dist/icon/alert-triangle.svg +5 -5
- package/dist/icon/archive.svg +1 -1
- package/dist/icon/arrow-down.svg +1 -1
- package/dist/icon/arrow-left.svg +1 -1
- package/dist/icon/arrow-right.svg +1 -1
- package/dist/icon/arrow-up.svg +1 -1
- package/dist/icon/at.svg +1 -1
- package/dist/icon/bell.svg +1 -1
- package/dist/icon/bookmark.svg +1 -1
- package/dist/icon/calendar.svg +1 -1
- package/dist/icon/camera.svg +1 -1
- package/dist/icon/chart-bar.svg +1 -1
- package/dist/icon/chart-line.svg +1 -1
- package/dist/icon/chart-pie.svg +1 -1
- package/dist/icon/checkbox.svg +1 -1
- package/dist/icon/checklist.svg +1 -1
- package/dist/icon/circle-check.svg +1 -1
- package/dist/icon/circle-x.svg +1 -1
- package/dist/icon/clock.svg +1 -1
- package/dist/icon/credit-card.svg +1 -1
- package/dist/icon/dots-vertical.svg +1 -1
- package/dist/icon/dots.svg +1 -1
- package/dist/icon/external-link.svg +1 -1
- package/dist/icon/eye-off.svg +1 -1
- package/dist/icon/eye.svg +1 -1
- package/dist/icon/filter.svg +1 -1
- package/dist/icon/fingerprint.svg +1 -1
- package/dist/icon/flag.svg +1 -1
- package/dist/icon/heart.svg +1 -1
- package/dist/icon/home.svg +1 -1
- package/dist/icon/key.svg +1 -1
- package/dist/icon/list-check.svg +1 -1
- package/dist/icon/login.svg +1 -1
- package/dist/icon/logout.svg +1 -1
- package/dist/icon/map-pin.svg +1 -1
- package/dist/icon/maximize.svg +1 -1
- package/dist/icon/microphone.svg +1 -1
- package/dist/icon/minimize.svg +1 -1
- package/dist/icon/note.svg +1 -1
- package/dist/icon/player-pause.svg +1 -1
- package/dist/icon/printer.svg +1 -1
- package/dist/icon/qrcode.svg +1 -1
- package/dist/icon/send.svg +1 -1
- package/dist/icon/settings.svg +1 -1
- package/dist/icon/share.svg +1 -1
- package/dist/icon/shopping-cart.svg +1 -1
- package/dist/icon/sort-ascending.svg +1 -1
- package/dist/icon/sort-descending.svg +1 -1
- package/dist/icon/star.svg +1 -1
- package/dist/icon/tag.svg +1 -1
- package/dist/icon/trending-down.svg +1 -1
- package/dist/icon/trending-up.svg +1 -1
- package/dist/icon/upload.svg +1 -1
- package/dist/icon/volume-off.svg +1 -1
- package/dist/icon/volume.svg +1 -1
- package/dist/icon/world.svg +1 -1
- package/dist/icon/zoom-in.svg +1 -1
- package/dist/icon/zoom-out.svg +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/layout/AppShell.svelte +169 -169
- package/dist/layout/CustomNavbar.svelte +61 -61
- package/dist/layout/Navbar.svelte +206 -206
- package/dist/layout/NavbarItemDisplay.svelte +29 -29
- package/dist/layout/Sidenav.svelte +712 -712
- package/dist/styles/components.css +199 -199
- package/dist/styles/dark.css +146 -146
- package/dist/styles/index.css +116 -116
- package/dist/styles/reset.css +110 -110
- package/dist/styles/semantic.css +86 -86
- package/dist/styles/tokens.css +203 -197
- package/dist/styles/utilities.css +523 -523
- package/dist/ui/Accordion.svelte +289 -289
- package/dist/ui/ActionIcon.svelte +76 -76
- package/dist/ui/Badge.svelte +329 -279
- package/dist/ui/Breadcrumbs.svelte +131 -131
- package/dist/ui/Button.svelte +432 -370
- package/dist/ui/ButtonVariant.d.ts +1 -1
- package/dist/ui/Dialog.svelte +307 -307
- package/dist/ui/Drawer.svelte +524 -524
- package/dist/ui/Dropdown.svelte +97 -97
- package/dist/ui/Dropzone.svelte +122 -122
- package/dist/ui/Link.svelte +32 -32
- package/dist/ui/Loader.svelte +70 -70
- package/dist/ui/LoadingOverlay.svelte +53 -53
- package/dist/ui/Pagination.svelte +135 -135
- package/dist/ui/Popover.svelte +225 -225
- package/dist/ui/Progress.svelte +191 -191
- package/dist/ui/RingProgress.svelte +141 -141
- package/dist/ui/Skeleton.svelte +85 -85
- package/dist/ui/Stepper.svelte +355 -355
- package/dist/ui/Table.svelte +345 -345
- package/dist/ui/Tabs.svelte +146 -146
- package/dist/ui/ThemeSwitcher.svelte +39 -39
- package/dist/ui/Timeline.svelte +225 -225
- package/dist/ui/Toaster.svelte +6 -6
- package/dist/ui/Tooltip.svelte +434 -434
- package/package.json +14 -14
|
@@ -1,460 +1,460 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { ComboBoxItem } from '../models/ComboBoxItem.js';
|
|
3
|
-
import ActionIcon from '../ui/ActionIcon.svelte';
|
|
4
|
-
import Button from '../ui/Button.svelte';
|
|
5
|
-
import Checkbox from './Checkbox.svelte';
|
|
6
|
-
import { iconPlus, iconTrash } from '../icon/index.js';
|
|
7
|
-
import FormGroup from './FormGroup.svelte';
|
|
8
|
-
|
|
9
|
-
interface Props {
|
|
10
|
-
label?: string;
|
|
11
|
-
items?: ComboBoxItem[];
|
|
12
|
-
defaultValues?: string[];
|
|
13
|
-
labelPlaceholder?: string;
|
|
14
|
-
valuePlaceholder?: string;
|
|
15
|
-
class?: string;
|
|
16
|
-
onchange?: (items: ComboBoxItem[], defaultValues: string[]) => void;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
let {
|
|
20
|
-
label = '',
|
|
21
|
-
items = $bindable([]),
|
|
22
|
-
defaultValues = $bindable([]),
|
|
23
|
-
labelPlaceholder = 'Label',
|
|
24
|
-
valuePlaceholder = 'Value',
|
|
25
|
-
class: classes = '',
|
|
26
|
-
onchange
|
|
27
|
-
}: Props = $props();
|
|
28
|
-
|
|
29
|
-
let newLabel = $state('');
|
|
30
|
-
let newValue = $state('');
|
|
31
|
-
let valueManuallyEdited = $state(false);
|
|
32
|
-
let labelInputEl: HTMLInputElement | undefined = $state(undefined);
|
|
33
|
-
|
|
34
|
-
// Copy label exactly for default value
|
|
35
|
-
function labelToValue(label: string): string {
|
|
36
|
-
return label.trim();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Handle label input - auto-sync to value if not manually edited
|
|
40
|
-
function handleLabelInput(value: string) {
|
|
41
|
-
newLabel = value;
|
|
42
|
-
if (!valueManuallyEdited) {
|
|
43
|
-
newValue = labelToValue(value);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Handle value input - mark as manually edited
|
|
48
|
-
function handleValueInput(value: string) {
|
|
49
|
-
newValue = value;
|
|
50
|
-
if (value !== labelToValue(newLabel)) {
|
|
51
|
-
valueManuallyEdited = true;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Check if a value already exists (excluding a specific index for editing)
|
|
56
|
-
function valueExists(value: string, excludeIndex?: number): boolean {
|
|
57
|
-
return items.some((item, i) =>
|
|
58
|
-
item.value === value && i !== excludeIndex
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Check if a label already exists (excluding a specific index for editing)
|
|
63
|
-
function labelExists(label: string, excludeIndex?: number): boolean {
|
|
64
|
-
return items.some((item, i) =>
|
|
65
|
-
item.label === label && i !== excludeIndex
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Check if new value/label would be a duplicate
|
|
70
|
-
let isDuplicateNewValue = $derived(
|
|
71
|
-
newValue.trim() !== '' && valueExists(newValue.trim())
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
let isDuplicateNewLabel = $derived(
|
|
75
|
-
newLabel.trim() !== '' && labelExists(newLabel.trim())
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
function addItem() {
|
|
79
|
-
if (!newLabel.trim() || !newValue.trim()) return;
|
|
80
|
-
if (isDuplicateNewValue || isDuplicateNewLabel) return;
|
|
81
|
-
|
|
82
|
-
const newItem: ComboBoxItem = {
|
|
83
|
-
label: newLabel.trim(),
|
|
84
|
-
value: newValue.trim()
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
items = [...items, newItem];
|
|
88
|
-
newLabel = '';
|
|
89
|
-
newValue = '';
|
|
90
|
-
valueManuallyEdited = false;
|
|
91
|
-
|
|
92
|
-
notifyChange();
|
|
93
|
-
|
|
94
|
-
// Focus back on label input for quick entry of next item
|
|
95
|
-
labelInputEl?.focus();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function removeItem(index: number) {
|
|
99
|
-
const removedItem = items[index];
|
|
100
|
-
items = items.filter((_, i) => i !== index);
|
|
101
|
-
|
|
102
|
-
// Remove from defaults if it was selected
|
|
103
|
-
if (defaultValues.includes(removedItem.value)) {
|
|
104
|
-
defaultValues = defaultValues.filter(v => v !== removedItem.value);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
notifyChange();
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function toggleDefault(value: string, checked: boolean) {
|
|
111
|
-
if (checked) {
|
|
112
|
-
defaultValues = [...defaultValues, value];
|
|
113
|
-
} else {
|
|
114
|
-
defaultValues = defaultValues.filter(v => v !== value);
|
|
115
|
-
}
|
|
116
|
-
notifyChange();
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function updateItemLabel(index: number, label: string) {
|
|
120
|
-
// Don't allow duplicate labels
|
|
121
|
-
if (labelExists(label, index)) return;
|
|
122
|
-
|
|
123
|
-
const currentItem = items[index];
|
|
124
|
-
|
|
125
|
-
// Check if label and value were in sync (exact case-sensitive match)
|
|
126
|
-
const wasInSync = currentItem.label === currentItem.value;
|
|
127
|
-
|
|
128
|
-
// If they were in sync, also update the value to maintain exact match (if it wouldn't create a duplicate)
|
|
129
|
-
if (wasInSync && !valueExists(label, index)) {
|
|
130
|
-
const oldValue = currentItem.value;
|
|
131
|
-
items = items.map((item, i) =>
|
|
132
|
-
i === index ? { ...item, label, value: label } : item
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
// Update defaults if the value was changed
|
|
136
|
-
if (defaultValues.includes(oldValue)) {
|
|
137
|
-
defaultValues = defaultValues.map(v => v === oldValue ? label : v);
|
|
138
|
-
}
|
|
139
|
-
} else {
|
|
140
|
-
items = items.map((item, i) =>
|
|
141
|
-
i === index ? { ...item, label } : item
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
notifyChange();
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function updateItemValue(index: number, newVal: string) {
|
|
149
|
-
// Don't allow duplicate values
|
|
150
|
-
if (valueExists(newVal, index)) return;
|
|
151
|
-
|
|
152
|
-
const oldValue = items[index].value;
|
|
153
|
-
items = items.map((item, i) =>
|
|
154
|
-
i === index ? { ...item, value: newVal } : item
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
// Update defaults if the value was changed
|
|
158
|
-
if (defaultValues.includes(oldValue)) {
|
|
159
|
-
defaultValues = defaultValues.map(v => v === oldValue ? newVal : v);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
notifyChange();
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function notifyChange() {
|
|
166
|
-
onchange?.(items, defaultValues);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function handleKeyDown(e: KeyboardEvent) {
|
|
170
|
-
if (e.key === 'Enter') {
|
|
171
|
-
e.preventDefault();
|
|
172
|
-
addItem();
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Computed output for display/copying
|
|
177
|
-
let outputItems = $derived(
|
|
178
|
-
items.map(item => ({
|
|
179
|
-
...item,
|
|
180
|
-
selected: defaultValues.includes(item.value)
|
|
181
|
-
}))
|
|
182
|
-
);
|
|
183
|
-
|
|
184
|
-
// Get labels for selected defaults
|
|
185
|
-
let defaultLabels = $derived(
|
|
186
|
-
defaultValues
|
|
187
|
-
.map(v => items.find(i => i.value === v)?.label ?? v)
|
|
188
|
-
.join(', ')
|
|
189
|
-
);
|
|
190
|
-
</script>
|
|
191
|
-
|
|
192
|
-
<div class="combobox-builder {classes}">
|
|
193
|
-
{#if label}
|
|
194
|
-
<FormGroup {label}>
|
|
195
|
-
<div></div>
|
|
196
|
-
</FormGroup>
|
|
197
|
-
{/if}
|
|
198
|
-
|
|
199
|
-
<div class="builder-content">
|
|
200
|
-
<!-- Header row -->
|
|
201
|
-
<div class="header-row">
|
|
202
|
-
<div class="col-default">Selected</div>
|
|
203
|
-
<div class="col-label">Label</div>
|
|
204
|
-
<div class="col-value">Value</div>
|
|
205
|
-
<div class="col-actions"></div>
|
|
206
|
-
</div>
|
|
207
|
-
|
|
208
|
-
<!-- Existing items -->
|
|
209
|
-
{#each items as item, index (index)}
|
|
210
|
-
<div class="item-row">
|
|
211
|
-
<div class="col-default">
|
|
212
|
-
<Checkbox
|
|
213
|
-
checked={defaultValues.includes(item.value)}
|
|
214
|
-
onchange={(checked) => toggleDefault(item.value, checked)}
|
|
215
|
-
/>
|
|
216
|
-
</div>
|
|
217
|
-
<div class="col-label">
|
|
218
|
-
<input
|
|
219
|
-
type="text"
|
|
220
|
-
class="item-input"
|
|
221
|
-
value={item.label}
|
|
222
|
-
oninput={(e) => updateItemLabel(index, e.currentTarget.value)}
|
|
223
|
-
placeholder={labelPlaceholder}
|
|
224
|
-
/>
|
|
225
|
-
</div>
|
|
226
|
-
<div class="col-value">
|
|
227
|
-
<input
|
|
228
|
-
type="text"
|
|
229
|
-
class="item-input"
|
|
230
|
-
value={item.value}
|
|
231
|
-
oninput={(e) => updateItemValue(index, e.currentTarget.value)}
|
|
232
|
-
placeholder={valuePlaceholder}
|
|
233
|
-
/>
|
|
234
|
-
</div>
|
|
235
|
-
<div class="col-actions">
|
|
236
|
-
<ActionIcon
|
|
237
|
-
variant="danger-subtle"
|
|
238
|
-
svg={iconTrash}
|
|
239
|
-
size="1rem"
|
|
240
|
-
onclick={() => removeItem(index)}
|
|
241
|
-
tooltip="Remove item"
|
|
242
|
-
/>
|
|
243
|
-
</div>
|
|
244
|
-
</div>
|
|
245
|
-
{/each}
|
|
246
|
-
|
|
247
|
-
<!-- Add new item row -->
|
|
248
|
-
<div class="add-row">
|
|
249
|
-
<div class="col-default"></div>
|
|
250
|
-
<div class="col-label">
|
|
251
|
-
<input
|
|
252
|
-
bind:this={labelInputEl}
|
|
253
|
-
type="text"
|
|
254
|
-
class="item-input new-input"
|
|
255
|
-
class:error={isDuplicateNewLabel}
|
|
256
|
-
value={newLabel}
|
|
257
|
-
oninput={(e) => handleLabelInput(e.currentTarget.value)}
|
|
258
|
-
placeholder={labelPlaceholder}
|
|
259
|
-
onkeydown={handleKeyDown}
|
|
260
|
-
/>
|
|
261
|
-
</div>
|
|
262
|
-
<div class="col-value">
|
|
263
|
-
<input
|
|
264
|
-
type="text"
|
|
265
|
-
class="item-input new-input"
|
|
266
|
-
class:error={isDuplicateNewValue}
|
|
267
|
-
value={newValue}
|
|
268
|
-
oninput={(e) => handleValueInput(e.currentTarget.value)}
|
|
269
|
-
placeholder={valuePlaceholder}
|
|
270
|
-
onkeydown={handleKeyDown}
|
|
271
|
-
/>
|
|
272
|
-
</div>
|
|
273
|
-
<div class="col-actions">
|
|
274
|
-
<ActionIcon
|
|
275
|
-
variant="auto-subtle"
|
|
276
|
-
svg={iconPlus}
|
|
277
|
-
size="1rem"
|
|
278
|
-
onclick={addItem}
|
|
279
|
-
disabled={!newLabel.trim() || !newValue.trim() || isDuplicateNewValue || isDuplicateNewLabel}
|
|
280
|
-
tooltip="Add item"
|
|
281
|
-
/>
|
|
282
|
-
</div>
|
|
283
|
-
</div>
|
|
284
|
-
|
|
285
|
-
{#if isDuplicateNewLabel || isDuplicateNewValue}
|
|
286
|
-
<div class="error-message">
|
|
287
|
-
{#if isDuplicateNewLabel && isDuplicateNewValue}
|
|
288
|
-
Label "{newLabel}" and value "{newValue}" already exist
|
|
289
|
-
{:else if isDuplicateNewLabel}
|
|
290
|
-
Label "{newLabel}" already exists
|
|
291
|
-
{:else}
|
|
292
|
-
Value "{newValue}" already exists
|
|
293
|
-
{/if}
|
|
294
|
-
</div>
|
|
295
|
-
{/if}
|
|
296
|
-
|
|
297
|
-
<!-- Quick add button for mobile/convenience -->
|
|
298
|
-
{#if newLabel.trim() && newValue.trim() && !isDuplicateNewValue && !isDuplicateNewLabel}
|
|
299
|
-
<div class="quick-add">
|
|
300
|
-
<Button variant="auto-subtle" onclick={addItem}>
|
|
301
|
-
Add "{newLabel}"
|
|
302
|
-
</Button>
|
|
303
|
-
</div>
|
|
304
|
-
{/if}
|
|
305
|
-
</div>
|
|
306
|
-
|
|
307
|
-
<!-- Summary -->
|
|
308
|
-
{#if items.length > 0}
|
|
309
|
-
<div class="summary">
|
|
310
|
-
<span class="summary-count">{items.length} item{items.length !== 1 ? 's' : ''}</span>
|
|
311
|
-
{#if defaultValues.length > 0}
|
|
312
|
-
<span class="summary-default">
|
|
313
|
-
Selected: {defaultLabels}
|
|
314
|
-
</span>
|
|
315
|
-
{/if}
|
|
316
|
-
</div>
|
|
317
|
-
{/if}
|
|
318
|
-
</div>
|
|
319
|
-
|
|
320
|
-
<style>
|
|
321
|
-
.combobox-builder {
|
|
322
|
-
width: 100%;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
.builder-content {
|
|
326
|
-
border: 1px solid var(--pui-border-default);
|
|
327
|
-
border-radius: var(--pui-radius-md);
|
|
328
|
-
overflow: hidden;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
.header-row {
|
|
332
|
-
display: grid;
|
|
333
|
-
grid-template-columns: 3.5rem 1fr 1fr 2.5rem;
|
|
334
|
-
gap: var(--pui-spacing-2);
|
|
335
|
-
padding: var(--pui-spacing-2) var(--pui-spacing-3);
|
|
336
|
-
background-color: var(--pui-input-bg);
|
|
337
|
-
border-bottom: 1px solid var(--pui-border-default);
|
|
338
|
-
font-size: var(--pui-font-size-xs);
|
|
339
|
-
font-weight: var(--pui-font-weight-semibold);
|
|
340
|
-
color: var(--pui-text-muted);
|
|
341
|
-
text-transform: uppercase;
|
|
342
|
-
letter-spacing: 0.025em;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
.item-row,
|
|
346
|
-
.add-row {
|
|
347
|
-
display: grid;
|
|
348
|
-
grid-template-columns: 3.5rem 1fr 1fr 2.5rem;
|
|
349
|
-
gap: var(--pui-spacing-2);
|
|
350
|
-
padding: var(--pui-spacing-2) var(--pui-spacing-3);
|
|
351
|
-
align-items: center;
|
|
352
|
-
border-bottom: 1px solid var(--pui-border-default);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
.item-row:last-of-type {
|
|
356
|
-
border-bottom: 1px solid var(--pui-border-default);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
.add-row {
|
|
360
|
-
background-color: var(--pui-bg-subtle);
|
|
361
|
-
border-bottom: none;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
.col-default {
|
|
365
|
-
display: flex;
|
|
366
|
-
justify-content: center;
|
|
367
|
-
align-items: center;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
.col-actions {
|
|
371
|
-
display: flex;
|
|
372
|
-
justify-content: center;
|
|
373
|
-
align-items: center;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
.item-input {
|
|
377
|
-
width: 100%;
|
|
378
|
-
padding: var(--pui-spacing-1_5) var(--pui-spacing-2);
|
|
379
|
-
border: 1px solid var(--pui-border-default);
|
|
380
|
-
border-radius: var(--pui-radius-sm);
|
|
381
|
-
background-color: var(--pui-input-bg);
|
|
382
|
-
color: var(--pui-text-primary);
|
|
383
|
-
font-size: var(--pui-font-size-sm);
|
|
384
|
-
transition: border-color var(--pui-transition-fast) var(--pui-ease-in-out);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
.item-input:focus {
|
|
388
|
-
outline: none;
|
|
389
|
-
border-color: var(--pui-input-border-focus);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
.item-input::placeholder {
|
|
393
|
-
color: var(--pui-text-placeholder);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
.new-input {
|
|
397
|
-
background-color: transparent;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
.item-input.error {
|
|
401
|
-
border-color: var(--pui-text-danger);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
.item-input.error:focus {
|
|
405
|
-
border-color: var(--pui-text-danger);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
.error-message {
|
|
409
|
-
padding: var(--pui-spacing-1_5) var(--pui-spacing-3);
|
|
410
|
-
font-size: var(--pui-font-size-xs);
|
|
411
|
-
color: var(--pui-text-danger);
|
|
412
|
-
background-color: var(--pui-bg-subtle);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
.quick-add {
|
|
416
|
-
padding: var(--pui-spacing-2) var(--pui-spacing-3);
|
|
417
|
-
background-color: var(--pui-bg-subtle);
|
|
418
|
-
display: flex;
|
|
419
|
-
justify-content: flex-end;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
.summary {
|
|
423
|
-
display: flex;
|
|
424
|
-
gap: var(--pui-spacing-4);
|
|
425
|
-
padding: var(--pui-spacing-2) 0;
|
|
426
|
-
font-size: var(--pui-font-size-sm);
|
|
427
|
-
color: var(--pui-text-muted);
|
|
428
|
-
margin-top: var(--pui-spacing-2);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
.summary-count {
|
|
432
|
-
font-weight: var(--pui-font-weight-medium);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
.summary-default {
|
|
436
|
-
color: var(--pui-accent-color);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
:global(.dark) .add-row,
|
|
440
|
-
:global(.dark) .quick-add,
|
|
441
|
-
:global(.dark) .error-message {
|
|
442
|
-
background-color: var(--pui-bg-subtle);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/* Responsive adjustments */
|
|
446
|
-
@media (max-width: 480px) {
|
|
447
|
-
.header-row,
|
|
448
|
-
.item-row,
|
|
449
|
-
.add-row {
|
|
450
|
-
grid-template-columns: 2.5rem 1fr 1fr 2rem;
|
|
451
|
-
gap: var(--pui-spacing-1);
|
|
452
|
-
padding: var(--pui-spacing-1_5) var(--pui-spacing-2);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
.item-input {
|
|
456
|
-
padding: var(--pui-spacing-1) var(--pui-spacing-1_5);
|
|
457
|
-
font-size: var(--pui-font-size-sm);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
</style>
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ComboBoxItem } from '../models/ComboBoxItem.js';
|
|
3
|
+
import ActionIcon from '../ui/ActionIcon.svelte';
|
|
4
|
+
import Button from '../ui/Button.svelte';
|
|
5
|
+
import Checkbox from './Checkbox.svelte';
|
|
6
|
+
import { iconPlus, iconTrash } from '../icon/index.js';
|
|
7
|
+
import FormGroup from './FormGroup.svelte';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
label?: string;
|
|
11
|
+
items?: ComboBoxItem[];
|
|
12
|
+
defaultValues?: string[];
|
|
13
|
+
labelPlaceholder?: string;
|
|
14
|
+
valuePlaceholder?: string;
|
|
15
|
+
class?: string;
|
|
16
|
+
onchange?: (items: ComboBoxItem[], defaultValues: string[]) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
label = '',
|
|
21
|
+
items = $bindable([]),
|
|
22
|
+
defaultValues = $bindable([]),
|
|
23
|
+
labelPlaceholder = 'Label',
|
|
24
|
+
valuePlaceholder = 'Value',
|
|
25
|
+
class: classes = '',
|
|
26
|
+
onchange
|
|
27
|
+
}: Props = $props();
|
|
28
|
+
|
|
29
|
+
let newLabel = $state('');
|
|
30
|
+
let newValue = $state('');
|
|
31
|
+
let valueManuallyEdited = $state(false);
|
|
32
|
+
let labelInputEl: HTMLInputElement | undefined = $state(undefined);
|
|
33
|
+
|
|
34
|
+
// Copy label exactly for default value
|
|
35
|
+
function labelToValue(label: string): string {
|
|
36
|
+
return label.trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Handle label input - auto-sync to value if not manually edited
|
|
40
|
+
function handleLabelInput(value: string) {
|
|
41
|
+
newLabel = value;
|
|
42
|
+
if (!valueManuallyEdited) {
|
|
43
|
+
newValue = labelToValue(value);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Handle value input - mark as manually edited
|
|
48
|
+
function handleValueInput(value: string) {
|
|
49
|
+
newValue = value;
|
|
50
|
+
if (value !== labelToValue(newLabel)) {
|
|
51
|
+
valueManuallyEdited = true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check if a value already exists (excluding a specific index for editing)
|
|
56
|
+
function valueExists(value: string, excludeIndex?: number): boolean {
|
|
57
|
+
return items.some((item, i) =>
|
|
58
|
+
item.value === value && i !== excludeIndex
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check if a label already exists (excluding a specific index for editing)
|
|
63
|
+
function labelExists(label: string, excludeIndex?: number): boolean {
|
|
64
|
+
return items.some((item, i) =>
|
|
65
|
+
item.label === label && i !== excludeIndex
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Check if new value/label would be a duplicate
|
|
70
|
+
let isDuplicateNewValue = $derived(
|
|
71
|
+
newValue.trim() !== '' && valueExists(newValue.trim())
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
let isDuplicateNewLabel = $derived(
|
|
75
|
+
newLabel.trim() !== '' && labelExists(newLabel.trim())
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
function addItem() {
|
|
79
|
+
if (!newLabel.trim() || !newValue.trim()) return;
|
|
80
|
+
if (isDuplicateNewValue || isDuplicateNewLabel) return;
|
|
81
|
+
|
|
82
|
+
const newItem: ComboBoxItem = {
|
|
83
|
+
label: newLabel.trim(),
|
|
84
|
+
value: newValue.trim()
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
items = [...items, newItem];
|
|
88
|
+
newLabel = '';
|
|
89
|
+
newValue = '';
|
|
90
|
+
valueManuallyEdited = false;
|
|
91
|
+
|
|
92
|
+
notifyChange();
|
|
93
|
+
|
|
94
|
+
// Focus back on label input for quick entry of next item
|
|
95
|
+
labelInputEl?.focus();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function removeItem(index: number) {
|
|
99
|
+
const removedItem = items[index];
|
|
100
|
+
items = items.filter((_, i) => i !== index);
|
|
101
|
+
|
|
102
|
+
// Remove from defaults if it was selected
|
|
103
|
+
if (defaultValues.includes(removedItem.value)) {
|
|
104
|
+
defaultValues = defaultValues.filter(v => v !== removedItem.value);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
notifyChange();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function toggleDefault(value: string, checked: boolean) {
|
|
111
|
+
if (checked) {
|
|
112
|
+
defaultValues = [...defaultValues, value];
|
|
113
|
+
} else {
|
|
114
|
+
defaultValues = defaultValues.filter(v => v !== value);
|
|
115
|
+
}
|
|
116
|
+
notifyChange();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function updateItemLabel(index: number, label: string) {
|
|
120
|
+
// Don't allow duplicate labels
|
|
121
|
+
if (labelExists(label, index)) return;
|
|
122
|
+
|
|
123
|
+
const currentItem = items[index];
|
|
124
|
+
|
|
125
|
+
// Check if label and value were in sync (exact case-sensitive match)
|
|
126
|
+
const wasInSync = currentItem.label === currentItem.value;
|
|
127
|
+
|
|
128
|
+
// If they were in sync, also update the value to maintain exact match (if it wouldn't create a duplicate)
|
|
129
|
+
if (wasInSync && !valueExists(label, index)) {
|
|
130
|
+
const oldValue = currentItem.value;
|
|
131
|
+
items = items.map((item, i) =>
|
|
132
|
+
i === index ? { ...item, label, value: label } : item
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Update defaults if the value was changed
|
|
136
|
+
if (defaultValues.includes(oldValue)) {
|
|
137
|
+
defaultValues = defaultValues.map(v => v === oldValue ? label : v);
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
items = items.map((item, i) =>
|
|
141
|
+
i === index ? { ...item, label } : item
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
notifyChange();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function updateItemValue(index: number, newVal: string) {
|
|
149
|
+
// Don't allow duplicate values
|
|
150
|
+
if (valueExists(newVal, index)) return;
|
|
151
|
+
|
|
152
|
+
const oldValue = items[index].value;
|
|
153
|
+
items = items.map((item, i) =>
|
|
154
|
+
i === index ? { ...item, value: newVal } : item
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// Update defaults if the value was changed
|
|
158
|
+
if (defaultValues.includes(oldValue)) {
|
|
159
|
+
defaultValues = defaultValues.map(v => v === oldValue ? newVal : v);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
notifyChange();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function notifyChange() {
|
|
166
|
+
onchange?.(items, defaultValues);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
170
|
+
if (e.key === 'Enter') {
|
|
171
|
+
e.preventDefault();
|
|
172
|
+
addItem();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Computed output for display/copying
|
|
177
|
+
let outputItems = $derived(
|
|
178
|
+
items.map(item => ({
|
|
179
|
+
...item,
|
|
180
|
+
selected: defaultValues.includes(item.value)
|
|
181
|
+
}))
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// Get labels for selected defaults
|
|
185
|
+
let defaultLabels = $derived(
|
|
186
|
+
defaultValues
|
|
187
|
+
.map(v => items.find(i => i.value === v)?.label ?? v)
|
|
188
|
+
.join(', ')
|
|
189
|
+
);
|
|
190
|
+
</script>
|
|
191
|
+
|
|
192
|
+
<div class="combobox-builder {classes}">
|
|
193
|
+
{#if label}
|
|
194
|
+
<FormGroup {label}>
|
|
195
|
+
<div></div>
|
|
196
|
+
</FormGroup>
|
|
197
|
+
{/if}
|
|
198
|
+
|
|
199
|
+
<div class="builder-content">
|
|
200
|
+
<!-- Header row -->
|
|
201
|
+
<div class="header-row">
|
|
202
|
+
<div class="col-default">Selected</div>
|
|
203
|
+
<div class="col-label">Label</div>
|
|
204
|
+
<div class="col-value">Value</div>
|
|
205
|
+
<div class="col-actions"></div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<!-- Existing items -->
|
|
209
|
+
{#each items as item, index (index)}
|
|
210
|
+
<div class="item-row">
|
|
211
|
+
<div class="col-default">
|
|
212
|
+
<Checkbox
|
|
213
|
+
checked={defaultValues.includes(item.value)}
|
|
214
|
+
onchange={(checked) => toggleDefault(item.value, checked)}
|
|
215
|
+
/>
|
|
216
|
+
</div>
|
|
217
|
+
<div class="col-label">
|
|
218
|
+
<input
|
|
219
|
+
type="text"
|
|
220
|
+
class="item-input"
|
|
221
|
+
value={item.label}
|
|
222
|
+
oninput={(e) => updateItemLabel(index, e.currentTarget.value)}
|
|
223
|
+
placeholder={labelPlaceholder}
|
|
224
|
+
/>
|
|
225
|
+
</div>
|
|
226
|
+
<div class="col-value">
|
|
227
|
+
<input
|
|
228
|
+
type="text"
|
|
229
|
+
class="item-input"
|
|
230
|
+
value={item.value}
|
|
231
|
+
oninput={(e) => updateItemValue(index, e.currentTarget.value)}
|
|
232
|
+
placeholder={valuePlaceholder}
|
|
233
|
+
/>
|
|
234
|
+
</div>
|
|
235
|
+
<div class="col-actions">
|
|
236
|
+
<ActionIcon
|
|
237
|
+
variant="danger-subtle"
|
|
238
|
+
svg={iconTrash}
|
|
239
|
+
size="1rem"
|
|
240
|
+
onclick={() => removeItem(index)}
|
|
241
|
+
tooltip="Remove item"
|
|
242
|
+
/>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
{/each}
|
|
246
|
+
|
|
247
|
+
<!-- Add new item row -->
|
|
248
|
+
<div class="add-row">
|
|
249
|
+
<div class="col-default"></div>
|
|
250
|
+
<div class="col-label">
|
|
251
|
+
<input
|
|
252
|
+
bind:this={labelInputEl}
|
|
253
|
+
type="text"
|
|
254
|
+
class="item-input new-input"
|
|
255
|
+
class:error={isDuplicateNewLabel}
|
|
256
|
+
value={newLabel}
|
|
257
|
+
oninput={(e) => handleLabelInput(e.currentTarget.value)}
|
|
258
|
+
placeholder={labelPlaceholder}
|
|
259
|
+
onkeydown={handleKeyDown}
|
|
260
|
+
/>
|
|
261
|
+
</div>
|
|
262
|
+
<div class="col-value">
|
|
263
|
+
<input
|
|
264
|
+
type="text"
|
|
265
|
+
class="item-input new-input"
|
|
266
|
+
class:error={isDuplicateNewValue}
|
|
267
|
+
value={newValue}
|
|
268
|
+
oninput={(e) => handleValueInput(e.currentTarget.value)}
|
|
269
|
+
placeholder={valuePlaceholder}
|
|
270
|
+
onkeydown={handleKeyDown}
|
|
271
|
+
/>
|
|
272
|
+
</div>
|
|
273
|
+
<div class="col-actions">
|
|
274
|
+
<ActionIcon
|
|
275
|
+
variant="auto-subtle"
|
|
276
|
+
svg={iconPlus}
|
|
277
|
+
size="1rem"
|
|
278
|
+
onclick={addItem}
|
|
279
|
+
disabled={!newLabel.trim() || !newValue.trim() || isDuplicateNewValue || isDuplicateNewLabel}
|
|
280
|
+
tooltip="Add item"
|
|
281
|
+
/>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
{#if isDuplicateNewLabel || isDuplicateNewValue}
|
|
286
|
+
<div class="error-message">
|
|
287
|
+
{#if isDuplicateNewLabel && isDuplicateNewValue}
|
|
288
|
+
Label "{newLabel}" and value "{newValue}" already exist
|
|
289
|
+
{:else if isDuplicateNewLabel}
|
|
290
|
+
Label "{newLabel}" already exists
|
|
291
|
+
{:else}
|
|
292
|
+
Value "{newValue}" already exists
|
|
293
|
+
{/if}
|
|
294
|
+
</div>
|
|
295
|
+
{/if}
|
|
296
|
+
|
|
297
|
+
<!-- Quick add button for mobile/convenience -->
|
|
298
|
+
{#if newLabel.trim() && newValue.trim() && !isDuplicateNewValue && !isDuplicateNewLabel}
|
|
299
|
+
<div class="quick-add">
|
|
300
|
+
<Button variant="auto-subtle" onclick={addItem}>
|
|
301
|
+
Add "{newLabel}"
|
|
302
|
+
</Button>
|
|
303
|
+
</div>
|
|
304
|
+
{/if}
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
<!-- Summary -->
|
|
308
|
+
{#if items.length > 0}
|
|
309
|
+
<div class="summary">
|
|
310
|
+
<span class="summary-count">{items.length} item{items.length !== 1 ? 's' : ''}</span>
|
|
311
|
+
{#if defaultValues.length > 0}
|
|
312
|
+
<span class="summary-default">
|
|
313
|
+
Selected: {defaultLabels}
|
|
314
|
+
</span>
|
|
315
|
+
{/if}
|
|
316
|
+
</div>
|
|
317
|
+
{/if}
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<style>
|
|
321
|
+
.combobox-builder {
|
|
322
|
+
width: 100%;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.builder-content {
|
|
326
|
+
border: 1px solid var(--pui-border-default);
|
|
327
|
+
border-radius: var(--pui-radius-md);
|
|
328
|
+
overflow: hidden;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.header-row {
|
|
332
|
+
display: grid;
|
|
333
|
+
grid-template-columns: 3.5rem 1fr 1fr 2.5rem;
|
|
334
|
+
gap: var(--pui-spacing-2);
|
|
335
|
+
padding: var(--pui-spacing-2) var(--pui-spacing-3);
|
|
336
|
+
background-color: var(--pui-input-bg);
|
|
337
|
+
border-bottom: 1px solid var(--pui-border-default);
|
|
338
|
+
font-size: var(--pui-font-size-xs);
|
|
339
|
+
font-weight: var(--pui-font-weight-semibold);
|
|
340
|
+
color: var(--pui-text-muted);
|
|
341
|
+
text-transform: uppercase;
|
|
342
|
+
letter-spacing: 0.025em;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.item-row,
|
|
346
|
+
.add-row {
|
|
347
|
+
display: grid;
|
|
348
|
+
grid-template-columns: 3.5rem 1fr 1fr 2.5rem;
|
|
349
|
+
gap: var(--pui-spacing-2);
|
|
350
|
+
padding: var(--pui-spacing-2) var(--pui-spacing-3);
|
|
351
|
+
align-items: center;
|
|
352
|
+
border-bottom: 1px solid var(--pui-border-default);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.item-row:last-of-type {
|
|
356
|
+
border-bottom: 1px solid var(--pui-border-default);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.add-row {
|
|
360
|
+
background-color: var(--pui-bg-subtle);
|
|
361
|
+
border-bottom: none;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.col-default {
|
|
365
|
+
display: flex;
|
|
366
|
+
justify-content: center;
|
|
367
|
+
align-items: center;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.col-actions {
|
|
371
|
+
display: flex;
|
|
372
|
+
justify-content: center;
|
|
373
|
+
align-items: center;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.item-input {
|
|
377
|
+
width: 100%;
|
|
378
|
+
padding: var(--pui-spacing-1_5) var(--pui-spacing-2);
|
|
379
|
+
border: 1px solid var(--pui-border-default);
|
|
380
|
+
border-radius: var(--pui-radius-sm);
|
|
381
|
+
background-color: var(--pui-input-bg);
|
|
382
|
+
color: var(--pui-text-primary);
|
|
383
|
+
font-size: var(--pui-font-size-sm);
|
|
384
|
+
transition: border-color var(--pui-transition-fast) var(--pui-ease-in-out);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.item-input:focus {
|
|
388
|
+
outline: none;
|
|
389
|
+
border-color: var(--pui-input-border-focus);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.item-input::placeholder {
|
|
393
|
+
color: var(--pui-text-placeholder);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.new-input {
|
|
397
|
+
background-color: transparent;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.item-input.error {
|
|
401
|
+
border-color: var(--pui-text-danger);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.item-input.error:focus {
|
|
405
|
+
border-color: var(--pui-text-danger);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.error-message {
|
|
409
|
+
padding: var(--pui-spacing-1_5) var(--pui-spacing-3);
|
|
410
|
+
font-size: var(--pui-font-size-xs);
|
|
411
|
+
color: var(--pui-text-danger);
|
|
412
|
+
background-color: var(--pui-bg-subtle);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.quick-add {
|
|
416
|
+
padding: var(--pui-spacing-2) var(--pui-spacing-3);
|
|
417
|
+
background-color: var(--pui-bg-subtle);
|
|
418
|
+
display: flex;
|
|
419
|
+
justify-content: flex-end;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.summary {
|
|
423
|
+
display: flex;
|
|
424
|
+
gap: var(--pui-spacing-4);
|
|
425
|
+
padding: var(--pui-spacing-2) 0;
|
|
426
|
+
font-size: var(--pui-font-size-sm);
|
|
427
|
+
color: var(--pui-text-muted);
|
|
428
|
+
margin-top: var(--pui-spacing-2);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.summary-count {
|
|
432
|
+
font-weight: var(--pui-font-weight-medium);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.summary-default {
|
|
436
|
+
color: var(--pui-accent-color);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
:global(.dark) .add-row,
|
|
440
|
+
:global(.dark) .quick-add,
|
|
441
|
+
:global(.dark) .error-message {
|
|
442
|
+
background-color: var(--pui-bg-subtle);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/* Responsive adjustments */
|
|
446
|
+
@media (max-width: 480px) {
|
|
447
|
+
.header-row,
|
|
448
|
+
.item-row,
|
|
449
|
+
.add-row {
|
|
450
|
+
grid-template-columns: 2.5rem 1fr 1fr 2rem;
|
|
451
|
+
gap: var(--pui-spacing-1);
|
|
452
|
+
padding: var(--pui-spacing-1_5) var(--pui-spacing-2);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.item-input {
|
|
456
|
+
padding: var(--pui-spacing-1) var(--pui-spacing-1_5);
|
|
457
|
+
font-size: var(--pui-font-size-sm);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
</style>
|