@marianmeres/stuic 3.164.0 → 3.166.0

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/API.md CHANGED
@@ -582,11 +582,13 @@ Closeable message banner with intent styling.
582
582
 
583
583
  #### `Progress`
584
584
 
585
- Progress bar.
585
+ Progress bar or circle.
586
586
 
587
- | Prop | Type | Default | Description |
588
- | ------- | -------- | ------- | ------------------------ |
589
- | `value` | `number` | `0` | Current progress (0-100) |
587
+ | Prop | Type | Default | Description |
588
+ | ---------- | ------------------- | ------- | --------------------------------- |
589
+ | `progress` | `number` | `0` | Current progress (0-100, clamped) |
590
+ | `type` | `"bar" \| "circle"` | `"bar"` | Rendering variant |
591
+ | `classBar` | `string` | - | Classes for the inner fill (bar) |
590
592
 
591
593
  #### `Spinner`
592
594
 
@@ -1206,12 +1208,26 @@ Responsive wrapper around Book that intelligently switches between book mode (du
1206
1208
 
1207
1209
  #### `Circle`
1208
1210
 
1209
- SVG-based circular progress indicator with configurable stroke width and rotation.
1211
+ SVG-based circular progress indicator (a Svelte wrapper around the [`svgCircle`](#svgcircleoptions)
1212
+ utility). The svg fills its container over a fixed `100x100` viewBox, so the container
1213
+ sets the size and `strokeWidth` is in viewBox units. The stroke is `currentColor`.
1214
+
1215
+ | Prop | Type | Default | Description |
1216
+ | ----------------------- | --------- | ------- | ---------------------------------------------------------------- |
1217
+ | `completeness` | `number` | `1` | Progress from 0 to 1 (clamped) |
1218
+ | `strokeWidth` | `number` | `10` | Stroke width in viewBox units (radius = `50 - strokeWidth / 2`) |
1219
+ | `strokeWidthRatio` | `number` | `0` | Caps `strokeWidth` at this fraction of the radius (`0` = no cap) |
1220
+ | `bgStrokeColor` | `string` | - | CSS color of the background ring behind the arc |
1221
+ | `roundedEdges` | `boolean` | `true` | Rounded (vs butt) stroke line caps |
1222
+ | `rotate` | `number` | `0` | Rotation in degrees (arc starts at 3 o'clock; `-90` = top) |
1223
+ | `animateCompletenessMs` | `number` | `0` | CSS transition duration on `stroke-dashoffset` (ms) |
1224
+ | `class` / `style` | `string` | - | Container div class / inline style |
1225
+ | `circleClass` | `string` | - | Classes for the `<svg>` element |
1226
+ | `circleStyle` | `string` | - | Inline styles for the `<circle>` element |
1210
1227
 
1211
- | Prop | Type | Default | Description |
1212
- | ------------- | -------- | ------- | ---------------------- |
1213
- | `value` | `number` | `0` | Progress value (0-100) |
1214
- | `strokeWidth` | `number` | `8` | Stroke width |
1228
+ ```svelte
1229
+ <Circle completeness={0.75} rotate={-90} bgStrokeColor="#e5e5e5" class="size-16" />
1230
+ ```
1215
1231
 
1216
1232
  #### `H`
1217
1233
 
@@ -1935,9 +1951,48 @@ Generate deterministic avatar colors from a name string.
1935
1951
 
1936
1952
  HSL color generation.
1937
1953
 
1938
- #### `svgCircle(radius, strokeWidth)`
1954
+ #### `svgCircle(options?)`
1955
+
1956
+ Build an SVG ring as a plain DOM node, with setters to update it in place. Framework
1957
+ agnostic - it returns the element, mounting it is up to the caller. Used by `Circle` and
1958
+ `Progress` internally.
1959
+
1960
+ The svg is `width/height: 100%` over a fixed `100x100` viewBox, so the container decides
1961
+ the rendered size; `strokeWidth` is in viewBox units and the radius is derived from it
1962
+ (`50 - strokeWidth / 2`).
1963
+
1964
+ **Options** (all optional):
1965
+
1966
+ | Option | Type | Default | Description |
1967
+ | ------------------ | --------- | ------- | ---------------------------------------------------------------- |
1968
+ | `completeness` | `number` | `1` | Arc length from 0 to 1 (clamped) |
1969
+ | `strokeWidth` | `number` | `10` | Stroke width in viewBox units |
1970
+ | `strokeWidthRatio` | `number` | `0` | Caps `strokeWidth` at this fraction of the radius (`0` = no cap) |
1971
+ | `rotate` | `number` | `0` | Rotation in degrees, modulo 360 (arc starts at 3 o'clock) |
1972
+ | `roundedEdges` | `boolean` | `true` | Rounded (vs butt) line caps |
1973
+ | `bgStrokeColor` | `string` | - | CSS color of a full background ring behind the arc |
1974
+ | `class` | `string` | - | Classes for the `<svg>` element |
1975
+ | `circleStyle` | `string` | - | Inline styles for the `<circle>` element |
1976
+ | `radius` | `number` | - | Currently ignored (derived from the viewBox and stroke width) |
1939
1977
 
1940
- Generate SVG circle path data.
1978
+ **Returns:** `{ svg: SVGSVGElement, setCompleteness(v: number): void, setRotate(deg: number): void }`
1979
+
1980
+ The setters mutate the existing element - reach for them instead of rebuilding when only
1981
+ the progress or rotation changes.
1982
+
1983
+ ```ts
1984
+ import { svgCircle } from "@marianmeres/stuic";
1985
+
1986
+ const { svg, setCompleteness } = svgCircle({
1987
+ completeness: 0.75,
1988
+ strokeWidth: 8,
1989
+ rotate: -90,
1990
+ bgStrokeColor: "#e5e5e5",
1991
+ });
1992
+ container.appendChild(svg);
1993
+
1994
+ setCompleteness(0.9); // no rebuild
1995
+ ```
1941
1996
 
1942
1997
  #### `oscillate(min, max, step)`
1943
1998
 
@@ -1,5 +1,24 @@
1
+ <script lang="ts" module>
2
+ import type { SvgCircleOptions } from "../../utils/svg-circle.js";
3
+
4
+ export interface Props extends Partial<SvgCircleOptions> {
5
+ /** Inline styles for the container element */
6
+ style?: string;
7
+ /**
8
+ * CSS classes for the `<svg>` element (forwarded to the helper's `class`
9
+ * option). Since the stroke is `currentColor`, a text color utility here
10
+ * colors the ring.
11
+ */
12
+ circleClass?: string;
13
+ /** Inline styles for the `<circle>` element */
14
+ circleStyle?: string;
15
+ /** Transition duration in ms on the stroke-dashoffset */
16
+ animateCompletenessMs?: number;
17
+ }
18
+ </script>
19
+
1
20
  <script lang="ts">
2
- import { svgCircle, type SvgCircleOptions } from "../../utils/svg-circle.js";
21
+ import { svgCircle } from "../../utils/svg-circle.js";
3
22
  import { twMerge } from "../../utils/tw-merge.js";
4
23
 
5
24
  let {
@@ -14,13 +33,7 @@
14
33
  circleClass,
15
34
  circleStyle = "",
16
35
  animateCompletenessMs = 0,
17
- }: Partial<SvgCircleOptions> & {
18
- style?: string;
19
- circleClass?: string;
20
- circleStyle?: string;
21
- // transition duration in ms on the stroke-dashoffset
22
- animateCompletenessMs?: number;
23
- } = $props();
36
+ }: Props = $props();
24
37
 
25
38
  let container: HTMLDivElement = $state()!;
26
39
 
@@ -42,10 +55,12 @@
42
55
  );
43
56
 
44
57
  $effect(() => {
45
- container.appendChild(circle.svg);
46
- return () => {
47
- circle.svg.remove();
48
- };
58
+ // Capture the current instance: `circle` is a derived, so reading it again in the
59
+ // teardown would resolve to the freshly rebuilt svg and leave the previous node
60
+ // behind (stacking one <svg> per structural prop change).
61
+ const { svg } = circle;
62
+ container.appendChild(svg);
63
+ return () => svg.remove();
49
64
  });
50
65
 
51
66
  $effect(() => {
@@ -1,10 +1,18 @@
1
- import { type SvgCircleOptions } from "../../utils/svg-circle.js";
2
- type $$ComponentProps = Partial<SvgCircleOptions> & {
1
+ import type { SvgCircleOptions } from "../../utils/svg-circle.js";
2
+ export interface Props extends Partial<SvgCircleOptions> {
3
+ /** Inline styles for the container element */
3
4
  style?: string;
5
+ /**
6
+ * CSS classes for the `<svg>` element (forwarded to the helper's `class`
7
+ * option). Since the stroke is `currentColor`, a text color utility here
8
+ * colors the ring.
9
+ */
4
10
  circleClass?: string;
11
+ /** Inline styles for the `<circle>` element */
5
12
  circleStyle?: string;
13
+ /** Transition duration in ms on the stroke-dashoffset */
6
14
  animateCompletenessMs?: number;
7
- };
8
- declare const Circle: import("svelte").Component<$$ComponentProps, {}, "">;
15
+ }
16
+ declare const Circle: import("svelte").Component<Props, {}, "">;
9
17
  type Circle = ReturnType<typeof Circle>;
10
18
  export default Circle;
@@ -1,22 +1,31 @@
1
1
  # Circle
2
2
 
3
- An SVG circle progress indicator with configurable stroke, rotation, and animated transitions.
3
+ An SVG circle progress indicator with configurable stroke, rotation, and animated
4
+ transitions. A thin Svelte wrapper around the [`svgCircle`](../../utils/svg-circle.ts)
5
+ utility.
6
+
7
+ The svg is rendered at `width/height: 100%` over a fixed `100x100` viewBox, so **the
8
+ container decides the size** (default `size-6`) and `strokeWidth` is expressed in viewBox
9
+ units - i.e. it scales with the box. The stroke defaults to `currentColor`.
4
10
 
5
11
  ## Props
6
12
 
7
- | Prop | Type | Default | Description |
8
- | ----------------------- | --------- | ------- | --------------------------------------------- |
9
- | `completeness` | `number` | `1` | Progress value from 0 to 1 |
10
- | `strokeWidth` | `number` | `10` | Stroke width in SVG units |
11
- | `bgStrokeColor` | `string` | - | Background circle stroke color |
12
- | `roundedEdges` | `boolean` | `true` | Use rounded stroke line caps |
13
- | `rotate` | `number` | `0` | Rotation in degrees |
14
- | `strokeWidthRatio` | `number` | `0` | Ratio for background stroke width |
15
- | `animateCompletenessMs` | `number` | `0` | Transition duration for progress changes (ms) |
16
- | `class` | `string` | - | CSS for container div |
17
- | `style` | `string` | - | Inline styles for container |
18
- | `circleClass` | `string` | - | CSS for SVG circle element |
19
- | `circleStyle` | `string` | - | Inline styles for circle |
13
+ | Prop | Type | Default | Description |
14
+ | ----------------------- | --------- | ------- | ---------------------------------------------------------------------- |
15
+ | `completeness` | `number` | `1` | Progress from 0 to 1 (clamped) |
16
+ | `strokeWidth` | `number` | `10` | Stroke width in viewBox units (radius = `50 - strokeWidth / 2`) |
17
+ | `strokeWidthRatio` | `number` | `0` | Caps `strokeWidth` at this fraction of the radius; `0` means no cap |
18
+ | `bgStrokeColor` | `string` | - | Any CSS color; adds a full background ring behind the arc |
19
+ | `roundedEdges` | `boolean` | `true` | Rounded (vs butt) stroke line caps |
20
+ | `rotate` | `number` | `0` | Rotation in degrees; the arc starts at 3 o'clock, so use `-90` for top |
21
+ | `animateCompletenessMs` | `number` | `0` | CSS transition duration on `stroke-dashoffset` (ms) |
22
+ | `class` | `string` | - | CSS classes for the container div |
23
+ | `style` | `string` | - | Inline styles for the container div |
24
+ | `circleClass` | `string` | - | CSS classes for the `<svg>` element |
25
+ | `circleStyle` | `string` | - | Inline styles for the `<circle>` element |
26
+
27
+ `completeness` and `rotate` are applied through the helper's setters, so changing them
28
+ only rewrites two attributes. Every other prop rebuilds the svg.
20
29
 
21
30
  ## Usage
22
31
 
@@ -24,17 +33,17 @@ An SVG circle progress indicator with configurable stroke, rotation, and animate
24
33
 
25
34
  ```svelte
26
35
  <script lang="ts">
27
- import { Circle } from "stuic";
36
+ import { Circle } from "@marianmeres/stuic";
28
37
  </script>
29
38
 
30
- <Circle completeness={0.75} class="size-16" />
39
+ <Circle completeness={0.75} rotate={-90} class="size-16" />
31
40
  ```
32
41
 
33
42
  ### Animated Progress
34
43
 
35
44
  ```svelte
36
45
  <script lang="ts">
37
- import { Circle } from "stuic";
46
+ import { Circle } from "@marianmeres/stuic";
38
47
 
39
48
  let progress = $state(0);
40
49
 
@@ -54,13 +63,40 @@ An SVG circle progress indicator with configurable stroke, rotation, and animate
54
63
 
55
64
  ### Custom Styling
56
65
 
66
+ The ring's stroke is `currentColor`, so a text color on the container (or on the svg via
67
+ `circleClass`) colors it. To set the stroke directly, use `circleStyle` - it lands on the
68
+ `<circle>` element, which is the only place that beats the `stroke="currentColor"`
69
+ presentation attribute.
70
+
57
71
  ```svelte
72
+ <!-- via currentColor -->
58
73
  <Circle
59
74
  completeness={0.5}
60
75
  strokeWidth={8}
61
76
  rotate={-90}
62
77
  bgStrokeColor="rgba(0,0,0,0.1)"
78
+ class="size-24 text-blue-500"
79
+ />
80
+
81
+ <!-- or explicitly on the circle element -->
82
+ <Circle
83
+ completeness={0.5}
84
+ rotate={-90}
63
85
  class="size-24"
64
- circleClass="stroke-blue-500"
86
+ circleStyle="stroke: var(--stuic-color-primary);"
65
87
  />
66
88
  ```
89
+
90
+ ### Content Inside the Ring
91
+
92
+ ```svelte
93
+ <div class="relative size-24">
94
+ <Circle
95
+ completeness={0.62}
96
+ rotate={-90}
97
+ bgStrokeColor="#e5e5e5"
98
+ class="absolute inset-0"
99
+ />
100
+ <div class="absolute inset-0 flex items-center justify-center">62%</div>
101
+ </div>
102
+ ```
@@ -0,0 +1 @@
1
+ export { default as Circle, type Props as CircleProps } from "./Circle.svelte";
@@ -0,0 +1 @@
1
+ export { default as Circle } from "./Circle.svelte";
@@ -420,11 +420,35 @@
420
420
  syncToValue();
421
421
  }
422
422
 
423
- function setExtra(row: Row, key: string, checked: boolean) {
424
- row.def.extras = { ...(row.def.extras ?? {}), [key]: checked };
423
+ // `undefined` REMOVES the key (and an emptied bag removes `extras` itself):
424
+ // "no value" must have exactly one representation downstream — a consumer
425
+ // reading `extras.unit` to decide whether to render a suffix should never
426
+ // have to special-case `""`, nor a `{}` that means nothing.
427
+ function setExtra(row: Row, key: string, value: unknown) {
428
+ const next = { ...(row.def.extras ?? {}) };
429
+ if (value === undefined) delete next[key];
430
+ else next[key] = value;
431
+ row.def.extras = Object.keys(next).length ? next : undefined;
425
432
  syncToValue();
426
433
  }
427
434
 
435
+ /** Display value of a string/select extra (a non-string is shown, not eaten). */
436
+ function extraText(row: Row, key: string): string {
437
+ const v = row.def.extras?.[key];
438
+ return v == null ? "" : String(v);
439
+ }
440
+
441
+ // while typing, the RAW value is stored (trimming here would fight the
442
+ // caret: a written-back trimmed value makes a trailing space untypable) —
443
+ // only the emptiness test is trimmed; `onchange` normalizes on commit
444
+ function onExtraStringInput(row: Row, key: string, raw: string) {
445
+ setExtra(row, key, raw.trim() ? raw : undefined);
446
+ }
447
+
448
+ function onExtraStringChange(row: Row, key: string, raw: string) {
449
+ setExtra(row, key, raw.trim() || undefined);
450
+ }
451
+
428
452
  function typeChanged(row: Row): boolean {
429
453
  return !!row.initialType && row.def.type !== row.initialType;
430
454
  }
@@ -642,6 +666,8 @@
642
666
  ".fb-options .fb-option-value, .fb-options .fb-add-option-btn"
643
667
  )
644
668
  ?.focus?.();
669
+ } else if (errs.extras) {
670
+ rowEls[row.rid]?.querySelector<HTMLElement>(".fb-extra-input")?.focus?.();
645
671
  }
646
672
  });
647
673
  }
@@ -734,6 +760,7 @@
734
760
  {@const showLabelError = !!(attempted && errs?.label)}
735
761
  {@const showKeyError = !!(errs?.key && (attempted || row.keyEdited))}
736
762
  {@const showOptionsError = !!(attempted && errs?.options)}
763
+ {@const showExtrasError = !!(attempted && errs?.extras)}
737
764
  {@const canDrag =
738
765
  !disabled && !row.deleted && !row.def.lock?.reorder && rows.length > 1}
739
766
  <div
@@ -807,7 +834,7 @@
807
834
  {getLocalizedText(entry.label, _defaultLanguage)}
808
835
  </span>
809
836
  {/if}
810
- {#if (showLabelError || showKeyError || showOptionsError) && !row.deleted}
837
+ {#if (showLabelError || showKeyError || showOptionsError || showExtrasError) && !row.deleted}
811
838
  <span class="fb-error-text shrink-0">
812
839
  <span aria-hidden="true"
813
840
  >{@html iconAlertWarning({ size: 14 })}</span
@@ -1035,36 +1062,120 @@
1035
1062
  {/if}
1036
1063
 
1037
1064
  {#if entry.extras?.length}
1038
- <div class="fb-field flex flex-col gap-1.5">
1039
- {#each entry.extras as ex (ex.key)}
1040
- <label
1041
- class="stuic-checkbox flex items-start gap-2 cursor-pointer"
1042
- >
1043
- <!--
1044
- Display the ACTUAL def value only (no `?? ex.default`
1045
- fallback): defaults are materialized into `extras` on
1046
- add/type-change, but a def loaded without the key must
1047
- not render checked while emitting nothing — the
1048
- checkbox must always match what `value` says.
1049
- -->
1050
- <input
1051
- type="checkbox"
1052
- checked={!!row.def.extras?.[ex.key]}
1053
- onchange={(e) =>
1054
- setExtra(row, ex.key, e.currentTarget.checked)}
1055
- {disabled}
1056
- {tabindex}
1057
- />
1058
- <span class="text-sm">
1059
- {getLocalizedText(ex.label, _defaultLanguage)}
1065
+ <!--
1066
+ Every arm displays the ACTUAL def value only (no
1067
+ `?? ex.default` fallback): defaults are materialized into
1068
+ `extras` on add/type-change, but a def loaded without the
1069
+ key must not render as if it held the default while
1070
+ emitting nothing — the control must always match what
1071
+ `value` says.
1072
+ -->
1073
+ <div class="fb-extras fb-field flex flex-col gap-2.5">
1074
+ {#each entry.extras as ex, exIdx (ex.key)}
1075
+ {#if ex.type === "string" || ex.type === "select"}
1076
+ {@const exId = `${id}-extra-${row.rid}-${exIdx}`}
1077
+ {@const exValue = extraText(row, ex.key)}
1078
+ <div class="fb-extra">
1079
+ <label class="fb-sub-label" for={exId}>
1080
+ {getLocalizedText(ex.label, _defaultLanguage)}
1081
+ </label>
1082
+ {#if ex.type === "string"}
1083
+ <input
1084
+ id={exId}
1085
+ type="text"
1086
+ class={twMerge(INPUT_CLS, "fb-extra-input w-full")}
1087
+ value={exValue}
1088
+ maxlength={ex.maxlength}
1089
+ placeholder={getLocalizedText(
1090
+ ex.placeholder,
1091
+ _defaultLanguage
1092
+ ) || undefined}
1093
+ oninput={(e) =>
1094
+ onExtraStringInput(
1095
+ row,
1096
+ ex.key,
1097
+ e.currentTarget.value
1098
+ )}
1099
+ onchange={(e) =>
1100
+ onExtraStringChange(
1101
+ row,
1102
+ ex.key,
1103
+ e.currentTarget.value
1104
+ )}
1105
+ {disabled}
1106
+ {tabindex}
1107
+ aria-invalid={showExtrasError || undefined}
1108
+ aria-describedby={showExtrasError
1109
+ ? `${id}-extras-err-${row.rid}`
1110
+ : undefined}
1111
+ />
1112
+ {:else}
1113
+ <select
1114
+ id={exId}
1115
+ class={twMerge(INPUT_CLS, "fb-extra-input w-full")}
1116
+ value={exValue}
1117
+ onchange={(e) =>
1118
+ setExtra(
1119
+ row,
1120
+ ex.key,
1121
+ e.currentTarget.value || undefined
1122
+ )}
1123
+ {disabled}
1124
+ {tabindex}
1125
+ >
1126
+ <option value="">
1127
+ {getLocalizedText(ex.placeholder, _defaultLanguage)}
1128
+ </option>
1129
+ {#each ex.options as opt (opt.value)}
1130
+ <option value={opt.value}>
1131
+ {getLocalizedText(opt.label, _defaultLanguage)}
1132
+ </option>
1133
+ {/each}
1134
+ <!-- a stored value outside the declared list stays
1135
+ visible and round-trips (same stance as an
1136
+ unknown field type) -->
1137
+ {#if exValue && !ex.options.some((o) => o.value === exValue)}
1138
+ <option value={exValue}>{exValue}</option>
1139
+ {/if}
1140
+ </select>
1141
+ {/if}
1060
1142
  {#if ex.description}
1061
- <span class="fb-hint block text-xs">
1143
+ <div class="fb-hint text-xs mt-0.5">
1062
1144
  {getLocalizedText(ex.description, _defaultLanguage)}
1063
- </span>
1145
+ </div>
1064
1146
  {/if}
1065
- </span>
1066
- </label>
1147
+ </div>
1148
+ {:else}
1149
+ <label
1150
+ class="stuic-checkbox fb-extra flex items-start gap-2 cursor-pointer"
1151
+ >
1152
+ <input
1153
+ type="checkbox"
1154
+ checked={!!row.def.extras?.[ex.key]}
1155
+ onchange={(e) =>
1156
+ setExtra(row, ex.key, e.currentTarget.checked)}
1157
+ {disabled}
1158
+ {tabindex}
1159
+ />
1160
+ <span class="text-sm">
1161
+ {getLocalizedText(ex.label, _defaultLanguage)}
1162
+ {#if ex.description}
1163
+ <span class="fb-hint block text-xs">
1164
+ {getLocalizedText(ex.description, _defaultLanguage)}
1165
+ </span>
1166
+ {/if}
1167
+ </span>
1168
+ </label>
1169
+ {/if}
1067
1170
  {/each}
1171
+ {#if showExtrasError}
1172
+ <div
1173
+ id="{id}-extras-err-{row.rid}"
1174
+ class="fb-error-text text-sm"
1175
+ >
1176
+ {errs?.extras}
1177
+ </div>
1178
+ {/if}
1068
1179
  </div>
1069
1180
  {/if}
1070
1181
 
@@ -60,8 +60,9 @@ Value membership rules:
60
60
  **retained** on the def (switching back restores them); consumers compiling the list
61
61
  should ignore `options` on non-choice types.
62
62
  - Palette `extras` defaults are materialized into `def.extras` when a field is added
63
- or its type changes; a def loaded _without_ an extra's key renders unchecked — the
64
- checkbox always reflects what `value` actually contains, never a phantom default.
63
+ or its type changes; a def loaded _without_ an extra's key renders empty/unchecked —
64
+ the control always reflects what `value` actually contains, never a phantom default.
65
+ Extras are **retained** across a type change too (same rule as `options`).
65
66
 
66
67
  ## The palette
67
68
 
@@ -72,17 +73,69 @@ interface FieldTypeDef {
72
73
  description?: LocalizedText;
73
74
  icon?: string | Snippet; // html string or snippet, shown in the row's type chip
74
75
  supportsOptions?: boolean; // renders the option editor
75
- extras?: {
76
- key: string;
77
- label: LocalizedText;
78
- description?: LocalizedText;
79
- type: "boolean"; // v1: booleans only (a checkbox per extra)
80
- default?: boolean;
81
- }[];
76
+ extras?: FieldTypeExtraDef[]; // per-type controls, see below
82
77
  preview?: Snippet<[FieldDef]>; // per-type preview of a single field
83
78
  }
84
79
  ```
85
80
 
81
+ ## Extras
82
+
83
+ `extras` declares extra per-field controls, each stored under `FieldDef.extras[key]`.
84
+ The component never interprets the values — it renders a control per declaration and
85
+ round-trips whatever is there. Discriminated by `type`:
86
+
87
+ ```ts
88
+ type FieldTypeExtraDef =
89
+ | { key; label; description?; type: "boolean"; default?: boolean }
90
+ | {
91
+ key;
92
+ label;
93
+ description?;
94
+ type: "string";
95
+ default?: string;
96
+ placeholder?: LocalizedText;
97
+ maxlength?: number;
98
+ }
99
+ | {
100
+ key;
101
+ label;
102
+ description?;
103
+ type: "select";
104
+ default?: string;
105
+ placeholder?: LocalizedText;
106
+ options: { value: string; label: LocalizedText }[];
107
+ };
108
+ ```
109
+
110
+ ```ts
111
+ {
112
+ type: "number",
113
+ label: "Number",
114
+ extras: [
115
+ // rendered after the number on the consumer's own page: "12.5 % vol"
116
+ { key: "unit", label: "Unit", type: "string", placeholder: "e.g. % vol", maxlength: 16 },
117
+ { key: "group", label: "Group", type: "select", placeholder: "— none —",
118
+ options: [{ value: "nutrition", label: "Nutrition declaration" }] },
119
+ ],
120
+ }
121
+ ```
122
+
123
+ - **Empty means absent.** Emptying a `string` extra or picking a `select`'s blank entry
124
+ **removes the key** (and removes `extras` itself once the bag is empty) rather than
125
+ storing `""` — "no unit" has exactly one representation downstream. Consequence worth
126
+ knowing: a `default` becomes eligible for re-seeding again, so a cleared extra
127
+ reappears if the field's type is changed away and back.
128
+ - **Whitespace is trimmed on commit, not while typing** — trimming on every keystroke
129
+ would make a trailing space untypable.
130
+ - **`maxlength` is checked twice** — the input's attribute bounds typing, and
131
+ `validateFieldDefs` re-checks the stored value (the attribute does not constrain a
132
+ programmatically seeded or imported one) and reports it as a row error.
133
+ - **A `select` value outside the declared `options` is preserved** and rendered as its
134
+ own entry, the same stance the component takes on an unknown field `type` — it stays
135
+ visible and round-trips instead of being silently coerced to blank.
136
+ - Extras have **no per-extra lock**; `FieldLock` does not cover them (the whole editor
137
+ still honours `disabled`).
138
+
86
139
  `FIELDS_BUILDER_DEFAULT_TYPES` ships a small general-purpose palette
87
140
  (text / longtext / number / checkbox / select / date) for demos and unopinionated
88
141
  consumers — `types` is still a required prop, so nobody gets it by accident.
@@ -59,6 +59,7 @@ export const FIELDS_BUILDER_MESSAGES_SK = {
59
59
  err_options_required: "Pridajte aspoň jednu možnosť",
60
60
  err_option_value_required: "Každá možnosť musí mať hodnotu",
61
61
  err_option_value_duplicate: "Hodnoty možností musia byť jedinečné",
62
+ err_extra_maxlength: "{{label}} — hodnota je príliš dlhá (max. {{max}} znakov)",
62
63
  err_max_fields: "Maximálny počet polí je {{max}}",
63
64
  };
64
65
  /**
@@ -53,6 +53,7 @@ export declare const FIELDS_BUILDER_MESSAGES_EN: {
53
53
  err_options_required: string;
54
54
  err_option_value_required: string;
55
55
  err_option_value_duplicate: string;
56
+ err_extra_maxlength: string;
56
57
  err_max_fields: string;
57
58
  };
58
59
  /** Every message key `FieldsBuilder` (and its internals) may look up. */
@@ -54,6 +54,7 @@ export const FIELDS_BUILDER_MESSAGES_EN = {
54
54
  err_options_required: "Add at least one choice",
55
55
  err_option_value_required: "Every choice needs a value",
56
56
  err_option_value_duplicate: "Choice values must be unique",
57
+ err_extra_maxlength: "{{label}} is too long (max {{max}} characters)",
57
58
  err_max_fields: "Maximum number of fields is {{max}}",
58
59
  };
59
60
  /**
@@ -1,5 +1,5 @@
1
1
  export { default as FieldsBuilder, type Props as FieldsBuilderProps, } from "./FieldsBuilder.svelte";
2
- export type { FieldDef, FieldLock, FieldOptionDef, FieldTypeDef, FieldTypeExtraDef, LocalizedText, } from "./types.js";
2
+ export type { FieldDef, FieldLock, FieldOptionDef, FieldTypeDef, FieldTypeExtraBaseDef, FieldTypeExtraBooleanDef, FieldTypeExtraDef, FieldTypeExtraSelectDef, FieldTypeExtraStringDef, LocalizedText, } from "./types.js";
3
3
  export { DEFAULT_FIELD_TYPES as FIELDS_BUILDER_DEFAULT_TYPES, DEFAULT_KEY_PATTERN as FIELDS_BUILDER_DEFAULT_KEY_PATTERN, getLocalizedText, slugifyKey, uniqueKey, validateFieldDefs, type FieldDefRowErrors, type FieldDefsValidationResult, type ValidateFieldDefsOptions, } from "./utils.js";
4
4
  export { createFieldsBuilderT, FIELDS_BUILDER_MESSAGES_EN, type FieldsBuilderMessageKey, type FieldsBuilderMessages, } from "./i18n.js";
5
5
  export { FIELDS_BUILDER_MESSAGES_SK, FIELDS_BUILDER_DEFAULT_TYPES_SK, } from "./i18n-sk.js";
@@ -40,24 +40,60 @@ export interface FieldDef {
40
40
  */
41
41
  options?: FieldOptionDef[];
42
42
  /**
43
- * Per-type extra flags, driven by the palette entry's `extras`. An open bag
43
+ * Per-type extra values, driven by the palette entry's `extras`. An open bag
44
44
  * on purpose — the component never interprets these, it only renders a
45
- * control per declared extra and round-trips the value.
45
+ * control per declared extra and round-trips the value. Like `options`,
46
+ * extras of a previous type are RETAINED when the type changes.
46
47
  */
47
48
  extras?: Record<string, unknown>;
48
49
  /** What the user may NOT change. Absent = fully editable. */
49
50
  lock?: FieldLock;
50
51
  }
51
- /** An extra per-field control declared by a palette entry (v1: booleans only). */
52
- export interface FieldTypeExtraDef {
52
+ /** Shared shape of every extra per-field control declared by a palette entry. */
53
+ export interface FieldTypeExtraBaseDef {
53
54
  /** Stored under `FieldDef.extras[key]`. */
54
55
  key: string;
55
56
  label: LocalizedText;
56
57
  description?: LocalizedText;
57
- /** v1: booleans only (rendered as a checkbox). */
58
+ }
59
+ /** A yes/no extra — rendered as a checkbox. */
60
+ export interface FieldTypeExtraBooleanDef extends FieldTypeExtraBaseDef {
58
61
  type: "boolean";
59
62
  default?: boolean;
60
63
  }
64
+ /**
65
+ * A short free-text extra (a unit, a suffix, a format hint...) — rendered as a
66
+ * single-line text input. An emptied value REMOVES `extras[key]` rather than
67
+ * storing `""`, so "no value" has exactly one representation downstream.
68
+ */
69
+ export interface FieldTypeExtraStringDef extends FieldTypeExtraBaseDef {
70
+ type: "string";
71
+ default?: string;
72
+ placeholder?: LocalizedText;
73
+ /**
74
+ * Enforced by the input's `maxlength` attribute AND re-checked by
75
+ * `validateFieldDefs` (the attribute does not bound a programmatically
76
+ * seeded value).
77
+ */
78
+ maxlength?: number;
79
+ }
80
+ /**
81
+ * One of a fixed list of values — rendered as a select. Selecting the empty
82
+ * entry REMOVES `extras[key]`. A stored value that is not among `options` is
83
+ * kept and rendered as-is (never silently coerced away).
84
+ */
85
+ export interface FieldTypeExtraSelectDef extends FieldTypeExtraBaseDef {
86
+ type: "select";
87
+ default?: string;
88
+ options: FieldOptionDef[];
89
+ /** Label of the "nothing selected" entry. Default: blank. */
90
+ placeholder?: LocalizedText;
91
+ }
92
+ /**
93
+ * An extra per-field control declared by a palette entry, rendered into
94
+ * `FieldDef.extras[key]`. Discriminated by `type`.
95
+ */
96
+ export type FieldTypeExtraDef = FieldTypeExtraBooleanDef | FieldTypeExtraStringDef | FieldTypeExtraSelectDef;
61
97
  /** One entry of the type palette (the `types` prop). */
62
98
  export interface FieldTypeDef {
63
99
  /** Stored in `FieldDef.type`. */
@@ -26,6 +26,8 @@ export interface FieldDefRowErrors {
26
26
  label?: string;
27
27
  key?: string;
28
28
  options?: string;
29
+ /** First offending extra of the row (its label is in the message). */
30
+ extras?: string;
29
31
  }
30
32
  export interface FieldDefsValidationResult {
31
33
  valid: boolean;
@@ -41,7 +43,7 @@ export interface ValidateFieldDefsOptions {
41
43
  * must never block the rest of the list), but their keys still count toward
42
44
  * uniqueness.
43
45
  */
44
- types?: Pick<FieldTypeDef, "type" | "supportsOptions">[];
46
+ types?: Pick<FieldTypeDef, "type" | "supportsOptions" | "extras">[];
45
47
  keyPattern?: RegExp;
46
48
  keyMaxLength?: number;
47
49
  reservedKeys?: string[] | ((key: string) => boolean);
@@ -53,7 +55,7 @@ export interface ValidateFieldDefsOptions {
53
55
  /**
54
56
  * Validate a list of field defs: label non-empty, key present/pattern/length/
55
57
  * unique/not-reserved, choice types have at least one option with non-empty
56
- * unique values, `maxFields` not exceeded.
58
+ * unique values, string extras within `maxlength`, `maxFields` not exceeded.
57
59
  *
58
60
  * This is client-side convenience only — a consumer persisting the list MUST
59
61
  * re-validate server-side; this function is not a security boundary.
@@ -65,7 +65,7 @@ export function isKeyReserved(key, reservedKeys) {
65
65
  /**
66
66
  * Validate a list of field defs: label non-empty, key present/pattern/length/
67
67
  * unique/not-reserved, choice types have at least one option with non-empty
68
- * unique values, `maxFields` not exceeded.
68
+ * unique values, string extras within `maxlength`, `maxFields` not exceeded.
69
69
  *
70
70
  * This is client-side convenience only — a consumer persisting the list MUST
71
71
  * re-validate server-side; this function is not a security boundary.
@@ -125,6 +125,19 @@ export function validateFieldDefs(defs, opts = {}) {
125
125
  put(i, "options", t("err_option_value_duplicate"));
126
126
  }
127
127
  }
128
+ // `maxlength` is re-checked here on purpose: the input's attribute stops
129
+ // typing, it does not bound a seeded or pasted-then-mutated value
130
+ for (const ex of typeMap?.get(d.type)?.extras ?? []) {
131
+ if (ex.type !== "string" || !ex.maxlength)
132
+ continue;
133
+ const v = d.extras?.[ex.key];
134
+ if (typeof v === "string" && v.length > ex.maxlength) {
135
+ put(i, "extras", t("err_extra_maxlength", {
136
+ label: getLocalizedText(ex.label, opts.defaultLanguage),
137
+ max: ex.maxlength,
138
+ }));
139
+ }
140
+ }
128
141
  });
129
142
  let message = rowErrors.find(Boolean)
130
143
  ? Object.values(rowErrors.find(Boolean))[0]
@@ -1,6 +1,7 @@
1
1
  <script lang="ts" module>
2
2
  import type { Snippet } from "svelte";
3
3
  import type { ValidateOptions } from "../../actions/validate.svelte.js";
4
+ import type { SwitchIntent } from "../Switch/Switch.svelte";
4
5
  import type { THC } from "../Thc/Thc.svelte";
5
6
  import type { InputWrapClassProps } from "./types.js";
6
7
 
@@ -32,6 +33,25 @@
32
33
  classInput?: string;
33
34
  style?: string;
34
35
  renderValue?: (rawValue: any) => string;
36
+ //
37
+ // Below: forwarded as-is to the underlying <Switch>.
38
+ //
39
+ /** Semantic color intent of the switch */
40
+ intent?: SwitchIntent;
41
+ /**
42
+ * Size of the switch itself. Deliberately separate from `renderSize`, which sizes
43
+ * the InputWrap shell (label, description, spacing) around it.
44
+ */
45
+ switchSize?: "xs" | "sm" | "md" | "lg" | string;
46
+ /** Classes for the switch's toggle dot/knob element */
47
+ dotClass?: string;
48
+ /** Snippet to render inside the dot when checked */
49
+ on?: Snippet;
50
+ /** Snippet to render inside the dot when unchecked */
51
+ off?: Snippet;
52
+ /** Async validation before toggle - return false to prevent change */
53
+ preHook?: (current: boolean) => Promise<false | any>;
54
+ onclick?: (event: MouseEvent) => void;
35
55
  }
36
56
  </script>
37
57
 
@@ -72,6 +92,15 @@
72
92
  labelLeftBreakpoint = 480,
73
93
  //
74
94
  classInput,
95
+ //
96
+ intent,
97
+ switchSize = "md",
98
+ dotClass,
99
+ on,
100
+ off,
101
+ preHook,
102
+ onclick,
103
+ //
75
104
  classLabel,
76
105
  classLabelBox,
77
106
  classInputBox,
@@ -150,13 +179,30 @@
150
179
  classInputBoxWrap={twMerge("input-wrap-transparent", classInputBoxWrap)}
151
180
  {style}
152
181
  >
182
+ <!--
183
+ `aria-labelledby` (not the InputWrap's `for={id}`) is what names the switch: the
184
+ <Switch> root is itself a <label>, so a `for` pointing at it would not associate,
185
+ and its inner checkbox is aria-hidden. Matches InputWrap's own `{id}-label`, and
186
+ only when there is a label to point at.
187
+ -->
153
188
  <Switch
154
189
  bind:this={switchRef}
155
190
  bind:checked
156
191
  {name}
157
192
  {required}
158
193
  {disabled}
194
+ {intent}
195
+ {dotClass}
196
+ {on}
197
+ {off}
198
+ {preHook}
199
+ {onclick}
200
+ {tabindex}
201
+ size={switchSize}
202
+ class={classInput}
203
+ aria-labelledby={label ? `${id}-label` : undefined}
159
204
  validate={validateProp}
160
205
  {setValidationResult}
206
+ {...rest}
161
207
  />
162
208
  </InputWrap>
@@ -1,5 +1,6 @@
1
1
  import type { Snippet } from "svelte";
2
2
  import type { ValidateOptions } from "../../actions/validate.svelte.js";
3
+ import type { SwitchIntent } from "../Switch/Switch.svelte";
3
4
  import type { THC } from "../Thc/Thc.svelte";
4
5
  import type { InputWrapClassProps } from "./types.js";
5
6
  type SnippetWithId = Snippet<[{
@@ -31,6 +32,22 @@ export interface Props extends InputWrapClassProps, Record<string, any> {
31
32
  classInput?: string;
32
33
  style?: string;
33
34
  renderValue?: (rawValue: any) => string;
35
+ /** Semantic color intent of the switch */
36
+ intent?: SwitchIntent;
37
+ /**
38
+ * Size of the switch itself. Deliberately separate from `renderSize`, which sizes
39
+ * the InputWrap shell (label, description, spacing) around it.
40
+ */
41
+ switchSize?: "xs" | "sm" | "md" | "lg" | string;
42
+ /** Classes for the switch's toggle dot/knob element */
43
+ dotClass?: string;
44
+ /** Snippet to render inside the dot when checked */
45
+ on?: Snippet;
46
+ /** Snippet to render inside the dot when unchecked */
47
+ off?: Snippet;
48
+ /** Async validation before toggle - return false to prevent change */
49
+ preHook?: (current: boolean) => Promise<false | any>;
50
+ onclick?: (event: MouseEvent) => void;
34
51
  }
35
52
  import type { ValidationResult } from "../../actions/validate.svelte.js";
36
53
  declare const FieldSwitch: import("svelte").Component<Props, {
@@ -148,6 +148,38 @@ Component-specific targets (e.g. `classInput` for the inner `<input>`/`<select>`
148
148
  <FieldCheckbox label="I agree to the terms" bind:checked={agreed} required />
149
149
  ```
150
150
 
151
+ ### Switch
152
+
153
+ `FieldSwitch` wraps a [`Switch`](../Switch/README.md) in the standard field scaffolding
154
+ (label, description, validation box) and forwards the switch's own props to it.
155
+
156
+ ```svelte
157
+ <script lang="ts">
158
+ import { FieldSwitch } from "stuic";
159
+
160
+ let published = $state(false);
161
+ </script>
162
+
163
+ <FieldSwitch
164
+ label="Published"
165
+ description="Whether the public page resolves."
166
+ intent="success"
167
+ bind:checked={published}
168
+ />
169
+ ```
170
+
171
+ | Prop | Goes to |
172
+ | ------------------------------------------------------------------- | -------------------------------------------------------------------- |
173
+ | `intent`, `dotClass`, `on`, `off`, `preHook`, `onclick`, `tabindex` | the inner `<Switch>` |
174
+ | `classInput` | the inner `<Switch>`'s class |
175
+ | `switchSize` | the switch's own size (`"xs" \| "sm" \| "md" \| "lg"`, default `md`) |
176
+ | `renderSize` | the surrounding field shell only — it does **not** size the switch |
177
+ | everything else unrecognized (`...rest`) | the inner `<Switch>` |
178
+
179
+ The visible label names the control via `aria-labelledby` (the switch is announced as
180
+ `switch, on/off` with that name). Clicking the label text does not toggle — the switch's
181
+ own root is a `<label>`, so an HTML `for` association is not possible; click the switch.
182
+
151
183
  ### Input with Addons
152
184
 
153
185
  ```svelte
@@ -117,8 +117,13 @@
117
117
  )}
118
118
  >
119
119
  {#if label}
120
+ <!--
121
+ The `{id}-label` id is what controls that cannot be reached by `for`
122
+ (e.g. FieldSwitch's <Switch>) point their `aria-labelledby` at.
123
+ -->
120
124
  <label
121
125
  for={id}
126
+ id="{id}-label"
122
127
  class={twMerge(
123
128
  "block flex-1 px-2 mb-1 text-base",
124
129
  required && "after:content-['*'] after:opacity-40 after:pl-1",
@@ -10,7 +10,7 @@ A toggle switch component with size variants, semantic intents, keyboard support
10
10
  | `size` | `"sm" \| "md" \| "lg" \| "xl" \| string` | `"lg"` | Switch size |
11
11
  | `intent` | `"primary" \| "accent" \| "success" \| "warning" \| "destructive"` | - | Semantic color intent |
12
12
  | `name` | `string` | - | Form field name for hidden checkbox |
13
- | `label` | `string` | - | Screen reader label (visually hidden) |
13
+ | `label` | `string` | - | Accessible name (rendered as `aria-label`) |
14
14
  | `required` | `boolean` | `false` | Mark as required |
15
15
  | `disabled` | `boolean` | `false` | Disable toggle |
16
16
  | `tabindex` | `number` | `0` | Tab index |
@@ -142,3 +142,30 @@ A toggle switch component with size variants, semantic intents, keyboard support
142
142
 
143
143
  - **Space**: Toggle switch
144
144
  - **Enter**: Toggle switch
145
+
146
+ ## Accessibility
147
+
148
+ The interactive element is the switch track itself (the root `<label class="stuic-switch">`):
149
+ it is focusable, keyboard operable, and carries `role="switch"` plus the state - `aria-checked`,
150
+ and `aria-disabled` / `aria-required` / `aria-invalid` when they apply.
151
+
152
+ **Always give it a name.** `role="switch"` takes its accessible name from the author only, never
153
+ from its content, so a switch without one is announced as an unnamed control:
154
+
155
+ ```svelte
156
+ <!-- name it directly … -->
157
+ <Switch label="Published" bind:checked={published} />
158
+
159
+ <!-- … or point at existing visible text -->
160
+ <span id="pub-label">Published</span>
161
+ <Switch aria-labelledby="pub-label" bind:checked={published} />
162
+ ```
163
+
164
+ An explicit `aria-label` / `aria-labelledby` wins over the `label` prop.
165
+ [`FieldSwitch`](../Input/README.md#switch) wires its own visible label up automatically.
166
+
167
+ The inner `<input type="checkbox">` is the form value carrier only (submission, `name`,
168
+ `required`, native validation) and is `aria-hidden` so that the switch is the single announced
169
+ control. It is therefore **not** reachable via role-based queries - locate it structurally
170
+ (`container.querySelector('input[type="checkbox"]')`) and use `getByRole("switch")` for the
171
+ control itself.
@@ -19,7 +19,12 @@
19
19
  class?: string;
20
20
  /** Classes for the toggle dot/knob element */
21
21
  dotClass?: string;
22
- /** Screen reader label (visually hidden) */
22
+ /**
23
+ * Accessible name for the switch, rendered as `aria-label` on the interactive
24
+ * element. An explicit `aria-label`/`aria-labelledby` passed by the caller wins.
25
+ * Required unless the switch is named some other way - `role="switch"` takes its
26
+ * name from the author only, never from its content.
27
+ */
23
28
  label?: string;
24
29
  required?: boolean;
25
30
  disabled?: boolean;
@@ -125,11 +130,27 @@
125
130
  }
126
131
  </script>
127
132
 
133
+ <!--
134
+ The <label> is the interactive element here: it is focusable, keyboard operable and
135
+ carries the whole toggle interaction, so it - not the hidden checkbox - is what must
136
+ be announced. Hence `role="switch"` + the aria state below. The inner checkbox stays
137
+ as the form value carrier only (aria-hidden, see there).
138
+
139
+ Note that `role` on a <label> is not allowed by ARIA-in-HTML (the element maps to no
140
+ role); it is nevertheless honoured by browsers/AT and is the least invasive way to
141
+ name and announce this control without restructuring the markup.
142
+ -->
128
143
  <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
129
144
  <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
130
145
  <label
131
146
  bind:this={wrap}
132
147
  class={twMerge("stuic-switch m-2", _preset.size[size], classProp)}
148
+ role="switch"
149
+ aria-checked={!!checked}
150
+ aria-disabled={disabled || undefined}
151
+ aria-required={required || undefined}
152
+ aria-invalid={_validation && !_validation.valid ? "true" : undefined}
153
+ aria-label={label}
133
154
  data-checked={checked}
134
155
  data-disabled={disabled}
135
156
  data-intent={intent}
@@ -165,11 +186,18 @@
165
186
  {@render off?.()}
166
187
  {/if}
167
188
  </span>
189
+ <!--
190
+ Value carrier only: it holds the checked state for form submission and native
191
+ validation. `aria-hidden` keeps it out of the a11y tree so the switch above is
192
+ the single announced control (it is never focused - `tabindex="-1"` and every
193
+ focus() call targets the wrapper).
194
+ -->
168
195
  <input
169
196
  bind:checked
170
197
  bind:this={checkbox}
171
198
  type="checkbox"
172
199
  class="opacity-0 size-0"
200
+ aria-hidden="true"
173
201
  {disabled}
174
202
  {required}
175
203
  {name}
@@ -13,7 +13,12 @@ export interface Props extends Omit<HTMLLabelAttributes, "children" | "onchange"
13
13
  class?: string;
14
14
  /** Classes for the toggle dot/knob element */
15
15
  dotClass?: string;
16
- /** Screen reader label (visually hidden) */
16
+ /**
17
+ * Accessible name for the switch, rendered as `aria-label` on the interactive
18
+ * element. An explicit `aria-label`/`aria-labelledby` passed by the caller wins.
19
+ * Required unless the switch is named some other way - `role="switch"` takes its
20
+ * name from the author only, never from its content.
21
+ */
17
22
  label?: string;
18
23
  required?: boolean;
19
24
  disabled?: boolean;
package/dist/index.d.ts CHANGED
@@ -35,6 +35,7 @@ export * from "./components/Cart/index.js";
35
35
  export * from "./components/Card/index.js";
36
36
  export * from "./components/Carousel/index.js";
37
37
  export * from "./components/Checkout/index.js";
38
+ export * from "./components/Circle/index.js";
38
39
  export * from "./components/Collapsible/index.js";
39
40
  export * from "./components/ColorScheme/index.js";
40
41
  export * from "./components/CommandMenu/index.js";
package/dist/index.js CHANGED
@@ -36,6 +36,7 @@ export * from "./components/Cart/index.js";
36
36
  export * from "./components/Card/index.js";
37
37
  export * from "./components/Carousel/index.js";
38
38
  export * from "./components/Checkout/index.js";
39
+ export * from "./components/Circle/index.js";
39
40
  export * from "./components/Collapsible/index.js";
40
41
  export * from "./components/ColorScheme/index.js";
41
42
  export * from "./components/CommandMenu/index.js";
@@ -110,7 +110,7 @@ const search = debounce((query: string) => {
110
110
  | `colors` | Color manipulation |
111
111
  | `avatarColors` | Deterministic avatar colors |
112
112
  | `paint` | HSL color generation |
113
- | `svgCircle` | SVG circle path |
113
+ | `svgCircle` | SVG progress ring (DOM node) |
114
114
  | `oscillate` | Value oscillation for animation |
115
115
 
116
116
  ### Example: Class Merging
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "3.164.0",
3
+ "version": "3.166.0",
4
4
  "packageManager": "pnpm@11.5.0",
5
5
  "scripts": {
6
6
  "dev": "vite dev",