@dorsk/tsumikit 0.31.0 → 0.32.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.
@@ -0,0 +1,68 @@
1
+ <script lang="ts">
2
+ // Shimmering placeholder bar(s) standing in for content that is still
3
+ // loading. Decorative only — the surrounding region should carry aria-busy.
4
+ let {
5
+ width = '100%',
6
+ height = '1em',
7
+ lines = 1,
8
+ circle = false,
9
+ radius = 'var(--r-sm)',
10
+ class: klass = '',
11
+ ...rest
12
+ }: {
13
+ width?: string;
14
+ height?: string;
15
+ /** Stacked bars; the last one is shortened to read as a paragraph. */
16
+ lines?: number;
17
+ /** Round avatar placeholder — width follows height. */
18
+ circle?: boolean;
19
+ radius?: string;
20
+ class?: string;
21
+ [key: string]: unknown;
22
+ } = $props();
23
+
24
+ const w = $derived(circle ? height : width);
25
+ const r = $derived(circle ? '50%' : radius);
26
+ const count = $derived(Math.max(1, Math.floor(lines)));
27
+ </script>
28
+
29
+ <span class="skeleton {klass}" data-tsu="Skeleton" aria-hidden="true" style:width={w} {...rest}>
30
+ {#each { length: count } as _, i (i)}
31
+ <span
32
+ class="skeleton-bar"
33
+ style:height
34
+ style:border-radius={r}
35
+ style:width={count > 1 && i === count - 1 ? '60%' : undefined}
36
+ ></span>
37
+ {/each}
38
+ </span>
39
+
40
+ <style>
41
+ .skeleton {
42
+ display: flex;
43
+ flex-direction: column;
44
+ gap: var(--sp-2);
45
+ max-width: 100%;
46
+ }
47
+ .skeleton-bar {
48
+ display: block;
49
+ width: 100%;
50
+ background: linear-gradient(90deg, var(--bg-elevated-2) 25%, var(--bg-elevated) 50%, var(--bg-elevated-2) 75%);
51
+ background-size: 200% 100%;
52
+ animation: tsu-shimmer 1.4s ease-in-out infinite;
53
+ }
54
+ @keyframes tsu-shimmer {
55
+ from {
56
+ background-position: 200% 0;
57
+ }
58
+ to {
59
+ background-position: -200% 0;
60
+ }
61
+ }
62
+ @media (prefers-reduced-motion: reduce) {
63
+ .skeleton-bar {
64
+ animation: none;
65
+ background: var(--bg-elevated-2);
66
+ }
67
+ }
68
+ </style>
@@ -0,0 +1,14 @@
1
+ type $$ComponentProps = {
2
+ width?: string;
3
+ height?: string;
4
+ /** Stacked bars; the last one is shortened to read as a paragraph. */
5
+ lines?: number;
6
+ /** Round avatar placeholder — width follows height. */
7
+ circle?: boolean;
8
+ radius?: string;
9
+ class?: string;
10
+ [key: string]: unknown;
11
+ };
12
+ declare const Skeleton: import("svelte").Component<$$ComponentProps, {}, "">;
13
+ type Skeleton = ReturnType<typeof Skeleton>;
14
+ export default Skeleton;
@@ -7,10 +7,13 @@
7
7
  // • a muted, max-width-limited description
8
8
  // • an optional action area (slot via `action`, or a built-in button when
9
9
  // `actionLabel`+`onAction` are passed)
10
- // Everything is token-driven. `compact` tightens spacing for inline/table use.
10
+ // Everything is token-driven. `size` picks the layout: `default`, `compact`
11
+ // (tighter spacing), or `inline` (a single muted line). `loading` swaps the
12
+ // chip for a spinner and marks the root busy.
11
13
  import type { Snippet } from 'svelte';
12
14
  import Button from '../atoms/Button.svelte';
13
15
  import Icon, { type IconName } from '../atoms/Icon.svelte';
16
+ import Spinner from '../atoms/Spinner.svelte';
14
17
 
15
18
  let {
16
19
  title,
@@ -22,8 +25,10 @@
22
25
  // richer (multiple buttons, links) use the `action` snippet instead.
23
26
  actionLabel,
24
27
  onAction,
25
- // Tighter padding/sizing for inline use (empty table body, narrow panels).
28
+ // Legacy alias for size="compact".
26
29
  compact = false,
30
+ size = 'default',
31
+ loading = false,
27
32
  class: klass = '',
28
33
  // Raw SVG markup for a custom glyph — passed straight to `Icon`.
29
34
  iconChildren,
@@ -31,22 +36,38 @@
31
36
  action,
32
37
  ...rest
33
38
  }: {
34
- title: string;
35
- description?: string;
39
+ title?: string;
40
+ description?: string | Snippet;
36
41
  icon?: IconName;
37
42
  tone?: 'neutral' | 'ok' | 'warn' | 'danger' | 'info';
38
43
  actionLabel?: string;
39
44
  onAction?: () => void;
40
45
  compact?: boolean;
46
+ size?: 'inline' | 'compact' | 'default';
47
+ loading?: boolean;
41
48
  class?: string;
42
49
  iconChildren?: Snippet;
43
50
  action?: Snippet;
44
51
  [key: string]: unknown;
45
52
  } = $props();
53
+
54
+ const sz = $derived(compact ? 'compact' : size);
46
55
  </script>
47
56
 
48
- <div class="empty empty-{tone} {compact ? 'empty-compact' : ''} {klass}" data-tsu="EmptyState" {...rest}>
49
- {#if icon || iconChildren}
57
+ <div
58
+ class="empty empty-{tone} {klass}"
59
+ class:empty-compact={sz === 'compact'}
60
+ class:empty-inline={sz === 'inline'}
61
+ class:empty-loading={loading}
62
+ aria-busy={loading ? 'true' : undefined}
63
+ data-tsu="EmptyState"
64
+ {...rest}
65
+ >
66
+ {#if loading}
67
+ <span class="empty-chip">
68
+ <Spinner />
69
+ </span>
70
+ {:else if (icon || iconChildren) && sz !== 'inline'}
50
71
  <span class="empty-chip" aria-hidden="true">
51
72
  {#if iconChildren}
52
73
  <Icon>{@render iconChildren()}</Icon>
@@ -55,8 +76,12 @@
55
76
  {/if}
56
77
  </span>
57
78
  {/if}
58
- <p class="empty-title">{title}</p>
59
- {#if description}<p class="empty-desc">{description}</p>{/if}
79
+ {#if title}<p class="empty-title">{title}</p>{/if}
80
+ {#if description}
81
+ <p class="empty-desc">
82
+ {#if typeof description === 'function'}{@render description()}{:else}{description}{/if}
83
+ </p>
84
+ {/if}
60
85
  {#if action}
61
86
  <div class="empty-action">{@render action()}</div>
62
87
  {:else if actionLabel && onAction}
@@ -113,6 +138,30 @@
113
138
  height: 2.25rem;
114
139
  font-size: 1.125rem;
115
140
  }
141
+ .empty-inline {
142
+ flex-direction: row;
143
+ justify-content: center;
144
+ gap: var(--sp-2);
145
+ padding: var(--sp-6) var(--sp-4);
146
+ }
147
+ .empty-inline .empty-chip {
148
+ width: auto;
149
+ height: auto;
150
+ font-size: 1em;
151
+ background: none;
152
+ border: none;
153
+ }
154
+ .empty-inline .empty-title {
155
+ color: var(--text-muted);
156
+ font-weight: var(--fw-normal);
157
+ font-size: var(--fs-sm);
158
+ }
159
+ .empty-inline .empty-desc {
160
+ max-width: none;
161
+ }
162
+ .empty-inline .empty-action {
163
+ margin-top: 0;
164
+ }
116
165
  .empty-title {
117
166
  margin: 0;
118
167
  font-size: var(--fs-md);
@@ -1,13 +1,15 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import { type IconName } from '../atoms/Icon.svelte';
3
3
  type $$ComponentProps = {
4
- title: string;
5
- description?: string;
4
+ title?: string;
5
+ description?: string | Snippet;
6
6
  icon?: IconName;
7
7
  tone?: 'neutral' | 'ok' | 'warn' | 'danger' | 'info';
8
8
  actionLabel?: string;
9
9
  onAction?: () => void;
10
10
  compact?: boolean;
11
+ size?: 'inline' | 'compact' | 'default';
12
+ loading?: boolean;
11
13
  class?: string;
12
14
  iconChildren?: Snippet;
13
15
  action?: Snippet;
package/dist/index.d.ts CHANGED
@@ -17,6 +17,7 @@ export { default as Progress } from './components/atoms/Progress.svelte';
17
17
  export { default as Scrim } from './components/atoms/Scrim.svelte';
18
18
  export { default as SegmentedProgress, type ProgressSegment, } from './components/atoms/SegmentedProgress.svelte';
19
19
  export { default as Select } from './components/atoms/Select.svelte';
20
+ export { default as Skeleton } from './components/atoms/Skeleton.svelte';
20
21
  export { default as Slider } from './components/atoms/Slider.svelte';
21
22
  export { default as Spinner } from './components/atoms/Spinner.svelte';
22
23
  export { default as Switch } from './components/atoms/Switch.svelte';
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ export { default as Progress } from './components/atoms/Progress.svelte';
22
22
  export { default as Scrim } from './components/atoms/Scrim.svelte';
23
23
  export { default as SegmentedProgress, } from './components/atoms/SegmentedProgress.svelte';
24
24
  export { default as Select } from './components/atoms/Select.svelte';
25
+ export { default as Skeleton } from './components/atoms/Skeleton.svelte';
25
26
  export { default as Slider } from './components/atoms/Slider.svelte';
26
27
  export { default as Spinner } from './components/atoms/Spinner.svelte';
27
28
  export { default as Switch } from './components/atoms/Switch.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorsk/tsumikit",
3
- "version": "0.31.0",
3
+ "version": "0.32.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",