@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 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 class="p-4 focus:outline-0" onclick={preview_previous} type="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 class="p-4 focus:outline-0" onclick={preview_next} type="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 = "md",
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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "2.7.0",
3
+ "version": "2.7.2",
4
4
  "files": [
5
5
  "dist",
6
6
  "!dist/**/*.test.*",