@dorsk/tsumikit 0.30.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.
- package/dist/components/atoms/Input.svelte +5 -1
- package/dist/components/atoms/Input.svelte.d.ts +2 -0
- package/dist/components/atoms/Kbd.svelte +96 -0
- package/dist/components/atoms/Kbd.svelte.d.ts +8 -0
- package/dist/components/atoms/Select.svelte +38 -4
- package/dist/components/atoms/Select.svelte.d.ts +5 -1
- package/dist/components/atoms/Skeleton.svelte +68 -0
- package/dist/components/atoms/Skeleton.svelte.d.ts +14 -0
- package/dist/components/atoms/Textarea.svelte +34 -0
- package/dist/components/atoms/Textarea.svelte.d.ts +5 -0
- package/dist/components/molecules/EmptyState.svelte +57 -8
- package/dist/components/molecules/EmptyState.svelte.d.ts +4 -2
- package/dist/components/molecules/Field.svelte +22 -5
- package/dist/components/molecules/Field.svelte.d.ts +4 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
width?: string;
|
|
30
30
|
/** Fires with the current value on Enter. */
|
|
31
31
|
onenter?: (value: string) => void;
|
|
32
|
+
/** Alias of `onenter`; both fire when given. */
|
|
33
|
+
onsubmit?: (value: string) => void;
|
|
32
34
|
class?: string;
|
|
33
35
|
value?: HTMLInputAttributes['value'];
|
|
34
36
|
el?: HTMLInputElement | null;
|
|
@@ -46,6 +48,7 @@
|
|
|
46
48
|
shape = 'square',
|
|
47
49
|
width,
|
|
48
50
|
onenter,
|
|
51
|
+
onsubmit,
|
|
49
52
|
onkeydown,
|
|
50
53
|
class: klass = '',
|
|
51
54
|
value = $bindable(),
|
|
@@ -58,6 +61,7 @@
|
|
|
58
61
|
function handleKeydown(e: KeyboardEvent & { currentTarget: EventTarget & HTMLInputElement }) {
|
|
59
62
|
onkeydown?.(e);
|
|
60
63
|
if (onenter && e.key === 'Enter' && !e.defaultPrevented) onenter(String(value ?? ''));
|
|
64
|
+
if (onsubmit && e.key === 'Enter' && !e.defaultPrevented) onsubmit(String(value ?? ''));
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
function clear() {
|
|
@@ -82,7 +86,7 @@
|
|
|
82
86
|
style:width={wrapped ? undefined : width}
|
|
83
87
|
bind:value
|
|
84
88
|
{...rest}
|
|
85
|
-
onkeydown={onenter || onkeydown ? handleKeydown : undefined}
|
|
89
|
+
onkeydown={onenter || onsubmit || onkeydown ? handleKeydown : undefined}
|
|
86
90
|
aria-invalid={invalid || undefined}
|
|
87
91
|
/>
|
|
88
92
|
{/snippet}
|
|
@@ -20,6 +20,8 @@ type Props = Omit<HTMLInputAttributes, 'size'> & {
|
|
|
20
20
|
width?: string;
|
|
21
21
|
/** Fires with the current value on Enter. */
|
|
22
22
|
onenter?: (value: string) => void;
|
|
23
|
+
/** Alias of `onenter`; both fire when given. */
|
|
24
|
+
onsubmit?: (value: string) => void;
|
|
23
25
|
class?: string;
|
|
24
26
|
value?: HTMLInputAttributes['value'];
|
|
25
27
|
el?: HTMLInputElement | null;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Keyboard shortcut chip: `keys="mod+enter"` (or an array of tokens) renders
|
|
3
|
+
// one <kbd> per key with platform-aware glyphs — `mod` is ⌘ on Mac, Ctrl
|
|
4
|
+
// elsewhere (Ctrl during SSR).
|
|
5
|
+
type Props = {
|
|
6
|
+
keys: string | string[];
|
|
7
|
+
size?: 'sm' | 'md';
|
|
8
|
+
class?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
let { keys, size = 'sm', class: klass = '' }: Props = $props();
|
|
12
|
+
|
|
13
|
+
const isMac =
|
|
14
|
+
typeof navigator !== 'undefined' &&
|
|
15
|
+
/mac|iphone|ipad|ipod/i.test(navigator.platform || navigator.userAgent || '');
|
|
16
|
+
|
|
17
|
+
function label(token: string): string {
|
|
18
|
+
const t = token.trim().toLowerCase();
|
|
19
|
+
switch (t) {
|
|
20
|
+
case 'mod':
|
|
21
|
+
return isMac ? '⌘' : 'Ctrl';
|
|
22
|
+
case 'ctrl':
|
|
23
|
+
case 'control':
|
|
24
|
+
return 'Ctrl';
|
|
25
|
+
case 'meta':
|
|
26
|
+
case 'cmd':
|
|
27
|
+
case 'command':
|
|
28
|
+
return '⌘';
|
|
29
|
+
case 'alt':
|
|
30
|
+
case 'option':
|
|
31
|
+
return isMac ? '⌥' : 'Alt';
|
|
32
|
+
case 'shift':
|
|
33
|
+
return '⇧';
|
|
34
|
+
case 'enter':
|
|
35
|
+
case 'return':
|
|
36
|
+
return '↵';
|
|
37
|
+
case 'esc':
|
|
38
|
+
case 'escape':
|
|
39
|
+
return 'Esc';
|
|
40
|
+
case 'tab':
|
|
41
|
+
return 'Tab';
|
|
42
|
+
case 'space':
|
|
43
|
+
return 'Space';
|
|
44
|
+
case 'up':
|
|
45
|
+
return '↑';
|
|
46
|
+
case 'down':
|
|
47
|
+
return '↓';
|
|
48
|
+
case 'left':
|
|
49
|
+
return '←';
|
|
50
|
+
case 'right':
|
|
51
|
+
return '→';
|
|
52
|
+
default:
|
|
53
|
+
return t.charAt(0).toUpperCase() + t.slice(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const tokens = $derived(
|
|
58
|
+
(Array.isArray(keys) ? keys : keys.split('+')).filter((k) => k.trim() !== '').map(label)
|
|
59
|
+
);
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<span data-tsu="Kbd" class="kbd-group {klass}" class:kbd-md={size === 'md'}>
|
|
63
|
+
{#each tokens as token, i (i)}
|
|
64
|
+
{#if i > 0}<span class="kbd-sep" aria-hidden="true">+</span>{/if}
|
|
65
|
+
<kbd class="kbd">{token}</kbd>
|
|
66
|
+
{/each}
|
|
67
|
+
</span>
|
|
68
|
+
|
|
69
|
+
<style>
|
|
70
|
+
.kbd-group {
|
|
71
|
+
display: inline-flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
gap: 2px;
|
|
74
|
+
font-size: var(--fs-xs);
|
|
75
|
+
color: var(--text-muted);
|
|
76
|
+
vertical-align: baseline;
|
|
77
|
+
}
|
|
78
|
+
.kbd-md {
|
|
79
|
+
font-size: var(--fs-sm);
|
|
80
|
+
}
|
|
81
|
+
.kbd {
|
|
82
|
+
font-family: var(--font-mono);
|
|
83
|
+
font-size: inherit;
|
|
84
|
+
line-height: 1.4;
|
|
85
|
+
padding: 0 var(--sp-1);
|
|
86
|
+
border: 1px solid var(--border-strong);
|
|
87
|
+
border-bottom-width: 2px;
|
|
88
|
+
border-radius: var(--r-sm);
|
|
89
|
+
background: var(--bg-elevated-2);
|
|
90
|
+
color: inherit;
|
|
91
|
+
}
|
|
92
|
+
.kbd-sep {
|
|
93
|
+
font-size: 0.85em;
|
|
94
|
+
color: var(--text-faint);
|
|
95
|
+
}
|
|
96
|
+
</style>
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
// transparently — for the "native select overlaid on a custom trigger" pattern
|
|
12
12
|
// (SelectButton): the platform popup with no custom outside-click logic, while
|
|
13
13
|
// a styled trigger shows through underneath.
|
|
14
|
+
//
|
|
15
|
+
// `variant="embedded"` is a borderless, elevated-surface select for inline
|
|
16
|
+
// use inside cards/toolbars; it hides the chevron unless asked for explicitly.
|
|
14
17
|
import type { Snippet } from 'svelte';
|
|
15
18
|
import type { HTMLSelectAttributes } from 'svelte/elements';
|
|
16
19
|
import Icon from './Icon.svelte';
|
|
@@ -18,7 +21,7 @@
|
|
|
18
21
|
// `size` shadows the native option-count attribute (unused in token layouts) to
|
|
19
22
|
// expose the sm|md height scale instead.
|
|
20
23
|
type Props = Omit<HTMLSelectAttributes, 'size'> & {
|
|
21
|
-
variant?: 'default' | 'ghost';
|
|
24
|
+
variant?: 'default' | 'ghost' | 'embedded';
|
|
22
25
|
/** Error state: danger border + aria-invalid. */
|
|
23
26
|
invalid?: boolean;
|
|
24
27
|
/** Compact inline form: smaller padding + font for dense toolbars/headers.
|
|
@@ -29,6 +32,10 @@
|
|
|
29
32
|
size?: 'sm' | 'md';
|
|
30
33
|
/** Draw the custom chevron (default). Set false for a bare inline select. */
|
|
31
34
|
chevron?: boolean;
|
|
35
|
+
/** `auto` sizes to the selected option instead of filling the row. */
|
|
36
|
+
width?: 'full' | 'auto';
|
|
37
|
+
/** Fill the available width of a flex/Cluster row (flex: 1). */
|
|
38
|
+
grow?: boolean;
|
|
32
39
|
class?: string;
|
|
33
40
|
value?: HTMLSelectAttributes['value'];
|
|
34
41
|
children?: Snippet;
|
|
@@ -39,7 +46,9 @@
|
|
|
39
46
|
invalid = false,
|
|
40
47
|
compact = false,
|
|
41
48
|
size = 'md',
|
|
42
|
-
chevron
|
|
49
|
+
chevron,
|
|
50
|
+
width = 'full',
|
|
51
|
+
grow = false,
|
|
43
52
|
class: klass = '',
|
|
44
53
|
value = $bindable(),
|
|
45
54
|
children,
|
|
@@ -47,6 +56,7 @@
|
|
|
47
56
|
}: Props = $props();
|
|
48
57
|
|
|
49
58
|
const small = $derived(compact || size === 'sm');
|
|
59
|
+
const showChevron = $derived(chevron ?? variant !== 'embedded');
|
|
50
60
|
</script>
|
|
51
61
|
|
|
52
62
|
{#if variant === 'ghost'}
|
|
@@ -54,18 +64,26 @@
|
|
|
54
64
|
{@render children?.()}
|
|
55
65
|
</select>
|
|
56
66
|
{:else}
|
|
57
|
-
<div
|
|
67
|
+
<div
|
|
68
|
+
class="select-wrap {klass}"
|
|
69
|
+
class:no-chevron={!showChevron}
|
|
70
|
+
class:w-auto={width === 'auto'}
|
|
71
|
+
class:select-grow={grow}
|
|
72
|
+
data-tsu="Select"
|
|
73
|
+
>
|
|
58
74
|
<select
|
|
59
75
|
class="select"
|
|
60
76
|
class:compact={small}
|
|
61
77
|
class:select-sm={size === 'sm'}
|
|
78
|
+
class:w-auto={width === 'auto'}
|
|
79
|
+
class:embedded={variant === 'embedded'}
|
|
62
80
|
bind:value
|
|
63
81
|
{...rest}
|
|
64
82
|
aria-invalid={invalid || undefined}
|
|
65
83
|
>
|
|
66
84
|
{@render children?.()}
|
|
67
85
|
</select>
|
|
68
|
-
{#if
|
|
86
|
+
{#if showChevron}
|
|
69
87
|
<span class="select-chevron" aria-hidden="true">
|
|
70
88
|
<Icon name="chevron-down" size={16} />
|
|
71
89
|
</span>
|
|
@@ -79,6 +97,14 @@
|
|
|
79
97
|
display: block;
|
|
80
98
|
width: 100%;
|
|
81
99
|
}
|
|
100
|
+
.select-wrap.w-auto {
|
|
101
|
+
display: inline-block;
|
|
102
|
+
width: auto;
|
|
103
|
+
}
|
|
104
|
+
.select-wrap.select-grow {
|
|
105
|
+
flex: 1 1 0;
|
|
106
|
+
min-width: 0;
|
|
107
|
+
}
|
|
82
108
|
.select {
|
|
83
109
|
width: 100%;
|
|
84
110
|
padding: var(--sp-3);
|
|
@@ -111,6 +137,14 @@
|
|
|
111
137
|
.select.select-sm {
|
|
112
138
|
height: var(--control-height-compact);
|
|
113
139
|
}
|
|
140
|
+
.select.w-auto {
|
|
141
|
+
width: auto;
|
|
142
|
+
}
|
|
143
|
+
.select.embedded {
|
|
144
|
+
background: var(--bg-elevated-2);
|
|
145
|
+
border: none;
|
|
146
|
+
border-radius: var(--r-sm);
|
|
147
|
+
}
|
|
114
148
|
.select:focus {
|
|
115
149
|
outline: none;
|
|
116
150
|
border-color: var(--accent);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { HTMLSelectAttributes } from 'svelte/elements';
|
|
3
3
|
type Props = Omit<HTMLSelectAttributes, 'size'> & {
|
|
4
|
-
variant?: 'default' | 'ghost';
|
|
4
|
+
variant?: 'default' | 'ghost' | 'embedded';
|
|
5
5
|
/** Error state: danger border + aria-invalid. */
|
|
6
6
|
invalid?: boolean;
|
|
7
7
|
/** Compact inline form: smaller padding + font for dense toolbars/headers.
|
|
@@ -12,6 +12,10 @@ type Props = Omit<HTMLSelectAttributes, 'size'> & {
|
|
|
12
12
|
size?: 'sm' | 'md';
|
|
13
13
|
/** Draw the custom chevron (default). Set false for a bare inline select. */
|
|
14
14
|
chevron?: boolean;
|
|
15
|
+
/** `auto` sizes to the selected option instead of filling the row. */
|
|
16
|
+
width?: 'full' | 'auto';
|
|
17
|
+
/** Fill the available width of a flex/Cluster row (flex: 1). */
|
|
18
|
+
grow?: boolean;
|
|
15
19
|
class?: string;
|
|
16
20
|
value?: HTMLSelectAttributes['value'];
|
|
17
21
|
children?: Snippet;
|
|
@@ -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;
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
// *floor* (min-height) rather than a fixed height: the textarea still grows
|
|
15
15
|
// with content, but dragging up reserves extra space. (A bottom handle makes
|
|
16
16
|
// no sense alongside content-driven sizing, so it's suppressed.)
|
|
17
|
+
//
|
|
18
|
+
// `onsubmit` fires with the value on Enter (`submitOn="enter"`, Shift+Enter
|
|
19
|
+
// still inserts a newline) or Ctrl/Meta+Enter (`submitOn="mod-enter"`, the
|
|
20
|
+
// default whenever `onsubmit` is given).
|
|
17
21
|
import type { HTMLTextareaAttributes } from 'svelte/elements';
|
|
18
22
|
import { autoresize as autoresizeAction } from '../../autoresize';
|
|
19
23
|
|
|
@@ -28,6 +32,11 @@
|
|
|
28
32
|
/** Error state: danger border + aria-invalid (also styles if a consumer
|
|
29
33
|
* sets aria-invalid directly). */
|
|
30
34
|
invalid?: boolean;
|
|
35
|
+
/** Cap the height (with autoresize: stop growing and scroll past this). */
|
|
36
|
+
maxHeight?: string;
|
|
37
|
+
/** Fires with the current value on the `submitOn` key combo. */
|
|
38
|
+
onsubmit?: (value: string) => void;
|
|
39
|
+
submitOn?: 'enter' | 'mod-enter' | 'none';
|
|
31
40
|
class?: string;
|
|
32
41
|
value?: HTMLTextareaAttributes['value'];
|
|
33
42
|
el?: HTMLTextAreaElement | null;
|
|
@@ -39,6 +48,10 @@
|
|
|
39
48
|
size = 'md',
|
|
40
49
|
resize = 'bottom',
|
|
41
50
|
invalid = false,
|
|
51
|
+
maxHeight,
|
|
52
|
+
onsubmit,
|
|
53
|
+
submitOn = 'none',
|
|
54
|
+
onkeydown,
|
|
42
55
|
class: klass = '',
|
|
43
56
|
value = $bindable(),
|
|
44
57
|
el = $bindable(null),
|
|
@@ -48,6 +61,18 @@
|
|
|
48
61
|
// With autoresize, only the top handle (a min-height floor) is meaningful.
|
|
49
62
|
const handleEdge = $derived(autoresize ? (resize === 'top' ? 'top' : 'none') : resize);
|
|
50
63
|
const showHandle = $derived(handleEdge !== 'none');
|
|
64
|
+
const submitMode = $derived(submitOn === 'none' && onsubmit ? 'mod-enter' : submitOn);
|
|
65
|
+
|
|
66
|
+
function handleKeydown(e: KeyboardEvent & { currentTarget: EventTarget & HTMLTextAreaElement }) {
|
|
67
|
+
onkeydown?.(e);
|
|
68
|
+
if (!onsubmit || e.key !== 'Enter' || e.defaultPrevented || submitMode === 'none') return;
|
|
69
|
+
const mod = e.ctrlKey || e.metaKey;
|
|
70
|
+
const submits =
|
|
71
|
+
submitMode === 'enter' ? !e.shiftKey && !mod && !e.altKey : mod && !e.shiftKey && !e.altKey;
|
|
72
|
+
if (!submits) return;
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
onsubmit(String(value ?? ''));
|
|
75
|
+
}
|
|
51
76
|
|
|
52
77
|
// --- manual resize drag (mirrors AppShell/Modal: rAF-throttled pointer drag) ---
|
|
53
78
|
let dragging = $state(false);
|
|
@@ -106,9 +131,12 @@
|
|
|
106
131
|
class="textarea {klass}"
|
|
107
132
|
class:mono
|
|
108
133
|
class:textarea-sm={size === 'sm'}
|
|
134
|
+
class:capped={!!maxHeight}
|
|
135
|
+
style:max-height={maxHeight}
|
|
109
136
|
bind:value
|
|
110
137
|
use:autoresizeAction={typeof value === 'string' ? value : ''}
|
|
111
138
|
{...rest}
|
|
139
|
+
onkeydown={onsubmit || onkeydown ? handleKeydown : undefined}
|
|
112
140
|
aria-invalid={invalid || undefined}
|
|
113
141
|
></textarea>
|
|
114
142
|
{:else}
|
|
@@ -117,8 +145,11 @@
|
|
|
117
145
|
class="textarea {klass}"
|
|
118
146
|
class:mono
|
|
119
147
|
class:textarea-sm={size === 'sm'}
|
|
148
|
+
class:capped={!!maxHeight}
|
|
149
|
+
style:max-height={maxHeight}
|
|
120
150
|
bind:value
|
|
121
151
|
{...rest}
|
|
152
|
+
onkeydown={onsubmit || onkeydown ? handleKeydown : undefined}
|
|
122
153
|
aria-invalid={invalid || undefined}
|
|
123
154
|
></textarea>
|
|
124
155
|
{/if}
|
|
@@ -165,6 +196,9 @@
|
|
|
165
196
|
outline: none;
|
|
166
197
|
border-color: var(--accent);
|
|
167
198
|
}
|
|
199
|
+
.textarea.capped {
|
|
200
|
+
overflow-y: auto;
|
|
201
|
+
}
|
|
168
202
|
.textarea-sm {
|
|
169
203
|
padding: var(--sp-1) var(--sp-2);
|
|
170
204
|
font-size: var(--fs-sm);
|
|
@@ -10,6 +10,11 @@ type Props = HTMLTextareaAttributes & {
|
|
|
10
10
|
/** Error state: danger border + aria-invalid (also styles if a consumer
|
|
11
11
|
* sets aria-invalid directly). */
|
|
12
12
|
invalid?: boolean;
|
|
13
|
+
/** Cap the height (with autoresize: stop growing and scroll past this). */
|
|
14
|
+
maxHeight?: string;
|
|
15
|
+
/** Fires with the current value on the `submitOn` key combo. */
|
|
16
|
+
onsubmit?: (value: string) => void;
|
|
17
|
+
submitOn?: 'enter' | 'mod-enter' | 'none';
|
|
13
18
|
class?: string;
|
|
14
19
|
value?: HTMLTextareaAttributes['value'];
|
|
15
20
|
el?: HTMLTextAreaElement | null;
|
|
@@ -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;
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// error text. Replaces the ad-hoc `<div class="field"><label class="label">`
|
|
4
4
|
// markup. Renders a real <label for> when `for` is given (associates with the
|
|
5
5
|
// control), else a plain <span> for control groups (radio/segment rows).
|
|
6
|
+
// `layout="inline"` puts the label beside the control (fixed `labelWidth`
|
|
7
|
+
// aligns a column of them); hint/error then follow the control on the row.
|
|
6
8
|
import type { Snippet } from 'svelte';
|
|
7
9
|
|
|
8
10
|
let {
|
|
@@ -10,14 +12,19 @@
|
|
|
10
12
|
for: forId,
|
|
11
13
|
hint,
|
|
12
14
|
error,
|
|
15
|
+
layout = 'stack',
|
|
16
|
+
labelWidth,
|
|
13
17
|
grow = false,
|
|
14
18
|
class: klass = '',
|
|
15
19
|
children
|
|
16
20
|
}: {
|
|
17
21
|
label?: string;
|
|
18
22
|
for?: string;
|
|
19
|
-
hint?: string;
|
|
23
|
+
hint?: string | Snippet;
|
|
20
24
|
error?: string;
|
|
25
|
+
layout?: 'stack' | 'inline';
|
|
26
|
+
/** Inline layout: fixed label column width. */
|
|
27
|
+
labelWidth?: string;
|
|
21
28
|
/** Fill the available width of a flex/Cluster row (flex: 1). */
|
|
22
29
|
grow?: boolean;
|
|
23
30
|
class?: string;
|
|
@@ -25,16 +32,18 @@
|
|
|
25
32
|
} = $props();
|
|
26
33
|
</script>
|
|
27
34
|
|
|
28
|
-
<div class="field {klass}" class:field-grow={grow} data-tsu="Field">
|
|
35
|
+
<div class="field {klass}" class:field-grow={grow} class:field-inline={layout === 'inline'} data-tsu="Field">
|
|
29
36
|
{#if label}
|
|
30
37
|
{#if forId}
|
|
31
|
-
<label class="label" for={forId}>{label}</label>
|
|
38
|
+
<label class="label" for={forId} style:width={labelWidth}>{label}</label>
|
|
32
39
|
{:else}
|
|
33
|
-
<span class="label">{label}</span>
|
|
40
|
+
<span class="label" style:width={labelWidth}>{label}</span>
|
|
34
41
|
{/if}
|
|
35
42
|
{/if}
|
|
36
43
|
{@render children?.()}
|
|
37
|
-
{#if hint}
|
|
44
|
+
{#if hint}
|
|
45
|
+
<span class="hint">{#if typeof hint === 'function'}{@render hint()}{:else}{hint}{/if}</span>
|
|
46
|
+
{/if}
|
|
38
47
|
{#if error}<span class="error">{error}</span>{/if}
|
|
39
48
|
</div>
|
|
40
49
|
|
|
@@ -48,6 +57,14 @@
|
|
|
48
57
|
flex: 1 1 0;
|
|
49
58
|
min-width: 0;
|
|
50
59
|
}
|
|
60
|
+
.field-inline {
|
|
61
|
+
flex-direction: row;
|
|
62
|
+
align-items: center;
|
|
63
|
+
gap: var(--sp-2);
|
|
64
|
+
}
|
|
65
|
+
.field-inline .label {
|
|
66
|
+
flex: none;
|
|
67
|
+
}
|
|
51
68
|
.label {
|
|
52
69
|
font-size: var(--fs-sm);
|
|
53
70
|
font-weight: var(--fw-medium);
|
|
@@ -2,8 +2,11 @@ import type { Snippet } from 'svelte';
|
|
|
2
2
|
type $$ComponentProps = {
|
|
3
3
|
label?: string;
|
|
4
4
|
for?: string;
|
|
5
|
-
hint?: string;
|
|
5
|
+
hint?: string | Snippet;
|
|
6
6
|
error?: string;
|
|
7
|
+
layout?: 'stack' | 'inline';
|
|
8
|
+
/** Inline layout: fixed label column width. */
|
|
9
|
+
labelWidth?: string;
|
|
7
10
|
/** Fill the available width of a flex/Cluster row (flex: 1). */
|
|
8
11
|
grow?: boolean;
|
|
9
12
|
class?: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -11,11 +11,13 @@ export { default as Heading } from './components/atoms/Heading.svelte';
|
|
|
11
11
|
export type { IconName } from './components/atoms/Icon.svelte';
|
|
12
12
|
export { default as Icon } from './components/atoms/Icon.svelte';
|
|
13
13
|
export { default as Input } from './components/atoms/Input.svelte';
|
|
14
|
+
export { default as Kbd } from './components/atoms/Kbd.svelte';
|
|
14
15
|
export { default as Link } from './components/atoms/Link.svelte';
|
|
15
16
|
export { default as Progress } from './components/atoms/Progress.svelte';
|
|
16
17
|
export { default as Scrim } from './components/atoms/Scrim.svelte';
|
|
17
18
|
export { default as SegmentedProgress, type ProgressSegment, } from './components/atoms/SegmentedProgress.svelte';
|
|
18
19
|
export { default as Select } from './components/atoms/Select.svelte';
|
|
20
|
+
export { default as Skeleton } from './components/atoms/Skeleton.svelte';
|
|
19
21
|
export { default as Slider } from './components/atoms/Slider.svelte';
|
|
20
22
|
export { default as Spinner } from './components/atoms/Spinner.svelte';
|
|
21
23
|
export { default as Switch } from './components/atoms/Switch.svelte';
|
package/dist/index.js
CHANGED
|
@@ -16,11 +16,13 @@ export { default as Dot } from './components/atoms/Dot.svelte';
|
|
|
16
16
|
export { default as Heading } from './components/atoms/Heading.svelte';
|
|
17
17
|
export { default as Icon } from './components/atoms/Icon.svelte';
|
|
18
18
|
export { default as Input } from './components/atoms/Input.svelte';
|
|
19
|
+
export { default as Kbd } from './components/atoms/Kbd.svelte';
|
|
19
20
|
export { default as Link } from './components/atoms/Link.svelte';
|
|
20
21
|
export { default as Progress } from './components/atoms/Progress.svelte';
|
|
21
22
|
export { default as Scrim } from './components/atoms/Scrim.svelte';
|
|
22
23
|
export { default as SegmentedProgress, } from './components/atoms/SegmentedProgress.svelte';
|
|
23
24
|
export { default as Select } from './components/atoms/Select.svelte';
|
|
25
|
+
export { default as Skeleton } from './components/atoms/Skeleton.svelte';
|
|
24
26
|
export { default as Slider } from './components/atoms/Slider.svelte';
|
|
25
27
|
export { default as Spinner } from './components/atoms/Spinner.svelte';
|
|
26
28
|
export { default as Switch } from './components/atoms/Switch.svelte';
|
package/package.json
CHANGED