@kerfjs/ui 5.0.0-beta.106 → 5.0.0-beta.108
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 +2 -1
- package/ai/application-ui-diagnostic-ids-v1.json +1 -0
- package/ai/application-ui-profile.defaults.json +6 -1
- package/ai/component-catalog.json +184 -39
- package/ai/component-composition-rules.json +20 -2
- package/ai/component-composition.d.ts +11 -0
- package/ai/component-composition.json +126 -21
- package/ai/component-composition.schema.json +32 -0
- package/ai/components/README.md +3 -2
- package/ai/components/button.md +9 -6
- package/ai/components/checkbox-group.md +89 -0
- package/ai/components/checkbox.md +3 -2
- package/ai/components/chip.md +1 -0
- package/ai/components/color-picker.md +10 -7
- package/ai/components/disclosure.md +6 -1
- package/ai/components/foundation.md +1 -1
- package/ai/components/list-detail-dialog.md +1 -1
- package/ai/components/list.md +1 -1
- package/ai/components/message-composer.md +2 -1
- package/ai/components/radio-group.md +4 -3
- package/ai/components/segmented-control.md +1 -0
- package/ai/components/split-button.md +5 -2
- package/ai/components/surface-scaffold.md +3 -1
- package/ai/components/switch.md +1 -1
- package/ai/components/text-field.md +3 -1
- package/ai/components/text.md +2 -0
- package/ai/components/toolbar-control-group.md +1 -0
- package/ai/components/toolbar.md +1 -1
- package/ai/components/wa-checkbox-group.md +5 -5
- package/ai/components/workbench-dialog.md +1 -1
- package/ai/public-api-signatures-v1.md +258 -35
- package/ai/skill.md +82 -65
- package/ai/webawesome-jsx-signatures-v1.md +1 -1
- package/dist/browser/checkbox-group.js +5 -0
- package/dist/button.d.ts +18 -5
- package/dist/button.js +1 -1
- package/dist/catalog.js +11 -10
- package/dist/catalog.js.map +1 -1
- package/dist/checkbox-group.d.ts +70 -0
- package/dist/checkbox-group.js +7 -0
- package/dist/checkbox-group.js.map +1 -0
- package/dist/chip.d.ts +20 -2
- package/dist/chip.js +2 -1
- package/dist/{chunk-SQ2Q3OQI.js → chunk-2LM3PYHA.js} +2 -2
- package/dist/{chunk-SQ2Q3OQI.js.map → chunk-2LM3PYHA.js.map} +1 -1
- package/dist/{chunk-HYORKKYP.js → chunk-3VZ6SFD4.js} +1 -1
- package/dist/chunk-3VZ6SFD4.js.map +1 -0
- package/dist/{chunk-EKBMUXFV.js → chunk-3Z7N2NAZ.js} +141 -54
- package/dist/chunk-3Z7N2NAZ.js.map +1 -0
- package/dist/{chunk-23PPPNH6.js → chunk-4OV272LF.js} +2 -2
- package/dist/{chunk-23PPPNH6.js.map → chunk-4OV272LF.js.map} +1 -1
- package/dist/{chunk-2PXZBFEC.js → chunk-5J6Z4BWY.js} +1 -1
- package/dist/{chunk-2PXZBFEC.js.map → chunk-5J6Z4BWY.js.map} +1 -1
- package/dist/{chunk-TAHR457X.js → chunk-6CDILXF4.js} +5 -1
- package/dist/chunk-6CDILXF4.js.map +1 -0
- package/dist/{chunk-DDSCVEO5.js → chunk-CPSGUGAY.js} +6 -6
- package/dist/{chunk-DDSCVEO5.js.map → chunk-CPSGUGAY.js.map} +1 -1
- package/dist/{chunk-C3RLKKFQ.js → chunk-EYDXFOSH.js} +13 -8
- package/dist/chunk-EYDXFOSH.js.map +1 -0
- package/dist/{chunk-PE2KPZDW.js → chunk-HDXUHLTQ.js} +7 -2
- package/dist/chunk-HDXUHLTQ.js.map +1 -0
- package/dist/chunk-HU5IHXCI.js +73 -0
- package/dist/chunk-HU5IHXCI.js.map +1 -0
- package/dist/{chunk-DF2YKGIX.js → chunk-JCCN3U6X.js} +2 -2
- package/dist/{chunk-DF2YKGIX.js.map → chunk-JCCN3U6X.js.map} +1 -1
- package/dist/{chunk-UTCAOOXU.js → chunk-KPINJHIC.js} +8 -2
- package/dist/chunk-KPINJHIC.js.map +1 -0
- package/dist/{chunk-N7MBGONH.js → chunk-LMHN2TJL.js} +22 -2
- package/dist/chunk-LMHN2TJL.js.map +1 -0
- package/dist/{chunk-NS2TBYM4.js → chunk-LZUUUHNH.js} +7 -3
- package/dist/chunk-LZUUUHNH.js.map +1 -0
- package/dist/{chunk-GCAK23PU.js → chunk-MVKHSAKE.js} +2 -2
- package/dist/{chunk-GCAK23PU.js.map → chunk-MVKHSAKE.js.map} +1 -1
- package/dist/{chunk-OY2MXIBB.js → chunk-NB6POCFH.js} +3 -1
- package/dist/chunk-NB6POCFH.js.map +1 -0
- package/dist/{chunk-ECFCAKWE.js → chunk-NR6O74ST.js} +5 -1
- package/dist/chunk-NR6O74ST.js.map +1 -0
- package/dist/{chunk-7W3GKMN4.js → chunk-OQP3PFKQ.js} +7 -4
- package/dist/chunk-OQP3PFKQ.js.map +1 -0
- package/dist/{chunk-FKKJHGN5.js → chunk-P7EJ4CAD.js} +2 -2
- package/dist/{chunk-FKKJHGN5.js.map → chunk-P7EJ4CAD.js.map} +1 -1
- package/dist/{chunk-ZXZAECHZ.js → chunk-SOFPUW3O.js} +4 -4
- package/dist/{chunk-ZXZAECHZ.js.map → chunk-SOFPUW3O.js.map} +1 -1
- package/dist/{chunk-WJKHLOP5.js → chunk-TI2GV36S.js} +8 -2
- package/dist/chunk-TI2GV36S.js.map +1 -0
- package/dist/{chunk-QLA25V2K.js → chunk-WPNIKZMJ.js} +15 -1
- package/dist/chunk-WPNIKZMJ.js.map +1 -0
- package/dist/{chunk-FG6U7KQQ.js → chunk-WSEJ2FDE.js} +4 -4
- package/dist/{chunk-FG6U7KQQ.js.map → chunk-WSEJ2FDE.js.map} +1 -1
- package/dist/{chunk-ACUKTVS3.js → chunk-XWNXGFX3.js} +52 -42
- package/dist/chunk-XWNXGFX3.js.map +1 -0
- package/dist/{chunk-WU57BYYM.js → chunk-YVZCKXR3.js} +8 -2
- package/dist/chunk-YVZCKXR3.js.map +1 -0
- package/dist/{chunk-CMLPFIC5.js → chunk-ZN7KUMPY.js} +3 -1
- package/dist/chunk-ZN7KUMPY.js.map +1 -0
- package/dist/collapsible-panel.js +2 -2
- package/dist/color-picker.d.ts +30 -5
- package/dist/color-picker.js +1 -1
- package/dist/disclosure.d.ts +13 -4
- package/dist/disclosure.js.map +1 -1
- package/dist/help-tags-register.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +24 -23
- package/dist/list-action-row.js +1 -1
- package/dist/list-detail-dialog.d.ts +7 -1
- package/dist/list-detail-dialog.js +7 -5
- package/dist/list-detail-dialog.js.map +1 -1
- package/dist/list-header.d.ts +8 -1
- package/dist/list-header.js +1 -1
- package/dist/list.d.ts +7 -1
- package/dist/list.js +1 -1
- package/dist/message-composer.js +1 -1
- package/dist/nav-stack.js +1 -1
- package/dist/popup-menu-register.js +2 -2
- package/dist/popup-menu.d.ts +15 -5
- package/dist/popup-menu.js +1 -1
- package/dist/segmented-control.d.ts +3 -1
- package/dist/segmented-control.js +1 -1
- package/dist/select-register.js +3 -3
- package/dist/split-button.d.ts +14 -2
- package/dist/split-button.js +1 -1
- package/dist/split-view.js +1 -1
- package/dist/styles/anchored-surface.css +3 -0
- package/dist/styles/button.css +57 -2
- package/dist/styles/catalog/components/catalog-example.css +55 -18
- package/dist/styles/checkbox-group.css +63 -0
- package/dist/styles/checkbox.css +7 -6
- package/dist/styles/color-picker.css +69 -34
- package/dist/styles/components/actions/button/button.css +57 -2
- package/dist/styles/components/actions/split-button/split-button.css +51 -5
- package/dist/styles/components/actions/toolbar/toolbar.css +9 -0
- package/dist/styles/components/actions/toolbar-control-group/toolbar-control-group.css +29 -1
- package/dist/styles/components/collections/framed-list/framed-list.css +6 -0
- package/dist/styles/components/collections/list/list.css +6 -0
- package/dist/styles/components/collections/list-header/list-header.css +7 -0
- package/dist/styles/components/feedback/anchored-surface/anchored-surface.css +3 -0
- package/dist/styles/components/forms/checkbox/checkbox.css +7 -6
- package/dist/styles/components/forms/checkbox-group/checkbox-group.css +63 -0
- package/dist/styles/components/forms/color-picker/color-picker.css +69 -34
- package/dist/styles/components/forms/radio-group/radio-group.css +7 -5
- package/dist/styles/components/forms/select/select.css +6 -2
- package/dist/styles/components/forms/switch/switch.css +3 -3
- package/dist/styles/components/layout/pane/pane.css +24 -0
- package/dist/styles/components/layout/workbench/workbench.css +4 -0
- package/dist/styles/components/navigation/nav-stack/nav-stack.css +77 -1
- package/dist/styles/components/navigation/tab-bar/tab-bar.css +6 -0
- package/dist/styles/components/navigation/tab-navigator/tab-navigator.css +17 -0
- package/dist/styles/components/surfaces/content-item/content-item.css +30 -0
- package/dist/styles/components/surfaces/disclosure/disclosure.css +38 -0
- package/dist/styles/components/surfaces/sunken-panel/sunken-panel.css +3 -0
- package/dist/styles/components/surfaces/surface-scaffold/surface-scaffold.css +8 -0
- package/dist/styles/components/typography/text/text.css +10 -0
- package/dist/styles/content-item.css +30 -0
- package/dist/styles/disclosure.css +38 -0
- package/dist/styles/foundation.css +78 -13
- package/dist/styles/framed-list.css +6 -0
- package/dist/styles/list-header.css +7 -0
- package/dist/styles/list.css +6 -0
- package/dist/styles/nav-stack.css +77 -1
- package/dist/styles/pane.css +24 -0
- package/dist/styles/radio-group.css +7 -5
- package/dist/styles/select.css +6 -2
- package/dist/styles/split-button.css +51 -5
- package/dist/styles/styles.css +1 -0
- package/dist/styles/sunken-panel.css +3 -0
- package/dist/styles/surface-scaffold.css +8 -0
- package/dist/styles/switch.css +3 -3
- package/dist/styles/tab-bar.css +6 -0
- package/dist/styles/tab-navigator.css +17 -0
- package/dist/styles/text.css +10 -0
- package/dist/styles/toolbar-control-group.css +29 -1
- package/dist/styles/toolbar.css +9 -0
- package/dist/styles/webawesome.css +56 -12
- package/dist/styles/workbench.css +4 -0
- package/dist/surface-scaffold.d.ts +15 -1
- package/dist/surface-scaffold.js +1 -1
- package/dist/text-field.d.ts +7 -1
- package/dist/text-field.js +1 -1
- package/dist/toolbar-control-group.d.ts +8 -2
- package/dist/toolbar-control-group.js +1 -1
- package/dist/toolbar.d.ts +6 -1
- package/dist/toolbar.js +1 -1
- package/dist/wire-catalog.js +1 -1
- package/dist/wire-color-fields.js +2 -2
- package/dist/wire-color-pickers.d.ts +4 -3
- package/dist/wire-color-pickers.js +1 -1
- package/dist/wire-comboboxes.js +1 -1
- package/dist/wire-workbench.js +1 -1
- package/dist/workbench-dialog.d.ts +11 -2
- package/dist/workbench-dialog.js +6 -3
- package/dist/workbench-dialog.js.map +1 -1
- package/dist/workbench.js +1 -1
- package/docs/accessibility.md +58 -3
- package/docs/anchored-surface.md +6 -1
- package/docs/catalog.md +26 -1
- package/docs/color-picker.md +39 -8
- package/docs/component-contract.md +47 -3
- package/docs/component-selection.md +58 -21
- package/docs/design/templates/button/full-width-dark.svg +1 -0
- package/docs/design/templates/button/full-width.svg +1 -0
- package/docs/design/templates/button/tones-dark.svg +1 -1
- package/docs/design/templates/button/tones.svg +1 -1
- package/docs/design/templates/button-dark.svg +1 -1
- package/docs/design/templates/button.svg +1 -1
- package/docs/design/templates/checkbox/error-dark.svg +1 -1
- package/docs/design/templates/checkbox/error.svg +1 -1
- package/docs/design/templates/checkbox/states-dark.svg +1 -1
- package/docs/design/templates/checkbox/states.svg +1 -1
- package/docs/design/templates/checkbox-dark.svg +1 -1
- package/docs/design/templates/checkbox-group/horizontal-dark.svg +1 -0
- package/docs/design/templates/checkbox-group/horizontal.svg +1 -0
- package/docs/design/templates/checkbox-group/vertical-dark.svg +1 -0
- package/docs/design/templates/checkbox-group/vertical.svg +1 -0
- package/docs/design/templates/checkbox-group-dark.svg +1 -0
- package/docs/design/templates/checkbox-group.svg +1 -0
- package/docs/design/templates/checkbox.svg +1 -1
- package/docs/design/templates/color-picker/gray-dark.svg +1 -1
- package/docs/design/templates/color-picker/gray.svg +1 -1
- package/docs/design/templates/color-picker/hsb-dark.svg +1 -0
- package/docs/design/templates/color-picker/hsb.svg +1 -0
- package/docs/design/templates/color-picker/rgb-dark.svg +1 -1
- package/docs/design/templates/color-picker/rgb.svg +1 -1
- package/docs/design/templates/color-picker-dark.svg +1 -1
- package/docs/design/templates/color-picker.svg +1 -1
- package/docs/design/templates/disclosure/embedded-dark.svg +1 -0
- package/docs/design/templates/disclosure/embedded.svg +1 -0
- package/docs/design/templates/disclosure-dark.svg +1 -1
- package/docs/design/templates/disclosure.svg +1 -1
- package/docs/design/templates/radio-group/horizontal-dark.svg +1 -1
- package/docs/design/templates/radio-group/horizontal.svg +1 -1
- package/docs/design/templates/radio-group/vertical-dark.svg +1 -1
- package/docs/design/templates/radio-group/vertical.svg +1 -1
- package/docs/design/templates/radio-group-dark.svg +1 -1
- package/docs/design/templates/radio-group.svg +1 -1
- package/docs/design/templates/split-button/full-width-dark.svg +1 -0
- package/docs/design/templates/split-button/full-width.svg +1 -0
- package/docs/design/templates/split-button/pill-dark.svg +1 -0
- package/docs/design/templates/split-button/pill.svg +1 -0
- package/docs/design/templates/split-button/states-dark.svg +1 -0
- package/docs/design/templates/split-button/states.svg +1 -0
- package/docs/design/templates/split-button/tones-dark.svg +1 -0
- package/docs/design/templates/split-button/tones.svg +1 -0
- package/docs/design/templates/split-button-dark.svg +1 -0
- package/docs/design/templates/split-button.svg +1 -0
- package/docs/design/templates/switch/states-dark.svg +1 -1
- package/docs/design/templates/switch/states.svg +1 -1
- package/docs/design/templates/switch-dark.svg +1 -1
- package/docs/design/templates/switch.svg +1 -1
- package/docs/design/templates/text-field/number-dark.svg +1 -0
- package/docs/design/templates/text-field/number.svg +1 -0
- package/docs/design/templates/text-field-dark.svg +1 -1
- package/docs/design/templates/text-field.svg +1 -1
- package/docs/design/templates.md +10 -3
- package/docs/disclosure.md +34 -0
- package/docs/layout.md +16 -5
- package/docs/list-detail-dialog.md +4 -0
- package/docs/nav-stack.md +6 -2
- package/docs/surface-scaffold.md +6 -2
- package/docs/ui-doctor.md +16 -4
- package/docs/webawesome-migration.md +3 -1
- package/docs/webawesome-theme.md +56 -18
- package/docs/workbench-dialog.md +4 -1
- package/docs/workbench.md +7 -1
- package/docs/writing.md +3 -3
- package/doctor/copy-capitalization-checks.mjs +10 -7
- package/llms.txt +65 -23
- package/package.json +7 -1
- package/dist/chunk-7W3GKMN4.js.map +0 -1
- package/dist/chunk-ACUKTVS3.js.map +0 -1
- package/dist/chunk-C3RLKKFQ.js.map +0 -1
- package/dist/chunk-CMLPFIC5.js.map +0 -1
- package/dist/chunk-ECFCAKWE.js.map +0 -1
- package/dist/chunk-EKBMUXFV.js.map +0 -1
- package/dist/chunk-HYORKKYP.js.map +0 -1
- package/dist/chunk-N7MBGONH.js.map +0 -1
- package/dist/chunk-NS2TBYM4.js.map +0 -1
- package/dist/chunk-OY2MXIBB.js.map +0 -1
- package/dist/chunk-PE2KPZDW.js.map +0 -1
- package/dist/chunk-QLA25V2K.js.map +0 -1
- package/dist/chunk-TAHR457X.js.map +0 -1
- package/dist/chunk-UTCAOOXU.js.map +0 -1
- package/dist/chunk-WJKHLOP5.js.map +0 -1
- package/dist/chunk-WU57BYYM.js.map +0 -1
|
@@ -36,6 +36,10 @@ stylesheets, or `@kerfjs/ui/styles.css`.
|
|
|
36
36
|
`list.footer`.
|
|
37
37
|
- **The surface draws no header.** Both columns start at the dialog's top
|
|
38
38
|
edge, so the divider between them runs the dialog's full height.
|
|
39
|
+
- **The description is yours to name.** The detail `description` describes the
|
|
40
|
+
selected record, not the dialog, so it is not the dialog's accessible
|
|
41
|
+
description. Pass `describedBy`, the id of app-owned summary copy, to render
|
|
42
|
+
`aria-describedby` on the dialog.
|
|
39
43
|
|
|
40
44
|
Each column is a `Pane`: its header and footer stay pinned and its content
|
|
41
45
|
scrolls on its own. Pass content that owns its geometry, usually a `List`.
|
package/docs/nav-stack.md
CHANGED
|
@@ -241,8 +241,12 @@ scrolls visibly, blurred, beneath it.
|
|
|
241
241
|
includes that padding, so views clear the chrome and the safe area exactly
|
|
242
242
|
once.
|
|
243
243
|
- **Transitions.** Only the live chrome is measured, never a cross-fading
|
|
244
|
-
copy.
|
|
245
|
-
|
|
244
|
+
copy. An overlay bar (top chrome or bottom toolbar) cross-fades its
|
|
245
|
+
contents, never its material: the live bar keeps the material at full
|
|
246
|
+
opacity while its new contents fade in, and the copy fading out above it
|
|
247
|
+
carries only the old contents, so the sliding views never show through the
|
|
248
|
+
chrome mid push or pop. The incoming view settles at the incoming chrome's
|
|
249
|
+
clearance.
|
|
246
250
|
- **Classic scrollbars.** Where the active view's scroller (the view, or a
|
|
247
251
|
sole header-less Pane's content) shows a classic, space-taking scrollbar,
|
|
248
252
|
`wireNavStack` measures it and the chrome ends where its track begins, so
|
package/docs/surface-scaffold.md
CHANGED
|
@@ -41,6 +41,11 @@ openDialog(document.getElementById('edit-workspace') as HTMLDialogElement);
|
|
|
41
41
|
- **Name.** `label` names the dialog and is its title text. With `heading`
|
|
42
42
|
(title content such as an xl `ToolbarText` with a description), pass
|
|
43
43
|
`labelledBy`, the id of the element that names it, or keep `label`.
|
|
44
|
+
- **Description.** `describedBy` is the id of the element whose text is the
|
|
45
|
+
dialog's accessible description, rendered as `aria-describedby` on the
|
|
46
|
+
`<dialog>`: the `heading` ToolbarText's `descriptionId`, or app-owned summary
|
|
47
|
+
copy in the body. `rootAttributes` takes only `data-*` attributes, so it can
|
|
48
|
+
never set or override `aria-describedby`.
|
|
44
49
|
- **Header.** The title, then `headerActions` and a close button (`closeLabel`,
|
|
45
50
|
default `Close`). `withoutHeader` renders none, for content that owns its
|
|
46
51
|
heading and close control.
|
|
@@ -447,8 +452,7 @@ pass `describedBy`, the id of the element that holds the summary. It renders as
|
|
|
447
452
|
</PopoverDialogSurface>;
|
|
448
453
|
```
|
|
449
454
|
|
|
450
|
-
`DialogSurface`
|
|
451
|
-
and keep any description in the body or its `heading`'s description.
|
|
455
|
+
`DialogSurface` takes the same `describedBy` for a modal dialog.
|
|
452
456
|
|
|
453
457
|
Open it with a control's `popoverTarget` (or `showPopover()`); the browser
|
|
454
458
|
places it centered in the top layer, light-dismisses it, and restores nothing
|
package/docs/ui-doctor.md
CHANGED
|
@@ -18,6 +18,16 @@ The catalog stage discovers the package-default application UI profile and works
|
|
|
18
18
|
|
|
19
19
|
TypeScript uses the compiler API with `noEmit`. ESLint loads the installed `eslint-plugin-kerfjs` `recommended-ui` preset (`--eslint strict-ui` opts into advisory rules as errors). This is deliberately an isolated Kerf lint pass rather than the consumer's complete ESLint configuration. For each file, the doctor projects the applicable core ESLint rules and `linterOptions` from the consumer configuration so core-rule suppression directives retain the same used/unused semantics as the application's normal lint command. Consumer plugin rules are not executed, and inline directives for plugins such as `@typescript-eslint` therefore do not produce false "rule definition not found" diagnostics; the consumer's normal ESLint command remains authoritative for those rules. Unknown `kerfjs/*` directives still fail the doctor pass. Stable KUI identifiers come from the packaged `application-ui-diagnostic-ids-v1.json` registry; the doctor also reads installed ESLint message metadata for semantic conflict detection instead of duplicating rule definitions. The analyzer calls the public `@kerfjs/ui/analyzer` contract. None of these stages executes generated application code.
|
|
20
20
|
|
|
21
|
+
Composition findings come from the same catalogs. `KUI-L201`–`KUI-L203`
|
|
22
|
+
report a cataloged component under a disallowed parent, a zone child the zone
|
|
23
|
+
does not accept, and a provable zone cardinality violation. `KUI-L204` reports
|
|
24
|
+
a direct child that brings its own content-item inset (1px border + 8px
|
|
25
|
+
padding) into a parent configured to expect inset-free content, such as a
|
|
26
|
+
block `Text` inside `Disclosure appearance="embedded"`, which double-insets the
|
|
27
|
+
owner chrome; the message names the child's inset-free form (`Text flush`) or
|
|
28
|
+
inline content. Values the rule cannot read statically, such as a spread or an
|
|
29
|
+
expression, are not reported.
|
|
30
|
+
|
|
21
31
|
Property-specific CSS values are evaluated from each first- or third-party
|
|
22
32
|
catalog entry's `cssValueProps`. `KUI-L013`–`KUI-L016` are blocking grammar
|
|
23
33
|
violations and name a preferred shorthand/helper; `KUI-L017` keeps an
|
|
@@ -182,9 +192,10 @@ covers these string literals:
|
|
|
182
192
|
`section`, `nav`, `aside`, `main`, `header`, `footer`, `form`, or `search`
|
|
183
193
|
element or an element with a landmark `role`;
|
|
184
194
|
- field and group labels: the `label` and `ariaLabel` of `TextField`,
|
|
185
|
-
`Select`, and `RadioGroup`, and the `label` of a raw
|
|
186
|
-
`wa-select`, `wa-textarea`,
|
|
187
|
-
optional Web Awesome
|
|
195
|
+
`Select`, `CheckboxGroup`, and `RadioGroup`, and the `label` of a raw
|
|
196
|
+
`wa-input`, `wa-select`, `wa-textarea`, `wa-checkbox-group`, or
|
|
197
|
+
`wa-radio-group` in an application using the optional Web Awesome
|
|
198
|
+
integration;
|
|
188
199
|
- the accessible names of icon-only controls, which the rule also gives title
|
|
189
200
|
style: an icon-only `Button`'s `ariaLabel`, `PopupMenu` `label`,
|
|
190
201
|
`SplitButton` `optionsLabel`, `SearchField` and `TextField`
|
|
@@ -203,7 +214,8 @@ The check is deliberately one-directional, to keep false positives rare:
|
|
|
203
214
|
is. A word joined to an interpolation (`` `${count}s` ``) is skipped.
|
|
204
215
|
- It ignores other dynamic values and every sentence-style surface: descriptions,
|
|
205
216
|
details, placeholders, tooltips, status text, and option text, including
|
|
206
|
-
`Checkbox` and `Switch` labels, `RadioGroup` choice
|
|
217
|
+
`Checkbox` and `Switch` labels, `CheckboxGroup` and `RadioGroup` choice
|
|
218
|
+
labels, and
|
|
207
219
|
`wa-checkbox`, `wa-switch`, and `wa-radio` labels. It does not read
|
|
208
220
|
`LoadingSpinner` or `Skeleton` labels, which announce a status.
|
|
209
221
|
- It skips tokens containing digits, code, paths, or URLs, and lowercase-led
|
|
@@ -17,13 +17,15 @@ standard DOM events, and keeps its state in native form controls, so a
|
|
|
17
17
|
delegated `input` / `change` / `click` handler usually keeps working once the
|
|
18
18
|
selector changes.
|
|
19
19
|
|
|
20
|
+
For a positive action such as Price Check, migrate `<wa-button variant="success">` to `<Button tone="success">`. The semantic success palette also applies to `secondary` and `plain` emphasis; use the component prop rather than an application color override.
|
|
21
|
+
|
|
20
22
|
| Web Awesome markup | Kerf replacement | Notes |
|
|
21
23
|
| ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
22
24
|
| `<wa-button variant="brand">Save</wa-button>` | `<Button tone="brand" action="save">Save</Button>` (`@kerfjs/ui/button`) | `tone` replaces `variant`; `emphasis` replaces `appearance`. Use `href` for a link button, `icon` with `ariaLabel` and no label for an icon-only one, and `pressed` for a toggle. |
|
|
23
25
|
| `<wa-input label="Name" hint="…" with-clear>` | `<TextField label="Name" hint="…" clearable />` (`@kerfjs/ui/text-field`) | Call `wireTextFields(root)` from `@kerfjs/ui/wire-text-fields` once for the clear button and a live `showCount`. Pass `error` for an invalid field. |
|
|
24
26
|
| `<wa-textarea rows="4" resize="auto">` | `<TextField multiline rows={4} maxRows={10} />` | `maxRows` grows the field with its content up to that height. |
|
|
25
27
|
| `<wa-checkbox>Email me</wa-checkbox>` | `<Checkbox label="Email me" action="toggle-email" />` (`@kerfjs/ui/checkbox`) | For a mixed parent, pass `indeterminate` and call `wireCheckboxes(root)` from `@kerfjs/ui/wire-checkboxes` once. |
|
|
26
|
-
| `<wa-checkbox-group>`
|
|
28
|
+
| `<wa-checkbox-group label="Notifications">` | `<CheckboxGroup label="Notifications" name="channels" value={channels.value} choices={[…]} />` (`@kerfjs/ui/checkbox-group`), or `ChoiceChips` with `multiple="toggle"` | |
|
|
27
29
|
| `<wa-radio-group label="Density"><wa-radio value="compact">…` | `<RadioGroup label="Density" name="density" value={density.value} choices={[{ value: 'compact', label: 'Compact' }]} />` | `@kerfjs/ui/radio-group`. Use `orientation="horizontal"` for a row. Use `SegmentedControl` for a view switch and `ChoiceChips` for wrapping pills. |
|
|
28
30
|
| `<wa-switch>Live updates</wa-switch>` | `<Switch label="Live updates" checked={live.value} action="toggle-live" />` (`@kerfjs/ui/switch`) | Only for a setting that applies immediately; use `Checkbox` for a submitted option. |
|
|
29
31
|
| `<wa-select>` with `<wa-option>` children | `<Select label="Status" name="status" value={status.value} choices={[…]} />` (`@kerfjs/ui/select`) | Import `@kerfjs/ui/select/register` once. It emits standard `input` and `change` events. |
|
package/docs/webawesome-theme.md
CHANGED
|
@@ -99,8 +99,8 @@ share Kerf's 44px control height — the height of a Kerf `Select`, a toolbar
|
|
|
99
99
|
group, and a list row — so a `wa-input` or `wa-button` sits flush beside a Kerf
|
|
100
100
|
`Select`. The theme sets `--wa-form-control-height` to `2.75em` (44px at the
|
|
101
101
|
16px default) on Button, Color Picker, Input, Known Date, Number Input, Radio
|
|
102
|
-
Group, Select,
|
|
103
|
-
scales proportionally while `size="small"` keeps Web Awesome's compact secondary
|
|
102
|
+
Group, Select, Tag Input (its box's minimum height), Textarea (its minimum
|
|
103
|
+
height), and Time Input, so `size="large"` scales proportionally while `size="small"` keeps Web Awesome's compact secondary
|
|
104
104
|
height. The token is scoped to those controls rather than the root because Web
|
|
105
105
|
Awesome also derives tag, copy-button, and toast geometry from it. Buttons
|
|
106
106
|
inside a `ToolbarControlGroup` keep the group's 40px (32px compact) control
|
|
@@ -114,26 +114,58 @@ uppercase 12px/650 treatment. Inline control labels (Checkbox, Switch) keep
|
|
|
114
114
|
their natural sentence case.
|
|
115
115
|
|
|
116
116
|
On coarse touch devices, the editable part of small and extra-small Web Awesome
|
|
117
|
-
Input, Number Input, Time Input, Textarea, and Select has a 16px
|
|
118
|
-
size. Small field chrome keeps its compact height and desktop
|
|
119
|
-
small. This prevents Safari from magnifying the page just to
|
|
120
|
-
not constrain a user's own pinch zoom. App-owned native fields need the same
|
|
117
|
+
Input, Number Input, Tag Input, Time Input, Textarea, and Select has a 16px
|
|
118
|
+
minimum text size. Small field chrome keeps its compact height and desktop
|
|
119
|
+
typography stays small. This prevents Safari from magnifying the page just to
|
|
120
|
+
focus text; it does not constrain a user's own pinch zoom. App-owned native fields need the same
|
|
121
121
|
editable-text audit.
|
|
122
122
|
|
|
123
123
|
Known Date's field captions and bordered text-like field hints use the same 9px
|
|
124
124
|
inline inset, keeping secondary text aligned with the value rather than the
|
|
125
125
|
field's outer border. This covers Input, Known Date, Number Input, OTP Input,
|
|
126
|
-
Select, Textarea, and Time Input hints
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
126
|
+
Select, Tag Input, Textarea, and Time Input hints.
|
|
127
|
+
OTP Input's label and hint both use the 9px field-text inset. Its code cells
|
|
128
|
+
render whole in a shrink-to-fit host (a start-aligned grid or flex item):
|
|
129
|
+
upstream caps the segments' focus-ring-padded box at the host's width, which
|
|
130
|
+
there is the cells' width minus that padding and so clipped the last cell's
|
|
131
|
+
inline-end edge, and the theme caps it at the host's width plus the padding its
|
|
132
|
+
negative margins reserve. A host narrower than the code still scrolls its
|
|
133
|
+
segments; the `contained` appearance keeps upstream's cap.
|
|
134
|
+
|
|
135
|
+
Checkbox Group, Radio Group, and Color Picker follow Kerf's `Checkbox`,
|
|
136
|
+
`RadioGroup`, and `ColorField`: the control region stays flush, so each box,
|
|
137
|
+
circle, or swatch trigger sits on the host's inline-start edge, while the label
|
|
138
|
+
and hint use the same 9px text column as field labels. Stacked checkboxes and
|
|
139
|
+
radios therefore share one control column with Kerf's own toggle controls;
|
|
140
|
+
place the group where you would place a Kerf `Checkbox` or `RadioGroup` rather
|
|
141
|
+
than adding margin to its parts. Stacked
|
|
142
|
+
default-appearance Radios sit 8px apart (`0.25em` block margins instead of Web
|
|
143
|
+
Awesome's `0.375em`, the first and last radio still flush), so a Radio Group
|
|
144
|
+
keeps the same 28px pitch as a Kerf `RadioGroup` and a Checkbox Group's 8px
|
|
145
|
+
gap; horizontal and button radios keep Web Awesome's spacing.
|
|
146
|
+
|
|
147
|
+
Bordered fields (Input, Known Date, Number Input, OTP Input, Select, Tag
|
|
148
|
+
Input, Textarea, and Time Input) and Switch likewise draw their border or
|
|
149
|
+
track on the host's inline-start edge, exactly where Kerf's `TextField`, `Select`, and `Switch`
|
|
150
|
+
draw theirs, with the label and hint on the same 9px text column. Place a Web
|
|
151
|
+
Awesome field where you would place the Kerf field it stands in for, so a mixed
|
|
152
|
+
form shares one border column and one text column; the UX catalog gives these
|
|
153
|
+
specimens the same `inline-control` alignment as the Kerf field demos. Tag
|
|
154
|
+
Input is Discouraged in favor of Kerf's `TagField`, but an application that
|
|
155
|
+
still renders it gets the same label, hint, height, and border placement as the
|
|
156
|
+
other bordered fields.
|
|
157
|
+
|
|
158
|
+
Slider's complete interactive region receives the
|
|
135
159
|
shared 8px logical inline outer inset through `::part(slider)`, keeping its
|
|
136
|
-
track, markers, references, and hint together without moving the label.
|
|
160
|
+
track, markers, references, and hint together without moving the label. Placed
|
|
161
|
+
where a Kerf `Slider` goes, its label and track land on the Kerf Slider's text
|
|
162
|
+
column.
|
|
163
|
+
|
|
164
|
+
Known Date and Slider size to their container rather than to their content, so
|
|
165
|
+
give them a width (a form column, or the container's full width) instead of a
|
|
166
|
+
shrink-to-fit parent, where Known Date collapses to nothing and Slider to a
|
|
167
|
+
stub. The UX catalog gives Known Date a compact field column and Slider the
|
|
168
|
+
Kerf Slider demo's medium width.
|
|
137
169
|
|
|
138
170
|
Non-field chrome follows explicit, overridable control, surface, and container
|
|
139
171
|
tiers:
|
|
@@ -374,8 +406,14 @@ application off them.
|
|
|
374
406
|
|
|
375
407
|
## Customization
|
|
376
408
|
|
|
377
|
-
|
|
378
|
-
|
|
409
|
+
To change the brand, set Kerf's opt-in `--kui-color-brand` on `:root`: the
|
|
410
|
+
theme derives `--wa-color-brand-fill-loud`, `--wa-color-brand-on-quiet`, and
|
|
411
|
+
`--wa-color-focus` from it (with the matching Kerf tokens) at AA-safe
|
|
412
|
+
lightness, so brand text and the focus ring follow the new hue. See
|
|
413
|
+
[Brand color](./component-selection.md#brand-color).
|
|
414
|
+
|
|
415
|
+
For anything else, load application overrides after the package theme, or
|
|
416
|
+
scope them to the smallest subtree that needs a different identity:
|
|
379
417
|
|
|
380
418
|
```css
|
|
381
419
|
:root {
|
package/docs/workbench-dialog.md
CHANGED
|
@@ -50,7 +50,8 @@ The dialog draws no surface header and no body inset. Its body is a
|
|
|
50
50
|
`Workbench` (`<id>-workbench`):
|
|
51
51
|
|
|
52
52
|
- **The work area's toolbar** carries the dialog's primary title (an xl
|
|
53
|
-
level-2 heading, id `<id>-title`) with an optional `description
|
|
53
|
+
level-2 heading, id `<id>-title`) with an optional `description` (id
|
|
54
|
+
`<id>-description`, the dialog's accessible description), any
|
|
54
55
|
`leading` groups, the `actions`, and the close control. Close stays the
|
|
55
56
|
outermost trailing control, after a hidden right rail's toggle.
|
|
56
57
|
- **The rails** (`leftRail`, `rightRail`) keep their own toolbars, with
|
|
@@ -74,6 +75,8 @@ Escape closes the dialog.
|
|
|
74
75
|
|
|
75
76
|
- `id`, `label` (required): identity and the dialog's accessible name.
|
|
76
77
|
- `title` (required), `description`, `leading`, `actions`.
|
|
78
|
+
- `describedBy`: the id of other content, such as summary copy in `main`,
|
|
79
|
+
that describes the dialog in place of `description`.
|
|
77
80
|
- `main` (required), `mainFooter`, `mainBottomToolbar`.
|
|
78
81
|
- `leftRail`, `rightRail`, `bottomDrawer`: `WorkbenchPanel`s.
|
|
79
82
|
- `compact`, `closeLabel` (default `Close`), `lightDismiss`.
|
package/docs/workbench.md
CHANGED
|
@@ -471,7 +471,13 @@ presents as one, mirroring `wireSidebar`'s compact overlay:
|
|
|
471
471
|
- **Focus moves in, and Tab stays in:** a panel that opens while it presents
|
|
472
472
|
as an overlay takes focus on its first focusable control (typically the
|
|
473
473
|
close control in its header), because it covers the work area and the
|
|
474
|
-
control that opened it.
|
|
474
|
+
control that opened it. That holds whichever control opens it: the
|
|
475
|
+
Workbench's own relocating toggle, or a persistent toggle outside the
|
|
476
|
+
Workbench (an app-bar button whose `aria-controls` names the panel) that
|
|
477
|
+
stays where it is. Focus moves in as soon as the panel can take it, even
|
|
478
|
+
when the app's render shows the panel a frame after the `collapsed` signal
|
|
479
|
+
changes; focus the person has moved elsewhere in the meantime stays put.
|
|
480
|
+
While it is open, Tab and Shift+Tab cycle through
|
|
475
481
|
its controls and never reach the covered work area; Escape, its own close
|
|
476
482
|
control, or an outside press leaves it. This is the ARIA dialog pattern
|
|
477
483
|
`wireSidebar`'s compact overlay follows. A panel opening inline never moves
|
package/docs/writing.md
CHANGED
|
@@ -115,9 +115,9 @@ interpolated value keeps its own spelling. The covered props are:
|
|
|
115
115
|
the `aria-label` of a landmark element (`section`, `nav`, `aside`, `main`,
|
|
116
116
|
`header`, `footer`, `form`, `search`) or landmark role;
|
|
117
117
|
- field and group labels: the `label` (or `ariaLabel`) of `TextField`,
|
|
118
|
-
`Select`, and `RadioGroup`, and, in an application using
|
|
119
|
-
Awesome integration, the `label` of a raw `wa-input`,
|
|
120
|
-
`wa-textarea`, or `wa-radio-group`;
|
|
118
|
+
`Select`, `CheckboxGroup`, and `RadioGroup`, and, in an application using
|
|
119
|
+
the optional Web Awesome integration, the `label` of a raw `wa-input`,
|
|
120
|
+
`wa-select`, `wa-textarea`, `wa-checkbox-group`, or `wa-radio-group`;
|
|
121
121
|
- the accessible names of icon-only controls: an icon-only `Button`'s `ariaLabel`, `PopupMenu` `label`, `SplitButton`
|
|
122
122
|
`optionsLabel`, `SearchField` and `TextField` `clearLabel`, and the `aria-label` of a
|
|
123
123
|
`button` or `wa-button` whose only content is an icon.
|
|
@@ -14,7 +14,7 @@ import { isUiTraversalExcluded } from '../traversal-exclusions.mjs';
|
|
|
14
14
|
// to known title-style props are read (a template literal's static words, never
|
|
15
15
|
// its interpolated values), so prose, descriptions, placeholders, tooltips,
|
|
16
16
|
// checkbox/switch option text, status lines, LoadingSpinner and Skeleton
|
|
17
|
-
// labels, Checkbox/Switch labels and RadioGroup choice labels, and dynamic
|
|
17
|
+
// labels, Checkbox/Switch labels and CheckboxGroup/RadioGroup choice labels, and dynamic
|
|
18
18
|
// data are never judged. The check reports lowercase words title style capitalizes; it
|
|
19
19
|
// does not report capitalized short words, because Apple capitalizes a
|
|
20
20
|
// preposition of any length inside a phrasal verb ("Log In to the Server").
|
|
@@ -179,9 +179,11 @@ const titleStyleProps = new Map([
|
|
|
179
179
|
'Select',
|
|
180
180
|
{ props: ['label', 'ariaLabel'], entries: { choices: ['label'] } },
|
|
181
181
|
],
|
|
182
|
-
// Field and group labels. A Checkbox or Switch label and a
|
|
183
|
-
// choice label are option text, which is sentence style and not
|
|
182
|
+
// Field and group labels. A Checkbox or Switch label and a CheckboxGroup or
|
|
183
|
+
// RadioGroup choice label are option text, which is sentence style and not
|
|
184
|
+
// read.
|
|
184
185
|
['TextField', { props: ['label', 'ariaLabel', 'clearLabel'] }],
|
|
186
|
+
['CheckboxGroup', { props: ['label', 'ariaLabel'] }],
|
|
185
187
|
['RadioGroup', { props: ['label', 'ariaLabel'] }],
|
|
186
188
|
['TabNavigator', { entries: { tabs: ['label'] } }],
|
|
187
189
|
// Region and landmark names: the accessible name of a pane, toolbar,
|
|
@@ -243,14 +245,15 @@ const landmarkRoles = new Set([
|
|
|
243
245
|
'form',
|
|
244
246
|
'search',
|
|
245
247
|
]);
|
|
246
|
-
// A raw Web Awesome field's or radio group's `label` (in an
|
|
247
|
-
// the optional integration) is a field label. The option
|
|
248
|
-
// `wa-checkbox`, `wa-switch`, and `wa-radio` is sentence style and is
|
|
249
|
-
// read.
|
|
248
|
+
// A raw Web Awesome field's, checkbox group's, or radio group's `label` (in an
|
|
249
|
+
// application using the optional integration) is a field label. The option
|
|
250
|
+
// text of `wa-checkbox`, `wa-switch`, and `wa-radio` is sentence style and is
|
|
251
|
+
// not read.
|
|
250
252
|
const fieldLabelTags = new Set([
|
|
251
253
|
'wa-input',
|
|
252
254
|
'wa-select',
|
|
253
255
|
'wa-textarea',
|
|
256
|
+
'wa-checkbox-group',
|
|
254
257
|
'wa-radio-group',
|
|
255
258
|
]);
|
|
256
259
|
const titleToolbarTextSizes = new Set(['xlarge', 'xlarge-fixed', 'large']);
|
package/llms.txt
CHANGED
|
@@ -324,7 +324,9 @@ headingLevel`, `SearchField clearIcon`, `ContentItem frame="framed"` for a
|
|
|
324
324
|
Public ListItem tokens set row, icon, label, and trailing geometry;
|
|
325
325
|
ListHeader `size="xs|sm|m|l|xl"` sets label scale independently of
|
|
326
326
|
`headingLevel` (`sm` keeps the current FieldLabel-sized uppercase style;
|
|
327
|
-
`m`/`l`/`xl` match themed h5/h4/h3).
|
|
327
|
+
`m`/`l`/`xl` match themed h5/h4/h3). The label is one ellipsized line;
|
|
328
|
+
set `wrap` where the whole label must stay readable in a narrow column
|
|
329
|
+
(it wraps instead of truncating). Its height, title padding, and border
|
|
328
330
|
tokens tune section density. A semantic `count` requires `countLabel` and
|
|
329
331
|
excludes other indicators; `badge` and `status` may render together, and
|
|
330
332
|
either may be explicitly `undefined` in conditional app state.
|
|
@@ -398,9 +400,14 @@ headingLevel`, `SearchField clearIcon`, `ContentItem frame="framed"` for a
|
|
|
398
400
|
When a dialog provides another dismissal affordance, add `hide-actions` to
|
|
399
401
|
the `wa-dialog` host; the theme hides its directly exported `header-actions`
|
|
400
402
|
part without an unsupported chained `::part()` selector.
|
|
401
|
-
Checkbox and Radio Group option regions
|
|
402
|
-
|
|
403
|
-
|
|
403
|
+
Checkbox and Radio Group option regions and the Color Picker trigger stay
|
|
404
|
+
flush, so each box, circle, or swatch sits on the host's inline-start edge
|
|
405
|
+
like Kerf's `Checkbox`, `RadioGroup`, and `ColorField`, with the label and
|
|
406
|
+
hint on the 9px text column. Bordered fields and Switch draw their border or
|
|
407
|
+
track on that same edge like `TextField`, `Select`, and `Switch`, so place a
|
|
408
|
+
Web Awesome field exactly where the Kerf field would go.
|
|
409
|
+
Stacked radios sit 8px apart (`0.25em` block margins) for the same 28px
|
|
410
|
+
pitch as `RadioGroup`.
|
|
404
411
|
Slider's complete interactive region uses the shared 8px logical inline outer
|
|
405
412
|
inset without shifting its already-aligned label.
|
|
406
413
|
Known Date's Month/Day/Year captions and bordered text-like field hints align
|
|
@@ -448,9 +455,18 @@ headingLevel`, `SearchField clearIcon`, `ContentItem frame="framed"` for a
|
|
|
448
455
|
or `sizing="grow"` with a `growBasis` that fits), and if a useful search
|
|
449
456
|
cannot fit beside the other controls, set aside those controls while the
|
|
450
457
|
search is open: mark them `visibility="yield-to-expanded-sibling"` (they
|
|
451
|
-
leave a 480px-or-narrower toolbar while a sibling is expanded) or
|
|
452
|
-
|
|
458
|
+
leave a 480px-or-narrower toolbar while a sibling is expanded), or, in a
|
|
459
|
+
`responsive="trailing-priority"` Toolbar, `visibility="yield-to-priority-row"`
|
|
460
|
+
(they leave exactly while the expanded control has its own row, from the
|
|
461
|
+
Toolbar's `responsiveAt`; never restate that breakpoint with `hideBelow`,
|
|
462
|
+
whose strict "below" is one pixel off the Toolbar's inclusive width), or
|
|
463
|
+
gather them behind a "…" `PopupMenu` overflow group. The app decides which
|
|
453
464
|
controls yield.
|
|
465
|
+
- **Change the brand with `--kui-color-brand`.** Set it once on `:root` (any
|
|
466
|
+
color): Kerf derives the brand fill, brand text and links, text on brand
|
|
467
|
+
fills, and the focus ring at WCAG AA, and Web Awesome's brand tokens follow.
|
|
468
|
+
Overriding `--kui-color-brand-fill-loud` alone leaves brand text and the
|
|
469
|
+
focus ring blue.
|
|
454
470
|
- **Toolbars hold only `ToolbarText` and `ToolbarControlGroup`.** Never put a bare
|
|
455
471
|
button, input, link, or loose markup in a `leading`/`center`/`trailing` zone.
|
|
456
472
|
Wrap every control — `SegmentedControl`, `Select`, a collapsible
|
|
@@ -470,9 +486,12 @@ headingLevel`, `SearchField clearIcon`, `ContentItem frame="framed"` for a
|
|
|
470
486
|
`detail.checked` is the state a checkbox choice asks for (the app owns and
|
|
471
487
|
renders that state);
|
|
472
488
|
use `context` with `anchor` + `openPopupMenu` (or `openPopupMenuAt`) and
|
|
473
|
-
`closePopupMenu` for pointer-anchored row menus
|
|
489
|
+
`closePopupMenu` for pointer-anchored row menus, whose `placement` applies
|
|
490
|
+
at the point (`top-start` opens above it); the menu owns its
|
|
474
491
|
zero-footprint anchor host, so add no wrapper or CSS. `openPopupMenuAt`
|
|
475
|
-
also opens a button-triggered menu at the pointer for one opening
|
|
492
|
+
also opens a button-triggered menu at the pointer for one opening
|
|
493
|
+
(`bottom-start`, or `openPopupMenuAt(menu, x, y, { placement: 'top-start' })`
|
|
494
|
+
to open above the point). A row's
|
|
476
495
|
trailing "…" overflow menu is `ListActionRow`'s `trailingMenu` (`label`,
|
|
477
496
|
`icon`, `items`, optional `placement` and `disabled`), which renders a
|
|
478
497
|
`PopupMenu` as the row's sibling trailing control. Do not force a dropdown or button width. Nothing
|
|
@@ -676,8 +695,23 @@ headingLevel`, `SearchField clearIcon`, `ContentItem frame="framed"` for a
|
|
|
676
695
|
the inset rounded card; never restyle a card into a strip.
|
|
677
696
|
- **Use `SplitButton` for a primary action with options.** One loud semantic
|
|
678
697
|
fill with a native primary button and an options trigger (`aria-expanded`,
|
|
679
|
-
`aria-haspopup`); `tone`, `fill`, `
|
|
680
|
-
|
|
698
|
+
`aria-haspopup`); `tone`, `fill`, `shape` (`rounded` or `pill`, as on
|
|
699
|
+
`Button`), `busy`, and `disabled` are props. Never build one from
|
|
700
|
+
`wa-button-group` or app CSS on native buttons.
|
|
701
|
+
- **Keep a Button's focus ring inside a clipping ancestor with
|
|
702
|
+
`focusRingInset`.** `Button` and `SplitButton` draw the ring 1px outside the
|
|
703
|
+
box; inside a container that clips overflow, set `focusRingInset` and the
|
|
704
|
+
ring moves inside the box with a surface-colored line inside it, so it keeps
|
|
705
|
+
3:1 contrast even on a loud fill of its own hue. The line follows the
|
|
706
|
+
surface context sunken, quiet, raised, and highlighted surfaces, app
|
|
707
|
+
`background` colors, and glass chrome publish
|
|
708
|
+
(`--kui-color-surface-current`, `-region`, `-tint`), so it never reads as a
|
|
709
|
+
halo there. Never re-offset the outline from app CSS.
|
|
710
|
+
- **Span a full-width primary action with `fill`.** `<Button tone="brand" fill action="create">Create Project</Button>`
|
|
711
|
+
spans the parent's available inline width instead of shrinking to its
|
|
712
|
+
label, with the label centered and ellipsized once it no longer fits; a
|
|
713
|
+
link (`href`) fills the same way. It is the same `fill` `SplitButton`
|
|
714
|
+
takes. Never set `width: 100%` on a Button from app CSS.
|
|
681
715
|
- **Make any `Button` a toggle with `pressed`.** In content or a pane footer,
|
|
682
716
|
`<Button icon={…} ariaLabel="Show Conversation" emphasis="plain" pressed={open} action="toggle-chat" />`
|
|
683
717
|
renders a native `aria-pressed` button whose pressed look draws on top of
|
|
@@ -724,14 +758,20 @@ headingLevel`, `SearchField clearIcon`, `ContentItem frame="framed"` for a
|
|
|
724
758
|
button, `multiline` with `rows` and a growing `maxRows`, and `maxLength` with
|
|
725
759
|
`showCount`, at the 44px height or `size="compact"`. Call
|
|
726
760
|
`wireTextFields(root)` (`@kerfjs/ui/wire-text-fields`) once for clearing and
|
|
727
|
-
counting.
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
(`
|
|
733
|
-
|
|
734
|
-
|
|
761
|
+
counting. For `type="number"`, pass native `min`, `max`, and `step` as numbers
|
|
762
|
+
or strings (`step="any"` accepts arbitrary decimals). Never render `wa-input`
|
|
763
|
+
or `wa-textarea` for a form field.
|
|
764
|
+
- **Use `Checkbox`, `CheckboxGroup`, `RadioGroup`, and `Switch` for native
|
|
765
|
+
toggles.** `@kerfjs/ui/checkbox` (with `indeterminate` and
|
|
766
|
+
`wireCheckboxes(root)` from `@kerfjs/ui/wire-checkboxes`),
|
|
767
|
+
`@kerfjs/ui/checkbox-group` (`label`, `name`, `choices`, an array `value`;
|
|
768
|
+
a labeled set laid out like RadioGroup), `@kerfjs/ui/radio-group` (`label`,
|
|
769
|
+
`name`, `choices`, `value`; one Tab stop, arrow keys), and
|
|
770
|
+
`@kerfjs/ui/switch` (`role="switch"`, only for a setting that applies
|
|
771
|
+
immediately) draw Kerf boxes over native inputs with hint and error text.
|
|
772
|
+
Stack standalone Checkboxes in a `List gap="xs"`: 8px apart, the same 28px
|
|
773
|
+
pitch as `RadioGroup` and `CheckboxGroup` choices. Never render
|
|
774
|
+
`wa-checkbox`, `wa-checkbox-group`, `wa-radio-group`, or `wa-switch`.
|
|
735
775
|
- **Use `Combobox` to pick or create a value by typing.** A themed native
|
|
736
776
|
input on the ARIA combobox pattern with filtered suggestions and an optional
|
|
737
777
|
create option; `wireComboboxes(root, { onCommit })` reports `{ id, value,
|
|
@@ -770,8 +810,9 @@ created, input }`. Never use a hand-styled `<input list>` + `<datalist>`.
|
|
|
770
810
|
Never use `wa-tooltip`.
|
|
771
811
|
- **Show and hide content with `Disclosure`** (`@kerfjs/ui/disclosure`): a
|
|
772
812
|
native details element with a DisclosureArrow on content-item geometry,
|
|
773
|
-
`plain` / `framed` / `sunken
|
|
774
|
-
|
|
813
|
+
`plain` / `framed` / `sunken`, or `embedded` inside a `ContentItem` (no
|
|
814
|
+
margin, border, or padding of its own, a compact summary, `flush` `Text`
|
|
815
|
+
content); Disclosures sharing a `name` form an accordion. Never use `wa-details` or `wa-accordion`.
|
|
775
816
|
- **Show progress with `ProgressBar` or `ProgressRing`** (`@kerfjs/ui/progress-bar`,
|
|
776
817
|
`@kerfjs/ui/progress-ring`): `value` of `max` (omit `value` for
|
|
777
818
|
indeterminate), a `label`, `valueText`, tones. Never use `wa-progress-bar` or
|
|
@@ -786,7 +827,8 @@ created, input }`. Never use a hand-styled `<input list>` + `<datalist>`.
|
|
|
786
827
|
a native radio group of named round swatches, one Tab stop, a ring that
|
|
787
828
|
does not depend on hue, and a `'transparent'` no-color swatch.
|
|
788
829
|
- **Choose any color with `ColorPicker`** (`@kerfjs/ui/color-picker`): an area,
|
|
789
|
-
|
|
830
|
+
channel sliders that follow the color model (R/G/B, H/S/L, H/S/B, or gray)
|
|
831
|
+
plus opacity, RGB/HSL/HSB/Grayscale fields, hex, presets, and
|
|
790
832
|
samples; call `wireColorPickers(root, { onChange })` and re-render.
|
|
791
833
|
- **Put a color in a form with `ColorField`** (`@kerfjs/ui/color-field`): a
|
|
792
834
|
swatch-and-hex trigger that opens a ColorPicker below it; call
|
|
@@ -962,10 +1004,10 @@ width policy. Keep primary actions visible and offer a complementary overflow
|
|
|
962
1004
|
group. See `docs/component-contract.md` "Toolbar width visibility" for lifecycle
|
|
963
1005
|
details.
|
|
964
1006
|
|
|
965
|
-
Select menus use native viewport shifting with 10px gutters: default menus grow to content (up to 20rem), while `fitMenu` matches trigger width and long labels wrap. Keep popup sizing and position repairs out of consumer CSS.
|
|
1007
|
+
Select menus use native viewport shifting with 10px gutters: default menus grow to content (up to 20rem), while `fitMenu` matches trigger width and long labels wrap. A listbox (like a PopupMenu menu) shifts only along its trigger's edge, never over the trigger: when neither side fits, it opens on the side with more room and scrolls inside its capped height, so a second press on the trigger still closes it. Keep popup sizing and position repairs out of consumer CSS.
|
|
966
1008
|
|
|
967
1009
|
Key sibling `DialogSurface`s that render conditionally with `rootAttributes={{ "data-key": "…" }}` so a re-render morphs each dialog into itself; never wrap one in a keyed app `div`. Modal `DialogSurface` bounds use typed `viewportGutter` and `maxHeight` (`CssLength` or `"viewport"`); explicit caps stay within the dynamic viewport minus both gutters. Omitted props keep the default caps (20px clear of each edge, 80% of the viewport height at 420px wide or narrower). Application render state chooses responsive values, and preview layout remains application-owned. `height` sets a definite bounded modal size and `bodyLayout="fill"` lets one child fill the body; `bodyLayout="fill-last"` keeps leading rows at their height above one filling last child, and `footerPresentation="floating"` (with a typed `footerOffset`) floats a chat composer over the body while the body reserves its measured height; give the transcript `scrollClearance="block-end"` (a `scrollable` `List` or a `Pane`) to let it scroll beneath the composer, with the last message still resting above it, through the published `--kui-scroll-clearance-block-end` context (overlaying chrome publishes `--kui-scroll-clearance-block-start` / `-end`; one scroller per covered edge opts in; a composed Pane opts in through `PaneConfig.scrollClearance` on a Workbench `mainPane` or panel `pane`, a `CollapsiblePanel` `pane`, or a `NavStack` view's `pane`). `headerPresentation="floating"` lays the title bar over the body on the translucent `--kui-chrome-material-raised` (opaque under reduced transparency, increased contrast, and forced colors); the register measures it and the body reserves it, and a `fill` body's only child with `scrollClearance="both"` scrolls beneath both the title bar and a floating composer through the published `--kui-scroll-clearance-block-start`. `presentation="bottom-sheet"` attaches the dialog to the bottom edge with square bottom corners, safe-area and virtual-keyboard clearance behind the panel, and a slide in/out (`enterMotion` / `exitMotion`); `resizable` adds a top grabber separator bounded by `minHeight` and `maxHeight`. `PopoverDialogSurface` is the non-modal, light-dismiss counterpart on the native Popover API, with the same size props and an `embedded` in-flow presentation; `describedBy` sets its `aria-describedby` (usually a title `ToolbarText`'s `descriptionId`).
|
|
968
1010
|
|
|
969
|
-
Render a modal dialog as `DialogSurface` itself, a native `<dialog
|
|
1011
|
+
Render a modal dialog as `DialogSurface` itself, a native `<dialog>` (`alert` renders `role="alertdialog"` for a destructive confirmation; pair it with `describedBy` on the message): `label` (or `labelledBy` with a `heading`, such as an xl `ToolbarText` with a description) names it, `describedBy` (the heading ToolbarText's `descriptionId`, or app-owned summary copy) describes it as `aria-describedby`, `children` is its body, `footer` holds its buttons, and `headerActions` sit before its close button (`withoutHeader` drops the header for content that owns its heading and close control). Open it with `openDialog(dialog)` and close it with `closeDialog(dialog)` from `@kerfjs/ui/surface-scaffold`; never toggle its `open` attribute. The close button, any `data-dialog-close` element inside (a Cancel button), Escape, and, with `lightDismiss`, a backdrop press dispatch a cancelable `kui-request-close` (`detail.source`: `close-button`, `escape`, or `backdrop`); cancel it to keep the dialog open (the panel pulses). The native `close` event fires once it closes. `openDialog` installs the listeners and, while the dialog is open, measures its floating header/footer, follows a bottom sheet's keyboard and grabber, and keeps that state across re-renders; import `@kerfjs/ui/surface-scaffold/register` only when a dialog may open some other way. Do not write `wa-dialog` inside a `DialogSurface`.
|
|
970
1012
|
|
|
971
1013
|
Toolbar visibility measurement probes and invisible state children are helper-owned and survive Kerf rerenders through `data-morph-preserve`. Actual CSS visibility stays stable while rendering removes transient parent markers, preventing geometry-driven render feedback and repeated probe recreation. Toolbar replacement, explicit internal-node removal and disposal retain their existing behavior.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kerfjs/ui",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.108",
|
|
4
4
|
"description": "Accessible, composable UI primitives for kerf applications.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -495,6 +495,11 @@
|
|
|
495
495
|
"browser": "./dist/browser/checkbox.js",
|
|
496
496
|
"import": "./dist/checkbox.js"
|
|
497
497
|
},
|
|
498
|
+
"./checkbox-group": {
|
|
499
|
+
"types": "./dist/checkbox-group.d.ts",
|
|
500
|
+
"browser": "./dist/browser/checkbox-group.js",
|
|
501
|
+
"import": "./dist/checkbox-group.js"
|
|
502
|
+
},
|
|
498
503
|
"./radio-group": {
|
|
499
504
|
"types": "./dist/radio-group.d.ts",
|
|
500
505
|
"browser": "./dist/browser/radio-group.js",
|
|
@@ -626,6 +631,7 @@
|
|
|
626
631
|
"./file-picker.css": "./dist/styles/file-picker.css",
|
|
627
632
|
"./text-field.css": "./dist/styles/text-field.css",
|
|
628
633
|
"./checkbox.css": "./dist/styles/checkbox.css",
|
|
634
|
+
"./checkbox-group.css": "./dist/styles/checkbox-group.css",
|
|
629
635
|
"./slider.css": "./dist/styles/slider.css",
|
|
630
636
|
"./radio-group.css": "./dist/styles/radio-group.css",
|
|
631
637
|
"./switch.css": "./dist/styles/switch.css",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/actions/popup-menu/internal/popup-menu-content-cache.ts","../src/components/actions/popup-menu/popup-menu.tsx"],"names":[],"mappings":"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","file":"chunk-7W3GKMN4.js","sourcesContent":["import type { SafeHtml } from 'kerfjs';\n\nimport type { PopupMenuEntry } from '../popup-menu.js';\n\n/**\n * Streaming content hash: slot 0 is the FNV-1a digest that names\n * the menu's `data-key`; slot 1 is an independent check hash (a 31-multiplier\n * polynomial whose dependency chain runs in parallel with the digest's), so\n * the markup cache is keyed by about 64 bits and a digest collision cannot\n * serve another menu's markup. An `Int32Array` keeps both accumulators unboxed\n * between calls.\n */\nconst hashState = new Int32Array(2);\n\nfunction hashCode(code: number): void {\n hashState[0] = Math.imul(hashState[0]! ^ code, 0x01000193);\n hashState[1] = (Math.imul(hashState[1]!, 31) + code) | 0;\n}\n\n/**\n * Hash a string's code points. A surrogate pair is combined into one code\n * point, matching the former `for…of` iteration over the joined key, so the\n * digest, and with it the rendered `data-key`, is unchanged.\n */\nfunction hashText(text: string): void {\n let digest = hashState[0]!;\n let check = hashState[1]!;\n const length = text.length;\n for (let index = 0; index < length; index++) {\n let code = text.charCodeAt(index);\n if ((code & 0xfc00) === 0xd800 && index + 1 < length) {\n const low = text.charCodeAt(index + 1);\n if ((low & 0xfc00) === 0xdc00) {\n code = ((code - 0xd800) << 10) + (low - 0xdc00) + 0x10000;\n index++;\n }\n }\n digest = Math.imul(digest ^ code, 0x01000193);\n check = (Math.imul(check, 31) + code) | 0;\n }\n hashState[0] = digest;\n hashState[1] = check;\n}\n\n/**\n * Parts at least this long (icon and details markup) are hashed through\n * {@link hashLongText}: by a memoized digest of the whole string instead of\n * character by character, so a glyph repeated on every row costs a map lookup.\n */\nexport const LONG_PART_LENGTH = 64;\n\n/** Bound on memoized long-part digests; past it the oldest is evicted. */\nexport const PART_DIGEST_LIMIT = 4096;\nconst partDigests = new Map<string, readonly [number, number]>();\n\n/**\n * Feed a long part as its own two hashes. A Lucide SVG is a few hundred\n * characters and repeats across every row's menu, so hashing its characters\n * dominated an unchanged menu's cost; V8 hashes a string key natively (once\n * per string object) and compares equal strings quickly.\n */\nfunction hashLongText(text: string): void {\n let pair = partDigests.get(text);\n if (!pair) {\n const digest = hashState[0]!;\n const check = hashState[1]!;\n hashState[0] = 0x811c9dc5;\n hashState[1] = 0x9747b28c;\n hashText(text);\n pair = [hashState[0], hashState[1]];\n hashState[0] = digest;\n hashState[1] = check;\n if (partDigests.size >= PART_DIGEST_LIMIT)\n partDigests.delete(partDigests.keys().next().value!);\n partDigests.set(text, pair);\n }\n hashCode(pair[0]);\n hashCode(pair[1]);\n}\n\n/** Hash a part, through its memoized digest when it is long. */\nfunction hashPart(text: string): void {\n if (text.length >= LONG_PART_LENGTH) hashLongText(text);\n else hashText(text);\n}\n\nconst PART_SEPARATOR = 0x00;\nconst FIELD_SEPARATOR = 0x01;\n\n/**\n * Feed one entry's structure: the fields, in order, separated by U+0001, with\n * each submenu entry appended the same way.\n */\nfunction hashEntry(entry: PopupMenuEntry): void {\n if (entry.kind === 'divider') {\n hashCode(0x2d); // '-'\n return;\n }\n if (entry.kind === 'heading') {\n hashCode(0x23); // '#'\n hashText(entry.label);\n return;\n }\n hashText(entry.label);\n hashCode(FIELD_SEPARATOR);\n hashText(entry.action ?? '');\n hashCode(FIELD_SEPARATOR);\n hashText(entry.value ?? '');\n hashCode(FIELD_SEPARATOR);\n hashCode(entry.disabled ? 0x31 : 0x30);\n hashCode(FIELD_SEPARATOR);\n hashCode(entry.checked === undefined ? 0x2d : entry.checked ? 0x31 : 0x30);\n hashCode(FIELD_SEPARATOR);\n hashText(entry.tone ?? '');\n hashCode(FIELD_SEPARATOR);\n hashText(entry.disabledReason ?? '');\n hashCode(FIELD_SEPARATOR);\n // SafeHtml stringifies to its stored markup without copying, and the key\n // must change when an icon or details change.\n hashPart(String(entry.icon ?? ''));\n hashCode(FIELD_SEPARATOR);\n hashPart(String(entry.details ?? ''));\n hashCode(FIELD_SEPARATOR);\n hashText(entry.attributes ? JSON.stringify(entry.attributes) : '{}');\n for (const child of entry.submenu ?? []) {\n hashCode(FIELD_SEPARATOR);\n hashEntry(child);\n }\n}\n\n/**\n * Bounded, least-recently-used cache of each menu's trigger and entry markup,\n * keyed by its content hashes. A list that re-renders unchanged menus then\n * pays one hash pass per menu instead of rendering every entry again, which\n * the morph would discard under `data-morph-skip-children` anyway. Sized above\n * a large list's distinct menus; past it, renders fall back to fresh markup.\n */\nexport const MARKUP_CACHE_LIMIT = 1024;\nconst markupCache = new Map<string, SafeHtml>();\n\n/**\n * The menu's `data-key` and its cache key. The digest covers, in order and\n * separated by U+0000, the trigger parts and then each entry, whose fields are\n * separated by U+0001; it is the FNV-1a hash of that joined text, except that\n * a part of {@link LONG_PART_LENGTH} or more characters contributes its own\n * two hashes in place of its characters.\n */\nexport function popupMenuContentKey(\n triggerParts: readonly string[],\n items: readonly PopupMenuEntry[],\n): { key: string; cacheKey: string } {\n hashState[0] = 0x811c9dc5;\n hashState[1] = 0x9747b28c;\n for (let index = 0; index < triggerParts.length; index++) {\n if (index > 0) hashCode(PART_SEPARATOR);\n hashPart(triggerParts[index]!);\n }\n for (const entry of items) {\n hashCode(PART_SEPARATOR);\n hashEntry(entry);\n }\n const digest = (hashState[0]! >>> 0).toString(36);\n return {\n key: `kui-popup-menu-${digest}`,\n cacheKey: `${digest}:${(hashState[1]! >>> 0).toString(36)}`,\n };\n}\n\n/** Cached markup for a cache key, refreshed as most recently used. */\nexport function cachedPopupMenuContent(cacheKey: string): SafeHtml | undefined {\n const content = markupCache.get(cacheKey);\n if (content) {\n markupCache.delete(cacheKey);\n markupCache.set(cacheKey, content);\n }\n return content;\n}\n\n/**\n * Remember rendered markup, evicting the least recently used entry when full.\n * Only static markup is reusable: content that holds a keyed list must be\n * rendered in each pass for the list reconciler.\n */\nexport function cachePopupMenuContent(\n cacheKey: string,\n content: SafeHtml,\n): void {\n if (content.__segment.kind !== 'static') return;\n if (markupCache.size >= MARKUP_CACHE_LIMIT)\n markupCache.delete(markupCache.keys().next().value!);\n markupCache.set(cacheKey, content);\n}\n\n/** Test hook: drop cached menu markup and memoized part digests. */\nexport function _clearPopupMenuMarkupCache(): void {\n markupCache.clear();\n partDigests.clear();\n}\n\n/** Test hook: the number of memoized long-part digests. */\nexport function _popupMenuPartDigestCount(): number {\n return partDigests.size;\n}\n\n/** Test hook: the number of cached menu markups. */\nexport function _popupMenuMarkupCacheSize(): number {\n return markupCache.size;\n}\n","import type { SafeHtml } from 'kerfjs';\nimport { Check } from 'lucide';\n\nimport type { KerfUiContent } from '../../../shared/content/semantic-content.js';\nimport {\n PopupCaretGlyph,\n PopupSubmenuGlyph,\n} from '../../../shared/popup/popup-glyphs.js';\nimport { LucideIcon } from '../../media/lucide-icon/lucide-icon.js';\nimport {\n cachedPopupMenuContent,\n cachePopupMenuContent,\n popupMenuContentKey,\n} from './internal/popup-menu-content-cache.js';\nimport {\n POPUP_MENU_CLOSE_EVENT,\n POPUP_MENU_OPEN_EVENT,\n type PopupMenuOpenDetail,\n} from './internal/popup-menu-events.js';\n\nexport type PopupMenuPlacement =\n 'bottom-start' | 'bottom' | 'bottom-end' | 'top-start' | 'top' | 'top-end';\n\ntype PopupMenuDataAttributes = Readonly<\n Record<`data-${string}`, string | undefined> & {\n 'data-action'?: never;\n 'data-component'?: never;\n 'data-key'?: never;\n 'data-morph-skip-children'?: never;\n }\n>;\n\n/** One command in the menu. */\nexport interface PopupMenuItem {\n kind?: 'item';\n label: string;\n /** Delegated `data-action` the app handles when the item is chosen. */\n action?: string;\n /** The item's value, reported in the menu's `kui-menu-select` event. */\n value?: string;\n /** Leading icon, typically a `LucideIcon`, before the label. */\n icon?: SafeHtml;\n /** A checkbox choice, including nested choices; omit for a plain command. */\n checked?: boolean;\n /** Trailing safe content, such as secondary status. */\n details?: SafeHtml;\n /** Destructive command styling. */\n tone?: 'default' | 'danger';\n disabled?: boolean;\n /** Native tooltip text for a disabled command. */\n disabledReason?: string;\n /** Application `data-*` metadata such as a record id. */\n attributes?: PopupMenuDataAttributes;\n /** Child commands, headings, and dividers opened by hover or keyboard navigation. */\n submenu?: readonly PopupMenuEntry[];\n}\n\n/** A labeled group heading; items that follow it belong to the group. */\nexport interface PopupMenuHeading {\n kind: 'heading';\n label: string;\n}\n\n/** A separator between groups of items. */\nexport interface PopupMenuDivider {\n kind: 'divider';\n}\n\nexport type PopupMenuEntry =\n PopupMenuItem | PopupMenuHeading | PopupMenuDivider;\n\ntype PopupMenuTriggerName =\n | {\n /** Visible trigger text, which is also its accessible name. */\n text: string;\n label?: never;\n triggerContent?: never;\n }\n | {\n text?: never;\n /**\n * Accessible name of an icon-only trigger, rendered as visually hidden\n * text inside the trigger button.\n */\n label: string;\n triggerContent?: never;\n }\n | {\n text?: never;\n /**\n * The trigger's accessible name (\"Status: Started\"); the visible\n * content is hidden from assistive technology.\n */\n label: string;\n /**\n * Visible content that is the whole trigger, such as a `Badge` showing\n * the current status. Kerf owns the bare button around it (no chrome,\n * a pill focus ring, the pointer cursor, `aria-haspopup`), and the\n * caret is off unless `caret` is set.\n */\n triggerContent: KerfUiContent;\n icon?: never;\n };\n\n/** A context PopupMenu's viewport anchor point. */\nexport interface PopupMenuAnchor {\n x: number;\n y: number;\n}\n\ntype PopupMenuTrigger =\n | (PopupMenuTriggerName & { context?: false; anchor?: never })\n | {\n context: true;\n label: string;\n triggerContent?: never;\n /**\n * The viewport point (CSS pixels, as in `MouseEvent.clientX/Y`) the\n * menu opens from. Rendered into the menu itself, so it survives\n * re-renders and replacement; open it with `openPopupMenu`.\n */\n anchor?: PopupMenuAnchor;\n text?: never;\n icon?: never;\n caret?: never;\n disabled?: never;\n };\n\nexport type PopupMenuProps = PopupMenuTrigger & {\n /** Trigger icon, typically a `LucideIcon`, before any visible text. */\n icon?: KerfUiContent;\n items: readonly PopupMenuEntry[];\n /** Where the menu opens relative to its trigger. Defaults to `bottom-start`. */\n placement?: PopupMenuPlacement;\n /** Show the disclosure caret after the trigger content. Defaults to true. */\n caret?: boolean;\n disabled?: boolean;\n /** Application `data-*` metadata on the menu root. */\n rootAttributes?: PopupMenuDataAttributes;\n /** Native named-slot assignment when composed inside a web component. */\n slot?: string;\n};\n\ntype PopupMenuCommand = PopupMenuItem & { kind?: 'item' };\n\nconst isCommand = (entry: PopupMenuEntry): entry is PopupMenuCommand =>\n entry.kind !== 'heading' && entry.kind !== 'divider';\n\n/** Whether a menu level holds a checkbox choice, so its plain items reserve the check column. */\nfunction hasChoice(entries: readonly PopupMenuEntry[]): boolean {\n return entries.some(\n (entry) => isCommand(entry) && entry.checked !== undefined,\n );\n}\n\nfunction renderItem(entry: PopupMenuCommand, levelHasChoice: boolean) {\n // A plain command beside checkbox choices keeps their check column, so its\n // icon and label line up with theirs.\n const choice = entry.checked !== undefined;\n const item = (\n <button\n {...(entry.attributes ?? {})}\n type=\"button\"\n class=\"kui-popup-menu__item\"\n role={choice ? 'menuitemcheckbox' : 'menuitem'}\n tabindex={-1}\n data-action={entry.action}\n value={entry.value}\n data-tone={entry.tone === 'danger' ? 'danger' : undefined}\n aria-checked={choice ? String(entry.checked) : undefined}\n aria-disabled={entry.disabled ? 'true' : undefined}\n aria-haspopup={entry.submenu ? 'menu' : undefined}\n aria-expanded={entry.submenu ? 'false' : undefined}\n title={entry.disabled ? entry.disabledReason : undefined}\n >\n {levelHasChoice ? (\n <span class=\"kui-popup-menu__check\" aria-hidden=\"true\">\n {choice ? <LucideIcon icon={Check} name=\"check\" /> : null}\n </span>\n ) : null}\n {entry.icon ? (\n <span class=\"kui-popup-menu__icon\" aria-hidden=\"true\">\n {entry.icon}\n </span>\n ) : null}\n <span class=\"kui-popup-menu__item-label\">{entry.label}</span>\n {entry.details ? (\n <span class=\"kui-popup-menu__details\">{entry.details}</span>\n ) : null}\n {entry.submenu ? (\n <span class=\"kui-popup-menu__submenu-icon\" aria-hidden=\"true\">\n <PopupSubmenuGlyph />\n </span>\n ) : null}\n </button>\n );\n if (!entry.submenu) return item;\n return (\n <div class=\"kui-popup-menu__submenu-host\" role=\"none\">\n {item}\n {renderMenu(entry.submenu, entry.label, true)}\n </div>\n );\n}\n\n/**\n * One menu level: its entries in order, with each heading opening a labeled\n * group that holds the items up to the next heading or divider.\n */\nfunction renderEntries(entries: readonly PopupMenuEntry[]): SafeHtml[] {\n const levelHasChoice = hasChoice(entries);\n const out: SafeHtml[] = [];\n let index = 0;\n while (index < entries.length) {\n const entry = entries[index]!;\n if (entry.kind === 'divider') {\n out.push(<div class=\"kui-popup-menu__divider\" role=\"separator\" />);\n index += 1;\n continue;\n }\n if (entry.kind === 'heading') {\n const members: SafeHtml[] = [];\n index += 1;\n while (index < entries.length) {\n const member = entries[index]!;\n if (!isCommand(member)) break;\n members.push(renderItem(member, levelHasChoice));\n index += 1;\n }\n out.push(\n <div\n class=\"kui-popup-menu__group\"\n role=\"group\"\n aria-label={entry.label}\n >\n <div class=\"kui-popup-menu__heading\" aria-hidden=\"true\">\n {entry.label}\n </div>\n {members}\n </div>,\n );\n continue;\n }\n out.push(renderItem(entry, levelHasChoice));\n index += 1;\n }\n return out;\n}\n\nfunction renderMenu(\n entries: readonly PopupMenuEntry[],\n name: string | undefined,\n submenu: boolean,\n) {\n return (\n <div\n class=\"kui-popup-menu__menu\"\n role=\"menu\"\n popover=\"manual\"\n tabindex={-1}\n aria-label={name}\n data-submenu={submenu ? '' : undefined}\n >\n {renderEntries(entries)}\n </div>\n );\n}\n\n/** A rendered PopupMenu root, the element the open and close helpers take. */\nexport type PopupMenuElement = HTMLElement;\n\nfunction requestOpen(menu: PopupMenuElement, detail: PopupMenuOpenDetail) {\n menu.dispatchEvent(\n new CustomEvent<PopupMenuOpenDetail>(POPUP_MENU_OPEN_EVENT, { detail }),\n );\n}\n\n/**\n * Open a PopupMenu at viewport pointer coordinates. A `context` menu rendered\n * without `anchor` moves its invisible anchor there (a menu rendered with\n * `anchor` opens with `openPopupMenu` instead). A button-triggered menu (such\n * as a `ListActionRow` trailing menu) opens `bottom-start` from the point,\n * like a context menu, for this opening only; its next opening returns to its\n * trigger and placement. Needs the `/register` entry.\n */\nexport function openPopupMenuAt(\n menu: PopupMenuElement,\n x: number,\n y: number,\n): void {\n menu.style.setProperty('--kui-popup-menu-context-x', `${x}px`);\n menu.style.setProperty('--kui-popup-menu-context-y', `${y}px`);\n requestOpen(menu, { point: { x, y } });\n}\n\n/**\n * Open a PopupMenu from its trigger, or a context PopupMenu at the `anchor`\n * it was rendered with (or the point a previous `openPopupMenuAt` set).\n * Needs the `/register` entry.\n */\nexport function openPopupMenu(menu: PopupMenuElement): void {\n requestOpen(menu, {});\n}\n\n/** Close an open PopupMenu, and any submenu it shows. */\nexport function closePopupMenu(menu: PopupMenuElement): void {\n menu.dispatchEvent(new CustomEvent(POPUP_MENU_CLOSE_EVENT));\n}\n\ninterface PopupMenuContentProps {\n context: boolean;\n label: string | undefined;\n icon: KerfUiContent;\n triggerContent: KerfUiContent;\n text: string | undefined;\n caret: boolean;\n disabled: boolean;\n items: readonly PopupMenuEntry[];\n}\n\n/** The trigger and the menu: everything inside the root. */\nfunction renderContent({\n context,\n label,\n icon,\n triggerContent,\n text,\n caret,\n disabled,\n items,\n}: PopupMenuContentProps): SafeHtml {\n return (\n <>\n {context ? (\n <button\n type=\"button\"\n class=\"kui-popup-menu__context-anchor\"\n aria-hidden=\"true\"\n tabindex={-1}\n />\n ) : (\n <button\n type=\"button\"\n class=\"kui-popup-menu__trigger\"\n aria-haspopup=\"menu\"\n aria-expanded=\"false\"\n disabled={disabled}\n >\n {icon}\n {triggerContent === undefined ? null : (\n <span class=\"kui-popup-menu__trigger-content\" aria-hidden=\"true\">\n {triggerContent}\n </span>\n )}\n {text ? (\n <span class=\"kui-popup-menu__text\">{text}</span>\n ) : (\n <span class=\"kui-popup-menu__label\">{label}</span>\n )}\n {caret ? (\n <span class=\"kui-popup-menu__caret\" aria-hidden=\"true\">\n <PopupCaretGlyph />\n </span>\n ) : null}\n </button>\n )}\n {renderMenu(items, text ?? label, false)}\n </>\n );\n}\n\n/**\n * An action menu: a trigger button that opens a list of commands in the top\n * layer. A `single` `ToolbarControlGroup` (with `nestedDropdown`) or a\n * `PopupSurface` sizes and insets it as usual. Items dispatch through\n * delegated `data-action`s, and the root reports each choice as a bubbling\n * `kui-menu-select` event; the app owns the commands and any open-state reaction.\n * Import `@kerfjs/ui/popup-menu/register` once to wire opening, keyboard\n * navigation, submenus, and dismissal. At viewport widths up to 480px,\n * submenus appear above or below their parent menu so their rows do not cover\n * the parent list.\n */\nexport function PopupMenu({\n text,\n label,\n icon,\n triggerContent,\n items,\n placement = 'bottom-start',\n caret = triggerContent === undefined,\n disabled = false,\n rootAttributes = {},\n slot,\n context = false,\n anchor,\n}: PopupMenuProps) {\n if (\n anchor !== undefined &&\n (!context || !Number.isFinite(anchor.x) || !Number.isFinite(anchor.y))\n )\n throw new RangeError(\n 'PopupMenu anchor requires context mode and finite x and y coordinates',\n );\n // The register owns the open menu's state (shown popovers, `aria-expanded`,\n // checked toggles, focus), so the morph skips the children; the content key\n // rebuilds the menu when they change. An unchanged menu reuses its cached\n // markup and costs one hash pass.\n const { key, cacheKey } = popupMenuContentKey(\n [\n text ?? '',\n label ?? '',\n String(icon ?? ''),\n String(caret),\n String(disabled),\n String(context),\n // Only a content trigger adds a part, so other menus keep their keys.\n ...(triggerContent === undefined ? [] : [String(triggerContent)]),\n ],\n items,\n );\n let content = cachedPopupMenuContent(cacheKey);\n if (!content) {\n content = renderContent({\n context,\n label,\n icon,\n triggerContent,\n text,\n caret,\n disabled,\n items,\n });\n cachePopupMenuContent(cacheKey, content);\n }\n return (\n <span\n {...rootAttributes}\n class=\"kui-popup-menu\"\n data-component=\"popup-menu\"\n data-key={key}\n data-morph-skip-children\n data-trigger={\n context\n ? 'context'\n : triggerContent === undefined\n ? 'button'\n : 'content'\n }\n data-placement={placement}\n style={\n anchor\n ? `--kui-popup-menu-context-x:${anchor.x}px;--kui-popup-menu-context-y:${anchor.y}px`\n : undefined\n }\n slot={slot}\n >\n {content}\n </span>\n );\n}\n"]}
|