@dorsk/tsumikit 0.55.0 → 0.57.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.
@@ -155,7 +155,14 @@
155
155
  .input-lg {
156
156
  min-height: var(--input-size, var(--control-height-large));
157
157
  padding: var(--sp-3) var(--sp-4);
158
- font-size: var(--fs-base);
158
+ font-size: max(16px, var(--fs-md));
159
+ }
160
+ /* iOS auto-zooms any field under 16px on focus; on touch the size modifiers
161
+ carry their difference through padding and height alone. */
162
+ @media (pointer: coarse) {
163
+ .input-sm {
164
+ font-size: max(16px, var(--fs-sm));
165
+ }
159
166
  }
160
167
  .input-grow {
161
168
  flex: 1 1 0;
@@ -228,7 +228,7 @@
228
228
  .select.select-lg {
229
229
  min-height: var(--select-size, var(--control-height-large));
230
230
  padding: var(--sp-3) var(--sp-4);
231
- font-size: var(--fs-base);
231
+ font-size: max(16px, var(--fs-md));
232
232
  }
233
233
  .select.compact {
234
234
  padding: var(--sp-1) var(--sp-2);
@@ -296,6 +296,16 @@
296
296
  .select-wrap.no-chevron .select-face.compact {
297
297
  padding-right: var(--sp-2);
298
298
  }
299
+ /* iOS auto-zooms any field under 16px on focus; on touch the size modifiers
300
+ carry their difference through padding and height alone. */
301
+ @media (pointer: coarse) {
302
+ .select.compact {
303
+ font-size: max(16px, var(--fs-xs));
304
+ }
305
+ .select-face.compact {
306
+ font-size: max(16px, var(--fs-xs));
307
+ }
308
+ }
299
309
  .select:disabled ~ .select-face {
300
310
  color: var(--text-muted);
301
311
  }
@@ -254,9 +254,16 @@
254
254
  }
255
255
  .textarea-lg {
256
256
  padding: var(--sp-3) var(--sp-4);
257
- font-size: var(--fs-base);
257
+ font-size: max(16px, var(--fs-md));
258
258
  min-height: var(--textarea-size, var(--control-height-large));
259
259
  }
260
+ /* iOS auto-zooms any field under 16px on focus; on touch the size modifiers
261
+ carry their difference through padding and height alone. */
262
+ @media (pointer: coarse) {
263
+ .textarea-sm {
264
+ font-size: max(16px, var(--fs-sm));
265
+ }
266
+ }
260
267
  .textarea[aria-invalid='true'],
261
268
  .textarea[aria-invalid='true']:focus {
262
269
  border-color: var(--danger);
@@ -2,9 +2,10 @@
2
2
  import type { FilterNode, Query } from '../../query/ast';
3
3
 
4
4
  /**
5
- * The reactive parse context handed to the `inline` and `below` snippets so a
6
- * host can render arbitrary Svelte nodes (badges, custom components, chips)
7
- * either inline inside the bar or below it — all derived from the same value.
5
+ * The reactive parse context handed to the `inline`, `display` and `below`
6
+ * snippets so a host can render arbitrary Svelte nodes (badges, custom
7
+ * components, chips) inside the bar, over the input or below it — all
8
+ * derived from the same value.
8
9
  */
9
10
  export interface FilterInputContext {
10
11
  /** The parsed AST for the current value. */
@@ -68,6 +69,7 @@
68
69
  onchange,
69
70
  onsubmit,
70
71
  inline,
72
+ display,
71
73
  below,
72
74
  class: klass = '',
73
75
  style: styleProp = '',
@@ -124,6 +126,15 @@
124
126
  * inline (no below-bar chips). Receives the reactive parse context.
125
127
  */
126
128
  inline?: Snippet<[FilterInputContext]>;
129
+ /**
130
+ * Rendering of the value shown over the input while the field is blurred
131
+ * and non-empty — a compact path, a formatted timestamp, a resolved id.
132
+ * Focusing the input (click, Tab, `hotkey`, `context.focus()`) reveals the
133
+ * raw text again. It is decoration, never a control: the overlay is
134
+ * `aria-hidden`, takes no pointer events and adds no tab stop, so the
135
+ * input stays the single focus target.
136
+ */
137
+ display?: Snippet<[FilterInputContext]>;
127
138
  /**
128
139
  * Content rendered below the bar (e.g. removable chips). FilterSearchBar
129
140
  * passes its chip row here; single-field hosts simply omit it.
@@ -181,6 +192,8 @@
181
192
  focus: () => el?.focus()
182
193
  });
183
194
 
195
+ const showDisplay = $derived(!!display && !focused && !!value);
196
+
184
197
  // Emit the AST whenever the parse result changes.
185
198
  $effect(() => {
186
199
  onchange?.(ast, value);
@@ -352,31 +365,37 @@
352
365
  <div class="fi__bar">
353
366
  {#if icon}<span class="fi__icon"><Icon name={icon} label="Search" /></span>{/if}
354
367
  {#if inline}{@render inline(ctx)}{/if}
355
- <input
356
- bind:this={el}
357
- bind:value
358
- class="fi__input"
359
- id={id ?? field?.id}
360
- aria-describedby={ariaDescribedby ?? field?.describedBy}
361
- aria-invalid={ariaInvalid ?? (field?.invalid ? 'true' : undefined)}
362
- spellcheck="false"
363
- autocomplete="off"
364
- placeholder={hint}
365
- {oninput}
366
- onclick={refresh}
367
- onkeyup={(e) => {
368
- if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') refresh();
369
- }}
370
- onfocus={() => {
371
- focused = true;
372
- refresh();
373
- }}
374
- onblur={() => {
375
- focused = false;
376
- setTimeout(() => (open = false), 120);
377
- }}
378
- {onkeydown}
379
- />
368
+ <span class="fi__field">
369
+ <input
370
+ bind:this={el}
371
+ bind:value
372
+ class="fi__input"
373
+ class:fi__input--masked={showDisplay}
374
+ id={id ?? field?.id}
375
+ aria-describedby={ariaDescribedby ?? field?.describedBy}
376
+ aria-invalid={ariaInvalid ?? (field?.invalid ? 'true' : undefined)}
377
+ spellcheck="false"
378
+ autocomplete="off"
379
+ placeholder={hint}
380
+ {oninput}
381
+ onclick={refresh}
382
+ onkeyup={(e) => {
383
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') refresh();
384
+ }}
385
+ onfocus={() => {
386
+ focused = true;
387
+ refresh();
388
+ }}
389
+ onblur={() => {
390
+ focused = false;
391
+ setTimeout(() => (open = false), 120);
392
+ }}
393
+ {onkeydown}
394
+ />
395
+ {#if display && showDisplay}
396
+ <span class="fi__display" aria-hidden="true">{@render display(ctx)}</span>
397
+ {/if}
398
+ </span>
380
399
  {#if showHotkey && hotkey && !focused && !value}
381
400
  <kbd class="fi__kbd" aria-hidden="true">{hotkey}</kbd>
382
401
  {/if}
@@ -479,6 +498,22 @@
479
498
  display: flex;
480
499
  color: var(--text-faint);
481
500
  }
501
+ .fi__field {
502
+ position: relative;
503
+ display: flex;
504
+ flex: 1;
505
+ min-width: 0;
506
+ }
507
+ .fi__display {
508
+ position: absolute;
509
+ inset: 0;
510
+ display: flex;
511
+ align-items: center;
512
+ gap: var(--sp-2);
513
+ min-width: 0;
514
+ overflow: hidden;
515
+ pointer-events: none;
516
+ }
482
517
  .fi__input {
483
518
  flex: 1;
484
519
  border: 0;
@@ -492,6 +527,9 @@
492
527
  .fi__input::placeholder {
493
528
  color: var(--text-faint);
494
529
  }
530
+ .fi__input--masked {
531
+ color: transparent;
532
+ }
495
533
  .fi__clear {
496
534
  display: flex;
497
535
  border: 0;
@@ -1,8 +1,9 @@
1
1
  import type { FilterNode, Query } from '../../query/ast';
2
2
  /**
3
- * The reactive parse context handed to the `inline` and `below` snippets so a
4
- * host can render arbitrary Svelte nodes (badges, custom components, chips)
5
- * either inline inside the bar or below it — all derived from the same value.
3
+ * The reactive parse context handed to the `inline`, `display` and `below`
4
+ * snippets so a host can render arbitrary Svelte nodes (badges, custom
5
+ * components, chips) inside the bar, over the input or below it — all
6
+ * derived from the same value.
6
7
  */
7
8
  export interface FilterInputContext {
8
9
  /** The parsed AST for the current value. */
@@ -73,6 +74,15 @@ type Own = {
73
74
  * inline (no below-bar chips). Receives the reactive parse context.
74
75
  */
75
76
  inline?: Snippet<[FilterInputContext]>;
77
+ /**
78
+ * Rendering of the value shown over the input while the field is blurred
79
+ * and non-empty — a compact path, a formatted timestamp, a resolved id.
80
+ * Focusing the input (click, Tab, `hotkey`, `context.focus()`) reveals the
81
+ * raw text again. It is decoration, never a control: the overlay is
82
+ * `aria-hidden`, takes no pointer events and adds no tab stop, so the
83
+ * input stays the single focus target.
84
+ */
85
+ display?: Snippet<[FilterInputContext]>;
76
86
  /**
77
87
  * Content rendered below the bar (e.g. removable chips). FilterSearchBar
78
88
  * passes its chip row here; single-field hosts simply omit it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorsk/tsumikit",
3
- "version": "0.55.0",
3
+ "version": "0.57.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",