@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.
Files changed (92) hide show
  1. package/README.md +3 -0
  2. package/ai/component-catalog-extension.schema.json +12 -0
  3. package/ai/component-catalog.json +14 -4
  4. package/ai/component-catalog.schema.json +14 -0
  5. package/ai/component-composition-rules.json +2 -2
  6. package/ai/component-composition.d.ts +2 -0
  7. package/ai/component-composition.json +2 -2
  8. package/ai/component-composition.schema.json +23 -0
  9. package/ai/components/loading-spinner.md +1 -0
  10. package/ai/components/tab-bar.md +5 -4
  11. package/ai/components/toolbar-control-group.md +2 -2
  12. package/ai/public-api-signatures-v1.md +34 -14
  13. package/ai/skill.md +18 -3
  14. package/ai/webawesome-jsx-signatures-v1.md +1 -1
  15. package/analyzer/index.d.mts +1 -0
  16. package/analyzer/index.mjs +108 -12
  17. package/dist/catalog.js +4 -4
  18. package/dist/catalog.js.map +1 -1
  19. package/dist/{chunk-P4UXSYIJ.js → chunk-2ZOLX3IE.js} +2 -2
  20. package/dist/{chunk-P4UXSYIJ.js.map → chunk-2ZOLX3IE.js.map} +1 -1
  21. package/dist/{chunk-E36EVCKG.js → chunk-47CKKLBE.js} +2 -2
  22. package/dist/{chunk-E36EVCKG.js.map → chunk-47CKKLBE.js.map} +1 -1
  23. package/dist/{chunk-JCV4GXWB.js → chunk-6DFDTWGG.js} +100 -61
  24. package/dist/chunk-6DFDTWGG.js.map +1 -0
  25. package/dist/{chunk-IAOAZUOO.js → chunk-7OKX3EF6.js} +2 -2
  26. package/dist/{chunk-IAOAZUOO.js.map → chunk-7OKX3EF6.js.map} +1 -1
  27. package/dist/{chunk-C74LITXE.js → chunk-B3XWUX3A.js} +2 -2
  28. package/dist/{chunk-C74LITXE.js.map → chunk-B3XWUX3A.js.map} +1 -1
  29. package/dist/{chunk-F7OCOBTG.js → chunk-CRVDF4A6.js} +4 -1
  30. package/dist/chunk-CRVDF4A6.js.map +1 -0
  31. package/dist/{chunk-TGM424PI.js → chunk-H7IODXGY.js} +3 -3
  32. package/dist/{chunk-TGM424PI.js.map → chunk-H7IODXGY.js.map} +1 -1
  33. package/dist/chunk-KVBRMEPM.js +44 -0
  34. package/dist/chunk-KVBRMEPM.js.map +1 -0
  35. package/dist/{chunk-EN4LDR5Y.js → chunk-NC4CFOID.js} +2 -2
  36. package/dist/{chunk-EN4LDR5Y.js.map → chunk-NC4CFOID.js.map} +1 -1
  37. package/dist/{chunk-5SYE4CCT.js → chunk-PQZPVBP4.js} +25 -18
  38. package/dist/chunk-PQZPVBP4.js.map +1 -0
  39. package/dist/{chunk-NXWE5S57.js → chunk-QJ5VFCW3.js} +12 -3
  40. package/dist/chunk-QJ5VFCW3.js.map +1 -0
  41. package/dist/{chunk-RKTTF3L4.js → chunk-X3TPECWG.js} +1 -1
  42. package/dist/chunk-X3TPECWG.js.map +1 -0
  43. package/dist/{chunk-VY6YRCTV.js → chunk-X3X4KRDK.js} +2 -2
  44. package/dist/{chunk-VY6YRCTV.js.map → chunk-X3X4KRDK.js.map} +1 -1
  45. package/dist/collapsible-panel.js +2 -2
  46. package/dist/empty-state.js +2 -1
  47. package/dist/index.js +10 -10
  48. package/dist/list-action-row.js +1 -1
  49. package/dist/list-item.js +1 -1
  50. package/dist/loading-spinner.d.ts +7 -2
  51. package/dist/loading-spinner.js +2 -1
  52. package/dist/nav-stack.js +1 -1
  53. package/dist/select.d.ts +4 -2
  54. package/dist/select.js +1 -1
  55. package/dist/split-view.js +1 -1
  56. package/dist/styles/app-tab.css +18 -1
  57. package/dist/styles/loading-spinner.css +4 -0
  58. package/dist/styles/select.css +79 -0
  59. package/dist/styles/tab-bar.css +12 -8
  60. package/dist/styles/token-search-field.css +19 -10
  61. package/dist/styles/toolbar-control-group.css +96 -3
  62. package/dist/tab-bar.d.ts +2 -0
  63. package/dist/tab-bar.js +1 -1
  64. package/dist/token-search-field.js +1 -1
  65. package/dist/toolbar-control-group.d.ts +13 -5
  66. package/dist/toolbar-control-group.js +2 -1
  67. package/dist/wire-catalog.js +1 -1
  68. package/dist/wire-scroll-dividers.d.ts +7 -4
  69. package/dist/wire-scroll-dividers.js +1 -1
  70. package/dist/wire-token-search-fields.js +1 -1
  71. package/dist/workbench.js +1 -1
  72. package/docs/accessibility.md +10 -0
  73. package/docs/catalog.md +3 -0
  74. package/docs/component-contract.md +58 -9
  75. package/docs/component-selection.md +22 -6
  76. package/docs/examples/component-catalog-extension.json +1 -0
  77. package/docs/layout.md +6 -0
  78. package/docs/ui-analyzer.md +12 -0
  79. package/docs/ui-doctor.md +15 -3
  80. package/doctor/cli.mjs +5 -0
  81. package/doctor/config.schema.json +2 -0
  82. package/doctor/index.d.mts +3 -0
  83. package/doctor/index.mjs +27 -1
  84. package/llms.txt +15 -5
  85. package/package.json +1 -1
  86. package/dist/chunk-5SYE4CCT.js.map +0 -1
  87. package/dist/chunk-F7OCOBTG.js.map +0 -1
  88. package/dist/chunk-JCV4GXWB.js.map +0 -1
  89. package/dist/chunk-NXWE5S57.js.map +0 -1
  90. package/dist/chunk-RKTTF3L4.js.map +0 -1
  91. package/dist/chunk-ZLWQZUGF.js +0 -27
  92. 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. Set `visibility="compact-only"` for a
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. Reach for
512
- `wa-button` only when you need
513
- a Web Awesome feature; a popup menu is a `PopupMenu`, which renders its own
514
- trigger. Nothing wraps inside a group: an icon sits beside its label on one
515
- row, and a group's controls never break onto a second line — the group sizes
516
- to its content and the enclosing `Toolbar`'s responsive policy relocates whole
517
- groups. For a compact mixed-content group, set `nestedDropdown`; a
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. Set `menuInset` to configure the dropdown menu surface
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. | None; pass a label when the spinner conveys status. | Loading lifecycle and adjacent status copy. | `@kerfjs/ui/loading-spinner` | [Accessibility](./accessibility.md#shared-rules) |
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. Nothing wraps inside a
381
- `ToolbarControlGroup`: an icon sits beside its label on one row and the group
382
- sizes to its content, so a row that does not fit relocates whole groups
383
- through the toolbar's `responsive` policy rather than breaking a group. A
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
@@ -19,6 +19,7 @@
19
19
  "publicTokens": ["--acme-filter-chip-accent"],
20
20
  "uses": ["chip"],
21
21
  "source": "src/filter-chip.tsx",
22
+ "styleSources": ["src/filter-chip.css"],
22
23
  "demoSource": "ux-demo/demos/filter-chip.tsx",
23
24
  "geometry": {
24
25
  "margin": "none",
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
@@ -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 error diagnostics;
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 but do not fail unless their originating preset promotes them to errors. A cached report keeps its original exit code and labels previously run stages `cached`.
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": {
@@ -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. `PopupMenu` items also support nested `submenu`
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. Managed
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@kerfjs/ui",
3
- "version": "5.0.0-beta.68",
3
+ "version": "5.0.0-beta.69",
4
4
  "description": "Accessible, composable UI primitives for kerf applications.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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"]}