@dorsk/tsumikit 0.31.0 → 0.33.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/dist/components/atoms/Skeleton.svelte +68 -0
- package/dist/components/atoms/Skeleton.svelte.d.ts +14 -0
- package/dist/components/molecules/EmptyState.svelte +57 -8
- package/dist/components/molecules/EmptyState.svelte.d.ts +4 -2
- package/dist/components/molecules/Menu.svelte +55 -4
- package/dist/components/molecules/Menu.svelte.d.ts +5 -2
- package/dist/components/molecules/SegmentedControl.svelte +46 -1
- package/dist/components/molecules/SegmentedControl.svelte.d.ts +5 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -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. `
|
|
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
|
-
//
|
|
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
|
|
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
|
|
49
|
-
{
|
|
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}
|
|
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
|
|
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;
|
|
@@ -14,21 +14,52 @@
|
|
|
14
14
|
// click). Selecting an item runs its action and closes the menu. Focus moves
|
|
15
15
|
// to the first item when the menu opens. Escape / click-outside close it
|
|
16
16
|
// (inherited from the native popover).
|
|
17
|
-
import type { Snippet } from 'svelte';
|
|
17
|
+
import type { ComponentProps, Snippet } from 'svelte';
|
|
18
18
|
import Popover from './Popover.svelte';
|
|
19
19
|
import Icon from '../atoms/Icon.svelte';
|
|
20
20
|
|
|
21
|
+
type PopoverProps = ComponentProps<typeof Popover>;
|
|
22
|
+
type TriggerChrome = Pick<
|
|
23
|
+
PopoverProps,
|
|
24
|
+
| 'variant'
|
|
25
|
+
| 'tone'
|
|
26
|
+
| 'size'
|
|
27
|
+
| 'box'
|
|
28
|
+
| 'control'
|
|
29
|
+
| 'block'
|
|
30
|
+
| 'triggerClass'
|
|
31
|
+
| 'bare'
|
|
32
|
+
| 'hitArea'
|
|
33
|
+
| 'disabled'
|
|
34
|
+
| 'gap'
|
|
35
|
+
| 'onopen'
|
|
36
|
+
| 'onclose'
|
|
37
|
+
>;
|
|
38
|
+
|
|
21
39
|
let {
|
|
22
40
|
label,
|
|
23
41
|
items,
|
|
24
42
|
trigger,
|
|
25
|
-
placement = 'bottom-start'
|
|
43
|
+
placement = 'bottom-start',
|
|
44
|
+
variant,
|
|
45
|
+
tone,
|
|
46
|
+
size,
|
|
47
|
+
box,
|
|
48
|
+
control,
|
|
49
|
+
block,
|
|
50
|
+
triggerClass,
|
|
51
|
+
bare,
|
|
52
|
+
hitArea,
|
|
53
|
+
disabled,
|
|
54
|
+
gap,
|
|
55
|
+
onopen,
|
|
56
|
+
onclose
|
|
26
57
|
}: {
|
|
27
58
|
label: string;
|
|
28
59
|
items: MenuItem[];
|
|
29
60
|
trigger: Snippet;
|
|
30
61
|
placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
|
|
31
|
-
} = $props();
|
|
62
|
+
} & TriggerChrome = $props();
|
|
32
63
|
|
|
33
64
|
let listEl = $state<HTMLDivElement | null>(null);
|
|
34
65
|
|
|
@@ -66,7 +97,27 @@
|
|
|
66
97
|
}
|
|
67
98
|
</script>
|
|
68
99
|
|
|
69
|
-
<Popover
|
|
100
|
+
<Popover
|
|
101
|
+
{label}
|
|
102
|
+
{placement}
|
|
103
|
+
{trigger}
|
|
104
|
+
{variant}
|
|
105
|
+
{tone}
|
|
106
|
+
{size}
|
|
107
|
+
{box}
|
|
108
|
+
{control}
|
|
109
|
+
{block}
|
|
110
|
+
{triggerClass}
|
|
111
|
+
{bare}
|
|
112
|
+
{hitArea}
|
|
113
|
+
{disabled}
|
|
114
|
+
{gap}
|
|
115
|
+
{onclose}
|
|
116
|
+
onopen={() => {
|
|
117
|
+
queueMicrotask(() => focusAt(0));
|
|
118
|
+
onopen?.();
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
70
121
|
<div bind:this={listEl} role="menu" aria-label={label} class="menu" data-tsu="Menu" tabindex="-1" {onkeydown}>
|
|
71
122
|
{#each items as item (item.label)}
|
|
72
123
|
<button
|
|
@@ -5,13 +5,16 @@ export interface MenuItem {
|
|
|
5
5
|
danger?: boolean;
|
|
6
6
|
disabled?: boolean;
|
|
7
7
|
}
|
|
8
|
-
import type { Snippet } from 'svelte';
|
|
8
|
+
import type { ComponentProps, Snippet } from 'svelte';
|
|
9
|
+
import Popover from './Popover.svelte';
|
|
10
|
+
type PopoverProps = ComponentProps<typeof Popover>;
|
|
11
|
+
type TriggerChrome = Pick<PopoverProps, 'variant' | 'tone' | 'size' | 'box' | 'control' | 'block' | 'triggerClass' | 'bare' | 'hitArea' | 'disabled' | 'gap' | 'onopen' | 'onclose'>;
|
|
9
12
|
type $$ComponentProps = {
|
|
10
13
|
label: string;
|
|
11
14
|
items: MenuItem[];
|
|
12
15
|
trigger: Snippet;
|
|
13
16
|
placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
|
|
14
|
-
};
|
|
17
|
+
} & TriggerChrome;
|
|
15
18
|
declare const Menu: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
16
19
|
type Menu = ReturnType<typeof Menu>;
|
|
17
20
|
export default Menu;
|
|
@@ -35,7 +35,11 @@
|
|
|
35
35
|
label = 'Options',
|
|
36
36
|
// `mobile` hides segment labels below the mobile breakpoint, collapsing
|
|
37
37
|
// labelled segments to centered icon-only squares (aria-label preserved).
|
|
38
|
+
// `container` does the same when the nearest size container is at most
|
|
39
|
+
// 35rem wide (the breakpoint is fixed: `@container` cannot read a var).
|
|
38
40
|
collapseLabels = 'never',
|
|
41
|
+
scroll = false,
|
|
42
|
+
block = false,
|
|
39
43
|
class: klass = '',
|
|
40
44
|
// Custom rendering per option (overrides the built-in label/count/icon).
|
|
41
45
|
option: optionSnippet
|
|
@@ -48,7 +52,11 @@
|
|
|
48
52
|
* `control`) so the whole segmented control height-matches its siblings. */
|
|
49
53
|
control?: boolean;
|
|
50
54
|
label?: string;
|
|
51
|
-
collapseLabels?: 'never' | 'mobile';
|
|
55
|
+
collapseLabels?: 'never' | 'mobile' | 'container';
|
|
56
|
+
/** Single non-wrapping row that scrolls horizontally (scrollbar hidden). */
|
|
57
|
+
scroll?: boolean;
|
|
58
|
+
/** Fill the parent width, sharing it equally between segments. */
|
|
59
|
+
block?: boolean;
|
|
52
60
|
class?: string;
|
|
53
61
|
option?: Snippet<[SegmentOption, boolean]>;
|
|
54
62
|
} = $props();
|
|
@@ -90,6 +98,9 @@
|
|
|
90
98
|
class="seg seg-{variant} seg-{size} {klass}"
|
|
91
99
|
class:seg-control={control}
|
|
92
100
|
class:seg-collapse-mobile={collapseLabels === 'mobile'}
|
|
101
|
+
class:collapse-container={collapseLabels === 'container'}
|
|
102
|
+
class:seg-scroll={scroll}
|
|
103
|
+
class:seg-block={block}
|
|
93
104
|
data-tsu="SegmentedControl"
|
|
94
105
|
{onkeydown}
|
|
95
106
|
>
|
|
@@ -132,6 +143,28 @@
|
|
|
132
143
|
.seg-icon {
|
|
133
144
|
border-radius: var(--r-md);
|
|
134
145
|
}
|
|
146
|
+
.seg-scroll {
|
|
147
|
+
flex: 1 0 100%;
|
|
148
|
+
flex-wrap: nowrap;
|
|
149
|
+
max-width: 100%;
|
|
150
|
+
overflow-x: auto;
|
|
151
|
+
scrollbar-width: none;
|
|
152
|
+
-webkit-overflow-scrolling: touch;
|
|
153
|
+
}
|
|
154
|
+
.seg-scroll::-webkit-scrollbar {
|
|
155
|
+
display: none;
|
|
156
|
+
}
|
|
157
|
+
.seg-scroll .seg-item {
|
|
158
|
+
flex: none;
|
|
159
|
+
}
|
|
160
|
+
.seg-block {
|
|
161
|
+
display: flex;
|
|
162
|
+
width: 100%;
|
|
163
|
+
}
|
|
164
|
+
.seg-block .seg-item {
|
|
165
|
+
flex: 1 1 0;
|
|
166
|
+
justify-content: center;
|
|
167
|
+
}
|
|
135
168
|
|
|
136
169
|
.seg-item {
|
|
137
170
|
display: inline-flex;
|
|
@@ -209,6 +242,18 @@
|
|
|
209
242
|
padding: 0.25rem;
|
|
210
243
|
}
|
|
211
244
|
}
|
|
245
|
+
@container (max-width: 35rem) {
|
|
246
|
+
.collapse-container .seg-item.has-label .seg-label {
|
|
247
|
+
display: none;
|
|
248
|
+
}
|
|
249
|
+
.collapse-container .seg-item.has-label {
|
|
250
|
+
gap: 0;
|
|
251
|
+
padding: 0.35rem;
|
|
252
|
+
}
|
|
253
|
+
.collapse-container.seg-sm .seg-item.has-label {
|
|
254
|
+
padding: 0.25rem;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
212
257
|
|
|
213
258
|
.seg-item:hover:not(:disabled):not(.selected) {
|
|
214
259
|
color: var(--text);
|
|
@@ -19,7 +19,11 @@ type $$ComponentProps = {
|
|
|
19
19
|
* `control`) so the whole segmented control height-matches its siblings. */
|
|
20
20
|
control?: boolean;
|
|
21
21
|
label?: string;
|
|
22
|
-
collapseLabels?: 'never' | 'mobile';
|
|
22
|
+
collapseLabels?: 'never' | 'mobile' | 'container';
|
|
23
|
+
/** Single non-wrapping row that scrolls horizontally (scrollbar hidden). */
|
|
24
|
+
scroll?: boolean;
|
|
25
|
+
/** Fill the parent width, sharing it equally between segments. */
|
|
26
|
+
block?: boolean;
|
|
23
27
|
class?: string;
|
|
24
28
|
option?: Snippet<[SegmentOption, boolean]>;
|
|
25
29
|
};
|
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