@dorsk/tsumikit 0.29.0 → 0.31.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.
@@ -3,17 +3,24 @@
3
3
  import type { HTMLButtonAttributes } from 'svelte/elements';
4
4
 
5
5
  type ButtonProps = HTMLButtonAttributes & {
6
- variant?: 'default' | 'primary' | 'ghost' | 'danger';
6
+ // `link`: no box at all an inline text action coloured like a link
7
+ // (or like `tone` when set), underlined on hover/focus.
8
+ variant?: 'default' | 'primary' | 'ghost' | 'danger' | 'link';
7
9
  // Semantic state tint layered on top of the variant: tints text/border and
8
10
  // adds a subtle fill on hover. For stateful controls — an "on"/active toggle
9
11
  // (`accent`), positive actions (`success`), or cost/severity states.
10
12
  tone?: 'none' | 'accent' | 'success' | 'info' | 'warn' | 'danger';
11
13
  size?: 'sm' | 'md' | 'lg';
12
14
  control?: boolean;
15
+ // Fully rounded ends (`--r-pill`) on any size/variant.
16
+ pill?: boolean;
13
17
  block?: boolean;
14
18
  // Fill the available width of a flex/Cluster row (flex: 1). `block` stretches
15
19
  // to 100% (grid/stack); `grow` shares a flex row with siblings.
16
20
  grow?: boolean;
21
+ // `shrink={false}` pins the button to its intrinsic width in a flex row
22
+ // (flex: none, top-aligned) so a growing sibling can't squash it.
23
+ shrink?: boolean;
17
24
  // Render as a link (`<a href>`) while keeping button chrome — for navigations
18
25
  // and open-in-new-tab actions that shouldn't be a bare <a>. Setting `href`
19
26
  // implies `as="a"`.
@@ -56,8 +63,10 @@
56
63
  tone = 'none',
57
64
  size = 'md',
58
65
  control = false,
66
+ pill = false,
59
67
  block = false,
60
68
  grow = false,
69
+ shrink = true,
61
70
  as = 'button',
62
71
  href,
63
72
  icon = false,
@@ -113,6 +122,9 @@
113
122
  class:btn-primary={variant === 'primary'}
114
123
  class:btn-ghost={variant === 'ghost'}
115
124
  class:btn-danger={variant === 'danger' || (tone === 'danger' && variant === 'default')}
125
+ class:btn-link={variant === 'link'}
126
+ class:btn-pill={pill}
127
+ class:no-shrink={!shrink}
116
128
  class:btn-sm={size === 'sm'}
117
129
  class:btn-lg={size === 'lg'}
118
130
  class:btn-control={control}
@@ -354,6 +366,33 @@
354
366
  filter: brightness(1.08);
355
367
  }
356
368
 
369
+ .no-shrink {
370
+ flex: none;
371
+ align-self: flex-start;
372
+ }
373
+ .btn-pill {
374
+ border-radius: var(--r-pill);
375
+ }
376
+ /* Link variant: no box, inherits the surrounding text size. Placed after the
377
+ size rules so their min-height doesn't reapply; a tone wins over --link. */
378
+ .btn-link {
379
+ min-height: auto;
380
+ height: auto;
381
+ padding: 0 var(--sp-1);
382
+ border: none;
383
+ background: none;
384
+ box-shadow: none;
385
+ border-radius: 0;
386
+ font-size: inherit;
387
+ line-height: 1;
388
+ color: var(--btn-tone, var(--link));
389
+ }
390
+ .btn-link:hover:not(:disabled),
391
+ .btn-link:focus-visible {
392
+ background: none;
393
+ border: none;
394
+ text-decoration: underline;
395
+ }
357
396
  /* Icon-only buttons (IconButton). Square box = consistent --box-md tap target. */
358
397
  .btn-icon {
359
398
  min-height: var(--box-md);
@@ -1,12 +1,14 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLButtonAttributes } from 'svelte/elements';
3
3
  type ButtonProps = HTMLButtonAttributes & {
4
- variant?: 'default' | 'primary' | 'ghost' | 'danger';
4
+ variant?: 'default' | 'primary' | 'ghost' | 'danger' | 'link';
5
5
  tone?: 'none' | 'accent' | 'success' | 'info' | 'warn' | 'danger';
6
6
  size?: 'sm' | 'md' | 'lg';
7
7
  control?: boolean;
8
+ pill?: boolean;
8
9
  block?: boolean;
9
10
  grow?: boolean;
11
+ shrink?: boolean;
10
12
  as?: 'button' | 'a';
11
13
  href?: string;
12
14
  target?: string;
@@ -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>
@@ -0,0 +1,8 @@
1
+ type Props = {
2
+ keys: string | string[];
3
+ size?: 'sm' | 'md';
4
+ class?: string;
5
+ };
6
+ declare const Kbd: import("svelte").Component<Props, {}, "">;
7
+ type Kbd = ReturnType<typeof Kbd>;
8
+ export default Kbd;
@@ -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 = true,
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 class="select-wrap {klass}" class:no-chevron={!chevron} data-tsu="Select">
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 chevron}
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;
@@ -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;
@@ -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}<span class="hint">{hint}</span>{/if}
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;
@@ -16,6 +16,7 @@
16
16
  disabled = false,
17
17
  variant = 'default',
18
18
  size = 'md',
19
+ control = false,
19
20
  box,
20
21
  hitArea = 'auto',
21
22
  class: klass = ''
@@ -38,6 +39,9 @@
38
39
  /** Match the Button atom's sizes so a FileButton lines up with buttons in
39
40
  * the same row. */
40
41
  size?: 'sm' | 'md' | 'lg';
42
+ /** Uniform `--control-height`, like Button's `control`, so it lines up in a
43
+ * composer/toolbar row. */
44
+ control?: boolean;
41
45
  /** Shared square box scale (`--box-xs/sm/md/lg`); implies `iconOnly`. */
42
46
  box?: 'xs' | 'sm' | 'md' | 'lg';
43
47
  /** Icon-only buttons grow a 44px hit slab on coarse pointers; `compact` opts out. */
@@ -62,6 +66,7 @@
62
66
  class:ghost={variant === 'ghost'}
63
67
  class:sm={size === 'sm'}
64
68
  class:lg={size === 'lg'}
69
+ class:control
65
70
  class:icon-only={onlyIcon}
66
71
  class:box={box !== undefined}
67
72
  class:hit-compact={hitArea === 'compact'}
@@ -125,6 +130,11 @@
125
130
  padding: var(--sp-3) var(--sp-5);
126
131
  font-size: var(--fs-base);
127
132
  }
133
+ .file-btn.control {
134
+ height: var(--control-height);
135
+ min-height: var(--control-height);
136
+ padding: 0 var(--sp-3);
137
+ }
128
138
  /* icon-only: square it up and drop the label gap. */
129
139
  .file-btn.icon-only {
130
140
  gap: 0;
@@ -18,6 +18,9 @@ type $$ComponentProps = {
18
18
  /** Match the Button atom's sizes so a FileButton lines up with buttons in
19
19
  * the same row. */
20
20
  size?: 'sm' | 'md' | 'lg';
21
+ /** Uniform `--control-height`, like Button's `control`, so it lines up in a
22
+ * composer/toolbar row. */
23
+ control?: boolean;
21
24
  /** Shared square box scale (`--box-xs/sm/md/lg`); implies `iconOnly`. */
22
25
  box?: 'xs' | 'sm' | 'md' | 'lg';
23
26
  /** Icon-only buttons grow a 44px hit slab on coarse pointers; `compact` opts out. */
@@ -16,6 +16,11 @@
16
16
  children?: Snippet;
17
17
  label: string;
18
18
  variant?: 'default' | 'primary' | 'ghost' | 'danger';
19
+ // Fully rounded (`--r-pill`) box. Forwarded to Button.
20
+ pill?: boolean;
21
+ // `shrink={false}` pins the box to its intrinsic size in a flex row.
22
+ // Forwarded to Button.
23
+ shrink?: boolean;
19
24
  // Semantic state tint (forwarded to Button). Pairs well with `chip`.
20
25
  tone?: 'none' | 'accent' | 'info' | 'warn' | 'danger';
21
26
  // Outlined `--box-lg` square icon-chip (header/toolbar actions).
@@ -55,6 +60,8 @@
55
60
  variant = 'ghost',
56
61
  tone = 'none',
57
62
  chip = false,
63
+ pill = false,
64
+ shrink = true,
58
65
  box,
59
66
  size = 18,
60
67
  glyphSize,
@@ -82,6 +89,8 @@
82
89
  {variant}
83
90
  {tone}
84
91
  {chip}
92
+ {pill}
93
+ {shrink}
85
94
  {box}
86
95
  {hitArea}
87
96
  {disabled}
@@ -12,6 +12,8 @@ type IconButtonProps = HTMLButtonAttributes & {
12
12
  children?: Snippet;
13
13
  label: string;
14
14
  variant?: 'default' | 'primary' | 'ghost' | 'danger';
15
+ pill?: boolean;
16
+ shrink?: boolean;
15
17
  tone?: 'none' | 'accent' | 'info' | 'warn' | 'danger';
16
18
  chip?: boolean;
17
19
  box?: 'xs' | 'sm' | 'md' | 'lg';
@@ -14,6 +14,9 @@
14
14
  pressed = false,
15
15
  pill = false,
16
16
  struck = false,
17
+ size = 'sm',
18
+ grow = false,
19
+ shrink = true,
17
20
  type = 'button',
18
21
  disabled = false,
19
22
  class: klass = '',
@@ -23,6 +26,11 @@
23
26
  pressed?: boolean;
24
27
  pill?: boolean;
25
28
  struck?: boolean;
29
+ size?: 'sm' | 'md';
30
+ // Share a flex row with siblings (flex: 1 1 0).
31
+ grow?: boolean;
32
+ // `shrink={false}` keeps the chip at its intrinsic width (flex: none).
33
+ shrink?: boolean;
26
34
  children?: Snippet;
27
35
  } = $props();
28
36
  </script>
@@ -35,6 +43,9 @@
35
43
  class="toggle {klass}"
36
44
  class:pill
37
45
  class:struck
46
+ class:md={size === 'md'}
47
+ class:grow
48
+ class:no-shrink={!shrink}
38
49
  class:on={pressed}
39
50
  aria-pressed={pressed}
40
51
  >
@@ -69,6 +80,17 @@
69
80
  .toggle.pill {
70
81
  border-radius: var(--r-pill);
71
82
  }
83
+ .toggle.md {
84
+ padding: 0.3rem var(--sp-2);
85
+ font-size: var(--fs-sm);
86
+ }
87
+ .toggle.grow {
88
+ flex: 1 1 0;
89
+ min-width: 0;
90
+ }
91
+ .toggle.no-shrink {
92
+ flex: none;
93
+ }
72
94
  .toggle:hover:not(:disabled) {
73
95
  border-color: var(--border-strong);
74
96
  }
@@ -4,6 +4,9 @@ type $$ComponentProps = HTMLButtonAttributes & {
4
4
  pressed?: boolean;
5
5
  pill?: boolean;
6
6
  struck?: boolean;
7
+ size?: 'sm' | 'md';
8
+ grow?: boolean;
9
+ shrink?: boolean;
7
10
  children?: Snippet;
8
11
  };
9
12
  declare const Toggle: import("svelte").Component<$$ComponentProps, {}, "">;
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ 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';
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ 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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorsk/tsumikit",
3
- "version": "0.29.0",
3
+ "version": "0.31.0",
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",