@dorsk/tsumikit 0.76.0 → 0.77.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.
@@ -100,8 +100,9 @@
100
100
  background: var(--divider-color);
101
101
  }
102
102
  .divider-label {
103
+ min-width: 0;
103
104
  color: var(--text-muted);
104
105
  font-size: var(--fs-sm);
105
- white-space: nowrap;
106
+ text-align: center;
106
107
  }
107
108
  </style>
@@ -12,7 +12,7 @@
12
12
 
13
13
  // `size` shadows the native char-width attribute (unused in token-sized
14
14
  // layouts) to expose a height preset instead.
15
- type Props = Omit<HTMLInputAttributes, 'size'> & {
15
+ type Props = Omit<HTMLInputAttributes, 'size' | 'onsubmit'> & {
16
16
  mono?: boolean;
17
17
  size?: ControlSize;
18
18
  /** Fill the available width of a flex/Cluster row (flex: 1) instead of
@@ -1,7 +1,7 @@
1
1
  import type { ControlSize } from '../../size';
2
2
  import type { HTMLInputAttributes } from 'svelte/elements';
3
3
  import { type IconName } from './Icon.svelte';
4
- type Props = Omit<HTMLInputAttributes, 'size'> & {
4
+ type Props = Omit<HTMLInputAttributes, 'size' | 'onsubmit'> & {
5
5
  mono?: boolean;
6
6
  size?: ControlSize;
7
7
  /** Fill the available width of a flex/Cluster row (flex: 1) instead of
@@ -199,6 +199,7 @@
199
199
  .select-wrap.w-auto {
200
200
  display: inline-block;
201
201
  width: auto;
202
+ max-width: 100%;
202
203
  }
203
204
  .select-wrap.select-grow {
204
205
  flex: 1 1 0;
@@ -253,6 +254,8 @@
253
254
  }
254
255
  .select.w-auto {
255
256
  width: auto;
257
+ max-width: 100%;
258
+ text-overflow: ellipsis;
256
259
  }
257
260
  .select.embedded {
258
261
  background: var(--select-bg, var(--bg-elevated-2));
@@ -23,7 +23,7 @@
23
23
  import { getFieldContext, warnUnlabelled } from '../../field-context';
24
24
  import { getInputGroupContext } from '../../input-group-context';
25
25
 
26
- type Props = HTMLTextareaAttributes & {
26
+ type Props = Omit<HTMLTextareaAttributes, 'onsubmit'> & {
27
27
  mono?: boolean;
28
28
  autoresize?: boolean;
29
29
  size?: ControlSize;
@@ -1,6 +1,6 @@
1
1
  import type { ControlSize } from '../../size';
2
2
  import type { HTMLTextareaAttributes } from 'svelte/elements';
3
- type Props = HTMLTextareaAttributes & {
3
+ type Props = Omit<HTMLTextareaAttributes, 'onsubmit'> & {
4
4
  mono?: boolean;
5
5
  autoresize?: boolean;
6
6
  size?: ControlSize;
@@ -60,7 +60,7 @@
60
60
  {variant}
61
61
  {box}
62
62
  {hitArea}
63
- class="copy-btn {klass}"
63
+ class="copy-btn {labelShown && box !== undefined ? 'copy-labelled' : ''} {klass}"
64
64
  sound={false}
65
65
  onclick={copy}
66
66
  aria-label={copied ? copiedLabel : label}
@@ -78,6 +78,10 @@
78
78
  /* Both glyphs share one grid cell so the swap never changes the width. The
79
79
  check springs in (the kit's one bounce: a rare confirmation); the copy
80
80
  glyph fades out fast and back in over the base duration on revert. */
81
+ :global(.copy-btn.btn-box.copy-labelled) {
82
+ width: auto;
83
+ padding-inline: var(--sp-2);
84
+ }
81
85
  .glyphs {
82
86
  display: inline-grid;
83
87
  place-items: center;
@@ -73,6 +73,7 @@
73
73
  display: flex;
74
74
  flex-direction: column;
75
75
  gap: var(--sp-1);
76
+ min-width: 0;
76
77
  }
77
78
  .field-grow {
78
79
  flex: 1 1 0;
@@ -93,7 +93,7 @@
93
93
  {:else if emoji}
94
94
  <span class="emoji" aria-hidden="true">{emoji}</span>
95
95
  {/if}
96
- <span class:sr-only={onlyIcon}>{label}</span>
96
+ <span class="text" class:sr-only={onlyIcon}>{label}</span>
97
97
  </label>
98
98
 
99
99
  <style>
@@ -114,6 +114,7 @@
114
114
  font-weight: var(--fw-medium);
115
115
  font-size: var(--control-fs-md);
116
116
  line-height: var(--lh-none);
117
+ max-width: 100%;
117
118
  white-space: nowrap;
118
119
  cursor: pointer;
119
120
  user-select: none;
@@ -179,6 +180,11 @@
179
180
  }
180
181
  }
181
182
  /* An emoji reads small next to an SVG glyph of the same size: one icon tier up. */
183
+ .text:not(.sr-only) {
184
+ min-width: 0;
185
+ overflow: hidden;
186
+ text-overflow: ellipsis;
187
+ }
182
188
  .emoji {
183
189
  display: inline-flex;
184
190
  font-size: var(--icon-lg);
@@ -219,6 +219,7 @@
219
219
 
220
220
  .rows {
221
221
  gap: var(--sp-1);
222
+ min-width: 0;
222
223
  }
223
224
  .row-wrap {
224
225
  min-width: 0;
@@ -250,8 +251,9 @@
250
251
  }
251
252
  .label-line {
252
253
  display: flex;
254
+ flex-wrap: wrap;
253
255
  align-items: baseline;
254
- gap: var(--sp-2);
256
+ gap: var(--sp-px) var(--sp-2);
255
257
  min-width: 0;
256
258
  }
257
259
  .row .label-text {
@@ -260,7 +262,6 @@
260
262
  .note {
261
263
  font-size: var(--fs-xs);
262
264
  color: var(--text-faint);
263
- white-space: nowrap;
264
265
  }
265
266
  .description {
266
267
  font-size: var(--fs-xs);
@@ -119,11 +119,21 @@
119
119
  });
120
120
 
121
121
  $effect(() => {
122
- if (!open || active < 0) return;
123
- const el = root?.ownerDocument.getElementById(optionId(active));
124
- el?.scrollIntoView?.({ block: 'nearest' });
122
+ if (open && active >= 0) revealActive();
125
123
  });
126
124
 
125
+ // Scrolls the listbox only: `scrollIntoView` on a top-layer panel also
126
+ // scrolls the document in Chromium.
127
+ function revealActive() {
128
+ const list = root?.querySelector<HTMLElement>('.rs-list');
129
+ const row = rowEls()[active];
130
+ if (!list || !row) return;
131
+ const top = row.offsetTop;
132
+ const bottom = top + row.offsetHeight;
133
+ if (top < list.scrollTop) list.scrollTop = top;
134
+ else if (bottom > list.scrollTop + list.clientHeight) list.scrollTop = bottom - list.clientHeight;
135
+ }
136
+
127
137
  function close() {
128
138
  open = false;
129
139
  }
@@ -303,8 +313,7 @@
303
313
  async function onOpen() {
304
314
  typeahead.reset();
305
315
  await tick();
306
- const el = active >= 0 ? root?.ownerDocument.getElementById(optionId(active)) : null;
307
- el?.scrollIntoView?.({ block: 'nearest' });
316
+ if (active >= 0) revealActive();
308
317
  }
309
318
 
310
319
  function onClose() {
@@ -459,7 +468,6 @@
459
468
  }
460
469
  .rs-placeholder {
461
470
  display: block;
462
- padding-inline: var(--sp-1);
463
471
  color: var(--text-muted);
464
472
  font-size: var(--control-fs-md);
465
473
  line-height: var(--lh-tight);
@@ -486,6 +494,7 @@
486
494
  padding: var(--sp-1);
487
495
  }
488
496
  .rs-list {
497
+ position: relative;
489
498
  display: flex;
490
499
  flex-direction: column;
491
500
  gap: var(--sp-0-5);
@@ -30,6 +30,7 @@
30
30
  shrink = true,
31
31
  block = false,
32
32
  sound = true,
33
+ disabled = false,
33
34
  class: klass = '',
34
35
  ...rest
35
36
  }: {
@@ -64,6 +65,7 @@
64
65
  hitArea?: 'auto' | 'compact';
65
66
  /** Play UI sounds when enabled globally; false silences this instance. */
66
67
  sound?: boolean;
68
+ disabled?: boolean;
67
69
  class?: string;
68
70
  [key: string]: unknown;
69
71
  } = $props();
@@ -81,6 +83,7 @@
81
83
  class="select-button {klass}"
82
84
  class:row={variant === 'row'}
83
85
  class:active
86
+ class:disabled
84
87
  data-tsu="SelectButton"
85
88
  {...rest}
86
89
  class:grow={grow}
@@ -98,6 +101,7 @@
98
101
  aria-label={label}
99
102
  {title}
100
103
  {value}
104
+ {disabled}
101
105
  onchange={change}
102
106
  >
103
107
  {#if groups}
@@ -138,18 +142,25 @@
138
142
  .select-button.active {
139
143
  color: var(--accent);
140
144
  }
145
+ .select-button.disabled {
146
+ opacity: var(--disabled-opacity);
147
+ cursor: not-allowed;
148
+ }
141
149
  /* The inert face takes no pointer events: mirror ghost hover, press and the
142
150
  focus ring from the select overlaying it. */
143
151
  .select-button:active :global(.btn-ghost) {
144
152
  --btn-scale-dur: var(--dur-press);
145
153
  scale: var(--press-scale-sm);
146
154
  }
155
+ .select-button.disabled:active :global(.btn-ghost) {
156
+ scale: none;
157
+ }
147
158
  .select-button:has(:focus-visible) {
148
159
  outline: var(--focus-ring);
149
160
  outline-offset: var(--focus-ring-offset);
150
161
  }
151
162
  @media (hover: hover) and (pointer: fine) {
152
- .select-button:hover :global(.btn-ghost) {
163
+ .select-button:not(.disabled):hover :global(.btn-ghost) {
153
164
  background: var(--bg-elevated-2);
154
165
  }
155
166
  }
@@ -34,6 +34,7 @@ type $$ComponentProps = {
34
34
  hitArea?: 'auto' | 'compact';
35
35
  /** Play UI sounds when enabled globally; false silences this instance. */
36
36
  sound?: boolean;
37
+ disabled?: boolean;
37
38
  class?: string;
38
39
  [key: string]: unknown;
39
40
  };
@@ -63,7 +63,7 @@
63
63
  class:on={pressed}
64
64
  aria-pressed={pressed}
65
65
  >
66
- {@render children?.()}
66
+ <span class="label">{@render children?.()}</span>
67
67
  </button>
68
68
 
69
69
  <style>
@@ -106,10 +106,20 @@
106
106
  font-size: var(--control-fs-md);
107
107
  --icon-size: var(--control-icon-md);
108
108
  }
109
+ .label {
110
+ display: contents;
111
+ }
109
112
  .toggle.grow {
110
113
  flex: 1 1 0;
111
114
  min-width: 0;
112
115
  }
116
+ .toggle.grow .label {
117
+ display: block;
118
+ min-width: 0;
119
+ overflow: hidden;
120
+ text-overflow: ellipsis;
121
+ text-align: center;
122
+ }
113
123
  .toggle.no-shrink {
114
124
  flex: none;
115
125
  }
package/dist/floating.js CHANGED
@@ -20,9 +20,15 @@ function alignPos(align, start, size, fSize) {
20
20
  export function place(trigger, floating, placement = 'bottom-start', gap = 6) {
21
21
  placeAt(trigger.getBoundingClientRect(), floating, placement, gap);
22
22
  }
23
+ // The enter transition starts the panel at `scale: var(--enter-scale)`, so a
24
+ // client rect taken on open is ~4% short; offset sizes ignore transforms.
25
+ function layoutSize(el) {
26
+ const { width, height } = el.getBoundingClientRect();
27
+ return { width: el.offsetWidth || width, height: el.offsetHeight || height };
28
+ }
23
29
  /** `place` against a viewport rect instead of an element. */
24
30
  export function placeAt(t, floating, placement = 'bottom-start', gap = 6) {
25
- const f = floating.getBoundingClientRect();
31
+ const f = layoutSize(floating);
26
32
  const vw = document.documentElement.clientWidth;
27
33
  const vh = document.documentElement.clientHeight;
28
34
  const [side, align] = placement.split('-');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorsk/tsumikit",
3
- "version": "0.76.0",
3
+ "version": "0.77.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",