@dorsk/tsumikit 0.51.0 → 0.53.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
@@ -190,7 +190,7 @@ sanctioned escape hatch for everything else.
190
190
  | component | published properties |
191
191
  | --- | --- |
192
192
  | `Button` | `--btn-bg`, `--btn-fg`, `--btn-border`, `--btn-size`, `--btn-radius`, `--btn-tone`, `--btn-on`, `--btn-box` |
193
- | `Badge` | `--badge-bg`, `--badge-fg`, `--badge-border`, `--badge-radius`, `--badge-tone`, `--badge-max-width` |
193
+ | `Badge` | `--badge-bg`, `--badge-fg`, `--badge-border`, `--badge-radius`, `--badge-tone`, `--badge-max-width`, `--badge-fs`, `--badge-fw`, `--badge-min-size` |
194
194
  | `Text` | `--txt-fg`, `--txt-size` |
195
195
  | `Select` | `--select-bg`, `--select-fg`, `--select-border`, `--select-size`, `--select-radius` |
196
196
  | `Input` | `--input-bg`, `--input-fg`, `--input-border`, `--input-size`, `--input-radius` |
@@ -204,10 +204,24 @@ sanctioned escape hatch for everything else.
204
204
  | `Fieldset` | `--fieldset-pad`, `--fieldset-border` |
205
205
  | `GitRef` | `--git-ref-tone`, `--git-ref-max-width` |
206
206
  | `EmptyState` | `--empty-tone` |
207
+ | `Popover` | `--pop-trigger-bg`, `--pop-trigger-fg`, `--pop-trigger-border`, `--pop-trigger-radius`, `--pop-trigger-size`, `--pop-trigger-pad`, `--pop-box` |
207
208
 
208
209
  `tests/css-custom-property-contract.test.js` reads this table and fails if a
209
210
  listed property is not actually read by its component, so the docs cannot drift.
210
211
 
212
+ `Popover`'s hooks paint the **trigger**; hover states keep their own feedback
213
+ colours. `--pop-box: auto` releases the square floor, so `pill` plus
214
+ `--pop-trigger-pad` reshapes the default trigger into a chip of any height. `--pop-trigger-border` needs the chromed trigger — `bare` means no ring
215
+ by definition. `Badge`'s `--badge-min-size` pairs with `numeric` (already centred
216
+ and tabular) to turn a count badge into a square/circular counter.
217
+
218
+ A `triggerClass` on `Popover` (or `class` on the pickers that wrap it) is a hook
219
+ for a **globally loaded** stylesheet only. Your component's *scoped* CSS cannot
220
+ reach it: Svelte's scoping attribute is added to elements your own template
221
+ renders, and the trigger belongs to `Popover`, so `.toolbar .my-trigger` compiles
222
+ to `.toolbar.svelte-x .my-trigger.svelte-x` and matches nothing. Use `style` and
223
+ the properties above instead.
224
+
211
225
  ## Components
212
226
 
213
227
  **Atoms:** Text, Heading, Button, Input (`icon` inset leading glyph,
@@ -267,7 +281,10 @@ hooks on head/row/cell; `responsive="stack"` turns rows into cards below
267
281
  prefix, the `<table>` stays a table for assistive tech), FilterSearchBar /
268
282
  FilterInput (`size="sm"` compact bar on `--control-height-compact`,
269
283
  `shape="pill"`, `surface`, `hotkey="/"` focuses the input from anywhere
270
- outside an editable element, `showHotkey` renders the `<kbd>` hint, `grow`).
284
+ outside an editable element, `showHotkey` renders the `<kbd>` hint, `grow`;
285
+ `key="cwd"` puts FilterInput in single-key mode — `value` is the bare value
286
+ instead of a `key:"value"` query, no key prefix in the box, the `placeholder`
287
+ shows while empty, and completion still runs through that field's provider).
271
288
 
272
289
  **Layouts:** AppShell (responsive header/sidebar/main/footer — persistent
273
290
  sidebar on desktop, overlay drawer on mobile, optionally resizable;
@@ -167,8 +167,8 @@
167
167
  gap: var(--sp-1);
168
168
  padding: 0.15rem var(--sp-2);
169
169
  border-radius: var(--badge-radius, var(--r-pill));
170
- font-size: var(--fs-xs);
171
- font-weight: var(--fw-medium);
170
+ font-size: var(--badge-fs, var(--fs-xs));
171
+ font-weight: var(--badge-fw, var(--fw-medium));
172
172
  line-height: 1.4;
173
173
  background: var(--badge-bg, var(--bg-elevated-2));
174
174
  color: var(--badge-fg, var(--text-muted));
@@ -178,7 +178,7 @@
178
178
  }
179
179
  .badge-sm,
180
180
  .badge-xs {
181
- font-size: calc(var(--fs-xs) * 0.92);
181
+ font-size: var(--badge-fs, calc(var(--fs-xs) * 0.92));
182
182
  gap: 0.15rem;
183
183
  }
184
184
  .badge-sm {
@@ -205,16 +205,17 @@
205
205
  }
206
206
  .mono {
207
207
  font-family: var(--font-mono);
208
- font-weight: var(--fw-normal);
208
+ font-weight: var(--badge-fw, var(--fw-normal));
209
209
  }
210
210
  .uppercase {
211
211
  text-transform: uppercase;
212
212
  letter-spacing: 0.04em;
213
- font-weight: var(--fw-semibold);
213
+ font-weight: var(--badge-fw, var(--fw-semibold));
214
214
  }
215
215
  .numeric {
216
216
  font-variant-numeric: tabular-nums;
217
- min-width: 1.5em;
217
+ min-width: var(--badge-min-size, 1.5em);
218
+ min-height: var(--badge-min-size, auto);
218
219
  justify-content: center;
219
220
  }
220
221
  .truncate {
@@ -56,7 +56,7 @@
56
56
  {...rest}
57
57
  >
58
58
  <div class="navbar-inner">
59
- {#each items as item (item.href)}
59
+ {#each items as item, i (`${i}:${item.href}`)}
60
60
  <a
61
61
  href={item.href}
62
62
  class="navbar-link"
@@ -43,13 +43,15 @@
43
43
  import { autoQuoteEdit, backspaceEmptyQuotes, closingQuoteExit } from '../../query/edit';
44
44
  import { parse } from '../../query/parser';
45
45
  import { type Schema } from '../../query/schema';
46
+ import { singleQuery, suggestSingle } from '../../query/single';
46
47
  import { suggest, type SuggestState } from '../../query/suggest';
47
48
  import { getFieldContext, warnUnlabelled } from '../../field-context';
48
49
 
49
50
  let {
50
51
  schema,
52
+ key,
51
53
  value = $bindable(''),
52
- placeholder = 'artist:"Daft Punk" AND year>=2000',
54
+ placeholder,
53
55
  autoQuote = true,
54
56
  showClear = true,
55
57
  icon = 'search',
@@ -70,13 +72,23 @@
70
72
  style: styleProp = '',
71
73
  }: {
72
74
  schema: Schema;
73
- /** The raw textual query (two-way bindable). */
75
+ /**
76
+ * Single-key mode: the name (or alias) of the ONE schema field being
77
+ * edited. `value` then holds the bare value instead of a `key:"value"`
78
+ * query — no key prefix in the box, the placeholder shows while empty and
79
+ * completion still runs through that field's provider. The equivalent query
80
+ * is synthesised for `onchange` and the snippet context.
81
+ */
82
+ key?: string;
83
+ /** The raw textual query, or the bare value in single-key mode (bindable). */
74
84
  value?: string;
85
+ /** Defaults to a sample query, or to nothing in single-key mode. */
75
86
  placeholder?: string;
76
87
  /**
77
88
  * When a string field's value step opens (`title:`), auto-insert a `""`
78
89
  * pair with the caret inside so multi-word values stay together; Tab exits
79
90
  * the quotes. Set false for bare typing where spaces split the value.
91
+ * Always off in single-key mode, which quotes nothing.
80
92
  */
81
93
  autoQuote?: boolean;
82
94
  /** Show the trailing clear (✕) button when the field is non-empty. */
@@ -147,8 +159,12 @@
147
159
  return () => document.removeEventListener('keydown', onHotkey);
148
160
  });
149
161
 
162
+ const single = $derived(!!key);
163
+ const quoting = $derived(autoQuote && !single);
164
+ const hint = $derived(placeholder ?? (single ? '' : 'artist:"Daft Punk" AND year>=2000'));
165
+
150
166
  // Parsed view (drives the onchange AST + the snippet context).
151
- const ast = $derived(parse(value, schema));
167
+ const ast = $derived(parse(key ? singleQuery(schema, key, value) : value, schema));
152
168
  const chips = $derived(filters(ast));
153
169
  const text = $derived(freeText(ast));
154
170
 
@@ -183,7 +199,7 @@
183
199
  }
184
200
 
185
201
  function oninput(e: Event) {
186
- if (autoQuote && el && (e as InputEvent).inputType?.startsWith('insert')) {
202
+ if (quoting && el && (e as InputEvent).inputType?.startsWith('insert')) {
187
203
  const pos = el.selectionStart ?? value.length;
188
204
  const edit = autoQuoteEdit(schema, value, pos);
189
205
  if (edit) {
@@ -200,7 +216,7 @@
200
216
  if (!el) return;
201
217
  const pos = el.selectionStart ?? value.length;
202
218
  const id = ++reqId;
203
- const next = await suggest(schema, value, pos);
219
+ const next = key ? await suggestSingle(schema, key, value) : await suggest(schema, value, pos);
204
220
  if (id !== reqId) return; // a newer keystroke won
205
221
  menu = next;
206
222
  active = 0;
@@ -217,7 +233,7 @@
217
233
  let caret = s.caret;
218
234
  // A field/operator pick that opens a string field's value step gets the
219
235
  // same auto-quotes as manual typing.
220
- if (autoQuote && s.advance) {
236
+ if (quoting && s.advance) {
221
237
  const edit = autoQuoteEdit(schema, value, caret);
222
238
  if (edit) {
223
239
  value = edit.value;
@@ -236,7 +252,7 @@
236
252
  }
237
253
 
238
254
  function onkeydown(e: KeyboardEvent) {
239
- if (autoQuote && el) {
255
+ if (quoting && el) {
240
256
  const pos = el.selectionStart ?? value.length;
241
257
  if (e.key === 'Tab') {
242
258
  // Inside auto-quotes Tab exits them (takes priority over accepting a
@@ -292,6 +308,14 @@
292
308
  }
293
309
 
294
310
  function removeChip(span: [number, number]) {
311
+ // Single-key spans index the synthesised query, not the box: the only
312
+ // clause there is the value itself, so removing it empties the field.
313
+ if (single) {
314
+ value = '';
315
+ onsubmit?.('');
316
+ queueMicrotask(() => el?.focus());
317
+ return;
318
+ }
295
319
  // Splice the clause out, plus trailing spaces to avoid doubles.
296
320
  const [a, b] = span;
297
321
  let end = b;
@@ -332,7 +356,7 @@
332
356
  aria-invalid={ariaInvalid ?? (field?.invalid ? 'true' : undefined)}
333
357
  spellcheck="false"
334
358
  autocomplete="off"
335
- {placeholder}
359
+ placeholder={hint}
336
360
  {oninput}
337
361
  onclick={refresh}
338
362
  onkeyup={(e) => {
@@ -24,13 +24,23 @@ import { type IconName } from '../atoms/Icon.svelte';
24
24
  import { type Schema } from '../../query/schema';
25
25
  type $$ComponentProps = {
26
26
  schema: Schema;
27
- /** The raw textual query (two-way bindable). */
27
+ /**
28
+ * Single-key mode: the name (or alias) of the ONE schema field being
29
+ * edited. `value` then holds the bare value instead of a `key:"value"`
30
+ * query — no key prefix in the box, the placeholder shows while empty and
31
+ * completion still runs through that field's provider. The equivalent query
32
+ * is synthesised for `onchange` and the snippet context.
33
+ */
34
+ key?: string;
35
+ /** The raw textual query, or the bare value in single-key mode (bindable). */
28
36
  value?: string;
37
+ /** Defaults to a sample query, or to nothing in single-key mode. */
29
38
  placeholder?: string;
30
39
  /**
31
40
  * When a string field's value step opens (`title:`), auto-insert a `""`
32
41
  * pair with the caret inside so multi-word values stay together; Tab exits
33
42
  * the quotes. Set false for bare typing where spaces split the value.
43
+ * Always off in single-key mode, which quotes nothing.
34
44
  */
35
45
  autoQuote?: boolean;
36
46
  /** Show the trailing clear (✕) button when the field is non-empty. */
@@ -4,18 +4,53 @@
4
4
  // Everything inside the popover is sized in plain rem, never --fs-*: if the
5
5
  // panel rescaled with the page the slider would move under the pointer
6
6
  // mid-drag and oscillate between steps.
7
+ import type { ComponentProps } from 'svelte';
7
8
  import Popover from './Popover.svelte';
8
9
  import Slider from '../atoms/Slider.svelte';
9
10
  import { fontScale, SCALE_LEVELS } from '../../stores/fontscale.svelte';
10
11
 
11
- let { class: klass = '' }: { class?: string } = $props();
12
+ type TriggerChrome = Pick<
13
+ ComponentProps<typeof Popover>,
14
+ 'variant' | 'tone' | 'size' | 'box' | 'pill' | 'control' | 'block' | 'bare' | 'hitArea' | 'placement' | 'disabled'
15
+ >;
16
+
17
+ let {
18
+ box = 'md',
19
+ placement = 'bottom-end',
20
+ variant,
21
+ tone,
22
+ size,
23
+ pill,
24
+ control,
25
+ block,
26
+ bare,
27
+ hitArea,
28
+ disabled,
29
+ class: klass = '',
30
+ style: styleProp = '',
31
+ }: TriggerChrome & { class?: string; style?: string } = $props();
12
32
 
13
33
  const index = $derived(Math.max(0, SCALE_LEVELS.findIndex((l) => l.id === fontScale.levelId)));
14
34
  const level = $derived(SCALE_LEVELS[index]);
15
35
  const set = (i: number) => fontScale.set(SCALE_LEVELS[Math.max(0, Math.min(SCALE_LEVELS.length - 1, i))].id);
16
36
  </script>
17
37
 
18
- <Popover label="Text size" placement="bottom-end" triggerClass={klass} box="md">
38
+ <Popover
39
+ label="Text size"
40
+ {placement}
41
+ {box}
42
+ {variant}
43
+ {tone}
44
+ {size}
45
+ {pill}
46
+ {control}
47
+ {block}
48
+ {bare}
49
+ {hitArea}
50
+ {disabled}
51
+ triggerClass={klass}
52
+ style={styleProp}
53
+ >
19
54
  {#snippet trigger()}<span class="glyph" data-tsu="FontScalePicker" title="Text size: {level.label}">A</span>{/snippet}
20
55
  <div class="panel">
21
56
  <div class="row">
@@ -1,5 +1,9 @@
1
- type $$ComponentProps = {
1
+ import type { ComponentProps } from 'svelte';
2
+ import Popover from './Popover.svelte';
3
+ type TriggerChrome = Pick<ComponentProps<typeof Popover>, 'variant' | 'tone' | 'size' | 'box' | 'pill' | 'control' | 'block' | 'bare' | 'hitArea' | 'placement' | 'disabled'>;
4
+ type $$ComponentProps = TriggerChrome & {
2
5
  class?: string;
6
+ style?: string;
3
7
  };
4
8
  declare const FontScalePicker: import("svelte").Component<$$ComponentProps, {}, "">;
5
9
  type FontScalePicker = ReturnType<typeof FontScalePicker>;
@@ -34,6 +34,7 @@
34
34
  | 'tone'
35
35
  | 'size'
36
36
  | 'box'
37
+ | 'pill'
37
38
  | 'control'
38
39
  | 'block'
39
40
  | 'triggerClass'
@@ -55,6 +56,7 @@
55
56
  tone,
56
57
  size,
57
58
  box,
59
+ pill,
58
60
  control,
59
61
  block,
60
62
  triggerClass,
@@ -135,6 +137,7 @@
135
137
  {tone}
136
138
  {size}
137
139
  {box}
140
+ {pill}
138
141
  {control}
139
142
  {block}
140
143
  {triggerClass}
@@ -15,7 +15,7 @@ export interface MenuItem {
15
15
  import type { ComponentProps, Snippet } from 'svelte';
16
16
  import Popover from './Popover.svelte';
17
17
  type PopoverProps = ComponentProps<typeof Popover>;
18
- type TriggerChrome = Pick<PopoverProps, 'variant' | 'tone' | 'size' | 'box' | 'control' | 'block' | 'triggerClass' | 'bare' | 'hitArea' | 'disabled' | 'gap' | 'onopen' | 'onclose'>;
18
+ type TriggerChrome = Pick<PopoverProps, 'variant' | 'tone' | 'size' | 'box' | 'pill' | 'control' | 'block' | 'triggerClass' | 'bare' | 'hitArea' | 'disabled' | 'gap' | 'onopen' | 'onclose'>;
19
19
  type $$ComponentProps = {
20
20
  label: string;
21
21
  items: MenuItem[];
@@ -32,6 +32,7 @@
32
32
  tone = 'none',
33
33
  size,
34
34
  box,
35
+ pill = false,
35
36
  control = false,
36
37
  block = false,
37
38
  hitArea = 'auto',
@@ -58,8 +59,11 @@
58
59
  /** Panel content. Receives `{ close }` to dismiss the panel from inside;
59
60
  * zero-argument snippets keep working. */
60
61
  children: Snippet<[{ close: () => void }]>;
61
- /** Extra class on the trigger button — style it from your own scoped CSS,
62
- * no :global needed (you supply the class). */
62
+ /** Extra class on the trigger button, for a stylesheet you already load
63
+ * globally. Your *scoped* CSS cannot reach it: the element is rendered by
64
+ * `Popover`, so it carries `Popover`'s scope hash, not yours, and a
65
+ * selector like `.toolbar .my-trigger` matches nothing. Use `style` and
66
+ * the published `--pop-trigger-*` / `--pop-box` properties instead. */
63
67
  triggerClass?: string;
64
68
  /** Drop the default ghost-icon chrome so the trigger is an unstyled button
65
69
  * you fully own (pair with `triggerClass`). */
@@ -71,9 +75,11 @@
71
75
  size?: TriggerSize;
72
76
  /** Shared square box scale (`--box-xs/sm/md/lg`) for an icon-only trigger,
73
77
  * pinning the square exactly. The default trigger floors at the `sm` box
74
- * and grows with its content; override that floor from `triggerClass`
75
- * with `--pop-box`. */
78
+ * and grows with its content; override that floor with
79
+ * `style="--pop-box: …"` for any square the four tiers do not cover. */
76
80
  box?: 'xs' | 'sm' | 'md' | 'lg';
81
+ /** Fully rounded trigger (`--r-pill`), as on `Button`. */
82
+ pill?: boolean;
77
83
  /** Use the shared `--control-height` toolbar/composer contract. */
78
84
  control?: boolean;
79
85
  block?: boolean;
@@ -226,6 +232,7 @@
226
232
  class:trigger-control={control}
227
233
  class:trigger-block={block}
228
234
  class:trigger-box={box !== undefined}
235
+ class:trigger-pill={pill}
229
236
  class:hit-compact={hitArea === 'compact'}
230
237
  style:--pop-box={box ? `var(--box-${box})` : undefined}
231
238
  class:trigger-tone-accent={tone === 'accent'}
@@ -271,19 +278,20 @@
271
278
  (triggerClass) overrides it; `:not(.bare)` keeps it off bare triggers such
272
279
  as the Timestamp <time>, which stay plain inline text. */
273
280
  /* The square is one knob, `--pop-box`: the `box` prop sets it inline, and a
274
- consumer's `triggerClass` can set it too (`--pop-box: var(--box-xs)`) — a
275
- plain width/height there would not have beaten a `min-*` floor. */
281
+ consumer reaches it with `style="--pop-box: var(--box-xs)"` — a plain
282
+ width/height would not have beaten a `min-*` floor. */
276
283
  :where(.pop-trigger:not(.bare)) {
277
284
  display: inline-flex;
278
285
  align-items: center;
279
286
  justify-content: center;
280
287
  min-height: var(--pop-box, var(--box-sm));
281
288
  min-width: var(--pop-box, var(--box-sm));
282
- padding: var(--sp-1);
283
- border: 1px solid transparent;
284
- border-radius: var(--r-md);
285
- background: transparent;
286
- color: var(--text);
289
+ padding: var(--pop-trigger-pad, var(--sp-1));
290
+ border: 1px solid var(--pop-trigger-border, transparent);
291
+ border-radius: var(--pop-trigger-radius, var(--r-md));
292
+ background: var(--pop-trigger-bg, transparent);
293
+ color: var(--pop-trigger-fg, var(--text));
294
+ font-size: var(--pop-trigger-size, inherit);
287
295
  transition:
288
296
  background 0.12s var(--ease),
289
297
  border-color 0.12s var(--ease);
@@ -296,11 +304,11 @@
296
304
  .pop-trigger.canonical {
297
305
  min-width: 0;
298
306
  min-height: var(--control-height-default);
299
- padding: var(--sp-2) var(--sp-4);
300
- border-color: var(--border-strong);
301
- background: var(--surface);
307
+ padding: var(--pop-trigger-pad, var(--sp-2) var(--sp-4));
308
+ border-color: var(--pop-trigger-border, var(--border-strong));
309
+ background: var(--pop-trigger-bg, var(--surface));
302
310
  font-weight: var(--fw-medium);
303
- font-size: var(--fs-sm);
311
+ font-size: var(--pop-trigger-size, var(--fs-sm));
304
312
  line-height: 1;
305
313
  user-select: none;
306
314
  white-space: nowrap;
@@ -310,9 +318,9 @@
310
318
  background: var(--surface);
311
319
  }
312
320
  .pop-trigger.trigger-primary {
313
- background: var(--accent);
314
- border-color: var(--accent);
315
- color: var(--text-on-accent);
321
+ background: var(--pop-trigger-bg, var(--accent));
322
+ border-color: var(--pop-trigger-border, var(--accent));
323
+ color: var(--pop-trigger-fg, var(--text-on-accent));
316
324
  font-weight: var(--fw-semibold);
317
325
  }
318
326
  .pop-trigger.trigger-primary:hover:not(:disabled) {
@@ -321,16 +329,16 @@
321
329
  filter: brightness(1.08);
322
330
  }
323
331
  .pop-trigger.trigger-ghost {
324
- background: transparent;
325
- border-color: transparent;
332
+ background: var(--pop-trigger-bg, transparent);
333
+ border-color: var(--pop-trigger-border, transparent);
326
334
  }
327
335
  .pop-trigger.trigger-ghost:hover:not(:disabled) {
328
336
  background: var(--bg-elevated-2);
329
337
  border-color: transparent;
330
338
  }
331
339
  .pop-trigger.trigger-danger {
332
- color: var(--danger);
333
- border-color: color-mix(in srgb, var(--danger) 50%, var(--border));
340
+ color: var(--pop-trigger-fg, var(--danger));
341
+ border-color: var(--pop-trigger-border, color-mix(in srgb, var(--danger) 50%, var(--border)));
334
342
  }
335
343
  .pop-trigger.trigger-danger:hover:not(:disabled) {
336
344
  background: color-mix(in srgb, var(--danger) 14%, transparent);
@@ -339,28 +347,31 @@
339
347
  .pop-trigger.trigger-sm {
340
348
  height: var(--control-height-compact);
341
349
  min-height: var(--control-height-compact);
342
- padding: var(--sp-1) var(--sp-3);
343
- font-size: var(--fs-xs);
350
+ padding: var(--pop-trigger-pad, var(--sp-1) var(--sp-3));
351
+ font-size: var(--pop-trigger-size, var(--fs-xs));
344
352
  }
345
353
  .pop-trigger.trigger-lg {
346
354
  min-height: var(--control-height-large);
347
- padding: var(--sp-3) var(--sp-5);
348
- font-size: var(--fs-base);
355
+ padding: var(--pop-trigger-pad, var(--sp-3) var(--sp-5));
356
+ font-size: var(--pop-trigger-size, var(--fs-base));
349
357
  }
350
358
  .pop-trigger.trigger-control {
351
359
  height: var(--control-height);
352
360
  min-height: var(--control-height);
353
- padding: 0 var(--sp-3);
361
+ padding: var(--pop-trigger-pad, 0 var(--sp-3));
354
362
  }
355
363
  .pop-trigger.trigger-block {
356
364
  width: 100%;
357
365
  }
366
+ .pop-trigger.trigger-pill {
367
+ border-radius: var(--pop-trigger-radius, var(--r-pill));
368
+ }
358
369
  .pop-trigger.trigger-box {
359
370
  width: var(--pop-box);
360
371
  min-width: var(--pop-box);
361
372
  height: var(--pop-box);
362
373
  min-height: var(--pop-box);
363
- padding: 0;
374
+ padding: var(--pop-trigger-pad, 0);
364
375
  flex: none;
365
376
  }
366
377
  /* Coarse pointers: icon-only triggers extend their hit area to --touch-target
@@ -395,8 +406,8 @@
395
406
  .pop-trigger.trigger-tone-info,
396
407
  .pop-trigger.trigger-tone-warn,
397
408
  .pop-trigger.trigger-tone-danger {
398
- color: var(--pop-trigger-tone);
399
- border-color: color-mix(in srgb, var(--pop-trigger-tone) 50%, var(--border));
409
+ color: var(--pop-trigger-fg, var(--pop-trigger-tone));
410
+ border-color: var(--pop-trigger-border, color-mix(in srgb, var(--pop-trigger-tone) 50%, var(--border)));
400
411
  }
401
412
  .pop-trigger.trigger-tone-accent:hover:not(:disabled),
402
413
  .pop-trigger.trigger-tone-success:hover:not(:disabled),
@@ -407,9 +418,9 @@
407
418
  border-color: var(--pop-trigger-tone);
408
419
  }
409
420
  .pop-trigger.trigger-primary.trigger-tone-success {
410
- background: var(--ok);
411
- border-color: var(--ok);
412
- color: var(--text-on-success);
421
+ background: var(--pop-trigger-bg, var(--ok));
422
+ border-color: var(--pop-trigger-border, var(--ok));
423
+ color: var(--pop-trigger-fg, var(--text-on-success));
413
424
  }
414
425
  .pop-trigger.trigger-primary.trigger-tone-success:hover:not(:disabled) {
415
426
  background: var(--ok);
@@ -431,12 +442,13 @@
431
442
  /* `bare`: strip the chrome down to a plain button the consumer styles. */
432
443
  :where(.pop-trigger.bare) {
433
444
  display: inline;
434
- padding: 0;
445
+ padding: var(--pop-trigger-pad, 0);
435
446
  border: 0;
436
- border-radius: 0;
437
- background: none;
438
- color: inherit;
447
+ border-radius: var(--pop-trigger-radius, 0);
448
+ background: var(--pop-trigger-bg, none);
449
+ color: var(--pop-trigger-fg, inherit);
439
450
  font: inherit;
451
+ font-size: var(--pop-trigger-size, inherit);
440
452
  line-height: inherit;
441
453
  vertical-align: baseline;
442
454
  cursor: pointer;
@@ -17,8 +17,11 @@ type $$ComponentProps = {
17
17
  children: Snippet<[{
18
18
  close: () => void;
19
19
  }]>;
20
- /** Extra class on the trigger button — style it from your own scoped CSS,
21
- * no :global needed (you supply the class). */
20
+ /** Extra class on the trigger button, for a stylesheet you already load
21
+ * globally. Your *scoped* CSS cannot reach it: the element is rendered by
22
+ * `Popover`, so it carries `Popover`'s scope hash, not yours, and a
23
+ * selector like `.toolbar .my-trigger` matches nothing. Use `style` and
24
+ * the published `--pop-trigger-*` / `--pop-box` properties instead. */
22
25
  triggerClass?: string;
23
26
  /** Drop the default ghost-icon chrome so the trigger is an unstyled button
24
27
  * you fully own (pair with `triggerClass`). */
@@ -30,9 +33,11 @@ type $$ComponentProps = {
30
33
  size?: TriggerSize;
31
34
  /** Shared square box scale (`--box-xs/sm/md/lg`) for an icon-only trigger,
32
35
  * pinning the square exactly. The default trigger floors at the `sm` box
33
- * and grows with its content; override that floor from `triggerClass`
34
- * with `--pop-box`. */
36
+ * and grows with its content; override that floor with
37
+ * `style="--pop-box: …"` for any square the four tiers do not cover. */
35
38
  box?: 'xs' | 'sm' | 'md' | 'lg';
39
+ /** Fully rounded trigger (`--r-pill`), as on `Button`. */
40
+ pill?: boolean;
36
41
  /** Use the shared `--control-height` toolbar/composer contract. */
37
42
  control?: boolean;
38
43
  block?: boolean;
@@ -5,18 +5,36 @@
5
5
  // (--c-bg, --c-surface, --c-text, --c-accent; the derived --bg etc. are
6
6
  // resolved once at :root and would not re-scope). The root default theme
7
7
  // has no [data-theme] block, so its swatch carries the :root values.
8
+ import type { ComponentProps } from 'svelte';
8
9
  import Popover from './Popover.svelte';
9
10
  import { type ThemeDef, theme } from '../../stores/theme.svelte';
10
11
  import { AUTO_THEME } from '../../theme-mode';
11
12
 
13
+ type TriggerChrome = Pick<
14
+ ComponentProps<typeof Popover>,
15
+ 'variant' | 'tone' | 'size' | 'box' | 'pill' | 'control' | 'block' | 'bare' | 'hitArea' | 'placement' | 'disabled'
16
+ >;
17
+
12
18
  let {
19
+ box = 'md',
20
+ placement = 'bottom-end',
21
+ variant,
22
+ tone,
23
+ size,
24
+ pill,
25
+ control,
26
+ block,
27
+ bare,
28
+ hitArea,
29
+ disabled,
13
30
  auto = false,
14
31
  autoLabel = 'Auto',
15
32
  autoHelp = 'Follow the system light/dark setting',
16
33
  lightLabel = 'Light',
17
34
  darkLabel = 'Dark',
18
35
  class: klass = '',
19
- }: {
36
+ style: styleProp = '',
37
+ }: TriggerChrome & {
20
38
  /** Offer an "auto" row that follows `prefers-color-scheme`, remembering
21
39
  * one light and one dark theme. */
22
40
  auto?: boolean;
@@ -25,6 +43,7 @@
25
43
  lightLabel?: string;
26
44
  darkLabel?: string;
27
45
  class?: string;
46
+ style?: string;
28
47
  } = $props();
29
48
 
30
49
  let hovered = $state<ThemeDef | null>(null);
@@ -54,7 +73,22 @@
54
73
  </span>
55
74
  {/snippet}
56
75
 
57
- <Popover label={title} placement="bottom-end" triggerClass={klass} box="md">
76
+ <Popover
77
+ label={title}
78
+ {placement}
79
+ {box}
80
+ {variant}
81
+ {tone}
82
+ {size}
83
+ {pill}
84
+ {control}
85
+ {block}
86
+ {bare}
87
+ {hitArea}
88
+ {disabled}
89
+ triggerClass={klass}
90
+ style={styleProp}
91
+ >
58
92
  {#snippet trigger()}<span class="trigger" data-tsu="ThemePicker" {title}>{@render swatch(theme.current)}{#if isAuto}<span class="auto-dot" aria-hidden="true">◐</span>{/if}</span>{/snippet}
59
93
  <div class="panel">
60
94
  {#each groups as g (g.mode)}
@@ -1,4 +1,7 @@
1
- type $$ComponentProps = {
1
+ import type { ComponentProps } from 'svelte';
2
+ import Popover from './Popover.svelte';
3
+ type TriggerChrome = Pick<ComponentProps<typeof Popover>, 'variant' | 'tone' | 'size' | 'box' | 'pill' | 'control' | 'block' | 'bare' | 'hitArea' | 'placement' | 'disabled'>;
4
+ type $$ComponentProps = TriggerChrome & {
2
5
  /** Offer an "auto" row that follows `prefers-color-scheme`, remembering
3
6
  * one light and one dark theme. */
4
7
  auto?: boolean;
@@ -7,6 +10,7 @@ type $$ComponentProps = {
7
10
  lightLabel?: string;
8
11
  darkLabel?: string;
9
12
  class?: string;
13
+ style?: string;
10
14
  };
11
15
  declare const ThemePicker: import("svelte").Component<$$ComponentProps, {}, "">;
12
16
  type ThemePicker = ReturnType<typeof ThemePicker>;
package/dist/index.d.ts CHANGED
@@ -87,7 +87,7 @@ export { default as FilterSearchBar } from './components/organisms/FilterSearchB
87
87
  export { EMOJI_GROUPS, type EmojiEntry, type EmojiGroup, searchEmoji } from './emoji';
88
88
  export { FIELD_KEY, type FieldContext, getFieldContext, setFieldContext, warnUnlabelled, } from './field-context';
89
89
  export * as filterQuery from './query';
90
- export { type AndNode, activeToken, compilePredicate, defaultOperator, type ExprNode, type FieldDef, type FieldType, type FilterNode, filters, findField, freeText, type LeafNode, type NotNode, OPERATORS, type Operator, type OperatorId, type OrNode, operatorByCode, operatorById, operatorsFor, parse, type Query, type QueryNode, resolveValues, type Schema, type Suggestion, type SuggestKind, type SuggestState, serialize, serializeFilter, suggest, type TextNode, toSql, type ValueContext, type ValueOption, type ValueProvider, walk, } from './query';
90
+ export { type AndNode, activeToken, compilePredicate, defaultOperator, type ExprNode, type FieldDef, type FieldType, type FilterNode, filters, findField, freeText, type LeafNode, type NotNode, OPERATORS, type Operator, type OperatorId, type OrNode, operatorByCode, operatorById, operatorsFor, parse, type Query, type QueryNode, resolveValues, type Schema, type Suggestion, type SuggestKind, type SuggestState, serialize, serializeFilter, singleQuery, suggest, suggestSingle, type TextNode, toSql, type ValueContext, type ValueOption, type ValueProvider, walk, } from './query';
91
91
  export { type OptionSection, sectionOptions } from './select-options';
92
92
  export type { ControlSize } from './size';
93
93
  export { fontScale, SCALE_LEVELS, type ScaleLevel } from './stores/fontscale.svelte';
package/dist/index.js CHANGED
@@ -98,7 +98,7 @@ export { EMOJI_GROUPS, searchEmoji } from './emoji';
98
98
  export { FIELD_KEY, getFieldContext, setFieldContext, warnUnlabelled, } from './field-context';
99
99
  // ---- query core (headless: schema / parser / AST / suggest / compilers) ----
100
100
  export * as filterQuery from './query';
101
- export { activeToken, compilePredicate, defaultOperator, filters, findField, freeText, OPERATORS, operatorByCode, operatorById, operatorsFor, parse, resolveValues, serialize, serializeFilter, suggest, toSql, walk, } from './query';
101
+ export { activeToken, compilePredicate, defaultOperator, filters, findField, freeText, OPERATORS, operatorByCode, operatorById, operatorsFor, parse, resolveValues, serialize, serializeFilter, singleQuery, suggest, suggestSingle, toSql, walk, } from './query';
102
102
  export { sectionOptions } from './select-options';
103
103
  export { fontScale, SCALE_LEVELS } from './stores/fontscale.svelte';
104
104
  // ---- stores / actions ----
@@ -5,5 +5,6 @@ export { parse } from './parser';
5
5
  export { compilePredicate, serialize, serializeFilter, toSql } from './query';
6
6
  export type { FieldDef, FieldType, Operator, OperatorId, Schema, ValueContext, ValueOption, ValueProvider, } from './schema';
7
7
  export { defaultOperator, findField, OPERATORS, operatorByCode, operatorById, operatorsFor, resolveValues, } from './schema';
8
+ export { singleQuery, suggestSingle } from './single';
8
9
  export type { Suggestion, SuggestKind, SuggestState } from './suggest';
9
10
  export { activeToken, suggest } from './suggest';
@@ -7,4 +7,5 @@ export { autoQuoteEdit, backspaceEmptyQuotes, closingQuoteExit, insideQuoteAtCar
7
7
  export { parse } from './parser';
8
8
  export { compilePredicate, serialize, serializeFilter, toSql } from './query';
9
9
  export { defaultOperator, findField, OPERATORS, operatorByCode, operatorById, operatorsFor, resolveValues, } from './schema';
10
+ export { singleQuery, suggestSingle } from './single';
10
11
  export { activeToken, suggest } from './suggest';
@@ -0,0 +1,15 @@
1
+ import { type Schema } from './schema';
2
+ import type { SuggestState } from './suggest';
3
+ /**
4
+ * The textual query a bare `value` stands for, using `key`'s default operator —
5
+ * `"/srv/my app"` under key `cwd` becomes `cwd:"/srv/my app"`. Empty values (and
6
+ * unknown keys) yield an empty query so the AST stays empty rather than parsing
7
+ * a half-written clause.
8
+ */
9
+ export declare function singleQuery(schema: Schema, key: string, value: string): string;
10
+ /**
11
+ * Value suggestions for `key` given the whole box as the fragment. The span
12
+ * always covers the entire value, so accepting an item replaces it outright —
13
+ * there is no field or operator step to advance to.
14
+ */
15
+ export declare function suggestSingle(schema: Schema, key: string, value: string): Promise<SuggestState | null>;
@@ -0,0 +1,43 @@
1
+ // ─────────────────────────────────────────────────────────────────────────
2
+ // Single-key mode — the headless half of FilterInput's `key` prop: the box
3
+ // holds the bare value (`/srv/app`, not `cwd:"/srv/app"`), completing through
4
+ // the field's own provider, and the equivalent query is synthesised only for
5
+ // the `inline` / `below` snippets and the `onchange` AST.
6
+ // ─────────────────────────────────────────────────────────────────────────
7
+ import { defaultOperator, findField, resolveValues } from './schema';
8
+ function quoteIfNeeded(v) {
9
+ return /[\s,()]/.test(v) ? `"${v}"` : v;
10
+ }
11
+ /**
12
+ * The textual query a bare `value` stands for, using `key`'s default operator —
13
+ * `"/srv/my app"` under key `cwd` becomes `cwd:"/srv/my app"`. Empty values (and
14
+ * unknown keys) yield an empty query so the AST stays empty rather than parsing
15
+ * a half-written clause.
16
+ */
17
+ export function singleQuery(schema, key, value) {
18
+ const field = findField(schema, key);
19
+ if (!field || !value)
20
+ return '';
21
+ return `${field.name}${defaultOperator(field).code}${quoteIfNeeded(value)}`;
22
+ }
23
+ /**
24
+ * Value suggestions for `key` given the whole box as the fragment. The span
25
+ * always covers the entire value, so accepting an item replaces it outright —
26
+ * there is no field or operator step to advance to.
27
+ */
28
+ export async function suggestSingle(schema, key, value) {
29
+ const field = findField(schema, key);
30
+ if (!field)
31
+ return null;
32
+ const span = [0, value.length];
33
+ const options = await resolveValues(field, value, { rawQuery: value, span, caret: value.length });
34
+ if (options.length === 0)
35
+ return null;
36
+ const items = options.map((o) => ({
37
+ label: o.label,
38
+ hint: o.hint,
39
+ insert: o.value,
40
+ caret: o.value.length,
41
+ }));
42
+ return { kind: 'value', span, items };
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dorsk/tsumikit",
3
- "version": "0.51.0",
3
+ "version": "0.53.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",