@dorsk/tsumikit 0.38.0 → 0.39.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/autoresize.d.ts +2 -0
- package/dist/autoresize.js +15 -1
- package/dist/components/atoms/Artwork.svelte +19 -18
- package/dist/components/atoms/Artwork.svelte.d.ts +3 -2
- package/dist/components/atoms/Badge.svelte +60 -26
- package/dist/components/atoms/Badge.svelte.d.ts +14 -3
- package/dist/components/atoms/Button.svelte +30 -12
- package/dist/components/atoms/Button.svelte.d.ts +4 -2
- package/dist/components/atoms/Card.svelte +47 -30
- package/dist/components/atoms/Card.svelte.d.ts +12 -3
- package/dist/components/atoms/Checkbox.svelte +16 -2
- package/dist/components/atoms/Checkbox.svelte.d.ts +2 -0
- package/dist/components/atoms/Dot.svelte +17 -11
- package/dist/components/atoms/Dot.svelte.d.ts +5 -2
- package/dist/components/atoms/Gauge.svelte +19 -18
- package/dist/components/atoms/Gauge.svelte.d.ts +3 -2
- package/dist/components/atoms/Heading.svelte +20 -19
- package/dist/components/atoms/Heading.svelte.d.ts +3 -2
- package/dist/components/atoms/Icon.svelte +33 -13
- package/dist/components/atoms/Icon.svelte.d.ts +17 -2
- package/dist/components/atoms/Input.svelte +17 -3
- package/dist/components/atoms/Input.svelte.d.ts +2 -1
- package/dist/components/atoms/Kbd.svelte +25 -3
- package/dist/components/atoms/Kbd.svelte.d.ts +5 -1
- package/dist/components/atoms/Link.svelte +16 -12
- package/dist/components/atoms/Link.svelte.d.ts +6 -2
- package/dist/components/atoms/Progress.svelte +119 -4
- package/dist/components/atoms/Progress.svelte.d.ts +28 -2
- package/dist/components/atoms/Scrim.svelte +7 -2
- package/dist/components/atoms/Scrim.svelte.d.ts +2 -0
- package/dist/components/atoms/SegmentedProgress.svelte +14 -2
- package/dist/components/atoms/SegmentedProgress.svelte.d.ts +5 -2
- package/dist/components/atoms/Select.svelte +18 -6
- package/dist/components/atoms/Select.svelte.d.ts +3 -3
- package/dist/components/atoms/Skeleton.svelte +12 -11
- package/dist/components/atoms/Skeleton.svelte.d.ts +3 -2
- package/dist/components/atoms/Slider.svelte +45 -3
- package/dist/components/atoms/Slider.svelte.d.ts +7 -0
- package/dist/components/atoms/Spinner.svelte +2 -0
- package/dist/components/atoms/Spinner.svelte.d.ts +2 -0
- package/dist/components/atoms/Switch.svelte +70 -18
- package/dist/components/atoms/Switch.svelte.d.ts +5 -1
- package/dist/components/atoms/Text.svelte +39 -26
- package/dist/components/atoms/Text.svelte.d.ts +7 -3
- package/dist/components/atoms/Textarea.svelte +47 -10
- package/dist/components/atoms/Textarea.svelte.d.ts +10 -3
- package/dist/components/layouts/AppShell.svelte +7 -3
- package/dist/components/layouts/AppShell.svelte.d.ts +2 -0
- package/dist/components/layouts/AutoGrid.svelte +4 -0
- package/dist/components/layouts/AutoGrid.svelte.d.ts +2 -0
- package/dist/components/layouts/Cluster.svelte +62 -1
- package/dist/components/layouts/Cluster.svelte.d.ts +9 -0
- package/dist/components/layouts/NavItem.svelte +22 -1
- package/dist/components/layouts/NavItem.svelte.d.ts +5 -0
- package/dist/components/layouts/NavSection.svelte +4 -1
- package/dist/components/layouts/NavSection.svelte.d.ts +2 -0
- package/dist/components/layouts/ResizablePanel.svelte +14 -3
- package/dist/components/layouts/ResizablePanel.svelte.d.ts +5 -0
- package/dist/components/layouts/Stack.svelte +41 -0
- package/dist/components/layouts/Stack.svelte.d.ts +9 -0
- package/dist/components/layouts/Toolbar.svelte +122 -0
- package/dist/components/layouts/Toolbar.svelte.d.ts +22 -0
- package/dist/components/molecules/AttachmentList.svelte +59 -0
- package/dist/components/molecules/AttachmentList.svelte.d.ts +13 -0
- package/dist/components/molecules/Breadcrumb.svelte +6 -2
- package/dist/components/molecules/Breadcrumb.svelte.d.ts +2 -0
- package/dist/components/molecules/Callout.svelte +10 -4
- package/dist/components/molecules/Callout.svelte.d.ts +1 -1
- package/dist/components/molecules/CapBar.svelte +5 -1
- package/dist/components/molecules/ChatBubble.svelte +158 -0
- package/dist/components/molecules/ChatBubble.svelte.d.ts +26 -0
- package/dist/components/molecules/CodeBlock.svelte +36 -3
- package/dist/components/molecules/CodeBlock.svelte.d.ts +8 -0
- package/dist/components/molecules/Composer.svelte +195 -0
- package/dist/components/molecules/Composer.svelte.d.ts +27 -0
- package/dist/components/molecules/ConfirmModal.svelte +15 -5
- package/dist/components/molecules/ConfirmModal.svelte.d.ts +5 -1
- package/dist/components/molecules/Dropzone.svelte +9 -3
- package/dist/components/molecules/Dropzone.svelte.d.ts +2 -0
- package/dist/components/molecules/EmptyState.svelte +9 -5
- package/dist/components/molecules/EmptyState.svelte.d.ts +3 -1
- package/dist/components/molecules/Field.svelte +7 -1
- package/dist/components/molecules/Field.svelte.d.ts +2 -0
- package/dist/components/molecules/FileButton.svelte +2 -1
- package/dist/components/molecules/FileButton.svelte.d.ts +2 -1
- package/dist/components/molecules/FilterInput.svelte +13 -3
- package/dist/components/molecules/FilterInput.svelte.d.ts +4 -1
- package/dist/components/molecules/FontScalePicker.svelte +9 -4
- package/dist/components/molecules/IconButton.svelte +32 -7
- package/dist/components/molecules/IconButton.svelte.d.ts +9 -1
- package/dist/components/molecules/KeyValue.svelte +4 -1
- package/dist/components/molecules/KeyValue.svelte.d.ts +2 -1
- package/dist/components/molecules/Menu.svelte +50 -5
- package/dist/components/molecules/Menu.svelte.d.ts +10 -1
- package/dist/components/molecules/Meter.svelte +91 -0
- package/dist/components/molecules/Meter.svelte.d.ts +29 -0
- package/dist/components/molecules/Metric.svelte +134 -8
- package/dist/components/molecules/Metric.svelte.d.ts +20 -2
- package/dist/components/molecules/Modal.svelte +26 -12
- package/dist/components/molecules/Modal.svelte.d.ts +7 -1
- package/dist/components/molecules/Pagination.svelte +25 -1
- package/dist/components/molecules/Pagination.svelte.d.ts +9 -2
- package/dist/components/molecules/Popover.svelte +14 -3
- package/dist/components/molecules/Popover.svelte.d.ts +5 -0
- package/dist/components/molecules/Prose.svelte +140 -0
- package/dist/components/molecules/Prose.svelte.d.ts +13 -0
- package/dist/components/molecules/RadioGroup.svelte +42 -2
- package/dist/components/molecules/RadioGroup.svelte.d.ts +4 -1
- package/dist/components/molecules/SectionHeader.svelte +5 -2
- package/dist/components/molecules/SectionHeader.svelte.d.ts +1 -1
- package/dist/components/molecules/SegmentedControl.svelte +36 -1
- package/dist/components/molecules/SegmentedControl.svelte.d.ts +11 -1
- package/dist/components/molecules/SelectButton.svelte +64 -4
- package/dist/components/molecules/SelectButton.svelte.d.ts +18 -1
- package/dist/components/molecules/Tabs.svelte +21 -3
- package/dist/components/molecules/Tabs.svelte.d.ts +6 -0
- package/dist/components/molecules/Timestamp.svelte +13 -6
- package/dist/components/molecules/Timestamp.svelte.d.ts +4 -0
- package/dist/components/molecules/Toaster.svelte +54 -38
- package/dist/components/molecules/Toaster.svelte.d.ts +5 -1
- package/dist/components/molecules/Toggle.svelte +2 -1
- package/dist/components/molecules/Toggle.svelte.d.ts +2 -1
- package/dist/components/molecules/Tooltip.svelte +32 -5
- package/dist/components/molecules/Tooltip.svelte.d.ts +9 -0
- package/dist/components/molecules/Truncate.svelte +156 -18
- package/dist/components/molecules/Truncate.svelte.d.ts +17 -1
- package/dist/components/molecules/WorkingDir.svelte +6 -0
- package/dist/components/molecules/WorkingDir.svelte.d.ts +2 -0
- package/dist/components/organisms/DataTable.svelte +38 -10
- package/dist/components/organisms/DataTable.svelte.d.ts +4 -0
- package/dist/components/organisms/FilterSearchBar.svelte +9 -2
- package/dist/components/organisms/FilterSearchBar.svelte.d.ts +4 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.js +8 -1
- package/dist/size.d.ts +2 -0
- package/dist/size.js +1 -0
- package/dist/styles/tokens.css +3 -0
- package/dist/timestamp.d.ts +2 -2
- package/dist/timestamp.js +4 -4
- package/dist/tone.d.ts +1 -1
- package/dist/tone.js +1 -1
- package/dist/truncate.d.ts +13 -1
- package/dist/truncate.js +39 -0
- package/package.json +5 -3
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// One conversation line: role-tinted bubble with a meta row, optional
|
|
3
|
+
// actions, delivery state, clamp-and-expand body and a copy affordance.
|
|
4
|
+
import type { Snippet } from 'svelte';
|
|
5
|
+
import Badge from '../atoms/Badge.svelte';
|
|
6
|
+
import Button from '../atoms/Button.svelte';
|
|
7
|
+
import CopyButton from './CopyButton.svelte';
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
role = 'assistant',
|
|
11
|
+
roleLabel,
|
|
12
|
+
align,
|
|
13
|
+
meta,
|
|
14
|
+
actions,
|
|
15
|
+
state: delivery,
|
|
16
|
+
onretry,
|
|
17
|
+
retryLabel = 'Retry',
|
|
18
|
+
clamp,
|
|
19
|
+
expandLabel = 'Show more',
|
|
20
|
+
collapseLabel = 'Show less',
|
|
21
|
+
selected = false,
|
|
22
|
+
copyText,
|
|
23
|
+
children,
|
|
24
|
+
class: klass = '',
|
|
25
|
+
style: styleProp = ''
|
|
26
|
+
}: {
|
|
27
|
+
role?: 'user' | 'assistant' | 'system' | 'tool' | 'mcp';
|
|
28
|
+
/** Text of the role pill; defaults to the role name. */
|
|
29
|
+
roleLabel?: string;
|
|
30
|
+
/** Defaults to `end` for the user, `start` otherwise. */
|
|
31
|
+
align?: 'start' | 'end';
|
|
32
|
+
meta?: Snippet;
|
|
33
|
+
actions?: Snippet;
|
|
34
|
+
state?: 'sending' | 'failed';
|
|
35
|
+
onretry?: () => void;
|
|
36
|
+
retryLabel?: string;
|
|
37
|
+
/** Max visible lines before an expand toggle appears. */
|
|
38
|
+
clamp?: number;
|
|
39
|
+
expandLabel?: string;
|
|
40
|
+
collapseLabel?: string;
|
|
41
|
+
selected?: boolean;
|
|
42
|
+
/** Renders a CopyButton in the actions row. */
|
|
43
|
+
copyText?: string;
|
|
44
|
+
children: Snippet;
|
|
45
|
+
class?: string;
|
|
46
|
+
style?: string;
|
|
47
|
+
} = $props();
|
|
48
|
+
|
|
49
|
+
const side = $derived(align ?? (role === 'user' ? 'end' : 'start'));
|
|
50
|
+
let expanded = $state(false);
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<article
|
|
54
|
+
data-tsu="ChatBubble"
|
|
55
|
+
data-role={role}
|
|
56
|
+
data-state={delivery}
|
|
57
|
+
data-selected={selected || undefined}
|
|
58
|
+
class="bubble role-{role} align-{side} {klass}"
|
|
59
|
+
class:selected
|
|
60
|
+
class:clamped={!!clamp && !expanded}
|
|
61
|
+
style="--clamp: {clamp ?? 'none'}; {styleProp}"
|
|
62
|
+
>
|
|
63
|
+
<header class="meta">
|
|
64
|
+
<Badge size="xs" class="role-pill">{roleLabel ?? role}</Badge>
|
|
65
|
+
{#if meta}{@render meta()}{/if}
|
|
66
|
+
{#if delivery === 'sending'}<span class="state">sending…</span>{/if}
|
|
67
|
+
{#if delivery === 'failed'}
|
|
68
|
+
<span class="state failed">not delivered</span>
|
|
69
|
+
{#if onretry}<Button size="sm" variant="link" onclick={onretry}>{retryLabel}</Button>{/if}
|
|
70
|
+
{/if}
|
|
71
|
+
{#if actions || copyText}
|
|
72
|
+
<span class="actions">
|
|
73
|
+
{#if actions}{@render actions()}{/if}
|
|
74
|
+
{#if copyText}<CopyButton text={copyText} showLabel={false} box="xs" />{/if}
|
|
75
|
+
</span>
|
|
76
|
+
{/if}
|
|
77
|
+
</header>
|
|
78
|
+
<div class="body">{@render children()}</div>
|
|
79
|
+
{#if clamp}
|
|
80
|
+
<Button size="sm" variant="link" onclick={() => (expanded = !expanded)}>
|
|
81
|
+
{expanded ? collapseLabel : expandLabel}
|
|
82
|
+
</Button>
|
|
83
|
+
{/if}
|
|
84
|
+
</article>
|
|
85
|
+
|
|
86
|
+
<style>
|
|
87
|
+
.bubble {
|
|
88
|
+
--role: var(--role-assistant);
|
|
89
|
+
display: flex;
|
|
90
|
+
flex-direction: column;
|
|
91
|
+
gap: var(--sp-1);
|
|
92
|
+
max-width: min(100%, 72ch);
|
|
93
|
+
padding: var(--sp-2) var(--sp-3);
|
|
94
|
+
border: 1px solid color-mix(in srgb, var(--role) 30%, var(--border));
|
|
95
|
+
border-inline-start: 3px solid var(--role);
|
|
96
|
+
border-radius: var(--r-lg);
|
|
97
|
+
background: color-mix(in srgb, var(--role) 6%, var(--surface));
|
|
98
|
+
font-size: var(--fs-sm);
|
|
99
|
+
}
|
|
100
|
+
.role-user {
|
|
101
|
+
--role: var(--role-user);
|
|
102
|
+
}
|
|
103
|
+
.role-system {
|
|
104
|
+
--role: var(--role-system);
|
|
105
|
+
}
|
|
106
|
+
.role-tool {
|
|
107
|
+
--role: var(--role-tool);
|
|
108
|
+
}
|
|
109
|
+
.role-mcp {
|
|
110
|
+
--role: var(--role-mcp);
|
|
111
|
+
}
|
|
112
|
+
.align-end {
|
|
113
|
+
align-self: flex-end;
|
|
114
|
+
}
|
|
115
|
+
.align-start {
|
|
116
|
+
align-self: flex-start;
|
|
117
|
+
}
|
|
118
|
+
.selected {
|
|
119
|
+
outline: 2px solid var(--accent);
|
|
120
|
+
outline-offset: 1px;
|
|
121
|
+
}
|
|
122
|
+
.bubble[data-state='sending'] {
|
|
123
|
+
opacity: 0.7;
|
|
124
|
+
}
|
|
125
|
+
.meta {
|
|
126
|
+
display: flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
flex-wrap: wrap;
|
|
129
|
+
gap: var(--sp-2);
|
|
130
|
+
font-size: var(--fs-xs);
|
|
131
|
+
color: var(--text-muted);
|
|
132
|
+
}
|
|
133
|
+
.meta :global(.role-pill) {
|
|
134
|
+
color: var(--role);
|
|
135
|
+
border-color: color-mix(in srgb, var(--role) 40%, transparent);
|
|
136
|
+
text-transform: capitalize;
|
|
137
|
+
}
|
|
138
|
+
.state.failed {
|
|
139
|
+
color: var(--danger);
|
|
140
|
+
}
|
|
141
|
+
.actions {
|
|
142
|
+
display: inline-flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
gap: var(--sp-1);
|
|
145
|
+
margin-inline-start: auto;
|
|
146
|
+
}
|
|
147
|
+
.body {
|
|
148
|
+
min-width: 0;
|
|
149
|
+
overflow-wrap: anywhere;
|
|
150
|
+
}
|
|
151
|
+
.clamped .body {
|
|
152
|
+
display: -webkit-box;
|
|
153
|
+
-webkit-box-orient: vertical;
|
|
154
|
+
-webkit-line-clamp: var(--clamp);
|
|
155
|
+
line-clamp: var(--clamp);
|
|
156
|
+
overflow: hidden;
|
|
157
|
+
}
|
|
158
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
role?: 'user' | 'assistant' | 'system' | 'tool' | 'mcp';
|
|
4
|
+
/** Text of the role pill; defaults to the role name. */
|
|
5
|
+
roleLabel?: string;
|
|
6
|
+
/** Defaults to `end` for the user, `start` otherwise. */
|
|
7
|
+
align?: 'start' | 'end';
|
|
8
|
+
meta?: Snippet;
|
|
9
|
+
actions?: Snippet;
|
|
10
|
+
state?: 'sending' | 'failed';
|
|
11
|
+
onretry?: () => void;
|
|
12
|
+
retryLabel?: string;
|
|
13
|
+
/** Max visible lines before an expand toggle appears. */
|
|
14
|
+
clamp?: number;
|
|
15
|
+
expandLabel?: string;
|
|
16
|
+
collapseLabel?: string;
|
|
17
|
+
selected?: boolean;
|
|
18
|
+
/** Renders a CopyButton in the actions row. */
|
|
19
|
+
copyText?: string;
|
|
20
|
+
children: Snippet;
|
|
21
|
+
class?: string;
|
|
22
|
+
style?: string;
|
|
23
|
+
};
|
|
24
|
+
declare const ChatBubble: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
25
|
+
type ChatBubble = ReturnType<typeof ChatBubble>;
|
|
26
|
+
export default ChatBubble;
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
showLineNumbers = false,
|
|
21
21
|
wrap = false,
|
|
22
22
|
copy = true,
|
|
23
|
+
maxHeight,
|
|
24
|
+
collapsible = false,
|
|
25
|
+
expandLabel = 'Show more',
|
|
26
|
+
collapseLabel = 'Show less',
|
|
27
|
+
breakAll = false,
|
|
23
28
|
class: klass = ''
|
|
24
29
|
}: {
|
|
25
30
|
code: string;
|
|
@@ -33,12 +38,23 @@
|
|
|
33
38
|
showLineNumbers?: boolean;
|
|
34
39
|
wrap?: boolean;
|
|
35
40
|
copy?: boolean;
|
|
41
|
+
/** Cap the body height; scrolls past it (or expands when `collapsible`). */
|
|
42
|
+
maxHeight?: string;
|
|
43
|
+
/** With `maxHeight`, a toggle in the header expands the body instead of scrolling. */
|
|
44
|
+
collapsible?: boolean;
|
|
45
|
+
expandLabel?: string;
|
|
46
|
+
collapseLabel?: string;
|
|
47
|
+
/** Break long unbroken strings (secrets, hashes) anywhere. */
|
|
48
|
+
breakAll?: boolean;
|
|
36
49
|
class?: string;
|
|
37
50
|
} = $props();
|
|
38
51
|
|
|
52
|
+
let expanded = $state(false);
|
|
53
|
+
|
|
39
54
|
const rendered = $derived(html ?? (highlight ? highlight(code, lang) : null));
|
|
40
55
|
const lines = $derived(showLineNumbers ? code.replace(/\n$/, '').split('\n').length : 0);
|
|
41
|
-
const hasHeader = $derived(!!(filename || lang || copy));
|
|
56
|
+
const hasHeader = $derived(!!(filename || lang || copy || (collapsible && maxHeight)));
|
|
57
|
+
const capped = $derived(!!maxHeight && !(collapsible && expanded));
|
|
42
58
|
</script>
|
|
43
59
|
|
|
44
60
|
<figure class="codeblock {klass}" data-tsu="CodeBlock">
|
|
@@ -46,10 +62,15 @@
|
|
|
46
62
|
<figcaption class="cb-head">
|
|
47
63
|
<span class="cb-name">{filename ?? lang ?? ''}</span>
|
|
48
64
|
<div class="spacer"></div>
|
|
65
|
+
{#if collapsible && maxHeight}
|
|
66
|
+
<button type="button" class="cb-toggle" aria-expanded={expanded} onclick={() => (expanded = !expanded)}>
|
|
67
|
+
{expanded ? collapseLabel : expandLabel}
|
|
68
|
+
</button>
|
|
69
|
+
{/if}
|
|
49
70
|
{#if copy}<CopyButton text={code} showLabel={false} />{/if}
|
|
50
71
|
</figcaption>
|
|
51
72
|
{/if}
|
|
52
|
-
<div class="cb-body" class:wrap class:numbered={showLineNumbers}>
|
|
73
|
+
<div class="cb-body" class:wrap class:numbered={showLineNumbers} class:break-all={breakAll} style:max-height={capped ? maxHeight : undefined}>
|
|
53
74
|
{#if showLineNumbers}
|
|
54
75
|
<span class="cb-gutter" aria-hidden="true">
|
|
55
76
|
{#each { length: lines } as _, i (i)}<span>{i + 1}</span>{/each}
|
|
@@ -82,7 +103,7 @@
|
|
|
82
103
|
}
|
|
83
104
|
.cb-body {
|
|
84
105
|
display: flex;
|
|
85
|
-
overflow
|
|
106
|
+
overflow: auto;
|
|
86
107
|
-webkit-overflow-scrolling: touch;
|
|
87
108
|
}
|
|
88
109
|
.cb-gutter {
|
|
@@ -113,6 +134,18 @@
|
|
|
113
134
|
white-space: pre-wrap;
|
|
114
135
|
word-break: break-word;
|
|
115
136
|
}
|
|
137
|
+
.cb-body.break-all .cb-pre {
|
|
138
|
+
white-space: pre-wrap;
|
|
139
|
+
word-break: break-all;
|
|
140
|
+
}
|
|
141
|
+
.cb-toggle {
|
|
142
|
+
border: 0;
|
|
143
|
+
background: none;
|
|
144
|
+
padding: 0 var(--sp-1);
|
|
145
|
+
font: inherit;
|
|
146
|
+
color: var(--accent);
|
|
147
|
+
cursor: pointer;
|
|
148
|
+
}
|
|
116
149
|
.cb-body.numbered .cb-pre {
|
|
117
150
|
white-space: pre; /* keep lines aligned with the gutter */
|
|
118
151
|
}
|
|
@@ -10,6 +10,14 @@ type $$ComponentProps = {
|
|
|
10
10
|
showLineNumbers?: boolean;
|
|
11
11
|
wrap?: boolean;
|
|
12
12
|
copy?: boolean;
|
|
13
|
+
/** Cap the body height; scrolls past it (or expands when `collapsible`). */
|
|
14
|
+
maxHeight?: string;
|
|
15
|
+
/** With `maxHeight`, a toggle in the header expands the body instead of scrolling. */
|
|
16
|
+
collapsible?: boolean;
|
|
17
|
+
expandLabel?: string;
|
|
18
|
+
collapseLabel?: string;
|
|
19
|
+
/** Break long unbroken strings (secrets, hashes) anywhere. */
|
|
20
|
+
breakAll?: boolean;
|
|
13
21
|
class?: string;
|
|
14
22
|
};
|
|
15
23
|
declare const CodeBlock: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Chat composer: autoresizing Textarea with attach + send controls, submit
|
|
3
|
+
// shortcuts, prompt history on ↑/↓ at the text edges, paste-to-attach and
|
|
4
|
+
// drag-over state. Attachments render as removable chips above the field.
|
|
5
|
+
import type { Snippet } from 'svelte';
|
|
6
|
+
import Button from '../atoms/Button.svelte';
|
|
7
|
+
import Textarea from '../atoms/Textarea.svelte';
|
|
8
|
+
import AttachmentList from './AttachmentList.svelte';
|
|
9
|
+
import FileButton from './FileButton.svelte';
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
value = $bindable(''),
|
|
13
|
+
onsubmit,
|
|
14
|
+
submitOn = 'auto',
|
|
15
|
+
placeholder = 'Message…',
|
|
16
|
+
history = [],
|
|
17
|
+
attachments = $bindable([]),
|
|
18
|
+
onfiles,
|
|
19
|
+
onremove,
|
|
20
|
+
accept,
|
|
21
|
+
busy = false,
|
|
22
|
+
disabled = false,
|
|
23
|
+
maxHeight = '40vh',
|
|
24
|
+
rows = 1,
|
|
25
|
+
sendLabel = 'Send',
|
|
26
|
+
attachLabel = 'Attach',
|
|
27
|
+
leading,
|
|
28
|
+
trailing,
|
|
29
|
+
class: klass = '',
|
|
30
|
+
style: styleProp = ''
|
|
31
|
+
}: {
|
|
32
|
+
value?: string;
|
|
33
|
+
onsubmit: (value: string, files: File[]) => void;
|
|
34
|
+
/** `auto`: Enter on fine pointers, mod+Enter on coarse ones. */
|
|
35
|
+
submitOn?: 'enter' | 'mod-enter' | 'auto';
|
|
36
|
+
placeholder?: string;
|
|
37
|
+
/** Previous prompts, newest last; ↑/↓ browse them at the text edges. */
|
|
38
|
+
history?: string[];
|
|
39
|
+
attachments?: File[];
|
|
40
|
+
onfiles?: (files: File[]) => void;
|
|
41
|
+
onremove?: (index: number) => void;
|
|
42
|
+
accept?: string;
|
|
43
|
+
busy?: boolean;
|
|
44
|
+
disabled?: boolean;
|
|
45
|
+
maxHeight?: string;
|
|
46
|
+
rows?: number;
|
|
47
|
+
sendLabel?: string;
|
|
48
|
+
attachLabel?: string;
|
|
49
|
+
leading?: Snippet;
|
|
50
|
+
trailing?: Snippet;
|
|
51
|
+
class?: string;
|
|
52
|
+
style?: string;
|
|
53
|
+
} = $props();
|
|
54
|
+
|
|
55
|
+
let el = $state<HTMLTextAreaElement | null>(null);
|
|
56
|
+
let dragging = $state(false);
|
|
57
|
+
let cursor = $state(-1);
|
|
58
|
+
let draft = '';
|
|
59
|
+
|
|
60
|
+
const fine = () => typeof matchMedia === 'function' && matchMedia('(pointer: fine)').matches;
|
|
61
|
+
const mode = $derived(submitOn === 'auto' ? (fine() ? 'enter' : 'mod-enter') : submitOn);
|
|
62
|
+
const canSend = $derived(!busy && !disabled && (value.trim().length > 0 || attachments.length > 0));
|
|
63
|
+
|
|
64
|
+
function submit() {
|
|
65
|
+
if (!canSend) return;
|
|
66
|
+
onsubmit(value, attachments);
|
|
67
|
+
cursor = -1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function addFiles(files: File[]) {
|
|
71
|
+
if (!files.length) return;
|
|
72
|
+
attachments = [...attachments, ...files];
|
|
73
|
+
onfiles?.(files);
|
|
74
|
+
}
|
|
75
|
+
function removeAt(i: number) {
|
|
76
|
+
attachments = attachments.filter((_, j) => j !== i);
|
|
77
|
+
onremove?.(i);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function onkeydown(e: KeyboardEvent) {
|
|
81
|
+
if (e.key === 'Enter' && !e.isComposing) {
|
|
82
|
+
const mod = e.ctrlKey || e.metaKey;
|
|
83
|
+
const submits = mode === 'enter' ? !e.shiftKey && !mod && !e.altKey : mod && !e.shiftKey;
|
|
84
|
+
if (submits) {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
submit();
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!history.length || !el) return;
|
|
91
|
+
const atStart = el.selectionStart === 0 && el.selectionEnd === 0;
|
|
92
|
+
const atEnd = el.selectionStart === value.length && el.selectionEnd === value.length;
|
|
93
|
+
if (e.key === 'ArrowUp' && atStart) {
|
|
94
|
+
if (cursor === -1) draft = value;
|
|
95
|
+
const next = cursor === -1 ? history.length - 1 : Math.max(0, cursor - 1);
|
|
96
|
+
cursor = next;
|
|
97
|
+
value = history[next];
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
} else if (e.key === 'ArrowDown' && atEnd && cursor !== -1) {
|
|
100
|
+
const next = cursor + 1;
|
|
101
|
+
if (next >= history.length) {
|
|
102
|
+
cursor = -1;
|
|
103
|
+
value = draft;
|
|
104
|
+
} else {
|
|
105
|
+
cursor = next;
|
|
106
|
+
value = history[next];
|
|
107
|
+
}
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function onpaste(e: ClipboardEvent) {
|
|
113
|
+
const files = Array.from(e.clipboardData?.files ?? []);
|
|
114
|
+
if (files.length) {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
addFiles(files);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function ondrop(e: DragEvent) {
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
dragging = false;
|
|
122
|
+
addFiles(Array.from(e.dataTransfer?.files ?? []));
|
|
123
|
+
}
|
|
124
|
+
</script>
|
|
125
|
+
|
|
126
|
+
<div
|
|
127
|
+
data-tsu="Composer"
|
|
128
|
+
class="composer {klass}"
|
|
129
|
+
class:dragging
|
|
130
|
+
class:busy
|
|
131
|
+
style={styleProp}
|
|
132
|
+
role="group"
|
|
133
|
+
ondragover={(e) => {
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
dragging = true;
|
|
136
|
+
}}
|
|
137
|
+
ondragleave={() => (dragging = false)}
|
|
138
|
+
{ondrop}
|
|
139
|
+
>
|
|
140
|
+
<AttachmentList files={attachments} onremove={removeAt} />
|
|
141
|
+
<div class="row">
|
|
142
|
+
{#if leading}{@render leading()}{/if}
|
|
143
|
+
{#if onfiles || accept !== undefined}
|
|
144
|
+
<FileButton onfiles={addFiles} {accept} multiple iconOnly label={attachLabel} variant="ghost" box="md" {disabled} />
|
|
145
|
+
{/if}
|
|
146
|
+
<Textarea
|
|
147
|
+
bind:value
|
|
148
|
+
bind:el
|
|
149
|
+
autoresize
|
|
150
|
+
{rows}
|
|
151
|
+
{maxHeight}
|
|
152
|
+
{placeholder}
|
|
153
|
+
resize="none"
|
|
154
|
+
grow
|
|
155
|
+
aria-label={placeholder}
|
|
156
|
+
disabled={disabled || busy}
|
|
157
|
+
{onkeydown}
|
|
158
|
+
{onpaste}
|
|
159
|
+
/>
|
|
160
|
+
{#if trailing}{@render trailing()}{/if}
|
|
161
|
+
<Button variant="primary" square loading={busy} disabled={!canSend} aria-label={sendLabel} title={sendLabel} onclick={submit}>
|
|
162
|
+
<span aria-hidden="true">➤</span>
|
|
163
|
+
</Button>
|
|
164
|
+
</div>
|
|
165
|
+
<span class="hint">{mode === 'enter' ? 'Enter to send · Shift+Enter for a new line' : 'Ctrl/⌘+Enter to send'}</span>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<style>
|
|
169
|
+
.composer {
|
|
170
|
+
display: flex;
|
|
171
|
+
flex-direction: column;
|
|
172
|
+
gap: var(--sp-2);
|
|
173
|
+
padding: var(--sp-2);
|
|
174
|
+
border: 1px solid var(--border-strong);
|
|
175
|
+
border-radius: var(--r-lg);
|
|
176
|
+
background: var(--surface);
|
|
177
|
+
transition: border-color 0.12s var(--ease);
|
|
178
|
+
}
|
|
179
|
+
.composer:focus-within {
|
|
180
|
+
border-color: var(--accent);
|
|
181
|
+
}
|
|
182
|
+
.composer.dragging {
|
|
183
|
+
border-style: dashed;
|
|
184
|
+
border-color: var(--accent);
|
|
185
|
+
}
|
|
186
|
+
.row {
|
|
187
|
+
display: flex;
|
|
188
|
+
align-items: flex-end;
|
|
189
|
+
gap: var(--sp-2);
|
|
190
|
+
}
|
|
191
|
+
.hint {
|
|
192
|
+
font-size: var(--fs-xs);
|
|
193
|
+
color: var(--text-faint);
|
|
194
|
+
}
|
|
195
|
+
</style>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
value?: string;
|
|
4
|
+
onsubmit: (value: string, files: File[]) => void;
|
|
5
|
+
/** `auto`: Enter on fine pointers, mod+Enter on coarse ones. */
|
|
6
|
+
submitOn?: 'enter' | 'mod-enter' | 'auto';
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
/** Previous prompts, newest last; ↑/↓ browse them at the text edges. */
|
|
9
|
+
history?: string[];
|
|
10
|
+
attachments?: File[];
|
|
11
|
+
onfiles?: (files: File[]) => void;
|
|
12
|
+
onremove?: (index: number) => void;
|
|
13
|
+
accept?: string;
|
|
14
|
+
busy?: boolean;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
maxHeight?: string;
|
|
17
|
+
rows?: number;
|
|
18
|
+
sendLabel?: string;
|
|
19
|
+
attachLabel?: string;
|
|
20
|
+
leading?: Snippet;
|
|
21
|
+
trailing?: Snippet;
|
|
22
|
+
class?: string;
|
|
23
|
+
style?: string;
|
|
24
|
+
};
|
|
25
|
+
declare const Composer: import("svelte").Component<$$ComponentProps, {}, "value" | "attachments">;
|
|
26
|
+
type Composer = ReturnType<typeof Composer>;
|
|
27
|
+
export default Composer;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { canonicalTone, type Tone } from '../../tone';
|
|
2
3
|
// Yes/no dialog on top of Modal. An async `onconfirm` keeps the dialog open
|
|
3
4
|
// and busy until it settles; a rejection surfaces its message and leaves the
|
|
4
5
|
// dialog open so the user can retry or cancel.
|
|
@@ -17,7 +18,9 @@
|
|
|
17
18
|
tone = 'primary',
|
|
18
19
|
busy = false,
|
|
19
20
|
onconfirm,
|
|
20
|
-
oncancel
|
|
21
|
+
oncancel,
|
|
22
|
+
class: klass = '',
|
|
23
|
+
style: styleProp = '',
|
|
21
24
|
}: {
|
|
22
25
|
open?: boolean;
|
|
23
26
|
title: string;
|
|
@@ -25,11 +28,16 @@
|
|
|
25
28
|
children?: Snippet;
|
|
26
29
|
confirmLabel?: string;
|
|
27
30
|
cancelLabel?: string;
|
|
28
|
-
|
|
31
|
+
/** `primary` is a legacy alias of `accent`. */
|
|
32
|
+
tone?: Tone | 'primary';
|
|
29
33
|
busy?: boolean;
|
|
30
34
|
onconfirm: () => void | Promise<void>;
|
|
31
35
|
oncancel?: () => void;
|
|
36
|
+
class?: string;
|
|
37
|
+
style?: string;
|
|
32
38
|
} = $props();
|
|
39
|
+
const t = $derived(canonicalTone(tone));
|
|
40
|
+
const modalTone = $derived(t === 'danger' || t === 'warn' || t === 'info' ? t : 'neutral');
|
|
33
41
|
|
|
34
42
|
let pending = $state(false);
|
|
35
43
|
let error = $state<string | null>(null);
|
|
@@ -66,10 +74,12 @@
|
|
|
66
74
|
<Modal
|
|
67
75
|
bind:open
|
|
68
76
|
{title}
|
|
69
|
-
tone={
|
|
77
|
+
tone={modalTone}
|
|
70
78
|
busy={working}
|
|
71
79
|
onclose={oncancel}
|
|
72
80
|
size="sm"
|
|
81
|
+
class={klass}
|
|
82
|
+
style={styleProp}
|
|
73
83
|
>
|
|
74
84
|
{#snippet body()}
|
|
75
85
|
<div class="confirm-body" data-tsu="ConfirmModal">
|
|
@@ -83,8 +93,8 @@
|
|
|
83
93
|
{#snippet footer()}
|
|
84
94
|
<Button onclick={cancel} disabled={working}>{cancelLabel}</Button>
|
|
85
95
|
<Button
|
|
86
|
-
variant={
|
|
87
|
-
tone={
|
|
96
|
+
variant={t === 'danger' ? 'danger' : 'primary'}
|
|
97
|
+
tone={t === 'warn' || t === 'ok' || t === 'info' ? t : 'none'}
|
|
88
98
|
loading={working}
|
|
89
99
|
onclick={confirm}
|
|
90
100
|
>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Tone } from '../../tone';
|
|
1
2
|
import type { Snippet } from 'svelte';
|
|
2
3
|
type $$ComponentProps = {
|
|
3
4
|
open?: boolean;
|
|
@@ -6,10 +7,13 @@ type $$ComponentProps = {
|
|
|
6
7
|
children?: Snippet;
|
|
7
8
|
confirmLabel?: string;
|
|
8
9
|
cancelLabel?: string;
|
|
9
|
-
|
|
10
|
+
/** `primary` is a legacy alias of `accent`. */
|
|
11
|
+
tone?: Tone | 'primary';
|
|
10
12
|
busy?: boolean;
|
|
11
13
|
onconfirm: () => void | Promise<void>;
|
|
12
14
|
oncancel?: () => void;
|
|
15
|
+
class?: string;
|
|
16
|
+
style?: string;
|
|
13
17
|
};
|
|
14
18
|
declare const ConfirmModal: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
15
19
|
type ConfirmModal = ReturnType<typeof ConfirmModal>;
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
overlay = false,
|
|
26
26
|
label = 'Drop files here',
|
|
27
27
|
hint = 'or click to browse',
|
|
28
|
-
children
|
|
28
|
+
children,
|
|
29
|
+
class: klass = '',
|
|
30
|
+
style: styleProp = '',
|
|
29
31
|
}: {
|
|
30
32
|
onfiles: (files: File[]) => void;
|
|
31
33
|
/** Fired when the drag-over (active) state changes — host can dim/highlight. */
|
|
@@ -38,6 +40,8 @@
|
|
|
38
40
|
label?: string;
|
|
39
41
|
hint?: string;
|
|
40
42
|
children?: Snippet;
|
|
43
|
+
class?: string;
|
|
44
|
+
style?: string;
|
|
41
45
|
} = $props();
|
|
42
46
|
|
|
43
47
|
let over = $state(false);
|
|
@@ -103,7 +107,8 @@
|
|
|
103
107
|
|
|
104
108
|
{#if overlay}
|
|
105
109
|
<div
|
|
106
|
-
class="dz-host"
|
|
110
|
+
class="dz-host {klass}"
|
|
111
|
+
style={styleProp}
|
|
107
112
|
data-tsu="Dropzone"
|
|
108
113
|
class:over
|
|
109
114
|
class:disabled
|
|
@@ -123,7 +128,8 @@
|
|
|
123
128
|
</div>
|
|
124
129
|
{:else}
|
|
125
130
|
<div
|
|
126
|
-
class="dz"
|
|
131
|
+
class="dz {klass}"
|
|
132
|
+
style={styleProp}
|
|
127
133
|
data-tsu="Dropzone"
|
|
128
134
|
class:over
|
|
129
135
|
class:disabled
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { canonicalTone, type Tone } from '../../tone';
|
|
2
3
|
// Empty / placeholder state — the "nothing here yet" panel every unworked
|
|
3
4
|
// settings section, empty list, and zero-result table reaches for. Centered
|
|
4
5
|
// stack of:
|
|
@@ -25,9 +26,8 @@
|
|
|
25
26
|
// richer (multiple buttons, links) use the `action` snippet instead.
|
|
26
27
|
actionLabel,
|
|
27
28
|
onAction,
|
|
28
|
-
// Legacy alias for size="compact".
|
|
29
29
|
compact = false,
|
|
30
|
-
size
|
|
30
|
+
size,
|
|
31
31
|
loading = false,
|
|
32
32
|
class: klass = '',
|
|
33
33
|
// Raw SVG markup for a custom glyph — passed straight to `Icon`.
|
|
@@ -39,9 +39,10 @@
|
|
|
39
39
|
title?: string;
|
|
40
40
|
description?: string | Snippet;
|
|
41
41
|
icon?: IconName;
|
|
42
|
-
tone?:
|
|
42
|
+
tone?: Tone;
|
|
43
43
|
actionLabel?: string;
|
|
44
44
|
onAction?: () => void;
|
|
45
|
+
/** @deprecated use `size="compact"`; `size` wins when both are set. */
|
|
45
46
|
compact?: boolean;
|
|
46
47
|
size?: 'inline' | 'compact' | 'default';
|
|
47
48
|
loading?: boolean;
|
|
@@ -51,11 +52,11 @@
|
|
|
51
52
|
[key: string]: unknown;
|
|
52
53
|
} = $props();
|
|
53
54
|
|
|
54
|
-
const sz = $derived(compact ? 'compact' :
|
|
55
|
+
const sz = $derived(size ?? (compact ? 'compact' : 'default'));
|
|
55
56
|
</script>
|
|
56
57
|
|
|
57
58
|
<div
|
|
58
|
-
class="empty empty-{tone} {klass}"
|
|
59
|
+
class="empty empty-{canonicalTone(tone)} {klass}"
|
|
59
60
|
class:empty-compact={sz === 'compact'}
|
|
60
61
|
class:empty-inline={sz === 'inline'}
|
|
61
62
|
class:empty-loading={loading}
|
|
@@ -115,6 +116,9 @@
|
|
|
115
116
|
:global(.empty-info) {
|
|
116
117
|
--empty-tone: var(--info);
|
|
117
118
|
}
|
|
119
|
+
:global(.empty-accent) {
|
|
120
|
+
--empty-tone: var(--accent);
|
|
121
|
+
}
|
|
118
122
|
.empty-compact {
|
|
119
123
|
gap: var(--sp-2);
|
|
120
124
|
padding: var(--sp-4) var(--sp-3);
|