@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.
Files changed (144) hide show
  1. package/dist/autoresize.d.ts +2 -0
  2. package/dist/autoresize.js +15 -1
  3. package/dist/components/atoms/Artwork.svelte +19 -18
  4. package/dist/components/atoms/Artwork.svelte.d.ts +3 -2
  5. package/dist/components/atoms/Badge.svelte +60 -26
  6. package/dist/components/atoms/Badge.svelte.d.ts +14 -3
  7. package/dist/components/atoms/Button.svelte +30 -12
  8. package/dist/components/atoms/Button.svelte.d.ts +4 -2
  9. package/dist/components/atoms/Card.svelte +47 -30
  10. package/dist/components/atoms/Card.svelte.d.ts +12 -3
  11. package/dist/components/atoms/Checkbox.svelte +16 -2
  12. package/dist/components/atoms/Checkbox.svelte.d.ts +2 -0
  13. package/dist/components/atoms/Dot.svelte +17 -11
  14. package/dist/components/atoms/Dot.svelte.d.ts +5 -2
  15. package/dist/components/atoms/Gauge.svelte +19 -18
  16. package/dist/components/atoms/Gauge.svelte.d.ts +3 -2
  17. package/dist/components/atoms/Heading.svelte +20 -19
  18. package/dist/components/atoms/Heading.svelte.d.ts +3 -2
  19. package/dist/components/atoms/Icon.svelte +33 -13
  20. package/dist/components/atoms/Icon.svelte.d.ts +17 -2
  21. package/dist/components/atoms/Input.svelte +17 -3
  22. package/dist/components/atoms/Input.svelte.d.ts +2 -1
  23. package/dist/components/atoms/Kbd.svelte +25 -3
  24. package/dist/components/atoms/Kbd.svelte.d.ts +5 -1
  25. package/dist/components/atoms/Link.svelte +16 -12
  26. package/dist/components/atoms/Link.svelte.d.ts +6 -2
  27. package/dist/components/atoms/Progress.svelte +119 -4
  28. package/dist/components/atoms/Progress.svelte.d.ts +28 -2
  29. package/dist/components/atoms/Scrim.svelte +7 -2
  30. package/dist/components/atoms/Scrim.svelte.d.ts +2 -0
  31. package/dist/components/atoms/SegmentedProgress.svelte +14 -2
  32. package/dist/components/atoms/SegmentedProgress.svelte.d.ts +5 -2
  33. package/dist/components/atoms/Select.svelte +18 -6
  34. package/dist/components/atoms/Select.svelte.d.ts +3 -3
  35. package/dist/components/atoms/Skeleton.svelte +12 -11
  36. package/dist/components/atoms/Skeleton.svelte.d.ts +3 -2
  37. package/dist/components/atoms/Slider.svelte +45 -3
  38. package/dist/components/atoms/Slider.svelte.d.ts +7 -0
  39. package/dist/components/atoms/Spinner.svelte +2 -0
  40. package/dist/components/atoms/Spinner.svelte.d.ts +2 -0
  41. package/dist/components/atoms/Switch.svelte +70 -18
  42. package/dist/components/atoms/Switch.svelte.d.ts +5 -1
  43. package/dist/components/atoms/Text.svelte +39 -26
  44. package/dist/components/atoms/Text.svelte.d.ts +7 -3
  45. package/dist/components/atoms/Textarea.svelte +47 -10
  46. package/dist/components/atoms/Textarea.svelte.d.ts +10 -3
  47. package/dist/components/layouts/AppShell.svelte +7 -3
  48. package/dist/components/layouts/AppShell.svelte.d.ts +2 -0
  49. package/dist/components/layouts/AutoGrid.svelte +4 -0
  50. package/dist/components/layouts/AutoGrid.svelte.d.ts +2 -0
  51. package/dist/components/layouts/Cluster.svelte +62 -1
  52. package/dist/components/layouts/Cluster.svelte.d.ts +9 -0
  53. package/dist/components/layouts/NavItem.svelte +22 -1
  54. package/dist/components/layouts/NavItem.svelte.d.ts +5 -0
  55. package/dist/components/layouts/NavSection.svelte +4 -1
  56. package/dist/components/layouts/NavSection.svelte.d.ts +2 -0
  57. package/dist/components/layouts/ResizablePanel.svelte +14 -3
  58. package/dist/components/layouts/ResizablePanel.svelte.d.ts +5 -0
  59. package/dist/components/layouts/Stack.svelte +41 -0
  60. package/dist/components/layouts/Stack.svelte.d.ts +9 -0
  61. package/dist/components/layouts/Toolbar.svelte +122 -0
  62. package/dist/components/layouts/Toolbar.svelte.d.ts +22 -0
  63. package/dist/components/molecules/AttachmentList.svelte +59 -0
  64. package/dist/components/molecules/AttachmentList.svelte.d.ts +13 -0
  65. package/dist/components/molecules/Breadcrumb.svelte +6 -2
  66. package/dist/components/molecules/Breadcrumb.svelte.d.ts +2 -0
  67. package/dist/components/molecules/Callout.svelte +10 -4
  68. package/dist/components/molecules/Callout.svelte.d.ts +1 -1
  69. package/dist/components/molecules/CapBar.svelte +5 -1
  70. package/dist/components/molecules/ChatBubble.svelte +158 -0
  71. package/dist/components/molecules/ChatBubble.svelte.d.ts +26 -0
  72. package/dist/components/molecules/CodeBlock.svelte +36 -3
  73. package/dist/components/molecules/CodeBlock.svelte.d.ts +8 -0
  74. package/dist/components/molecules/Composer.svelte +195 -0
  75. package/dist/components/molecules/Composer.svelte.d.ts +27 -0
  76. package/dist/components/molecules/ConfirmModal.svelte +15 -5
  77. package/dist/components/molecules/ConfirmModal.svelte.d.ts +5 -1
  78. package/dist/components/molecules/Dropzone.svelte +9 -3
  79. package/dist/components/molecules/Dropzone.svelte.d.ts +2 -0
  80. package/dist/components/molecules/EmptyState.svelte +9 -5
  81. package/dist/components/molecules/EmptyState.svelte.d.ts +3 -1
  82. package/dist/components/molecules/Field.svelte +7 -1
  83. package/dist/components/molecules/Field.svelte.d.ts +2 -0
  84. package/dist/components/molecules/FileButton.svelte +2 -1
  85. package/dist/components/molecules/FileButton.svelte.d.ts +2 -1
  86. package/dist/components/molecules/FilterInput.svelte +13 -3
  87. package/dist/components/molecules/FilterInput.svelte.d.ts +4 -1
  88. package/dist/components/molecules/FontScalePicker.svelte +9 -4
  89. package/dist/components/molecules/IconButton.svelte +32 -7
  90. package/dist/components/molecules/IconButton.svelte.d.ts +9 -1
  91. package/dist/components/molecules/KeyValue.svelte +4 -1
  92. package/dist/components/molecules/KeyValue.svelte.d.ts +2 -1
  93. package/dist/components/molecules/Menu.svelte +50 -5
  94. package/dist/components/molecules/Menu.svelte.d.ts +10 -1
  95. package/dist/components/molecules/Meter.svelte +91 -0
  96. package/dist/components/molecules/Meter.svelte.d.ts +29 -0
  97. package/dist/components/molecules/Metric.svelte +134 -8
  98. package/dist/components/molecules/Metric.svelte.d.ts +20 -2
  99. package/dist/components/molecules/Modal.svelte +26 -12
  100. package/dist/components/molecules/Modal.svelte.d.ts +7 -1
  101. package/dist/components/molecules/Pagination.svelte +25 -1
  102. package/dist/components/molecules/Pagination.svelte.d.ts +9 -2
  103. package/dist/components/molecules/Popover.svelte +14 -3
  104. package/dist/components/molecules/Popover.svelte.d.ts +5 -0
  105. package/dist/components/molecules/Prose.svelte +140 -0
  106. package/dist/components/molecules/Prose.svelte.d.ts +13 -0
  107. package/dist/components/molecules/RadioGroup.svelte +42 -2
  108. package/dist/components/molecules/RadioGroup.svelte.d.ts +4 -1
  109. package/dist/components/molecules/SectionHeader.svelte +5 -2
  110. package/dist/components/molecules/SectionHeader.svelte.d.ts +1 -1
  111. package/dist/components/molecules/SegmentedControl.svelte +36 -1
  112. package/dist/components/molecules/SegmentedControl.svelte.d.ts +11 -1
  113. package/dist/components/molecules/SelectButton.svelte +64 -4
  114. package/dist/components/molecules/SelectButton.svelte.d.ts +18 -1
  115. package/dist/components/molecules/Tabs.svelte +21 -3
  116. package/dist/components/molecules/Tabs.svelte.d.ts +6 -0
  117. package/dist/components/molecules/Timestamp.svelte +13 -6
  118. package/dist/components/molecules/Timestamp.svelte.d.ts +4 -0
  119. package/dist/components/molecules/Toaster.svelte +54 -38
  120. package/dist/components/molecules/Toaster.svelte.d.ts +5 -1
  121. package/dist/components/molecules/Toggle.svelte +2 -1
  122. package/dist/components/molecules/Toggle.svelte.d.ts +2 -1
  123. package/dist/components/molecules/Tooltip.svelte +32 -5
  124. package/dist/components/molecules/Tooltip.svelte.d.ts +9 -0
  125. package/dist/components/molecules/Truncate.svelte +156 -18
  126. package/dist/components/molecules/Truncate.svelte.d.ts +17 -1
  127. package/dist/components/molecules/WorkingDir.svelte +6 -0
  128. package/dist/components/molecules/WorkingDir.svelte.d.ts +2 -0
  129. package/dist/components/organisms/DataTable.svelte +38 -10
  130. package/dist/components/organisms/DataTable.svelte.d.ts +4 -0
  131. package/dist/components/organisms/FilterSearchBar.svelte +9 -2
  132. package/dist/components/organisms/FilterSearchBar.svelte.d.ts +4 -1
  133. package/dist/index.d.ts +10 -2
  134. package/dist/index.js +8 -1
  135. package/dist/size.d.ts +2 -0
  136. package/dist/size.js +1 -0
  137. package/dist/styles/tokens.css +3 -0
  138. package/dist/timestamp.d.ts +2 -2
  139. package/dist/timestamp.js +4 -4
  140. package/dist/tone.d.ts +1 -1
  141. package/dist/tone.js +1 -1
  142. package/dist/truncate.d.ts +13 -1
  143. package/dist/truncate.js +39 -0
  144. 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-x: auto;
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
- tone?: 'primary' | 'danger' | 'warn';
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={tone === 'primary' ? 'neutral' : 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={tone === 'danger' ? 'danger' : 'primary'}
87
- tone={tone === 'warn' ? 'warn' : 'none'}
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
- tone?: 'primary' | 'danger' | 'warn';
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
@@ -11,6 +11,8 @@ type $$ComponentProps = {
11
11
  label?: string;
12
12
  hint?: string;
13
13
  children?: Snippet;
14
+ class?: string;
15
+ style?: string;
14
16
  };
15
17
  declare const Dropzone: import("svelte").Component<$$ComponentProps, {}, "">;
16
18
  type Dropzone = ReturnType<typeof Dropzone>;
@@ -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 = 'default',
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?: 'neutral' | 'ok' | 'warn' | 'danger' | 'info';
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' : size);
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);