@kerfjs/ui 5.0.0-beta.68 → 5.0.0-beta.69
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 +3 -0
- package/ai/component-catalog-extension.schema.json +12 -0
- package/ai/component-catalog.json +14 -4
- package/ai/component-catalog.schema.json +14 -0
- package/ai/component-composition-rules.json +2 -2
- package/ai/component-composition.d.ts +2 -0
- package/ai/component-composition.json +2 -2
- package/ai/component-composition.schema.json +23 -0
- package/ai/components/loading-spinner.md +1 -0
- package/ai/components/tab-bar.md +5 -4
- package/ai/components/toolbar-control-group.md +2 -2
- package/ai/public-api-signatures-v1.md +34 -14
- package/ai/skill.md +18 -3
- package/ai/webawesome-jsx-signatures-v1.md +1 -1
- package/analyzer/index.d.mts +1 -0
- package/analyzer/index.mjs +108 -12
- package/dist/catalog.js +4 -4
- package/dist/catalog.js.map +1 -1
- package/dist/{chunk-P4UXSYIJ.js → chunk-2ZOLX3IE.js} +2 -2
- package/dist/{chunk-P4UXSYIJ.js.map → chunk-2ZOLX3IE.js.map} +1 -1
- package/dist/{chunk-E36EVCKG.js → chunk-47CKKLBE.js} +2 -2
- package/dist/{chunk-E36EVCKG.js.map → chunk-47CKKLBE.js.map} +1 -1
- package/dist/{chunk-JCV4GXWB.js → chunk-6DFDTWGG.js} +100 -61
- package/dist/chunk-6DFDTWGG.js.map +1 -0
- package/dist/{chunk-IAOAZUOO.js → chunk-7OKX3EF6.js} +2 -2
- package/dist/{chunk-IAOAZUOO.js.map → chunk-7OKX3EF6.js.map} +1 -1
- package/dist/{chunk-C74LITXE.js → chunk-B3XWUX3A.js} +2 -2
- package/dist/{chunk-C74LITXE.js.map → chunk-B3XWUX3A.js.map} +1 -1
- package/dist/{chunk-F7OCOBTG.js → chunk-CRVDF4A6.js} +4 -1
- package/dist/chunk-CRVDF4A6.js.map +1 -0
- package/dist/{chunk-TGM424PI.js → chunk-H7IODXGY.js} +3 -3
- package/dist/{chunk-TGM424PI.js.map → chunk-H7IODXGY.js.map} +1 -1
- package/dist/chunk-KVBRMEPM.js +44 -0
- package/dist/chunk-KVBRMEPM.js.map +1 -0
- package/dist/{chunk-EN4LDR5Y.js → chunk-NC4CFOID.js} +2 -2
- package/dist/{chunk-EN4LDR5Y.js.map → chunk-NC4CFOID.js.map} +1 -1
- package/dist/{chunk-5SYE4CCT.js → chunk-PQZPVBP4.js} +25 -18
- package/dist/chunk-PQZPVBP4.js.map +1 -0
- package/dist/{chunk-NXWE5S57.js → chunk-QJ5VFCW3.js} +12 -3
- package/dist/chunk-QJ5VFCW3.js.map +1 -0
- package/dist/{chunk-RKTTF3L4.js → chunk-X3TPECWG.js} +1 -1
- package/dist/chunk-X3TPECWG.js.map +1 -0
- package/dist/{chunk-VY6YRCTV.js → chunk-X3X4KRDK.js} +2 -2
- package/dist/{chunk-VY6YRCTV.js.map → chunk-X3X4KRDK.js.map} +1 -1
- package/dist/collapsible-panel.js +2 -2
- package/dist/empty-state.js +2 -1
- package/dist/index.js +10 -10
- package/dist/list-action-row.js +1 -1
- package/dist/list-item.js +1 -1
- package/dist/loading-spinner.d.ts +7 -2
- package/dist/loading-spinner.js +2 -1
- package/dist/nav-stack.js +1 -1
- package/dist/select.d.ts +4 -2
- package/dist/select.js +1 -1
- package/dist/split-view.js +1 -1
- package/dist/styles/app-tab.css +18 -1
- package/dist/styles/loading-spinner.css +4 -0
- package/dist/styles/select.css +79 -0
- package/dist/styles/tab-bar.css +12 -8
- package/dist/styles/token-search-field.css +19 -10
- package/dist/styles/toolbar-control-group.css +96 -3
- package/dist/tab-bar.d.ts +2 -0
- package/dist/tab-bar.js +1 -1
- package/dist/token-search-field.js +1 -1
- package/dist/toolbar-control-group.d.ts +13 -5
- package/dist/toolbar-control-group.js +2 -1
- package/dist/wire-catalog.js +1 -1
- package/dist/wire-scroll-dividers.d.ts +7 -4
- package/dist/wire-scroll-dividers.js +1 -1
- package/dist/wire-token-search-fields.js +1 -1
- package/dist/workbench.js +1 -1
- package/docs/accessibility.md +10 -0
- package/docs/catalog.md +3 -0
- package/docs/component-contract.md +58 -9
- package/docs/component-selection.md +22 -6
- package/docs/examples/component-catalog-extension.json +1 -0
- package/docs/layout.md +6 -0
- package/docs/ui-analyzer.md +12 -0
- package/docs/ui-doctor.md +15 -3
- package/doctor/cli.mjs +5 -0
- package/doctor/config.schema.json +2 -0
- package/doctor/index.d.mts +3 -0
- package/doctor/index.mjs +27 -1
- package/llms.txt +15 -5
- package/package.json +1 -1
- package/dist/chunk-5SYE4CCT.js.map +0 -1
- package/dist/chunk-F7OCOBTG.js.map +0 -1
- package/dist/chunk-JCV4GXWB.js.map +0 -1
- package/dist/chunk-NXWE5S57.js.map +0 -1
- package/dist/chunk-RKTTF3L4.js.map +0 -1
- package/dist/chunk-ZLWQZUGF.js +0 -27
- package/dist/chunk-ZLWQZUGF.js.map +0 -1
|
@@ -92,6 +92,16 @@ the `data-*` attributes it actually writes. First-party entries always carry
|
|
|
92
92
|
the list, empty when their helpers write none; it is optional in extension
|
|
93
93
|
catalogs.
|
|
94
94
|
|
|
95
|
+
An extension catalog may add `wiring.sources` entries with `export` and
|
|
96
|
+
`source` for each application-owned helper. The source path is relative to its
|
|
97
|
+
package root; `ui-wiring` resolves helper imports through this file, including
|
|
98
|
+
relative re-export barrels. Every named export must also appear in
|
|
99
|
+
`wiring.helpers`.
|
|
100
|
+
|
|
101
|
+
The selection catalog's `wiringProviders` lists public helpers that install
|
|
102
|
+
other wiring helpers. The `ui-wiring` rule accepts a provider call for each
|
|
103
|
+
helper it declares; `wireCatalog` provides `wireScrollDividers`.
|
|
104
|
+
|
|
95
105
|
The generator starts with permissive defaults. An `any` mode records that the
|
|
96
106
|
catalog has no defensible prohibition; it does not claim every composition is
|
|
97
107
|
recommended. Objective rules live in
|
|
@@ -252,6 +262,8 @@ claim Kerf delivery paths for them.
|
|
|
252
262
|
Extension entries may add `uses` for direct composition dependencies and
|
|
253
263
|
repository-relative `source` and `demoSource` files. Resolve each dependency
|
|
254
264
|
against the combined Kerf and consumer catalogs; do not duplicate a Kerf id.
|
|
265
|
+
For Doctor's opt-in component ownership mode, add `styleSources` with the CSS
|
|
266
|
+
paths each entry owns, relative to the consumer package root.
|
|
255
267
|
|
|
256
268
|
- Components own semantic markup, stable anatomy, documented variants, ARIA projection, and package CSS hooks.
|
|
257
269
|
- Applications own signals/stores, product copy, domain-state mapping, persistence, routing, permissions, and transport.
|
|
@@ -506,20 +518,49 @@ a plain `<button>` — the group styles `> button` fully, and it keeps the group
|
|
|
506
518
|
free of a Web Awesome dependency and shadow DOM. Use `ToolbarActionLink` when
|
|
507
519
|
the action must retain native anchor navigation; the group owns its geometry,
|
|
508
520
|
hover, and focus treatment. Set `overflow="scroll"` when a row of controls must
|
|
509
|
-
stay inside the available toolbar width.
|
|
521
|
+
stay inside the available toolbar width. A disabled native button keeps the
|
|
522
|
+
group's geometry, dims to half opacity, uses the `not-allowed` cursor, and
|
|
523
|
+
paints neither its own nor a lone group's hover surface.
|
|
524
|
+
Set `visibility="compact-only"` for a
|
|
510
525
|
group that replaces wider toolbar controls below the Toolbar's compact container
|
|
511
|
-
breakpoint; do not hide the group with an application class.
|
|
512
|
-
`
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
526
|
+
breakpoint; do not hide the group with an application class.
|
|
527
|
+
Use `sizing="grow"` on an expanded search group in a wrapping zone: it grows from
|
|
528
|
+
`growBasis` (19rem by default) into available row space, then takes a whole
|
|
529
|
+
row at 480px container width or less. The collapsed search stays icon-sized.
|
|
530
|
+
Use `sizing="fill"` for a group, including a non-search segmented control,
|
|
531
|
+
that spans its wrapping row. Set `visibility="hide-collapsed-tiny"` on a
|
|
532
|
+
collapsible group to omit its closed icon at 224px or less; expanded content
|
|
533
|
+
remains visible. These sizes use the enclosing Toolbar's container width.
|
|
534
|
+
In a stacked Toolbar, set `placement="end"` on a collapsed group that should
|
|
535
|
+
sit at the trailing edge of its flex row. An expanded search with
|
|
536
|
+
`sizing="fill"` takes a whole row and enters from the row above; reduced-motion
|
|
537
|
+
preferences remove that entrance animation.
|
|
538
|
+
Use `visibility="yield-to-expanded-sibling"` on trailing groups that should
|
|
539
|
+
leave a 480px-or-narrower toolbar while a sibling trailing group is expanded.
|
|
540
|
+
They return when it collapses or the toolbar widens; leading and center zones
|
|
541
|
+
remain visible.
|
|
542
|
+
Reach for `wa-button` only when you need a Web Awesome feature; a popup menu is a `PopupMenu`, which renders its own
|
|
543
|
+
trigger. By default a group stays on one row: each icon sits beside its label,
|
|
544
|
+
and the enclosing `Toolbar`'s responsive policy relocates whole groups. Set
|
|
545
|
+
`overflow="wrap"` only for independent links that should move as whole actions
|
|
546
|
+
to another row within the group; use `overflow="scroll"` for a bounded one-row
|
|
547
|
+
action strip. A wrapping group centers in the toolbar band on one row and
|
|
548
|
+
starts at its top when it grows to multiple rows. For a compact mixed-content
|
|
549
|
+
group, set `nestedDropdown`; a
|
|
518
550
|
text-and-caret trigger grows to its intrinsic width while an icon-only trigger
|
|
519
551
|
can remain square. A compact icon `<button>` is a 32px circle, so a selected
|
|
520
552
|
compact segment never becomes an oval. An icon-only `Select` joins a group as
|
|
521
553
|
one more segment with its siblings' inset, radius, hover, and focus ring; only a
|
|
522
|
-
lone Select grows the group to its icon-and-caret pill.
|
|
554
|
+
lone Select grows the group to its icon-and-caret pill.
|
|
555
|
+
|
|
556
|
+
For a rail view heading, use `Select presentation="title"`: its intrinsic
|
|
557
|
+
36px borderless trigger has a bold large label flush with its leading edge and
|
|
558
|
+
a caret beside that label. Set `focusRingInset` when a NavStack or panel clips
|
|
559
|
+
outside painting; the Select keeps its focus outline 3px inside the trigger
|
|
560
|
+
and drops it while the listbox is open. Both settings belong to Select, not an
|
|
561
|
+
application `::part()` override.
|
|
562
|
+
|
|
563
|
+
Set `menuInset` to configure the dropdown menu surface
|
|
523
564
|
without application `::part(menu)` CSS; the default `standard` keeps each menu's
|
|
524
565
|
own inset (8px for a raw `wa-dropdown`, the Select listbox's 4px for a
|
|
525
566
|
`PopupMenu`), while `compact` and `list-zero` apply to every nested menu. A
|
|
@@ -655,6 +696,8 @@ the only shrinking, horizontally scrolling region; `leading`, `trailing`, and
|
|
|
655
696
|
trailing action belongs immediately after the final tab and use `end` for a
|
|
656
697
|
separate action pinned to the far edge. Applications own the action content,
|
|
657
698
|
but must not restyle TabBar descendants to recreate that allocation.
|
|
699
|
+
When a leading AppTab is pinned, the leading scroll divider appears at that
|
|
700
|
+
tab's trailing edge while peer tabs scroll beneath it, including in RTL.
|
|
658
701
|
|
|
659
702
|
CSS, the generated wrappers that make it
|
|
660
703
|
reachable, and the registration module are the package's only declared side
|
|
@@ -676,6 +719,12 @@ line of its own and centers in its slot. It is a
|
|
|
676
719
|
placeholder, not progress: use `LoadingSpinner` for known busy activity, and never
|
|
677
720
|
animate the skeleton.
|
|
678
721
|
|
|
722
|
+
`LoadingSpinner` accepts the same `size` steps as `LucideIcon`: `xs`, `s`, `m`,
|
|
723
|
+
`l`, `xl` (12/16/20/24/32px at a 16px root) or a positive finite pixel
|
|
724
|
+
number converted to rem. Omit `size` to keep its inherited 1em box. A supplied
|
|
725
|
+
size is exposed as `data-size`; callers configure the spinner through this prop
|
|
726
|
+
instead of sizing its SVG from outside.
|
|
727
|
+
|
|
679
728
|
A placeholder is loading, not unavailable, so it keeps the `disabled` attribute (its
|
|
680
729
|
controls stay inert and out of the keyboard order) but never shows disabled chrome.
|
|
681
730
|
`skeleton.css`, which every component that renders a skeleton loads, neutralizes generic
|
|
@@ -171,7 +171,7 @@ beside a `ContentItem` value, and connect its id to the preview group's
|
|
|
171
171
|
| Panel, dialog, or page heading — `Toolbar` + `ToolbarText` | A panel, dialog, or page needs an extra-large title, optional icon, grouped trailing actions, and optional supporting copy. | Do not add a private heading wrapper or bespoke layout. The toolbar supplies structure, not modal behavior; use an application overlay or Web Awesome `wa-dialog` for that behavior. In a narrow peripheral rail or drawer, where xlarge would truncate at the panel's minimum size, use the default `ToolbarText` size as pane identity. | Connect title/supporting-copy ids to the host, group controls, and delegate actions. **For a page or view title set `headingLevel` (usually `1`)** on `ToolbarText`; omit it for a dialog title referenced by `aria-labelledby`. | Open state, focus lifecycle, dismissal, controls, ids, copy, and heading level. | `@kerfjs/ui/toolbar` + `@kerfjs/ui/toolbar-text` | [Toolbar headings](./accessibility.md#toolbar-headings) |
|
|
172
172
|
| Dialog or popup geometry — `DialogSurface`, `PopupSurface` | Recurring Web Awesome dialogs need standard sizes/presentations/body/footer insets, or dropdown menus need list-compatible inset ownership. | Do not use these wrappers as generic layout or to replace native dialog/dropdown behavior. Avoid consumer `::part()` overrides and do not stack a dialog body inset around list-owned child geometry. | Wrap one `wa-dialog` or `wa-dropdown`; choose the typed geometry while retaining native labeling, open state, focus, dismissal, and menu semantics. Compose a dialog body as a `List` by default, use `bodyInset="none"` when its children own list geometry, and wrap bare prose in `ListInsetText`. | State, focus lifecycle, dismissal, labels, content, and outer placement. | `@kerfjs/ui/surface-scaffold` | [Surface scaffolds](./surface-scaffold.md) |
|
|
173
173
|
| Key/value facts — `ValueTable`, `ValueTableRow` | Read-only labels and values form a semantic definition list, optionally with a leading icon. | Do not use it for editable form fields or a row/column data grid; use native form or table semantics. | Compose typed `ValueTableRow` entries; pass `icon` when a 24px leading icon adds useful context. | Values, formatting, icon meaning, and empty/loading policy. | `@kerfjs/ui/value-table` | [Component ownership](./component-contract.md#ownership-boundaries) |
|
|
174
|
-
| Indeterminate activity — `LoadingSpinner` | A Kerf surface needs compact, labeled or decorative indeterminate progress. | Do not use it for known progress; use Web Awesome `wa-progress-bar` or `wa-progress-ring`. Direct Web Awesome UI may use `wa-spinner`; do not mix spinner systems within one surface. |
|
|
174
|
+
| Indeterminate activity — `LoadingSpinner` | A Kerf surface needs compact, labeled or decorative indeterminate progress. | Do not use it for known progress; use Web Awesome `wa-progress-bar` or `wa-progress-ring`. Direct Web Awesome UI may use `wa-spinner`; do not mix spinner systems within one surface. | Pass a label when the spinner conveys status; use size for a named icon step or positive pixel size. | Loading lifecycle and adjacent status copy. | `@kerfjs/ui/loading-spinner` | [Accessibility](./accessibility.md#shared-rules) |
|
|
175
175
|
| Loading placeholder — `Skeleton` + a component's `placeholder` prop | A value or a whole component is still loading and should hold its space as a subtle, unanimated block, keeping the layout stable — an inspector or detail view rendering its real chrome with per-record values absent. | Do not use it for known progress (use `LoadingSpinner`), do not animate it, and do not hand-rebuild a component's empty state — set `placeholder` on the component instead. Prefer it over `wa-skeleton`, which the pure-Kerf primitives avoid to stay Web-Awesome-free. | Set `placeholder` on a value-bearing component (`Select`, `ListHeader`, `ListItem`, `ValueTableRow`, `SegmentedControl`, `StateBanner`, `AppTab`, `ToolbarText`, `ListActionRow`) to render skeletons in its value slots with interactivity disabled; use the standalone `Skeleton` for a custom slot. | Loading lifecycle, which slots are unknown, and announcing the loading region. | `@kerfjs/ui/skeleton` | [Accessibility](./accessibility.md#shared-rules) |
|
|
176
176
|
| Command menu — `PopupMenu` | A short list of commands sits behind one trigger, such as a toolbar sort or More menu, optionally grouped under headings and dividers with item icons. | Do not use it to choose a persistent value; use `Select`. Do not hand-write `wa-dropdown`/`wa-dropdown-item` markup for an action menu. | Import `@kerfjs/ui/popup-menu/register` once. Place it in a `single` `ToolbarControlGroup` with `nestedDropdown` and a `menuInset`, or wrap it in `PopupSurface` outside a toolbar. Handle item `action`s by delegation. | Commands, enabled state, and reactions to opening or closing. | `@kerfjs/ui/popup-menu` | [Toolbar composition](#toolbar-composition) |
|
|
177
177
|
| Value selection — `Select` | A controlled form, toolbar, or compact navigation value comes from a moderate or long choice list, possibly grouped, icon-bearing, icon-only when closed, or truncating. Set `multiple` with an array `value` when the person may choose any number of them; add `selectedPresentation="icon-only"` and a fixed `triggerIcon` for a toolbar filter menu. | Do not use it for commands; use `PopupMenu`. Do not use it for a small visible choice set; use `SegmentedControl`. Prefer it over direct `wa-select`, `wa-option`, or value-like `wa-dropdown`/`wa-dropdown-item` composition. | Import `@kerfjs/ui/select/register` once for registration and current-request animation ownership; listen for standard input/change events. Do not add consumer popup timing or positioning repairs. Use typed presentation props rather than consumer `::part()` overrides; let the parent own placement. | Controlled value, validation, choices, domain mapping, and outer placement. | `@kerfjs/ui/select` | [Web Awesome integration](../README.md#web-awesome-theme) |
|
|
@@ -268,11 +268,16 @@ instead of the default ellipsis. A `TabBar` owns its
|
|
|
268
268
|
scrolling strip; its public `--kui-tab-bar-strip-*` tokens configure strip
|
|
269
269
|
height, spacing, border, radius, background, and scroll inset without styling
|
|
270
270
|
its internal classes. `--kui-tab-bar-trailing-flex` controls the trailing zone.
|
|
271
|
+
For a distinct application action, use `end` to pin it at the far edge; use
|
|
272
|
+
`trailingPlacement="adjacent"` to keep a tab-local `trailing` action beside the
|
|
273
|
+
strip. Each zone accepts one `ToolbarControlGroup` or one standalone
|
|
274
|
+
`wa-button` for a primary action with its own chrome. Avoid wrappers and
|
|
275
|
+
application flex overrides.
|
|
271
276
|
Set `pinned` on the first `AppTab` when that tab must remain visible as peers
|
|
272
277
|
scroll. It stays inside the tablist and keyboard order. Set the public
|
|
273
278
|
`--kui-app-tab-pinned-background` token if the surrounding surface differs from
|
|
274
279
|
the default; the pinned tab must cover peers as they scroll beneath it. Its
|
|
275
|
-
background also covers the strip's inline padding so scrolled labels cannot
|
|
280
|
+
background also covers the strip's inline padding and border so scrolled labels cannot
|
|
276
281
|
paint beside the pinned edge, including in right-to-left strips. Call
|
|
277
282
|
`wireTabBars` for keyboard selection and reorder behavior and its WebKit RTL
|
|
278
283
|
scroll correction.
|
|
@@ -345,6 +350,11 @@ search content before saving.
|
|
|
345
350
|
The form-field corners and required marker use the same Web Awesome
|
|
346
351
|
form-control radius, marker color, and offset tokens as a neighboring
|
|
347
352
|
`wa-input`.
|
|
353
|
+
The four public color tokens (`--kui-token-search-background`,
|
|
354
|
+
`--kui-token-search-border`, `--kui-token-search-token-background`, and
|
|
355
|
+
`--kui-token-search-token-foreground`) inherit from an app ancestor or the
|
|
356
|
+
field's root. `className` marks that root, including the outer wrapper in
|
|
357
|
+
`form-field` presentation, so one app class can configure either form.
|
|
348
358
|
`TokenSearchRule.suggest(input, state)` receives the unfinished value and current committed tokens, so a rule can omit values already selected. Existing one-argument suggestion callbacks continue to work.
|
|
349
359
|
For a sibling date picker or other app helper, call `model.commit(value)` while a `name:` prefix is active. It parses and commits that value for the active rule even when it is absent from suggestions, replacing the unfinished prefix text. Invalid values and calls without an active prefix leave the query unchanged; `choose(value)` remains restricted to suggested tokens.
|
|
350
360
|
To apply a saved search or restore a query programmatically, call `model.replace({ query, tokens })`. It publishes parsed state and rebuilds the DOM-owned editor text even when no chip changed. For a field without a model, change the field's `revision` prop when setting `query` yourself.
|
|
@@ -356,6 +366,9 @@ passive content such as a shortcut hint.
|
|
|
356
366
|
Set `fill` on a collapsible `TokenSearchField` in a stretched Toolbar center to
|
|
357
367
|
use the available width when expanded; the field and its search group retain
|
|
358
368
|
their compact widths when collapsed.
|
|
369
|
+
When the expanded toolbar field has focus, its inset surface uses
|
|
370
|
+
`--kui-token-search-background` or the default surface color if that token is
|
|
371
|
+
unset.
|
|
359
372
|
|
|
360
373
|
## Toolbar composition
|
|
361
374
|
|
|
@@ -377,10 +390,13 @@ copy below as app-owned content. Size the title to its track: a narrow
|
|
|
377
390
|
peripheral rail or drawer (a navigator, inspector, or console beside a work
|
|
378
391
|
area that carries the extra-large title, often with its own close control in
|
|
379
392
|
the header) labels itself with the default size, because extra-large truncates
|
|
380
|
-
there — "Inspector" becomes "Ins…" in a 160px rail.
|
|
381
|
-
`ToolbarControlGroup`: an icon sits beside its label on one row and the
|
|
382
|
-
sizes to its content, so a row that does not fit relocates whole groups
|
|
383
|
-
through the toolbar's `responsive` policy
|
|
393
|
+
there — "Inspector" becomes "Ins…" in a 160px rail. By default, nothing wraps
|
|
394
|
+
inside a `ToolbarControlGroup`: an icon sits beside its label on one row and the
|
|
395
|
+
group sizes to its content, so a row that does not fit relocates whole groups
|
|
396
|
+
through the toolbar's `responsive` policy. For independent action links that
|
|
397
|
+
must all remain visible, set `overflow="wrap"`: whole links move to another
|
|
398
|
+
row inside the group without breaking an icon from its label. Use
|
|
399
|
+
`overflow="scroll"` when a single-row action strip is intentional. A
|
|
384
400
|
toolbar draws no divider by default: pinned over or under scrolling content,
|
|
385
401
|
it gets one from the scroll state instead — the `Pane` it heads draws the line
|
|
386
402
|
only while content is scrolled beneath it, once `wireScrollDividers` is wired
|
package/docs/layout.md
CHANGED
|
@@ -111,6 +111,12 @@ scrolls under its pinned header; with a header it draws the line under that
|
|
|
111
111
|
header itself, so the stack's chrome never doubles it. It follows re-renders: panes that appear later are paired, and an
|
|
112
112
|
attribute a morph drops returns before paint.
|
|
113
113
|
|
|
114
|
+
Multiple calls in one document share one attribute writer, including calls on
|
|
115
|
+
overlapping roots. Each call contributes its own root and optional named
|
|
116
|
+
targets; its disposer removes only that registration. When the last disposer
|
|
117
|
+
runs, the writer removes the attributes it owns. An app that calls `wireCatalog`
|
|
118
|
+
for a root already has scroll dividers there and does not need another call.
|
|
119
|
+
|
|
114
120
|
The wiring only reports state; every component draws its own line. It writes
|
|
115
121
|
`data-scroll-overflow` (the edges with content hidden beyond them, in canonical
|
|
116
122
|
`t`/`r`/`b`/`l` order) on each scroller and `data-scroll-divider` (the sides to
|
package/docs/ui-analyzer.md
CHANGED
|
@@ -144,6 +144,10 @@ which element a selector styles.
|
|
|
144
144
|
imported component's JSX (`<Toolbar className="header" />`) is the subject
|
|
145
145
|
of a rule in a stylesheet that file imports, so the application restyles the
|
|
146
146
|
component through it. The evidence names the stylesheet, line, and selector.
|
|
147
|
+
For cataloged application components, the import may be direct and relative
|
|
148
|
+
or use an exact or wildcard TypeScript `paths` alias from the nearest
|
|
149
|
+
`tsconfig.json` inside the analyzed root (including inherited options).
|
|
150
|
+
Unresolved aliases remain unclassified; the analyzer does not execute code.
|
|
147
151
|
|
|
148
152
|
Every message names the configuration to use (the component's typed props,
|
|
149
153
|
public tokens, and variants from its catalog entry) and ends with the same
|
|
@@ -188,6 +192,14 @@ findings. `eslint-plugin-kerfjs`'s `ui-component-ownership` rule reports
|
|
|
188
192
|
`KUI-L020` and `KUI-L021` in JavaScript and TypeScript (style strings, style
|
|
189
193
|
objects, and `style.setProperty()`), where this analyzer reads only CSS.
|
|
190
194
|
|
|
195
|
+
Pass `ownership: 'component'` to `analyzeUiProject` to enforce the boundary
|
|
196
|
+
between components in one package. In that mode each catalog entry owns only
|
|
197
|
+
its declared `styleSources` for CSS and its selection catalog `source` module
|
|
198
|
+
for JSX; sibling files are foreign. Paths are relative to that entry's package
|
|
199
|
+
root. Declare `styleSources` on the selection catalog extension entry or
|
|
200
|
+
composition entry. Entries without those paths own no files in component mode,
|
|
201
|
+
so add them before adopting it.
|
|
202
|
+
|
|
191
203
|
### Retired: `KUI-L005` (forced component dimension)
|
|
192
204
|
|
|
193
205
|
`KUI-L005` used to report, as a review finding, any `width`/`height`/`min-*`/
|
package/docs/ui-doctor.md
CHANGED
|
@@ -7,6 +7,7 @@ npx kerf-ui-doctor --full
|
|
|
7
7
|
npx kerf-ui-doctor --changed
|
|
8
8
|
npx kerf-ui-doctor --package @acme/admin --changed
|
|
9
9
|
npx kerf-ui-doctor --full --browser-url http://127.0.0.1:4173
|
|
10
|
+
npx kerf-ui-doctor --full --fail-on warning
|
|
10
11
|
```
|
|
11
12
|
|
|
12
13
|
The default terminal output is short and repair-oriented. `--format json` emits the schema-version-1 report; `--output report.json` writes the same representation. The package exports its contract from `@kerfjs/ui/doctor`, its configuration schema from `@kerfjs/ui/doctor/config.schema.json`, and its report schema from `@kerfjs/ui/doctor/report.schema.json`.
|
|
@@ -39,6 +40,15 @@ root (`KUI-L022`); the ESLint stage's `ui-component-ownership` rule reports
|
|
|
39
40
|
component's configuration, and its message routes a need with no prop to a
|
|
40
41
|
component-gap report instead of an override (see
|
|
41
42
|
[the analyzer rule](./ui-analyzer.md#component-ownership-kui-l019kui-l022)).
|
|
43
|
+
The analyzer resolves a cataloged application component imported through an
|
|
44
|
+
exact or wildcard TypeScript `paths` alias from the nearest consumer tsconfig,
|
|
45
|
+
so the same root hook-class check applies in those files.
|
|
46
|
+
Set `"ownership": "component"` in `.kerf-ui-doctor.json` to enforce the same
|
|
47
|
+
boundary between a consumer package's own cataloged components. The default is
|
|
48
|
+
`"package"`. Each local catalog entry then owns only its declared
|
|
49
|
+
`styleSources` (relative to its package root) and its selection catalog
|
|
50
|
+
`source` module. The component mode reports sibling CSS and hook-class
|
|
51
|
+
restyles while still allowing a component's own stylesheet and module.
|
|
42
52
|
|
|
43
53
|
The browser evaluator is different: it runs the application and is disabled by default. It only runs when configuration supplies `browser.url` or the command receives `--browser-url`. Start and authorize the target application separately.
|
|
44
54
|
|
|
@@ -66,8 +76,10 @@ Place `.kerf-ui-doctor.json` at the workspace root:
|
|
|
66
76
|
"$schema": "./node_modules/@kerfjs/ui/doctor/config.schema.json",
|
|
67
77
|
"schemaVersion": 1,
|
|
68
78
|
"mode": "full",
|
|
79
|
+
"ownership": "component",
|
|
69
80
|
"stages": { "browser": false },
|
|
70
81
|
"cache": true,
|
|
82
|
+
"failOn": "warning",
|
|
71
83
|
"suppressions": [
|
|
72
84
|
{
|
|
73
85
|
"id": "legacy-toolbar",
|
|
@@ -91,14 +103,14 @@ Reports replace the workspace's absolute path with `<repo-root>` and express in-
|
|
|
91
103
|
|
|
92
104
|
Every diagnostic has a stable `id`, `severity`, `stage`, message, and—when applicable—an exact source location with JSON path or DOM context/selector. Analyzer evidence, catalog facts, documentation links, and safe next actions are preserved when the source tool provides them. Identical findings merge with their source stages; same identifiers with conflicting severities remain separate and add `KUI-D003`.
|
|
93
105
|
|
|
94
|
-
Exit codes are deterministic
|
|
106
|
+
Exit codes are deterministic. `failOn` defaults to `error`; `--fail-on error|review|warning` overrides the config for a run. `review` fails on errors or review findings; `warning` also fails on warnings. Suppressed diagnostics never count. The effective threshold is part of the cache key.
|
|
95
107
|
|
|
96
|
-
- `0`: no active
|
|
108
|
+
- `0`: no active diagnostics at or above the selected threshold;
|
|
97
109
|
- `1`: repairable application findings;
|
|
98
110
|
- `2`: malformed configuration, unavailable required tooling, or a failed stage;
|
|
99
111
|
- `130`: cancellation.
|
|
100
112
|
|
|
101
|
-
Warnings and review findings remain visible
|
|
113
|
+
Warnings and review findings remain visible at every threshold. A cached report keeps its original exit code and labels previously run stages `cached`.
|
|
102
114
|
|
|
103
115
|
## Monorepos and repair loops
|
|
104
116
|
|
package/doctor/cli.mjs
CHANGED
|
@@ -54,6 +54,7 @@ Options:
|
|
|
54
54
|
--browser <list> Comma-separated chromium,firefox,webkit
|
|
55
55
|
--eslint <preset> recommended-ui or strict-ui
|
|
56
56
|
--format <format> text or json (default: text)
|
|
57
|
+
--fail-on <severity> error, review, or warning (default: error)
|
|
57
58
|
--output <path> Also write the merged report
|
|
58
59
|
--no-cache Disable the content-addressed cache
|
|
59
60
|
|
|
@@ -68,6 +69,7 @@ Exit codes: 0 clean, 1 findings, 2 configuration/tool failure, 130 cancelled.`);
|
|
|
68
69
|
const browserList = take('--browser');
|
|
69
70
|
const eslintConfig = take('--eslint') ?? 'recommended-ui';
|
|
70
71
|
const format = take('--format') ?? 'text';
|
|
72
|
+
const failOn = take('--fail-on');
|
|
71
73
|
const output = take('--output');
|
|
72
74
|
const full = args.includes('--full');
|
|
73
75
|
const changed = args.includes('--changed');
|
|
@@ -80,6 +82,8 @@ Exit codes: 0 clean, 1 findings, 2 configuration/tool failure, 130 cancelled.`);
|
|
|
80
82
|
throw new Error('--full and --changed are mutually exclusive.');
|
|
81
83
|
if (!['text', 'json'].includes(format))
|
|
82
84
|
throw new Error('--format must be text or json.');
|
|
85
|
+
if (failOn && !['error', 'review', 'warning'].includes(failOn))
|
|
86
|
+
throw new Error('--fail-on must be error, review, or warning.');
|
|
83
87
|
if (!['recommended-ui', 'strict-ui'].includes(eslintConfig))
|
|
84
88
|
throw new Error('--eslint must be recommended-ui or strict-ui.');
|
|
85
89
|
const abort = new globalThis.AbortController();
|
|
@@ -95,6 +99,7 @@ Exit codes: 0 clean, 1 findings, 2 configuration/tool failure, 130 cancelled.`);
|
|
|
95
99
|
cache: !noCache,
|
|
96
100
|
signal: abort.signal,
|
|
97
101
|
eslintConfig,
|
|
102
|
+
failOn,
|
|
98
103
|
browser: url
|
|
99
104
|
? { url, browsers: browserList?.split(',').map((item) => item.trim()) }
|
|
100
105
|
: undefined,
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"schemaVersion": { "const": 1 },
|
|
10
10
|
"mode": { "enum": ["full", "changed"] },
|
|
11
11
|
"package": { "type": "string", "minLength": 1 },
|
|
12
|
+
"ownership": { "enum": ["package", "component"] },
|
|
12
13
|
"stages": {
|
|
13
14
|
"type": "object",
|
|
14
15
|
"properties": {
|
|
@@ -37,6 +38,7 @@
|
|
|
37
38
|
"additionalProperties": false
|
|
38
39
|
},
|
|
39
40
|
"cache": { "type": "boolean" },
|
|
41
|
+
"failOn": { "enum": ["error", "review", "warning"] },
|
|
40
42
|
"suppressions": {
|
|
41
43
|
"type": "array",
|
|
42
44
|
"items": {
|
package/doctor/index.d.mts
CHANGED
|
@@ -22,6 +22,7 @@ export interface UiDoctorConfig {
|
|
|
22
22
|
schemaVersion: 1;
|
|
23
23
|
mode?: 'full' | 'changed';
|
|
24
24
|
package?: string;
|
|
25
|
+
ownership?: 'package' | 'component';
|
|
25
26
|
stages?: Partial<
|
|
26
27
|
Record<
|
|
27
28
|
'catalog' | 'typescript' | 'eslint' | 'analyzer' | 'browser',
|
|
@@ -36,6 +37,7 @@ export interface UiDoctorConfig {
|
|
|
36
37
|
reportPath?: string;
|
|
37
38
|
};
|
|
38
39
|
cache?: boolean;
|
|
40
|
+
failOn?: 'error' | 'review' | 'warning';
|
|
39
41
|
suppressions?: UiDoctorSuppression[];
|
|
40
42
|
}
|
|
41
43
|
|
|
@@ -109,6 +111,7 @@ export function runUiDoctor(options?: {
|
|
|
109
111
|
config?: UiDoctorConfig;
|
|
110
112
|
configPath?: string;
|
|
111
113
|
cache?: boolean;
|
|
114
|
+
failOn?: 'error' | 'review' | 'warning';
|
|
112
115
|
signal?: AbortSignal;
|
|
113
116
|
browser?: UiDoctorConfig['browser'];
|
|
114
117
|
eslintConfig?: 'recommended-ui' | 'strict-ui';
|
package/doctor/index.mjs
CHANGED
|
@@ -131,9 +131,11 @@ export function validateUiDoctorConfig(
|
|
|
131
131
|
'schemaVersion',
|
|
132
132
|
'mode',
|
|
133
133
|
'package',
|
|
134
|
+
'ownership',
|
|
134
135
|
'stages',
|
|
135
136
|
'browser',
|
|
136
137
|
'cache',
|
|
138
|
+
'failOn',
|
|
137
139
|
'suppressions',
|
|
138
140
|
]);
|
|
139
141
|
for (const key of Object.keys(config))
|
|
@@ -152,6 +154,11 @@ export function validateUiDoctorConfig(
|
|
|
152
154
|
'$.package',
|
|
153
155
|
'package must be a non-empty workspace package name or path.',
|
|
154
156
|
);
|
|
157
|
+
if (
|
|
158
|
+
config.ownership !== undefined &&
|
|
159
|
+
!['package', 'component'].includes(config.ownership)
|
|
160
|
+
)
|
|
161
|
+
add('$.ownership', 'ownership must be package or component.');
|
|
155
162
|
if (config.stages !== undefined) {
|
|
156
163
|
if (
|
|
157
164
|
!config.stages ||
|
|
@@ -224,6 +231,11 @@ export function validateUiDoctorConfig(
|
|
|
224
231
|
}
|
|
225
232
|
if (config.cache !== undefined && typeof config.cache !== 'boolean')
|
|
226
233
|
add('$.cache', 'cache must be boolean.');
|
|
234
|
+
if (
|
|
235
|
+
config.failOn !== undefined &&
|
|
236
|
+
!['error', 'review', 'warning'].includes(config.failOn)
|
|
237
|
+
)
|
|
238
|
+
add('$.failOn', 'failOn must be error, review, or warning.');
|
|
227
239
|
if (config.suppressions !== undefined) {
|
|
228
240
|
if (!Array.isArray(config.suppressions))
|
|
229
241
|
add('$.suppressions', 'suppressions must be an array.');
|
|
@@ -975,6 +987,7 @@ export async function runUiDoctor({
|
|
|
975
987
|
config: suppliedConfig,
|
|
976
988
|
configPath,
|
|
977
989
|
cache,
|
|
990
|
+
failOn,
|
|
978
991
|
signal,
|
|
979
992
|
browser,
|
|
980
993
|
runners = {},
|
|
@@ -1008,6 +1021,14 @@ export async function runUiDoctor({
|
|
|
1008
1021
|
);
|
|
1009
1022
|
}
|
|
1010
1023
|
const selectedMode = mode ?? config.mode ?? 'full';
|
|
1024
|
+
const selectedFailOn = failOn ?? config.failOn ?? 'error';
|
|
1025
|
+
if (failOn !== undefined && !['error', 'review', 'warning'].includes(failOn))
|
|
1026
|
+
initialDiagnostics.push(
|
|
1027
|
+
configDiagnostic('failOn must be error, review, or warning.', {
|
|
1028
|
+
file: '<options>',
|
|
1029
|
+
path: '$.failOn',
|
|
1030
|
+
}),
|
|
1031
|
+
);
|
|
1011
1032
|
const stages = { ...defaultStages, ...config.stages };
|
|
1012
1033
|
if (browser?.url || config.browser?.url) stages.browser = true;
|
|
1013
1034
|
const selectedPaths =
|
|
@@ -1035,6 +1056,7 @@ export async function runUiDoctor({
|
|
|
1035
1056
|
const cachePath = resolve(root, '.kerf-cache/ui-doctor-v1.json');
|
|
1036
1057
|
const key = await cacheKey(root, packageRoot, selectedMode, selectedPaths, {
|
|
1037
1058
|
...config,
|
|
1059
|
+
failOn: selectedFailOn,
|
|
1038
1060
|
browser: config.browser
|
|
1039
1061
|
? { ...config.browser, url: '<redacted-url>' }
|
|
1040
1062
|
: undefined,
|
|
@@ -1123,6 +1145,7 @@ export async function runUiDoctor({
|
|
|
1123
1145
|
root,
|
|
1124
1146
|
packageRoot,
|
|
1125
1147
|
paths: selectedPaths,
|
|
1148
|
+
ownership: config.ownership ?? 'package',
|
|
1126
1149
|
signal,
|
|
1127
1150
|
})
|
|
1128
1151
|
: (async () => ({
|
|
@@ -1130,6 +1153,7 @@ export async function runUiDoctor({
|
|
|
1130
1153
|
await analyzeUiProject({
|
|
1131
1154
|
root: packageRoot,
|
|
1132
1155
|
paths: selectedPaths,
|
|
1156
|
+
ownership: config.ownership ?? 'package',
|
|
1133
1157
|
knownRules: [...registeredRuleIds, ...eslintRuleIds],
|
|
1134
1158
|
}),
|
|
1135
1159
|
),
|
|
@@ -1228,7 +1252,9 @@ export async function runUiDoctor({
|
|
|
1228
1252
|
? UI_DOCTOR_EXIT.cancelled
|
|
1229
1253
|
: infrastructureFailure
|
|
1230
1254
|
? UI_DOCTOR_EXIT.configuration
|
|
1231
|
-
: summary.errors
|
|
1255
|
+
: summary.errors ||
|
|
1256
|
+
(selectedFailOn !== 'error' && summary.review) ||
|
|
1257
|
+
(selectedFailOn === 'warning' && summary.warnings)
|
|
1232
1258
|
? UI_DOCTOR_EXIT.findings
|
|
1233
1259
|
: UI_DOCTOR_EXIT.clean;
|
|
1234
1260
|
const report = redact(root, {
|
package/llms.txt
CHANGED
|
@@ -109,7 +109,7 @@ not a reason to override — and your own components follow the same rule.
|
|
|
109
109
|
pair measures below 4.5:1, is review finding `KUI-L018` (set both); CSS that restyles a component
|
|
110
110
|
(`KUI-L019`), touches its private `--_*` variables (`KUI-L020`), overrides a token a typed prop sets
|
|
111
111
|
(`KUI-L021`), or styles it through a hook class on its root (`KUI-L022`) is an error: configure the
|
|
112
|
-
component instead, and report a component gap when no prop exists; repair source-located errors until exit 0. Use full mode for release gates;
|
|
112
|
+
component instead, and report a component gap when no prop exists; application-component imports may use TypeScript `paths` aliases; use `"ownership": "component"` with catalog `styleSources` to apply this boundary between an app's own components; repair source-located errors until exit 0. Use full mode for release gates;
|
|
113
113
|
generated `dist`/`coverage`, dependency, cache, evidence, and nested-worktree
|
|
114
114
|
directories are excluded consistently from every static stage.
|
|
115
115
|
[`kerf-ui-analyze`](./docs/ui-analyzer.md) remains available when only static
|
|
@@ -165,6 +165,9 @@ headingLevel`, `TokenSearchField clearIcon`, `ContentItem frame="framed"` for a
|
|
|
165
165
|
expanded trailing control to a full second row at `responsiveAt`; the leading
|
|
166
166
|
title remains above it. Public leading-min-width, trailing-gap/justify, and
|
|
167
167
|
per-zone inline-padding tokens cover app-specific toolbar geometry.
|
|
168
|
+
In a stacked Toolbar, `ToolbarControlGroup placement="end"` places a collapsed
|
|
169
|
+
search at its row's trailing edge; `sizing="fill"` gives its expanded state a
|
|
170
|
+
full row and a reduced-motion-aware downward entrance.
|
|
168
171
|
`--kui-toolbar-inset: 0px` removes an embedded Toolbar's outer padding and
|
|
169
172
|
corresponding minimum-height allowance while preserving its zone gap and
|
|
170
173
|
safe-area compensation.
|
|
@@ -286,7 +289,9 @@ headingLevel`, `TokenSearchField clearIcon`, `ContentItem frame="framed"` for a
|
|
|
286
289
|
(`@kerfjs/ui/popup-menu`, registered once via `@kerfjs/ui/popup-menu/register`)
|
|
287
290
|
in a `single` `ToolbarControlGroup` with `nestedDropdown`; never hand-write
|
|
288
291
|
`wa-dropdown` markup for a menu. Let the group size the trigger from its icon,
|
|
289
|
-
localized text, and caret. `
|
|
292
|
+
localized text, and caret. Set `overflow="wrap"` for independent action links
|
|
293
|
+
that must remain fully visible at narrow widths; whole links move to a second
|
|
294
|
+
row, as in the catalog resource footer. `PopupMenu` items also support nested `submenu`
|
|
290
295
|
actions, headings, dividers, checked or trailing choice marks, danger tone,
|
|
291
296
|
and disabled reasons; nested headings/dividers stay out of keyboard choices;
|
|
292
297
|
use `context` with `openPopupMenuAt`/`closePopupMenu` for pointer-anchored row
|
|
@@ -379,7 +384,9 @@ headingLevel`, `TokenSearchField clearIcon`, `ContentItem frame="framed"` for a
|
|
|
379
384
|
callbacks. For a tab-local action plus a separate far-edge action, set
|
|
380
385
|
`TabBar trailingPlacement="adjacent"`, put the local action in `trailing`, and
|
|
381
386
|
the workspace action in `end`; the component keeps both visible while its tab
|
|
382
|
-
list shrinks and scrolls, without application descendant CSS.
|
|
387
|
+
list shrinks and scrolls, without application descendant CSS. Each action
|
|
388
|
+
zone accepts one ToolbarControlGroup or a standalone `wa-button` for its own
|
|
389
|
+
Web Awesome chrome. Managed
|
|
383
390
|
TokenSearchField clear also preserves open state and replacement
|
|
384
391
|
editor focus before the next input task, so shortcut-letter typing stays in search.
|
|
385
392
|
Select All deletion does the same; deletion
|
|
@@ -416,7 +423,8 @@ headingLevel`, `TokenSearchField clearIcon`, `ContentItem frame="framed"` for a
|
|
|
416
423
|
`busy` spinner while its `status` (and a `ListItem`'s `trailing`) content
|
|
417
424
|
become skeletons. Use the standalone `Skeleton` for a custom slot. Prefer it over `wa-skeleton`
|
|
418
425
|
(the pure-Kerf primitives avoid Web Awesome), and never animate it — use
|
|
419
|
-
`LoadingSpinner` for known busy activity instead.
|
|
426
|
+
`LoadingSpinner` for known busy activity instead. Set its `size` prop to a
|
|
427
|
+
named icon step or positive pixel number when the inherited 1em is unsuitable.
|
|
420
428
|
- **Keep ListItem content inside the row.** The rounded `ListItem` root clips
|
|
421
429
|
overflowing label, status, and dormant trailing content. Set `multiline` when
|
|
422
430
|
the primary label should wrap; use `ListActionRow` instead when trailing content
|
|
@@ -435,7 +443,9 @@ headingLevel`, `TokenSearchField clearIcon`, `ContentItem frame="framed"` for a
|
|
|
435
443
|
- **Size a Select through its API.** `triggerWidth` chooses `fit-content`,
|
|
436
444
|
`max-content`, or `fill`; `--kui-select-trigger-max-width` caps it. For custom
|
|
437
445
|
selected text, use the public color, font-size, and font-weight tokens; the
|
|
438
|
-
closed value ellipsizes when space runs out.
|
|
446
|
+
closed value ellipsizes when space runs out. `presentation="title"` gives a
|
|
447
|
+
rail view heading a 36px bold, flush-start trigger and adjacent caret;
|
|
448
|
+
`focusRingInset` keeps its ring inside clipping ancestors.
|
|
439
449
|
- **Configure application tabs at their owners.** AppTab's `icon-only`
|
|
440
450
|
presentation keeps `name` accessible, `labelMaxWidth` ellipsizes a long name,
|
|
441
451
|
`nameOverflow="visible"` shows the full name during inline loading, and
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/token-search-field.tsx"],"names":[],"mappings":"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","file":"chunk-5SYE4CCT.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 interface TokenSearchTrailingAction {\n icon: SafeHtml;\n label: string;\n action: string;\n id?: 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 /** Let an expanded collapsible field consume the available inline width. */\n fill?: boolean;\n leading?: KerfUiContent;\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 TokenSearchTrailingProps =\n | { trailing?: KerfUiContent; trailingAction?: never }\n | { trailing?: never; trailingAction: TokenSearchTrailingAction };\n\ntype TokenSearchPresentationProps =\n | {\n /** Standalone field chrome or the inset visual layer of a configured toolbar group. */\n presentation?: 'standalone' | 'toolbar-group';\n hint?: never;\n required?: never;\n }\n | {\n /** Visible form label and optional hint around the full-width standalone control. */\n presentation: 'form-field';\n hint?: string;\n /** Marks the editor required; the app validates its value. */\n required?: boolean;\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 TokenSearchTrailingProps &\n TokenSearchPresentationProps;\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 fill = false,\n collapsible = false,\n expanded = false,\n expandAction = 'expand-token-search',\n expandLabel,\n leading,\n trailing,\n trailingAction,\n presentation = 'standalone',\n hint,\n required = false,\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 const control = (\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-fill={String(fill)}\n data-has-trailing={String(Boolean(trailing || trailingAction))}\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={presentation === 'form-field' ? undefined : label}\n aria-labelledby={\n presentation === 'form-field' ? `${id}-label` : undefined\n }\n aria-describedby={\n presentation === 'form-field' && hint ? `${id}-hint` : undefined\n }\n aria-required={required ? 'true' : undefined}\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 {trailingAction ? (\n <button\n type=\"button\"\n class=\"kui-token-search__trailing-action\"\n id={trailingAction.id}\n data-action={trailingAction.action}\n aria-label={trailingAction.label}\n title={trailingAction.label}\n disabled={disabled}\n >\n {trailingAction.icon}\n </button>\n ) : trailing ? (\n <span class=\"kui-token-search__trailing\">{trailing}</span>\n ) : null}\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 if (presentation !== 'form-field') return control;\n return (\n <div class=\"kui-token-search__field\" data-token-search-form-field={id}>\n <div\n class=\"kui-token-search__field-label\"\n id={`${id}-label`}\n data-token-search-form-label={id}\n >\n {label}\n {required && (\n <span class=\"kui-token-search__field-required\" aria-hidden=\"true\">\n *\n </span>\n )}\n </div>\n {control}\n {hint && (\n <div class=\"kui-token-search__field-hint\" id={`${id}-hint`}>\n {hint}\n </div>\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"]}
|