@kerfjs/ui 5.0.0-beta.58 → 5.0.0-beta.59
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 +11 -1
- package/ai/component-catalog.json +48 -15
- package/ai/component-catalog.schema.json +16 -0
- package/ai/component-composition.json +16 -2
- package/ai/components/README.md +2 -2
- package/ai/components/popup-menu.md +96 -7
- package/ai/components/text.md +3 -3
- package/ai/components/token-search-field.md +6 -4
- package/ai/public-api-signatures-v1.md +49 -81
- package/ai/skill.md +5 -1
- package/ai/webawesome-jsx-signatures-v1.md +1 -1
- package/dist/catalog.js +3 -3
- package/dist/chunk-F7F7KRO2.js +187 -0
- package/dist/chunk-F7F7KRO2.js.map +1 -0
- package/dist/{chunk-63ZWBGE3.js → chunk-RRB26JRJ.js} +25 -3
- package/dist/chunk-RRB26JRJ.js.map +1 -0
- package/dist/{chunk-NBYTIVHS.js → chunk-UFYZHYWU.js} +118 -12
- package/dist/chunk-UFYZHYWU.js.map +1 -0
- package/dist/{chunk-WLG6SSNT.js → chunk-UWONG4QE.js} +41 -8
- package/dist/chunk-UWONG4QE.js.map +1 -0
- package/dist/{chunk-553ASUK6.js → chunk-UX2VVXZ3.js} +2 -2
- package/dist/{chunk-553ASUK6.js.map → chunk-UX2VVXZ3.js.map} +1 -1
- package/dist/{chunk-24PHKUR3.js → chunk-UXT7TE6B.js} +1 -1
- package/dist/chunk-UXT7TE6B.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -5
- package/dist/list-header.js +1 -1
- package/dist/popup-menu.d.ts +31 -3
- package/dist/popup-menu.js +1 -1
- package/dist/styles/popup-menu.css +33 -4
- package/dist/styles/text.css +8 -0
- package/dist/styles/token-search-field.css +29 -0
- package/dist/text.d.ts +1 -1
- package/dist/text.js +1 -1
- package/dist/token-search-field-CCke1yvQ.d.ts +126 -0
- package/dist/token-search-field.d.ts +3 -75
- package/dist/token-search-field.js +1 -1
- package/dist/token-search-model.d.ts +3 -0
- package/dist/token-search-model.js +3 -0
- package/dist/token-search-model.js.map +1 -0
- package/dist/wire-token-search-fields.d.ts +5 -1
- package/dist/wire-token-search-fields.js +1 -1
- package/docs/accessibility.md +14 -0
- package/docs/component-contract.md +2 -1
- package/docs/component-integration.md +8 -1
- package/docs/component-selection.md +40 -0
- package/docs/layout.md +4 -1
- package/llms.txt +6 -3
- package/package.json +5 -1
- package/dist/chunk-24PHKUR3.js.map +0 -1
- package/dist/chunk-63ZWBGE3.js.map +0 -1
- package/dist/chunk-NBYTIVHS.js.map +0 -1
- package/dist/chunk-WLG6SSNT.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Public API signatures for the UI authoring corpus
|
|
2
2
|
|
|
3
|
-
Generated from emitted declarations for `@kerfjs/ui@5.0.0-beta.
|
|
3
|
+
Generated from emitted declarations for `@kerfjs/ui@5.0.0-beta.59` and `kerfjs@5.0.0-beta.59`. This bounded reference covers only APIs used by the seven-task corpus. It is interface evidence, not an implementation or runtime guarantee.
|
|
4
4
|
|
|
5
5
|
## `@kerfjs/ui/css-values`
|
|
6
6
|
|
|
@@ -405,9 +405,19 @@ interface PopupMenuItem {
|
|
|
405
405
|
value?: string;
|
|
406
406
|
/** Leading icon, typically a `LucideIcon`; it is placed in the item's icon slot. */
|
|
407
407
|
icon?: SafeHtml;
|
|
408
|
+
/** A checked menu choice, including nested choices. */
|
|
409
|
+
checked?: boolean;
|
|
410
|
+
/** Trailing safe content, such as a selected-choice tick. */
|
|
411
|
+
details?: SafeHtml;
|
|
412
|
+
/** Destructive command styling. */
|
|
413
|
+
tone?: 'default' | 'danger';
|
|
408
414
|
disabled?: boolean;
|
|
415
|
+
/** Native tooltip text for a disabled command. */
|
|
416
|
+
disabledReason?: string;
|
|
409
417
|
/** Application `data-*` metadata such as a record id. */
|
|
410
418
|
attributes?: PopupMenuDataAttributes;
|
|
419
|
+
/** Child commands opened by hover or keyboard navigation. */
|
|
420
|
+
submenu?: readonly PopupMenuItem[];
|
|
411
421
|
}
|
|
412
422
|
/** A labeled group heading; items that follow it belong to the group. */
|
|
413
423
|
interface PopupMenuHeading {
|
|
@@ -431,7 +441,17 @@ type PopupMenuTriggerName = {
|
|
|
431
441
|
*/
|
|
432
442
|
label: string;
|
|
433
443
|
};
|
|
434
|
-
type
|
|
444
|
+
type PopupMenuTrigger = (PopupMenuTriggerName & {
|
|
445
|
+
context?: false;
|
|
446
|
+
}) | {
|
|
447
|
+
context: true;
|
|
448
|
+
label: string;
|
|
449
|
+
text?: never;
|
|
450
|
+
icon?: never;
|
|
451
|
+
caret?: never;
|
|
452
|
+
disabled?: never;
|
|
453
|
+
};
|
|
454
|
+
type PopupMenuProps = PopupMenuTrigger & {
|
|
435
455
|
/** Trigger icon, typically a `LucideIcon`, before any visible text. */
|
|
436
456
|
icon?: KerfUiContent;
|
|
437
457
|
items: readonly PopupMenuEntry[];
|
|
@@ -445,6 +465,14 @@ type PopupMenuProps = PopupMenuTriggerName & {
|
|
|
445
465
|
/** Native named-slot assignment when composed inside a web component. */
|
|
446
466
|
slot?: string;
|
|
447
467
|
};
|
|
468
|
+
/** A rendered context PopupMenu with Web Awesome's controlled open state. */
|
|
469
|
+
type PopupMenuElement = HTMLElement & {
|
|
470
|
+
open: boolean;
|
|
471
|
+
};
|
|
472
|
+
/** Open a context PopupMenu at viewport pointer coordinates. */
|
|
473
|
+
declare function openPopupMenuAt(menu: PopupMenuElement, x: number, y: number): void;
|
|
474
|
+
/** Close a programmatically opened PopupMenu. */
|
|
475
|
+
declare function closePopupMenu(menu: PopupMenuElement): void;
|
|
448
476
|
/**
|
|
449
477
|
* An action menu: a trigger button that opens a list of commands. Renders the
|
|
450
478
|
* `wa-dropdown` root directly, so a `single` `ToolbarControlGroup` (with
|
|
@@ -453,9 +481,9 @@ type PopupMenuProps = PopupMenuTriggerName & {
|
|
|
453
481
|
* open-state reaction. Import `@kerfjs/ui/popup-menu/register` once to register
|
|
454
482
|
* the Web Awesome elements.
|
|
455
483
|
*/
|
|
456
|
-
declare function PopupMenu({ text, label, icon, items, placement, caret, disabled, rootAttributes, slot, }: PopupMenuProps): SafeHtml;
|
|
484
|
+
declare function PopupMenu({ text, label, icon, items, placement, caret, disabled, rootAttributes, slot, context, }: PopupMenuProps): SafeHtml;
|
|
457
485
|
|
|
458
|
-
export { PopupMenu, type PopupMenuDivider, type PopupMenuEntry, type PopupMenuHeading, type PopupMenuItem, type PopupMenuPlacement, type PopupMenuProps };
|
|
486
|
+
export { PopupMenu, type PopupMenuDivider, type PopupMenuElement, type PopupMenuEntry, type PopupMenuHeading, type PopupMenuItem, type PopupMenuPlacement, type PopupMenuProps, closePopupMenu, openPopupMenuAt };
|
|
459
487
|
```
|
|
460
488
|
|
|
461
489
|
## `@kerfjs/ui/list-header`
|
|
@@ -2715,87 +2743,17 @@ export { SunkenPanel, type SunkenPanelProps, type SunkenPanelShape };
|
|
|
2715
2743
|
## `@kerfjs/ui/token-search-field`
|
|
2716
2744
|
|
|
2717
2745
|
```ts
|
|
2718
|
-
import
|
|
2719
|
-
import
|
|
2720
|
-
|
|
2721
|
-
interface TokenSearchToken {
|
|
2722
|
-
value: string;
|
|
2723
|
-
label: string;
|
|
2724
|
-
offset?: number;
|
|
2725
|
-
accessibleLabel?: string;
|
|
2726
|
-
}
|
|
2727
|
-
type TokenSearchEditorAttributes = Readonly<Record<`data-${string}`, string | undefined> & {
|
|
2728
|
-
'data-component'?: never;
|
|
2729
|
-
'data-key'?: never;
|
|
2730
|
-
'data-morph-skip'?: never;
|
|
2731
|
-
'data-token-search-editor'?: never;
|
|
2732
|
-
'data-token-count'?: never;
|
|
2733
|
-
'data-placeholder'?: never;
|
|
2734
|
-
}>;
|
|
2735
|
-
interface TokenSearchFieldBaseProps {
|
|
2736
|
-
id: string;
|
|
2737
|
-
label: string;
|
|
2738
|
-
query?: string;
|
|
2739
|
-
tokens?: readonly TokenSearchToken[];
|
|
2740
|
-
/**
|
|
2741
|
-
* Editor identity for a programmatic text replacement. The editor's text is
|
|
2742
|
-
* DOM-owned between token changes, so a new `query` with the same tokens
|
|
2743
|
-
* does not re-render it (that would reset the caret on every keystroke).
|
|
2744
|
-
* Change `revision` when the app replaces the text itself — reseeding a
|
|
2745
|
-
* persistent dialog's field on reopen, applying a saved search — and the
|
|
2746
|
-
* editor is rebuilt from `query` and `tokens`.
|
|
2747
|
-
*/
|
|
2748
|
-
revision?: string | number;
|
|
2749
|
-
placeholder?: string;
|
|
2750
|
-
tokenPlaceholder?: string;
|
|
2751
|
-
disabled?: boolean;
|
|
2752
|
-
autofocus?: boolean;
|
|
2753
|
-
leading?: KerfUiContent;
|
|
2754
|
-
trailing?: KerfUiContent;
|
|
2755
|
-
/** Standalone field chrome or the inset visual layer of a configured toolbar group. */
|
|
2756
|
-
presentation?: 'standalone' | 'toolbar-group';
|
|
2757
|
-
editAction?: string;
|
|
2758
|
-
removeAction?: string;
|
|
2759
|
-
clearAction?: string;
|
|
2760
|
-
clearLabel?: string;
|
|
2761
|
-
/** Icon for the clear button. Default: the Lucide `x` glyph token chips also use. */
|
|
2762
|
-
clearIcon?: SafeHtml;
|
|
2763
|
-
className?: string;
|
|
2764
|
-
editorAttributes?: TokenSearchEditorAttributes;
|
|
2765
|
-
/** Native named-slot assignment when composed inside a web component. */
|
|
2766
|
-
slot?: string;
|
|
2767
|
-
}
|
|
2768
|
-
type TokenSearchCollapsibleProps = {
|
|
2769
|
-
/** Allow an empty field to render as one iconic action. */
|
|
2770
|
-
collapsible: true;
|
|
2771
|
-
/** Keep an empty collapsible field open while the application owns focus. */
|
|
2772
|
-
expanded?: boolean;
|
|
2773
|
-
expandAction?: string;
|
|
2774
|
-
expandLabel?: string;
|
|
2775
|
-
} | {
|
|
2776
|
-
collapsible?: false;
|
|
2777
|
-
expanded?: never;
|
|
2778
|
-
expandAction?: never;
|
|
2779
|
-
expandLabel?: never;
|
|
2780
|
-
};
|
|
2781
|
-
type TokenSearchFieldProps = TokenSearchFieldBaseProps & TokenSearchCollapsibleProps;
|
|
2782
|
-
interface TokenSearchFieldValue {
|
|
2783
|
-
query: string;
|
|
2784
|
-
tokens: TokenSearchToken[];
|
|
2785
|
-
}
|
|
2786
|
-
declare function TokenSearchField({ id, label, query, tokens, revision, placeholder, tokenPlaceholder, disabled, autofocus, collapsible, expanded, expandAction, expandLabel, leading, trailing, presentation, editAction, removeAction, clearAction, clearLabel, clearIcon, className, editorAttributes, slot, }: TokenSearchFieldProps): SafeHtml;
|
|
2787
|
-
/** Read editable text and ordered token offsets from a rendered TokenSearchField editor. */
|
|
2788
|
-
declare function readTokenSearchField(editor: HTMLElement, knownTokens?: readonly TokenSearchToken[]): TokenSearchFieldValue;
|
|
2789
|
-
/** Focus an editor and place its caret at a text offset, skipping atomic token chips. */
|
|
2790
|
-
declare function placeTokenSearchCaret(editor: HTMLElement, offset?: number): void;
|
|
2791
|
-
|
|
2792
|
-
export { type TokenSearchEditorAttributes, TokenSearchField, type TokenSearchFieldProps, type TokenSearchFieldValue, type TokenSearchToken, placeTokenSearchCaret, readTokenSearchField };
|
|
2746
|
+
import 'kerfjs';
|
|
2747
|
+
import './semantic-content-BbzjvSu9.js';
|
|
2748
|
+
export { T as TokenSearchEditorAttributes, a as TokenSearchField, b as TokenSearchFieldProps, c as TokenSearchFieldValue, j as TokenSearchToken, p as placeTokenSearchCaret, r as readTokenSearchField } from './token-search-field-CCke1yvQ.js';
|
|
2793
2749
|
```
|
|
2794
2750
|
|
|
2795
2751
|
## `@kerfjs/ui/wire-token-search-fields`
|
|
2796
2752
|
|
|
2797
2753
|
```ts
|
|
2798
2754
|
import { Signal } from 'kerfjs';
|
|
2755
|
+
import { d as TokenSearchModel } from './token-search-field-CCke1yvQ.js';
|
|
2756
|
+
import './semantic-content-BbzjvSu9.js';
|
|
2799
2757
|
|
|
2800
2758
|
interface TokenSearchSubmit {
|
|
2801
2759
|
id: string;
|
|
@@ -2873,6 +2831,8 @@ interface TokenSearchCollapsibleOptions {
|
|
|
2873
2831
|
signals?: Readonly<Record<string, Signal<boolean>>>;
|
|
2874
2832
|
}
|
|
2875
2833
|
interface WireTokenSearchFieldsOptions {
|
|
2834
|
+
/** Optional automatic grammar/suggestion models, keyed by TokenSearchField id. */
|
|
2835
|
+
models?: Readonly<Record<string, TokenSearchModel>>;
|
|
2876
2836
|
onSubmit?: (submission: TokenSearchSubmit) => void;
|
|
2877
2837
|
/** Fired on every editor `input`, after the browser mutates it, so a caller can drop its own `input` listener. */
|
|
2878
2838
|
onEdit?: (edit: TokenSearchEdit) => void;
|
|
@@ -2903,7 +2863,7 @@ interface TokenSearchFieldsHandle {
|
|
|
2903
2863
|
* expand/collapse/focus. Returns a {@link TokenSearchFieldsHandle} — a disposer that also
|
|
2904
2864
|
* exposes the managed `expanded` state per field id.
|
|
2905
2865
|
*/
|
|
2906
|
-
declare function wireTokenSearchFields(root: HTMLElement, { onSubmit, onEdit, collapsible, keyboard, }?: WireTokenSearchFieldsOptions): TokenSearchFieldsHandle;
|
|
2866
|
+
declare function wireTokenSearchFields(root: HTMLElement, { models, onSubmit, onEdit, collapsible, keyboard, }?: WireTokenSearchFieldsOptions): TokenSearchFieldsHandle;
|
|
2907
2867
|
|
|
2908
2868
|
export { type TokenSearchCollapsibleOptions, type TokenSearchEdit, type TokenSearchFieldsHandle, type TokenSearchKeyboardOptions, type TokenSearchSubmit, type TokenSearchTokenRemoval, type WireTokenSearchFieldsOptions, wireTokenSearchFields };
|
|
2909
2869
|
```
|
|
@@ -3107,7 +3067,7 @@ import { K as KerfUiContent } from './semantic-content-BbzjvSu9.js';
|
|
|
3107
3067
|
|
|
3108
3068
|
type TextVariant = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span';
|
|
3109
3069
|
type TextTone = 'default' | 'quiet' | 'danger';
|
|
3110
|
-
type TextSize = 'default' | '
|
|
3070
|
+
type TextSize = 'compact' | 'default' | 'large' | 'xlarge';
|
|
3111
3071
|
type TextFont = 'default' | 'monospace';
|
|
3112
3072
|
type TextBorder = 'transparent' | 'none';
|
|
3113
3073
|
type TextContent = KerfUiContent | string | number | readonly TextContent[];
|
|
@@ -3250,3 +3210,11 @@ declare function Spacer({ flex, width, height, className, slot, }: SpacerProps):
|
|
|
3250
3210
|
|
|
3251
3211
|
export { CssLength, Spacer, type SpacerProps, UiSpaceName };
|
|
3252
3212
|
```
|
|
3213
|
+
|
|
3214
|
+
## `@kerfjs/ui/token-search-model`
|
|
3215
|
+
|
|
3216
|
+
```ts
|
|
3217
|
+
import 'kerfjs';
|
|
3218
|
+
export { d as TokenSearchModel, e as TokenSearchModelOptions, f as TokenSearchResolvedToken, g as TokenSearchRule, h as TokenSearchState, i as TokenSearchSuggestion, k as createTokenSearchModel } from './token-search-field-CCke1yvQ.js';
|
|
3219
|
+
import './semantic-content-BbzjvSu9.js';
|
|
3220
|
+
```
|
package/ai/skill.md
CHANGED
|
@@ -194,7 +194,7 @@ Hard rules:
|
|
|
194
194
|
8. `ResizableRegion` uses `wireResizableRegions()` for Arrow, Shift+Arrow, Home/End, and pointer behavior. The app owns size persistence. `handleIcon` replaces decorative dormant glyph content only. Set `responsiveFillAt="narrow"` or `"compact"` when a responsive composition exposes the pane as its only inline track; the component fills that track and hides its dormant separator. For compact overlays, set `presentation="overlay"` and the overlay maximum variables. A lone child (normally a `Pane`) fills the region and owns scrolling; several children stack at natural height. Do not add descendant width/height overrides.
|
|
195
195
|
9. Use controlled `SegmentedControl` for a small exclusive choice set. Select `appearance="toolbar"` when nesting it inside `ToolbarControlGroup`; use rounded or pill shapes for standalone contexts. The package derives inset hover/selection radii from the owning outer shape through `--kui-layout-highlight-inset`; do not restyle an inner highlight to an independent radius. Handle its action, update `value`, keep meaningful choice labels, and preserve every enabled native button in sequential Tab order.
|
|
196
196
|
10. Compose `AppTab` inside controlled `TabBar`; call `wireTabBars()` once and retain its disposer. It owns same-bar drag mechanics, including proximity-based horizontal edge autoscroll, while the app applies `onReorder` and owns order, selection, panels, close policy, routing, and persistence. For a tab-local action followed by a separately pinned workspace action, set `trailingPlacement="adjacent"`, put the local action in `trailing`, and the workspace action in `end`; TabBar owns the shrinking/scrolling tabs and fixed action geometry, so do not add descendant flex overrides. Arrow/Home/End select on move by default and restore focus by bar/tab identity when controlled activation replaces the strip; pass `activation: 'manual'` (or `TabBar activation="manual"` per strip) so arrow keys move focus only and the user selects with Enter/Space/click — use it when selecting a tab is a heavy action. Use runtime-filtered `rootAttributes` for domain metadata and keep an optional `closeIcon` decorative and noninteractive.
|
|
197
|
-
11. Use token-controlled `TokenSearchField` when free text and removable structured filters share one editor. Editable text stays DOM-owned between token changes; to replace the text programmatically (reseeding a persistent dialog's field on reopen, applying a saved search) change the field's `revision` along with `query`, never re-key a wrapper. The leading icon, first text line, clear action, and trailing slot share one fixed row when content wraps.
|
|
197
|
+
11. Use token-controlled `TokenSearchField` when free text and removable structured filters share one editor. Editable text stays DOM-owned between token changes; to replace the text programmatically (reseeding a persistent dialog's field on reopen, applying a saved search) change the field's `revision` along with `query`, never re-key a wrapper. The leading icon, first text line, clear action, and trailing slot share one fixed row when content wraps. For automatic parsing, chips, suggestions, and evaluation, use createTokenSearchModel with grammar rules, pass it as TokenSearchField.model, and register it in wireTokenSearchFields models; otherwise the app owns parsing and suggestions; call `readTokenSearchField()` on input, empty `textContent` on clear, use `placeTokenSearchCaret()` after explicit controlled focus changes, and call `wireTokenSearchFields()` once so Enter submits without inserting a line break and keyboard chip deletion restores focus plus the text-relative caret after controlled replacement. `clearIcon` replaces the clear action's glyph (its `clearLabel` still names it). Enable `collapsible` for an animated iconic closed state, standalone or inside `ToolbarControlGroup`; the field keeps text or tokens expanded, and `wireTokenSearchFields()` manages the transient expand/collapse/focus by default (activate to reveal + focus, Escape or empty blur to collapse). Bind the field's `expanded` to the returned handle's `expanded(id)` signal or adopt your own via `collapsible.signals`; opt a behavior out only when the app must own it. Do not hand-roll the open handler, the focusout collapse, a clear-button mousedown guard, or reopening after controlled clear or Select All deletion — the helper keeps the replacement editor open and focused. Clear and deletion restoration finish at the replacement mutation checkpoint before the next input task, so shortcut-letter typing cannot escape to the page and delayed animation frames cannot overwrite a later selection. Persist both query and tokens returned by the DOM read. For real-app adoption it also provides a focusout keep-open exception (`data-token-search-keep-open` regions or `collapsible.keepOpenOn(target)`, so a sibling suggestions/date/help surface does not collapse an empty field), opt-in atomic-chip keyboard (`keyboard`: Backspace/Delete remove the adjacent chip via `onRemoveToken`, ArrowRight moves the caret past a trailing chip), and an `onEdit({id, editor, event})` input callback (the `InputEvent` lets you gate on `inputType`/`data`) — reach for these instead of re-adding hand-rolled keydown/input handlers around the field.
|
|
198
198
|
12. Demo work uses public production component subpaths and their browser-selected CSS. Focused demos and recipes have no local stylesheet, inline style, or styling-only class; use public components and `CatalogExample.viewport` for unavoidable specimen constraints (a recipe declares its frame through its exported `presentation`). When a recipe needs a capability no component offers, report the component gap instead of adding CSS. Use exactly three demo kinds: a `component` is one component with variants/states, a `composition` is a focused container of components commonly used together, and a `recipe` is the recommended production approach to a task. Organize catalog routes into product-important functional groups; within each group put all components before compositions, order each kind by importance with display-name alphabetical tie-breaking, and put recipes after every component/composition group. Show `Composition` as an entry tag instead of repeating the word in its name. Give every public visual component its own category-grouped catalog route; list themed third-party components under a clearly labeled collapsible ecosystem section, with a focused route for each. For the complete taxonomy, focused/composition decision, `CatalogExample` / `CatalogExampleStack` row/group nesting, exact specimen selection, geometry-overlay legend and exclusions, skip-marker behavior, and metadata ownership, follow the single [Catalog demo authoring contract](../docs/catalog.md#catalog-demo-authoring-contract), discovered machine-readably through [`catalog-authoring.json`](./catalog-authoring.json). Never infer structure from private `kui-catalog-*` classes. Project each component's deterministic repository-relative demo source and existing documentation path so the detail can expose `View demo source` and `Read guidance` links without a runtime export; also derive first-party component implementation paths from their canonical browser imports, and label Web Awesome documentation as Kerf integration guidance. Declare direct `uses` relationships so `Used by` stays derivable, and theme shell chrome through the same semantic tokens as the stage instead of drawing a substitute. Enable `wireCatalog({ revealSelection: true })` for long desktop sidebars so controlled selections remain visible without moving focus; keep its default compact media guard unless compact scrolling is an explicit product behavior, and use `revealCatalogEntry` for an initial deep link.
|
|
199
199
|
13. The Web Awesome theme makes Tooltip and Popover arrowless by default. Keep that default unless a pointer materially clarifies the anchor; opt back in with `--wa-tooltip-arrow-size`, `--kui-wa-popover-arrow-size`, or a popover's public `--arrow-size`, and use `without-arrow` when local no-arrow intent should survive theme changes. Its non-field chrome uses `--kui-wa-control-inset` (8px controls/items), `--kui-wa-surface-margin` / `--kui-wa-surface-inset` (8px around/inside Accordion, Card, Details, Callout, and Include), and `--kui-wa-container-inset` (16px Tab Panel content and framed disclosure regions). Plain Details use the 8px surface inset on the trigger and keep the body flush; plain Accordion items use it on both trigger and body. Outlined and sunken Details use the 16px container inset on the trigger and 8px on the body; framed Accordion items use 16px on both. Dialog maps its body to the 8px surface tier and its footer to the 16px container tier independently of `--spacing`. Override those tiers instead of restyling individual parts. Filled badges use each variant's quiet fill with normal text so their compact copy stays WCAG-AA readable; override the pair with `--kui-wa-badge-filled-background` / `--kui-wa-badge-filled-foreground`, not host selectors. When a dialog provides another dismissal affordance, put `hide-actions` on the `wa-dialog` host to hide its directly exported `header-actions` part; never chain `::part()` selectors through internal parts. Checkbox and Radio Group option regions, the Color Picker trigger, and Slider's complete interactive region receive the shared 8px logical inline outer inset because those controls have no bordered field shell. Known Date field captions and bordered text-like field hints align with values at the 9px border-plus-padding inset. OTP Input's label uses the same uppercase xs/650 treatment as other field labels, and both its label and hint use that 9px inset.
|
|
200
200
|
14. Treat the complete Web Awesome catalog as support coverage, not a recommendation list. Its UX sidebar marks superseded and exceptional entries `Discouraged`. Consider Popup when it replaces custom anchored positioning. Prefer Kerf `Select` over direct Dropdown/Dropdown Item/Select/Option composition, `SegmentedControl` over Button Group, `TabBar` or `SegmentedControl` over Web Awesome Tabs, `LucideIcon` over Web Awesome Icon, and `ResizableRegion` over Split Panel. Use Tree/Tree Item, Animated Image, and Comparison only for a specific required behavior; avoid Zoomable Frame. Never render a `Discouraged` element directly in application, demo, or recipe markup; reach it only through the Kerf component that wraps it (`PopupMenu` renders `wa-dropdown`, `Select` renders `wa-select` / `wa-option`), and treat a Kerf component gap as a request to file, not a reason to drop to the raw element.
|
|
@@ -250,6 +250,10 @@ Common mistakes:
|
|
|
250
250
|
| Keep chrome, a label, or a readout that aids no decision | Delete it; every element must help a person decide or act |
|
|
251
251
|
| Override a component's default size or color because it "looks off" | Trust the default (a LucideIcon is 24px) and fix the surrounding layout instead |
|
|
252
252
|
|
|
253
|
+
For prominent content text, use `Text size="large"`; use `size="xlarge"` for display copy. Both sizes work with semantic `variant`, `tone`, and `font`, so keep heading levels based on the document outline.
|
|
254
|
+
|
|
255
|
+
For nested action menus, use `PopupMenuItem.submenu` and its `checked`, `icon`, `details`, `disabledReason`, and `tone` options. The app owns choice state. For a right-click menu, use `PopupMenu` with `context: true` and call `openPopupMenuAt` from the app's `contextmenu` handler; the package positions and opens the menu. Call `closePopupMenu` for programmatic dismissal.
|
|
256
|
+
|
|
253
257
|
## Select lifecycle ownership
|
|
254
258
|
|
|
255
259
|
Import `@kerfjs/ui/select/register` once. It owns the canonical Select animation
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Web Awesome JSX signatures for the UI authoring corpus
|
|
2
2
|
|
|
3
|
-
Generated from the emitted `@kerfjs/ui@5.0.0-beta.
|
|
3
|
+
Generated from the emitted `@kerfjs/ui@5.0.0-beta.59` declaration boundary. Import `@kerfjs/ui/webawesome` for type effects when authoring direct `wa-*` JSX. The module emits no runtime behavior and does not register custom elements.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
6
|
import { KerfCustomElement } from 'kerfjs/jsx-runtime';
|
package/dist/catalog.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Workbench } from './chunk-2CCMJTVY.js';
|
|
2
2
|
|
|
3
|
-
import { PopupMenu } from './chunk-
|
|
3
|
+
import { PopupMenu } from './chunk-UWONG4QE.js';
|
|
4
4
|
|
|
5
5
|
import { Row } from './chunk-VZOFZPSO.js';
|
|
6
6
|
import { ListInsetText } from './chunk-DG6G5EPF.js';
|
|
@@ -11,8 +11,8 @@ import { ToolbarText } from './chunk-XDKAYSVD.js';
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
import { ListHeader } from './chunk-
|
|
15
|
-
import { Text } from './chunk-
|
|
14
|
+
import { ListHeader } from './chunk-UX2VVXZ3.js';
|
|
15
|
+
import { Text } from './chunk-UXT7TE6B.js';
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { signal } from 'kerfjs';
|
|
2
|
+
|
|
3
|
+
// src/token-search-model.ts
|
|
4
|
+
var escapePattern = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5
|
+
var quote = (text) => /[\s()"\\]/.test(text) ? `"${text.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"` : text;
|
|
6
|
+
var unquote = (text) => text.startsWith('"') && text.endsWith('"') ? text.slice(1, -1).replace(/\\(["\\])/g, "$1") : text;
|
|
7
|
+
function createTokenSearchModel({
|
|
8
|
+
rules = [],
|
|
9
|
+
initial = { query: "", tokens: [] },
|
|
10
|
+
evaluate,
|
|
11
|
+
onSubmit
|
|
12
|
+
} = {}) {
|
|
13
|
+
const byName = new Map(rules.map((rule) => [rule.name.toLowerCase(), rule]));
|
|
14
|
+
if (byName.size !== rules.length || rules.some((rule) => !/^[\w-]+$/.test(rule.name)))
|
|
15
|
+
throw new Error("Token search rule names must be unique words");
|
|
16
|
+
const names = [...byName.keys()].map(escapePattern).join("|");
|
|
17
|
+
const pattern = names ? new RegExp(
|
|
18
|
+
`(?:^|[\\s(])((?:${names}):(?:"(?:\\\\.|[^"])*"|[^"\\s()][^\\s()]*))(?=$|[\\s)])`,
|
|
19
|
+
"gi"
|
|
20
|
+
) : void 0;
|
|
21
|
+
const activePattern = names ? new RegExp(
|
|
22
|
+
`(?:^|[\\s(])((?:${names}):(?:"(?:\\\\.|[^"])*|[^\\s()]*))$`,
|
|
23
|
+
"i"
|
|
24
|
+
) : void 0;
|
|
25
|
+
const resolve = (raw, offset) => {
|
|
26
|
+
const separator = raw.indexOf(":");
|
|
27
|
+
const kind = raw.slice(0, separator).toLowerCase();
|
|
28
|
+
const rule = byName.get(kind);
|
|
29
|
+
if (!rule || separator < 0) return;
|
|
30
|
+
const input = unquote(raw.slice(separator + 1));
|
|
31
|
+
if (!input) return;
|
|
32
|
+
const parsedValue = rule.parse ? rule.parse(input) : input;
|
|
33
|
+
if (parsedValue === void 0) return;
|
|
34
|
+
return {
|
|
35
|
+
kind,
|
|
36
|
+
parsedValue,
|
|
37
|
+
value: `${kind}:${quote(input)}`,
|
|
38
|
+
label: rule.label?.(parsedValue, input) ?? `${kind}:${input}`,
|
|
39
|
+
offset
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
const rehydrate = (token) => {
|
|
43
|
+
const resolved = resolve(token.value, token.offset ?? 0);
|
|
44
|
+
return resolved ? { ...resolved, ...token } : void 0;
|
|
45
|
+
};
|
|
46
|
+
const state = signal({
|
|
47
|
+
query: initial.query,
|
|
48
|
+
tokens: initial.tokens.flatMap((token) => {
|
|
49
|
+
const resolved = rehydrate(token);
|
|
50
|
+
return resolved ? [resolved] : [];
|
|
51
|
+
})
|
|
52
|
+
});
|
|
53
|
+
const editorRevision = signal(0);
|
|
54
|
+
const suggestions = signal([]);
|
|
55
|
+
const result = signal(evaluate?.(state.value));
|
|
56
|
+
let activeStart;
|
|
57
|
+
const refreshSuggestions = () => {
|
|
58
|
+
const match = activePattern?.exec(state.value.query);
|
|
59
|
+
const raw = match?.[1];
|
|
60
|
+
const separator = raw?.indexOf(":") ?? -1;
|
|
61
|
+
const rule = separator < 0 ? void 0 : byName.get(raw.slice(0, separator).toLowerCase());
|
|
62
|
+
const input = separator < 0 ? "" : unquote(raw.slice(separator + 1));
|
|
63
|
+
activeStart = match && raw ? match.index + match[0].lastIndexOf(raw) : void 0;
|
|
64
|
+
suggestions.value = (rule?.suggest?.(input) ?? []).flatMap((candidate) => {
|
|
65
|
+
const value = typeof candidate === "string" ? candidate : candidate.value;
|
|
66
|
+
const token = resolve(`${rule.name}:${quote(value)}`, 0);
|
|
67
|
+
return token ? [
|
|
68
|
+
{
|
|
69
|
+
value: token.value,
|
|
70
|
+
label: typeof candidate === "string" ? token.label : candidate.label
|
|
71
|
+
}
|
|
72
|
+
] : [];
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
const publish = (next) => {
|
|
76
|
+
state.value = next;
|
|
77
|
+
result.value = evaluate?.(next);
|
|
78
|
+
refreshSuggestions();
|
|
79
|
+
};
|
|
80
|
+
refreshSuggestions();
|
|
81
|
+
const edit = (value, commit = false) => {
|
|
82
|
+
let query = "";
|
|
83
|
+
let cursor = 0;
|
|
84
|
+
const added = [];
|
|
85
|
+
const removed = [];
|
|
86
|
+
for (const match of value.query.matchAll(pattern ?? /(?!) /g)) {
|
|
87
|
+
const raw = match[1];
|
|
88
|
+
const start = match.index + match[0].lastIndexOf(raw);
|
|
89
|
+
const end = start + raw.length;
|
|
90
|
+
if (end === value.query.length && !commit && !/\s$/.test(value.query))
|
|
91
|
+
continue;
|
|
92
|
+
const token = resolve(raw, query.length + start - cursor);
|
|
93
|
+
if (!token) continue;
|
|
94
|
+
query += value.query.slice(cursor, start);
|
|
95
|
+
token.offset = query.length;
|
|
96
|
+
added.push(token);
|
|
97
|
+
removed.push({ start, end });
|
|
98
|
+
cursor = end;
|
|
99
|
+
}
|
|
100
|
+
const suffix = value.query.slice(cursor);
|
|
101
|
+
query += added.length && /^\s+$/.test(suffix) ? "" : suffix;
|
|
102
|
+
const existing = value.tokens.flatMap((token) => {
|
|
103
|
+
const resolved = rehydrate(token);
|
|
104
|
+
if (!resolved) return [];
|
|
105
|
+
const offset = token.offset ?? value.query.length;
|
|
106
|
+
return [
|
|
107
|
+
{
|
|
108
|
+
...resolved,
|
|
109
|
+
offset: offset - removed.reduce(
|
|
110
|
+
(total, range) => total + Math.max(0, Math.min(offset, range.end) - range.start),
|
|
111
|
+
0
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
];
|
|
115
|
+
});
|
|
116
|
+
const tokens = [...existing, ...added].sort(
|
|
117
|
+
(left, right) => (left.offset ?? 0) - (right.offset ?? 0)
|
|
118
|
+
);
|
|
119
|
+
publish({ query, tokens });
|
|
120
|
+
};
|
|
121
|
+
const choose = (value) => {
|
|
122
|
+
if (!suggestions.value.some((suggestion) => suggestion.value === value) || activeStart === void 0)
|
|
123
|
+
return;
|
|
124
|
+
const token = resolve(value, activeStart);
|
|
125
|
+
if (!token) return;
|
|
126
|
+
const query = state.value.query.slice(0, activeStart);
|
|
127
|
+
publish({ query, tokens: [...state.value.tokens, token] });
|
|
128
|
+
};
|
|
129
|
+
return {
|
|
130
|
+
state,
|
|
131
|
+
editorRevision,
|
|
132
|
+
suggestions,
|
|
133
|
+
result,
|
|
134
|
+
edit,
|
|
135
|
+
submit(value) {
|
|
136
|
+
edit(value, true);
|
|
137
|
+
onSubmit?.(state.value, result.value);
|
|
138
|
+
},
|
|
139
|
+
choose,
|
|
140
|
+
expandToken(value) {
|
|
141
|
+
const index = state.value.tokens.findIndex(
|
|
142
|
+
(token) => token.value === value
|
|
143
|
+
);
|
|
144
|
+
if (index < 0) return;
|
|
145
|
+
const offset = Math.max(
|
|
146
|
+
0,
|
|
147
|
+
Math.min(
|
|
148
|
+
state.value.query.length,
|
|
149
|
+
state.value.tokens[index].offset ?? state.value.query.length
|
|
150
|
+
)
|
|
151
|
+
);
|
|
152
|
+
const insertion = `${value} `;
|
|
153
|
+
publish({
|
|
154
|
+
query: `${state.value.query.slice(0, offset)}${insertion}${state.value.query.slice(offset)}`,
|
|
155
|
+
tokens: state.value.tokens.flatMap(
|
|
156
|
+
(token, at) => at === index ? [] : [
|
|
157
|
+
{
|
|
158
|
+
...token,
|
|
159
|
+
offset: (token.offset ?? 0) + (at > index ? insertion.length : 0)
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
)
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
remove(value) {
|
|
166
|
+
const index = state.value.tokens.findIndex(
|
|
167
|
+
(token) => token.value === value
|
|
168
|
+
);
|
|
169
|
+
if (index < 0) return;
|
|
170
|
+
publish({
|
|
171
|
+
...state.value,
|
|
172
|
+
tokens: state.value.tokens.filter((_, at) => at !== index)
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
clear() {
|
|
176
|
+
publish({ query: "", tokens: [] });
|
|
177
|
+
editorRevision.value++;
|
|
178
|
+
},
|
|
179
|
+
dismiss() {
|
|
180
|
+
suggestions.value = [];
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export { createTokenSearchModel };
|
|
186
|
+
|
|
187
|
+
//# sourceMappingURL=chunk-F7F7KRO2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/token-search-model.ts"],"names":[],"mappings":";;;AA0DA,IAAM,gBAAgB,CAAC,IAAA,KACrB,IAAA,CAAK,OAAA,CAAQ,uBAAuB,MAAM,CAAA;AAC5C,IAAM,QAAQ,CAAC,IAAA,KACb,WAAA,CAAY,IAAA,CAAK,IAAI,CAAA,GACjB,CAAA,CAAA,EAAI,IAAA,CAAK,UAAA,CAAW,MAAM,MAAM,CAAA,CAAE,WAAW,GAAA,EAAK,KAAK,CAAC,CAAA,CAAA,CAAA,GACxD,IAAA;AACN,IAAM,UAAU,CAAC,IAAA,KACf,KAAK,UAAA,CAAW,GAAG,KAAK,IAAA,CAAK,QAAA,CAAS,GAAG,CAAA,GACrC,IAAA,CAAK,MAAM,CAAA,EAAG,EAAE,EAAE,OAAA,CAAQ,YAAA,EAAc,IAAI,CAAA,GAC5C,IAAA;AAGC,SAAS,sBAAA,CAAyC;AAAA,EACvD,QAAQ,EAAC;AAAA,EACT,UAAU,EAAE,KAAA,EAAO,EAAA,EAAI,MAAA,EAAQ,EAAC,EAAE;AAAA,EAClC,QAAA;AAAA,EACA;AACF,CAAA,GAAqC,EAAC,EAA6B;AACjE,EAAA,MAAM,MAAA,GAAS,IAAI,GAAA,CAAI,KAAA,CAAM,IAAI,CAAC,IAAA,KAAS,CAAC,IAAA,CAAK,IAAA,CAAK,WAAA,EAAY,EAAG,IAAI,CAAC,CAAC,CAAA;AAC3E,EAAA,IACE,MAAA,CAAO,IAAA,KAAS,KAAA,CAAM,MAAA,IACtB,KAAA,CAAM,IAAA,CAAK,CAAC,IAAA,KAAS,CAAC,UAAA,CAAW,IAAA,CAAK,IAAA,CAAK,IAAI,CAAC,CAAA;AAEhD,IAAA,MAAM,IAAI,MAAM,8CAA8C,CAAA;AAChE,EAAA,MAAM,KAAA,GAAQ,CAAC,GAAG,MAAA,CAAO,IAAA,EAAM,CAAA,CAAE,GAAA,CAAI,aAAa,CAAA,CAAE,IAAA,CAAK,GAAG,CAAA;AAC5D,EAAA,MAAM,OAAA,GAAU,QACZ,IAAI,MAAA;AAAA,IACF,mBAAmB,KAAK,CAAA,uDAAA,CAAA;AAAA,IACxB;AAAA,GACF,GACA,MAAA;AACJ,EAAA,MAAM,aAAA,GAAgB,QAClB,IAAI,MAAA;AAAA,IACF,mBAAmB,KAAK,CAAA,kCAAA,CAAA;AAAA,IACxB;AAAA,GACF,GACA,MAAA;AAEJ,EAAA,MAAM,OAAA,GAAU,CACd,GAAA,EACA,MAAA,KACyC;AACzC,IAAA,MAAM,SAAA,GAAY,GAAA,CAAI,OAAA,CAAQ,GAAG,CAAA;AACjC,IAAA,MAAM,OAAO,GAAA,CAAI,KAAA,CAAM,CAAA,EAAG,SAAS,EAAE,WAAA,EAAY;AACjD,IAAA,MAAM,IAAA,GAAO,MAAA,CAAO,GAAA,CAAI,IAAI,CAAA;AAC5B,IAAA,IAAI,CAAC,IAAA,IAAQ,SAAA,GAAY,CAAA,EAAG;AAC5B,IAAA,MAAM,QAAQ,OAAA,CAAQ,GAAA,CAAI,KAAA,CAAM,SAAA,GAAY,CAAC,CAAC,CAAA;AAC9C,IAAA,IAAI,CAAC,KAAA,EAAO;AACZ,IAAA,MAAM,cAAc,IAAA,CAAK,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,KAAK,CAAA,GAAI,KAAA;AACrD,IAAA,IAAI,gBAAgB,MAAA,EAAW;AAC/B,IAAA,OAAO;AAAA,MACL,IAAA;AAAA,MACA,WAAA;AAAA,MACA,OAAO,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,KAAA,CAAM,KAAK,CAAC,CAAA,CAAA;AAAA,MAC9B,KAAA,EAAO,KAAK,KAAA,GAAQ,WAAA,EAAa,KAAK,CAAA,IAAK,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,KAAK,CAAA,CAAA;AAAA,MAC3D;AAAA,KACF;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,SAAA,GAAY,CAChB,KAAA,KACyC;AACzC,IAAA,MAAM,WAAW,OAAA,CAAQ,KAAA,CAAM,KAAA,EAAO,KAAA,CAAM,UAAU,CAAC,CAAA;AACvD,IAAA,OAAO,WAAW,EAAE,GAAG,QAAA,EAAU,GAAG,OAAM,GAAI,MAAA;AAAA,EAChD,CAAA;AAEA,EAAA,MAAM,QAAQ,MAAA,CAAyB;AAAA,IACrC,OAAO,OAAA,CAAQ,KAAA;AAAA,IACf,MAAA,EAAQ,OAAA,CAAQ,MAAA,CAAO,OAAA,CAAQ,CAAC,KAAA,KAAU;AACxC,MAAA,MAAM,QAAA,GAAW,UAAU,KAAK,CAAA;AAChC,MAAA,OAAO,QAAA,GAAW,CAAC,QAAQ,CAAA,GAAI,EAAC;AAAA,IAClC,CAAC;AAAA,GACF,CAAA;AACD,EAAA,MAAM,cAAA,GAAiB,OAAO,CAAC,CAAA;AAC/B,EAAA,MAAM,WAAA,GAAc,MAAA,CAAyC,EAAE,CAAA;AAC/D,EAAA,MAAM,MAAA,GAAS,MAAA,CAA2B,QAAA,GAAW,KAAA,CAAM,KAAK,CAAC,CAAA;AACjE,EAAA,IAAI,WAAA;AAEJ,EAAA,MAAM,qBAAqB,MAAM;AAC/B,IAAA,MAAM,KAAA,GAAQ,aAAA,EAAe,IAAA,CAAK,KAAA,CAAM,MAAM,KAAK,CAAA;AACnD,IAAA,MAAM,GAAA,GAAM,QAAQ,CAAC,CAAA;AACrB,IAAA,MAAM,SAAA,GAAY,GAAA,EAAK,OAAA,CAAQ,GAAG,CAAA,IAAK,EAAA;AACvC,IAAA,MAAM,IAAA,GACJ,SAAA,GAAY,CAAA,GACR,MAAA,GACA,MAAA,CAAO,GAAA,CAAI,GAAA,CAAK,KAAA,CAAM,CAAA,EAAG,SAAS,CAAA,CAAE,WAAA,EAAa,CAAA;AACvD,IAAA,MAAM,KAAA,GAAQ,YAAY,CAAA,GAAI,EAAA,GAAK,QAAQ,GAAA,CAAK,KAAA,CAAM,SAAA,GAAY,CAAC,CAAC,CAAA;AACpE,IAAA,WAAA,GACE,KAAA,IAAS,MAAM,KAAA,CAAM,KAAA,GAAQ,MAAM,CAAC,CAAA,CAAE,WAAA,CAAY,GAAG,CAAA,GAAI,MAAA;AAC3D,IAAA,WAAA,CAAY,KAAA,GAAA,CAAS,MAAM,OAAA,GAAU,KAAK,KAAK,EAAC,EAAG,OAAA,CAAQ,CAAC,SAAA,KAAc;AACxE,MAAA,MAAM,KAAA,GAAQ,OAAO,SAAA,KAAc,QAAA,GAAW,YAAY,SAAA,CAAU,KAAA;AACpE,MAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,CAAA,EAAG,IAAA,CAAM,IAAI,IAAI,KAAA,CAAM,KAAK,CAAC,CAAA,CAAA,EAAI,CAAC,CAAA;AACxD,MAAA,OAAO,KAAA,GACH;AAAA,QACE;AAAA,UACE,OAAO,KAAA,CAAM,KAAA;AAAA,UACb,OACE,OAAO,SAAA,KAAc,QAAA,GAAW,KAAA,CAAM,QAAQ,SAAA,CAAU;AAAA;AAC5D,UAEF,EAAC;AAAA,IACP,CAAC,CAAA;AAAA,EACH,CAAA;AACA,EAAA,MAAM,OAAA,GAAU,CAAC,IAAA,KAA2B;AAC1C,IAAA,KAAA,CAAM,KAAA,GAAQ,IAAA;AACd,IAAA,MAAA,CAAO,KAAA,GAAQ,WAAW,IAAI,CAAA;AAC9B,IAAA,kBAAA,EAAmB;AAAA,EACrB,CAAA;AACA,EAAA,kBAAA,EAAmB;AAEnB,EAAA,MAAM,IAAA,GAAO,CAAC,KAAA,EAA8B,MAAA,GAAS,KAAA,KAAU;AAC7D,IAAA,IAAI,KAAA,GAAQ,EAAA;AACZ,IAAA,IAAI,MAAA,GAAS,CAAA;AACb,IAAA,MAAM,QAAoC,EAAC;AAC3C,IAAA,MAAM,UAAiD,EAAC;AACxD,IAAA,KAAA,MAAW,SAAS,KAAA,CAAM,KAAA,CAAM,QAAA,CAAS,OAAA,IAAW,QAAQ,CAAA,EAAG;AAC7D,MAAA,MAAM,GAAA,GAAM,MAAM,CAAC,CAAA;AACnB,MAAA,MAAM,QAAQ,KAAA,CAAM,KAAA,GAAQ,MAAM,CAAC,CAAA,CAAE,YAAY,GAAG,CAAA;AACpD,MAAA,MAAM,GAAA,GAAM,QAAQ,GAAA,CAAI,MAAA;AACxB,MAAA,IAAI,GAAA,KAAQ,KAAA,CAAM,KAAA,CAAM,MAAA,IAAU,CAAC,UAAU,CAAC,KAAA,CAAM,IAAA,CAAK,KAAA,CAAM,KAAK,CAAA;AAClE,QAAA;AACF,MAAA,MAAM,QAAQ,OAAA,CAAQ,GAAA,EAAK,KAAA,CAAM,MAAA,GAAS,QAAQ,MAAM,CAAA;AACxD,MAAA,IAAI,CAAC,KAAA,EAAO;AACZ,MAAA,KAAA,IAAS,KAAA,CAAM,KAAA,CAAM,KAAA,CAAM,MAAA,EAAQ,KAAK,CAAA;AACxC,MAAA,KAAA,CAAM,SAAS,KAAA,CAAM,MAAA;AACrB,MAAA,KAAA,CAAM,KAAK,KAAK,CAAA;AAChB,MAAA,OAAA,CAAQ,IAAA,CAAK,EAAE,KAAA,EAAO,GAAA,EAAK,CAAA;AAC3B,MAAA,MAAA,GAAS,GAAA;AAAA,IACX;AACA,IAAA,MAAM,MAAA,GAAS,KAAA,CAAM,KAAA,CAAM,KAAA,CAAM,MAAM,CAAA;AACvC,IAAA,KAAA,IAAS,MAAM,MAAA,IAAU,OAAA,CAAQ,IAAA,CAAK,MAAM,IAAI,EAAA,GAAK,MAAA;AACrD,IAAA,MAAM,QAAA,GAAW,KAAA,CAAM,MAAA,CAAO,OAAA,CAAQ,CAAC,KAAA,KAAU;AAC/C,MAAA,MAAM,QAAA,GAAW,UAAU,KAAK,CAAA;AAChC,MAAA,IAAI,CAAC,QAAA,EAAU,OAAO,EAAC;AACvB,MAAA,MAAM,MAAA,GAAS,KAAA,CAAM,MAAA,IAAU,KAAA,CAAM,KAAA,CAAM,MAAA;AAC3C,MAAA,OAAO;AAAA,QACL;AAAA,UACE,GAAG,QAAA;AAAA,UACH,MAAA,EACE,SACA,OAAA,CAAQ,MAAA;AAAA,YACN,CAAC,KAAA,EAAO,KAAA,KACN,KAAA,GAAQ,KAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,MAAA,EAAQ,KAAA,CAAM,GAAG,CAAA,GAAI,MAAM,KAAK,CAAA;AAAA,YAC/D;AAAA;AACF;AACJ,OACF;AAAA,IACF,CAAC,CAAA;AACD,IAAA,MAAM,SAAS,CAAC,GAAG,QAAA,EAAU,GAAG,KAAK,CAAA,CAAE,IAAA;AAAA,MACrC,CAAC,IAAA,EAAM,KAAA,KAAA,CAAW,KAAK,MAAA,IAAU,CAAA,KAAM,MAAM,MAAA,IAAU,CAAA;AAAA,KACzD;AACA,IAAA,OAAA,CAAQ,EAAE,KAAA,EAAO,MAAA,EAAQ,CAAA;AAAA,EAC3B,CAAA;AAEA,EAAA,MAAM,MAAA,GAAS,CAAC,KAAA,KAAkB;AAChC,IAAA,IACE,CAAC,WAAA,CAAY,KAAA,CAAM,IAAA,CAAK,CAAC,eAAe,UAAA,CAAW,KAAA,KAAU,KAAK,CAAA,IAClE,WAAA,KAAgB,MAAA;AAEhB,MAAA;AACF,IAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,KAAA,EAAO,WAAW,CAAA;AACxC,IAAA,IAAI,CAAC,KAAA,EAAO;AACZ,IAAA,MAAM,QAAQ,KAAA,CAAM,KAAA,CAAM,KAAA,CAAM,KAAA,CAAM,GAAG,WAAW,CAAA;AACpD,IAAA,OAAA,CAAQ,EAAE,KAAA,EAAO,MAAA,EAAQ,CAAC,GAAG,MAAM,KAAA,CAAM,MAAA,EAAQ,KAAK,CAAA,EAAG,CAAA;AAAA,EAC3D,CAAA;AAEA,EAAA,OAAO;AAAA,IACL,KAAA;AAAA,IACA,cAAA;AAAA,IACA,WAAA;AAAA,IACA,MAAA;AAAA,IACA,IAAA;AAAA,IACA,OAAO,KAAA,EAAO;AACZ,MAAA,IAAA,CAAK,OAAO,IAAI,CAAA;AAChB,MAAA,QAAA,GAAW,KAAA,CAAM,KAAA,EAAO,MAAA,CAAO,KAAK,CAAA;AAAA,IACtC,CAAA;AAAA,IACA,MAAA;AAAA,IACA,YAAY,KAAA,EAAO;AACjB,MAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,KAAA,CAAM,MAAA,CAAO,SAAA;AAAA,QAC/B,CAAC,KAAA,KAAU,KAAA,CAAM,KAAA,KAAU;AAAA,OAC7B;AACA,MAAA,IAAI,QAAQ,CAAA,EAAG;AACf,MAAA,MAAM,SAAS,IAAA,CAAK,GAAA;AAAA,QAClB,CAAA;AAAA,QACA,IAAA,CAAK,GAAA;AAAA,UACH,KAAA,CAAM,MAAM,KAAA,CAAM,MAAA;AAAA,UAClB,KAAA,CAAM,MAAM,MAAA,CAAO,KAAK,EAAE,MAAA,IAAU,KAAA,CAAM,MAAM,KAAA,CAAM;AAAA;AACxD,OACF;AACA,MAAA,MAAM,SAAA,GAAY,GAAG,KAAK,CAAA,CAAA,CAAA;AAC1B,MAAA,OAAA,CAAQ;AAAA,QACN,OAAO,CAAA,EAAG,KAAA,CAAM,KAAA,CAAM,KAAA,CAAM,MAAM,CAAA,EAAG,MAAM,CAAC,CAAA,EAAG,SAAS,CAAA,EAAG,KAAA,CAAM,MAAM,KAAA,CAAM,KAAA,CAAM,MAAM,CAAC,CAAA,CAAA;AAAA,QAC1F,MAAA,EAAQ,KAAA,CAAM,KAAA,CAAM,MAAA,CAAO,OAAA;AAAA,UAAQ,CAAC,KAAA,EAAO,EAAA,KACzC,EAAA,KAAO,KAAA,GACH,EAAC,GACD;AAAA,YACE;AAAA,cACE,GAAG,KAAA;AAAA,cACH,SACG,KAAA,CAAM,MAAA,IAAU,MAAM,EAAA,GAAK,KAAA,GAAQ,UAAU,MAAA,GAAS,CAAA;AAAA;AAC3D;AACF;AACN,OACD,CAAA;AAAA,IACH,CAAA;AAAA,IACA,OAAO,KAAA,EAAO;AACZ,MAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,KAAA,CAAM,MAAA,CAAO,SAAA;AAAA,QAC/B,CAAC,KAAA,KAAU,KAAA,CAAM,KAAA,KAAU;AAAA,OAC7B;AACA,MAAA,IAAI,QAAQ,CAAA,EAAG;AACf,MAAA,OAAA,CAAQ;AAAA,QACN,GAAG,KAAA,CAAM,KAAA;AAAA,QACT,MAAA,EAAQ,MAAM,KAAA,CAAM,MAAA,CAAO,OAAO,CAAC,CAAA,EAAG,EAAA,KAAO,EAAA,KAAO,KAAK;AAAA,OAC1D,CAAA;AAAA,IACH,CAAA;AAAA,IACA,KAAA,GAAQ;AACN,MAAA,OAAA,CAAQ,EAAE,KAAA,EAAO,EAAA,EAAI,MAAA,EAAQ,IAAI,CAAA;AACjC,MAAA,cAAA,CAAe,KAAA,EAAA;AAAA,IACjB,CAAA;AAAA,IACA,OAAA,GAAU;AACR,MAAA,WAAA,CAAY,QAAQ,EAAC;AAAA,IACvB;AAAA,GACF;AACF","file":"chunk-F7F7KRO2.js","sourcesContent":["import { type Signal, signal } from 'kerfjs';\n\nimport type {\n TokenSearchFieldValue,\n TokenSearchToken,\n} from './token-search-field.js';\n\nexport interface TokenSearchRule {\n /** The word before `:` (for example `tag` or `status`). */\n name: string;\n /** Return a canonical value, or undefined to leave invalid input as text. */\n parse?: (input: string) => string | undefined;\n /** Label for a committed chip. Defaults to `name:input`. */\n label?: (value: string, input: string) => string;\n /** Suggestions for the unfinished value after `name:`. */\n suggest?: (\n input: string,\n ) => readonly (string | { value: string; label: string })[];\n}\n\nexport interface TokenSearchResolvedToken extends TokenSearchToken {\n kind: string;\n parsedValue: string;\n}\n\nexport interface TokenSearchState {\n query: string;\n tokens: TokenSearchResolvedToken[];\n}\n\nexport interface TokenSearchSuggestion {\n value: string;\n label: string;\n}\n\nexport interface TokenSearchModel<Result = unknown> {\n state: Signal<TokenSearchState>;\n /** Changes when a model action must replace DOM-owned editor text. */\n editorRevision: Signal<number>;\n suggestions: Signal<readonly TokenSearchSuggestion[]>;\n result: Signal<Result | undefined>;\n edit(value: TokenSearchFieldValue, commit?: boolean): void;\n submit(value: TokenSearchFieldValue): void;\n choose(value: string): void;\n expandToken(value: string): void;\n remove(value: string): void;\n clear(): void;\n dismiss(): void;\n}\n\nexport interface TokenSearchModelOptions<Result> {\n rules?: readonly TokenSearchRule[];\n initial?: TokenSearchFieldValue;\n /** Runs after every state change, including initial state. */\n evaluate?: (state: TokenSearchState) => Result;\n onSubmit?: (state: TokenSearchState, result: Result | undefined) => void;\n}\n\nconst escapePattern = (text: string) =>\n text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\nconst quote = (text: string) =>\n /[\\s()\"\\\\]/.test(text)\n ? `\"${text.replaceAll('\\\\', '\\\\\\\\').replaceAll('\"', '\\\\\"')}\"`\n : text;\nconst unquote = (text: string) =>\n text.startsWith('\"') && text.endsWith('\"')\n ? text.slice(1, -1).replace(/\\\\([\"\\\\])/g, '$1')\n : text;\n\n/** Opt-in state, token parsing, suggestions, and evaluation for TokenSearchField. */\nexport function createTokenSearchModel<Result = unknown>({\n rules = [],\n initial = { query: '', tokens: [] },\n evaluate,\n onSubmit,\n}: TokenSearchModelOptions<Result> = {}): TokenSearchModel<Result> {\n const byName = new Map(rules.map((rule) => [rule.name.toLowerCase(), rule]));\n if (\n byName.size !== rules.length ||\n rules.some((rule) => !/^[\\w-]+$/.test(rule.name))\n )\n throw new Error('Token search rule names must be unique words');\n const names = [...byName.keys()].map(escapePattern).join('|');\n const pattern = names\n ? new RegExp(\n `(?:^|[\\\\s(])((?:${names}):(?:\"(?:\\\\\\\\.|[^\"])*\"|[^\"\\\\s()][^\\\\s()]*))(?=$|[\\\\s)])`,\n 'gi',\n )\n : undefined;\n const activePattern = names\n ? new RegExp(\n `(?:^|[\\\\s(])((?:${names}):(?:\"(?:\\\\\\\\.|[^\"])*|[^\\\\s()]*))$`,\n 'i',\n )\n : undefined;\n\n const resolve = (\n raw: string,\n offset: number,\n ): TokenSearchResolvedToken | undefined => {\n const separator = raw.indexOf(':');\n const kind = raw.slice(0, separator).toLowerCase();\n const rule = byName.get(kind);\n if (!rule || separator < 0) return;\n const input = unquote(raw.slice(separator + 1));\n if (!input) return;\n const parsedValue = rule.parse ? rule.parse(input) : input;\n if (parsedValue === undefined) return;\n return {\n kind,\n parsedValue,\n value: `${kind}:${quote(input)}`,\n label: rule.label?.(parsedValue, input) ?? `${kind}:${input}`,\n offset,\n };\n };\n\n const rehydrate = (\n token: TokenSearchToken,\n ): TokenSearchResolvedToken | undefined => {\n const resolved = resolve(token.value, token.offset ?? 0);\n return resolved ? { ...resolved, ...token } : undefined;\n };\n\n const state = signal<TokenSearchState>({\n query: initial.query,\n tokens: initial.tokens.flatMap((token) => {\n const resolved = rehydrate(token);\n return resolved ? [resolved] : [];\n }),\n });\n const editorRevision = signal(0);\n const suggestions = signal<readonly TokenSearchSuggestion[]>([]);\n const result = signal<Result | undefined>(evaluate?.(state.value));\n let activeStart: number | undefined;\n\n const refreshSuggestions = () => {\n const match = activePattern?.exec(state.value.query);\n const raw = match?.[1];\n const separator = raw?.indexOf(':') ?? -1;\n const rule =\n separator < 0\n ? undefined\n : byName.get(raw!.slice(0, separator).toLowerCase());\n const input = separator < 0 ? '' : unquote(raw!.slice(separator + 1));\n activeStart =\n match && raw ? match.index + match[0].lastIndexOf(raw) : undefined;\n suggestions.value = (rule?.suggest?.(input) ?? []).flatMap((candidate) => {\n const value = typeof candidate === 'string' ? candidate : candidate.value;\n const token = resolve(`${rule!.name}:${quote(value)}`, 0);\n return token\n ? [\n {\n value: token.value,\n label:\n typeof candidate === 'string' ? token.label : candidate.label,\n },\n ]\n : [];\n });\n };\n const publish = (next: TokenSearchState) => {\n state.value = next;\n result.value = evaluate?.(next);\n refreshSuggestions();\n };\n refreshSuggestions();\n\n const edit = (value: TokenSearchFieldValue, commit = false) => {\n let query = '';\n let cursor = 0;\n const added: TokenSearchResolvedToken[] = [];\n const removed: Array<{ start: number; end: number }> = [];\n for (const match of value.query.matchAll(pattern ?? /(?!) /g)) {\n const raw = match[1];\n const start = match.index + match[0].lastIndexOf(raw);\n const end = start + raw.length;\n if (end === value.query.length && !commit && !/\\s$/.test(value.query))\n continue;\n const token = resolve(raw, query.length + start - cursor);\n if (!token) continue;\n query += value.query.slice(cursor, start);\n token.offset = query.length;\n added.push(token);\n removed.push({ start, end });\n cursor = end;\n }\n const suffix = value.query.slice(cursor);\n query += added.length && /^\\s+$/.test(suffix) ? '' : suffix;\n const existing = value.tokens.flatMap((token) => {\n const resolved = rehydrate(token);\n if (!resolved) return [];\n const offset = token.offset ?? value.query.length;\n return [\n {\n ...resolved,\n offset:\n offset -\n removed.reduce(\n (total, range) =>\n total + Math.max(0, Math.min(offset, range.end) - range.start),\n 0,\n ),\n },\n ];\n });\n const tokens = [...existing, ...added].sort(\n (left, right) => (left.offset ?? 0) - (right.offset ?? 0),\n );\n publish({ query, tokens });\n };\n\n const choose = (value: string) => {\n if (\n !suggestions.value.some((suggestion) => suggestion.value === value) ||\n activeStart === undefined\n )\n return;\n const token = resolve(value, activeStart);\n if (!token) return;\n const query = state.value.query.slice(0, activeStart);\n publish({ query, tokens: [...state.value.tokens, token] });\n };\n\n return {\n state,\n editorRevision,\n suggestions,\n result,\n edit,\n submit(value) {\n edit(value, true);\n onSubmit?.(state.value, result.value);\n },\n choose,\n expandToken(value) {\n const index = state.value.tokens.findIndex(\n (token) => token.value === value,\n );\n if (index < 0) return;\n const offset = Math.max(\n 0,\n Math.min(\n state.value.query.length,\n state.value.tokens[index].offset ?? state.value.query.length,\n ),\n );\n const insertion = `${value} `;\n publish({\n query: `${state.value.query.slice(0, offset)}${insertion}${state.value.query.slice(offset)}`,\n tokens: state.value.tokens.flatMap((token, at) =>\n at === index\n ? []\n : [\n {\n ...token,\n offset:\n (token.offset ?? 0) + (at > index ? insertion.length : 0),\n },\n ],\n ),\n });\n },\n remove(value) {\n const index = state.value.tokens.findIndex(\n (token) => token.value === value,\n );\n if (index < 0) return;\n publish({\n ...state.value,\n tokens: state.value.tokens.filter((_, at) => at !== index),\n });\n },\n clear() {\n publish({ query: '', tokens: [] });\n editorRevision.value++;\n },\n dismiss() {\n suggestions.value = [];\n },\n };\n}\n"]}
|
|
@@ -28,6 +28,7 @@ function TokenSearchField({
|
|
|
28
28
|
label,
|
|
29
29
|
query = "",
|
|
30
30
|
tokens = [],
|
|
31
|
+
model,
|
|
31
32
|
revision,
|
|
32
33
|
placeholder = "Search",
|
|
33
34
|
tokenPlaceholder = "Add search\u2026",
|
|
@@ -49,9 +50,14 @@ function TokenSearchField({
|
|
|
49
50
|
editorAttributes = {},
|
|
50
51
|
slot
|
|
51
52
|
}) {
|
|
53
|
+
if (model) {
|
|
54
|
+
query = model.state.value.query;
|
|
55
|
+
tokens = model.state.value.tokens;
|
|
56
|
+
}
|
|
57
|
+
const suggestions = model?.suggestions.value ?? [];
|
|
52
58
|
const parts = orderedParts(query, tokens);
|
|
53
59
|
const tokenKey = tokens.map((token) => token.value).join("|");
|
|
54
|
-
const key = revision === void 0 ? `${id}:${tokenKey}` : `${id}@${String(revision)}:${tokenKey}`;
|
|
60
|
+
const key = model ? `${id}@${String(revision ?? "")}:${model.editorRevision.value}:${tokenKey}` : revision === void 0 ? `${id}:${tokenKey}` : `${id}@${String(revision)}:${tokenKey}`;
|
|
55
61
|
const resolvedExpanded = !collapsible || expanded || query.length > 0 || tokens.length > 0;
|
|
56
62
|
return /* @__PURE__ */ jsx(
|
|
57
63
|
"div",
|
|
@@ -152,7 +158,23 @@ function TokenSearchField({
|
|
|
152
158
|
children: clearIcon ?? /* @__PURE__ */ jsx(CloseIcon, {})
|
|
153
159
|
}
|
|
154
160
|
),
|
|
155
|
-
trailing && /* @__PURE__ */ jsx("span", { class: "kui-token-search__trailing", children: trailing })
|
|
161
|
+
trailing && /* @__PURE__ */ jsx("span", { class: "kui-token-search__trailing", children: trailing }),
|
|
162
|
+
!disabled && suggestions.length > 0 && /* @__PURE__ */ jsx(
|
|
163
|
+
"div",
|
|
164
|
+
{
|
|
165
|
+
class: "kui-token-search__suggestions",
|
|
166
|
+
"data-token-search-keep-open": true,
|
|
167
|
+
children: suggestions.map((suggestion) => /* @__PURE__ */ jsx(
|
|
168
|
+
"button",
|
|
169
|
+
{
|
|
170
|
+
type: "button",
|
|
171
|
+
class: "kui-token-search__suggestion",
|
|
172
|
+
"data-token-search-suggestion": suggestion.value,
|
|
173
|
+
children: suggestion.label
|
|
174
|
+
}
|
|
175
|
+
))
|
|
176
|
+
}
|
|
177
|
+
)
|
|
156
178
|
] })
|
|
157
179
|
}
|
|
158
180
|
);
|
|
@@ -222,4 +244,4 @@ function placeTokenSearchCaret(editor, offset) {
|
|
|
222
244
|
|
|
223
245
|
export { TokenSearchField, placeTokenSearchCaret, readTokenSearchField };
|
|
224
246
|
|
|
225
|
-
//# sourceMappingURL=chunk-
|
|
247
|
+
//# sourceMappingURL=chunk-RRB26JRJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/token-search-field.tsx"],"names":[],"mappings":"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","file":"chunk-RRB26JRJ.js","sourcesContent":["import type { SafeHtml } from 'kerfjs';\nimport { Search, X } from 'lucide';\n\nimport { LucideIcon } from './lucide-icon.js';\nimport type { KerfUiContent } from './semantic-content.js';\nimport type { TokenSearchModel } from './token-search-model.js';\n\nexport interface TokenSearchToken {\n value: string;\n label: string;\n offset?: number;\n accessibleLabel?: string;\n}\n\nexport type TokenSearchEditorAttributes = Readonly<\n Record<`data-${string}`, string | undefined> & {\n 'data-component'?: never;\n 'data-key'?: never;\n 'data-morph-skip'?: never;\n 'data-token-search-editor'?: never;\n 'data-token-count'?: never;\n 'data-placeholder'?: never;\n }\n>;\n\ninterface TokenSearchFieldBaseProps {\n id: string;\n label: string;\n query?: string;\n tokens?: readonly TokenSearchToken[];\n /** Opt-in grammar, suggestion, and evaluation state. Overrides `query` and `tokens`. */\n model?: TokenSearchModel;\n /**\n * Editor identity for a programmatic text replacement. The editor's text is\n * DOM-owned between token changes, so a new `query` with the same tokens\n * does not re-render it (that would reset the caret on every keystroke).\n * Change `revision` when the app replaces the text itself — reseeding a\n * persistent dialog's field on reopen, applying a saved search — and the\n * editor is rebuilt from `query` and `tokens`.\n */\n revision?: string | number;\n placeholder?: string;\n tokenPlaceholder?: string;\n disabled?: boolean;\n autofocus?: boolean;\n leading?: KerfUiContent;\n trailing?: KerfUiContent;\n /** Standalone field chrome or the inset visual layer of a configured toolbar group. */\n presentation?: 'standalone' | 'toolbar-group';\n editAction?: string;\n removeAction?: string;\n clearAction?: string;\n clearLabel?: string;\n /** Icon for the clear button. Default: the Lucide `x` glyph token chips also use. */\n clearIcon?: SafeHtml;\n className?: string;\n editorAttributes?: TokenSearchEditorAttributes;\n /** Native named-slot assignment when composed inside a web component. */\n slot?: string;\n}\n\ntype TokenSearchCollapsibleProps =\n | {\n /** Allow an empty field to render as one iconic action. */\n collapsible: true;\n /** Keep an empty collapsible field open while the application owns focus. */\n expanded?: boolean;\n expandAction?: string;\n expandLabel?: string;\n }\n | {\n collapsible?: false;\n expanded?: never;\n expandAction?: never;\n expandLabel?: never;\n };\n\nexport type TokenSearchFieldProps = TokenSearchFieldBaseProps &\n TokenSearchCollapsibleProps;\n\nexport interface TokenSearchFieldValue {\n query: string;\n tokens: TokenSearchToken[];\n}\n\ntype TokenSearchPart =\n { kind: 'text'; value: string } | { kind: 'token'; token: TokenSearchToken };\n\nfunction orderedParts(\n query: string,\n tokens: readonly TokenSearchToken[],\n): TokenSearchPart[] {\n const ordered = tokens\n .map((token, index) => ({\n token,\n index,\n offset: Math.max(0, Math.min(query.length, token.offset ?? query.length)),\n }))\n .sort(\n (left, right) => left.offset - right.offset || left.index - right.index,\n );\n const parts: TokenSearchPart[] = [];\n let cursor = 0;\n for (const { token, offset } of ordered) {\n parts.push({ kind: 'text', value: query.slice(cursor, offset) });\n parts.push({ kind: 'token', token: { ...token, offset } });\n cursor = offset;\n }\n parts.push({ kind: 'text', value: query.slice(cursor) });\n return parts;\n}\n\nfunction CloseIcon() {\n return <LucideIcon icon={X} name=\"x\" />;\n}\n\nexport function TokenSearchField({\n id,\n label,\n query = '',\n tokens = [],\n model,\n revision,\n placeholder = 'Search',\n tokenPlaceholder = 'Add search…',\n disabled = false,\n autofocus = false,\n collapsible = false,\n expanded = false,\n expandAction = 'expand-token-search',\n expandLabel,\n leading,\n trailing,\n presentation = 'standalone',\n editAction = 'edit-search-token',\n removeAction = 'remove-search-token',\n clearAction = 'clear-token-search',\n clearLabel = 'Clear search',\n clearIcon,\n className = '',\n editorAttributes = {},\n slot,\n}: TokenSearchFieldProps) {\n if (model) {\n query = model.state.value.query;\n tokens = model.state.value.tokens;\n }\n const suggestions = model?.suggestions.value ?? [];\n const parts = orderedParts(query, tokens);\n const tokenKey = tokens.map((token) => token.value).join('|');\n const key = model\n ? `${id}@${String(revision ?? '')}:${model.editorRevision.value}:${tokenKey}`\n : revision === undefined\n ? `${id}:${tokenKey}`\n : `${id}@${String(revision)}:${tokenKey}`;\n const resolvedExpanded =\n !collapsible || expanded || query.length > 0 || tokens.length > 0;\n return (\n <div\n class={`kui-token-search ${className}`.trim()}\n data-component=\"token-search-field\"\n data-token-search-id={id}\n data-disabled={String(disabled)}\n data-collapsible={String(collapsible)}\n data-expanded={String(resolvedExpanded)}\n data-has-trailing={String(Boolean(trailing))}\n data-presentation={presentation}\n slot={slot}\n >\n {!resolvedExpanded ? (\n <button\n type=\"button\"\n class=\"kui-token-search__expand\"\n data-action={expandAction}\n aria-label={expandLabel ?? label}\n title={expandLabel ?? label}\n disabled={disabled}\n >\n <LucideIcon icon={Search} name=\"search\" />\n </button>\n ) : (\n <>\n <span class=\"kui-token-search__leading\" aria-hidden=\"true\">\n {leading ?? <LucideIcon icon={Search} name=\"search\" />}\n </span>\n <div\n {...editorAttributes}\n class=\"kui-token-search__editor\"\n data-key={key}\n data-morph-skip\n data-token-search-editor={id}\n data-token-count={tokens.length}\n data-placeholder={tokens.length ? tokenPlaceholder : placeholder}\n role=\"searchbox\"\n aria-label={label}\n aria-disabled={disabled ? 'true' : undefined}\n contenteditable={disabled ? 'false' : 'true'}\n spellcheck=\"false\"\n autofocus={autofocus}\n >\n {parts.map((part) =>\n part.kind === 'text' ? (\n <span\n data-token-search-text\n data-empty={String(part.value.length === 0)}\n >\n {part.value || (tokens.length ? '\\u200b' : '')}\n </span>\n ) : (\n <span\n class=\"kui-token-search__token\"\n contenteditable=\"false\"\n data-component=\"token-search-token\"\n data-token-value={part.token.value}\n >\n <button\n type=\"button\"\n class=\"kui-token-search__token-edit\"\n data-action={editAction}\n data-token-value={part.token.value}\n aria-label={`Edit ${part.token.accessibleLabel ?? part.token.label}`}\n disabled={disabled}\n >\n {part.token.label}\n </button>\n <button\n type=\"button\"\n class=\"kui-token-search__token-remove\"\n data-action={removeAction}\n data-token-value={part.token.value}\n aria-label={`Remove ${part.token.accessibleLabel ?? part.token.label}`}\n disabled={disabled}\n >\n <CloseIcon />\n </button>\n </span>\n ),\n )}\n </div>\n {(query || tokens.length > 0) && (\n <button\n type=\"button\"\n class=\"kui-token-search__clear\"\n data-action={clearAction}\n aria-label={clearLabel}\n title={clearLabel}\n disabled={disabled}\n >\n {clearIcon ?? <CloseIcon />}\n </button>\n )}\n {trailing && (\n <span class=\"kui-token-search__trailing\">{trailing}</span>\n )}\n {!disabled && suggestions.length > 0 && (\n <div\n class=\"kui-token-search__suggestions\"\n data-token-search-keep-open\n >\n {suggestions.map((suggestion) => (\n <button\n type=\"button\"\n class=\"kui-token-search__suggestion\"\n data-token-search-suggestion={suggestion.value}\n >\n {suggestion.label}\n </button>\n ))}\n </div>\n )}\n </>\n )}\n </div>\n );\n}\n\n/** Read editable text and ordered token offsets from a rendered TokenSearchField editor. */\nexport function readTokenSearchField(\n editor: HTMLElement,\n knownTokens: readonly TokenSearchToken[] = [],\n): TokenSearchFieldValue {\n let query = '';\n const tokens: TokenSearchToken[] = [];\n const visit = (node: Node): void => {\n if (node.nodeType === Node.TEXT_NODE) {\n query += (node.textContent ?? '')\n .replaceAll('\\u00a0', ' ')\n .replaceAll('\\u200b', '');\n return;\n }\n if (!(node instanceof HTMLElement)) return;\n if (node.matches('[data-component=\"token-search-token\"]')) {\n const value = node.dataset.tokenValue;\n const known = knownTokens.find((token) => token.value === value);\n if (known) tokens.push({ ...known, offset: query.length });\n else if (value)\n tokens.push({ value, label: value, offset: query.length });\n return;\n }\n if (node.tagName === 'BR') {\n query += ' ';\n return;\n }\n for (const child of node.childNodes) visit(child);\n };\n for (const child of editor.childNodes) visit(child);\n return { query, tokens };\n}\n\n/** Focus an editor and place its caret at a text offset, skipping atomic token chips. */\nexport function placeTokenSearchCaret(\n editor: HTMLElement,\n offset?: number,\n): void {\n editor.focus({ preventScroll: true });\n const selection = globalThis.getSelection();\n if (!selection) return;\n const range = document.createRange();\n if (offset === undefined) {\n range.selectNodeContents(editor);\n range.collapse(false);\n } else {\n let remaining = Math.max(0, offset);\n let found = false;\n const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT, {\n acceptNode: (node) =>\n node.parentElement?.closest('[data-component=\"token-search-token\"]')\n ? NodeFilter.FILTER_REJECT\n : NodeFilter.FILTER_ACCEPT,\n });\n for (let node = walker.nextNode(); node; node = walker.nextNode()) {\n const source = node.textContent ?? '';\n const length = source.replaceAll('\\u200b', '').length;\n if (remaining <= length) {\n range.setStart(\n node,\n source === '\\u200b' ? 0 : Math.min(remaining, source.length),\n );\n range.collapse(true);\n found = true;\n break;\n }\n remaining -= length;\n }\n if (!found) {\n range.selectNodeContents(editor);\n range.collapse(false);\n }\n }\n selection.removeAllRanges();\n selection.addRange(range);\n}\n"]}
|