@dorsk/tsumikit 0.63.0 → 0.64.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.
package/README.md CHANGED
@@ -248,7 +248,8 @@ any test, and change without a major.
248
248
  `clearable` + `onclear`, `shape="pill"`, `width` fixed + `flex: none`,
249
249
  `onenter(value)`; the bare `<input>` DOM is unchanged unless `icon`/`clearable`
250
250
  is used), Textarea, Select (`options` array with per-option `icon`/`emoji`/`hint`/`disabled`;
251
- the trigger overlays the selected option's glyph + muted hint on the native control), Switch, Checkbox,
251
+ the trigger overlays the selected option's glyph + muted hint on the native control), Switch, Checkbox (`size`
252
+ sm/md/lg as on `Switch`, or `--cb-box` for the box alone),
252
253
  Slider (`ticks` draws a dot per step for a segmented slider), Progress, Gauge (vertical consumption cell, `variant` continuous/segments,
253
254
  threshold tones via `warnAt`/`dangerAt`, `corner` snippet), Artwork (lazy cover image with seeded gradient + initials
254
255
  fallback, `aspect`, `status` overlay), Avatar (grapheme-safe initial or `glyph`, hue hashed from
@@ -266,13 +267,16 @@ horizontal/vertical hairline, `tone`, `spacing`, optional centred label,
266
267
  always/hover/none, `align`), Icon (open registry — pass a `children` snippet for
267
268
  any custom SVG).
268
269
 
269
- **Molecules:** Field (`grow`), IconButton, SelectButton, Toggle, OptionButton (`value` +
270
+ **Molecules:** Field (`grow`), IconButton (`filled` forwards to `Icon`, for a
271
+ filled pin/star toggle), SelectButton, Toggle, OptionButton (`value` +
270
272
  `description` secondary text wired via `aria-describedby`, `onfocuschange(value)` on
271
273
  focus/hover to drive a preview pane), Modal,
272
274
  Popover, Menu (items take a free-form trailing `tag` + `tagTone`, or a `tag` snippet,
273
275
  and an `attrs` object for `data-*`/test ids on the row; a checkable `pressed` item keeps
274
276
  its icon leading and shows its check trailing, or uses the leading check slot when it has
275
- no icon; selecting closes the menu unless `closeOnSelect={false}` or the item sets `keepOpen`),
277
+ no icon; selecting closes the menu unless `closeOnSelect={false}` or the item sets `keepOpen`;
278
+ rest props land on the trigger, `count`/`countMax` paint its corner badge and `panelStyle`
279
+ sizes the panel),
276
280
  Tabs (omit `panel` for a strip-only tablist of document tabs: `closable` + `onclose(id)`
277
281
  close through the ✕, Delete and middle-click, per-tab `leading` snippet, `title`,
278
282
  trailing `actions` snippet; `--tab-max-width` truncates long labels),
@@ -281,9 +285,10 @@ RadioGroup (per-option `description` in every variant, announced via
281
285
  `variant="rows"`: bordered rows, per-option `note`,
282
286
  `action(option)` trailing control that never toggles, `below(option)` inline panel),
283
287
  Tooltip, Disclosure (single collapsible: `header` snippet, bindable `open` or `open` + `onchange` for
284
- controlled mode, `chevron` start/end/false, native button with `aria-expanded`/`aria-controls` over a
285
- `region` panel), Accordion (a stack of Disclosures; items take a `title` string or a `summary` snippet,
286
- per-item `open`/`onchange`, `multiple=false` keeps one open), CopyButton, FileButton, InputGroup (a field with inset
288
+ controlled mode, `chevron` start/end/false, `size="compact"` for strip rows, native button with
289
+ `aria-expanded`/`aria-controls` over a `region` panel), Accordion (a stack of Disclosures; items take a
290
+ `title` string or a `summary` snippet that receives the `item` it heads, per-item `open`/`onchange`,
291
+ `multiple=false` keeps one open, `variant="plain"` drops the outer border, `size` forwards to every item), CopyButton, FileButton, InputGroup (a field with inset
287
292
  leading/trailing controls fused into one bordered unit, see below),
288
293
  Dropzone, CodeBlock, Callout, EmptyState, ConfirmModal, Pagination, Toaster,
289
294
  ThemePicker (popover grid of 2×2 palette swatches: bg · surface · text · accent per theme),
@@ -383,6 +388,11 @@ fixed non-modal drawer (`role="dialog"`) on the viewport edge: `bind:open`,
383
388
  the dim backdrop, `fullWidthBelow="960px"` makes the drawer span the viewport (no
384
389
  handle, no scrim) on small screens, and `clampToViewport` (default true) keeps a
385
390
  stored width inside a shrunken window. `children` is optional in overlay mode.
391
+ `onresizestart` / `onresize(width)` / `onresizeend(width)` report the drag: start
392
+ fires on pointer-down, `onresize` once per animation frame (and per keyboard
393
+ step), `onresizeend` with the settled width on release — enough to suspend
394
+ expensive panel work only while the width is in flux. A keyboard step commits
395
+ without a start.
386
396
 
387
397
  ```svelte
388
398
  <ResizablePanel mode="overlay" side="right" label="Conversation" bind:open
@@ -702,7 +712,11 @@ panel.
702
712
  one outer radius, square inner corners, a 1px divider, two tab stops. The
703
713
  primary takes `onclick`, `type` (submit inside a form) and `loading`; the caret
704
714
  takes `label` (its accessible name), `items`, `placement` and the `Menu`
705
- panel props (`tag`, `panelClass`, `gap`, `open`, `onopen`, `onclose`).
715
+ panel props (`tag`, `panelClass`, `panelStyle`, `gap`, `open`, `onopen`,
716
+ `onclose`). `primaryProps` sets attributes on the primary segment itself
717
+ (`title`, `aria-describedby`, `data-*` — a disabled-with-reason tooltip), while
718
+ rest props stay on the root box; the props SplitButton owns are applied after it,
719
+ so the fused chrome cannot be clobbered.
706
720
  `variant` (default | primary | danger), `size` and `control` apply to both
707
721
  segments and match `Button` heights; `disabled` turns off the whole control,
708
722
  `menuDisabled` only the caret. `caret="half"` narrows the caret to half the
@@ -791,7 +805,7 @@ Button and Popover share the same semantic tones. For a confirmed positive
791
805
  action, `tone="success"` gives neutral controls a success tint; combine it with
792
806
  `variant="primary"` for a filled success action without consumer CSS.
793
807
 
794
- `count` on `Button`, `IconButton` and `Popover` (its trigger) paints a corner
808
+ `count` on `Button`, `IconButton`, `Popover` and `Menu` (their trigger) paints a corner
795
809
  count pill (unread, selected, queued) on any size, variant or `box`. It hides at
796
810
  `0`/`undefined`, displays `99+` past `countMax`, and the exact number joins the
797
811
  accessible name (`"Inbox, 120"`) whether that comes from `aria-label`, `label`
@@ -5,6 +5,7 @@
5
5
  // native attribute via `...rest`.
6
6
  import type { HTMLInputAttributes } from 'svelte/elements';
7
7
  import { getFieldContext } from '../../field-context';
8
+ import type { ControlSize } from '../../size';
8
9
 
9
10
  let {
10
11
  checked = $bindable(false),
@@ -12,13 +13,14 @@
12
13
  invalid = false,
13
14
  label,
14
15
  labelHidden = false,
16
+ size = 'md',
15
17
  id,
16
18
  'aria-describedby': ariaDescribedby,
17
19
  'aria-invalid': ariaInvalid,
18
20
  class: klass = '',
19
21
  el = $bindable(null),
20
22
  ...rest
21
- }: HTMLInputAttributes & {
23
+ }: Omit<HTMLInputAttributes, 'size'> & {
22
24
  checked?: boolean;
23
25
  indeterminate?: boolean;
24
26
  /** Error state: danger box border + aria-invalid. */
@@ -26,6 +28,9 @@
26
28
  label: string;
27
29
  /** Keep `label` for AT only (sr-only). */
28
30
  labelHidden?: boolean;
31
+ /** Box + label scale, as on `Switch`. Override the box alone with
32
+ * `style="--cb-box: 1rem"`. */
33
+ size?: ControlSize;
29
34
  id?: string;
30
35
  'aria-describedby'?: string | null;
31
36
  'aria-invalid'?: HTMLInputAttributes['aria-invalid'];
@@ -41,7 +46,12 @@
41
46
  });
42
47
  </script>
43
48
 
44
- <label class="checkbox {klass}" data-tsu="Checkbox">
49
+ <label
50
+ class="checkbox {klass}"
51
+ class:checkbox-sm={size === 'sm'}
52
+ class:checkbox-lg={size === 'lg'}
53
+ data-tsu="Checkbox"
54
+ >
45
55
  <input
46
56
  bind:this={el}
47
57
  type="checkbox"
@@ -72,11 +82,19 @@
72
82
  opacity: 0;
73
83
  margin: 0;
74
84
  }
85
+ .checkbox-sm {
86
+ --cb-box: 0.95rem;
87
+ font-size: var(--fs-xs);
88
+ }
89
+ .checkbox-lg {
90
+ --cb-box: 1.35rem;
91
+ font-size: var(--fs-base);
92
+ }
75
93
  .box {
76
94
  position: relative;
77
95
  flex: none;
78
- width: 1.15rem;
79
- height: 1.15rem;
96
+ width: var(--cb-box, 1.15rem);
97
+ height: var(--cb-box, 1.15rem);
80
98
  border: 1px solid var(--border-strong);
81
99
  border-radius: var(--r-sm);
82
100
  background: var(--bg);
@@ -1,5 +1,6 @@
1
1
  import type { HTMLInputAttributes } from 'svelte/elements';
2
- type $$ComponentProps = HTMLInputAttributes & {
2
+ import type { ControlSize } from '../../size';
3
+ type $$ComponentProps = Omit<HTMLInputAttributes, 'size'> & {
3
4
  checked?: boolean;
4
5
  indeterminate?: boolean;
5
6
  /** Error state: danger box border + aria-invalid. */
@@ -7,6 +8,9 @@ type $$ComponentProps = HTMLInputAttributes & {
7
8
  label: string;
8
9
  /** Keep `label` for AT only (sr-only). */
9
10
  labelHidden?: boolean;
11
+ /** Box + label scale, as on `Switch`. Override the box alone with
12
+ * `style="--cb-box: 1rem"`. */
13
+ size?: ControlSize;
10
14
  id?: string;
11
15
  'aria-describedby'?: string | null;
12
16
  'aria-invalid'?: HTMLInputAttributes['aria-invalid'];
@@ -35,6 +35,8 @@
35
35
  upload: '<path d="M12 3v12" /><path d="m17 8-5-5-5 5" /><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />',
36
36
  send: '<path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z" /><path d="m21.854 2.147-10.94 10.939" />',
37
37
  share: '<circle cx="18" cy="5" r="3" /><circle cx="6" cy="12" r="3" /><circle cx="18" cy="19" r="3" /><line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /><line x1="15.41" x2="8.59" y1="6.51" y2="10.49" />',
38
+ reply: '<path d="M20 18v-2a4 4 0 0 0-4-4H4" /><path d="m9 17-5-5 5-5" />',
39
+ quote: '<path d="M16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z" /><path d="M5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z" />',
38
40
  retry: '<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" /><path d="M21 3v5h-5" /><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" /><path d="M8 16H3v5" />',
39
41
  settings: '<path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /><circle cx="12" cy="12" r="3" />',
40
42
  recycle: '<path d="M7 19H4.815a1.83 1.83 0 0 1-1.57-.881 1.785 1.785 0 0 1-.004-1.784L7.196 9.5" /><path d="M11 19h8.203a1.83 1.83 0 0 0 1.556-.89 1.784 1.784 0 0 0 0-1.775l-1.226-2.12" /><path d="m14 16-3 3 3 3" /><path d="M8.293 13.596 7.196 9.5 3.1 10.598" /><path d="m9.344 5.811 1.093-1.892A1.83 1.83 0 0 1 11.985 3a1.784 1.784 0 0 1 1.546.888l3.943 6.843" /><path d="m13.378 9.633 4.096 1.098 1.097-4.096" />',
@@ -26,6 +26,8 @@ declare const ICONS: {
26
26
  readonly upload: "<path d=\"M12 3v12\" /><path d=\"m17 8-5-5-5 5\" /><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" />";
27
27
  readonly send: "<path d=\"M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z\" /><path d=\"m21.854 2.147-10.94 10.939\" />";
28
28
  readonly share: "<circle cx=\"18\" cy=\"5\" r=\"3\" /><circle cx=\"6\" cy=\"12\" r=\"3\" /><circle cx=\"18\" cy=\"19\" r=\"3\" /><line x1=\"8.59\" x2=\"15.42\" y1=\"13.51\" y2=\"17.49\" /><line x1=\"15.41\" x2=\"8.59\" y1=\"6.51\" y2=\"10.49\" />";
29
+ readonly reply: "<path d=\"M20 18v-2a4 4 0 0 0-4-4H4\" /><path d=\"m9 17-5-5 5-5\" />";
30
+ readonly quote: "<path d=\"M16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z\" /><path d=\"M5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z\" />";
29
31
  readonly retry: "<path d=\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\" /><path d=\"M21 3v5h-5\" /><path d=\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\" /><path d=\"M8 16H3v5\" />";
30
32
  readonly settings: "<path d=\"M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915\" /><circle cx=\"12\" cy=\"12\" r=\"3\" />";
31
33
  readonly recycle: "<path d=\"M7 19H4.815a1.83 1.83 0 0 1-1.57-.881 1.785 1.785 0 0 1-.004-1.784L7.196 9.5\" /><path d=\"M11 19h8.203a1.83 1.83 0 0 0 1.556-.89 1.784 1.784 0 0 0 0-1.775l-1.226-2.12\" /><path d=\"m14 16-3 3 3 3\" /><path d=\"M8.293 13.596 7.196 9.5 3.1 10.598\" /><path d=\"m9.344 5.811 1.093-1.892A1.83 1.83 0 0 1 11.985 3a1.784 1.784 0 0 1 1.546.888l3.943 6.843\" /><path d=\"m13.378 9.633 4.096 1.098 1.097-4.096\" />";
@@ -19,6 +19,9 @@
19
19
  collapsed = $bindable(false),
20
20
  persistCollapsed = true,
21
21
  resizeStep = 16,
22
+ onresizestart,
23
+ onresize,
24
+ onresizeend,
22
25
  handlePlacement = 'bottom',
23
26
  stickyHandle = true,
24
27
  mode = 'inline',
@@ -53,6 +56,14 @@
53
56
  persistCollapsed?: boolean;
54
57
  /** Pixels added or removed by each resize-separator arrow key press. */
55
58
  resizeStep?: number;
59
+ /** A pointer drag of the separator began. Pair with `onresizeend` to
60
+ * suspend expensive work in the panel body while the width is in flux. */
61
+ onresizestart?: () => void;
62
+ /** Live width, once per animation frame while dragging and on each
63
+ * keyboard step. */
64
+ onresize?: (width: number) => void;
65
+ /** Settled width: on pointer release, and after each keyboard step. */
66
+ onresizeend?: (width: number) => void;
56
67
  /** Anchor the collapse handle at the top or bottom of the panel edge. */
57
68
  handlePlacement?: 'top' | 'bottom';
58
69
  /** Keep the collapse handle in view when the panel scrolls past the
@@ -206,6 +217,17 @@
206
217
 
207
218
  function setWidth(nextWidth: number) {
208
219
  panelWidth = Math.round(Math.max(boundedMin, Math.min(nextWidth, boundedMax)));
220
+ onresize?.(panelWidth);
221
+ }
222
+
223
+ function commitWidth(nextWidth: number) {
224
+ persistWidth(nextWidth);
225
+ onresizeend?.(nextWidth);
226
+ }
227
+
228
+ function setResizing(active: boolean) {
229
+ resizing = active;
230
+ if (active) onresizestart?.();
209
231
  }
210
232
 
211
233
  // Keep the collapse handle within the viewport (and its panel) while a long
@@ -322,8 +344,8 @@
322
344
  step: resizeStep,
323
345
  measure: () => currentWidth,
324
346
  onwidth: setWidth,
325
- oncommit: persistWidth,
326
- onactive: (active) => (resizing = active)
347
+ oncommit: commitWidth,
348
+ onactive: setResizing
327
349
  }}
328
350
  ></div>
329
351
  {/if}
@@ -525,7 +547,9 @@
525
547
 
526
548
  /* Overlay mode: a fixed non-modal drawer on the viewport edge. The page
527
549
  (`.main`) flows as normal underneath; the handle overhang is a viewport
528
- edge, so the container clamp does not apply. */
550
+ edge, so the container clamp does not apply. Widths clamp against the
551
+ containing block, never `100vw`: the reset's `scrollbar-gutter: stable`
552
+ makes `100vw` wider than the viewport and clips the panel's leading edge. */
529
553
  .panel-layout.overlay {
530
554
  display: block;
531
555
  container: none;
@@ -535,7 +559,7 @@
535
559
  inset-block: 0;
536
560
  left: 0;
537
561
  z-index: var(--z-drawer);
538
- width: min(var(--panel-current-width), 100vw);
562
+ width: min(var(--panel-current-width), 100%);
539
563
  box-shadow: var(--shadow-md);
540
564
  outline: none;
541
565
  animation: panel-slide-left 0.18s var(--ease);
@@ -549,7 +573,9 @@
549
573
  --handle-shift: -6px;
550
574
  }
551
575
  .overlay.full-bleed .panel {
552
- width: 100vw;
576
+ left: 0;
577
+ right: 0;
578
+ width: auto;
553
579
  border: 0;
554
580
  box-shadow: none;
555
581
  }
@@ -22,6 +22,14 @@ type $$ComponentProps = {
22
22
  persistCollapsed?: boolean;
23
23
  /** Pixels added or removed by each resize-separator arrow key press. */
24
24
  resizeStep?: number;
25
+ /** A pointer drag of the separator began. Pair with `onresizeend` to
26
+ * suspend expensive work in the panel body while the width is in flux. */
27
+ onresizestart?: () => void;
28
+ /** Live width, once per animation frame while dragging and on each
29
+ * keyboard step. */
30
+ onresize?: (width: number) => void;
31
+ /** Settled width: on pointer release, and after each keyboard step. */
32
+ onresizeend?: (width: number) => void;
25
33
  /** Anchor the collapse handle at the top or bottom of the panel edge. */
26
34
  handlePlacement?: 'top' | 'bottom';
27
35
  /** Keep the collapse handle in view when the panel scrolls past the
@@ -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}
@@ -1,12 +1,18 @@
1
1
  <script lang="ts" module>
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { DisclosureHeaderContext } from './Disclosure.svelte';
4
+ export type AccordionVariant = 'default' | 'plain';
5
+ /** What a `summary` snippet receives: the live open state plus the item it is
6
+ * rendering, so one shared snippet can head every item. */
7
+ export interface AccordionHeaderContext extends DisclosureHeaderContext {
8
+ item: AccordionItem;
9
+ }
4
10
  export interface AccordionItem {
5
11
  id: string;
6
12
  /** Plain-text header; `summary` wins when both are given. */
7
13
  title?: string;
8
- /** Rich header content; receives the live open state. */
9
- summary?: Snippet<[DisclosureHeaderContext]>;
14
+ /** Rich header content; receives the live open state and this item. */
15
+ summary?: Snippet<[AccordionHeaderContext]>;
10
16
  /** Panel content for this item. */
11
17
  content: Snippet;
12
18
  /** Open state. Re-passing a new value takes over from any local toggle. */
@@ -22,12 +28,14 @@
22
28
  // closes the other items when one opens.
23
29
  import type { HTMLAttributes } from 'svelte/elements';
24
30
  import Disclosure from './Disclosure.svelte';
25
- import type { DisclosureChevron } from './Disclosure.svelte';
31
+ import type { DisclosureChevron, DisclosureSize } from './Disclosure.svelte';
26
32
 
27
33
  let {
28
34
  items,
29
35
  multiple = true,
30
36
  chevron = 'end',
37
+ variant = 'default',
38
+ size = 'default',
31
39
  onchange,
32
40
  class: klass = '',
33
41
  style: styleProp = '',
@@ -38,6 +46,11 @@
38
46
  items: AccordionItem[];
39
47
  multiple?: boolean;
40
48
  chevron?: DisclosureChevron;
49
+ /** `plain` drops the outer border and radius and keeps the item dividers,
50
+ * for a stack that only separates rows. */
51
+ variant?: AccordionVariant;
52
+ /** Header/panel density of every item; forwarded to `Disclosure`. */
53
+ size?: DisclosureSize;
41
54
  onchange?: (id: string, open: boolean) => void;
42
55
  class?: string;
43
56
  style?: string;
@@ -75,7 +88,13 @@
75
88
  <span class="acc-title">{title}</span>
76
89
  {/snippet}
77
90
 
78
- <div {...rest} class="accordion {klass}" style={styleProp} data-tsu="Accordion">
91
+ <div
92
+ {...rest}
93
+ class="accordion {klass}"
94
+ class:accordion--plain={variant === 'plain'}
95
+ style={styleProp}
96
+ data-tsu="Accordion"
97
+ >
79
98
  {#each items as item (item.id)}
80
99
  <Disclosure
81
100
  class="acc-item"
@@ -84,10 +103,11 @@
84
103
  onchange={(o) => set(item, o)}
85
104
  disabled={item.disabled}
86
105
  {chevron}
106
+ {size}
87
107
  >
88
108
  {#snippet header(ctx)}
89
109
  {#if item.summary}
90
- {@render item.summary(ctx)}
110
+ {@render item.summary({ ...ctx, item })}
91
111
  {:else}
92
112
  {@render plain(item.title)}
93
113
  {/if}
@@ -103,6 +123,10 @@
103
123
  border-radius: var(--r-lg);
104
124
  overflow: hidden;
105
125
  }
126
+ .accordion--plain {
127
+ border: 0;
128
+ border-radius: 0;
129
+ }
106
130
  .accordion > :global(.acc-item + .acc-item) {
107
131
  border-top: 1px solid var(--border);
108
132
  }
@@ -1,11 +1,17 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { DisclosureHeaderContext } from './Disclosure.svelte';
3
+ export type AccordionVariant = 'default' | 'plain';
4
+ /** What a `summary` snippet receives: the live open state plus the item it is
5
+ * rendering, so one shared snippet can head every item. */
6
+ export interface AccordionHeaderContext extends DisclosureHeaderContext {
7
+ item: AccordionItem;
8
+ }
3
9
  export interface AccordionItem {
4
10
  id: string;
5
11
  /** Plain-text header; `summary` wins when both are given. */
6
12
  title?: string;
7
- /** Rich header content; receives the live open state. */
8
- summary?: Snippet<[DisclosureHeaderContext]>;
13
+ /** Rich header content; receives the live open state and this item. */
14
+ summary?: Snippet<[AccordionHeaderContext]>;
9
15
  /** Panel content for this item. */
10
16
  content: Snippet;
11
17
  /** Open state. Re-passing a new value takes over from any local toggle. */
@@ -14,11 +20,16 @@ export interface AccordionItem {
14
20
  disabled?: boolean;
15
21
  }
16
22
  import type { HTMLAttributes } from 'svelte/elements';
17
- import type { DisclosureChevron } from './Disclosure.svelte';
23
+ import type { DisclosureChevron, DisclosureSize } from './Disclosure.svelte';
18
24
  type Own = {
19
25
  items: AccordionItem[];
20
26
  multiple?: boolean;
21
27
  chevron?: DisclosureChevron;
28
+ /** `plain` drops the outer border and radius and keeps the item dividers,
29
+ * for a stack that only separates rows. */
30
+ variant?: AccordionVariant;
31
+ /** Header/panel density of every item; forwarded to `Disclosure`. */
32
+ size?: DisclosureSize;
22
33
  onchange?: (id: string, open: boolean) => void;
23
34
  class?: string;
24
35
  style?: string;
@@ -1,5 +1,6 @@
1
1
  <script lang="ts" module>
2
2
  export type DisclosureChevron = 'start' | 'end' | false;
3
+ export type DisclosureSize = 'default' | 'compact';
3
4
  export interface DisclosureHeaderContext {
4
5
  open: boolean;
5
6
  }
@@ -18,6 +19,7 @@
18
19
  header,
19
20
  children,
20
21
  chevron = 'end',
22
+ size = 'default',
21
23
  id,
22
24
  onchange,
23
25
  disabled = false,
@@ -35,6 +37,10 @@
35
37
  children: Snippet;
36
38
  /** Chevron placement; `false` hides it. */
37
39
  chevron?: DisclosureChevron;
40
+ /** Header/panel density. `compact` shrinks both to `--fs-xs` and tighter
41
+ * padding for strip rows; tune either with `--disclosure-pad`,
42
+ * `--disclosure-fs`, `--disclosure-panel-pad`. */
43
+ size?: DisclosureSize;
38
44
  /** Base for the button/panel ids the ARIA wiring uses. */
39
45
  id?: string;
40
46
  onchange?: (open: boolean) => void;
@@ -60,6 +66,7 @@
60
66
  {...rest}
61
67
  class="disclosure {klass}"
62
68
  class:disclosure--open={open}
69
+ class:disclosure--compact={size === 'compact'}
63
70
  style={styleProp}
64
71
  data-tsu="Disclosure"
65
72
  >
@@ -101,13 +108,13 @@
101
108
  justify-content: space-between;
102
109
  gap: var(--sp-2);
103
110
  width: 100%;
104
- padding: var(--sp-3) var(--sp-4);
111
+ padding: var(--disclosure-pad, var(--sp-3) var(--sp-4));
105
112
  border: 0;
106
113
  background: none;
107
114
  color: inherit;
108
115
  font: inherit;
109
116
  font-weight: var(--fw-medium);
110
- font-size: var(--fs-sm);
117
+ font-size: var(--disclosure-fs, var(--fs-sm));
111
118
  text-align: start;
112
119
  cursor: pointer;
113
120
  user-select: none;
@@ -145,10 +152,15 @@
145
152
  }
146
153
  }
147
154
  .disclosure__panel {
148
- padding: 0 var(--sp-4) var(--sp-4);
149
- font-size: var(--fs-sm);
155
+ padding: var(--disclosure-panel-pad, 0 var(--sp-4) var(--sp-4));
156
+ font-size: var(--disclosure-fs, var(--fs-sm));
150
157
  color: var(--text-muted);
151
158
  }
159
+ .disclosure--compact {
160
+ --disclosure-pad: var(--sp-1) var(--sp-2);
161
+ --disclosure-fs: var(--fs-xs);
162
+ --disclosure-panel-pad: 0 var(--sp-2) var(--sp-2);
163
+ }
152
164
  .disclosure__panel[hidden] {
153
165
  display: none;
154
166
  }
@@ -1,4 +1,5 @@
1
1
  export type DisclosureChevron = 'start' | 'end' | false;
2
+ export type DisclosureSize = 'default' | 'compact';
2
3
  export interface DisclosureHeaderContext {
3
4
  open: boolean;
4
5
  }
@@ -11,6 +12,10 @@ type Own = {
11
12
  children: Snippet;
12
13
  /** Chevron placement; `false` hides it. */
13
14
  chevron?: DisclosureChevron;
15
+ /** Header/panel density. `compact` shrinks both to `--fs-xs` and tighter
16
+ * padding for strip rows; tune either with `--disclosure-pad`,
17
+ * `--disclosure-fs`, `--disclosure-panel-pad`. */
18
+ size?: DisclosureSize;
14
19
  /** Base for the button/panel ids the ARIA wiring uses. */
15
20
  id?: string;
16
21
  onchange?: (open: boolean) => void;
@@ -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);
@@ -54,6 +54,9 @@
54
54
  showLabel?: boolean | 'row';
55
55
  /** Rotate the glyph (refresh in flight). */
56
56
  spin?: boolean;
57
+ /** Force the glyph filled (or outlined), overriding its registry default —
58
+ * a pin/star toggle painting its "on" state. Forwarded to Icon. */
59
+ filled?: boolean;
57
60
  /** Forwarded to Button: spinner, blocks clicks, aria-busy. */
58
61
  loading?: boolean;
59
62
  /** Corner count indicator, forwarded to Button; the count joins `label`
@@ -83,6 +86,7 @@
83
86
  pressed,
84
87
  showLabel = false,
85
88
  spin = false,
89
+ filled,
86
90
  loading = false,
87
91
  count,
88
92
  countMax,
@@ -126,17 +130,17 @@
126
130
  >
127
131
  {#if children}
128
132
  {#if glyphCss}
129
- <span class="glyph" style="font-size: {glyphCss}"><Icon {spin}>{@render children()}</Icon></span>
133
+ <span class="glyph" style="font-size: {glyphCss}"><Icon {spin} {filled}>{@render children()}</Icon></span>
130
134
  {:else}
131
- <Icon {size} {spin}>{@render children()}</Icon>
135
+ <Icon {size} {spin} {filled}>{@render children()}</Icon>
132
136
  {/if}
133
137
  {:else if emoji}
134
138
  <span class="emoji" style="font-size: {emojiCss}" aria-hidden="true">{emoji}</span>
135
139
  {:else if icon}
136
140
  {#if glyphCss}
137
- <span class="glyph" style="font-size: {glyphCss}"><Icon name={icon} {spin} /></span>
141
+ <span class="glyph" style="font-size: {glyphCss}"><Icon name={icon} {spin} {filled} /></span>
138
142
  {:else}
139
- <Icon name={icon} {size} {spin} />
143
+ <Icon name={icon} {size} {spin} {filled} />
140
144
  {/if}
141
145
  {/if}
142
146
  {#if showLabel}<span class="ib-label">{label}</span>{/if}
@@ -31,6 +31,9 @@ type IconButtonProps = HTMLButtonAttributes & {
31
31
  showLabel?: boolean | 'row';
32
32
  /** Rotate the glyph (refresh in flight). */
33
33
  spin?: boolean;
34
+ /** Force the glyph filled (or outlined), overriding its registry default —
35
+ * a pin/star toggle painting its "on" state. Forwarded to Icon. */
36
+ filled?: boolean;
34
37
  /** Forwarded to Button: spinner, blocks clicks, aria-busy. */
35
38
  loading?: boolean;
36
39
  /** Corner count indicator, forwarded to Button; the count joins `label`
@@ -36,6 +36,7 @@
36
36
  // (inherited from the native popover). An item's `tag` renders as a soft
37
37
  // Badge pill after the label; the `tag` snippet replaces that pill.
38
38
  import type { ComponentProps, Snippet } from 'svelte';
39
+ import type { HTMLAttributes } from 'svelte/elements';
39
40
  import Popover from './Popover.svelte';
40
41
  import Icon from '../atoms/Icon.svelte';
41
42
  import Badge from '../atoms/Badge.svelte';
@@ -52,10 +53,13 @@
52
53
  | 'control'
53
54
  | 'block'
54
55
  | 'triggerClass'
56
+ | 'title'
55
57
  | 'bare'
56
58
  | 'hitArea'
57
59
  | 'disabled'
58
60
  | 'gap'
61
+ | 'count'
62
+ | 'countMax'
59
63
  | 'onopen'
60
64
  | 'onclose'
61
65
  >;
@@ -74,10 +78,13 @@
74
78
  control,
75
79
  block,
76
80
  triggerClass,
81
+ title,
77
82
  bare,
78
83
  hitArea,
79
84
  disabled,
80
85
  gap,
86
+ count,
87
+ countMax,
81
88
  onopen,
82
89
  onclose,
83
90
  open = $bindable(false),
@@ -85,7 +92,11 @@
85
92
  class: klass = '',
86
93
  style: styleProp = '',
87
94
  panelClass = '',
88
- }: {
95
+ panelStyle = '',
96
+ ...rest
97
+ }: Omit<HTMLAttributes<HTMLElement>, keyof Own> & Own = $props();
98
+
99
+ type Own = {
89
100
  label: string;
90
101
  items: MenuItem[];
91
102
  trigger: Snippet;
@@ -98,8 +109,11 @@
98
109
  closeOnSelect?: boolean;
99
110
  class?: string;
100
111
  style?: string;
112
+ /** Class / inline style on the floating panel; `panelStyle` is how you
113
+ * size or cap it (`min-width`, `max-height`, `padding`). */
101
114
  panelClass?: string;
102
- } & TriggerChrome = $props();
115
+ panelStyle?: string;
116
+ } & TriggerChrome;
103
117
 
104
118
  let listEl = $state<HTMLDivElement | null>(null);
105
119
 
@@ -137,6 +151,7 @@
137
151
  </script>
138
152
 
139
153
  <Popover
154
+ {...rest}
140
155
  {label}
141
156
  {placement}
142
157
  {trigger}
@@ -148,13 +163,17 @@
148
163
  {control}
149
164
  {block}
150
165
  {triggerClass}
166
+ {title}
151
167
  class={klass}
152
168
  style={styleProp}
153
169
  {panelClass}
170
+ {panelStyle}
154
171
  {bare}
155
172
  {hitArea}
156
173
  {disabled}
157
174
  {gap}
175
+ {count}
176
+ {countMax}
158
177
  role="menu"
159
178
  haspopup="menu"
160
179
  bind:open
@@ -25,10 +25,11 @@ export interface MenuItem {
25
25
  attrs?: import('svelte/elements').HTMLButtonAttributes;
26
26
  }
27
27
  import type { ComponentProps, Snippet } from 'svelte';
28
+ import type { HTMLAttributes } from 'svelte/elements';
28
29
  import Popover from './Popover.svelte';
29
30
  type PopoverProps = ComponentProps<typeof Popover>;
30
- type TriggerChrome = Pick<PopoverProps, 'variant' | 'tone' | 'size' | 'box' | 'pill' | 'control' | 'block' | 'triggerClass' | 'bare' | 'hitArea' | 'disabled' | 'gap' | 'onopen' | 'onclose'>;
31
- type $$ComponentProps = {
31
+ type TriggerChrome = Pick<PopoverProps, 'variant' | 'tone' | 'size' | 'box' | 'pill' | 'control' | 'block' | 'triggerClass' | 'title' | 'bare' | 'hitArea' | 'disabled' | 'gap' | 'count' | 'countMax' | 'onopen' | 'onclose'>;
32
+ type Own = {
32
33
  label: string;
33
34
  items: MenuItem[];
34
35
  trigger: Snippet;
@@ -41,8 +42,12 @@ type $$ComponentProps = {
41
42
  closeOnSelect?: boolean;
42
43
  class?: string;
43
44
  style?: string;
45
+ /** Class / inline style on the floating panel; `panelStyle` is how you
46
+ * size or cap it (`min-width`, `max-height`, `padding`). */
44
47
  panelClass?: string;
48
+ panelStyle?: string;
45
49
  } & TriggerChrome;
50
+ type $$ComponentProps = Omit<HTMLAttributes<HTMLElement>, keyof Own> & Own;
46
51
  declare const Menu: import("svelte").Component<$$ComponentProps, {}, "open">;
47
52
  type Menu = ReturnType<typeof Menu>;
48
53
  export default Menu;
@@ -6,7 +6,7 @@
6
6
  // properties so the fused look needs no reach into their scoped CSS.
7
7
  import type { ControlSize } from '../../size';
8
8
  import type { ComponentProps, Snippet } from 'svelte';
9
- import type { HTMLAttributes } from 'svelte/elements';
9
+ import type { HTMLAttributes, HTMLButtonAttributes } from 'svelte/elements';
10
10
  import Button from '../atoms/Button.svelte';
11
11
  import Icon from '../atoms/Icon.svelte';
12
12
  import Menu, { type MenuItem } from './Menu.svelte';
@@ -32,6 +32,11 @@
32
32
  /** Custom trailing content for items that carry a `tag`. */
33
33
  tag?: MenuProps['tag'];
34
34
  panelClass?: string;
35
+ panelStyle?: MenuProps['panelStyle'];
36
+ /** Attributes for the primary segment itself — `title`,
37
+ * `aria-describedby`, `data-*`. Rest props stay on the root box, which is
38
+ * where a layout class or `data-journey` for the pair belongs. */
39
+ primaryProps?: Omit<HTMLButtonAttributes, 'class'> & { class?: string };
35
40
  gap?: number;
36
41
  /** Bindable open state of the menu. */
37
42
  open?: boolean;
@@ -61,6 +66,8 @@
61
66
  placement = 'bottom-end',
62
67
  tag,
63
68
  panelClass,
69
+ panelStyle,
70
+ primaryProps,
64
71
  gap,
65
72
  open = $bindable(false),
66
73
  onopen,
@@ -100,6 +107,7 @@
100
107
  >
101
108
  <span class="split-main">
102
109
  <Button
110
+ {...primaryProps}
103
111
  {variant}
104
112
  {size}
105
113
  {control}
@@ -120,6 +128,7 @@
120
128
  {placement}
121
129
  {tag}
122
130
  {panelClass}
131
+ {panelStyle}
123
132
  {gap}
124
133
  {variant}
125
134
  {size}
@@ -1,6 +1,6 @@
1
1
  import type { ControlSize } from '../../size';
2
2
  import type { ComponentProps, Snippet } from 'svelte';
3
- import type { HTMLAttributes } from 'svelte/elements';
3
+ import type { HTMLAttributes, HTMLButtonAttributes } from 'svelte/elements';
4
4
  import Menu, { type MenuItem } from './Menu.svelte';
5
5
  type MenuProps = ComponentProps<typeof Menu>;
6
6
  type Own = {
@@ -22,6 +22,13 @@ type Own = {
22
22
  /** Custom trailing content for items that carry a `tag`. */
23
23
  tag?: MenuProps['tag'];
24
24
  panelClass?: string;
25
+ panelStyle?: MenuProps['panelStyle'];
26
+ /** Attributes for the primary segment itself — `title`,
27
+ * `aria-describedby`, `data-*`. Rest props stay on the root box, which is
28
+ * where a layout class or `data-journey` for the pair belongs. */
29
+ primaryProps?: Omit<HTMLButtonAttributes, 'class'> & {
30
+ class?: string;
31
+ };
25
32
  gap?: number;
26
33
  /** Bindable open state of the menu. */
27
34
  open?: boolean;
package/dist/index.d.ts CHANGED
@@ -42,7 +42,7 @@ export { default as ResizablePanel } from './components/layouts/ResizablePanel.s
42
42
  export { type ResizeHandleParams, resizeHandle, } from './components/layouts/resizable-panel-frame.js';
43
43
  export { default as Stack } from './components/layouts/Stack.svelte';
44
44
  export { default as Toolbar } from './components/layouts/Toolbar.svelte';
45
- export { type AccordionItem, default as Accordion, } from './components/molecules/Accordion.svelte';
45
+ export { type AccordionHeaderContext, type AccordionItem, type AccordionVariant, default as Accordion, } from './components/molecules/Accordion.svelte';
46
46
  export { type Attachment, default as AttachmentList, } from './components/molecules/AttachmentList.svelte';
47
47
  export { type BreadcrumbItem, default as Breadcrumb, } from './components/molecules/Breadcrumb.svelte';
48
48
  export { default as Callout } from './components/molecules/Callout.svelte';
@@ -55,7 +55,7 @@ export { default as Composer } from './components/molecules/Composer.svelte';
55
55
  export { default as ConfirmModal } from './components/molecules/ConfirmModal.svelte';
56
56
  export { default as CopyButton } from './components/molecules/CopyButton.svelte';
57
57
  export { applyTrigger, comboboxAction, findTrigger, nextComboboxIndex, } from './components/molecules/combobox-keyboard.js';
58
- export { type DisclosureChevron, type DisclosureHeaderContext, default as Disclosure, } from './components/molecules/Disclosure.svelte';
58
+ export { type DisclosureChevron, type DisclosureHeaderContext, type DisclosureSize, default as Disclosure, } from './components/molecules/Disclosure.svelte';
59
59
  export { default as Drawer } from './components/molecules/Drawer.svelte';
60
60
  export { default as Dropzone } from './components/molecules/Dropzone.svelte';
61
61
  export { default as EmojiPicker } from './components/molecules/EmojiPicker.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorsk/tsumikit",
3
- "version": "0.63.0",
3
+ "version": "0.64.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",