@dorsk/tsumikit 0.62.0 → 0.63.1

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 CHANGED
@@ -206,6 +206,7 @@ sanctioned escape hatch for everything else.
206
206
  | `GitRef` | `--git-ref-tone`, `--git-ref-max-width` |
207
207
  | `EmptyState` | `--empty-tone` |
208
208
  | `Combobox` | `--cb-bg`, `--cb-border`, `--cb-radius`, `--cb-active-bg`, `--cb-min-width`, `--cb-max-height` |
209
+ | `Toggle` | `--toggle-accent`, `--toggle-size` |
209
210
  | `Popover` | `--pop-trigger-bg`, `--pop-trigger-fg`, `--pop-trigger-border`, `--pop-trigger-radius`, `--pop-trigger-size`, `--pop-trigger-pad`, `--pop-box`, `--pop-count-bg`, `--pop-count-fg`, `--pop-count-size` |
210
211
 
211
212
  `tests/css-custom-property-contract.test.js` reads this table and fails if a
@@ -763,6 +764,21 @@ triggers, and `SegmentedControl`. Each renders an exact
763
764
  original ghost icon-button default. Use `control` on `Button` or `Popover` when
764
765
  the roomier shared `--control-height` composer contract is required.
765
766
 
767
+ `variant="toggle"` renders the trigger as a `Toggle` chip — the component
768
+ itself, not a copy of its chrome — so a popover trigger sitting next to a
769
+ `Toggle` in a toolbar shares one box. `size` (`sm` default, `md`), `pill`,
770
+ `pressed` and `tone` behave as they do on `Toggle`; a `Toggle`'s height comes
771
+ from `--toggle-size`, never from its content, so an emoji, an `Icon` and text
772
+ all leave the chip the same height.
773
+
774
+ ```svelte
775
+ <Toggle pressed={auto} onclick={toggleAuto}><Icon name="zap" size={12} /> Auto</Toggle>
776
+ <Popover label="Pins" variant="toggle" pressed={pinned.length > 0} count={pinned.length}>
777
+ {#snippet trigger()}Pins{/snippet}
778
+ {#snippet children()}…{/snippet}
779
+ </Popover>
780
+ ```
781
+
766
782
  `Toolbar` implements the keyboard contract its `role="toolbar"` announces: the
767
783
  bar is a **single tab stop**, and `←`/`→` (plus `Home`/`End`) move between the
768
784
  controls that are actually visible — collapsed `data-overflow` children drop out
@@ -95,6 +95,7 @@
95
95
  sun: '<circle cx="12" cy="12" r="4" /><path d="M12 2v2" /><path d="M12 20v2" /><path d="m4.93 4.93 1.41 1.41" /><path d="m17.66 17.66 1.41 1.41" /><path d="M2 12h2" /><path d="M20 12h2" /><path d="m6.34 17.66-1.41 1.41" /><path d="m19.07 4.93-1.41 1.41" />',
96
96
  moon: '<path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401" />',
97
97
  flame: '<path d="M12 3q1 4 4 6.5t3 5.5a1 1 0 0 1-14 0 5 5 0 0 1 1-3 1 1 0 0 0 5 0c0-2-1.5-3-1.5-5q0-2 2.5-4" />',
98
+ zap: '<path d="M13 2 4.093 12.688a1 1 0 0 0 .764 1.642H11l-1 7.67L18.907 11.31a1 1 0 0 0-.764-1.642H13z" />',
98
99
  loader: '<path d="M21 12a9 9 0 1 1-6.219-8.56" />',
99
100
 
100
101
  // — circled status —
@@ -78,6 +78,7 @@ declare const ICONS: {
78
78
  readonly sun: "<circle cx=\"12\" cy=\"12\" r=\"4\" /><path d=\"M12 2v2\" /><path d=\"M12 20v2\" /><path d=\"m4.93 4.93 1.41 1.41\" /><path d=\"m17.66 17.66 1.41 1.41\" /><path d=\"M2 12h2\" /><path d=\"M20 12h2\" /><path d=\"m6.34 17.66-1.41 1.41\" /><path d=\"m19.07 4.93-1.41 1.41\" />";
79
79
  readonly moon: "<path d=\"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401\" />";
80
80
  readonly flame: "<path d=\"M12 3q1 4 4 6.5t3 5.5a1 1 0 0 1-14 0 5 5 0 0 1 1-3 1 1 0 0 0 5 0c0-2-1.5-3-1.5-5q0-2 2.5-4\" />";
81
+ readonly zap: "<path d=\"M13 2 4.093 12.688a1 1 0 0 0 .764 1.642H11l-1 7.67L18.907 11.31a1 1 0 0 0-.764-1.642H13z\" />";
81
82
  readonly loader: "<path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />";
82
83
  readonly info: "<circle cx=\"12\" cy=\"12\" r=\"10\" /><path d=\"M12 16v-4\" /><path d=\"M12 8h.01\" />";
83
84
  readonly warning: "<path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\" /><path d=\"M12 9v4\" /><path d=\"M12 17h.01\" />";
@@ -525,7 +525,9 @@
525
525
 
526
526
  /* Overlay mode: a fixed non-modal drawer on the viewport edge. The page
527
527
  (`.main`) flows as normal underneath; the handle overhang is a viewport
528
- edge, so the container clamp does not apply. */
528
+ edge, so the container clamp does not apply. Widths clamp against the
529
+ containing block, never `100vw`: the reset's `scrollbar-gutter: stable`
530
+ makes `100vw` wider than the viewport and clips the panel's leading edge. */
529
531
  .panel-layout.overlay {
530
532
  display: block;
531
533
  container: none;
@@ -535,7 +537,7 @@
535
537
  inset-block: 0;
536
538
  left: 0;
537
539
  z-index: var(--z-drawer);
538
- width: min(var(--panel-current-width), 100vw);
540
+ width: min(var(--panel-current-width), 100%);
539
541
  box-shadow: var(--shadow-md);
540
542
  outline: none;
541
543
  animation: panel-slide-left 0.18s var(--ease);
@@ -549,7 +551,9 @@
549
551
  --handle-shift: -6px;
550
552
  }
551
553
  .overlay.full-bleed .panel {
552
- width: 100vw;
554
+ left: 0;
555
+ right: 0;
556
+ width: auto;
553
557
  border: 0;
554
558
  box-shadow: none;
555
559
  }
@@ -9,7 +9,7 @@
9
9
  // controls actually visible, the `…` trigger included. Children are arbitrary
10
10
  // snippet content, so the ring is read from the DOM rather than a list prop.
11
11
  import type { Snippet } from 'svelte';
12
- import IconButton from '../molecules/IconButton.svelte';
12
+ import Icon from '../atoms/Icon.svelte';
13
13
  import Menu, { type MenuItem } from '../molecules/Menu.svelte';
14
14
  import Popover from '../molecules/Popover.svelte';
15
15
  import {
@@ -161,12 +161,12 @@
161
161
  {@render children()}
162
162
  {#if collapsed && hasOverflow}
163
163
  {#if items?.length}
164
- <Menu label={overflowLabel} {items} placement="bottom-end" box="sm">
165
- {#snippet trigger()}<IconButton icon="more" label={overflowLabel} box="sm" />{/snippet}
164
+ <Menu label={overflowLabel} title={overflowLabel} {items} placement="bottom-end" box="sm">
165
+ {#snippet trigger()}<Icon name="more" size={18} />{/snippet}
166
166
  </Menu>
167
167
  {:else if overflow}
168
- <Popover label={overflowLabel} placement="bottom-end" box="sm">
169
- {#snippet trigger()}<IconButton icon="more" label={overflowLabel} box="sm" />{/snippet}
168
+ <Popover label={overflowLabel} title={overflowLabel} placement="bottom-end" box="sm">
169
+ {#snippet trigger()}<Icon name="more" size={18} />{/snippet}
170
170
  <div class="overflow-panel">{@render overflow()}</div>
171
171
  </Popover>
172
172
  {/if}
@@ -266,7 +266,7 @@
266
266
 
267
267
  @media (min-width: 48rem) {
268
268
  .panel {
269
- width: min(var(--drawer-w), 100vw);
269
+ width: min(var(--drawer-w), 100%);
270
270
  padding-top: 0;
271
271
  padding-bottom: 0;
272
272
  border-left: 1px solid var(--border-strong);
@@ -52,6 +52,7 @@
52
52
  | 'control'
53
53
  | 'block'
54
54
  | 'triggerClass'
55
+ | 'title'
55
56
  | 'bare'
56
57
  | 'hitArea'
57
58
  | 'disabled'
@@ -74,6 +75,7 @@
74
75
  control,
75
76
  block,
76
77
  triggerClass,
78
+ title,
77
79
  bare,
78
80
  hitArea,
79
81
  disabled,
@@ -148,6 +150,7 @@
148
150
  {control}
149
151
  {block}
150
152
  {triggerClass}
153
+ {title}
151
154
  class={klass}
152
155
  style={styleProp}
153
156
  {panelClass}
@@ -27,7 +27,7 @@ export interface MenuItem {
27
27
  import type { ComponentProps, Snippet } from 'svelte';
28
28
  import Popover from './Popover.svelte';
29
29
  type PopoverProps = ComponentProps<typeof Popover>;
30
- type TriggerChrome = Pick<PopoverProps, 'variant' | 'tone' | 'size' | 'box' | 'pill' | 'control' | 'block' | 'triggerClass' | 'bare' | 'hitArea' | 'disabled' | 'gap' | 'onopen' | 'onclose'>;
30
+ type TriggerChrome = Pick<PopoverProps, 'variant' | 'tone' | 'size' | 'box' | 'pill' | 'control' | 'block' | 'triggerClass' | 'title' | 'bare' | 'hitArea' | 'disabled' | 'gap' | 'onopen' | 'onclose'>;
31
31
  type $$ComponentProps = {
32
32
  label: string;
33
33
  items: MenuItem[];
@@ -13,10 +13,11 @@
13
13
  import type { HTMLAttributes } from 'svelte/elements';
14
14
  import { formatCount, hasCount } from '../../count';
15
15
  import { place } from '../../floating';
16
+ import Toggle from './Toggle.svelte';
16
17
  import { HOVER_CLOSE_GRACE, HOVER_OPEN_DELAY, createHoverIntent, opensOnHover } from './popover-hover.js';
17
18
 
18
19
  type Placement = 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
19
- type TriggerVariant = 'default' | 'primary' | 'ghost' | 'danger';
20
+ type TriggerVariant = 'default' | 'primary' | 'ghost' | 'danger' | 'toggle';
20
21
  type TriggerTone = 'none' | 'accent' | 'success' | 'info' | 'warn' | 'danger';
21
22
  type TriggerSize = 'sm' | 'md' | 'lg';
22
23
  type PanelRole = 'dialog' | 'menu' | 'listbox' | 'group';
@@ -42,8 +43,12 @@
42
43
  * you fully own (pair with `triggerClass`). */
43
44
  bare?: boolean;
44
45
  /** Button-compatible trigger chrome. Omitting every chrome prop preserves
45
- * the original ghost icon-button trigger. */
46
+ * the original ghost icon-button trigger. `toggle` renders the trigger as
47
+ * the `Toggle` chip itself, so a trigger and a Toggle side by side share
48
+ * one box — `size`, `pill` and `pressed` behave as they do on `Toggle`. */
46
49
  variant?: TriggerVariant;
50
+ /** `variant="toggle"` only: paint the chip in its "on" tint. */
51
+ pressed?: boolean;
47
52
  tone?: TriggerTone;
48
53
  size?: TriggerSize;
49
54
  /** Shared square box scale (`--box-xs/sm/md/lg`) for an icon-only trigger,
@@ -96,6 +101,7 @@
96
101
  triggerClass = '',
97
102
  bare = false,
98
103
  variant,
104
+ pressed = false,
99
105
  tone = 'none',
100
106
  size,
101
107
  box,
@@ -122,12 +128,29 @@
122
128
  ...rest
123
129
  }: Omit<HTMLAttributes<HTMLElement>, keyof Own> & Own = $props();
124
130
 
131
+ // The Toggle chip is not restyled here, it is rendered: the trigger *is* a
132
+ // `Toggle`, so the two cannot drift apart. A link trigger keeps the canonical
133
+ // chrome — `Toggle` is always a button.
134
+ const toggleChrome = $derived(variant === 'toggle' && as === 'button');
125
135
  const canonicalChrome = $derived(
126
- variant !== undefined || tone !== 'none' || size !== undefined || control || block
136
+ !toggleChrome && (variant !== undefined || tone !== 'none' || size !== undefined || control || block)
127
137
  );
128
138
  const counted = $derived(hasCount(count));
129
139
  const countText = $derived(hasCount(count) ? formatCount(count, countMax) : '');
130
140
  const triggerName = $derived(counted ? `${label}, ${count}` : label);
141
+ const toneVar = $derived(
142
+ { none: undefined, accent: 'var(--accent)', success: 'var(--ok)', info: 'var(--info)', warn: 'var(--warn)', danger: 'var(--danger)' }[
143
+ tone
144
+ ]
145
+ );
146
+ // The count pill is absolutely positioned against the chip, which therefore
147
+ // needs to be its containing block; Popover's scoped CSS cannot reach an
148
+ // element rendered by `Toggle`, so the two declarations ride inline.
149
+ const toggleStyle = $derived(
150
+ [counted ? 'position:relative' : '', toneVar ? `--toggle-accent:${toneVar}` : '', styleProp]
151
+ .filter(Boolean)
152
+ .join(';')
153
+ );
131
154
 
132
155
  const id = `pop-${Math.random().toString(36).slice(2, 8)}`;
133
156
  let triggerEl = $state<HTMLElement | null>(null);
@@ -241,7 +264,30 @@
241
264
  }
242
265
  </script>
243
266
 
244
- <svelte:element
267
+ {#if toggleChrome}
268
+ <Toggle
269
+ {...rest}
270
+ {...triggerAttrs}
271
+ {pressed}
272
+ {pill}
273
+ size={size === 'md' ? 'md' : 'sm'}
274
+ data-tsu="Popover"
275
+ class="pop-trigger trigger-toggle {triggerClass} {klass}"
276
+ style={toggleStyle}
277
+ aria-label={triggerName}
278
+ aria-haspopup={haspopup}
279
+ aria-expanded={open}
280
+ onpointerenter={onPointerEnter}
281
+ onpointerleave={onPointerLeave}
282
+ {@attach (node) => {
283
+ triggerEl = node;
284
+ }}
285
+ >
286
+ {@render trigger()}
287
+ {#if counted}<span class="pop-count count-sm" aria-hidden="true">{countText}</span>{/if}
288
+ </Toggle>
289
+ {:else}
290
+ <svelte:element
245
291
  this={as}
246
292
  bind:this={triggerEl}
247
293
  {...rest}
@@ -280,6 +326,7 @@
280
326
  {@render trigger()}
281
327
  {#if counted}<span class="pop-count" aria-hidden="true">{countText}</span>{/if}
282
328
  </svelte:element>
329
+ {/if}
283
330
 
284
331
  <div
285
332
  bind:this={panelEl}
@@ -495,7 +542,8 @@
495
542
  pointer-events: none;
496
543
  }
497
544
  .trigger-sm .pop-count,
498
- .bare .pop-count {
545
+ .bare .pop-count,
546
+ .pop-count.count-sm {
499
547
  --pop-count-size: 1rem;
500
548
  font-size: calc(var(--fs-xs) * 0.85);
501
549
  }
@@ -1,7 +1,7 @@
1
1
  import { type Snippet } from 'svelte';
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
3
  type Placement = 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
4
- type TriggerVariant = 'default' | 'primary' | 'ghost' | 'danger';
4
+ type TriggerVariant = 'default' | 'primary' | 'ghost' | 'danger' | 'toggle';
5
5
  type TriggerTone = 'none' | 'accent' | 'success' | 'info' | 'warn' | 'danger';
6
6
  type TriggerSize = 'sm' | 'md' | 'lg';
7
7
  type PanelRole = 'dialog' | 'menu' | 'listbox' | 'group';
@@ -28,8 +28,12 @@ type Own = {
28
28
  * you fully own (pair with `triggerClass`). */
29
29
  bare?: boolean;
30
30
  /** Button-compatible trigger chrome. Omitting every chrome prop preserves
31
- * the original ghost icon-button trigger. */
31
+ * the original ghost icon-button trigger. `toggle` renders the trigger as
32
+ * the `Toggle` chip itself, so a trigger and a Toggle side by side share
33
+ * one box — `size`, `pill` and `pressed` behave as they do on `Toggle`. */
32
34
  variant?: TriggerVariant;
35
+ /** `variant="toggle"` only: paint the chip in its "on" tint. */
36
+ pressed?: boolean;
33
37
  tone?: TriggerTone;
34
38
  size?: TriggerSize;
35
39
  /** Shared square box scale (`--box-xs/sm/md/lg`) for an icon-only trigger,
@@ -54,16 +54,20 @@
54
54
  </button>
55
55
 
56
56
  <style>
57
+ /* The height is pinned to the `--toggle-size` box, never to the line box:
58
+ an emoji, a 12px Icon and plain text all leave the chip the same height. */
57
59
  .toggle {
58
60
  display: inline-flex;
59
61
  align-items: center;
60
62
  justify-content: center;
61
63
  gap: 4px;
62
- padding: 0.15rem var(--sp-2);
64
+ box-sizing: border-box;
65
+ height: var(--toggle-size, var(--box-xs));
66
+ padding: 0 var(--sp-2);
63
67
  border-radius: var(--r-sm);
64
68
  font-size: var(--fs-xs);
65
69
  font-weight: var(--fw-medium);
66
- line-height: 1.4;
70
+ line-height: 1;
67
71
  background: var(--bg-elevated-2);
68
72
  color: var(--text-muted);
69
73
  border: 1px solid var(--border);
@@ -82,7 +86,7 @@
82
86
  border-radius: var(--r-pill);
83
87
  }
84
88
  .toggle.md {
85
- padding: 0.3rem var(--sp-2);
89
+ height: var(--toggle-size, var(--control-height-compact));
86
90
  font-size: var(--fs-sm);
87
91
  }
88
92
  .toggle.grow {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorsk/tsumikit",
3
- "version": "0.62.0",
3
+ "version": "0.63.1",
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",