@dorsk/tsumikit 0.82.0 → 0.82.2
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/README.md +30 -0
- package/dist/components/atoms/Textarea.svelte +101 -0
- package/dist/components/atoms/Textarea.svelte.d.ts +10 -0
- package/dist/components/layouts/AppShell.svelte +8 -3
- package/dist/components/layouts/AppShell.svelte.d.ts +3 -2
- package/dist/components/molecules/Composer.svelte +5 -1
- package/dist/components/molecules/Composer.svelte.d.ts +3 -0
- package/dist/components/molecules/Modal.svelte +4 -1
- package/dist/components/molecules/RichSelect.svelte +8 -1
- package/dist/index.d.ts +1 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -944,6 +944,36 @@ opaque and the blur is dropped; `prefers-contrast: more` also uses the opaque sc
|
|
|
944
944
|
</Backdrop>
|
|
945
945
|
```
|
|
946
946
|
|
|
947
|
+
### Masked spans while typing (Textarea `segments`)
|
|
948
|
+
|
|
949
|
+
`Textarea` (and `Composer`, which forwards it) accepts `segments`, a function that splits the
|
|
950
|
+
current value into spans for a mirror overlay rendered behind a text-transparent native textarea.
|
|
951
|
+
Each segment's `text` is rendered verbatim so glyph widths match 1:1; `mask: true` keeps the span's
|
|
952
|
+
layout but hides its glyphs under a token fill (`--textarea-mask` overrides it); `class` is added
|
|
953
|
+
to the span. The texts must concatenate back to the value. Caret, selection, IME composition, undo,
|
|
954
|
+
autoresize and scrolling stay native. The overlay is `aria-hidden`: assistive tech and the form
|
|
955
|
+
still get the raw value. Without `segments` the Textarea renders exactly as before.
|
|
956
|
+
|
|
957
|
+
```svelte
|
|
958
|
+
<script lang="ts">
|
|
959
|
+
import { Textarea } from '@dorsk/tsumikit';
|
|
960
|
+
const TAG = /(<redacted>)([\s\S]*?)(<\/redacted>)/g;
|
|
961
|
+
function segments(text: string) {
|
|
962
|
+
const out = [];
|
|
963
|
+
let last = 0;
|
|
964
|
+
for (const m of text.matchAll(TAG)) {
|
|
965
|
+
if (m.index > last) out.push({ text: text.slice(last, m.index) });
|
|
966
|
+
out.push({ text: m[1], class: 'tag' }, { text: m[2], mask: true }, { text: m[3], class: 'tag' });
|
|
967
|
+
last = m.index + m[0].length;
|
|
968
|
+
}
|
|
969
|
+
if (last < text.length) out.push({ text: text.slice(last) });
|
|
970
|
+
return out;
|
|
971
|
+
}
|
|
972
|
+
</script>
|
|
973
|
+
|
|
974
|
+
<Textarea bind:value autoresize {segments} aria-label="Message" />
|
|
975
|
+
```
|
|
976
|
+
|
|
947
977
|
### Breakpoints
|
|
948
978
|
|
|
949
979
|
One list for every media query: `sm` 40rem (640px), `md` 48rem (768px), `lg`
|
|
@@ -18,11 +18,20 @@
|
|
|
18
18
|
// `onsubmit` fires with the value on Enter (`submitOn="enter"`, Shift+Enter
|
|
19
19
|
// still inserts a newline) or Ctrl/Meta+Enter (`submitOn="mod-enter"`, the
|
|
20
20
|
// default whenever `onsubmit` is given).
|
|
21
|
+
//
|
|
22
|
+
// `segments` opts into a mirror overlay: a layer behind the (text-transparent)
|
|
23
|
+
// textarea that renders the value split into spans, so tagged spans can show
|
|
24
|
+
// masked while the user types. The native control keeps caret, selection, IME,
|
|
25
|
+
// undo, autoresize and scrolling; the overlay shares its box, font and
|
|
26
|
+
// wrapping rules and follows its scroll offset. The raw value stays what
|
|
27
|
+
// assistive tech reads and what the form submits.
|
|
21
28
|
import type { HTMLTextareaAttributes } from 'svelte/elements';
|
|
22
29
|
import { autoresize as autoresizeAction, rowsFloor, rowsHeight } from '../../autoresize';
|
|
23
30
|
import { getFieldContext, warnUnlabelled } from '../../field-context';
|
|
24
31
|
import { getInputGroupContext } from '../../input-group-context';
|
|
25
32
|
|
|
33
|
+
export type TextareaSegment = { text: string; mask?: boolean; class?: string };
|
|
34
|
+
|
|
26
35
|
type Props = Omit<HTMLTextareaAttributes, 'onsubmit'> & {
|
|
27
36
|
mono?: boolean;
|
|
28
37
|
autoresize?: boolean;
|
|
@@ -45,6 +54,11 @@
|
|
|
45
54
|
/** Fires with the current value on the `submitOn` key combo. */
|
|
46
55
|
onsubmit?: (value: string) => void;
|
|
47
56
|
submitOn?: 'enter' | 'mod-enter' | 'none';
|
|
57
|
+
/** Split the value into spans for the mirror overlay; the texts must
|
|
58
|
+
* concatenate back to the value so glyphs line up 1:1. `mask` keeps a
|
|
59
|
+
* span's layout but hides its glyphs under a fill; `class` is added to
|
|
60
|
+
* the span. Unset: no overlay, no extra DOM. */
|
|
61
|
+
segments?: (value: string) => TextareaSegment[];
|
|
48
62
|
id?: string;
|
|
49
63
|
'aria-describedby'?: string | null;
|
|
50
64
|
'aria-invalid'?: HTMLTextareaAttributes['aria-invalid'];
|
|
@@ -66,7 +80,10 @@
|
|
|
66
80
|
maxHeight,
|
|
67
81
|
onsubmit,
|
|
68
82
|
submitOn = 'none',
|
|
83
|
+
segments,
|
|
69
84
|
onkeydown,
|
|
85
|
+
oninput,
|
|
86
|
+
onscroll,
|
|
70
87
|
id,
|
|
71
88
|
'aria-describedby': ariaDescribedby,
|
|
72
89
|
'aria-invalid': ariaInvalid,
|
|
@@ -136,6 +153,27 @@
|
|
|
136
153
|
onsubmit(String(value ?? ''));
|
|
137
154
|
}
|
|
138
155
|
|
|
156
|
+
let mirror = $state<HTMLDivElement | null>(null);
|
|
157
|
+
const segs = $derived(segments ? segments(String(value ?? '')) : null);
|
|
158
|
+
|
|
159
|
+
function syncScroll() {
|
|
160
|
+
if (!el || !mirror) return;
|
|
161
|
+
mirror.scrollTop = el.scrollTop;
|
|
162
|
+
mirror.scrollLeft = el.scrollLeft;
|
|
163
|
+
}
|
|
164
|
+
function handleInput(e: Event & { currentTarget: EventTarget & HTMLTextAreaElement }) {
|
|
165
|
+
oninput?.(e);
|
|
166
|
+
if (segs) syncScroll();
|
|
167
|
+
}
|
|
168
|
+
function handleScroll(e: UIEvent & { currentTarget: EventTarget & HTMLTextAreaElement }) {
|
|
169
|
+
onscroll?.(e);
|
|
170
|
+
if (segs) syncScroll();
|
|
171
|
+
}
|
|
172
|
+
$effect(() => {
|
|
173
|
+
void segs;
|
|
174
|
+
syncScroll();
|
|
175
|
+
});
|
|
176
|
+
|
|
139
177
|
// --- manual resize drag (mirrors AppShell/Modal: rAF-throttled pointer drag) ---
|
|
140
178
|
let dragging = $state(false);
|
|
141
179
|
let rafId = 0;
|
|
@@ -195,7 +233,23 @@
|
|
|
195
233
|
class:grouped={!!group}
|
|
196
234
|
class:bar={!!group?.bar}
|
|
197
235
|
data-tsu="Textarea"
|
|
236
|
+
class:mirrored={!!segs}
|
|
198
237
|
>
|
|
238
|
+
{#if segs}
|
|
239
|
+
<div
|
|
240
|
+
bind:this={mirror}
|
|
241
|
+
class="mirror textarea"
|
|
242
|
+
class:mono
|
|
243
|
+
class:textarea-sm={sizeEff === 'sm'}
|
|
244
|
+
class:textarea-lg={sizeEff === 'lg'}
|
|
245
|
+
class:grouped={!!group}
|
|
246
|
+
class:bar={!!group?.bar}
|
|
247
|
+
class:capped={!!maxHeight}
|
|
248
|
+
style:max-height={maxHeight}
|
|
249
|
+
aria-hidden="true"
|
|
250
|
+
data-tsu="TextareaMirror"
|
|
251
|
+
>{#each segs as seg}<span class={seg.class} class:masked={!!seg.mask}>{seg.text}</span>{/each}<span class="tail">{'\u200b'}</span></div>
|
|
252
|
+
{/if}
|
|
199
253
|
{#if autoresize}
|
|
200
254
|
<textarea
|
|
201
255
|
bind:this={el}
|
|
@@ -206,6 +260,7 @@
|
|
|
206
260
|
class:grouped={!!group}
|
|
207
261
|
class:bar={!!group?.bar}
|
|
208
262
|
class:capped={!!maxHeight}
|
|
263
|
+
class:mirrored={!!segs}
|
|
209
264
|
disabled={isDisabled}
|
|
210
265
|
style:max-height={maxHeight}
|
|
211
266
|
bind:value
|
|
@@ -213,6 +268,8 @@
|
|
|
213
268
|
{...rest}
|
|
214
269
|
placeholder={rest.placeholder ?? (field ? ' ' : undefined)}
|
|
215
270
|
onkeydown={onsubmit || onkeydown ? handleKeydown : undefined}
|
|
271
|
+
oninput={segs || oninput ? handleInput : undefined}
|
|
272
|
+
onscroll={segs || onscroll ? handleScroll : undefined}
|
|
216
273
|
id={id ?? field?.id}
|
|
217
274
|
aria-describedby={ariaDescribedby ?? field?.describedBy}
|
|
218
275
|
aria-invalid={ariaInvalid ?? (isInvalid ? 'true' : undefined)}
|
|
@@ -227,12 +284,15 @@
|
|
|
227
284
|
class:grouped={!!group}
|
|
228
285
|
class:bar={!!group?.bar}
|
|
229
286
|
class:capped={!!maxHeight}
|
|
287
|
+
class:mirrored={!!segs}
|
|
230
288
|
disabled={isDisabled}
|
|
231
289
|
style:max-height={maxHeight}
|
|
232
290
|
bind:value
|
|
233
291
|
{...rest}
|
|
234
292
|
placeholder={rest.placeholder ?? (field ? ' ' : undefined)}
|
|
235
293
|
onkeydown={onsubmit || onkeydown ? handleKeydown : undefined}
|
|
294
|
+
oninput={segs || oninput ? handleInput : undefined}
|
|
295
|
+
onscroll={segs || onscroll ? handleScroll : undefined}
|
|
236
296
|
id={id ?? field?.id}
|
|
237
297
|
aria-describedby={ariaDescribedby ?? field?.describedBy}
|
|
238
298
|
aria-invalid={ariaInvalid ?? (isInvalid ? 'true' : undefined)}
|
|
@@ -294,6 +354,47 @@
|
|
|
294
354
|
font-size: var(--control-fs-md);
|
|
295
355
|
line-height: var(--lh-tight);
|
|
296
356
|
font-family: inherit;
|
|
357
|
+
white-space: pre-wrap;
|
|
358
|
+
overflow-wrap: break-word;
|
|
359
|
+
word-break: normal;
|
|
360
|
+
tab-size: 4;
|
|
361
|
+
}
|
|
362
|
+
/* Mirror overlay: same box, font and wrapping as the textarea; the textarea
|
|
363
|
+
above it keeps its border, caret and selection but shows no glyphs. */
|
|
364
|
+
.textarea-wrap.mirrored {
|
|
365
|
+
isolation: isolate;
|
|
366
|
+
}
|
|
367
|
+
.mirror {
|
|
368
|
+
position: absolute;
|
|
369
|
+
inset: 0;
|
|
370
|
+
z-index: 0;
|
|
371
|
+
box-sizing: border-box;
|
|
372
|
+
min-height: 0;
|
|
373
|
+
overflow: auto;
|
|
374
|
+
border-color: transparent;
|
|
375
|
+
box-shadow: none;
|
|
376
|
+
pointer-events: none;
|
|
377
|
+
transition: none;
|
|
378
|
+
}
|
|
379
|
+
.textarea.mirrored {
|
|
380
|
+
position: relative;
|
|
381
|
+
z-index: 1;
|
|
382
|
+
color: transparent;
|
|
383
|
+
caret-color: var(--textarea-fg, var(--text));
|
|
384
|
+
background: transparent;
|
|
385
|
+
}
|
|
386
|
+
.textarea.mirrored::selection {
|
|
387
|
+
color: transparent;
|
|
388
|
+
background: color-mix(in srgb, var(--accent) 30%, transparent);
|
|
389
|
+
}
|
|
390
|
+
.mirrored .resize-handle {
|
|
391
|
+
z-index: 2;
|
|
392
|
+
}
|
|
393
|
+
.mirror .masked {
|
|
394
|
+
color: transparent;
|
|
395
|
+
background: var(--textarea-mask, color-mix(in srgb, var(--text) 18%, transparent));
|
|
396
|
+
border-radius: var(--r-sm);
|
|
397
|
+
box-decoration-break: clone;
|
|
297
398
|
}
|
|
298
399
|
.textarea:focus {
|
|
299
400
|
outline: none;
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { ControlSize } from '../../size';
|
|
2
2
|
import type { HTMLTextareaAttributes } from 'svelte/elements';
|
|
3
|
+
export type TextareaSegment = {
|
|
4
|
+
text: string;
|
|
5
|
+
mask?: boolean;
|
|
6
|
+
class?: string;
|
|
7
|
+
};
|
|
3
8
|
type Props = Omit<HTMLTextareaAttributes, 'onsubmit'> & {
|
|
4
9
|
mono?: boolean;
|
|
5
10
|
autoresize?: boolean;
|
|
@@ -22,6 +27,11 @@ type Props = Omit<HTMLTextareaAttributes, 'onsubmit'> & {
|
|
|
22
27
|
/** Fires with the current value on the `submitOn` key combo. */
|
|
23
28
|
onsubmit?: (value: string) => void;
|
|
24
29
|
submitOn?: 'enter' | 'mod-enter' | 'none';
|
|
30
|
+
/** Split the value into spans for the mirror overlay; the texts must
|
|
31
|
+
* concatenate back to the value so glyphs line up 1:1. `mask` keeps a
|
|
32
|
+
* span's layout but hides its glyphs under a fill; `class` is added to
|
|
33
|
+
* the span. Unset: no overlay, no extra DOM. */
|
|
34
|
+
segments?: (value: string) => TextareaSegment[];
|
|
25
35
|
id?: string;
|
|
26
36
|
'aria-describedby'?: string | null;
|
|
27
37
|
'aria-invalid'?: HTMLTextareaAttributes['aria-invalid'];
|
|
@@ -57,8 +57,9 @@
|
|
|
57
57
|
/** `'sidebar-full'`: the sidebar spans the whole height on desktop/tablet
|
|
58
58
|
* and the header only covers the content column (brand sits top-left). */
|
|
59
59
|
layout?: 'header-top' | 'sidebar-full';
|
|
60
|
-
/** Pin the desktop sidebar to the viewport (
|
|
61
|
-
*
|
|
60
|
+
/** Pin the desktop sidebar to the viewport (own scroll) so its footer
|
|
61
|
+
* stays visible on long pages. Sits below the header in `header-top`,
|
|
62
|
+
* spans the full viewport height in `sidebar-full`. */
|
|
62
63
|
stickySidebar?: boolean;
|
|
63
64
|
/** Inner padding of the sidebar; `'none'` when the nav owns its gutters. */
|
|
64
65
|
sidebarPadding?: 'none' | 'sm' | 'md';
|
|
@@ -414,9 +415,13 @@
|
|
|
414
415
|
}
|
|
415
416
|
.shell.sticky-sidebar .shell-sidebar {
|
|
416
417
|
position: sticky;
|
|
418
|
+
top: var(--shell-header-h);
|
|
419
|
+
height: calc(100dvh - var(--shell-header-h));
|
|
420
|
+
align-self: start;
|
|
421
|
+
}
|
|
422
|
+
.shell.sticky-sidebar.sidebar-full .shell-sidebar {
|
|
417
423
|
top: 0;
|
|
418
424
|
height: 100dvh;
|
|
419
|
-
align-self: start;
|
|
420
425
|
}
|
|
421
426
|
.shell-scrim,
|
|
422
427
|
.shell-menu-btn {
|
|
@@ -18,8 +18,9 @@ type $$ComponentProps = {
|
|
|
18
18
|
/** `'sidebar-full'`: the sidebar spans the whole height on desktop/tablet
|
|
19
19
|
* and the header only covers the content column (brand sits top-left). */
|
|
20
20
|
layout?: 'header-top' | 'sidebar-full';
|
|
21
|
-
/** Pin the desktop sidebar to the viewport (
|
|
22
|
-
*
|
|
21
|
+
/** Pin the desktop sidebar to the viewport (own scroll) so its footer
|
|
22
|
+
* stays visible on long pages. Sits below the header in `header-top`,
|
|
23
|
+
* spans the full viewport height in `sidebar-full`. */
|
|
23
24
|
stickySidebar?: boolean;
|
|
24
25
|
/** Inner padding of the sidebar; `'none'` when the nav owns its gutters. */
|
|
25
26
|
sidebarPadding?: 'none' | 'sm' | 'md';
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// `toolbarEnd` move attach + send into a row under the field.
|
|
7
7
|
import type { Snippet } from 'svelte';
|
|
8
8
|
import Button from '../atoms/Button.svelte';
|
|
9
|
-
import Textarea from '../atoms/Textarea.svelte';
|
|
9
|
+
import Textarea, { type TextareaSegment } from '../atoms/Textarea.svelte';
|
|
10
10
|
import AttachmentList from './AttachmentList.svelte';
|
|
11
11
|
import FileButton from './FileButton.svelte';
|
|
12
12
|
import InputGroup from './InputGroup.svelte';
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
maxHeight = '40vh',
|
|
27
27
|
rows = 1,
|
|
28
28
|
resize = 'none',
|
|
29
|
+
segments,
|
|
29
30
|
sendLabel = 'Send',
|
|
30
31
|
attachLabel = 'Attach',
|
|
31
32
|
leading,
|
|
@@ -52,6 +53,8 @@
|
|
|
52
53
|
rows?: number;
|
|
53
54
|
/** Manual drag handle on the top edge; `none` hides it. */
|
|
54
55
|
resize?: 'none' | 'top';
|
|
56
|
+
/** Mirror overlay segments, forwarded to the Textarea. */
|
|
57
|
+
segments?: (value: string) => TextareaSegment[];
|
|
55
58
|
sendLabel?: string;
|
|
56
59
|
attachLabel?: string;
|
|
57
60
|
leading?: Snippet;
|
|
@@ -200,6 +203,7 @@
|
|
|
200
203
|
{maxHeight}
|
|
201
204
|
{placeholder}
|
|
202
205
|
{resize}
|
|
206
|
+
{segments}
|
|
203
207
|
aria-label={placeholder}
|
|
204
208
|
disabled={disabled || busy}
|
|
205
209
|
{onkeydown}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
+
import { type TextareaSegment } from '../atoms/Textarea.svelte';
|
|
2
3
|
type $$ComponentProps = {
|
|
3
4
|
value?: string;
|
|
4
5
|
onsubmit: (value: string, files: File[]) => void;
|
|
@@ -17,6 +18,8 @@ type $$ComponentProps = {
|
|
|
17
18
|
rows?: number;
|
|
18
19
|
/** Manual drag handle on the top edge; `none` hides it. */
|
|
19
20
|
resize?: 'none' | 'top';
|
|
21
|
+
/** Mirror overlay segments, forwarded to the Textarea. */
|
|
22
|
+
segments?: (value: string) => TextareaSegment[];
|
|
20
23
|
sendLabel?: string;
|
|
21
24
|
attachLabel?: string;
|
|
22
25
|
leading?: Snippet;
|
|
@@ -231,12 +231,15 @@
|
|
|
231
231
|
|
|
232
232
|
<style>
|
|
233
233
|
/* Reset the UA dialog box: we own the surface via the inner .sheet. The
|
|
234
|
-
dialog itself is just the centering/backdrop layer.
|
|
234
|
+
dialog itself is just the centering/backdrop layer. It must never scroll:
|
|
235
|
+
the UA `overflow: auto` lets the initial focus scroll the sheet into view
|
|
236
|
+
while it still sits at `translate: 0 100%`, which eats the slide-in. */
|
|
235
237
|
.modal {
|
|
236
238
|
margin: 0;
|
|
237
239
|
padding: 0;
|
|
238
240
|
border: 0;
|
|
239
241
|
background: none;
|
|
242
|
+
overflow: clip;
|
|
240
243
|
max-width: 100vw;
|
|
241
244
|
max-height: 100dvh;
|
|
242
245
|
width: 100%;
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
|
|
84
84
|
const uid = Math.random().toString(36).slice(2, 8);
|
|
85
85
|
const listId = `rs-list-${uid}`;
|
|
86
|
+
const nameId = `rs-name-${uid}`;
|
|
86
87
|
const optionId = (i: number) => `rs-opt-${uid}-${i}`;
|
|
87
88
|
|
|
88
89
|
let root = $state<HTMLDivElement | null>(null);
|
|
@@ -96,6 +97,7 @@
|
|
|
96
97
|
|
|
97
98
|
const selectedIndex = $derived(options.findIndex((o) => getKey(o) === value));
|
|
98
99
|
const selectedOption = $derived(selectedIndex >= 0 ? options[selectedIndex] : undefined);
|
|
100
|
+
const triggerName = $derived(selectedOption === undefined ? label : `${label}, ${getLabel(selectedOption)}`);
|
|
99
101
|
const typeahead = createTypeahead();
|
|
100
102
|
|
|
101
103
|
const triggerEl = () => root?.querySelector<HTMLElement>('.rs-trigger') ?? null;
|
|
@@ -150,6 +152,7 @@
|
|
|
150
152
|
if (key !== value) {
|
|
151
153
|
value = key;
|
|
152
154
|
onchange?.(key, next);
|
|
155
|
+
if (!closeAfter) announcement = getLabel(next);
|
|
153
156
|
playSound(closeAfter ? 'click' : 'tick', sound);
|
|
154
157
|
}
|
|
155
158
|
if (closeAfter) {
|
|
@@ -354,6 +357,7 @@
|
|
|
354
357
|
onopen={onOpen}
|
|
355
358
|
onclose={onClose}
|
|
356
359
|
aria-controls={listId}
|
|
360
|
+
aria-labelledby={nameId}
|
|
357
361
|
aria-activedescendant={open && active >= 0 ? optionId(active) : undefined}
|
|
358
362
|
>
|
|
359
363
|
{#snippet trigger()}
|
|
@@ -407,6 +411,7 @@
|
|
|
407
411
|
<div class="rs-footer">{@render footer({ close })}</div>
|
|
408
412
|
{/if}
|
|
409
413
|
</Popover>
|
|
414
|
+
<span class="sr-only" id={nameId}>{triggerName}</span>
|
|
410
415
|
<span class="sr-only" aria-live="polite">{announcement}</span>
|
|
411
416
|
</div>
|
|
412
417
|
|
|
@@ -487,8 +492,10 @@
|
|
|
487
492
|
.rich-select.is-open .rs-chevron {
|
|
488
493
|
rotate: 180deg;
|
|
489
494
|
}
|
|
490
|
-
.rich-select :global(.pop-panel.rs-panel) {
|
|
495
|
+
.rich-select :global(.pop-panel.rs-panel:popover-open) {
|
|
491
496
|
display: flex;
|
|
497
|
+
}
|
|
498
|
+
.rich-select :global(.pop-panel.rs-panel) {
|
|
492
499
|
flex-direction: column;
|
|
493
500
|
gap: var(--sp-1);
|
|
494
501
|
box-sizing: border-box;
|
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export { default as Spinner } from './components/atoms/Spinner.svelte';
|
|
|
31
31
|
export { default as Swatch } from './components/atoms/Swatch.svelte';
|
|
32
32
|
export { default as Switch } from './components/atoms/Switch.svelte';
|
|
33
33
|
export { default as Text } from './components/atoms/Text.svelte';
|
|
34
|
+
export type { TextareaSegment } from './components/atoms/Textarea.svelte';
|
|
34
35
|
export { default as Textarea } from './components/atoms/Textarea.svelte';
|
|
35
36
|
export { default as Thinking } from './components/atoms/Thinking.svelte';
|
|
36
37
|
export { default as TimeDisplay } from './components/atoms/TimeDisplay.svelte';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dorsk/tsumikit",
|
|
3
|
-
"version": "0.82.
|
|
3
|
+
"version": "0.82.2",
|
|
4
4
|
"description": "Minimal, dependency-free Svelte 5 + pure-CSS UI kit. Token-driven atoms, molecules & layouts with theming out of the box.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -72,7 +72,8 @@
|
|
|
72
72
|
"design": "npm run package && npm run design:bundle && npm run design:verify",
|
|
73
73
|
"design:bundle": "node .design-sync/build-bundle.mjs",
|
|
74
74
|
"design:verify": "node .design-sync/verify-bundle.mjs",
|
|
75
|
-
"design:clean": "rm -rf ds-bundle .ds-proto"
|
|
75
|
+
"design:clean": "rm -rf ds-bundle .ds-proto",
|
|
76
|
+
"demo:props": "node scripts/gen-demo-props.mjs"
|
|
76
77
|
},
|
|
77
78
|
"peerDependencies": {
|
|
78
79
|
"svelte": "^5.0.0"
|