@dorsk/tsumikit 0.19.1 → 0.20.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/README.md CHANGED
@@ -69,12 +69,12 @@ import { Button, Field, Input, Modal, ThemePicker } from '@dorsk/tsumikit';
69
69
  ## Components
70
70
 
71
71
  **Atoms:** Text, Heading, Button, Input, Textarea, Select, Switch, Checkbox,
72
- Slider, Progress, Card, Badge, Dot, Link, Icon (open registry — pass a
72
+ Slider, Progress, Card (`tone` tints the surface for inline banners), Badge, Dot, Link, Icon (open registry — pass a
73
73
  `children` snippet for any custom SVG).
74
74
 
75
75
  **Molecules:** Field, IconButton, SelectButton, Toggle, OptionButton, Modal,
76
76
  Popover, Menu, Tabs, RadioGroup, Tooltip, Accordion, CopyButton, FileButton,
77
- Dropzone, CodeBlock, Toaster, ThemePicker, FontScalePicker.
77
+ Dropzone, CodeBlock, Callout, EmptyState, Toaster, ThemePicker, FontScalePicker.
78
78
 
79
79
  **Organisms:** DataTable (generic `<T>`, typed columns + cell snippets).
80
80
 
@@ -84,6 +84,46 @@ sidebar on desktop, overlay drawer on mobile, optionally resizable), NavItem
84
84
  (vertical), Cluster (wrapping row), AutoGrid (intrinsically responsive columns —
85
85
  no media/container query needed).
86
86
 
87
+ ### Stacked distribution + legend
88
+
89
+ `SegmentedProgress mode="stacked"` turns the bar into one shared track whose slice
90
+ widths follow `value` (not `max`), fully filled, no gaps; a zero value collapses to
91
+ nothing. Pass a top-level `max` to show the remainder as empty track. `legend`
92
+ renders a dot + label + count per segment (`true`/`'below'` or `'inline'`), or takes
93
+ a snippet for custom rendering. In this mode the bar is `role="img"`, labelled from
94
+ the segments. `gap` (px or CSS length, default `2`) applies in segments mode, and
95
+ `tone: 'ok'` is an alias of `'success'`.
96
+
97
+ ```svelte
98
+ <SegmentedProgress
99
+ mode="stacked"
100
+ label="Analysis"
101
+ max={analyzedCount}
102
+ legend
103
+ segments={[
104
+ { value: 412, max: 0, tone: 'ok', label: 'conforming' },
105
+ { value: 12, max: 0, tone: 'warn', label: 'nonconforming' },
106
+ { value: 0, max: 0, tone: 'danger', label: 'blocked' }
107
+ ]}
108
+ />
109
+ ```
110
+
111
+ ### Tinted surfaces & Callout
112
+
113
+ `Card` takes `tone="neutral" | "ok" | "warn" | "danger" | "info"` (default
114
+ `neutral`, unchanged look) to tint its border and wash its background with the
115
+ semantic hue. `Callout` builds on it for inline messages: leading glyph (auto
116
+ per tone, or `icon`), optional `title`, body, right-aligned `actions` snippet,
117
+ `dismissible` + `ondismiss`, and `busy` to show a Spinner while work runs. It
118
+ is a live region — `role="status"`, or `role="alert"` when `tone="danger"`.
119
+
120
+ ```svelte
121
+ <Callout tone="danger" title="Search failed" dismissible ondismiss={clear}>
122
+ The provider returned 503.
123
+ {#snippet actions()}<Button size="sm" onclick={retry}>Retry</Button>{/snippet}
124
+ </Callout>
125
+ ```
126
+
87
127
  ## Container queries
88
128
 
89
129
  AppShell's `main` and `sidebar` are query containers (`container-name: main` /
@@ -4,6 +4,8 @@
4
4
  // interactive hover/active affordance for tappable list items (e.g. session
5
5
  // rows); `as` lets it be a button/anchor when the whole surface is clickable.
6
6
  // `padding` dials the inner spacing (none/sm/md/lg) for denser cards.
7
+ // `tone` tints the surface itself (border + faint background wash) with a
8
+ // semantic hue for inline banners; `neutral` is the plain card.
7
9
  //
8
10
  // `stacked` fakes a pile of cards by drawing two layers peeking out below
9
11
  // (and optionally to the right) via pseudo-elements. `stackTone` tints those
@@ -20,6 +22,7 @@
20
22
  as = 'div',
21
23
  padding = 'md',
22
24
  surface = 'base',
25
+ tone = 'neutral',
23
26
  stacked = false,
24
27
  stackTone = 'neutral',
25
28
  stackY = 8,
@@ -33,6 +36,7 @@
33
36
  as?: 'div' | 'button' | 'a' | 'li' | 'section' | 'form';
34
37
  padding?: 'none' | 'sm' | 'md' | 'lg';
35
38
  surface?: 'base' | 'raised' | 'sunken';
39
+ tone?: Tone;
36
40
  stacked?: boolean;
37
41
  stackTone?: Tone;
38
42
  stackY?: number;
@@ -58,6 +62,10 @@
58
62
  class:surface-raised={surface === 'raised'}
59
63
  class:surface-sunken={surface === 'sunken'}
60
64
  class:card-tap={tap}
65
+ class:card-ok={tone === 'ok'}
66
+ class:card-warn={tone === 'warn'}
67
+ class:card-danger={tone === 'danger'}
68
+ class:card-info={tone === 'info'}
61
69
  class:card-stacked={stacked}
62
70
  class:stack-ok={stacked && stackTone === 'ok'}
63
71
  class:stack-warn={stacked && stackTone === 'warn'}
@@ -107,6 +115,26 @@
107
115
  border-color: var(--border-strong);
108
116
  }
109
117
 
118
+ .card-ok {
119
+ --card-tone: var(--ok);
120
+ }
121
+ .card-warn {
122
+ --card-tone: var(--warn);
123
+ }
124
+ .card-danger {
125
+ --card-tone: var(--danger);
126
+ }
127
+ .card-info {
128
+ --card-tone: var(--info);
129
+ }
130
+ .card-ok,
131
+ .card-warn,
132
+ .card-danger,
133
+ .card-info {
134
+ border-color: color-mix(in srgb, var(--card-tone) 55%, var(--border));
135
+ background: color-mix(in srgb, var(--card-tone) 8%, var(--bg-elevated));
136
+ }
137
+
110
138
  /* Stacked effect — two back layers peeking out bottom-right. The front
111
139
  surface keeps its own background so the layers only show at the edges. */
112
140
  .card-stacked {
@@ -5,6 +5,7 @@ type $$ComponentProps = {
5
5
  as?: 'div' | 'button' | 'a' | 'li' | 'section' | 'form';
6
6
  padding?: 'none' | 'sm' | 'md' | 'lg';
7
7
  surface?: 'base' | 'raised' | 'sunken';
8
+ tone?: Tone;
8
9
  stacked?: boolean;
9
10
  stackTone?: Tone;
10
11
  stackY?: number;
@@ -1,67 +1,156 @@
1
1
  <script lang="ts" module>
2
- // One bar split into proportional segments (e.g. one per season of a series),
3
- // each filled value/max with its own tone. Token-styled, thin separators.
2
+ // One bar split into segments, each with its own tone. Two modes:
3
+ // segments (default): widths ∝ `max`, each filled value/max, thin gaps.
4
+ // • stacked: one shared track, widths ∝ `value` (Σvalue = full width, or
5
+ // `max` when given), every slice 100 % filled, no gaps; a zero value
6
+ // collapses to 0 width.
4
7
  export type ProgressSegment = {
5
8
  value: number;
9
+ // Ignored in `stacked` mode.
6
10
  max: number;
7
- // Fill colour per segment. `muted` renders a faint fill for empty parts.
8
- tone?: 'accent' | 'success' | 'warn' | 'danger' | 'muted';
9
- // Native tooltip / accessible name for the segment.
11
+ // Fill colour per segment. `ok` is an alias of `success`; `muted` renders a
12
+ // faint fill for empty parts.
13
+ tone?: 'accent' | 'success' | 'ok' | 'warn' | 'danger' | 'muted';
14
+ // Native tooltip / accessible name / legend caption for the segment.
10
15
  label?: string;
11
16
  };
17
+
18
+ export const TONE_FILL: Record<NonNullable<ProgressSegment['tone']>, string> = {
19
+ accent: 'var(--accent)',
20
+ success: 'var(--ok)',
21
+ ok: 'var(--ok)',
22
+ warn: 'var(--warn)',
23
+ danger: 'var(--danger)',
24
+ muted: 'var(--text-faint)',
25
+ };
12
26
  </script>
13
27
 
14
28
  <script lang="ts">
29
+ import type { Snippet } from 'svelte';
30
+ import Dot from './Dot.svelte';
31
+ import Text from './Text.svelte';
32
+
15
33
  let {
16
34
  segments,
17
35
  label,
18
36
  size = 'md',
19
- class: klass = ''
37
+ mode = 'segments',
38
+ gap = 2,
39
+ max,
40
+ legend = false,
41
+ class: klass = '',
20
42
  }: {
21
43
  segments: ProgressSegment[];
22
44
  label?: string;
23
45
  // Track height. `sm` is a thin ~5px track for inline rows.
24
46
  size?: 'sm' | 'md';
47
+ mode?: 'segments' | 'stacked';
48
+ // Space between segments in `segments` mode; a bare number is px.
49
+ gap?: number | string;
50
+ // `stacked` only: total the slices are measured against. When Σvalue < max
51
+ // the remainder shows the empty track.
52
+ max?: number;
53
+ // `true` = 'below'. A snippet receives the segments for custom rendering.
54
+ legend?: boolean | 'inline' | 'below' | Snippet<[ProgressSegment[]]>;
25
55
  class?: string;
26
56
  } = $props();
27
57
 
58
+ const stacked = $derived(mode === 'stacked');
28
59
  const totalMax = $derived(segments.reduce((s, seg) => s + Math.max(0, seg.max), 0));
29
60
  const totalValue = $derived(
30
- segments.reduce((s, seg) => s + Math.max(0, Math.min(seg.value, seg.max)), 0)
61
+ segments.reduce((s, seg) => s + Math.max(0, Math.min(seg.value, seg.max)), 0),
62
+ );
63
+ const stackedTotal = $derived(segments.reduce((s, seg) => s + Math.max(0, seg.value), 0));
64
+ const remainder = $derived(max === undefined ? 0 : Math.max(0, max - stackedTotal));
65
+ const gapCss = $derived(stacked ? '0' : typeof gap === 'number' ? `${gap}px` : gap);
66
+ const legendPlacement = $derived(
67
+ legend === true ? 'below' : legend === 'inline' || legend === 'below' ? legend : null,
68
+ );
69
+ const legendSnippet = $derived(typeof legend === 'function' ? legend : null);
70
+ const hasLegend = $derived(legendPlacement !== null || legendSnippet !== null);
71
+ const stackedLabel = $derived(
72
+ [label, segments.map((seg) => `${seg.label ?? seg.tone ?? 'accent'} ${seg.value}`).join(', ')]
73
+ .filter(Boolean)
74
+ .join(': '),
31
75
  );
32
76
 
33
77
  function pct(seg: ProgressSegment): number {
78
+ if (stacked) return 100;
34
79
  if (seg.max <= 0) return 0;
35
80
  return Math.max(0, Math.min(100, (seg.value / seg.max) * 100));
36
81
  }
82
+
83
+ function grow(seg: ProgressSegment): number {
84
+ return stacked ? Math.max(0, seg.value) : Math.max(seg.max, 1);
85
+ }
37
86
  </script>
38
87
 
39
- <div
40
- data-tsu="SegmentedProgress"
41
- class="segmented-progress size-{size} {klass}"
42
- role="progressbar"
43
- aria-label={label}
44
- aria-valuemin={0}
45
- aria-valuemax={totalMax}
46
- aria-valuenow={totalValue}
47
- >
48
- {#each segments as seg, i (i)}
88
+ {#snippet bar(rootClass: string)}
89
+ {#if stacked}
49
90
  <div
50
- class="segment tone-{seg.tone ?? 'accent'}"
51
- style="flex-grow: {Math.max(seg.max, 1)}"
52
- title={seg.label}
91
+ data-tsu="SegmentedProgress"
92
+ class="segmented-progress stacked size-{size} {rootClass}"
93
+ style="gap: {gapCss}"
94
+ role="img"
95
+ aria-label={stackedLabel}
53
96
  >
54
- <div class="bar" style="width: {pct(seg)}%"></div>
97
+ {#each segments as seg, i (i)}
98
+ <div class="segment tone-{seg.tone ?? 'accent'}" style="flex-grow: {grow(seg)}" title={seg.label}>
99
+ <div class="bar" style="width: {pct(seg)}%"></div>
100
+ </div>
101
+ {/each}
102
+ {#if remainder > 0}
103
+ <div class="segment remainder" style="flex-grow: {remainder}"></div>
104
+ {/if}
55
105
  </div>
56
- {/each}
57
- </div>
106
+ {:else}
107
+ <div
108
+ data-tsu="SegmentedProgress"
109
+ class="segmented-progress size-{size} {rootClass}"
110
+ style="gap: {gapCss}"
111
+ role="progressbar"
112
+ aria-label={label}
113
+ aria-valuemin={0}
114
+ aria-valuemax={totalMax}
115
+ aria-valuenow={totalValue}
116
+ >
117
+ {#each segments as seg, i (i)}
118
+ <div class="segment tone-{seg.tone ?? 'accent'}" style="flex-grow: {grow(seg)}" title={seg.label}>
119
+ <div class="bar" style="width: {pct(seg)}%"></div>
120
+ </div>
121
+ {/each}
122
+ </div>
123
+ {/if}
124
+ {/snippet}
125
+
126
+ {#if hasLegend}
127
+ <div data-tsu="SegmentedProgress" class="segmented-progress-wrap legend-{legendPlacement ?? 'below'} {klass}">
128
+ {@render bar('')}
129
+ {#if legendSnippet}
130
+ {@render legendSnippet(segments)}
131
+ {:else}
132
+ <ul class="legend">
133
+ {#each segments as seg, i (i)}
134
+ <li class="legend-item">
135
+ <Dot color={TONE_FILL[seg.tone ?? 'accent']} />
136
+ {#if seg.label}
137
+ <Text variant="caption">{seg.label}</Text>
138
+ {/if}
139
+ <Text variant="caption" weight="medium" numeric>{seg.value}</Text>
140
+ </li>
141
+ {/each}
142
+ </ul>
143
+ {/if}
144
+ </div>
145
+ {:else}
146
+ {@render bar(klass)}
147
+ {/if}
58
148
 
59
149
  <style>
60
150
  .segmented-progress {
61
151
  display: flex;
62
152
  width: 100%;
63
153
  height: 0.5rem;
64
- gap: 2px;
65
154
  border-radius: var(--r-pill);
66
155
  }
67
156
  .segmented-progress.size-sm {
@@ -87,7 +176,8 @@
87
176
  background: var(--fill, var(--accent));
88
177
  transition: width 0.2s var(--ease);
89
178
  }
90
- .tone-success {
179
+ .tone-success,
180
+ .tone-ok {
91
181
  --fill: var(--ok);
92
182
  }
93
183
  .tone-warn {
@@ -99,4 +189,44 @@
99
189
  .tone-muted {
100
190
  --fill: var(--text-faint);
101
191
  }
192
+
193
+ .stacked {
194
+ overflow: hidden;
195
+ background: var(--bg-elevated-2);
196
+ }
197
+ .stacked .segment {
198
+ min-width: 0;
199
+ border-radius: 0;
200
+ background: transparent;
201
+ transition: flex-grow 0.2s var(--ease);
202
+ }
203
+
204
+ .segmented-progress-wrap {
205
+ display: flex;
206
+ width: 100%;
207
+ gap: var(--sp-2);
208
+ }
209
+ .legend-below {
210
+ flex-direction: column;
211
+ }
212
+ .legend-inline {
213
+ align-items: center;
214
+ }
215
+ .legend-inline .segmented-progress {
216
+ flex: 1 1 0%;
217
+ width: auto;
218
+ }
219
+ .legend {
220
+ display: flex;
221
+ flex-wrap: wrap;
222
+ gap: var(--sp-1) var(--sp-3);
223
+ margin: 0;
224
+ padding: 0;
225
+ list-style: none;
226
+ }
227
+ .legend-item {
228
+ display: inline-flex;
229
+ align-items: center;
230
+ gap: var(--sp-1);
231
+ }
102
232
  </style>
@@ -1,13 +1,19 @@
1
1
  export type ProgressSegment = {
2
2
  value: number;
3
3
  max: number;
4
- tone?: 'accent' | 'success' | 'warn' | 'danger' | 'muted';
4
+ tone?: 'accent' | 'success' | 'ok' | 'warn' | 'danger' | 'muted';
5
5
  label?: string;
6
6
  };
7
+ export declare const TONE_FILL: Record<NonNullable<ProgressSegment['tone']>, string>;
8
+ import type { Snippet } from 'svelte';
7
9
  type $$ComponentProps = {
8
10
  segments: ProgressSegment[];
9
11
  label?: string;
10
12
  size?: 'sm' | 'md';
13
+ mode?: 'segments' | 'stacked';
14
+ gap?: number | string;
15
+ max?: number;
16
+ legend?: boolean | 'inline' | 'below' | Snippet<[ProgressSegment[]]>;
11
17
  class?: string;
12
18
  };
13
19
  declare const SegmentedProgress: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -0,0 +1,144 @@
1
+ <script lang="ts">
2
+ // Inline message banner — the one way to say "heads up" inside a page:
3
+ // tone-tinted Card with a leading glyph, optional title, body, right-aligned
4
+ // actions and an optional dismiss button. `busy` swaps the glyph for a
5
+ // Spinner (auto-search / long-running states). Announced as a live region:
6
+ // `status` normally, `alert` for danger so errors interrupt.
7
+ import type { Snippet } from 'svelte';
8
+ import Card from '../atoms/Card.svelte';
9
+ import Icon, { type IconName } from '../atoms/Icon.svelte';
10
+ import Spinner from '../atoms/Spinner.svelte';
11
+ import IconButton from './IconButton.svelte';
12
+
13
+ type Tone = 'neutral' | 'ok' | 'warn' | 'danger' | 'info';
14
+
15
+ const DEFAULT_ICON: Record<Tone, IconName> = {
16
+ neutral: 'info',
17
+ info: 'info',
18
+ ok: 'check',
19
+ warn: 'warning',
20
+ danger: 'warning'
21
+ };
22
+
23
+ let {
24
+ tone = 'info',
25
+ icon,
26
+ title,
27
+ dismissible = false,
28
+ dismissLabel = 'Dismiss',
29
+ ondismiss,
30
+ busy = false,
31
+ class: klass = '',
32
+ children,
33
+ actions,
34
+ ...rest
35
+ }: {
36
+ tone?: Tone;
37
+ icon?: IconName;
38
+ title?: string;
39
+ dismissible?: boolean;
40
+ dismissLabel?: string;
41
+ ondismiss?: () => void;
42
+ busy?: boolean;
43
+ class?: string;
44
+ children?: Snippet;
45
+ actions?: Snippet;
46
+ [key: string]: unknown;
47
+ } = $props();
48
+
49
+ let glyph = $derived(icon ?? DEFAULT_ICON[tone]);
50
+ </script>
51
+
52
+ <Card
53
+ {tone}
54
+ padding="none"
55
+ role={tone === 'danger' ? 'alert' : 'status'}
56
+ class="callout callout-{tone} {klass}"
57
+ data-tsu="Callout"
58
+ {...rest}
59
+ >
60
+ <span class="callout-icon" aria-hidden={busy ? undefined : true}>
61
+ {#if busy}
62
+ <Spinner />
63
+ {:else}
64
+ <Icon name={glyph} />
65
+ {/if}
66
+ </span>
67
+ <div class="callout-body">
68
+ {#if title}<p class="callout-title">{title}</p>{/if}
69
+ {#if children}<div class="callout-text">{@render children()}</div>{/if}
70
+ </div>
71
+ {#if actions}
72
+ <div class="callout-actions">{@render actions()}</div>
73
+ {/if}
74
+ {#if dismissible}
75
+ <span class="callout-dismiss">
76
+ <IconButton icon="x" inline label={dismissLabel} onclick={() => ondismiss?.()} />
77
+ </span>
78
+ {/if}
79
+ </Card>
80
+
81
+ <style>
82
+ :global(.card.callout) {
83
+ --callout-tone: var(--text-muted);
84
+ display: flex;
85
+ flex-wrap: wrap;
86
+ align-items: flex-start;
87
+ gap: var(--sp-3);
88
+ padding: var(--sp-3);
89
+ }
90
+ :global(.callout-ok) {
91
+ --callout-tone: var(--ok);
92
+ }
93
+ :global(.callout-warn) {
94
+ --callout-tone: var(--warn);
95
+ }
96
+ :global(.callout-danger) {
97
+ --callout-tone: var(--danger);
98
+ }
99
+ :global(.callout-info) {
100
+ --callout-tone: var(--info);
101
+ }
102
+
103
+ .callout-icon {
104
+ display: inline-flex;
105
+ flex: none;
106
+ align-items: center;
107
+ color: var(--callout-tone);
108
+ font-size: 1.125rem;
109
+ line-height: 1;
110
+ min-height: 1.4em;
111
+ }
112
+ .callout-body {
113
+ flex: 1 1 12rem;
114
+ min-width: 0;
115
+ display: flex;
116
+ flex-direction: column;
117
+ gap: var(--sp-1);
118
+ font-size: var(--fs-sm);
119
+ line-height: 1.5;
120
+ color: var(--text);
121
+ }
122
+ .callout-title {
123
+ margin: 0;
124
+ font-weight: var(--fw-semibold);
125
+ }
126
+ .callout-text {
127
+ overflow-wrap: anywhere;
128
+ }
129
+ .callout-actions {
130
+ display: flex;
131
+ flex-wrap: wrap;
132
+ align-items: center;
133
+ gap: var(--sp-2);
134
+ margin-left: auto;
135
+ }
136
+ .callout-dismiss {
137
+ display: inline-flex;
138
+ flex: none;
139
+ margin-left: auto;
140
+ }
141
+ .callout-actions + .callout-dismiss {
142
+ margin-left: 0;
143
+ }
144
+ </style>
@@ -0,0 +1,19 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { type IconName } from '../atoms/Icon.svelte';
3
+ type Tone = 'neutral' | 'ok' | 'warn' | 'danger' | 'info';
4
+ type $$ComponentProps = {
5
+ tone?: Tone;
6
+ icon?: IconName;
7
+ title?: string;
8
+ dismissible?: boolean;
9
+ dismissLabel?: string;
10
+ ondismiss?: () => void;
11
+ busy?: boolean;
12
+ class?: string;
13
+ children?: Snippet;
14
+ actions?: Snippet;
15
+ [key: string]: unknown;
16
+ };
17
+ declare const Callout: import("svelte").Component<$$ComponentProps, {}, "">;
18
+ type Callout = ReturnType<typeof Callout>;
19
+ export default Callout;
package/dist/index.d.ts CHANGED
@@ -28,6 +28,7 @@ export { default as ResizablePanel } from './components/layouts/ResizablePanel.s
28
28
  export { default as Stack } from './components/layouts/Stack.svelte';
29
29
  export { type AccordionItem, default as Accordion, } from './components/molecules/Accordion.svelte';
30
30
  export { type BreadcrumbItem, default as Breadcrumb, } from './components/molecules/Breadcrumb.svelte';
31
+ export { default as Callout } from './components/molecules/Callout.svelte';
31
32
  export { default as CodeBlock } from './components/molecules/CodeBlock.svelte';
32
33
  export { default as CopyButton } from './components/molecules/CopyButton.svelte';
33
34
  export { default as Dropzone } from './components/molecules/Dropzone.svelte';
package/dist/index.js CHANGED
@@ -35,6 +35,7 @@ export { default as ResizablePanel } from './components/layouts/ResizablePanel.s
35
35
  export { default as Stack } from './components/layouts/Stack.svelte';
36
36
  export { default as Accordion, } from './components/molecules/Accordion.svelte';
37
37
  export { default as Breadcrumb, } from './components/molecules/Breadcrumb.svelte';
38
+ export { default as Callout } from './components/molecules/Callout.svelte';
38
39
  export { default as CodeBlock } from './components/molecules/CodeBlock.svelte';
39
40
  export { default as CopyButton } from './components/molecules/CopyButton.svelte';
40
41
  export { default as Dropzone } from './components/molecules/Dropzone.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorsk/tsumikit",
3
- "version": "0.19.1",
3
+ "version": "0.20.0",
4
4
  "description": "Minimal, dependency-free Svelte 5 + pure-CSS UI kit. Token-driven atoms, molecules & layouts with theming out of the box.",
5
5
  "type": "module",
6
6
  "license": "MIT",