@marianmeres/stuic 2.7.0 → 2.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/README.md +1 -0
- package/dist/actions/tooltip/tooltip.svelte.d.ts +1 -1
- package/dist/actions/tooltip/tooltip.svelte.js +4 -0
- package/dist/components/Input/FieldAssets.svelte +19 -9
- package/dist/components/Input/FieldAssets.svelte.d.ts +1 -0
- package/dist/components/Input/FieldKeyValues.svelte +10 -8
- package/dist/components/Input/README.md +42 -0
- package/package.json +1 -1
package/dist/README.md
CHANGED
|
@@ -44,6 +44,7 @@ npm install @marianmeres/stuic
|
|
|
44
44
|
- **FieldFile** - File upload input
|
|
45
45
|
- **FieldAssets** - Multi-file upload with preview
|
|
46
46
|
- **FieldOptions** - Modal-based multi-select picker
|
|
47
|
+
- **FieldKeyValues** - Key-value pairs editor with JSON serialization
|
|
47
48
|
- **FieldSwitch** - Toggle switch within a form
|
|
48
49
|
- **Fieldset** - Group of form fields with legend
|
|
49
50
|
|
|
@@ -20,7 +20,7 @@ export declare function isTooltipSupported(): boolean;
|
|
|
20
20
|
/**
|
|
21
21
|
* Valid positions for tooltip placement relative to the anchor element.
|
|
22
22
|
*/
|
|
23
|
-
export type TooltipPosition = "top" | "top-left" | "top-right" | "bottom" | "bottom-left" | "bottom-right" | "left" | "right";
|
|
23
|
+
export type TooltipPosition = "top" | "top-left" | "top-right" | "top-span-left" | "top-span-right" | "bottom" | "bottom-left" | "bottom-right" | "bottom-span-left" | "bottom-span-right" | "left" | "right";
|
|
24
24
|
/**
|
|
25
25
|
* A Svelte action that displays a tooltip anchored to an element using CSS Anchor Positioning.
|
|
26
26
|
*
|
|
@@ -37,9 +37,13 @@ const POSITION_MAP = {
|
|
|
37
37
|
top: "top",
|
|
38
38
|
"top-left": "top left",
|
|
39
39
|
"top-right": "top right",
|
|
40
|
+
"top-span-left": "top span-left",
|
|
41
|
+
"top-span-right": "top span-right",
|
|
40
42
|
bottom: "bottom",
|
|
41
43
|
"bottom-left": "bottom left",
|
|
42
44
|
"bottom-right": "bottom right",
|
|
45
|
+
"bottom-span-left": "bottom span-left",
|
|
46
|
+
"bottom-span-right": "bottom span-right",
|
|
43
47
|
left: "left",
|
|
44
48
|
right: "right",
|
|
45
49
|
};
|
|
@@ -173,6 +173,7 @@
|
|
|
173
173
|
onProgress?: (blobUrl: string, progress: number) => any
|
|
174
174
|
) => Promise<FieldAssetWithBlobUrl[]>;
|
|
175
175
|
withOnProgress?: boolean;
|
|
176
|
+
classControls?: string;
|
|
176
177
|
}
|
|
177
178
|
</script>
|
|
178
179
|
|
|
@@ -229,6 +230,7 @@
|
|
|
229
230
|
// onChange,
|
|
230
231
|
processAssets,
|
|
231
232
|
withOnProgress,
|
|
233
|
+
classControls = "",
|
|
232
234
|
parseValue = (strigifiedModels: string) => {
|
|
233
235
|
const val = strigifiedModels ?? "[]";
|
|
234
236
|
try {
|
|
@@ -378,7 +380,7 @@
|
|
|
378
380
|
{@const _is_img = isImage(asset.type ?? thumb)}
|
|
379
381
|
<div class="relative group">
|
|
380
382
|
<button
|
|
381
|
-
class={[objectSize, "bg-black/10 grid place-content-center"]}
|
|
383
|
+
class={[objectSize, "bg-black/10 grid place-content-center", classControls]}
|
|
382
384
|
onclick={(e) => {
|
|
383
385
|
e.stopPropagation();
|
|
384
386
|
e.preventDefault();
|
|
@@ -433,7 +435,7 @@
|
|
|
433
435
|
e.stopPropagation();
|
|
434
436
|
inputEl.click();
|
|
435
437
|
}}
|
|
436
|
-
class={[objectSize, " grid place-content-center group"]}
|
|
438
|
+
class={[objectSize, " grid place-content-center group", classControls]}
|
|
437
439
|
>
|
|
438
440
|
{@html iconAdd({ size: 32, class: "opacity-75 group-hover:opacity-100" })}
|
|
439
441
|
</button>
|
|
@@ -441,7 +443,7 @@
|
|
|
441
443
|
{/snippet}
|
|
442
444
|
|
|
443
445
|
<div
|
|
444
|
-
class="w-full"
|
|
446
|
+
class="w-full stuic-field-assets"
|
|
445
447
|
use:highlightDragover={() => ({
|
|
446
448
|
enabled: typeof processAssets === "function",
|
|
447
449
|
classes: ["outline-dashed outline-2 outline-neutral-300"],
|
|
@@ -559,7 +561,7 @@
|
|
|
559
561
|
classBackdrop="p-4 md:p-4"
|
|
560
562
|
classInner="max-w-full h-full"
|
|
561
563
|
class="max-h-full md:max-h-full"
|
|
562
|
-
classMain="flex items-center justify-center relative "
|
|
564
|
+
classMain="flex items-center justify-center relative stuic-field-assets stuic-field-assets-open"
|
|
563
565
|
>
|
|
564
566
|
{@const previewAsset = assets?.[previewIdx]}
|
|
565
567
|
{#if previewAsset}
|
|
@@ -585,13 +587,21 @@
|
|
|
585
587
|
|
|
586
588
|
{#if assets?.length > 1}
|
|
587
589
|
<div class={["absolute inset-0 flex items-center justify-between"]}>
|
|
588
|
-
<button
|
|
590
|
+
<button
|
|
591
|
+
class={twMerge("p-4", classControls)}
|
|
592
|
+
onclick={preview_previous}
|
|
593
|
+
type="button"
|
|
594
|
+
>
|
|
589
595
|
<span class="bg-white rounded-full p-3 block">
|
|
590
596
|
{@html iconPrevious()}
|
|
591
597
|
</span>
|
|
592
598
|
</button>
|
|
593
599
|
|
|
594
|
-
<button
|
|
600
|
+
<button
|
|
601
|
+
class={twMerge("p-4", classControls)}
|
|
602
|
+
onclick={preview_next}
|
|
603
|
+
type="button"
|
|
604
|
+
>
|
|
595
605
|
<span class="bg-white rounded-full p-3 block">
|
|
596
606
|
{@html iconNext()}
|
|
597
607
|
</span>
|
|
@@ -602,7 +612,7 @@
|
|
|
602
612
|
<!-- bg-white rounded-md p-2 -->
|
|
603
613
|
<div class="absolute top-4 right-4 flex items-center space-x-3">
|
|
604
614
|
<button
|
|
605
|
-
class={TOP_BUTTON_CLS}
|
|
615
|
+
class={twMerge(TOP_BUTTON_CLS, classControls)}
|
|
606
616
|
onclick={(e) => {
|
|
607
617
|
e.preventDefault();
|
|
608
618
|
remove_by_idx(previewIdx);
|
|
@@ -615,7 +625,7 @@
|
|
|
615
625
|
{@html iconDelete({ class: "size-6" })}
|
|
616
626
|
</button>
|
|
617
627
|
<button
|
|
618
|
-
class={TOP_BUTTON_CLS}
|
|
628
|
+
class={twMerge(TOP_BUTTON_CLS, classControls)}
|
|
619
629
|
type="button"
|
|
620
630
|
onclick={(e) => {
|
|
621
631
|
e.preventDefault();
|
|
@@ -627,7 +637,7 @@
|
|
|
627
637
|
{@html iconDownload({ class: "size-6" })}
|
|
628
638
|
</button>
|
|
629
639
|
<button
|
|
630
|
-
class={TOP_BUTTON_CLS}
|
|
640
|
+
class={twMerge(TOP_BUTTON_CLS, classControls)}
|
|
631
641
|
onclick={modal.close}
|
|
632
642
|
aria-label={t("close")}
|
|
633
643
|
type="button"
|
|
@@ -63,6 +63,7 @@ export interface Props extends Record<string, any> {
|
|
|
63
63
|
accept?: string;
|
|
64
64
|
processAssets?: (assets: FieldAsset[], onProgress?: (blobUrl: string, progress: number) => any) => Promise<FieldAssetWithBlobUrl[]>;
|
|
65
65
|
withOnProgress?: boolean;
|
|
66
|
+
classControls?: string;
|
|
66
67
|
}
|
|
67
68
|
declare const FieldAssets: import("svelte").Component<Props, {}, "value">;
|
|
68
69
|
type FieldAssets = ReturnType<typeof FieldAssets>;
|
|
@@ -86,15 +86,17 @@
|
|
|
86
86
|
return isPlainObject(values) ? replaceMap(out, values as any) : out;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
const SERIALIZED_DEFAULT = "[]";
|
|
90
|
+
|
|
89
91
|
let {
|
|
90
|
-
value = $bindable(
|
|
92
|
+
value = $bindable(),
|
|
91
93
|
name,
|
|
92
94
|
id = getId(),
|
|
93
95
|
label,
|
|
94
96
|
description,
|
|
95
97
|
class: classProp,
|
|
96
98
|
tabindex = 0,
|
|
97
|
-
renderSize = "
|
|
99
|
+
renderSize = "sm",
|
|
98
100
|
required = false,
|
|
99
101
|
disabled = false,
|
|
100
102
|
validate,
|
|
@@ -124,13 +126,13 @@
|
|
|
124
126
|
let hiddenInputEl: HTMLInputElement | undefined = $state();
|
|
125
127
|
let keyInputRefs: HTMLInputElement[] = $state([]);
|
|
126
128
|
|
|
127
|
-
// Internal state
|
|
128
|
-
let entries: KeyValueEntry[] = $state(parseValue(value));
|
|
129
|
+
// Internal state - handle undefined value from async form initialization
|
|
130
|
+
let entries: KeyValueEntry[] = $state(parseValue(value ?? SERIALIZED_DEFAULT));
|
|
129
131
|
|
|
130
132
|
// Parse external JSON string to internal entries
|
|
131
133
|
function parseValue(jsonString: string): KeyValueEntry[] {
|
|
132
134
|
try {
|
|
133
|
-
const parsed = JSON.parse(jsonString ||
|
|
135
|
+
const parsed = JSON.parse(jsonString || SERIALIZED_DEFAULT);
|
|
134
136
|
if (!Array.isArray(parsed)) return [];
|
|
135
137
|
return parsed.map(([key, val]: [string, string]) => ({
|
|
136
138
|
id: getId("entry-"),
|
|
@@ -158,7 +160,7 @@
|
|
|
158
160
|
|
|
159
161
|
// Sync external value to internal state when it changes externally
|
|
160
162
|
$effect(() => {
|
|
161
|
-
const newEntries = parseValue(value);
|
|
163
|
+
const newEntries = parseValue(value ?? SERIALIZED_DEFAULT);
|
|
162
164
|
const currentSerialized = serializeValue(entries);
|
|
163
165
|
const newSerialized = serializeValue(newEntries);
|
|
164
166
|
if (currentSerialized !== newSerialized) {
|
|
@@ -210,7 +212,7 @@
|
|
|
210
212
|
customValidator(val: any, context: Record<string, any> | undefined, el: any) {
|
|
211
213
|
// Validate JSON structure
|
|
212
214
|
try {
|
|
213
|
-
const parsed = JSON.parse(val ||
|
|
215
|
+
const parsed = JSON.parse(val || SERIALIZED_DEFAULT);
|
|
214
216
|
if (!Array.isArray(parsed)) return "typeMismatch";
|
|
215
217
|
for (const entry of parsed) {
|
|
216
218
|
if (!Array.isArray(entry) || entry.length !== 2) return "typeMismatch";
|
|
@@ -386,7 +388,7 @@
|
|
|
386
388
|
<input
|
|
387
389
|
type="hidden"
|
|
388
390
|
{name}
|
|
389
|
-
{value}
|
|
391
|
+
value={value ?? SERIALIZED_DEFAULT}
|
|
390
392
|
bind:this={hiddenInputEl}
|
|
391
393
|
use:validateAction={() => wrappedValidate}
|
|
392
394
|
/>
|
|
@@ -14,6 +14,7 @@ A comprehensive form input system with multiple field components, validation sup
|
|
|
14
14
|
| `FieldSwitch` | Toggle switch field |
|
|
15
15
|
| `FieldFile` | File upload input |
|
|
16
16
|
| `FieldAssets` | Asset/image upload with preview |
|
|
17
|
+
| `FieldKeyValues` | Key-value pairs editor with JSON serialization |
|
|
17
18
|
| `FieldLikeButton` | Like/favorite toggle button |
|
|
18
19
|
| `Fieldset` | Fieldset with legend |
|
|
19
20
|
|
|
@@ -172,6 +173,47 @@ A comprehensive form input system with multiple field components, validation sup
|
|
|
172
173
|
/>
|
|
173
174
|
```
|
|
174
175
|
|
|
176
|
+
### Key-Value Pairs
|
|
177
|
+
|
|
178
|
+
```svelte
|
|
179
|
+
<script lang="ts">
|
|
180
|
+
import { FieldKeyValues } from 'stuic';
|
|
181
|
+
|
|
182
|
+
// Value is a JSON string of [[key, value], [key, value], ...]
|
|
183
|
+
let headers = $state('[]');
|
|
184
|
+
</script>
|
|
185
|
+
|
|
186
|
+
<FieldKeyValues
|
|
187
|
+
label="HTTP Headers"
|
|
188
|
+
description="Add custom headers as key-value pairs"
|
|
189
|
+
bind:value={headers}
|
|
190
|
+
name="headers"
|
|
191
|
+
required
|
|
192
|
+
/>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
#### FieldKeyValues Props
|
|
196
|
+
|
|
197
|
+
| Prop | Type | Default | Description |
|
|
198
|
+
|------|------|---------|-------------|
|
|
199
|
+
| `value` | `string` | `"[]"` | JSON string of `[[key, value], ...]` (bindable) |
|
|
200
|
+
| `name` | `string` | - | Form field name |
|
|
201
|
+
| `keyPlaceholder` | `string` | `"Key"` | Placeholder for key input |
|
|
202
|
+
| `valuePlaceholder` | `string` | `"Value"` | Placeholder for value textarea |
|
|
203
|
+
| `addLabel` | `string` | `"Add"` | Label for add button |
|
|
204
|
+
| `emptyMessage` | `string` | `"No entries"` | Message when no entries |
|
|
205
|
+
| `classEntry` | `string` | - | CSS for each entry row |
|
|
206
|
+
| `classKeyInput` | `string` | - | CSS for key inputs |
|
|
207
|
+
| `classValueInput` | `string` | - | CSS for value textareas |
|
|
208
|
+
| `onChange` | `(value: string) => void` | - | Callback on value change |
|
|
209
|
+
|
|
210
|
+
Features:
|
|
211
|
+
- Add/remove key-value pairs with + and trash buttons
|
|
212
|
+
- Reorder entries with up/down arrow buttons
|
|
213
|
+
- Duplicate keys are allowed
|
|
214
|
+
- Value is serialized as ordered map: `[[key, value], [key2, value2]]`
|
|
215
|
+
- Validation at top level only (not individual pairs)
|
|
216
|
+
|
|
175
217
|
## Validation
|
|
176
218
|
|
|
177
219
|
Validation is handled by the `validate` action. Pass `validate={true}` for default HTML5 validation, or pass options:
|