@kerfjs/ui 4.4.1 → 5.0.0-beta.11
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 +241 -10
- package/ai/component-catalog.json +4828 -0
- package/ai/component-catalog.schema.json +79 -0
- package/ai/public-api-signatures-v1.md +1371 -0
- package/ai/skill.md +73 -11
- package/ai/webawesome-jsx-signatures-v1.md +106 -0
- package/dist/app-tab.d.ts +14 -2
- package/dist/app-tab.js +3 -1
- package/dist/browser/app-tab.js +3 -2
- package/dist/browser/disclosure-arrow.js +4 -0
- package/dist/browser/empty-state.js +3 -3
- package/dist/browser/floating-toolbar.js +3 -0
- package/dist/browser/list-action-row.js +4 -0
- package/dist/browser/list-header.js +6 -0
- package/dist/browser/list-item.js +4 -0
- package/dist/browser/loading-spinner.js +2 -2
- package/dist/browser/lucide-icon.js +2 -2
- package/dist/browser/panel-header.js +7 -0
- package/dist/browser/resizable-region.js +2 -2
- package/dist/browser/segmented-control.js +3 -2
- package/dist/browser/select.js +4 -3
- package/dist/browser/skeleton.js +3 -0
- package/dist/browser/state-banner.js +3 -2
- package/dist/browser/tab-bar.js +2 -2
- package/dist/browser/token-search-field.js +4 -0
- package/dist/browser/toolbar-control-group.js +2 -2
- package/dist/browser/toolbar-text.js +3 -2
- package/dist/browser/toolbar.js +2 -2
- package/dist/browser/value-table.js +3 -2
- package/dist/catalog.d.ts +119 -0
- package/dist/catalog.js +146 -0
- package/dist/catalog.js.map +1 -0
- package/dist/chunk-2Z5TWHKQ.js +34 -0
- package/dist/chunk-2Z5TWHKQ.js.map +1 -0
- package/dist/chunk-37T3VNIZ.js +19 -0
- package/dist/chunk-37T3VNIZ.js.map +1 -0
- package/dist/chunk-56RJXPHG.js +43 -0
- package/dist/chunk-56RJXPHG.js.map +1 -0
- package/dist/chunk-5ATCDSQS.js +42 -0
- package/dist/chunk-5ATCDSQS.js.map +1 -0
- package/dist/chunk-5E2HVJ33.js +10 -0
- package/dist/chunk-5E2HVJ33.js.map +1 -0
- package/dist/chunk-7BR3DO2A.js +31 -0
- package/dist/chunk-7BR3DO2A.js.map +1 -0
- package/dist/chunk-7JB7VPRI.js +34 -0
- package/dist/chunk-7JB7VPRI.js.map +1 -0
- package/dist/chunk-7LDCTU7D.js +134 -0
- package/dist/chunk-7LDCTU7D.js.map +1 -0
- package/dist/{chunk-4HHCYSGT.js → chunk-AQNKJRFH.js} +2 -2
- package/dist/{chunk-4HHCYSGT.js.map → chunk-AQNKJRFH.js.map} +1 -1
- package/dist/{chunk-MBR3TYNL.js → chunk-AVZHJN6C.js} +3 -3
- package/dist/chunk-AVZHJN6C.js.map +1 -0
- package/dist/chunk-B24C4A6T.js +22 -0
- package/dist/chunk-B24C4A6T.js.map +1 -0
- package/dist/chunk-BLK3UEEI.js +23 -0
- package/dist/chunk-BLK3UEEI.js.map +1 -0
- package/dist/chunk-CH4SQ6H5.js +39 -0
- package/dist/chunk-CH4SQ6H5.js.map +1 -0
- package/dist/chunk-I7INRJO4.js +17 -0
- package/dist/chunk-I7INRJO4.js.map +1 -0
- package/dist/chunk-L4OKFDJY.js +24 -0
- package/dist/chunk-L4OKFDJY.js.map +1 -0
- package/dist/chunk-LCOBDOCC.js +265 -0
- package/dist/chunk-LCOBDOCC.js.map +1 -0
- package/dist/{chunk-3O7ZEUW2.js → chunk-M5JRI6MN.js} +3 -3
- package/dist/chunk-M5JRI6MN.js.map +1 -0
- package/dist/{chunk-MFKMHK5W.js → chunk-RNPGFQ33.js} +3 -3
- package/dist/chunk-RNPGFQ33.js.map +1 -0
- package/dist/chunk-RUNWEPR4.js +10 -0
- package/dist/chunk-RUNWEPR4.js.map +1 -0
- package/dist/chunk-XE5ZLPCC.js +20 -0
- package/dist/chunk-XE5ZLPCC.js.map +1 -0
- package/dist/{chunk-MQIZAD4A.js → chunk-XXMOARTH.js} +5 -3
- package/dist/chunk-XXMOARTH.js.map +1 -0
- package/dist/{chunk-TRNBHPNW.js → chunk-YGN7OMLT.js} +11 -8
- package/dist/chunk-YGN7OMLT.js.map +1 -0
- package/dist/collapsible-panel.d.ts +66 -0
- package/dist/collapsible-panel.js +22 -0
- package/dist/collapsible-panel.js.map +1 -0
- package/dist/device-class.d.ts +62 -0
- package/dist/device-class.js +75 -0
- package/dist/device-class.js.map +1 -0
- package/dist/disclosure-arrow.d.ts +14 -0
- package/dist/disclosure-arrow.js +4 -0
- package/dist/disclosure-arrow.js.map +1 -0
- package/dist/floating-toolbar.d.ts +28 -0
- package/dist/floating-toolbar.js +3 -0
- package/dist/floating-toolbar.js.map +1 -0
- package/dist/index.d.ts +11 -6
- package/dist/index.js +22 -16
- package/dist/list-action-row.d.ts +52 -0
- package/dist/list-action-row.js +5 -0
- package/dist/list-action-row.js.map +1 -0
- package/dist/list-header.d.ts +43 -0
- package/dist/list-header.js +7 -0
- package/dist/list-header.js.map +1 -0
- package/dist/list-item.d.ts +33 -0
- package/dist/list-item.js +5 -0
- package/dist/list-item.js.map +1 -0
- package/dist/nav-stack.d.ts +38 -0
- package/dist/nav-stack.js +5 -0
- package/dist/nav-stack.js.map +1 -0
- package/dist/panel-header.d.ts +36 -0
- package/dist/panel-header.js +7 -0
- package/dist/panel-header.js.map +1 -0
- package/dist/resizable-region.d.ts +3 -1
- package/dist/resizable-region.js +1 -1
- package/dist/segmented-control.d.ts +3 -1
- package/dist/segmented-control.js +2 -1
- package/dist/select-register.d.ts +5 -1
- package/dist/select-register.js +3 -0
- package/dist/select-register.js.map +1 -1
- package/dist/select.d.ts +5 -2
- package/dist/select.js +2 -1
- package/dist/skeleton.d.ts +24 -0
- package/dist/skeleton.js +3 -0
- package/dist/skeleton.js.map +1 -0
- package/dist/split-view.d.ts +42 -0
- package/dist/split-view.js +21 -0
- package/dist/split-view.js.map +1 -0
- package/dist/state-banner.d.ts +3 -1
- package/dist/state-banner.js +2 -1
- package/dist/styles/app-tab.css +152 -0
- package/dist/styles/catalog.css +522 -0
- package/dist/styles/collapsible-panel.css +146 -0
- package/dist/styles/disclosure-arrow.css +23 -0
- package/dist/styles/empty-state.css +47 -0
- package/dist/styles/floating-toolbar.css +38 -0
- package/dist/styles/foundation.css +347 -0
- package/dist/styles/layout.css +73 -0
- package/dist/styles/list-action-row.css +160 -0
- package/dist/styles/list-header.css +160 -0
- package/dist/styles/list-item.css +123 -0
- package/dist/styles/loading-spinner.css +21 -0
- package/dist/styles/lucide-icon.css +6 -0
- package/dist/styles/nav-stack.css +111 -0
- package/dist/styles/panel-header.css +64 -0
- package/dist/styles/resizable-region.css +111 -0
- package/{src → dist/styles}/segmented-control.css +62 -31
- package/dist/styles/select.css +138 -0
- package/dist/styles/skeleton.css +31 -0
- package/dist/styles/split-view.css +35 -0
- package/dist/styles/state-banner.css +173 -0
- package/dist/styles/styles.css +23 -0
- package/dist/styles/tab-bar.css +45 -0
- package/dist/styles/tab-scaffold.css +84 -0
- package/dist/styles/token-search-field.css +289 -0
- package/dist/styles/toolbar-control-group.css +225 -0
- package/dist/styles/toolbar-text.css +40 -0
- package/dist/styles/toolbar.css +61 -0
- package/dist/styles/value-table.css +75 -0
- package/{src → dist/styles}/webawesome.css +119 -29
- package/dist/styles/workbench.css +94 -0
- package/dist/tab-bar.d.ts +8 -1
- package/dist/tab-bar.js +1 -1
- package/dist/tab-scaffold.d.ts +30 -0
- package/dist/tab-scaffold.js +16 -0
- package/dist/tab-scaffold.js.map +1 -0
- package/dist/token-search-field.d.ts +43 -0
- package/dist/token-search-field.js +4 -0
- package/dist/token-search-field.js.map +1 -0
- package/dist/toolbar-control-group.d.ts +3 -1
- package/dist/toolbar-control-group.js +1 -1
- package/dist/toolbar-text.d.ts +15 -3
- package/dist/toolbar-text.js +2 -1
- package/dist/value-table.d.ts +12 -2
- package/dist/value-table.js +2 -1
- package/dist/webawesome.d.ts +100 -0
- package/dist/webawesome.js +3 -0
- package/dist/webawesome.js.map +1 -0
- package/dist/wire-catalog.d.ts +26 -0
- package/dist/wire-catalog.js +48 -0
- package/dist/wire-catalog.js.map +1 -0
- package/dist/wire-nav-stack.d.ts +15 -0
- package/dist/wire-nav-stack.js +88 -0
- package/dist/wire-nav-stack.js.map +1 -0
- package/dist/wire-resizable-regions.js +1 -1
- package/dist/wire-sidebar.d.ts +43 -0
- package/dist/wire-sidebar.js +117 -0
- package/dist/wire-sidebar.js.map +1 -0
- package/dist/wire-tab-bars.d.ts +15 -2
- package/dist/wire-tab-bars.js +1 -1
- package/dist/wire-tab-scaffold.d.ts +11 -0
- package/dist/wire-tab-scaffold.js +16 -0
- package/dist/wire-tab-scaffold.js.map +1 -0
- package/dist/wire-token-search-fields.d.ts +106 -0
- package/dist/wire-token-search-fields.js +5 -0
- package/dist/wire-token-search-fields.js.map +1 -0
- package/dist/workbench.d.ts +33 -0
- package/dist/workbench.js +17 -0
- package/dist/workbench.js.map +1 -0
- package/docs/accessibility.md +112 -3
- package/docs/app-layouts.md +61 -0
- package/docs/catalog.md +155 -0
- package/docs/collapsible-panel.md +102 -0
- package/docs/component-contract.md +169 -4
- package/docs/component-selection.md +186 -0
- package/docs/design/templates/empty-state/filtered-dark.svg +1 -0
- package/docs/design/templates/empty-state/filtered.svg +1 -0
- package/docs/design/templates/empty-state/with-action-dark.svg +1 -0
- package/docs/design/templates/empty-state/with-action.svg +1 -0
- package/docs/design/templates/empty-state-dark.svg +7 -0
- package/docs/design/templates/empty-state.svg +7 -0
- package/docs/design/templates/list-action-row/default-dark.svg +1 -0
- package/docs/design/templates/list-action-row/default.svg +1 -0
- package/docs/design/templates/list-action-row/selected-dark.svg +1 -0
- package/docs/design/templates/list-action-row/selected.svg +1 -0
- package/docs/design/templates/list-action-row-dark.svg +7 -0
- package/docs/design/templates/list-action-row.svg +7 -0
- package/docs/design/templates/list-header/action-dark.svg +1 -0
- package/docs/design/templates/list-header/action.svg +1 -0
- package/docs/design/templates/list-header/count-dark.svg +1 -0
- package/docs/design/templates/list-header/count.svg +1 -0
- package/docs/design/templates/list-header/toggle-collapsed-dark.svg +1 -0
- package/docs/design/templates/list-header/toggle-collapsed.svg +1 -0
- package/docs/design/templates/list-header/toggle-expanded-dark.svg +1 -0
- package/docs/design/templates/list-header/toggle-expanded.svg +1 -0
- package/docs/design/templates/list-header-dark.svg +11 -0
- package/docs/design/templates/list-header.svg +11 -0
- package/docs/design/templates/list-item/default-dark.svg +1 -0
- package/docs/design/templates/list-item/default.svg +1 -0
- package/docs/design/templates/list-item/multiline-dark.svg +1 -0
- package/docs/design/templates/list-item/multiline.svg +1 -0
- package/docs/design/templates/list-item/selected-dark.svg +1 -0
- package/docs/design/templates/list-item/selected.svg +1 -0
- package/docs/design/templates/list-item/trailing-dark.svg +1 -0
- package/docs/design/templates/list-item/trailing.svg +1 -0
- package/docs/design/templates/list-item-dark.svg +11 -0
- package/docs/design/templates/list-item.svg +11 -0
- package/docs/design/templates/panel-header/icon-actions-dark.svg +1 -0
- package/docs/design/templates/panel-header/icon-actions.svg +1 -0
- package/docs/design/templates/panel-header/icon-summary-actions-dark.svg +1 -0
- package/docs/design/templates/panel-header/icon-summary-actions.svg +1 -0
- package/docs/design/templates/panel-header/no-icon-dark.svg +1 -0
- package/docs/design/templates/panel-header/no-icon.svg +1 -0
- package/docs/design/templates/panel-header/page-heading-dark.svg +1 -0
- package/docs/design/templates/panel-header/page-heading.svg +1 -0
- package/docs/design/templates/panel-header/title-only-dark.svg +1 -0
- package/docs/design/templates/panel-header/title-only.svg +1 -0
- package/docs/design/templates/panel-header-dark.svg +13 -0
- package/docs/design/templates/panel-header.svg +13 -0
- package/docs/design/templates/segmented-control/equal-dark.svg +1 -0
- package/docs/design/templates/segmented-control/equal.svg +1 -0
- package/docs/design/templates/segmented-control/pill-small-dark.svg +1 -0
- package/docs/design/templates/segmented-control/pill-small.svg +1 -0
- package/docs/design/templates/segmented-control-dark.svg +7 -0
- package/docs/design/templates/segmented-control.svg +7 -0
- package/docs/design/templates/skeleton/avatar-dark.svg +1 -0
- package/docs/design/templates/skeleton/avatar.svg +1 -0
- package/docs/design/templates/skeleton/block-dark.svg +1 -0
- package/docs/design/templates/skeleton/block.svg +1 -0
- package/docs/design/templates/skeleton/lines-dark.svg +1 -0
- package/docs/design/templates/skeleton/lines.svg +1 -0
- package/docs/design/templates/skeleton-dark.svg +9 -0
- package/docs/design/templates/skeleton.svg +9 -0
- package/docs/design/templates/state-banner/danger-dark.svg +1 -0
- package/docs/design/templates/state-banner/danger.svg +1 -0
- package/docs/design/templates/state-banner/info-dark.svg +1 -0
- package/docs/design/templates/state-banner/info.svg +1 -0
- package/docs/design/templates/state-banner/success-dark.svg +1 -0
- package/docs/design/templates/state-banner/success.svg +1 -0
- package/docs/design/templates/state-banner/warning-dark.svg +1 -0
- package/docs/design/templates/state-banner/warning.svg +1 -0
- package/docs/design/templates/state-banner-dark.svg +11 -0
- package/docs/design/templates/state-banner.svg +11 -0
- package/docs/design/templates/tab-bar/workspace-tabs-dark.svg +1 -0
- package/docs/design/templates/tab-bar/workspace-tabs.svg +1 -0
- package/docs/design/templates/tab-bar-dark.svg +5 -0
- package/docs/design/templates/tab-bar.svg +5 -0
- package/docs/design/templates/token-search-field/empty-dark.svg +1 -0
- package/docs/design/templates/token-search-field/empty.svg +1 -0
- package/docs/design/templates/token-search-field/tokens-dark.svg +1 -0
- package/docs/design/templates/token-search-field/tokens.svg +1 -0
- package/docs/design/templates/token-search-field-dark.svg +7 -0
- package/docs/design/templates/token-search-field.svg +7 -0
- package/docs/design/templates/toolbar/title-and-actions-dark.svg +1 -0
- package/docs/design/templates/toolbar/title-and-actions.svg +1 -0
- package/docs/design/templates/toolbar-control-group/borderless-single-dark.svg +1 -0
- package/docs/design/templates/toolbar-control-group/borderless-single.svg +1 -0
- package/docs/design/templates/toolbar-control-group/icon-buttons-dark.svg +1 -0
- package/docs/design/templates/toolbar-control-group/icon-buttons.svg +1 -0
- package/docs/design/templates/toolbar-control-group/push-buttons-dark.svg +1 -0
- package/docs/design/templates/toolbar-control-group/push-buttons.svg +1 -0
- package/docs/design/templates/toolbar-control-group/with-text-dark.svg +1 -0
- package/docs/design/templates/toolbar-control-group/with-text.svg +1 -0
- package/docs/design/templates/toolbar-control-group-dark.svg +11 -0
- package/docs/design/templates/toolbar-control-group.svg +11 -0
- package/docs/design/templates/toolbar-dark.svg +5 -0
- package/docs/design/templates/toolbar-text/default-dark.svg +1 -0
- package/docs/design/templates/toolbar-text/default.svg +1 -0
- package/docs/design/templates/toolbar-text/large-dark.svg +1 -0
- package/docs/design/templates/toolbar-text/large.svg +1 -0
- package/docs/design/templates/toolbar-text/small-dark.svg +1 -0
- package/docs/design/templates/toolbar-text/small.svg +1 -0
- package/docs/design/templates/toolbar-text-dark.svg +9 -0
- package/docs/design/templates/toolbar-text.svg +9 -0
- package/docs/design/templates/toolbar.svg +5 -0
- package/docs/design/templates/value-table/details-dark.svg +1 -0
- package/docs/design/templates/value-table/details.svg +1 -0
- package/docs/design/templates/value-table-dark.svg +5 -0
- package/docs/design/templates/value-table.svg +5 -0
- package/docs/design/templates.md +114 -0
- package/docs/design-philosophy.md +61 -1
- package/docs/device-class.md +54 -0
- package/docs/examples/command-palette-adapter.tsx +70 -0
- package/docs/layout.md +131 -0
- package/docs/nav-stack.md +47 -0
- package/docs/recipes.md +167 -0
- package/docs/split-view.md +49 -0
- package/docs/tab-scaffold.md +41 -0
- package/docs/ux-demo.md +23 -9
- package/docs/webawesome-theme.md +15 -0
- package/docs/workbench.md +47 -0
- package/llms.txt +123 -14
- package/package.json +147 -44
- package/ux-demo/recipes/app-shell.tsx +32 -0
- package/ux-demo/recipes/collapsible-sidebar.tsx +100 -0
- package/ux-demo/recipes/compact-toolbar.tsx +43 -0
- package/ux-demo/recipes/composer-form.tsx +63 -0
- package/ux-demo/recipes/list-detail-dialog.tsx +45 -0
- package/ux-demo/recipes/list-workspace-states.tsx +26 -0
- package/ux-demo/recipes/loaders.ts +33 -0
- package/ux-demo/recipes/loading-inspector.tsx +81 -0
- package/ux-demo/recipes/mount-recipe.ts +43 -0
- package/ux-demo/recipes/navigation-sidebar.tsx +26 -0
- package/ux-demo/recipes/navigation-stack.tsx +76 -0
- package/ux-demo/recipes/recipes.css +448 -0
- package/ux-demo/recipes/types.ts +17 -0
- package/ux-demo/recipes/workspace-header.tsx +12 -0
- package/dist/browser/dialog-header.js +0 -3
- package/dist/browser/menu-header.js +0 -3
- package/dist/browser/menu-item.js +0 -3
- package/dist/browser/page-header.js +0 -3
- package/dist/chunk-2PES33HS.js +0 -13
- package/dist/chunk-2PES33HS.js.map +0 -1
- package/dist/chunk-3O7ZEUW2.js.map +0 -1
- package/dist/chunk-723YNJBH.js +0 -17
- package/dist/chunk-723YNJBH.js.map +0 -1
- package/dist/chunk-GY5WH7TO.js +0 -10
- package/dist/chunk-GY5WH7TO.js.map +0 -1
- package/dist/chunk-MBR3TYNL.js.map +0 -1
- package/dist/chunk-MFKMHK5W.js.map +0 -1
- package/dist/chunk-MQIZAD4A.js.map +0 -1
- package/dist/chunk-NHXX4JI5.js +0 -28
- package/dist/chunk-NHXX4JI5.js.map +0 -1
- package/dist/chunk-OPCJRCYO.js +0 -19
- package/dist/chunk-OPCJRCYO.js.map +0 -1
- package/dist/chunk-T5XNMVOI.js +0 -24
- package/dist/chunk-T5XNMVOI.js.map +0 -1
- package/dist/chunk-TRNBHPNW.js.map +0 -1
- package/dist/chunk-VHW5YNIB.js +0 -17
- package/dist/chunk-VHW5YNIB.js.map +0 -1
- package/dist/chunk-VKH2JLDZ.js +0 -10
- package/dist/chunk-VKH2JLDZ.js.map +0 -1
- package/dist/chunk-WY4CAEPG.js +0 -14
- package/dist/chunk-WY4CAEPG.js.map +0 -1
- package/dist/dialog-header.d.ts +0 -14
- package/dist/dialog-header.js +0 -3
- package/dist/dialog-header.js.map +0 -1
- package/dist/menu-header.d.ts +0 -15
- package/dist/menu-header.js +0 -3
- package/dist/menu-header.js.map +0 -1
- package/dist/menu-item.d.ts +0 -22
- package/dist/menu-item.js +0 -3
- package/dist/menu-item.js.map +0 -1
- package/dist/page-header.d.ts +0 -9
- package/dist/page-header.js +0 -3
- package/dist/page-header.js.map +0 -1
- package/src/app-tab.css +0 -20
- package/src/dialog-header.css +0 -9
- package/src/empty-state.css +0 -6
- package/src/foundation.css +0 -106
- package/src/loading-spinner.css +0 -4
- package/src/lucide-icon.css +0 -1
- package/src/menu-header.css +0 -13
- package/src/menu-item.css +0 -12
- package/src/page-header.css +0 -3
- package/src/resizable-region.css +0 -19
- package/src/select.css +0 -18
- package/src/state-banner.css +0 -31
- package/src/styles.css +0 -18
- package/src/tab-bar.css +0 -4
- package/src/toolbar-control-group.css +0 -25
- package/src/toolbar-text.css +0 -4
- package/src/toolbar.css +0 -9
- package/src/value-table.css +0 -5
|
@@ -18,12 +18,65 @@ A reference image defines hierarchy, relationships, and intended feel. It does n
|
|
|
18
18
|
|
|
19
19
|
### Continuity is correctness
|
|
20
20
|
|
|
21
|
-
Unrelated updates must not close controls, move focus, reset a draft, replace a selection, or jump scroll. Give every durable and transient state an explicit owner. Preserve DOM identity when meaning has not changed. Distinguish blocking foreground work from background synchronization.
|
|
21
|
+
Unrelated updates must not close controls, move focus, reset a draft, replace a selection, or jump scroll. Give every durable and transient state an explicit owner. The default owner is the application: domain data and persisted layout preferences — a navigation stack, a selection, a tab order, a pane size, a `collapsed`/`expanded` disclosure flag — are the app's signals, which it reads to render. A `wire…` helper owns only the *ephemeral mechanics* around that state (a push/pop animation, overflow autoscroll, a live resize preview) and reports committed changes through callbacks. A helper takes over a piece of transient state itself only when hand-rolling that behavior is substantial and error-prone enough to cause real, inconsistent variation across apps — `wireTokenSearchFields`'s collapsible reveal/focus/Escape/blur-collapse is the bar; a one-line boolean toggle is not. Preserve DOM identity when meaning has not changed. Distinguish blocking foreground work from background synchronization.
|
|
22
22
|
|
|
23
23
|
### Hierarchy precedes decoration
|
|
24
24
|
|
|
25
25
|
Establish one reading order and one dominant action per decision point. Group first with alignment, spacing, typography, and shared surfaces. A border, fill, badge, or nested card must communicate a real distinction.
|
|
26
26
|
|
|
27
|
+
### Reach for the primitive, not for CSS
|
|
28
|
+
|
|
29
|
+
The package is designed to look right *unstyled*. A screen built from the
|
|
30
|
+
primitives, their props, and the semantic tokens should already read well, so
|
|
31
|
+
custom CSS is the exception. Before adding any `padding`, `margin`, `width`,
|
|
32
|
+
`height`, `border`, `background`, wrapper card, or decoration, check whether the
|
|
33
|
+
component, the pane, or the content-item already owns it — it almost always does,
|
|
34
|
+
and adding more usually **double-insets** or fights the component (the most common
|
|
35
|
+
mistake). Trust component defaults: render a control at its natural size and color
|
|
36
|
+
and fix the surrounding layout rather than overriding the control. Legitimate
|
|
37
|
+
custom CSS is limited to genuinely new structure, and even then may only join a
|
|
38
|
+
component's documented `publicClasses` or override documented `--kui-*` tokens at a
|
|
39
|
+
real composition boundary — never size, space, or re-skin a primitive by hand, and
|
|
40
|
+
never build a heading, toolbar, card, or pane geometry that a primitive provides.
|
|
41
|
+
|
|
42
|
+
### Panes share one child-owned geometry
|
|
43
|
+
|
|
44
|
+
Sidebars, main areas, inspectors, and dialogs use the same unpadded pane. Their
|
|
45
|
+
children own margin, border, background, padding, and radius, so a transparent
|
|
46
|
+
surface occupies exactly the same geometry as a visible one. Major content
|
|
47
|
+
groups use 24px vertical separation; the inside of an item and the gap between
|
|
48
|
+
toolbar groups use 8px. Rows and actions keep 44px targets.
|
|
49
|
+
|
|
50
|
+
Use the package composition so the geometry has one owner:
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
<aside class="kui-pane">
|
|
54
|
+
<nav class="kui-pane__content kui-content">
|
|
55
|
+
<section>
|
|
56
|
+
<ListHeader label="Workspace" />
|
|
57
|
+
<ListItem action="open" label="Inbox" icon={inboxIcon} />
|
|
58
|
+
<ListItem action="open" label="Drafts" />
|
|
59
|
+
</section>
|
|
60
|
+
<div class="kui-content-item">Panel contents</div>
|
|
61
|
+
</nav>
|
|
62
|
+
</aside>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Do not pad the pane and then pad every wrapper. That duplicates the geometry
|
|
66
|
+
and makes transparent borders behave differently from visible ones:
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
<aside class="pane padded">
|
|
70
|
+
<section class="padded">
|
|
71
|
+
<ListHeader label="Workspace" />
|
|
72
|
+
<div class="panel padded-again">Panel contents</div>
|
|
73
|
+
</section>
|
|
74
|
+
</aside>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Change the shared item tokens at a real composition boundary when a product
|
|
78
|
+
needs different geometry; do not compensate with one-off negative margins.
|
|
79
|
+
|
|
27
80
|
### Prefer directness
|
|
28
81
|
|
|
29
82
|
Use the shortest understandable, recoverable interaction. Avoid modes, dialogs, confirmations, and explicit saves when direct manipulation can safely express the same result. Low ceremony still requires a discoverable affordance, visible focus, and honest feedback.
|
|
@@ -42,6 +95,13 @@ foregrounds compatible with Hot Sheet 2 and Web Awesome. Consumers may replace
|
|
|
42
95
|
the global palette, one semantic role, or one component instance through
|
|
43
96
|
documented custom properties without rewriting component CSS.
|
|
44
97
|
|
|
98
|
+
Spacing is likewise a finite, semantic scale, not a free measurement. Five
|
|
99
|
+
canonical steps — 0, 4, 8, 16, and 24px, each a `--kui-space-*` token — map to a
|
|
100
|
+
single relationship apiece, chosen by how connected two elements are: no
|
|
101
|
+
separation, very minor air, standard within a group, between homogeneous groups,
|
|
102
|
+
and major between heterogeneous groups. Off-scale values (12px, 32px) are
|
|
103
|
+
deliberate exceptions. See [`layout.md`](layout.md) "Spacing scale".
|
|
104
|
+
|
|
45
105
|
### Responsive design reprioritizes
|
|
46
106
|
|
|
47
107
|
Protect primary content, readable type, recognizable icons, and usable targets. Relocate secondary information before compressing it below a usable scale. Keep one clear scroll owner per region and test narrow, wide, zoomed, and intermediate layouts.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Device class
|
|
2
|
+
|
|
3
|
+
`@kerfjs/ui/device-class` reports the current viewport as a reactive **device
|
|
4
|
+
class** — a size bucket × orientation × viewport-segment count — so a layout can
|
|
5
|
+
pick its presentation from one signal instead of hand-wiring `matchMedia`. It is
|
|
6
|
+
the foundation the app/dialog layouts build on (see
|
|
7
|
+
[`../../docs/23-app-layouts.md`](../../docs/23-app-layouts.md)).
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { deviceClass } from '@kerfjs/ui/device-class';
|
|
11
|
+
|
|
12
|
+
const device = deviceClass(); // ReadonlySignal<DeviceClass>
|
|
13
|
+
|
|
14
|
+
effect(() => {
|
|
15
|
+
if (device.value.compact) showStack();
|
|
16
|
+
else showSplit();
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## `DeviceClass`
|
|
21
|
+
|
|
22
|
+
| Field | Meaning |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| `size` | `xs-mobile` \| `mobile` \| `tablet` \| `desktop` \| `xl-desktop` |
|
|
25
|
+
| `orientation` | `portrait` \| `landscape` |
|
|
26
|
+
| `segments` / `verticalSegments` | viewport segment counts (foldables / dual-screen); `1` on ordinary devices |
|
|
27
|
+
| `handset` | `xs-mobile` or `mobile` |
|
|
28
|
+
| `compact` | "one pane at a time" — a handset or a portrait tablet |
|
|
29
|
+
| `atLeast(size)` | true when the current size is `size` or larger |
|
|
30
|
+
|
|
31
|
+
Reading `device.value` inside an `effect`/`computed` re-runs when the viewport
|
|
32
|
+
crosses a breakpoint, rotates, or changes its segment count. One shared viewport
|
|
33
|
+
source backs every reader.
|
|
34
|
+
|
|
35
|
+
## Breakpoints
|
|
36
|
+
|
|
37
|
+
Default minimum widths (px): `mobile` 360, `tablet` 720, `desktop` 1024,
|
|
38
|
+
`xl-desktop` 1440. Override per reader:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const device = deviceClass({ breakpoints: { tablet: 900 } });
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The same numbers are mirrored as CSS custom properties in the foundation
|
|
45
|
+
(`--kui-bp-mobile`, `--kui-bp-tablet`, `--kui-bp-desktop`, `--kui-bp-xl-desktop`)
|
|
46
|
+
so CSS media queries and the JS signal read one source. `classifyViewport(width,
|
|
47
|
+
orientation, segments?, verticalSegments?, breakpoints?)` is exported as the
|
|
48
|
+
pure, DOM-free bucketing core.
|
|
49
|
+
|
|
50
|
+
## Server rendering
|
|
51
|
+
|
|
52
|
+
Without a DOM, `deviceClass()` resolves to an SSR default (1024×768, landscape,
|
|
53
|
+
one segment) and hydrates to the real class on the client. Override the assumed
|
|
54
|
+
viewport with `deviceClass({ ssr: { width, height } })`.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import '@kerfjs/ui/layout.css';
|
|
2
|
+
|
|
3
|
+
import { delegate, mount, signal } from 'kerfjs';
|
|
4
|
+
import { delegateActions } from 'kerfjs/actions';
|
|
5
|
+
|
|
6
|
+
export interface AppCommand {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
keywords?: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CommandPaletteCallbacks {
|
|
13
|
+
onClose(): void;
|
|
14
|
+
onRun(command: AppCommand): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Application-local example, not an @kerfjs/ui export. The application owns
|
|
19
|
+
* ranking, history, shortcuts, focus policy, command availability, and copy.
|
|
20
|
+
*/
|
|
21
|
+
export function mountCommandPaletteAdapter(
|
|
22
|
+
root: HTMLElement,
|
|
23
|
+
commands: AppCommand[],
|
|
24
|
+
{ onClose, onRun }: CommandPaletteCallbacks,
|
|
25
|
+
): () => void {
|
|
26
|
+
const query = signal('');
|
|
27
|
+
const render = () => {
|
|
28
|
+
const needle = query.value.trim().toLocaleLowerCase();
|
|
29
|
+
const matches = commands.filter((command) => !needle ||
|
|
30
|
+
[command.label, ...(command.keywords ?? [])].some((value) => value.toLocaleLowerCase().includes(needle)));
|
|
31
|
+
return <section class="app-command-palette kui-content" aria-label="Command palette">
|
|
32
|
+
<div class="kui-content-item"><label for="app-command-query">Search commands</label><input id="app-command-query" type="search" value={query.value} autocomplete="off" data-command-query /></div>
|
|
33
|
+
<p class="kui-content-item" aria-live="polite">{matches.length} matching commands</p>
|
|
34
|
+
<ul class="kui-content-item" aria-label="Matching commands">{matches.map((command) => <li><button type="button" data-action="run-command" data-command-id={command.id}>{command.label}</button></li>)}</ul>
|
|
35
|
+
<footer class="kui-control-cluster kui-content-item" aria-label="Command palette actions">
|
|
36
|
+
<button type="button" data-action="clear-query">Clear search</button>
|
|
37
|
+
<button type="button" data-action="close-palette">Close</button>
|
|
38
|
+
</footer>
|
|
39
|
+
</section>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const stopMount = mount(root, render);
|
|
43
|
+
const stopActions = delegateActions(root, 'click', {
|
|
44
|
+
'run-command': (_event, element) => {
|
|
45
|
+
const id = (element as HTMLElement).dataset.commandId;
|
|
46
|
+
const command = commands.find((candidate) => candidate.id === id);
|
|
47
|
+
if (command) onRun(command);
|
|
48
|
+
},
|
|
49
|
+
'clear-query': () => {
|
|
50
|
+
query.value = '';
|
|
51
|
+
root.querySelector<HTMLInputElement>('[data-command-query]')?.focus();
|
|
52
|
+
},
|
|
53
|
+
'close-palette': onClose,
|
|
54
|
+
});
|
|
55
|
+
const stopInput = delegate<HTMLInputElement>(root, 'input', '[data-command-query]', (_event, input) => {
|
|
56
|
+
query.value = input.value;
|
|
57
|
+
});
|
|
58
|
+
const stopEscape = delegate(root, 'keydown', '[data-command-query]', (event) => {
|
|
59
|
+
if ((event as KeyboardEvent).key === 'Escape') onClose();
|
|
60
|
+
});
|
|
61
|
+
let disposed = false;
|
|
62
|
+
return () => {
|
|
63
|
+
if (disposed) return;
|
|
64
|
+
disposed = true;
|
|
65
|
+
stopEscape();
|
|
66
|
+
stopInput();
|
|
67
|
+
stopActions();
|
|
68
|
+
stopMount();
|
|
69
|
+
};
|
|
70
|
+
}
|
package/docs/layout.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Pane and content layout
|
|
2
|
+
|
|
3
|
+
Import `@kerfjs/ui/layout.css` for the shared geometry used by sidebars, main
|
|
4
|
+
areas, inspectors, and dialogs. The vocabulary deliberately describes structure
|
|
5
|
+
rather than location: the same pane can be placed anywhere.
|
|
6
|
+
|
|
7
|
+
## Anatomy
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
<aside class="kui-pane">
|
|
11
|
+
<div class="kui-pane__toolbar"><Toolbar label="Workspace" ... /></div>
|
|
12
|
+
<div class="kui-pane__content kui-content">
|
|
13
|
+
<section>...</section>
|
|
14
|
+
<section>...</section>
|
|
15
|
+
</div>
|
|
16
|
+
<footer class="kui-pane__footer"><Toolbar label="Actions" ... /></footer>
|
|
17
|
+
</aside>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`.kui-pane` has no padding. It reserves rows for an optional toolbar, one
|
|
21
|
+
scrolling content area, and an optional footer. A main area or dialog often
|
|
22
|
+
omits the footer; a navigation pane commonly uses all three. Fixed chrome stays
|
|
23
|
+
outside `.kui-pane__content`, which is the pane's scroll owner.
|
|
24
|
+
|
|
25
|
+
`.kui-content` is a vertical stack with a 24px gap between major children.
|
|
26
|
+
Sections may contain adjacent `ListItem` rows without adding another major gap.
|
|
27
|
+
Ordinary surface-like children use `.kui-content-item` and own their complete
|
|
28
|
+
geometry:
|
|
29
|
+
|
|
30
|
+
- 8px inline margin from the pane edge
|
|
31
|
+
- 1px border, transparent by default
|
|
32
|
+
- 8px internal padding and 8px internal gap
|
|
33
|
+
- `calc(1px + remify(11px))`, or 12px, rounded corners
|
|
34
|
+
|
|
35
|
+
Use `.kui-content-item--pill` for the 22px pill radius expressed as
|
|
36
|
+
`calc(1px + remify(21px))`. Consumers can make a content item visible without
|
|
37
|
+
changing its geometry by setting `--kui-content-item-border` and
|
|
38
|
+
`--kui-content-item-background`.
|
|
39
|
+
|
|
40
|
+
A visible parent surface does not make every child another visible card. The
|
|
41
|
+
composer recipe keeps its form as the single surface, uses `PanelHeader` for
|
|
42
|
+
its task hierarchy, and places field and action control edges on the shared 8px
|
|
43
|
+
inline gutter instead of nesting them inside another padded content item. A
|
|
44
|
+
conditional `StateBanner` remains visibly distinct because it communicates
|
|
45
|
+
semantic status.
|
|
46
|
+
|
|
47
|
+
## Public roles and tokens
|
|
48
|
+
|
|
49
|
+
| Need | Class | Token / default |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| Unpadded toolbar/content/footer structure | `.kui-pane` | — |
|
|
52
|
+
| Scrolling pane content | `.kui-pane__content` | — |
|
|
53
|
+
| Major vertical rhythm | `.kui-content` | `--kui-layout-content-gap: 24px` |
|
|
54
|
+
| Self-contained child geometry | `.kui-content-item` | 8px margin + 1px border + 8px padding |
|
|
55
|
+
| Pill child | `.kui-content-item--pill` | `--kui-layout-pill-radius: 22px` |
|
|
56
|
+
| Related controls | `.kui-control-cluster` | `--kui-layout-control-gap: 8px` |
|
|
57
|
+
| Inline metadata | `.kui-inline-metadata` | `--kui-layout-metadata-gap: 4px` |
|
|
58
|
+
| Explicit scroll owner outside a pane | `.kui-scroll-owner` | `overflow: auto` |
|
|
59
|
+
|
|
60
|
+
The component layer applies the same contract to `Toolbar`, `ListHeader`,
|
|
61
|
+
`ListItem`, `PanelHeader`, `StateBanner`, `ValueTable`,
|
|
62
|
+
`ValueTableRow`, tabs, and form controls. A value-table row separator starts at
|
|
63
|
+
the row's 8px content inset, or at 40px when the row contains its 24px leading
|
|
64
|
+
icon and 8px gap, and always ends 8px from the right edge. Each value-table row
|
|
65
|
+
also keeps 8px of root-scaled block padding independently of its semantic inline
|
|
66
|
+
inset. Most interactive rows and toolbar groups are 44px tall.
|
|
67
|
+
Toolbar groups reserve a real 1px outer border around a 42px inner area, even
|
|
68
|
+
when their border and background are transparent.
|
|
69
|
+
`PanelHeader` is a plain top `Toolbar` used as a panel, dialog, or page heading
|
|
70
|
+
and overrides no Toolbar styles. The leading zone holds the optional icon (a
|
|
71
|
+
normal bordered `ToolbarControlGroup` given a brand fill with a matching border,
|
|
72
|
+
a 22px glyph) and the title as extra-large `ToolbarText`; the app's trailing
|
|
73
|
+
controls go straight into the trailing zone, and the icon group is omitted when
|
|
74
|
+
no icon is passed. The optional subtitle is a separate row below the toolbar, its
|
|
75
|
+
text left-aligned with the title text.
|
|
76
|
+
|
|
77
|
+
## Spacing scale
|
|
78
|
+
|
|
79
|
+
Spacing is not a free choice. The official scale is five canonical steps, each
|
|
80
|
+
expressing exactly one relationship — pick the step by **how connected two
|
|
81
|
+
elements are**, not by eye. Every scalable value is `remify`-authored against the
|
|
82
|
+
fixed 16px baseline, so it delivers as `rem`.
|
|
83
|
+
|
|
84
|
+
| Value | Token | Relationship — when to use |
|
|
85
|
+
| --- | --- | --- |
|
|
86
|
+
| 0px | `--kui-space-none` | No separation. The elements read as a single unit (a control and its own affordance, adjacent `ListItem` rows). |
|
|
87
|
+
| 4px | `--kui-space-2xs` | Very minor. Still one connected cluster, but readability or aesthetics demand a hair of air (inline metadata, an icon beside its label). |
|
|
88
|
+
| 8px | `--kui-space-xs` | Standard. Between elements **within a group** — the content-item padding/gutter, gaps between toolbar controls in a group. |
|
|
89
|
+
| 16px | `--kui-space-m` | Minor. Between **homogeneous groups** — two lists, two sibling sections of the same kind. |
|
|
90
|
+
| 24px | `--kui-space-l` | Major. Between **heterogeneous groups** — the `.kui-content` rhythm between major, differing regions. |
|
|
91
|
+
|
|
92
|
+
The two relationships that get confused most are 8px vs 24px: 8px is *inside* a
|
|
93
|
+
group, 24px is *between* major, differing regions. 16px sits between them for
|
|
94
|
+
same-kind groups.
|
|
95
|
+
|
|
96
|
+
`--kui-space-s` (12px) and `--kui-space-xl` (32px) exist but are **off the
|
|
97
|
+
canonical rhythm** — reach for them only as a deliberate exception, never as a
|
|
98
|
+
default step. Prefer the five canonical tokens so spacing stays legible and
|
|
99
|
+
consistent across every surface.
|
|
100
|
+
|
|
101
|
+
## Ownership rules
|
|
102
|
+
|
|
103
|
+
1. Do not pad a sidebar, main area, dialog, or `.kui-pane` shell. Children own
|
|
104
|
+
their own margin, border, background, padding, and radius.
|
|
105
|
+
2. Use 24px gaps for major vertical separation and 8px gaps inside an item or
|
|
106
|
+
between toolbar groups. Do not confuse the two relationships.
|
|
107
|
+
3. Wrap toolbar content in `ToolbarControlGroup`, including dormant text. A
|
|
108
|
+
transparent group still reserves the same 44px geometry as a visible group.
|
|
109
|
+
4. Keep one scrolling content owner per pane. Toolbar and footer siblings stay
|
|
110
|
+
fixed while the content scrolls.
|
|
111
|
+
5. A split item keeps dormant and interactive regions separate. For example,
|
|
112
|
+
`ListHeader` renders its title/count-or-badge cluster separately from its optional
|
|
113
|
+
logical-end 44px action. The header fills the available inline width and its
|
|
114
|
+
action glyph defaults to 18px; disclosure mode makes the title cluster itself
|
|
115
|
+
the button and supplies the production `DisclosureArrow` unless `actionIcon`
|
|
116
|
+
replaces it.
|
|
117
|
+
`ListActionRow` uses a noninteractive row root around sibling 44px primary
|
|
118
|
+
and trailing buttons. `ListItem.trailing` remains dormant content.
|
|
119
|
+
In multiline `ListItem` and `ListActionRow` rows, the leading icon stays
|
|
120
|
+
centered on the label's first line rather than the full wrapped label.
|
|
121
|
+
6. Reading width, column placement, and responsive relocation remain application
|
|
122
|
+
decisions. The shared classes define local geometry, not the whole shell.
|
|
123
|
+
7. A visible collapsible pane owns its collapse control in that pane's toolbar.
|
|
124
|
+
When the pane is hidden, move the restore control into the adjacent main
|
|
125
|
+
toolbar on the same logical edge: an inline-start pane restores from the
|
|
126
|
+
main toolbar's leading group, and an inline-end pane restores from its
|
|
127
|
+
trailing group. Do not leave an otherwise empty icon-only rail behind.
|
|
128
|
+
|
|
129
|
+
At narrow widths or 200% zoom, relocate or stack panes before shrinking targets.
|
|
130
|
+
The 8/1/8 item contract and 44px controls remain stable, so screenshots and
|
|
131
|
+
focus-order tests exercise the same model at every viewport.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Navigation stack
|
|
2
|
+
|
|
3
|
+
`@kerfjs/ui/nav-stack` is an iOS-style push/pop navigation stack: views slide in
|
|
4
|
+
and out over one another while the top chrome settles. A **single-pane layout is
|
|
5
|
+
a `NavStack` with one entry**. It is one of the opt-in app layouts (see
|
|
6
|
+
[`../../docs/23-app-layouts.md`](../../docs/23-app-layouts.md)).
|
|
7
|
+
|
|
8
|
+
Import the component and its companion stylesheet:
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { NavStack, type NavStackView } from '@kerfjs/ui/nav-stack';
|
|
12
|
+
import { wireNavStack } from '@kerfjs/ui/wire-nav-stack';
|
|
13
|
+
import '@kerfjs/ui/nav-stack.css';
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## State lives in the app
|
|
17
|
+
|
|
18
|
+
Like every `@kerfjs/ui` component, `NavStack` is declarative: the app owns the
|
|
19
|
+
stack as a `signal<NavStackView[]>`, `NavStack({ views })` renders it, and
|
|
20
|
+
`wireNavStack` animates the transitions.
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
const views = signal<NavStackView[]>([{ key: 'inbox', title: 'Inbox', content: <InboxView /> }]);
|
|
24
|
+
|
|
25
|
+
// render inside mount():
|
|
26
|
+
<NavStack id="mail" label="Mail" views={views.value} />;
|
|
27
|
+
|
|
28
|
+
// once, after first render:
|
|
29
|
+
const dispose = wireNavStack(root, { onBack: () => { views.value = views.value.slice(0, -1); } });
|
|
30
|
+
|
|
31
|
+
// push / pop by editing the signal:
|
|
32
|
+
views.value = [...views.value, { key: id, title: 'Message', content: <MessageView id={id} /> }];
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`NavStack` renders every entry stacked, the last one active and the rest kept
|
|
36
|
+
mounted (so their DOM state and focus survive) but hidden. Each entry carries a
|
|
37
|
+
`key` (stable identity), `content`, an optional `title`, and optional per-view
|
|
38
|
+
`toolbar` actions. The back control appears automatically once the stack has more
|
|
39
|
+
than one entry; `wireNavStack`'s `onBack` is where the app pops its own signal.
|
|
40
|
+
|
|
41
|
+
## Transitions
|
|
42
|
+
|
|
43
|
+
`wireNavStack(root, { onBack, duration? })` observes the rendered stack and
|
|
44
|
+
animates each change: a pushed view slides in from the trailing edge; a popped
|
|
45
|
+
view slides back off it over the revealed view. It returns a disposer. The
|
|
46
|
+
animation honors `prefers-reduced-motion` (transitions collapse to instant) and
|
|
47
|
+
`duration: 0` disables it. Applicable at every device size and inside dialogs.
|
package/docs/recipes.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Production composition recipes
|
|
2
|
+
|
|
3
|
+
These seven reference compositions sit between individual primitives and product
|
|
4
|
+
code. Open each stable UX-catalog route to run it, then copy the linked TSX and
|
|
5
|
+
CSS. The examples import public package subpaths, use semantic layout owners,
|
|
6
|
+
and keep state in a per-instance application adapter. They are not new
|
|
7
|
+
monolithic components.
|
|
8
|
+
|
|
9
|
+
Copy the recipe source together with the catalog-independent
|
|
10
|
+
[`mount-recipe.ts`](../ux-demo/recipes/mount-recipe.ts) adapter. It mounts the
|
|
11
|
+
controller at one stable application root, uses `delegateActions()` for recipe
|
|
12
|
+
commands, forwards form and dialog lifecycle events, wires resize commits with
|
|
13
|
+
the public `onCommit` callback, retains every disposer, and returns one
|
|
14
|
+
idempotent disposer:
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { createRecipe } from './navigation-sidebar.js';
|
|
18
|
+
import { mountRecipe } from './mount-recipe.js';
|
|
19
|
+
|
|
20
|
+
const root = document.querySelector<HTMLElement>('#navigation')!;
|
|
21
|
+
const stopRecipe = mountRecipe(root, createRecipe(announce));
|
|
22
|
+
window.addEventListener('pagehide', stopRecipe, { once: true });
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The adapter is delivered as reference source, not a new package runtime export.
|
|
26
|
+
`delegate()` remains a valid alternative when an application needs selector-
|
|
27
|
+
specific dispatch; either way, wire once at a stable root and retain disposal.
|
|
28
|
+
|
|
29
|
+
## Desktop application shell
|
|
30
|
+
|
|
31
|
+
[Open the recipe](../ux-demo/?component=recipe-app-shell) · [TSX source](../ux-demo/recipes/app-shell.tsx) · [shared CSS](../ux-demo/recipes/recipes.css)
|
|
32
|
+
|
|
33
|
+
Use `Toolbar`, controlled `ResizableRegion` panes, and one
|
|
34
|
+
`.kui-pane__content` scroll owner per pane. The recipe owns the shell topology;
|
|
35
|
+
the app owns routing, responsive pane visibility, sizes, persistence, and data.
|
|
36
|
+
Keep each visible pane's collapse action in its own toolbar. Once hidden, put
|
|
37
|
+
its restore action in the adjacent main toolbar on the same logical edge:
|
|
38
|
+
leading for an inline-start sidebar and trailing for an inline-end inspector.
|
|
39
|
+
Collapse the pane completely rather than preserving an empty icon rail.
|
|
40
|
+
Adapt only public `--kui-layout-*` and component variables.
|
|
41
|
+
|
|
42
|
+
## Navigation sidebar
|
|
43
|
+
|
|
44
|
+
[Open the recipe](../ux-demo/?component=recipe-navigation-sidebar) · [TSX source](../ux-demo/recipes/navigation-sidebar.tsx)
|
|
45
|
+
|
|
46
|
+
One unpadded `.kui-pane` owns toolbar/content/footer structure. Its
|
|
47
|
+
`.kui-content` uses 24px major gaps; `ListHeader`, `ListItem`, and other
|
|
48
|
+
`.kui-content-item` children own their 8px margin, 1px border, and 8px padding.
|
|
49
|
+
Rows and footer toolbar groups remain 44px tall. The app owns routes,
|
|
50
|
+
permissions, labels, selection, valid section counts and their localized
|
|
51
|
+
`countLabel` phrases, non-count badge content, disclosure state, and revealed
|
|
52
|
+
content. A toggled `ListHeader` supplies the production `DisclosureArrow` when
|
|
53
|
+
no custom `actionIcon` is needed; ordinary navigation rows stay chevron-free.
|
|
54
|
+
|
|
55
|
+
## Workspace header
|
|
56
|
+
|
|
57
|
+
[Open the recipe](../ux-demo/?component=recipe-workspace-header) · [TSX source](../ux-demo/recipes/workspace-header.tsx)
|
|
58
|
+
|
|
59
|
+
`PanelHeader` owns the page heading while one control cluster holds secondary,
|
|
60
|
+
overflow, and primary actions. The app owns authorization and command policy;
|
|
61
|
+
controls relocate without changing focus order.
|
|
62
|
+
|
|
63
|
+
## List-detail dialog
|
|
64
|
+
|
|
65
|
+
[Open the recipe](../ux-demo/?component=recipe-list-detail-dialog) · [TSX source](../ux-demo/recipes/list-detail-dialog.tsx)
|
|
66
|
+
|
|
67
|
+
The production Web Awesome dialog owns modal focus and Escape; the thin recipe
|
|
68
|
+
adapter restores the invoking control consistently after the hide event.
|
|
69
|
+
`PanelHeader`, `ListHeader`, `ListItem`, and `ValueTable` own their included
|
|
70
|
+
anatomy. The application owns open state, selection, dismissal policy, and
|
|
71
|
+
record actions. `PanelHeader` places the trailing controls the app passes
|
|
72
|
+
directly into its top toolbar's trailing zone.
|
|
73
|
+
The header sits on the dialog edge while retaining its internal control inset;
|
|
74
|
+
the selected title receives the full content gutter, the metadata table fills
|
|
75
|
+
the available detail width between the usual outer margins, and the action
|
|
76
|
+
cluster uses one outer gutter without a second content-item inset.
|
|
77
|
+
Do not rebuild the dialog or reach into private shadow parts.
|
|
78
|
+
|
|
79
|
+
## Composer form
|
|
80
|
+
|
|
81
|
+
[Open the recipe](../ux-demo/?component=recipe-composer-form) · [TSX source](../ux-demo/recipes/composer-form.tsx)
|
|
82
|
+
|
|
83
|
+
`PanelHeader` supplies the task title and summary, with their ids referenced
|
|
84
|
+
by the form. Production fields own labels, help, and native focus. The field
|
|
85
|
+
and footer control edges sit directly on the shared 8px inline gutter rather
|
|
86
|
+
than acquiring a second content-item padding inset; major children remain 24px
|
|
87
|
+
apart and related controls use 8px gaps. The app owns
|
|
88
|
+
validation, drafts, permissions, and transport. Persistent error or success
|
|
89
|
+
feedback is the only nested semantic surface and uses `StateBanner`, not a toast.
|
|
90
|
+
Because upgraded Web Awesome fields retain live value properties, controlled
|
|
91
|
+
resets synchronize both those properties and the rendered value attributes;
|
|
92
|
+
the Reset action also announces `Draft reset` through the catalog live region.
|
|
93
|
+
|
|
94
|
+
## List workspace states
|
|
95
|
+
|
|
96
|
+
[Open the recipe](../ux-demo/?component=recipe-list-workspace-states) · [TSX source](../ux-demo/recipes/list-workspace-states.tsx)
|
|
97
|
+
|
|
98
|
+
The same content region moves deterministically through loading, empty,
|
|
99
|
+
populated, stale/background refresh, and error/retry states. The recipe owns
|
|
100
|
+
feedback placement; the app owns fetching, cache age, retry policy, and domain
|
|
101
|
+
rows.
|
|
102
|
+
|
|
103
|
+
## Compact toolbar choices and actions
|
|
104
|
+
|
|
105
|
+
[Open the recipe](../ux-demo/?component=recipe-compact-toolbar) · [TSX source](../ux-demo/recipes/compact-toolbar.tsx)
|
|
106
|
+
|
|
107
|
+
Use `ToolbarControlGroup` for related commands, `SegmentedControl` for a few
|
|
108
|
+
visible exclusive choices, `Select` for a longer value list, and an ordinary
|
|
109
|
+
button for an independent command. The app owns values, actions, persistence,
|
|
110
|
+
and responsive priority.
|
|
111
|
+
|
|
112
|
+
## Navigation stack
|
|
113
|
+
|
|
114
|
+
[Open the recipe](../ux-demo/?component=recipe-navigation-stack) · [TSX source](../ux-demo/recipes/navigation-stack.tsx)
|
|
115
|
+
|
|
116
|
+
Drill from a library list into a detail and back with `NavStack`
|
|
117
|
+
(`@kerfjs/ui/nav-stack`): the app owns the stack as a signal of views and
|
|
118
|
+
pushes/pops it, `NavStack` renders it, and `wireNavStack` slides the content and
|
|
119
|
+
settles the chrome (reduced motion collapses the slide to instant). A live
|
|
120
|
+
`deviceClass()` badge shows the current size/orientation. See the layout guide
|
|
121
|
+
[`app-layouts.md`](app-layouts.md) for choosing among `NavStack`, `SplitView`,
|
|
122
|
+
`Workbench`, and `TabScaffold`.
|
|
123
|
+
|
|
124
|
+
## Loading inspector
|
|
125
|
+
|
|
126
|
+
[Open the recipe](../ux-demo/?component=recipe-loading-inspector) · [TSX source](../ux-demo/recipes/loading-inspector.tsx)
|
|
127
|
+
|
|
128
|
+
A record inspector whose per-record values load asynchronously. Every
|
|
129
|
+
value-bearing component (`PanelHeader`, `ValueTable`/`ValueTableRow`, `Select`,
|
|
130
|
+
`SegmentedControl`, `ListItem`, `StateBanner`) takes its `placeholder` from one
|
|
131
|
+
loading flag, so the same real chrome renders a faithful loading state and then
|
|
132
|
+
the populated record — no separate skeleton markup. The composition is the point;
|
|
133
|
+
`Skeleton` is the primitive it builds on. The app owns the loading lifecycle and
|
|
134
|
+
which values are still unknown.
|
|
135
|
+
|
|
136
|
+
## Collapsible sidebar
|
|
137
|
+
|
|
138
|
+
[Open the recipe](../ux-demo/?component=recipe-collapsible-sidebar) · [TSX source](../ux-demo/recipes/collapsible-sidebar.tsx)
|
|
139
|
+
|
|
140
|
+
A mini app frame whose left navigation rail and bottom activity drawer are
|
|
141
|
+
standalone `CollapsiblePanel`s (`@kerfjs/ui/collapsible-panel`) driven by
|
|
142
|
+
`wireSidebar` (`@kerfjs/ui/wire-sidebar`). `CollapsiblePanelToggle` supplies the
|
|
143
|
+
standard per-side glyph: a collapse toggle lives inside each panel and an expand
|
|
144
|
+
toggle lives in the always-visible main header, so a collapsed panel is still
|
|
145
|
+
reachable. `wireSidebar` owns the toggle delegation, moves focus into a panel on
|
|
146
|
+
open and restores it to the trigger on close, and — when a `deviceClass()` reports
|
|
147
|
+
`compact` — switches the open panel to a dismissable **overlay** (backdrop, Escape
|
|
148
|
+
and backdrop-click collapse, and a trapped Tab ring, the ARIA dialog pattern). It
|
|
149
|
+
also persists each panel's collapsed state through a supplied storage hook. The app
|
|
150
|
+
owns each `collapsed` signal, the panel sizes, and the content; the wire owns the
|
|
151
|
+
ephemeral interaction. For a full three-pane shell use `Workbench` instead — see
|
|
152
|
+
[`app-layouts.md`](app-layouts.md). This recipe is covered end-to-end across
|
|
153
|
+
Chromium, Firefox, and WebKit by `tests/browser/collapsible-sidebar-recipe.spec.ts`.
|
|
154
|
+
|
|
155
|
+
## Rules shared by every recipe
|
|
156
|
+
|
|
157
|
+
- Import `@kerfjs/ui/layout.css`; keep every pane unpadded and use exactly one
|
|
158
|
+
`.kui-pane__content` scroll owner for each real boundary.
|
|
159
|
+
- For direct `wa-*` JSX, import types from `@kerfjs/ui/webawesome`. Import only
|
|
160
|
+
individual Web Awesome registration modules and theme them with
|
|
161
|
+
`@kerfjs/ui/webawesome.css`.
|
|
162
|
+
- Prefer public props and variables at the composition boundary. When a recipe
|
|
163
|
+
needs responsive topology, join only classes listed in the catalog's
|
|
164
|
+
`publicClasses`; do not copy component markup or select descendants by tag,
|
|
165
|
+
id, attribute alone, or an unlisted implementation class.
|
|
166
|
+
- Start from the copyable mount adapter, or reproduce its complete boundary:
|
|
167
|
+
wire stable `data-action` hooks once and retain every disposer.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Split view (list-detail)
|
|
2
|
+
|
|
3
|
+
`@kerfjs/ui/split-view` is a list-detail layout: a list and a
|
|
4
|
+
detail side by side on roomy screens, collapsing to a `NavStack` (list → detail)
|
|
5
|
+
on compact ones. One of the opt-in app layouts (see
|
|
6
|
+
[`../../docs/23-app-layouts.md`](../../docs/23-app-layouts.md)).
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { SplitView } from '@kerfjs/ui/split-view';
|
|
10
|
+
import { deviceClass } from '@kerfjs/ui/device-class';
|
|
11
|
+
import '@kerfjs/ui/split-view.css';
|
|
12
|
+
// plus nav-stack.css when the compact path is reachable, and
|
|
13
|
+
// wireResizableRegions / wireNavStack for the interactive behavior.
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Responsive by device class
|
|
17
|
+
|
|
18
|
+
`SplitView` is declarative; the app derives `compact` from the device class and
|
|
19
|
+
tracks its own selection:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
const device = deviceClass();
|
|
23
|
+
const selected = signal<string | null>(null);
|
|
24
|
+
|
|
25
|
+
<SplitView
|
|
26
|
+
id="mail"
|
|
27
|
+
label="Mail"
|
|
28
|
+
compact={device.value.compact}
|
|
29
|
+
detailActive={selected.value !== null}
|
|
30
|
+
list={<ThreadList />}
|
|
31
|
+
detail={<Message id={selected.value} />}
|
|
32
|
+
listTitle="Threads"
|
|
33
|
+
detailTitle="Message"
|
|
34
|
+
resizable={{ size: 320, min: 220, max: 480 }}
|
|
35
|
+
/>;
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- **Roomy** (`compact: false`): both panes show. With `resizable`, the list sits
|
|
39
|
+
in a `ResizableRegion` (wire it with `wireResizableRegions`); without it the
|
|
40
|
+
list takes a fixed `--kui-split-view-list-width` (default 320px) and the detail
|
|
41
|
+
fills the rest.
|
|
42
|
+
- **Compact** (`compact: true`): the split collapses to a `NavStack`. The list is
|
|
43
|
+
the root; when `detailActive` is true the detail is pushed over it with an
|
|
44
|
+
automatic back control (wire it with `wireNavStack`, whose `onBack` clears the
|
|
45
|
+
app's selection). This is the portrait-tablet / handset presentation; as a
|
|
46
|
+
dialog the compact form is a full-screen or large partial-cover modal.
|
|
47
|
+
|
|
48
|
+
Compose the interactive wiring from the existing helpers — `SplitView` adds no
|
|
49
|
+
wire of its own.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Tab scaffold
|
|
2
|
+
|
|
3
|
+
`@kerfjs/ui/tab-scaffold` is a mobile-first, iOS-like bottom tab bar that switches
|
|
4
|
+
between major app sections, where **each tab keeps its own content mounted** —
|
|
5
|
+
usually a `NavStack`, so each tab's stack and scroll survive a switch. One of the
|
|
6
|
+
opt-in app layouts (see [`../../docs/23-app-layouts.md`](../../docs/23-app-layouts.md)).
|
|
7
|
+
It is distinct from `TabBar` (document-oriented, reorderable strips).
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { TabScaffold } from '@kerfjs/ui/tab-scaffold';
|
|
11
|
+
import { wireTabScaffold } from '@kerfjs/ui/wire-tab-scaffold';
|
|
12
|
+
import '@kerfjs/ui/tab-scaffold.css';
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Controlled selection
|
|
16
|
+
|
|
17
|
+
The app owns the active tab (a signal); `TabScaffold` renders every tab's scene
|
|
18
|
+
(only the active one visible) plus the bottom bar, and `wireTabScaffold` reports
|
|
19
|
+
clicks.
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
const active = signal('home');
|
|
23
|
+
|
|
24
|
+
<TabScaffold
|
|
25
|
+
id="app"
|
|
26
|
+
label="Sections"
|
|
27
|
+
active={active.value}
|
|
28
|
+
tabs={[
|
|
29
|
+
{ id: 'home', label: 'Home', icon: <HomeIcon />, content: <HomeStack /> },
|
|
30
|
+
{ id: 'search', label: 'Search', icon: <SearchIcon />, content: <SearchStack /> },
|
|
31
|
+
]}
|
|
32
|
+
/>;
|
|
33
|
+
|
|
34
|
+
// once, after first render:
|
|
35
|
+
const dispose = wireTabScaffold(root, { onSelect: (id) => { active.value = id; } });
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Each `TabScaffoldTab` has an `id`, `label`, optional `icon`, and `content`. The
|
|
39
|
+
bottom bar respects the home-indicator safe area (`env(safe-area-inset-bottom)`)
|
|
40
|
+
and keeps 44px targets. On larger device classes, promote the tab set to a
|
|
41
|
+
`Workbench` rail or a persistent sidebar instead of a bottom bar.
|