@dorsk/tsumikit 0.51.0 → 0.52.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
@@ -267,7 +267,10 @@ hooks on head/row/cell; `responsive="stack"` turns rows into cards below
267
267
  prefix, the `<table>` stays a table for assistive tech), FilterSearchBar /
268
268
  FilterInput (`size="sm"` compact bar on `--control-height-compact`,
269
269
  `shape="pill"`, `surface`, `hotkey="/"` focuses the input from anywhere
270
- outside an editable element, `showHotkey` renders the `<kbd>` hint, `grow`).
270
+ outside an editable element, `showHotkey` renders the `<kbd>` hint, `grow`;
271
+ `key="cwd"` puts FilterInput in single-key mode — `value` is the bare value
272
+ instead of a `key:"value"` query, no key prefix in the box, the `placeholder`
273
+ shows while empty, and completion still runs through that field's provider).
271
274
 
272
275
  **Layouts:** AppShell (responsive header/sidebar/main/footer — persistent
273
276
  sidebar on desktop, overlay drawer on mobile, optionally resizable;
@@ -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. */
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.52.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",