@kerfjs/ui 5.0.0-beta.29 → 5.0.0-beta.30
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 +43 -8
- package/ai/application-ui-diagnostic-ids-v1.json +5 -0
- package/ai/compile-time-contracts-v1.json +37 -0
- package/ai/compile-time-contracts-v1.schema.json +3 -1
- package/ai/component-catalog-v2-overrides.json +198 -1
- package/ai/component-catalog-v2.d.ts +22 -0
- package/ai/component-catalog-v2.json +488 -32
- package/ai/component-catalog-v2.schema.json +51 -0
- package/ai/component-catalog.json +251 -36
- package/ai/component-catalog.schema.json +23 -0
- package/ai/public-api-signatures-v1.md +231 -84
- package/ai/skill.md +32 -10
- package/ai/webawesome-jsx-signatures-v1.md +1 -1
- package/analyzer/index.mjs +247 -5
- package/dist/app-tab.d.ts +3 -2
- package/dist/app-tab.js +2 -1
- package/dist/catalog-resources.d.ts +1 -0
- package/dist/catalog.d.ts +7 -6
- package/dist/catalog.js +8 -7
- package/dist/catalog.js.map +1 -1
- package/dist/{chunk-PX3YSS5C.js → chunk-6NDBE2OK.js} +1 -1
- package/dist/chunk-6NDBE2OK.js.map +1 -0
- package/dist/{chunk-B3A3IVO5.js → chunk-AQFWBG23.js} +3 -3
- package/dist/chunk-AQFWBG23.js.map +1 -0
- package/dist/{chunk-WNWD54GR.js → chunk-B6JYRX3P.js} +4 -2
- package/dist/chunk-B6JYRX3P.js.map +1 -0
- package/dist/{chunk-HP2B2L26.js → chunk-CAB6NQ2T.js} +1 -1
- package/dist/chunk-CAB6NQ2T.js.map +1 -0
- package/dist/{chunk-2NMQW7FX.js → chunk-CK6P5AUK.js} +4 -3
- package/dist/chunk-CK6P5AUK.js.map +1 -0
- package/dist/{chunk-VCE4MPVE.js → chunk-DAO2VFTT.js} +5 -6
- package/dist/chunk-DAO2VFTT.js.map +1 -0
- package/dist/{chunk-LPOMRC4C.js → chunk-DYBPI5O2.js} +1 -1
- package/dist/chunk-DYBPI5O2.js.map +1 -0
- package/dist/{chunk-IJBSZ4NX.js → chunk-E34TE2ZI.js} +4 -3
- package/dist/chunk-E34TE2ZI.js.map +1 -0
- package/dist/{chunk-4EF2CMYS.js → chunk-I45QO2IZ.js} +8 -7
- package/dist/chunk-I45QO2IZ.js.map +1 -0
- package/dist/{chunk-JRKSK2HX.js → chunk-I4V3UDSA.js} +1 -1
- package/dist/chunk-I4V3UDSA.js.map +1 -0
- package/dist/{chunk-FEY65TBV.js → chunk-KFUUFNTS.js} +1 -1
- package/dist/chunk-KFUUFNTS.js.map +1 -0
- package/dist/{chunk-QO55FN2Y.js → chunk-MFR2IIFG.js} +1 -1
- package/dist/chunk-MFR2IIFG.js.map +1 -0
- package/dist/{chunk-GWEAPEZI.js → chunk-OC275HHS.js} +4 -3
- package/dist/chunk-OC275HHS.js.map +1 -0
- package/dist/chunk-OL4TC3U2.js +156 -0
- package/dist/chunk-OL4TC3U2.js.map +1 -0
- package/dist/{chunk-OT6RPYAP.js → chunk-OMSRSL3B.js} +4 -3
- package/dist/chunk-OMSRSL3B.js.map +1 -0
- package/dist/{chunk-J5BFYY7Q.js → chunk-RL6COPKF.js} +12 -3
- package/dist/chunk-RL6COPKF.js.map +1 -0
- package/dist/{chunk-TB6DY7H5.js → chunk-SOK5EUF7.js} +1 -1
- package/dist/chunk-SOK5EUF7.js.map +1 -0
- package/dist/{chunk-RKVQEH4J.js → chunk-T7DOWS6T.js} +3 -3
- package/dist/chunk-T7DOWS6T.js.map +1 -0
- package/dist/{chunk-7KEUJIIC.js → chunk-TIGSG6BX.js} +1 -1
- package/dist/chunk-TIGSG6BX.js.map +1 -0
- package/dist/{chunk-W5L2JSBY.js → chunk-UDHHKZWO.js} +6 -5
- package/dist/chunk-UDHHKZWO.js.map +1 -0
- package/dist/{chunk-R7ZWUN64.js → chunk-V6GQKOX2.js} +4 -3
- package/dist/chunk-V6GQKOX2.js.map +1 -0
- package/dist/{chunk-KWWR5VMS.js → chunk-V6HCTCVD.js} +4 -3
- package/dist/chunk-V6HCTCVD.js.map +1 -0
- package/dist/{chunk-JFGXQBIP.js → chunk-VDUJZF5G.js} +76 -13
- package/dist/chunk-VDUJZF5G.js.map +1 -0
- package/dist/{chunk-SYDMYBPG.js → chunk-VXFGMDA7.js} +1 -1
- package/dist/chunk-VXFGMDA7.js.map +1 -0
- package/dist/{chunk-YMKH5XTF.js → chunk-WN52USHV.js} +2 -2
- package/dist/{chunk-YMKH5XTF.js.map → chunk-WN52USHV.js.map} +1 -1
- package/dist/{chunk-OBLTAKSX.js → chunk-WSXUPSID.js} +1 -1
- package/dist/chunk-WSXUPSID.js.map +1 -0
- package/dist/{chunk-X2U3QJCJ.js → chunk-ZPWZIVI6.js} +1 -1
- package/dist/chunk-ZPWZIVI6.js.map +1 -0
- package/dist/collapsible-panel.d.ts +5 -2
- package/dist/collapsible-panel.js.map +1 -1
- package/dist/css-values.d.ts +75 -0
- package/dist/css-values.js +3 -0
- package/dist/css-values.js.map +1 -0
- package/dist/empty-state.d.ts +2 -1
- package/dist/empty-state.js +1 -1
- package/dist/floating-toolbar.d.ts +4 -3
- package/dist/floating-toolbar.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +26 -25
- package/dist/list-action-row.d.ts +1 -2
- package/dist/list-action-row.js +2 -1
- package/dist/list-header.d.ts +1 -1
- package/dist/list-header.js +2 -1
- package/dist/list-inset-control.d.ts +4 -3
- package/dist/list-inset-control.js +1 -1
- package/dist/list-inset-text.d.ts +4 -3
- package/dist/list-inset-text.js +1 -1
- package/dist/list-item.d.ts +4 -3
- package/dist/list-item.js +2 -1
- package/dist/list.d.ts +10 -8
- package/dist/list.js +2 -1
- package/dist/nav-stack.d.ts +9 -6
- package/dist/nav-stack.js +2 -1
- package/dist/pane.d.ts +6 -5
- package/dist/pane.js +1 -1
- package/dist/resizable-region.d.ts +2 -1
- package/dist/resizable-region.js +1 -1
- package/dist/segmented-control.d.ts +4 -3
- package/dist/segmented-control.js +2 -1
- package/dist/select.d.ts +3 -1
- package/dist/select.js +2 -1
- package/dist/semantic-content-BbzjvSu9.d.ts +14 -0
- package/dist/skeleton.d.ts +7 -6
- package/dist/skeleton.js +2 -1
- package/dist/split-view.d.ts +5 -4
- package/dist/split-view.js +3 -2
- package/dist/split-view.js.map +1 -1
- package/dist/state-banner.d.ts +3 -2
- package/dist/state-banner.js +2 -1
- package/dist/styles/catalog.css +8 -1
- package/dist/styles/collapsible-panel.css +6 -2
- package/dist/styles/foundation.css +44 -0
- package/dist/styles/list-header.css +6 -0
- package/dist/styles/list-item.css +9 -0
- package/dist/styles/nav-stack.css +32 -2
- package/dist/styles/segmented-control.css +8 -1
- package/dist/styles/select.css +2 -1
- package/dist/styles/state-banner.css +14 -0
- package/dist/styles/tab-scaffold.css +1 -0
- package/dist/styles/token-search-field.css +4 -0
- package/dist/styles/toolbar-control-group.css +98 -16
- package/dist/styles/webawesome.css +22 -0
- package/dist/styles/workbench.css +4 -0
- package/dist/sunken-panel.d.ts +4 -3
- package/dist/sunken-panel.js +1 -1
- package/dist/surface-scaffold.d.ts +6 -5
- package/dist/surface-scaffold.js +1 -1
- package/dist/tab-bar.d.ts +6 -5
- package/dist/tab-bar.js +1 -1
- package/dist/tab-scaffold.d.ts +2 -1
- package/dist/tab-scaffold.js.map +1 -1
- package/dist/token-search-field.d.ts +5 -4
- package/dist/token-search-field.js +1 -1
- package/dist/toolbar-control-group.d.ts +10 -4
- package/dist/toolbar-control-group.js +1 -1
- package/dist/toolbar-text.js +2 -1
- package/dist/toolbar.d.ts +6 -5
- package/dist/toolbar.js +1 -1
- package/dist/value-table.d.ts +4 -2
- package/dist/value-table.js +2 -1
- package/dist/wire-nav-stack.d.ts +4 -1
- package/dist/wire-nav-stack.js +125 -0
- package/dist/wire-nav-stack.js.map +1 -1
- package/dist/wire-resizable-regions.js +1 -1
- package/dist/wire-tab-bars.d.ts +1 -0
- package/dist/wire-token-search-fields.js +1 -1
- package/dist/workbench.d.ts +7 -4
- package/dist/workbench.js.map +1 -1
- package/docs/accessibility.md +9 -3
- package/docs/collapsible-panel.md +4 -1
- package/docs/component-contract.md +56 -8
- package/docs/component-selection.md +45 -8
- package/docs/design/templates/list/compact-dark.svg +1 -1
- package/docs/design/templates/list/compact.svg +1 -1
- package/docs/design/templates/list/stack-dark.svg +1 -1
- package/docs/design/templates/list/stack.svg +1 -1
- package/docs/design/templates/list-action-row/default-dark.svg +1 -1
- package/docs/design/templates/list-action-row/default.svg +1 -1
- package/docs/design/templates/list-action-row/selected-dark.svg +1 -1
- package/docs/design/templates/list-action-row/selected.svg +1 -1
- package/docs/design/templates/list-action-row-dark.svg +1 -1
- package/docs/design/templates/list-action-row.svg +1 -1
- package/docs/design/templates/list-dark.svg +1 -1
- package/docs/design/templates/list-item/default-dark.svg +1 -1
- package/docs/design/templates/list-item/default.svg +1 -1
- package/docs/design/templates/list-item/multiline-dark.svg +1 -1
- package/docs/design/templates/list-item/multiline.svg +1 -1
- package/docs/design/templates/list-item/selected-dark.svg +1 -1
- package/docs/design/templates/list-item/selected.svg +1 -1
- package/docs/design/templates/list-item/trailing-dark.svg +1 -1
- package/docs/design/templates/list-item/trailing.svg +1 -1
- package/docs/design/templates/list-item-dark.svg +1 -1
- package/docs/design/templates/list-item.svg +1 -1
- package/docs/design/templates/list.svg +1 -1
- package/docs/design-philosophy.md +4 -2
- package/docs/examples/component-catalog-extension-v2.json +2 -0
- package/docs/layout.md +12 -4
- package/docs/nav-stack.md +29 -8
- package/docs/recipes.md +8 -4
- package/docs/split-view.md +6 -0
- package/docs/surface-scaffold.md +8 -0
- package/docs/tab-scaffold.md +3 -1
- package/docs/type-contracts.md +39 -12
- package/docs/ui-analyzer.md +17 -3
- package/docs/ui-doctor.md +7 -0
- package/docs/ux-demo.md +10 -4
- package/docs/webawesome-theme.md +6 -0
- package/docs/workbench.md +6 -4
- package/llms.txt +46 -6
- package/package.json +5 -1
- package/ux-demo/recipes/compact-toolbar.tsx +10 -9
- package/ux-demo/recipes/composer-form.tsx +6 -3
- package/ux-demo/recipes/loading-inspector.tsx +2 -1
- package/dist/chunk-2NMQW7FX.js.map +0 -1
- package/dist/chunk-4EF2CMYS.js.map +0 -1
- package/dist/chunk-7KEUJIIC.js.map +0 -1
- package/dist/chunk-B3A3IVO5.js.map +0 -1
- package/dist/chunk-FEY65TBV.js.map +0 -1
- package/dist/chunk-GWEAPEZI.js.map +0 -1
- package/dist/chunk-HP2B2L26.js.map +0 -1
- package/dist/chunk-IJBSZ4NX.js.map +0 -1
- package/dist/chunk-J5BFYY7Q.js.map +0 -1
- package/dist/chunk-JFGXQBIP.js.map +0 -1
- package/dist/chunk-JRKSK2HX.js.map +0 -1
- package/dist/chunk-KWWR5VMS.js.map +0 -1
- package/dist/chunk-LPOMRC4C.js.map +0 -1
- package/dist/chunk-OBLTAKSX.js.map +0 -1
- package/dist/chunk-OT6RPYAP.js.map +0 -1
- package/dist/chunk-PX3YSS5C.js.map +0 -1
- package/dist/chunk-QO55FN2Y.js.map +0 -1
- package/dist/chunk-R7ZWUN64.js.map +0 -1
- package/dist/chunk-RKVQEH4J.js.map +0 -1
- package/dist/chunk-SYDMYBPG.js.map +0 -1
- package/dist/chunk-TB6DY7H5.js.map +0 -1
- package/dist/chunk-VCE4MPVE.js.map +0 -1
- package/dist/chunk-W5L2JSBY.js.map +0 -1
- package/dist/chunk-WNWD54GR.js.map +0 -1
- package/dist/chunk-X2U3QJCJ.js.map +0 -1
package/ai/skill.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: kerf-ui
|
|
3
3
|
description: Build interfaces with kerfjs and the @kerfjs/ui production component package. Use whenever code imports @kerfjs/ui or a task asks for Kerf UI components.
|
|
4
|
-
kerf-ui-skill-version: 1.
|
|
4
|
+
kerf-ui-skill-version: 1.41.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Building with @kerfjs/ui
|
|
@@ -39,6 +39,18 @@ composition. Join catalogs by `key` (`package:id`), never bare `id`. Treat
|
|
|
39
39
|
as a prohibition. Emit a diagnostic only after mechanically establishing its
|
|
40
40
|
exact `when` condition; subjective guidance stays prose. App entries use the
|
|
41
41
|
v2 extension schema and retain their own package identity across Kerf edges.
|
|
42
|
+
Treat a zone's optional `jsx.prop` as its only authoritative JSX binding; never
|
|
43
|
+
assume the zone id itself is a prop. `children` carries one homogeneous primary
|
|
44
|
+
region. Semantic positions and replacement content use explicit named content
|
|
45
|
+
props (`header`, `footer`, `leading`, `trailing`, `action`, and similar). Multi-
|
|
46
|
+
content positions use the recursive `KerfUiContent` type: `SafeHtml`, runtime-
|
|
47
|
+
empty boolean/nullish values, and readonly nested arrays. Put conditional
|
|
48
|
+
components and mapped component arrays directly in these positions; do not add
|
|
49
|
+
a `Fragment` merely to satisfy types. Arbitrary strings, numbers, and signals
|
|
50
|
+
are rejected unless a component explicitly exposes a text position such as
|
|
51
|
+
`ListInsetText`. These are typed props, not native `<slot>` elements,
|
|
52
|
+
wrapper slot components, or a generic `slots` object. Preserve dynamic
|
|
53
|
+
expressions as unknown when their content cannot be established statically.
|
|
42
54
|
|
|
43
55
|
Before selecting for an application, call the Node-side discovery API in
|
|
44
56
|
`./application-ui-profile.mjs` (or implement its documented filename/order)
|
|
@@ -65,7 +77,7 @@ Quick routing:
|
|
|
65
77
|
|
|
66
78
|
| Need | Choose | Nearest alternatives / boundary |
|
|
67
79
|
| ------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
68
|
-
| Navigation row | `ListItem` | Use an ordinary link or button when sidebar/menu anatomy and state do not apply. Multiline leading icons stay aligned with the first text line.
|
|
80
|
+
| Navigation row | `ListItem` | Use an ordinary link or button when sidebar/menu anatomy and state do not apply. Its rounded root clips overflowing descendants; set `multiline` when the primary label should wrap. Multiline leading icons stay aligned with the first text line. |
|
|
69
81
|
| Navigation row with a trailing action | `ListActionRow` | Use `ListItem` when the trailing region is dormant; never put controls inside either component's SafeHtml slots. Multiline leading icons stay aligned with the first text line. |
|
|
70
82
|
| Page, panel, or dialog heading | `Toolbar` + `ToolbarText` | Use a direct extra-large `ToolbarText`, optional grouped icon, grouped trailing controls, and app-owned supporting copy below. |
|
|
71
83
|
| Exclusive choice | `TabBar`, `SegmentedControl`, or `Select` | Tabs switch tabpanels; segments expose a few choices; Select handles a longer value list. |
|
|
@@ -90,30 +102,39 @@ workflows and `--fail-on-review` when the project requires a zero-review budget.
|
|
|
90
102
|
Do not suppress a finding in source; add only a narrowly targeted, justified
|
|
91
103
|
profile exception when the deviation is deliberate.
|
|
92
104
|
|
|
105
|
+
For every CSS-adjacent component prop, read that entry's `cssValueProps`
|
|
106
|
+
contract. Choose a canonical shorthand first, then an accepted typed helper.
|
|
107
|
+
Never substitute an arbitrary string, a helper from another grammar, or an
|
|
108
|
+
expression-only helper such as `plus()` without its cataloged composer. Treat
|
|
109
|
+
`KUI-L013`–`KUI-L016` as required repairs; `KUI-L017` is a deliberate off-scale
|
|
110
|
+
choice that should remain only when application context justifies it. Apply the
|
|
111
|
+
same metadata when authoring consumer component catalogs so lint, analyzer, and
|
|
112
|
+
doctor feedback covers local components too.
|
|
113
|
+
|
|
93
114
|
Hard rules:
|
|
94
115
|
|
|
95
116
|
1. Import visual components from explicit JavaScript subpaths. In CSS-aware browser builds each subpath brings in its own reachable CSS, including UI subcomponents, while unrelated CSS remains out. The root barrel and `@kerfjs/ui/unstyled` are CSS-free; pair the root barrel with `styles.css` only when the complete layer is intentional. Manual CSS subpaths remain available for custom pipelines.
|
|
96
117
|
2. Components return Kerf `SafeHtml`. Never pass DOM nodes as children or use inline JSX event handlers.
|
|
97
118
|
3. Keep state, product copy, persistence, and domain mappings in the application. Do not add product-specific actions or fields to a generic component. Put ListItem/ListActionRow/ListHeader/AppTab domain `data-*` metadata in `rootAttributes`; use ListActionRow `trailingActionAttributes` and ListHeader `triggerAttributes` only for domain `data-*` or native popover target/action and `aria-controls`/`aria-haspopup`. These slots do not replace component-owned action, item/tab identity, selection, disclosure, naming, disabled, icon, or role semantics.
|
|
98
119
|
4. Wire `data-action` hooks from one stable root with `delegate()` or `delegateActions()` and retain disposers.
|
|
99
|
-
5. Use the opinionated `--kui-color-*` semantic ramps and component-level override properties. Override tokens at the narrowest useful scope and prefer equivalent props/tokens. Public-class-to-public-class selectors are supported when every Kerf class appears in the catalog entry's `publicClasses`; never target descendant tags, ids, attribute-only anatomy, or unlisted implementation classes.
|
|
120
|
+
5. Use the opinionated `--kui-color-*` semantic ramps and component-level override properties. Use `pop` only for attractive non-status emphasis such as featured, novel, or celebratory content; never substitute it for info, success, warning, or danger. Override tokens at the narrowest useful scope and prefer equivalent props/tokens. Public-class-to-public-class selectors are supported when every Kerf class appears in the catalog entry's `publicClasses`; never target descendant tags, ids, attribute-only anatomy, or unlisted implementation classes.
|
|
100
121
|
6. `Select` is pure until the app explicitly imports `@kerfjs/ui/select/register`; do not import Web Awesome's full registration bundle. When an app writes direct `wa-*` JSX, add `import type {} from '@kerfjs/ui/webawesome'` for the catalog-supported intrinsic-element declarations, import the CSS-only `@kerfjs/ui/webawesome.css` theme once, and keep importing individual Web Awesome component modules so their JavaScript remains tree-shakeable. The type boundary emits no code and registers nothing. Pass icon-bearing `choices` and `renderSelected` content normally: `Select` preserves its slotted option icons across Kerf rerenders and keys selected content by the controlled value, so app wrappers must not add competing morph-control attributes. Give it a visible `label` or an `ariaLabel` for a visually hidden name; the nonempty visible label takes precedence and the package names the actual shadow combobox, including `renderSelected`, without adding label geometry. Use `hint` for persistent supporting text below the control and `placeholderText` only for the empty value inside the closed control; loading placeholders retain the hint. When testing name or description, target the element with the `combobox` role rather than the `wa-select` wrapper. Web Awesome's hint attribute and explicit hint slot both describe that shadow combobox across supported engines; do not add an `aria-description` mirror or application shadow-DOM patch.
|
|
101
122
|
7. Decorative icons are hidden; controls are named; focus is visible; state never relies on color alone; reduced motion and increased contrast remain usable. `DisclosureArrow` defaults to an 18px root-scaled visual and exposes `--kui-disclosure-arrow-size` for consumer sizing; it never becomes the interaction or accessible-name owner. Put it in an owning native control with a stable accessible name and controlled `aria-expanded`. `ListHeader` toggle mode supplies it when `actionIcon` is omitted, but the app must reveal matching content; a custom icon replaces it. Author replacement icon content facing right before its configured direction transform is applied. Direction changes take the shortest rotation path, with counterclockwise chosen for a 180-degree closed-to-open tie. Kerf `Select` separately keeps its intrinsic Web Awesome expand glyph at `--kui-disclosure-icon-scale: .5`.
|
|
102
123
|
8. `ResizableRegion` uses `wireResizableRegions()` for Arrow, Shift+Arrow, Home/End, and pointer behavior. The app owns size persistence. `handleIcon` replaces decorative dormant glyph content only. For compact overlays, set `presentation="overlay"` and the overlay maximum variables; Kerf clamps both the region track and its fixed-size animated content, so do not add descendant width/height overrides.
|
|
103
|
-
9. Use controlled `SegmentedControl` for a small exclusive choice set. Select `appearance="toolbar"` when nesting it inside `ToolbarControlGroup`; use rounded or pill shapes for standalone contexts. Handle its action, update `value`, keep meaningful choice labels, and preserve every enabled native button in sequential Tab order.
|
|
124
|
+
9. Use controlled `SegmentedControl` for a small exclusive choice set. Select `appearance="toolbar"` when nesting it inside `ToolbarControlGroup`; use rounded or pill shapes for standalone contexts. The package derives inset hover/selection radii from the owning outer shape through `--kui-layout-highlight-inset`; do not restyle an inner highlight to an independent radius. Handle its action, update `value`, keep meaningful choice labels, and preserve every enabled native button in sequential Tab order.
|
|
104
125
|
10. Compose `AppTab` inside controlled `TabBar`; call `wireTabBars()` once and retain its disposer. It owns same-bar drag mechanics, including proximity-based horizontal edge autoscroll, while the app applies `onReorder` and owns order, selection, panels, close policy, routing, and persistence. Arrow/Home/End select on move by default and restore focus by bar/tab identity when controlled activation replaces the strip; pass `activation: 'manual'` (or `TabBar activation="manual"` per strip) so arrow keys move focus only and the user selects with Enter/Space/click — use it when selecting a tab is a heavy action. Use runtime-filtered `rootAttributes` for domain metadata and keep an optional `closeIcon` decorative and noninteractive.
|
|
105
|
-
11. Use token-controlled `TokenSearchField` when free text and removable structured filters share one editor. Editable text stays DOM-owned between token changes. The leading icon, first text line, clear action, and trailing slot share one fixed row when content wraps. The app owns parsing and suggestions; call `readTokenSearchField()` on input, empty `textContent` on clear, use `placeTokenSearchCaret()` after explicit controlled focus changes, and call `wireTokenSearchFields()` once so Enter submits without inserting a line break and keyboard chip deletion restores focus plus the text-relative caret after controlled replacement. Enable `collapsible` for an animated iconic closed state, standalone or inside `ToolbarControlGroup`; the field keeps text or tokens expanded, and `wireTokenSearchFields()` manages the transient expand/collapse/focus by default (activate to reveal + focus, Escape or empty blur to collapse). Bind the field's `expanded` to the returned handle's `expanded(id)` signal or adopt your own via `collapsible.signals`; opt a behavior out only when the app must own it. Do not hand-roll the open handler, the focusout collapse, a clear-button mousedown guard, or reopening after controlled clear or Select All deletion — the helper keeps the replacement editor open and focused.
|
|
126
|
+
11. Use token-controlled `TokenSearchField` when free text and removable structured filters share one editor. Editable text stays DOM-owned between token changes. The leading icon, first text line, clear action, and trailing slot share one fixed row when content wraps. The app owns parsing and suggestions; call `readTokenSearchField()` on input, empty `textContent` on clear, use `placeTokenSearchCaret()` after explicit controlled focus changes, and call `wireTokenSearchFields()` once so Enter submits without inserting a line break and keyboard chip deletion restores focus plus the text-relative caret after controlled replacement. Enable `collapsible` for an animated iconic closed state, standalone or inside `ToolbarControlGroup`; the field keeps text or tokens expanded, and `wireTokenSearchFields()` manages the transient expand/collapse/focus by default (activate to reveal + focus, Escape or empty blur to collapse). Bind the field's `expanded` to the returned handle's `expanded(id)` signal or adopt your own via `collapsible.signals`; opt a behavior out only when the app must own it. Do not hand-roll the open handler, the focusout collapse, a clear-button mousedown guard, or reopening after controlled clear or Select All deletion — the helper keeps the replacement editor open and focused. Clear and deletion restoration finish at the replacement mutation checkpoint before the next input task, so shortcut-letter typing cannot escape to the page and delayed animation frames cannot overwrite a later selection. Persist both query and tokens returned by the DOM read. For real-app adoption it also provides a focusout keep-open exception (`data-token-search-keep-open` regions or `collapsible.keepOpenOn(target)`, so a sibling suggestions/date/help surface does not collapse an empty field), opt-in atomic-chip keyboard (`keyboard`: Backspace/Delete remove the adjacent chip via `onRemoveToken`, ArrowRight moves the caret past a trailing chip), and an `onEdit({id, editor, event})` input callback (the `InputEvent` lets you gate on `inputType`/`data`) — reach for these instead of re-adding hand-rolled keydown/input handlers around the field.
|
|
106
127
|
12. Demo work uses public production component subpaths and their browser-selected CSS. Give every public visual component its own category-grouped catalog route; list themed third-party components under a clearly labeled collapsible ecosystem section, with a focused route for each. For the complete focused/composition decision, `CatalogExample` / `CatalogExampleStack` row/group nesting, exact specimen selection, geometry-overlay legend and exclusions, skip-marker behavior, and metadata ownership, follow the single [Catalog demo authoring contract](../docs/catalog.md#catalog-demo-authoring-contract), discovered machine-readably through [`catalog-authoring.json`](./catalog-authoring.json). Never infer structure from private `kui-catalog-*` classes. Project each component's deterministic repository-relative demo source and existing documentation path so the detail can expose `View demo source` and `Read guidance` links without a runtime export; also derive first-party component implementation paths from their canonical browser imports, and label Web Awesome documentation as Kerf integration guidance. Declare direct `uses` relationships so `Used by` stays derivable, and theme shell chrome through the same semantic tokens as the stage instead of drawing a substitute. Enable `wireCatalog({ revealSelection: true })` for long desktop sidebars so controlled selections remain visible without moving focus; keep its default compact media guard unless compact scrolling is an explicit product behavior, and use `revealCatalogEntry` for an initial deep link.
|
|
107
|
-
13. The Web Awesome theme makes Tooltip and Popover arrowless by default. Keep that default unless a pointer materially clarifies the anchor; opt back in with `--wa-tooltip-arrow-size`, `--kui-wa-popover-arrow-size`, or a popover's public `--arrow-size`, and use `without-arrow` when local no-arrow intent should survive theme changes. Its non-field chrome uses `--kui-wa-control-inset` (8px controls/items), `--kui-wa-surface-margin` / `--kui-wa-surface-inset` (8px around/inside Accordion, Card, Details, Callout, and Include), and `--kui-wa-container-inset` (16px Tab Panel content); Dialog maps its body to the 8px surface tier and its footer to the 16px container tier independently of `--spacing`. Override those tiers instead of restyling individual parts. Checkbox and Radio Group option regions, the Color Picker trigger, and Slider's complete interactive region receive the shared 8px logical inline outer inset because those controls have no bordered field shell. Known Date field captions and bordered text-like field hints align with values at the 9px border-plus-padding inset. OTP Input's label uses the same uppercase xs/650 treatment as other field labels, and both its label and hint use that 9px inset.
|
|
128
|
+
13. The Web Awesome theme makes Tooltip and Popover arrowless by default. Keep that default unless a pointer materially clarifies the anchor; opt back in with `--wa-tooltip-arrow-size`, `--kui-wa-popover-arrow-size`, or a popover's public `--arrow-size`, and use `without-arrow` when local no-arrow intent should survive theme changes. Its non-field chrome uses `--kui-wa-control-inset` (8px controls/items), `--kui-wa-surface-margin` / `--kui-wa-surface-inset` (8px around/inside Accordion, Card, Details, Callout, and Include), and `--kui-wa-container-inset` (16px Tab Panel content); Dialog maps its body to the 8px surface tier and its footer to the 16px container tier independently of `--spacing`. Override those tiers instead of restyling individual parts. When a dialog provides another dismissal affordance, put `hide-actions` on the `wa-dialog` host to hide its directly exported `header-actions` part; never chain `::part()` selectors through internal parts. Checkbox and Radio Group option regions, the Color Picker trigger, and Slider's complete interactive region receive the shared 8px logical inline outer inset because those controls have no bordered field shell. Known Date field captions and bordered text-like field hints align with values at the 9px border-plus-padding inset. OTP Input's label uses the same uppercase xs/650 treatment as other field labels, and both its label and hint use that 9px inset.
|
|
108
129
|
14. Treat the complete Web Awesome catalog as support coverage, not a recommendation list. Its UX sidebar marks superseded and exceptional entries `Discouraged`. Consider Popup when it replaces custom anchored positioning. Prefer Kerf `Select` over direct Dropdown/Dropdown Item/Select/Option composition, `SegmentedControl` over Button Group, `TabBar` or `SegmentedControl` over Web Awesome Tabs, `LucideIcon` over Web Awesome Icon, and `ResizableRegion` over Split Panel. Use Tree/Tree Item, Animated Image, and Comparison only for a specific required behavior; avoid Zoomable Frame.
|
|
109
|
-
15. Build sidebars, main areas, inspectors, and dialogs from `@kerfjs/ui/layout.css`: an unpadded `.kui-pane`, optional `.kui-pane__toolbar`, one scrolling `.kui-pane__content`, and optional `.kui-pane__footer`. Use `List` instead of a hand-written flex-column wrapper when rows or sections need stretch alignment, standard/custom gap, flex growth, one vertical scroll owner, or physical-edge dividers; `dividerSides` uses canonical `t`/`r`/`b`/`l` order. Add `.kui-content` for 24px major vertical separation and `.kui-content-item` for a child-owned 8px inline margin, 1px transparent-or-visible border, 8px padding, and 12px radius. Use the pill modifier for 22px. Do not pad pane shells, duplicate item geometry in wrappers, or create nested scroll owners.
|
|
130
|
+
15. Build sidebars, main areas, inspectors, and dialogs from `@kerfjs/ui/layout.css`: an unpadded `.kui-pane`, optional `.kui-pane__toolbar`, one scrolling `.kui-pane__content`, and optional `.kui-pane__footer`. Use `List` instead of a hand-written flex-column wrapper when rows or sections need stretch alignment, standard/custom gap, flex growth, one vertical scroll owner, or physical-edge dividers; `dividerSides` uses canonical `t`/`r`/`b`/`l` order. A dialog body is generally a `List`: set `DialogSurface bodyInset="none"` when its list children own geometry, and wrap bare dialog prose in `ListInsetText` so it aligns with bordered siblings without a second inset. Add `.kui-content` for 24px major vertical separation and `.kui-content-item` for a child-owned 8px inline margin, 1px transparent-or-visible border, 8px padding, and 12px radius. Use the pill modifier for 22px. Do not pad pane shells, duplicate item geometry in wrappers, or create nested scroll owners.
|
|
110
131
|
16. Keep a visible collapsible pane's collapse control in its own toolbar. When hidden, move its restore control into the adjacent main toolbar on the corresponding logical edge: leading for an inline-start sidebar and trailing for an inline-end inspector. Collapse the pane completely; do not preserve an empty icon-only rail.
|
|
111
|
-
17. The only direct children of a `Toolbar` zone (`leading`/`center`/`trailing`) are `ToolbarText` (identity/title text) and `ToolbarControlGroup`; never drop bare buttons, inputs, links, or arbitrary markup straight into a zone. Put identity/title text directly in the zone as `ToolbarText`; wrap interactive controls and icon tiles in `ToolbarControlGroup`. `SegmentedControl`, `Select`, a collapsible `TokenSearchField`, and Web Awesome controls all live inside a group. For an ordinary icon/action control inside a group, use a plain `<button>` (the group styles `> button` fully) — that is the default; reach for `wa-button` only when you need a Web Awesome feature, chiefly a `wa-dropdown` popup trigger. A popup menu in a toolbar is a `single` group wrapping a `wa-dropdown` whose `slot="trigger"` `wa-button` is the toolbar button and whose `wa-dropdown-item`s are the menu, with the dropdown kept under `data-morph-skip-children`. Compose panel/dialog/page headings with a direct extra-large `ToolbarText`, optional grouped icon, grouped trailing controls, and app-owned supporting copy below. A group remains 44px outside (`calc(2px + remify(42px))`) when its border/background are transparent; use 8px between groups and inside items. Split dormant and interactive regions: `ListHeader` fills the available inline width and keeps its label and mutually exclusive semantic count or non-count `badge` together, with an independent logical-end 44px action unless disclosure mode makes the title cluster the button. Its action visual defaults to 18px through `--kui-list-header-action-icon-size`; never shrink the target to match it. Pass every non-negative safe-integer section quantity through `count` with a localized full spoken `countLabel`; never concatenate it into `label` or put a number in `badge`. `ListItem.trailing` is dormant; use `ListActionRow` when primary and trailing actions need sibling 44px native buttons. Its `label`, `icon`, and `trailingActionIcon` slots are also dormant and cannot contain controls. Let panes relocate at narrow widths instead of shrinking targets.
|
|
132
|
+
17. The only direct children of a `Toolbar` zone (`leading`/`center`/`trailing`) are `ToolbarText` (identity/title text) and `ToolbarControlGroup`; never drop bare buttons, inputs, links, or arbitrary markup straight into a zone. Put identity/title text directly in the zone as `ToolbarText`; wrap interactive controls and icon tiles in `ToolbarControlGroup`. `SegmentedControl`, `Select`, a collapsible `TokenSearchField`, and Web Awesome controls all live inside a group. For an ordinary icon/action control inside a group, use a plain `<button>` (the group styles `> button` fully) — that is the default; reach for `wa-button` only when you need a Web Awesome feature, chiefly a `wa-dropdown` popup trigger. A popup menu in a toolbar is a `single` group wrapping a `wa-dropdown` whose `slot="trigger"` `wa-button` is the toolbar button and whose `wa-dropdown-item`s are the menu, with the dropdown kept under `data-morph-skip-children`. For an avatar control, set `content="avatar"` and `avatarImage` on the group instead of inserting an `img`: a lone control paints the contained image on the outer group and a multi-button group paints it only on the pressed selection highlight. Compose panel/dialog/page headings with a direct extra-large `ToolbarText`, optional grouped icon, grouped trailing controls, and app-owned supporting copy below. A group remains 44px outside (`calc(2px + remify(42px))`) when its border/background are transparent; use 8px between groups and inside items. Split dormant and interactive regions: `ListHeader` fills the available inline width and keeps its label and mutually exclusive semantic count or non-count `badge` together, with an independent logical-end 44px action unless disclosure mode makes the title cluster the button. Its action visual defaults to 18px through `--kui-list-header-action-icon-size`; never shrink the target to match it. Pass every non-negative safe-integer section quantity through `count` with a localized full spoken `countLabel`; never concatenate it into `label` or put a number in `badge`. `ListItem.trailing` is dormant; use `ListActionRow` when primary and trailing actions need sibling 44px native buttons. Its `label`, `icon`, and `trailingActionIcon` slots are also dormant and cannot contain controls. Let panes relocate at narrow widths instead of shrinking targets.
|
|
112
133
|
18. When a recurring concept has no matching export or production recipe, keep its semantics in a thin application adapter while reusing the public layout vocabulary. The composer recipe uses one visible form surface, toolbar title/supporting-copy ids, shared 8px field/action gutters, 24px major rhythm, and a conditional StateBanner as its only nested semantic surface; do not turn every section into a card or double-inset intrinsically bordered controls. The application-local `../docs/examples/command-palette-adapter.tsx` is reference source for one such missing concept, not an `@kerfjs/ui` runtime export or catalog recipe. The application owns its registration, ranking, history, permissions, availability, shortcut policy, focus policy, dispatch, and copy. If a missing concept recurs across products, open an upstream component or recipe request.
|
|
113
134
|
19. Compose `ValueTable` from typed `ValueTableRow` entries instead of handwritten `dt`/`dd` wrappers. Pass `icon` for the optional 24px leading visual; the row owns 8px of root-scaled top and bottom padding, the 8px iconless or 40px icon-bearing separator start, and the common 8px right inset.
|
|
114
135
|
20. Use the standard direct `Toolbar` composition for a panel, dialog, or page heading, not a private wrapper or custom heading row. Put an optional icon in a `ToolbarControlGroup`, the title directly in the leading zone as extra-large `ToolbarText`, and actions in a trailing group. Set `headingLevel` for page/section landmarks. Supporting copy stays below the toolbar as app-owned content. The app owns modal behavior, focus, dismissal, command policy, ids, and action handling.
|
|
115
|
-
21. Space with the official five-step scale, picked by how connected two elements are — not by eye. `0` `--kui-space-none` = no separation (one unit); `4px` `--kui-space-2xs` = very minor air on a connected cluster; `8px` `--kui-space-xs` = standard, between elements within a group; `16px` `--kui-space-m` = minor, between homogeneous groups; `24px` `--kui-space-l` = major, between heterogeneous groups (the `.kui-content` rhythm). The 8px-vs-24px distinction is inside-a-group vs between-major-differing-regions. `--kui-space-s` (12px) and `--kui-space-xl` (32px) are off-scale exceptions
|
|
116
|
-
22. Pick a whole-screen layout from the opt-in, tree-shakeable subpaths by data + interaction + device, and derive responsiveness from `@kerfjs/ui/device-class`'s `deviceClass()` (`compact` = handset or portrait tablet = one pane at a time). Simple/flat → `NavStack` with one entry (single pane), plus `TabScaffold` (`@kerfjs/ui/tab-scaffold`, iOS bottom tabs, each tab its own `NavStack`) for 2–5 co-equal sections on `compact`. Drill-down → `NavStack` (`@kerfjs/ui/nav-stack`), upgrading to `SplitView` (`@kerfjs/ui/split-view`, list-detail) once both panes fit (`atLeast('tablet')` landscape); `SplitView` collapses to a `NavStack` on `compact`. Complex tool with peripheral panels → `Workbench` (`@kerfjs/ui/workbench`, collapsible rails + drawer) `atLeast('desktop')`, degrading to `NavStack`/overlays below. These are declarative (the app owns the stack/selection/collapsed/active state as signals) with disposer-returning `wire…` helpers; each ships a companion CSS import and stays out of the barrel. For a standalone collapsible rail or bottom drawer outside a full shell, use `CollapsiblePanel` + `CollapsiblePanelToggle` (`@kerfjs/ui/collapsible-panel`) with `wireSidebar` (`@kerfjs/ui/wire-sidebar`): the standard collapse animation and per-side icon convention (`PanelLeft*`/`PanelRight*`/`PanelBottom*`) plus toggle, focus, compact overlay, and persistence semantics. Dialogs pick the same inner layout, then present per device class (full-screen modal on `compact`, inline on desktop). See `docs/app-layouts.md` and `docs/collapsible-panel.md`.
|
|
136
|
+
21. Space with the official five-step scale, picked by how connected two elements are — not by eye. `0` `--kui-space-none` = no separation (one unit); `4px` `--kui-space-2xs` = very minor air on a connected cluster; `8px` `--kui-space-xs` = standard, between elements within a group; `16px` `--kui-space-m` = minor, between homogeneous groups; `24px` `--kui-space-l` = major, between heterogeneous groups (the `.kui-content` rhythm). The 8px-vs-24px distinction is inside-a-group vs between-major-differing-regions. `--kui-space-s` (12px) and `--kui-space-xl` (32px) are off-scale exceptions. For `List.gap`, pass a finite shorthand such as `gap="xs"` or `gap="m"`; when one token is insufficient, import typed builders from the CSS-free `@kerfjs/ui/css-values` subpath. Preserve property grammar: lengths for gap/Skeleton geometry, `flex()` for List flex, and `uiColor()`/`colorVar()` for choice icons. Never cast between brands or emit raw row `style` declarations. `plus` is not standalone and must be wrapped in `calc`; `remify()` remains source-CSS syntax and is never a runtime prop value. See `docs/layout.md` "Spacing scale".
|
|
137
|
+
22. Pick a whole-screen layout from the opt-in, tree-shakeable subpaths by data + interaction + device, and derive responsiveness from `@kerfjs/ui/device-class`'s `deviceClass()` (`compact` = handset or portrait tablet = one pane at a time). Simple/flat → `NavStack` with one entry (single pane), plus `TabScaffold` (`@kerfjs/ui/tab-scaffold`, iOS bottom tabs, each tab its own `NavStack`) for 2–5 co-equal sections on `compact`. Drill-down → `NavStack` (`@kerfjs/ui/nav-stack`), upgrading to `SplitView` (`@kerfjs/ui/split-view`, list-detail) once both panes fit (`atLeast('tablet')` landscape); `SplitView` collapses to a `NavStack` on `compact`. Keep each stack's ordered views controlled; put view-specific bottom chrome on `NavStackView.bottomToolbar` (or use the component-level persistent fallback), then call `wireNavStack` so content slides while the active view's top and bottom chrome cross-fade and focus moves into the new top view/restores on pop. Mark a preferred initial heading or control with `data-nav-focus` when DOM order is not sufficient. Complex tool with peripheral panels → `Workbench` (`@kerfjs/ui/workbench`, collapsible rails + drawer) `atLeast('desktop')`, degrading to `NavStack`/overlays below. These are declarative (the app owns the stack/selection/collapsed/active state as signals) with disposer-returning `wire…` helpers; each ships a companion CSS import and stays out of the barrel. For a standalone collapsible rail or bottom drawer outside a full shell, use `CollapsiblePanel` + `CollapsiblePanelToggle` (`@kerfjs/ui/collapsible-panel`) with `wireSidebar` (`@kerfjs/ui/wire-sidebar`): the standard collapse animation and per-side icon convention (`PanelLeft*`/`PanelRight*`/`PanelBottom*`) plus toggle, focus, compact overlay, and persistence semantics. Dialogs pick the same inner layout, then present per device class (full-screen modal on `compact`, inline on desktop). See `docs/app-layouts.md` and `docs/collapsible-panel.md`.
|
|
117
138
|
|
|
118
139
|
Common mistakes:
|
|
119
140
|
|
|
@@ -142,5 +163,6 @@ Common mistakes:
|
|
|
142
163
|
| Force a `width`/`height`/`padding` on a component to size or space it | Let it size to its content and tokens; a forced box leaves a halo or a stretched oval — adjust an icon-size or spacing token, not the box |
|
|
143
164
|
| Wrap a component or region in a card, border, backdrop, or outline to "contain" it | Let it sit on the surface; add a `.kui-content-item` only for a real distinction — hierarchy comes from alignment, spacing, and type first |
|
|
144
165
|
| Add another container's padding on top of a content-item's own margin | Pick one owner of the inset; a pane has no padding and its `.kui-content` children own the 8/1/8 geometry — stacking them double-insets |
|
|
166
|
+
| Put raw prose directly in a padded dialog body | Compose the body as `List`, set `bodyInset="none"` for list-owned geometry, and wrap bare prose in `ListInsetText` |
|
|
145
167
|
| Keep chrome, a label, or a readout that aids no decision | Delete it; every element must help a person decide or act |
|
|
146
168
|
| Override a component's default size or color because it "looks off" | Trust the default (a LucideIcon is 24px) and fix the surrounding layout instead |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Web Awesome JSX signatures for the UI authoring corpus
|
|
2
2
|
|
|
3
|
-
Generated from the emitted `@kerfjs/ui@5.0.0-beta.
|
|
3
|
+
Generated from the emitted `@kerfjs/ui@5.0.0-beta.30` declaration boundary. Import `@kerfjs/ui/webawesome` for type effects when authoring direct `wa-*` JSX. The module emits no runtime behavior and does not register custom elements.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
6
|
import { KerfCustomElement } from 'kerfjs/jsx-runtime';
|
package/analyzer/index.mjs
CHANGED
|
@@ -36,6 +36,11 @@ export const UI_ANALYSIS_RULES = Object.freeze({
|
|
|
36
36
|
},
|
|
37
37
|
'KUI-L011': { severity: 'error', title: 'Uncataloged shadow part override' },
|
|
38
38
|
'KUI-L012': { severity: 'error', title: 'Private Kerf token assignment' },
|
|
39
|
+
'KUI-L013': { severity: 'error', title: 'Uncataloged CSS value literal' },
|
|
40
|
+
'KUI-L014': { severity: 'error', title: 'Wrong CSS value helper dimension' },
|
|
41
|
+
'KUI-L015': { severity: 'error', title: 'Non-standalone CSS expression' },
|
|
42
|
+
'KUI-L016': { severity: 'error', title: 'Forbidden declaration-list escape' },
|
|
43
|
+
'KUI-L017': { severity: 'review', title: 'Exceptional spacing shorthand' },
|
|
39
44
|
});
|
|
40
45
|
|
|
41
46
|
const adoptionRules = new Set([
|
|
@@ -46,7 +51,7 @@ const adoptionRules = new Set([
|
|
|
46
51
|
'KUI-L012',
|
|
47
52
|
]);
|
|
48
53
|
|
|
49
|
-
const sourceExtensions = new Set(['.
|
|
54
|
+
const sourceExtensions = new Set(['.js', '.jsx', '.mjs', '.ts', '.tsx']);
|
|
50
55
|
const spacingProperties = /^(?:margin|padding|gap|inset)(?:-|$)/;
|
|
51
56
|
const dimensionProperties =
|
|
52
57
|
/^(?:width|height|min-width|max-width|min-height|max-height)$/;
|
|
@@ -133,7 +138,13 @@ function relativeStyleImports(file, source) {
|
|
|
133
138
|
});
|
|
134
139
|
return imports;
|
|
135
140
|
}
|
|
136
|
-
const syntax = file.endsWith('.tsx')
|
|
141
|
+
const syntax = file.endsWith('.tsx')
|
|
142
|
+
? ts.ScriptKind.TSX
|
|
143
|
+
: file.endsWith('.jsx')
|
|
144
|
+
? ts.ScriptKind.JSX
|
|
145
|
+
: file.endsWith('.ts')
|
|
146
|
+
? ts.ScriptKind.TS
|
|
147
|
+
: ts.ScriptKind.JS;
|
|
137
148
|
const module = ts.createSourceFile(
|
|
138
149
|
file,
|
|
139
150
|
source,
|
|
@@ -426,6 +437,168 @@ function literalClasses(attribute) {
|
|
|
426
437
|
return { values: [], dynamic: true };
|
|
427
438
|
}
|
|
428
439
|
|
|
440
|
+
function cssPreferred(contract) {
|
|
441
|
+
return (
|
|
442
|
+
[
|
|
443
|
+
...(contract.canonicalShorthands ?? []).map((item) => `\`${item}\``),
|
|
444
|
+
...(contract.helpers ?? []).map((item) => `\`${item}()\``),
|
|
445
|
+
].join(' or ') || 'a cataloged component prop'
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function objectProperty(object, name) {
|
|
450
|
+
return object?.properties.find(
|
|
451
|
+
(property) =>
|
|
452
|
+
ts.isPropertyAssignment(property) &&
|
|
453
|
+
property.name.getText().replaceAll(/["']/g, '') === name,
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function nestedCssValues(value, tail) {
|
|
458
|
+
if (!tail) return value ? [value] : [];
|
|
459
|
+
if (!value || !ts.isArrayLiteralExpression(value)) return [];
|
|
460
|
+
return value.elements.flatMap((element) => {
|
|
461
|
+
if (!ts.isObjectLiteralExpression(element)) return [];
|
|
462
|
+
const property = objectProperty(element, tail);
|
|
463
|
+
return property ? [property.initializer] : [];
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function jsxCssValues(opening, path) {
|
|
468
|
+
const [head, tail] = path.split('[].');
|
|
469
|
+
const attribute = opening.attributes.properties.find(
|
|
470
|
+
(item) => ts.isJsxAttribute(item) && item.name.getText() === head,
|
|
471
|
+
);
|
|
472
|
+
if (!attribute?.initializer) return [];
|
|
473
|
+
const value = ts.isJsxExpression(attribute.initializer)
|
|
474
|
+
? attribute.initializer.expression
|
|
475
|
+
: attribute.initializer;
|
|
476
|
+
return nestedCssValues(value, tail);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function callCssValues(call, path) {
|
|
480
|
+
const options = call.arguments[0];
|
|
481
|
+
if (!options || !ts.isObjectLiteralExpression(options)) return [];
|
|
482
|
+
const [head, tail] = path.split('[].');
|
|
483
|
+
const property = objectProperty(options, head);
|
|
484
|
+
return property ? nestedCssValues(property.initializer, tail) : [];
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function cssLiteral(value) {
|
|
488
|
+
if (
|
|
489
|
+
ts.isStringLiteralLike(value) ||
|
|
490
|
+
ts.isNoSubstitutionTemplateLiteral(value)
|
|
491
|
+
)
|
|
492
|
+
return value.text;
|
|
493
|
+
return undefined;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function importedName(expression, helperImports, namespaces, packageName) {
|
|
497
|
+
let helper;
|
|
498
|
+
if (ts.isIdentifier(expression)) helper = helperImports.get(expression.text);
|
|
499
|
+
else if (
|
|
500
|
+
ts.isPropertyAccessExpression(expression) &&
|
|
501
|
+
ts.isIdentifier(expression.expression) &&
|
|
502
|
+
namespaces.has(expression.expression.text)
|
|
503
|
+
)
|
|
504
|
+
helper = {
|
|
505
|
+
imported: expression.name.text,
|
|
506
|
+
source: namespaces.get(expression.expression.text),
|
|
507
|
+
};
|
|
508
|
+
else helper = undefined;
|
|
509
|
+
if (!helper) return undefined;
|
|
510
|
+
return helper.source === packageName ||
|
|
511
|
+
helper.source.startsWith(`${packageName}/`)
|
|
512
|
+
? helper.imported
|
|
513
|
+
: undefined;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function inspectCssValues(
|
|
517
|
+
file,
|
|
518
|
+
source,
|
|
519
|
+
entry,
|
|
520
|
+
valuesFor,
|
|
521
|
+
helperImports,
|
|
522
|
+
namespaces,
|
|
523
|
+
diagnostics,
|
|
524
|
+
) {
|
|
525
|
+
for (const contract of entry.cssValueProps ?? []) {
|
|
526
|
+
const preferred = cssPreferred(contract);
|
|
527
|
+
for (const value of valuesFor(contract.path)) {
|
|
528
|
+
const at = location(file, value, source);
|
|
529
|
+
if (
|
|
530
|
+
contract.grammar === 'declarations' &&
|
|
531
|
+
contract.rawPolicy !== 'allow'
|
|
532
|
+
) {
|
|
533
|
+
diagnostics.push(
|
|
534
|
+
diagnostic(
|
|
535
|
+
'KUI-L016',
|
|
536
|
+
at,
|
|
537
|
+
`\`${entry.name}.${contract.path}\` is a forbidden declaration-list escape; use \`className\`, public tokens, or cataloged props.`,
|
|
538
|
+
{ component: entry.key, path: contract.path },
|
|
539
|
+
),
|
|
540
|
+
);
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const literal = cssLiteral(value);
|
|
544
|
+
if (literal !== undefined) {
|
|
545
|
+
if (contract.exceptionalShorthands?.includes(literal)) {
|
|
546
|
+
diagnostics.push(
|
|
547
|
+
diagnostic(
|
|
548
|
+
'KUI-L017',
|
|
549
|
+
at,
|
|
550
|
+
`\`${literal}\` is exceptional for \`${entry.name}.${contract.path}\`; prefer ${preferred} unless the off-scale choice is deliberate.`,
|
|
551
|
+
{ component: entry.key, path: contract.path, value: literal },
|
|
552
|
+
),
|
|
553
|
+
);
|
|
554
|
+
} else if (
|
|
555
|
+
!contract.shorthands?.includes(literal) &&
|
|
556
|
+
contract.rawPolicy !== 'allow'
|
|
557
|
+
) {
|
|
558
|
+
diagnostics.push(
|
|
559
|
+
diagnostic(
|
|
560
|
+
'KUI-L013',
|
|
561
|
+
at,
|
|
562
|
+
`\`${entry.name}.${contract.path}\` uses ${contract.grammar} grammar; replace raw \`${literal}\` with ${preferred}.`,
|
|
563
|
+
{ component: entry.key, path: contract.path, value: literal },
|
|
564
|
+
),
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (!ts.isCallExpression(value)) continue;
|
|
570
|
+
const helper = importedName(
|
|
571
|
+
value.expression,
|
|
572
|
+
helperImports,
|
|
573
|
+
namespaces,
|
|
574
|
+
entry.package,
|
|
575
|
+
);
|
|
576
|
+
if (!helper) continue;
|
|
577
|
+
if (contract.nonStandaloneHelpers?.includes(helper))
|
|
578
|
+
diagnostics.push(
|
|
579
|
+
diagnostic(
|
|
580
|
+
'KUI-L015',
|
|
581
|
+
at,
|
|
582
|
+
`\`${helper}()\` is not standalone for \`${entry.name}.${contract.path}\`; wrap it with an accepted composer such as \`calc()\`.`,
|
|
583
|
+
{ component: entry.key, path: contract.path, helper },
|
|
584
|
+
),
|
|
585
|
+
);
|
|
586
|
+
else if (
|
|
587
|
+
!contract.helpers?.includes(helper) &&
|
|
588
|
+
contract.unsafeHelper !== helper
|
|
589
|
+
)
|
|
590
|
+
diagnostics.push(
|
|
591
|
+
diagnostic(
|
|
592
|
+
'KUI-L014',
|
|
593
|
+
at,
|
|
594
|
+
`\`${helper}()\` has the wrong grammar for \`${entry.name}.${contract.path}\`; use ${preferred}.`,
|
|
595
|
+
{ component: entry.key, path: contract.path, helper },
|
|
596
|
+
),
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
429
602
|
function inspectTsx(file, sourceText, facts, cssFacts, diagnostics) {
|
|
430
603
|
const source = ts.createSourceFile(
|
|
431
604
|
file,
|
|
@@ -435,6 +608,8 @@ function inspectTsx(file, sourceText, facts, cssFacts, diagnostics) {
|
|
|
435
608
|
ts.ScriptKind.TSX,
|
|
436
609
|
);
|
|
437
610
|
const imports = new Map();
|
|
611
|
+
const helperImports = new Map();
|
|
612
|
+
const namespaces = new Map();
|
|
438
613
|
for (const statement of source.statements) {
|
|
439
614
|
if (
|
|
440
615
|
!ts.isImportDeclaration(statement) ||
|
|
@@ -442,13 +617,27 @@ function inspectTsx(file, sourceText, facts, cssFacts, diagnostics) {
|
|
|
442
617
|
)
|
|
443
618
|
continue;
|
|
444
619
|
const module = statement.moduleSpecifier.text;
|
|
445
|
-
|
|
620
|
+
const importClause = statement.importClause;
|
|
621
|
+
if (importClause?.name)
|
|
622
|
+
helperImports.set(importClause.name.text, {
|
|
623
|
+
imported: 'default',
|
|
624
|
+
source: module,
|
|
625
|
+
});
|
|
446
626
|
const bindings = statement.importClause?.namedBindings;
|
|
627
|
+
if (bindings && ts.isNamespaceImport(bindings)) {
|
|
628
|
+
namespaces.set(bindings.name.text, module);
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
447
631
|
if (!bindings || !ts.isNamedImports(bindings)) continue;
|
|
448
632
|
for (const item of bindings.elements) {
|
|
449
633
|
const exported = item.propertyName?.text ?? item.name.text;
|
|
634
|
+
helperImports.set(item.name.text, { imported: exported, source: module });
|
|
450
635
|
const entry = facts.exportEntries.get(exported);
|
|
451
|
-
if (
|
|
636
|
+
if (
|
|
637
|
+
entry &&
|
|
638
|
+
(module === entry.package || module.startsWith(`${entry.package}/`))
|
|
639
|
+
)
|
|
640
|
+
imports.set(item.name.text, entry);
|
|
452
641
|
}
|
|
453
642
|
}
|
|
454
643
|
const stack = [];
|
|
@@ -456,7 +645,23 @@ function inspectTsx(file, sourceText, facts, cssFacts, diagnostics) {
|
|
|
456
645
|
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
457
646
|
const opening = ts.isJsxElement(node) ? node.openingElement : node;
|
|
458
647
|
const tag = opening.tagName.getText(source);
|
|
459
|
-
const
|
|
648
|
+
const namespaceEntry =
|
|
649
|
+
ts.isPropertyAccessExpression(opening.tagName) &&
|
|
650
|
+
ts.isIdentifier(opening.tagName.expression)
|
|
651
|
+
? facts.exportEntries.get(opening.tagName.name.text)
|
|
652
|
+
: undefined;
|
|
653
|
+
const namespaceSource =
|
|
654
|
+
ts.isPropertyAccessExpression(opening.tagName) &&
|
|
655
|
+
ts.isIdentifier(opening.tagName.expression)
|
|
656
|
+
? namespaces.get(opening.tagName.expression.text)
|
|
657
|
+
: undefined;
|
|
658
|
+
const entry =
|
|
659
|
+
imports.get(tag) ??
|
|
660
|
+
(namespaceEntry &&
|
|
661
|
+
(namespaceSource === namespaceEntry.package ||
|
|
662
|
+
namespaceSource?.startsWith(`${namespaceEntry.package}/`))
|
|
663
|
+
? namespaceEntry
|
|
664
|
+
: undefined);
|
|
460
665
|
const classAttribute = opening.attributes.properties.find(
|
|
461
666
|
(item) =>
|
|
462
667
|
ts.isJsxAttribute(item) &&
|
|
@@ -464,6 +669,16 @@ function inspectTsx(file, sourceText, facts, cssFacts, diagnostics) {
|
|
|
464
669
|
);
|
|
465
670
|
const classes = literalClasses(classAttribute);
|
|
466
671
|
const at = location(file, opening, source);
|
|
672
|
+
if (entry)
|
|
673
|
+
inspectCssValues(
|
|
674
|
+
file,
|
|
675
|
+
source,
|
|
676
|
+
entry,
|
|
677
|
+
(path) => jsxCssValues(opening, path),
|
|
678
|
+
helperImports,
|
|
679
|
+
namespaces,
|
|
680
|
+
diagnostics,
|
|
681
|
+
);
|
|
467
682
|
if (classes.dynamic)
|
|
468
683
|
diagnostics.push(
|
|
469
684
|
diagnostic(
|
|
@@ -528,6 +743,33 @@ function inspectTsx(file, sourceText, facts, cssFacts, diagnostics) {
|
|
|
528
743
|
stack.pop();
|
|
529
744
|
return;
|
|
530
745
|
}
|
|
746
|
+
if (ts.isCallExpression(node)) {
|
|
747
|
+
const namespaceEntry = ts.isPropertyAccessExpression(node.expression)
|
|
748
|
+
? facts.exportEntries.get(node.expression.name.text)
|
|
749
|
+
: undefined;
|
|
750
|
+
const namespaceSource =
|
|
751
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
752
|
+
ts.isIdentifier(node.expression.expression)
|
|
753
|
+
? namespaces.get(node.expression.expression.text)
|
|
754
|
+
: undefined;
|
|
755
|
+
const entry = ts.isIdentifier(node.expression)
|
|
756
|
+
? imports.get(node.expression.text)
|
|
757
|
+
: namespaceEntry &&
|
|
758
|
+
(namespaceSource === namespaceEntry.package ||
|
|
759
|
+
namespaceSource?.startsWith(`${namespaceEntry.package}/`))
|
|
760
|
+
? namespaceEntry
|
|
761
|
+
: undefined;
|
|
762
|
+
if (entry)
|
|
763
|
+
inspectCssValues(
|
|
764
|
+
file,
|
|
765
|
+
source,
|
|
766
|
+
entry,
|
|
767
|
+
(path) => callCssValues(node, path),
|
|
768
|
+
helperImports,
|
|
769
|
+
namespaces,
|
|
770
|
+
diagnostics,
|
|
771
|
+
);
|
|
772
|
+
}
|
|
531
773
|
ts.forEachChild(node, visit);
|
|
532
774
|
};
|
|
533
775
|
visit(source);
|
package/dist/app-tab.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SafeHtml } from 'kerfjs';
|
|
2
|
+
import { K as KerfUiContent } from './semantic-content-BbzjvSu9.js';
|
|
2
3
|
|
|
3
4
|
type AppTabRootAttributes = Readonly<Record<`data-${string}`, string | undefined> & {
|
|
4
5
|
'data-component'?: never;
|
|
@@ -16,8 +17,8 @@ interface AppTabProps {
|
|
|
16
17
|
selected?: boolean;
|
|
17
18
|
closable?: boolean;
|
|
18
19
|
draggable?: boolean;
|
|
19
|
-
leading?:
|
|
20
|
-
trailing?:
|
|
20
|
+
leading?: KerfUiContent;
|
|
21
|
+
trailing?: KerfUiContent;
|
|
21
22
|
/** Visual treatment within a TabBar. Icon-only tabs retain `name` as their accessible name. */
|
|
22
23
|
presentation?: AppTabPresentation;
|
|
23
24
|
/** Compact tabs use the 32px application-rail height. */
|
package/dist/app-tab.js
CHANGED
package/dist/catalog.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SafeHtml } from 'kerfjs';
|
|
2
|
+
import { K as KerfUiContent } from './semantic-content-BbzjvSu9.js';
|
|
2
3
|
|
|
3
4
|
/** A reference link shown in the detail footer for the active entry. */
|
|
4
5
|
interface CatalogResource {
|
|
@@ -53,19 +54,19 @@ interface CatalogProps {
|
|
|
53
54
|
/** The controlled active entry id — the app owns this signal. */
|
|
54
55
|
active: string;
|
|
55
56
|
/** The rendered preview for the active entry; the app computes it from `active`. */
|
|
56
|
-
content:
|
|
57
|
+
content: KerfUiContent;
|
|
57
58
|
/** Whether the sidebar is collapsed (controlled). */
|
|
58
59
|
collapsed?: boolean;
|
|
59
60
|
/** Current theme; when set, a theme toggle is shown that switches to the opposite. Omit to hide it. */
|
|
60
61
|
theme?: 'light' | 'dark';
|
|
61
62
|
/** Extra header controls placed before the theme toggle (each a `ToolbarControlGroup`). */
|
|
62
|
-
headerActions?:
|
|
63
|
+
headerActions?: KerfUiContent;
|
|
63
64
|
/** A secondary "ecosystem" group of sections below the primary category groups. */
|
|
64
65
|
secondarySections?: CatalogSecondaryGroup;
|
|
65
66
|
/** Extra sidebar content below the category groups (and the secondary group). */
|
|
66
|
-
sidebarFooter?:
|
|
67
|
+
sidebarFooter?: KerfUiContent;
|
|
67
68
|
/** Status line content shown at the start of the detail footer. */
|
|
68
|
-
status?:
|
|
69
|
+
status?: KerfUiContent;
|
|
69
70
|
/**
|
|
70
71
|
* Whether to highlight specimens' computed borders (or transparent outer
|
|
71
72
|
* bounds) and non-zero margins. Pass a boolean (rather than omitting the
|
|
@@ -121,7 +122,7 @@ interface CatalogExampleProps {
|
|
|
121
122
|
/** Safe authoring `data-*` metadata for the rendered section. Helper-owned structural attributes remain protected. */
|
|
122
123
|
rootAttributes?: CatalogExampleRootAttributes;
|
|
123
124
|
className?: string;
|
|
124
|
-
children?:
|
|
125
|
+
children?: KerfUiContent;
|
|
125
126
|
}
|
|
126
127
|
/**
|
|
127
128
|
* One labeled example in a catalog preview: a `ListHeader` label, an optional
|
|
@@ -138,7 +139,7 @@ interface CatalogExampleStackProps {
|
|
|
138
139
|
/** Safe authoring `data-*` metadata for the rendered stack. Helper-owned structural attributes remain protected. */
|
|
139
140
|
rootAttributes?: CatalogExampleStackRootAttributes;
|
|
140
141
|
className?: string;
|
|
141
|
-
children?:
|
|
142
|
+
children?: KerfUiContent;
|
|
142
143
|
}
|
|
143
144
|
/**
|
|
144
145
|
* A vertically-stacked group of {@link CatalogExample}s with the catalog's
|
package/dist/catalog.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { Pane } from './chunk-
|
|
4
|
-
import { ListHeader } from './chunk-
|
|
5
|
-
import { ListItem } from './chunk-
|
|
6
|
-
import { List } from './chunk-
|
|
1
|
+
import { ToolbarControlGroup } from './chunk-B6JYRX3P.js';
|
|
2
|
+
import { Toolbar } from './chunk-VXFGMDA7.js';
|
|
3
|
+
import { Pane } from './chunk-I4V3UDSA.js';
|
|
4
|
+
import { ListHeader } from './chunk-OC275HHS.js';
|
|
5
|
+
import { ListItem } from './chunk-I45QO2IZ.js';
|
|
6
|
+
import { List } from './chunk-RL6COPKF.js';
|
|
7
7
|
import { filterDataAttributes } from './chunk-SRSJO5QE.js';
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
|
|
10
11
|
import { LucideIcon } from './chunk-W2FK7ZNY.js';
|
|
11
12
|
|
|
12
13
|
import { PanelLeftClose, Waypoints, ExternalLink, Moon, Sun, PanelLeftOpen } from 'lucide';
|
|
@@ -282,7 +283,7 @@ function Catalog({
|
|
|
282
283
|
children: [
|
|
283
284
|
/* @__PURE__ */ jsx("wa-button", { slot: "trigger", appearance: "plain", "with-caret": true, children: /* @__PURE__ */ jsxs("span", { class: "kui-catalog__related-trigger", children: [
|
|
284
285
|
/* @__PURE__ */ jsx(LucideIcon, { icon: Waypoints, name: "waypoints" }),
|
|
285
|
-
/* @__PURE__ */ jsx("span", { class: "kui-catalog__related-label", children: "
|
|
286
|
+
/* @__PURE__ */ jsx("span", { class: "kui-catalog__related-label", children: "Components" })
|
|
286
287
|
] }) }),
|
|
287
288
|
relatedMenuItems(related, selectAction)
|
|
288
289
|
]
|