@dorsk/tsumikit 0.16.0 → 0.17.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.
@@ -11,6 +11,16 @@
11
11
  size?: 'sm' | 'md' | 'lg';
12
12
  control?: boolean;
13
13
  block?: boolean;
14
+ // Fill the available width of a flex/Cluster row (flex: 1). `block` stretches
15
+ // to 100% (grid/stack); `grow` shares a flex row with siblings.
16
+ grow?: boolean;
17
+ // Render as a link (`<a href>`) while keeping button chrome — for navigations
18
+ // and open-in-new-tab actions that shouldn't be a bare <a>. Setting `href`
19
+ // implies `as="a"`.
20
+ as?: 'button' | 'a';
21
+ href?: string;
22
+ target?: string;
23
+ rel?: string;
14
24
  // Square 2.25rem icon-only tap target (IconButton). `iconInline` is the
15
25
  // borderless, compact variant (chip-remove ✕, inline edit ✎); pair with
16
26
  // `hoverDanger` to tint it red on hover (delete affordances).
@@ -33,6 +43,9 @@
33
43
  size = 'md',
34
44
  control = false,
35
45
  block = false,
46
+ grow = false,
47
+ as = 'button',
48
+ href,
36
49
  icon = false,
37
50
  chip = false,
38
51
  iconInline = false,
@@ -46,16 +59,27 @@
46
59
  children,
47
60
  ...rest
48
61
  }: ButtonProps = $props();
62
+
63
+ // A link-flavoured Button is a real <a>; native `disabled`/`type` don't apply,
64
+ // so a disabled link is expressed via aria-disabled + inert pointer handling.
65
+ const tag = $derived(href !== undefined || as === 'a' ? 'a' : 'button');
66
+ const inactive = $derived(disabled || loading);
67
+ const elementAttrs = $derived(
68
+ tag === 'a'
69
+ ? { href, 'aria-disabled': inactive || undefined }
70
+ : { type, disabled: inactive }
71
+ );
49
72
  </script>
50
73
 
51
- <button
74
+ <svelte:element
75
+ this={tag}
52
76
  data-tsu="Button"
53
77
  {...rest}
54
- {type}
55
- disabled={disabled || loading}
78
+ {...elementAttrs}
56
79
  aria-busy={loading || undefined}
57
80
  {title}
58
81
  class="btn {klass}"
82
+ class:btn-grow={grow}
59
83
  class:btn-primary={variant === 'primary'}
60
84
  class:btn-ghost={variant === 'ghost'}
61
85
  class:btn-danger={variant === 'danger'}
@@ -77,7 +101,7 @@
77
101
  >
78
102
  {#if loading}<span class="btn-spinner" aria-hidden="true"></span>{/if}
79
103
  {@render children?.()}
80
- </button>
104
+ </svelte:element>
81
105
 
82
106
  <style>
83
107
  /* The canonical control: owns its variants/sizes/modifiers from theme tokens.
@@ -151,6 +175,19 @@
151
175
  .btn-block {
152
176
  width: 100%;
153
177
  }
178
+ .btn-grow {
179
+ flex: 1 1 0;
180
+ }
181
+ /* Link-flavoured Button: keep button chrome, drop the anchor's default link
182
+ look, and make a disabled link truly inert. */
183
+ a.btn {
184
+ text-decoration: none;
185
+ }
186
+ a.btn[aria-disabled='true'] {
187
+ opacity: 0.45;
188
+ pointer-events: none;
189
+ cursor: not-allowed;
190
+ }
154
191
 
155
192
  /* Semantic state tones — tint text + border, subtle fill on hover. Layered on
156
193
  the neutral/ghost variant; each defines --btn-tone so one ruleset paints. */
@@ -6,6 +6,11 @@ type ButtonProps = HTMLButtonAttributes & {
6
6
  size?: 'sm' | 'md' | 'lg';
7
7
  control?: boolean;
8
8
  block?: boolean;
9
+ grow?: boolean;
10
+ as?: 'button' | 'a';
11
+ href?: string;
12
+ target?: string;
13
+ rel?: string;
9
14
  icon?: boolean;
10
15
  chip?: boolean;
11
16
  iconInline?: boolean;
@@ -9,6 +9,9 @@
9
9
  type Props = Omit<HTMLInputAttributes, 'size'> & {
10
10
  mono?: boolean;
11
11
  size?: 'sm' | 'md';
12
+ /** Fill the available width of a flex/Cluster row (flex: 1) instead of
13
+ * needing a raw `style="flex:1"` at the call-site. */
14
+ grow?: boolean;
12
15
  /** Error state: danger border + aria-invalid (also styles if a consumer
13
16
  * sets aria-invalid directly). */
14
17
  invalid?: boolean;
@@ -20,6 +23,7 @@
20
23
  let {
21
24
  mono = false,
22
25
  size = 'md',
26
+ grow = false,
23
27
  invalid = false,
24
28
  class: klass = '',
25
29
  value = $bindable(),
@@ -34,6 +38,7 @@
34
38
  class="input {klass}"
35
39
  class:mono
36
40
  class:input-sm={size === 'sm'}
41
+ class:input-grow={grow}
37
42
  bind:value
38
43
  {...rest}
39
44
  aria-invalid={invalid || undefined}
@@ -57,6 +62,11 @@
57
62
  padding: var(--sp-2);
58
63
  font-size: var(--fs-sm);
59
64
  }
65
+ .input-grow {
66
+ flex: 1 1 0;
67
+ width: auto;
68
+ min-width: 0;
69
+ }
60
70
  .input[aria-invalid='true'] {
61
71
  border-color: var(--danger);
62
72
  }
@@ -2,6 +2,9 @@ import type { HTMLInputAttributes } from 'svelte/elements';
2
2
  type Props = Omit<HTMLInputAttributes, 'size'> & {
3
3
  mono?: boolean;
4
4
  size?: 'sm' | 'md';
5
+ /** Fill the available width of a flex/Cluster row (flex: 1) instead of
6
+ * needing a raw `style="flex:1"` at the call-site. */
7
+ grow?: boolean;
5
8
  /** Error state: danger border + aria-invalid (also styles if a consumer
6
9
  * sets aria-invalid directly). */
7
10
  invalid?: boolean;
@@ -15,12 +15,18 @@
15
15
  import type { HTMLSelectAttributes } from 'svelte/elements';
16
16
  import Icon from './Icon.svelte';
17
17
 
18
- type Props = HTMLSelectAttributes & {
18
+ // `size` shadows the native option-count attribute (unused in token layouts) to
19
+ // expose the sm|md height scale instead.
20
+ type Props = Omit<HTMLSelectAttributes, 'size'> & {
19
21
  variant?: 'default' | 'ghost';
20
22
  /** Error state: danger border + aria-invalid. */
21
23
  invalid?: boolean;
22
- /** Compact inline form: smaller padding + font for dense toolbars/headers. */
24
+ /** Compact inline form: smaller padding + font for dense toolbars/headers.
25
+ * Deprecated alias for `size="sm"` (kept for backward compatibility). */
23
26
  compact?: boolean;
27
+ /** Size scale matching Button/SegmentedControl: `sm` also adopts the shared
28
+ * `--control-height-compact` toolbar height so it lines up with siblings. */
29
+ size?: 'sm' | 'md';
24
30
  /** Draw the custom chevron (default). Set false for a bare inline select. */
25
31
  chevron?: boolean;
26
32
  class?: string;
@@ -32,12 +38,15 @@
32
38
  variant = 'default',
33
39
  invalid = false,
34
40
  compact = false,
41
+ size = 'md',
35
42
  chevron = true,
36
43
  class: klass = '',
37
44
  value = $bindable(),
38
45
  children,
39
46
  ...rest
40
47
  }: Props = $props();
48
+
49
+ const small = $derived(compact || size === 'sm');
41
50
  </script>
42
51
 
43
52
  {#if variant === 'ghost'}
@@ -48,7 +57,8 @@
48
57
  <div class="select-wrap {klass}" class:no-chevron={!chevron} data-tsu="Select">
49
58
  <select
50
59
  class="select"
51
- class:compact
60
+ class:compact={small}
61
+ class:select-sm={size === 'sm'}
52
62
  bind:value
53
63
  {...rest}
54
64
  aria-invalid={invalid || undefined}
@@ -96,6 +106,11 @@
96
106
  .select-wrap:not(.no-chevron) .select.compact {
97
107
  padding-right: calc(var(--sp-2) + 1rem);
98
108
  }
109
+ /* Toolbar contract: size="sm" shares the compact control height so it lines up
110
+ with Button size="sm", Popover size="sm" and SegmentedControl size="sm". */
111
+ .select.select-sm {
112
+ height: var(--control-height-compact);
113
+ }
99
114
  .select:focus {
100
115
  outline: none;
101
116
  border-color: var(--accent);
@@ -1,11 +1,15 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLSelectAttributes } from 'svelte/elements';
3
- type Props = HTMLSelectAttributes & {
3
+ type Props = Omit<HTMLSelectAttributes, 'size'> & {
4
4
  variant?: 'default' | 'ghost';
5
5
  /** Error state: danger border + aria-invalid. */
6
6
  invalid?: boolean;
7
- /** Compact inline form: smaller padding + font for dense toolbars/headers. */
7
+ /** Compact inline form: smaller padding + font for dense toolbars/headers.
8
+ * Deprecated alias for `size="sm"` (kept for backward compatibility). */
8
9
  compact?: boolean;
10
+ /** Size scale matching Button/SegmentedControl: `sm` also adopts the shared
11
+ * `--control-height-compact` toolbar height so it lines up with siblings. */
12
+ size?: 'sm' | 'md';
9
13
  /** Draw the custom chevron (default). Set false for a bare inline select. */
10
14
  chevron?: boolean;
11
15
  class?: string;
@@ -5,12 +5,25 @@
5
5
  // `wrap={false}` to keep one line. Polymorphic via `as`.
6
6
  import type { Snippet } from 'svelte';
7
7
 
8
+ // `size` cascades a uniform control tier to descendant controls that honour the
9
+ // shared `--control-height` contract (Button/IconButton/Popover `control`,
10
+ // SegmentedControl/Select `control`), so a whole toolbar lines up from one prop
11
+ // instead of per-child sizing. `grow` makes direct children share the row width
12
+ // equally (flex: 1) — the toolbar equivalent of `style="flex:1"` on each child.
13
+ const CONTROL_TIER = {
14
+ sm: 'var(--control-height-compact)',
15
+ md: 'var(--control-height-default)',
16
+ lg: 'var(--control-height-large)'
17
+ } as const;
18
+
8
19
  let {
9
20
  as = 'div',
10
21
  gap = 'var(--sp-2)',
11
22
  align = 'center',
12
23
  justify,
13
24
  wrap = true,
25
+ size,
26
+ grow = false,
14
27
  class: klass = '',
15
28
  children,
16
29
  ...rest
@@ -20,6 +33,8 @@
20
33
  align?: string;
21
34
  justify?: string;
22
35
  wrap?: boolean;
36
+ size?: 'sm' | 'md' | 'lg';
37
+ grow?: boolean;
23
38
  class?: string;
24
39
  children?: Snippet;
25
40
  [key: string]: unknown;
@@ -30,10 +45,12 @@
30
45
  this={as}
31
46
  data-tsu="Cluster"
32
47
  class="cluster-c {klass}"
48
+ class:cluster-grow={grow}
33
49
  style:gap
34
50
  style:align-items={align}
35
51
  style:justify-content={justify}
36
52
  style:flex-wrap={wrap ? 'wrap' : 'nowrap'}
53
+ style:--control-height={size ? CONTROL_TIER[size] : undefined}
37
54
  {...rest}
38
55
  >
39
56
  {@render children?.()}
@@ -43,4 +60,8 @@
43
60
  .cluster-c {
44
61
  display: flex;
45
62
  }
63
+ .cluster-grow > :global(*) {
64
+ flex: 1 1 0;
65
+ min-width: 0;
66
+ }
46
67
  </style>
@@ -5,6 +5,8 @@ type $$ComponentProps = {
5
5
  align?: string;
6
6
  justify?: string;
7
7
  wrap?: boolean;
8
+ size?: 'sm' | 'md' | 'lg';
9
+ grow?: boolean;
8
10
  class?: string;
9
11
  children?: Snippet;
10
12
  [key: string]: unknown;
@@ -16,7 +16,9 @@
16
16
  widthKey,
17
17
  collapsed = $bindable(false),
18
18
  persistCollapsed = true,
19
- resizeStep = 16
19
+ resizeStep = 16,
20
+ handlePlacement = 'bottom',
21
+ stickyHandle = true
20
22
  }: {
21
23
  /** Content shown while the panel is expanded. */
22
24
  panel: Snippet;
@@ -38,12 +40,19 @@
38
40
  persistCollapsed?: boolean;
39
41
  /** Pixels added or removed by each resize-separator arrow key press. */
40
42
  resizeStep?: number;
43
+ /** Anchor the collapse handle at the top or bottom of the panel edge. */
44
+ handlePlacement?: 'top' | 'bottom';
45
+ /** Keep the collapse handle in view when the panel scrolls past the
46
+ * viewport, repositioning on scroll/resize via requestAnimationFrame. */
47
+ stickyHandle?: boolean;
41
48
  } = $props();
42
49
 
43
50
  let root: HTMLDivElement;
51
+ let handleEl = $state<HTMLButtonElement | null>(null);
44
52
  let panelWidth = $state<number>();
45
53
  let resizing = $state(false);
46
54
  let restored = false;
55
+ let stickyShift = $state(0);
47
56
 
48
57
  const boundedMin = $derived(Math.max(1, Math.min(minWidth, maxWidth)));
49
58
  const boundedMax = $derived(Math.max(boundedMin, maxWidth));
@@ -96,6 +105,55 @@
96
105
  (nextWidth) => setWidth(nextWidth, false)
97
106
  );
98
107
 
108
+ // Keep the collapse handle within the viewport (and its panel) while a long
109
+ // panel scrolls past the fold. Scroll fires often, so coalesce recomputes
110
+ // into one per animation frame.
111
+ let stickyFrame: number | undefined;
112
+
113
+ function computeSticky() {
114
+ if (!stickyHandle || !root || !handleEl) {
115
+ stickyShift = 0;
116
+ return;
117
+ }
118
+ const bounds = root.getBoundingClientRect();
119
+ const viewport = window.innerHeight || document.documentElement.clientHeight;
120
+ const handleHeight = handleEl.offsetHeight;
121
+ const margin = 8;
122
+ const naturalTop =
123
+ handlePlacement === 'top'
124
+ ? bounds.top + margin
125
+ : bounds.bottom - margin - handleHeight;
126
+ const panelLo = bounds.top + margin;
127
+ const panelHi = bounds.bottom - margin - handleHeight;
128
+ const inViewport = Math.min(Math.max(naturalTop, margin), viewport - margin - handleHeight);
129
+ const clamped = Math.min(Math.max(inViewport, panelLo), Math.max(panelLo, panelHi));
130
+ stickyShift = Math.round(clamped - naturalTop);
131
+ }
132
+
133
+ function scheduleSticky() {
134
+ if (!browser || stickyFrame !== undefined) return;
135
+ stickyFrame = requestAnimationFrame(() => {
136
+ stickyFrame = undefined;
137
+ computeSticky();
138
+ });
139
+ }
140
+
141
+ $effect(() => {
142
+ if (!browser || !stickyHandle) {
143
+ stickyShift = 0;
144
+ return;
145
+ }
146
+ computeSticky();
147
+ addEventListener('scroll', scheduleSticky, true);
148
+ addEventListener('resize', scheduleSticky);
149
+ return () => {
150
+ removeEventListener('scroll', scheduleSticky, true);
151
+ removeEventListener('resize', scheduleSticky);
152
+ if (stickyFrame !== undefined) cancelAnimationFrame(stickyFrame);
153
+ stickyFrame = undefined;
154
+ };
155
+ });
156
+
99
157
  onDestroy(() => pointerWidths.discard());
100
158
 
101
159
  function toggle() {
@@ -196,14 +254,18 @@
196
254
  {/if}
197
255
 
198
256
  <button
257
+ bind:this={handleEl}
199
258
  type="button"
200
259
  class="collapse-control"
260
+ class:top={handlePlacement === 'top'}
261
+ class:bottom={handlePlacement === 'bottom'}
201
262
  aria-label={toggleLabel}
202
263
  aria-expanded={!collapsed}
203
264
  title={toggleLabel}
265
+ style="transform: translateY({stickyShift}px)"
204
266
  onclick={toggle}
205
267
  >
206
- <Icon name={toggleIcon} size={20} />
268
+ <Icon name={toggleIcon} size={14} />
207
269
  </button>
208
270
  </aside>
209
271
 
@@ -260,32 +322,45 @@
260
322
  min-height: 0;
261
323
  overflow: auto;
262
324
  }
325
+ /* Subtle chevron handle anchored to the panel's inner edge — never a filled
326
+ button, never overlapping the neighbouring main content. It stays visible
327
+ when collapsed (the panel is 0px wide) because it lives on the layout edge,
328
+ and its `transform` is nudged by the sticky logic to track the viewport. */
263
329
  .collapse-control {
264
330
  position: absolute;
265
- right: calc(-1 * var(--control-height));
266
- bottom: var(--sp-3);
331
+ left: var(--sp-1);
267
332
  z-index: 3;
268
- display: flex;
333
+ display: inline-flex;
269
334
  align-items: center;
270
335
  justify-content: center;
271
- width: var(--control-height);
272
- height: var(--control-height);
336
+ width: 18px;
337
+ height: 30px;
273
338
  padding: 0;
274
- color: var(--text-muted);
339
+ color: var(--text-faint);
275
340
  font: inherit;
276
- background: var(--bg-elevated-2);
277
- border: 1px solid var(--border);
341
+ background: transparent;
342
+ border: 1px solid transparent;
278
343
  border-radius: var(--r-sm);
279
- box-shadow: var(--shadow-sm);
280
344
  cursor: pointer;
345
+ transition:
346
+ color 0.12s var(--ease),
347
+ background 0.12s var(--ease),
348
+ border-color 0.12s var(--ease);
349
+ }
350
+ .collapse-control.top {
351
+ top: var(--sp-2);
352
+ }
353
+ .collapse-control.bottom {
354
+ bottom: var(--sp-2);
281
355
  }
282
356
  .right .collapse-control {
283
- right: auto;
284
- left: calc(-1 * var(--control-height));
357
+ left: auto;
358
+ right: var(--sp-1);
285
359
  }
286
360
  .collapse-control:hover {
287
361
  color: var(--text);
288
- background: var(--surface);
362
+ background: var(--bg-elevated-2);
363
+ border-color: var(--border);
289
364
  }
290
365
  .collapse-control:focus-visible,
291
366
  .resize-handle:focus-visible {
@@ -20,6 +20,11 @@ type $$ComponentProps = {
20
20
  persistCollapsed?: boolean;
21
21
  /** Pixels added or removed by each resize-separator arrow key press. */
22
22
  resizeStep?: number;
23
+ /** Anchor the collapse handle at the top or bottom of the panel edge. */
24
+ handlePlacement?: 'top' | 'bottom';
25
+ /** Keep the collapse handle in view when the panel scrolls past the
26
+ * viewport, repositioning on scroll/resize via requestAnimationFrame. */
27
+ stickyHandle?: boolean;
23
28
  };
24
29
  declare const ResizablePanel: import("svelte").Component<$$ComponentProps, {}, "collapsed">;
25
30
  type ResizablePanel = ReturnType<typeof ResizablePanel>;
@@ -30,6 +30,10 @@
30
30
  // the glyph with the accent when on. Override the tint per-instance with
31
31
  // `style="--btn-on: var(--warn)"`.
32
32
  pressed?: boolean;
33
+ // Render as a link (`<a href>`) while keeping icon-button chrome — e.g. an
34
+ // open-on-GitHub action. Forwarded to Button; `href` implies `as="a"`.
35
+ as?: 'button' | 'a';
36
+ href?: string;
33
37
  class?: string;
34
38
  };
35
39
 
@@ -18,6 +18,8 @@ type IconButtonProps = HTMLButtonAttributes & {
18
18
  inline?: boolean;
19
19
  hoverDanger?: boolean;
20
20
  pressed?: boolean;
21
+ as?: 'button' | 'a';
22
+ href?: string;
21
23
  class?: string;
22
24
  };
23
25
  declare const IconButton: import("svelte").Component<IconButtonProps, {}, "">;
@@ -13,6 +13,8 @@
13
13
  let {
14
14
  selected = false,
15
15
  row = false,
16
+ align = 'center',
17
+ block = false,
16
18
  type = 'button',
17
19
  disabled = false,
18
20
  class: klass = '',
@@ -21,6 +23,11 @@
21
23
  }: HTMLButtonAttributes & {
22
24
  selected?: boolean;
23
25
  row?: boolean;
26
+ /** Horizontal content alignment. `start` gives a left-aligned list-row
27
+ * variant (pair with `block` for a full-width menu item). */
28
+ align?: 'center' | 'start';
29
+ /** Stretch to fill the container width (full-width list/menu row). */
30
+ block?: boolean;
24
31
  children?: Snippet;
25
32
  } = $props();
26
33
  </script>
@@ -32,6 +39,8 @@
32
39
  {disabled}
33
40
  class="opt {klass}"
34
41
  class:row
42
+ class:align-start={align === 'start'}
43
+ class:block
35
44
  class:selected
36
45
  aria-pressed={selected}
37
46
  >
@@ -71,6 +80,16 @@
71
80
  color: var(--text-muted);
72
81
  font-weight: var(--fw-medium);
73
82
  }
83
+ .opt.block {
84
+ width: 100%;
85
+ }
86
+ /* Left-aligned list-row variant: content packs to the start on both axes. */
87
+ .opt.align-start {
88
+ align-items: flex-start;
89
+ }
90
+ .opt.row.align-start {
91
+ justify-content: flex-start;
92
+ }
74
93
  .opt.selected {
75
94
  --oc: var(--opt-accent, var(--accent));
76
95
  /* Retint slotted `.faint` hint text toward the accent — via an inherited
@@ -3,6 +3,11 @@ import type { HTMLButtonAttributes } from 'svelte/elements';
3
3
  type $$ComponentProps = HTMLButtonAttributes & {
4
4
  selected?: boolean;
5
5
  row?: boolean;
6
+ /** Horizontal content alignment. `start` gives a left-aligned list-row
7
+ * variant (pair with `block` for a full-width menu item). */
8
+ align?: 'center' | 'start';
9
+ /** Stretch to fill the container width (full-width list/menu row). */
10
+ block?: boolean;
6
11
  children?: Snippet;
7
12
  };
8
13
  declare const OptionButton: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -31,6 +31,7 @@
31
31
  // accessible name only); `pill` keeps the labelled filter-pill layout.
32
32
  variant = 'pill',
33
33
  size = 'md',
34
+ control = false,
34
35
  label = 'Options',
35
36
  // `mobile` hides segment labels below the mobile breakpoint, collapsing
36
37
  // labelled segments to centered icon-only squares (aria-label preserved).
@@ -43,6 +44,9 @@
43
44
  value?: string;
44
45
  variant?: 'pill' | 'icon';
45
46
  size?: 'sm' | 'md';
47
+ /** Adopt the shared `--control-height` toolbar contract (like Popover/Button
48
+ * `control`) so the whole segmented control height-matches its siblings. */
49
+ control?: boolean;
46
50
  label?: string;
47
51
  collapseLabels?: 'never' | 'mobile';
48
52
  class?: string;
@@ -84,6 +88,7 @@
84
88
  aria-label={label}
85
89
  tabindex="-1"
86
90
  class="seg seg-{variant} seg-{size} {klass}"
91
+ class:seg-control={control}
87
92
  class:seg-collapse-mobile={collapseLabels === 'mobile'}
88
93
  data-tsu="SegmentedControl"
89
94
  {onkeydown}
@@ -160,6 +165,14 @@
160
165
  .seg-sm .seg-item {
161
166
  height: 100%;
162
167
  }
168
+ /* Shared toolbar/composer height contract — matches Button/Popover `control`. */
169
+ .seg-control {
170
+ height: var(--control-height);
171
+ min-height: var(--control-height);
172
+ }
173
+ .seg-control .seg-item {
174
+ height: 100%;
175
+ }
163
176
  .seg-icon .seg-item {
164
177
  gap: 0;
165
178
  padding: 0.35rem;
@@ -15,6 +15,9 @@ type $$ComponentProps = {
15
15
  value?: string;
16
16
  variant?: 'pill' | 'icon';
17
17
  size?: 'sm' | 'md';
18
+ /** Adopt the shared `--control-height` toolbar contract (like Popover/Button
19
+ * `control`) so the whole segmented control height-matches its siblings. */
20
+ control?: boolean;
18
21
  label?: string;
19
22
  collapseLabels?: 'never' | 'mobile';
20
23
  class?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorsk/tsumikit",
3
- "version": "0.16.0",
3
+ "version": "0.17.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",