@dorsk/tsumikit 0.4.0 → 0.6.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.
@@ -1,12 +1,22 @@
1
1
  <script lang="ts">
2
2
  // Progress bar. Determinate when `value` is a number (0..max); omit `value`
3
- // for an indeterminate animation. Uses role="progressbar" with the right
4
- // aria-value* attributes. Token-styled.
3
+ // (or pass `indeterminate`) for an indeterminate animation. Uses
4
+ // role="progressbar" with the right aria-value* attributes. Token-styled.
5
+ //
6
+ // Variants (all token-driven, composable):
7
+ // size='sm' — thin track (~5px) for inline download/health rows.
8
+ // gradient — accent→teal fill (storage meter / library-health bars).
9
+ // striped — diagonal stripes; animated while in-flight (unpacking).
10
+ // indeterminate — explicit "scanning"/"importing" mode without a percentage.
5
11
  let {
6
12
  value,
7
13
  max = 100,
8
14
  label,
9
15
  tone = 'accent',
16
+ size = 'md',
17
+ gradient = false,
18
+ striped = false,
19
+ indeterminate: indeterminateProp = false,
10
20
  class: klass = ''
11
21
  }: {
12
22
  value?: number;
@@ -15,16 +25,26 @@
15
25
  // Fill colour. `accent` is the default brand fill; the semantic tones
16
26
  // retint the bar for severity (e.g. usage meters going warm/hot).
17
27
  tone?: 'accent' | 'success' | 'warn' | 'danger';
28
+ // Track height. `sm` is a thin ~5px track for inline rows.
29
+ size?: 'sm' | 'md';
30
+ // Accent→teal gradient fill (overrides the flat tone colour).
31
+ gradient?: boolean;
32
+ // Diagonal stripes; animated in indeterminate mode.
33
+ striped?: boolean;
34
+ // Force indeterminate mode even when a `value` is supplied.
35
+ indeterminate?: boolean;
18
36
  class?: string;
19
37
  } = $props();
20
38
 
21
39
  const pct = $derived(value == null ? 0 : Math.max(0, Math.min(100, (value / max) * 100)));
22
- const indeterminate = $derived(value == null);
40
+ const indeterminate = $derived(indeterminateProp || value == null);
23
41
  </script>
24
42
 
25
43
  <div
26
- class="progress tone-{tone} {klass}"
44
+ class="progress tone-{tone} size-{size} {klass}"
27
45
  class:indeterminate
46
+ class:gradient
47
+ class:striped
28
48
  role="progressbar"
29
49
  aria-label={label}
30
50
  aria-valuemin={indeterminate ? undefined : 0}
@@ -42,6 +62,9 @@
42
62
  border-radius: var(--r-pill);
43
63
  overflow: hidden;
44
64
  }
65
+ .progress.size-sm {
66
+ height: 0.3125rem;
67
+ }
45
68
  .bar {
46
69
  height: 100%;
47
70
  background: var(--fill, var(--accent));
@@ -57,11 +80,52 @@
57
80
  .tone-danger {
58
81
  --fill: var(--danger);
59
82
  }
83
+ .progress.gradient .bar {
84
+ background: linear-gradient(90deg, var(--fill, var(--accent)), var(--c-teal));
85
+ }
86
+ .progress.striped .bar {
87
+ background-image: linear-gradient(
88
+ 45deg,
89
+ rgba(255, 255, 255, 0.18) 25%,
90
+ transparent 25%,
91
+ transparent 50%,
92
+ rgba(255, 255, 255, 0.18) 50%,
93
+ rgba(255, 255, 255, 0.18) 75%,
94
+ transparent 75%,
95
+ transparent
96
+ );
97
+ background-size: 1rem 1rem;
98
+ }
99
+ .progress.gradient.striped .bar {
100
+ background-image:
101
+ linear-gradient(
102
+ 45deg,
103
+ rgba(255, 255, 255, 0.18) 25%,
104
+ transparent 25%,
105
+ transparent 50%,
106
+ rgba(255, 255, 255, 0.18) 50%,
107
+ rgba(255, 255, 255, 0.18) 75%,
108
+ transparent 75%,
109
+ transparent
110
+ ),
111
+ linear-gradient(90deg, var(--fill, var(--accent)), var(--c-teal));
112
+ background-size:
113
+ 1rem 1rem,
114
+ 100% 100%;
115
+ }
60
116
  .progress.indeterminate .bar {
61
117
  width: 40%;
62
- animation: progress-slide 1.1s ease-in-out infinite;
118
+ animation: kt-progress-slide 1.1s ease-in-out infinite;
63
119
  }
64
- @keyframes progress-slide {
120
+ .progress.striped .bar {
121
+ animation: kt-stripe 0.6s linear infinite;
122
+ }
123
+ .progress.striped.indeterminate .bar {
124
+ animation:
125
+ kt-progress-slide 1.1s ease-in-out infinite,
126
+ kt-stripe 0.6s linear infinite;
127
+ }
128
+ @keyframes kt-progress-slide {
65
129
  0% {
66
130
  transform: translateX(-110%);
67
131
  }
@@ -69,4 +133,19 @@
69
133
  transform: translateX(310%);
70
134
  }
71
135
  }
136
+ @keyframes kt-stripe {
137
+ 0% {
138
+ background-position: 0 0;
139
+ }
140
+ 100% {
141
+ background-position: 1rem 0;
142
+ }
143
+ }
144
+ @media (prefers-reduced-motion: reduce) {
145
+ .progress.indeterminate .bar,
146
+ .progress.striped .bar,
147
+ .progress.striped.indeterminate .bar {
148
+ animation: none;
149
+ }
150
+ }
72
151
  </style>
@@ -3,6 +3,10 @@ type $$ComponentProps = {
3
3
  max?: number;
4
4
  label?: string;
5
5
  tone?: 'accent' | 'success' | 'warn' | 'danger';
6
+ size?: 'sm' | 'md';
7
+ gradient?: boolean;
8
+ striped?: boolean;
9
+ indeterminate?: boolean;
6
10
  class?: string;
7
11
  };
8
12
  declare const Progress: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -0,0 +1,138 @@
1
+ <script lang="ts">
2
+ // KPI / stat tile — the dashboard's repeating "one number" card. Composes on
3
+ // top of `Card` and lays out four parts:
4
+ // • an uppercase, letter-spaced micro-label (top-left)
5
+ // • a tinted icon chip (top-right) when `icon` is set
6
+ // • a large MONO value with an optional small unit baseline-aligned after it
7
+ // • a faint sub-line under the value (delta / context)
8
+ // `tone` tints the icon chip (and, when set, the value) with a semantic hue;
9
+ // `neutral` keeps the chip on the plain accent. Everything is token-driven.
10
+ import type { Snippet } from 'svelte';
11
+ import Card from '../atoms/Card.svelte';
12
+ import Icon, { type IconName } from '../atoms/Icon.svelte';
13
+
14
+ type Tone = 'neutral' | 'ok' | 'warn' | 'danger' | 'info';
15
+
16
+ let {
17
+ label,
18
+ value,
19
+ unit,
20
+ sub,
21
+ icon,
22
+ tone = 'neutral',
23
+ // Tint the value itself with the tone (not just the chip).
24
+ tintValue = false,
25
+ class: klass = '',
26
+ // Raw SVG markup for a custom icon — passed through to `Icon` so any glyph
27
+ // outside the registry can fill the chip.
28
+ iconChildren,
29
+ ...rest
30
+ }: {
31
+ label: string;
32
+ value: string | number;
33
+ unit?: string;
34
+ sub?: string;
35
+ icon?: IconName;
36
+ tone?: Tone;
37
+ tintValue?: boolean;
38
+ class?: string;
39
+ iconChildren?: Snippet;
40
+ [key: string]: unknown;
41
+ } = $props();
42
+ </script>
43
+
44
+ <Card
45
+ class="metric metric-{tone} {tintValue ? 'metric-tint' : ''} {klass}"
46
+ {...rest}
47
+ >
48
+ <div class="metric-head">
49
+ <span class="metric-label">{label}</span>
50
+ {#if icon || iconChildren}
51
+ <span class="metric-chip" aria-hidden="true">
52
+ <Icon name={icon}>{@render iconChildren?.()}</Icon>
53
+ </span>
54
+ {/if}
55
+ </div>
56
+ <div class="metric-value">
57
+ <span class="metric-num">{value}</span>
58
+ {#if unit}<span class="metric-unit">{unit}</span>{/if}
59
+ </div>
60
+ {#if sub}<div class="metric-sub">{sub}</div>{/if}
61
+ </Card>
62
+
63
+ <style>
64
+ :global(.metric) {
65
+ /* The tone hue the chip / value pull from; neutral falls back to accent. */
66
+ --metric-tone: var(--accent);
67
+ display: flex;
68
+ flex-direction: column;
69
+ gap: var(--sp-2);
70
+ }
71
+ :global(.metric-ok) {
72
+ --metric-tone: var(--ok);
73
+ }
74
+ :global(.metric-warn) {
75
+ --metric-tone: var(--warn);
76
+ }
77
+ :global(.metric-danger) {
78
+ --metric-tone: var(--danger);
79
+ }
80
+ :global(.metric-info) {
81
+ --metric-tone: var(--info);
82
+ }
83
+
84
+ .metric-head {
85
+ display: flex;
86
+ align-items: flex-start;
87
+ justify-content: space-between;
88
+ gap: var(--sp-2);
89
+ }
90
+ .metric-label {
91
+ font-size: var(--fs-xs);
92
+ font-weight: var(--fw-semibold);
93
+ text-transform: uppercase;
94
+ letter-spacing: 0.06em;
95
+ color: var(--text-muted);
96
+ font-family: var(--font-mono);
97
+ line-height: 1.4;
98
+ }
99
+ .metric-chip {
100
+ display: inline-flex;
101
+ align-items: center;
102
+ justify-content: center;
103
+ flex: none;
104
+ width: 1.75rem;
105
+ height: 1.75rem;
106
+ border-radius: var(--r-md);
107
+ font-size: 1rem;
108
+ color: var(--metric-tone);
109
+ background: color-mix(in srgb, var(--metric-tone) 14%, transparent);
110
+ border: 1px solid color-mix(in srgb, var(--metric-tone) 30%, transparent);
111
+ }
112
+ .metric-value {
113
+ display: flex;
114
+ align-items: baseline;
115
+ gap: var(--sp-1);
116
+ font-family: var(--font-mono);
117
+ line-height: 1.1;
118
+ }
119
+ .metric-num {
120
+ font-size: var(--fs-2xl);
121
+ font-weight: var(--fw-semibold);
122
+ color: var(--text);
123
+ font-variant-numeric: tabular-nums;
124
+ }
125
+ :global(.metric-tint) .metric-num {
126
+ color: var(--metric-tone);
127
+ }
128
+ .metric-unit {
129
+ font-size: var(--fs-sm);
130
+ font-weight: var(--fw-normal);
131
+ color: var(--text-muted);
132
+ }
133
+ .metric-sub {
134
+ font-size: var(--fs-xs);
135
+ color: var(--text-faint);
136
+ line-height: 1.4;
137
+ }
138
+ </style>
@@ -0,0 +1,18 @@
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
+ label: string;
6
+ value: string | number;
7
+ unit?: string;
8
+ sub?: string;
9
+ icon?: IconName;
10
+ tone?: Tone;
11
+ tintValue?: boolean;
12
+ class?: string;
13
+ iconChildren?: Snippet;
14
+ [key: string]: unknown;
15
+ };
16
+ declare const Metric: import("svelte").Component<$$ComponentProps, {}, "">;
17
+ type Metric = ReturnType<typeof Metric>;
18
+ export default Metric;
package/dist/index.d.ts CHANGED
@@ -32,6 +32,7 @@ export { default as FileButton } from './components/molecules/FileButton.svelte'
32
32
  export { default as FontScalePicker } from './components/molecules/FontScalePicker.svelte';
33
33
  export { default as IconButton } from './components/molecules/IconButton.svelte';
34
34
  export { default as Menu, type MenuItem } from './components/molecules/Menu.svelte';
35
+ export { default as Metric, default as StatTile, } from './components/molecules/Metric.svelte';
35
36
  export { default as Modal } from './components/molecules/Modal.svelte';
36
37
  export { default as OptionButton } from './components/molecules/OptionButton.svelte';
37
38
  export { default as Popover } from './components/molecules/Popover.svelte';
package/dist/index.js CHANGED
@@ -40,6 +40,9 @@ export { default as FileButton } from './components/molecules/FileButton.svelte'
40
40
  export { default as FontScalePicker } from './components/molecules/FontScalePicker.svelte';
41
41
  export { default as IconButton } from './components/molecules/IconButton.svelte';
42
42
  export { default as Menu } from './components/molecules/Menu.svelte';
43
+ export { default as Metric,
44
+ // StatTile is an alias for Metric — same component, dashboard-friendly name.
45
+ default as StatTile, } from './components/molecules/Metric.svelte';
43
46
  export { default as Modal } from './components/molecules/Modal.svelte';
44
47
  export { default as OptionButton } from './components/molecules/OptionButton.svelte';
45
48
  export { default as Popover } from './components/molecules/Popover.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorsk/tsumikit",
3
- "version": "0.4.0",
3
+ "version": "0.6.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",