@kerfjs/ui 5.0.0-beta.97 → 5.0.0-beta.99
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 +42 -1
- package/ai/application-ui-diagnostic-ids-v1.json +1 -0
- package/ai/component-catalog.json +487 -66
- package/ai/component-catalog.schema.json +1 -1
- package/ai/component-composition-rules.json +22 -2
- package/ai/component-composition.json +360 -33
- package/ai/component-composition.schema.json +1 -1
- package/ai/components/README.md +18 -17
- package/ai/components/anchored-surface.md +13 -0
- package/ai/components/application-tabs.md +1 -1
- package/ai/components/chip.md +9 -5
- package/ai/components/choice-chips.md +15 -8
- package/ai/components/collapsible-panel.md +3 -2
- package/ai/components/composer.md +83 -0
- package/ai/components/content-item.md +2 -2
- package/ai/components/corner-status.md +1 -1
- package/ai/components/document-baseline.md +3 -3
- package/ai/components/foundation.md +2 -2
- package/ai/components/layout.md +1 -1
- package/ai/components/list-action-row.md +5 -1
- package/ai/components/list-item.md +5 -1
- package/ai/components/list.md +19 -3
- package/ai/components/nav-stack.md +11 -2
- package/ai/components/pane.md +13 -4
- package/ai/components/popup-menu.md +4 -4
- package/ai/components/recipe-app-shell.md +3 -3
- package/ai/components/recipe-collapsible-sidebar.md +3 -3
- package/ai/components/recipe-compact-toolbar.md +1 -1
- package/ai/components/recipe-composer-form.md +3 -3
- package/ai/components/recipe-list-detail-dialog.md +2 -2
- package/ai/components/recipe-list-workspace-states.md +3 -3
- package/ai/components/recipe-loading-inspector.md +1 -1
- package/ai/components/recipe-navigation-sidebar.md +3 -3
- package/ai/components/recipe-navigation-stack.md +3 -3
- package/ai/components/recipe-workspace-header.md +3 -3
- package/ai/components/resize.md +2 -2
- package/ai/components/split-view.md +4 -2
- package/ai/components/sunken-panel.md +2 -2
- package/ai/components/surface-scaffold.md +15 -3
- package/ai/components/tab-bar.md +1 -0
- package/ai/components/tab-navigator.md +4 -0
- package/ai/components/toolbar-control-group.md +2 -0
- package/ai/components/toolbar-text.md +1 -0
- package/ai/components/toolbar.md +1 -0
- package/ai/components/workbench.md +1 -0
- package/ai/public-api-signatures-v1.md +504 -80
- package/ai/skill.md +41 -14
- package/ai/webawesome-jsx-signatures-v1.md +1 -1
- package/analyzer/component-ownership.mjs +1 -1
- package/analyzer/index.mjs +41 -14
- package/dist/anchored-surface.d.ts +30 -2
- package/dist/anchored-surface.js +1 -1
- package/dist/app-tab.js +1 -1
- package/dist/browser/composer.js +4 -0
- package/dist/catalog-resources.d.ts +3 -2
- package/dist/catalog-resources.js +4 -4
- package/dist/catalog-resources.js.map +1 -1
- package/dist/catalog.d.ts +39 -8
- package/dist/catalog.js +55 -32
- package/dist/catalog.js.map +1 -1
- package/dist/chip.d.ts +41 -9
- package/dist/chip.js +1 -1
- package/dist/choice-chips.d.ts +40 -16
- package/dist/choice-chips.js +1 -1
- package/dist/chunk-34U4ANDJ.js +119 -0
- package/dist/chunk-34U4ANDJ.js.map +1 -0
- package/dist/{chunk-36TL6BCR.js → chunk-3GLYIIFR.js} +34 -6
- package/dist/chunk-3GLYIIFR.js.map +1 -0
- package/dist/{chunk-HWY4BSLS.js → chunk-3VXDSIBJ.js} +2 -2
- package/dist/{chunk-HWY4BSLS.js.map → chunk-3VXDSIBJ.js.map} +1 -1
- package/dist/chunk-3WYREXW4.js +120 -0
- package/dist/chunk-3WYREXW4.js.map +1 -0
- package/dist/chunk-AV7CPAPT.js +82 -0
- package/dist/chunk-AV7CPAPT.js.map +1 -0
- package/dist/{chunk-XK4E225T.js → chunk-B53PS2LE.js} +2 -2
- package/dist/{chunk-XK4E225T.js.map → chunk-B53PS2LE.js.map} +1 -1
- package/dist/{chunk-R42D2BAO.js → chunk-CA7LNX3J.js} +2 -2
- package/dist/{chunk-R42D2BAO.js.map → chunk-CA7LNX3J.js.map} +1 -1
- package/dist/{chunk-AYHC62OZ.js → chunk-CMETHPKR.js} +26 -29
- package/dist/chunk-CMETHPKR.js.map +1 -0
- package/dist/{chunk-Y43B6DS3.js → chunk-CVD5IZGB.js} +2 -2
- package/dist/{chunk-Y43B6DS3.js.map → chunk-CVD5IZGB.js.map} +1 -1
- package/dist/{chunk-6STQXRT4.js → chunk-CXZXHZTR.js} +121 -12
- package/dist/chunk-CXZXHZTR.js.map +1 -0
- package/dist/{chunk-GZE2XVKC.js → chunk-DZJ4GBSP.js} +3 -3
- package/dist/{chunk-GZE2XVKC.js.map → chunk-DZJ4GBSP.js.map} +1 -1
- package/dist/chunk-ELCMSQNJ.js +49 -0
- package/dist/chunk-ELCMSQNJ.js.map +1 -0
- package/dist/{chunk-P74VXMB6.js → chunk-EOO6PYGF.js} +7 -14
- package/dist/chunk-EOO6PYGF.js.map +1 -0
- package/dist/{chunk-PRG4CL3Q.js → chunk-FR76ZJQC.js} +21 -19
- package/dist/chunk-FR76ZJQC.js.map +1 -0
- package/dist/{chunk-QQ3TTWFH.js → chunk-G3J7JDP6.js} +1 -1
- package/dist/{chunk-QQ3TTWFH.js.map → chunk-G3J7JDP6.js.map} +1 -1
- package/dist/{chunk-IGTWXPTN.js → chunk-HGV7J4T6.js} +3 -3
- package/dist/{chunk-IGTWXPTN.js.map → chunk-HGV7J4T6.js.map} +1 -1
- package/dist/{chunk-YWQGSUF2.js → chunk-HLDRH23D.js} +3 -3
- package/dist/{chunk-YWQGSUF2.js.map → chunk-HLDRH23D.js.map} +1 -1
- package/dist/{chunk-PBT3CPPY.js → chunk-HQZYKJ56.js} +24 -4
- package/dist/chunk-HQZYKJ56.js.map +1 -0
- package/dist/{chunk-V6W56V3N.js → chunk-HZPHCSCS.js} +16 -3
- package/dist/chunk-HZPHCSCS.js.map +1 -0
- package/dist/{chunk-GJYQ27C2.js → chunk-INI4WDWC.js} +8 -1
- package/dist/chunk-INI4WDWC.js.map +1 -0
- package/dist/{chunk-U657IHFB.js → chunk-IQSRHXZR.js} +10 -4
- package/dist/chunk-IQSRHXZR.js.map +1 -0
- package/dist/{chunk-KQDU7Z7P.js → chunk-K4AT3V3J.js} +2 -1
- package/dist/chunk-K4AT3V3J.js.map +1 -0
- package/dist/{chunk-QIAPOWHH.js → chunk-KCS6NJ3O.js} +2 -2
- package/dist/{chunk-QIAPOWHH.js.map → chunk-KCS6NJ3O.js.map} +1 -1
- package/dist/{chunk-UKKRPP56.js → chunk-LOSRXZAH.js} +2 -2
- package/dist/{chunk-UKKRPP56.js.map → chunk-LOSRXZAH.js.map} +1 -1
- package/dist/{chunk-ROZLJULF.js → chunk-MF6ZUUJZ.js} +8 -3
- package/dist/chunk-MF6ZUUJZ.js.map +1 -0
- package/dist/{chunk-6IVNMC6R.js → chunk-MHMWQ7RB.js} +2 -2
- package/dist/{chunk-6IVNMC6R.js.map → chunk-MHMWQ7RB.js.map} +1 -1
- package/dist/{chunk-JYD7G4RG.js → chunk-PIVY32AE.js} +15 -5
- package/dist/chunk-PIVY32AE.js.map +1 -0
- package/dist/{chunk-BU23Z5HZ.js → chunk-PYBBYSZ3.js} +13 -18
- package/dist/chunk-PYBBYSZ3.js.map +1 -0
- package/dist/chunk-QDSZTQXL.js +28 -0
- package/dist/chunk-QDSZTQXL.js.map +1 -0
- package/dist/{chunk-YXOMI7ES.js → chunk-QIG4IGUP.js} +2 -2
- package/dist/{chunk-YXOMI7ES.js.map → chunk-QIG4IGUP.js.map} +1 -1
- package/dist/{chunk-6BRRUPJY.js → chunk-QROYYGWU.js} +3 -3
- package/dist/chunk-QROYYGWU.js.map +1 -0
- package/dist/{chunk-FOQK4F3L.js → chunk-RHD7RKGI.js} +28 -7
- package/dist/chunk-RHD7RKGI.js.map +1 -0
- package/dist/{chunk-BBU2SPRE.js → chunk-T4WJ5VGP.js} +4 -4
- package/dist/{chunk-BBU2SPRE.js.map → chunk-T4WJ5VGP.js.map} +1 -1
- package/dist/{chunk-6NOLL5KG.js → chunk-TGLUDQP2.js} +3 -3
- package/dist/{chunk-6NOLL5KG.js.map → chunk-TGLUDQP2.js.map} +1 -1
- package/dist/{chunk-HO6FU3G7.js → chunk-TLMMFQWM.js} +4 -4
- package/dist/{chunk-HO6FU3G7.js.map → chunk-TLMMFQWM.js.map} +1 -1
- package/dist/chunk-UMKV7YVQ.js +20 -0
- package/dist/chunk-UMKV7YVQ.js.map +1 -0
- package/dist/{chunk-M2NRCFRO.js → chunk-VFYO3UGZ.js} +2 -2
- package/dist/{chunk-M2NRCFRO.js.map → chunk-VFYO3UGZ.js.map} +1 -1
- package/dist/{chunk-YOKEL75M.js → chunk-WVS6CUCT.js} +2 -2
- package/dist/{chunk-YOKEL75M.js.map → chunk-WVS6CUCT.js.map} +1 -1
- package/dist/{chunk-MN3POJFN.js → chunk-YMUIQRWK.js} +2 -2
- package/dist/{chunk-MN3POJFN.js.map → chunk-YMUIQRWK.js.map} +1 -1
- package/dist/{chunk-Y3LC7JQO.js → chunk-YOA5W5ZH.js} +2 -2
- package/dist/{chunk-Y3LC7JQO.js.map → chunk-YOA5W5ZH.js.map} +1 -1
- package/dist/{chunk-44A6LP54.js → chunk-YQ2AC4E2.js} +2 -2
- package/dist/{chunk-44A6LP54.js.map → chunk-YQ2AC4E2.js.map} +1 -1
- package/dist/chunk-ZNROKDSZ.js +54 -0
- package/dist/chunk-ZNROKDSZ.js.map +1 -0
- package/dist/collapsible-panel.d.ts +3 -2
- package/dist/collapsible-panel.js +4 -3
- package/dist/collapsible-panel.js.map +1 -1
- package/dist/composer.d.ts +62 -0
- package/dist/composer.js +5 -0
- package/dist/composer.js.map +1 -0
- package/dist/corner-status.d.ts +1 -1
- package/dist/corner-status.js +1 -1
- package/dist/css-values.d.ts +9 -1
- package/dist/css-values.js +1 -1
- package/dist/disclosure-arrow.js +1 -1
- package/dist/empty-state.js +1 -1
- package/dist/grid.js +1 -1
- package/dist/icon-button.js +1 -1
- package/dist/icon-toggle-button.js +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.js +40 -37
- package/dist/list-action-row.d.ts +10 -2
- package/dist/list-action-row.js +1 -1
- package/dist/list-header.js +1 -1
- package/dist/list-icon-tile-B8dr97OW.d.ts +18 -0
- package/dist/list-item.d.ts +10 -3
- package/dist/list-item.js +1 -1
- package/dist/list.d.ts +36 -4
- package/dist/list.js +1 -1
- package/dist/loading-spinner.js +1 -1
- package/dist/lucide-icon.js +1 -1
- package/dist/{nav-stack-BvSKY448.d.ts → nav-stack-VXTumzKa.d.ts} +53 -4
- package/dist/nav-stack.d.ts +3 -2
- package/dist/nav-stack.js +2 -1
- package/dist/pane.d.ts +40 -3
- package/dist/pane.js +1 -1
- package/dist/{panel-toolbar-CL9tYOkh.d.ts → panel-toolbar-vg6Ir5Dy.d.ts} +6 -1
- package/dist/popup-menu-register.d.ts +2 -1
- package/dist/popup-menu-register.js +97 -22
- package/dist/popup-menu-register.js.map +1 -1
- package/dist/popup-menu.js +1 -1
- package/dist/row.js +1 -1
- package/dist/scroll-clearance-BikHljTc.d.ts +14 -0
- package/dist/select-register.js +11 -0
- package/dist/select-register.js.map +1 -1
- package/dist/select.js +1 -1
- package/dist/skeleton.js +1 -1
- package/dist/spacer.js +1 -1
- package/dist/split-button.js +1 -1
- package/dist/split-view.d.ts +7 -5
- package/dist/split-view.js +6 -3
- package/dist/split-view.js.map +1 -1
- package/dist/state-banner.d.ts +6 -1
- package/dist/state-banner.js +1 -1
- package/dist/styles/anchored-surface.css +27 -1
- package/dist/styles/catalog/components/catalog-example-stack.css +5 -0
- package/dist/styles/catalog/components/catalog-example.css +33 -0
- package/dist/styles/catalog/components/catalog-stage.css +7 -0
- package/dist/styles/chip.css +70 -0
- package/dist/styles/components/actions/popup-menu/popup-menu.css +21 -4
- package/dist/styles/components/actions/toolbar/toolbar.css +19 -0
- package/dist/styles/components/actions/toolbar-control-group/toolbar-control-group.css +75 -1
- package/dist/styles/components/actions/toolbar-text/toolbar-text.css +75 -0
- package/dist/styles/components/collections/list/list.css +77 -7
- package/dist/styles/components/collections/list-action-row/list-action-row.css +177 -22
- package/dist/styles/components/collections/list-item/list-item.css +158 -11
- package/dist/styles/components/feedback/anchored-surface/anchored-surface.css +27 -1
- package/dist/styles/components/feedback/chip/chip.css +70 -0
- package/dist/styles/components/feedback/state-banner/state-banner.css +24 -13
- package/dist/styles/components/forms/composer/composer.css +100 -0
- package/dist/styles/components/forms/select/select.css +1 -0
- package/dist/styles/components/forms/token-search-field/token-search-field.css +13 -2
- package/dist/styles/components/layout/pane/pane.css +81 -2
- package/dist/styles/components/navigation/nav-stack/nav-stack.css +84 -0
- package/dist/styles/components/navigation/tab-bar/tab-bar.css +15 -4
- package/dist/styles/components/navigation/tab-navigator/tab-navigator.css +79 -4
- package/dist/styles/components/surfaces/surface-scaffold/surface-scaffold.css +195 -0
- package/dist/styles/composer.css +100 -0
- package/dist/styles/foundation.css +66 -0
- package/dist/styles/list-action-row.css +177 -22
- package/dist/styles/list-item.css +158 -11
- package/dist/styles/list.css +77 -7
- package/dist/styles/nav-stack.css +84 -0
- package/dist/styles/pane.css +81 -2
- package/dist/styles/popup-menu.css +21 -4
- package/dist/styles/select.css +1 -0
- package/dist/styles/state-banner.css +24 -13
- package/dist/styles/styles.css +1 -0
- package/dist/styles/surface-scaffold.css +195 -0
- package/dist/styles/tab-bar.css +15 -4
- package/dist/styles/tab-navigator.css +79 -4
- package/dist/styles/token-search-field.css +13 -2
- package/dist/styles/toolbar-control-group.css +75 -1
- package/dist/styles/toolbar-text.css +75 -0
- package/dist/styles/toolbar.css +19 -0
- package/dist/styles/webawesome.css +18 -0
- package/dist/surface-scaffold-register.d.ts +6 -1
- package/dist/surface-scaffold-register.js +89 -3
- package/dist/surface-scaffold-register.js.map +1 -1
- package/dist/surface-scaffold.d.ts +66 -7
- package/dist/surface-scaffold.js +1 -1
- package/dist/tab-navigator.d.ts +23 -2
- package/dist/tab-navigator.js +2 -0
- package/dist/tab-navigator.js.map +1 -1
- package/dist/token-search-field.d.ts +1 -1
- package/dist/token-search-field.js +1 -1
- package/dist/{token-search-model-DTcTXWMS.d.ts → token-search-model-aYR0YAei.d.ts} +32 -2
- package/dist/token-search-model.d.ts +1 -1
- package/dist/toolbar-control-group.js +1 -1
- package/dist/toolbar-text.js +1 -1
- package/dist/toolbar.d.ts +1 -0
- package/dist/value-table.js +1 -1
- package/dist/wire-catalog.d.ts +3 -2
- package/dist/wire-catalog.js +2 -2
- package/dist/wire-composers.d.ts +16 -0
- package/dist/wire-composers.js +3 -0
- package/dist/wire-composers.js.map +1 -0
- package/dist/wire-list-focus-scroll.d.ts +16 -0
- package/dist/wire-list-focus-scroll.js +3 -0
- package/dist/wire-list-focus-scroll.js.map +1 -0
- package/dist/wire-nav-stack.js +135 -0
- package/dist/wire-nav-stack.js.map +1 -1
- package/dist/wire-scroll-dividers.d.ts +9 -0
- package/dist/wire-scroll-dividers.js +1 -1
- package/dist/wire-tab-bars.js +1 -1
- package/dist/wire-tab-navigator.d.ts +3 -1
- package/dist/wire-tab-navigator.js +133 -1
- package/dist/wire-tab-navigator.js.map +1 -1
- package/dist/wire-token-search-fields.d.ts +1 -1
- package/dist/wire-token-search-fields.js +1 -1
- package/dist/workbench.d.ts +8 -3
- package/dist/workbench.js +2 -1
- package/docs/accessibility.md +132 -3
- package/docs/anchored-surface.md +46 -4
- package/docs/app-layouts.md +23 -2
- package/docs/apple-hig.md +1 -0
- package/docs/catalog.md +61 -9
- package/docs/collapsible-panel.md +9 -6
- package/docs/component-contract.md +46 -13
- package/docs/component-selection.md +147 -20
- package/docs/composer.md +143 -0
- package/docs/design/templates/choice-chips/multiple-dark.svg +1 -0
- package/docs/design/templates/choice-chips/multiple.svg +1 -0
- package/docs/design/templates/choice-chips-dark.svg +1 -1
- package/docs/design/templates/choice-chips.svg +1 -1
- package/docs/design/templates/composer/chat-dark.svg +1 -0
- package/docs/design/templates/composer/chat.svg +1 -0
- package/docs/design/templates/composer/prompt-dark.svg +1 -0
- package/docs/design/templates/composer/prompt.svg +1 -0
- package/docs/design/templates/composer-dark.svg +1 -0
- package/docs/design/templates/composer.svg +1 -0
- package/docs/design/templates/empty-state/filtered-dark.svg +1 -1
- package/docs/design/templates/empty-state/filtered.svg +1 -1
- package/docs/design/templates/empty-state/with-action-dark.svg +1 -1
- package/docs/design/templates/empty-state/with-action.svg +1 -1
- package/docs/design/templates/empty-state-dark.svg +1 -1
- package/docs/design/templates/empty-state.svg +1 -1
- package/docs/design/templates/list/compact-dark.svg +1 -1
- package/docs/design/templates/list/compact.svg +1 -1
- package/docs/design/templates/list-action-row/default-dark.svg +1 -1
- package/docs/design/templates/list-action-row/default.svg +1 -1
- package/docs/design/templates/list-action-row/icon-tile-dark.svg +1 -0
- package/docs/design/templates/list-action-row/icon-tile.svg +1 -0
- package/docs/design/templates/list-action-row/selected-dark.svg +1 -1
- package/docs/design/templates/list-action-row/selected.svg +1 -1
- package/docs/design/templates/list-action-row/trailing-menu-dark.svg +1 -1
- package/docs/design/templates/list-action-row/trailing-menu.svg +1 -1
- package/docs/design/templates/list-action-row-dark.svg +1 -1
- package/docs/design/templates/list-action-row.svg +1 -1
- package/docs/design/templates/list-dark.svg +1 -1
- package/docs/design/templates/list-header/size-l-dark.svg +1 -1
- package/docs/design/templates/list-header/size-l.svg +1 -1
- package/docs/design/templates/list-header/size-m-dark.svg +1 -1
- package/docs/design/templates/list-header/size-m.svg +1 -1
- package/docs/design/templates/list-header/size-xl-dark.svg +1 -1
- package/docs/design/templates/list-header/size-xl.svg +1 -1
- package/docs/design/templates/list-header-dark.svg +1 -1
- package/docs/design/templates/list-header.svg +1 -1
- package/docs/design/templates/list-item/icon-tile-dark.svg +1 -0
- package/docs/design/templates/list-item/icon-tile.svg +1 -0
- package/docs/design/templates/list-item/selected-dark.svg +1 -1
- package/docs/design/templates/list-item/selected.svg +1 -1
- package/docs/design/templates/list-item-dark.svg +1 -1
- package/docs/design/templates/list-item.svg +1 -1
- package/docs/design/templates/list.svg +1 -1
- package/docs/design/templates/segmented-control-dark.svg +1 -1
- package/docs/design/templates/segmented-control.svg +1 -1
- package/docs/design/templates/skeleton-dark.svg +1 -1
- package/docs/design/templates/skeleton.svg +1 -1
- package/docs/design/templates/state-banner/danger-dark.svg +1 -1
- package/docs/design/templates/state-banner/danger.svg +1 -1
- package/docs/design/templates/state-banner/info-badge-dark.svg +1 -1
- package/docs/design/templates/state-banner/info-badge.svg +1 -1
- package/docs/design/templates/state-banner/info-dark.svg +1 -1
- package/docs/design/templates/state-banner/info.svg +1 -1
- package/docs/design/templates/state-banner/strip-dark.svg +1 -1
- package/docs/design/templates/state-banner/strip.svg +1 -1
- package/docs/design/templates/state-banner/success-dark.svg +1 -1
- package/docs/design/templates/state-banner/success.svg +1 -1
- package/docs/design/templates/state-banner/warning-dark.svg +1 -1
- package/docs/design/templates/state-banner/warning.svg +1 -1
- package/docs/design/templates/state-banner-dark.svg +1 -1
- package/docs/design/templates/state-banner.svg +1 -1
- package/docs/design/templates/tab-bar-dark.svg +1 -1
- package/docs/design/templates/tab-bar.svg +1 -1
- package/docs/design/templates/token-search-field-dark.svg +1 -1
- package/docs/design/templates/token-search-field.svg +1 -1
- package/docs/design/templates/toolbar-control-group-dark.svg +1 -1
- package/docs/design/templates/toolbar-control-group.svg +1 -1
- package/docs/design/templates/toolbar-dark.svg +1 -1
- package/docs/design/templates/toolbar-text/default-dark.svg +1 -1
- package/docs/design/templates/toolbar-text/default.svg +1 -1
- package/docs/design/templates/toolbar-text/large-dark.svg +1 -1
- package/docs/design/templates/toolbar-text/large.svg +1 -1
- package/docs/design/templates/toolbar-text-dark.svg +1 -1
- package/docs/design/templates/toolbar-text.svg +1 -1
- package/docs/design/templates/toolbar.svg +1 -1
- package/docs/design/templates/value-table/details-dark.svg +1 -1
- package/docs/design/templates/value-table/details.svg +1 -1
- package/docs/design/templates/value-table-dark.svg +1 -1
- package/docs/design/templates/value-table.svg +1 -1
- package/docs/design-philosophy.md +9 -1
- package/docs/examples/command-palette-adapter.tsx +3 -3
- package/docs/layout.md +147 -14
- package/docs/nav-stack.md +132 -8
- package/docs/recipes.md +9 -9
- package/docs/split-view.md +3 -3
- package/docs/sunken-panel.md +2 -2
- package/docs/surface-scaffold.md +138 -7
- package/docs/tab-navigator.md +38 -1
- package/docs/type-contracts.md +16 -16
- package/docs/ui-analyzer.md +3 -1
- package/docs/ui-doctor.md +71 -0
- package/docs/ux-demo.md +6 -6
- package/docs/webawesome-theme.md +7 -0
- package/docs/workbench.md +10 -6
- package/docs/writing.md +152 -0
- package/doctor/copy-capitalization-checks.mjs +591 -0
- package/doctor/index.mjs +17 -8
- package/llms.txt +105 -11
- package/package.json +15 -1
- package/ux-demo/recipes/app-shell.tsx +11 -11
- package/ux-demo/recipes/collapsible-sidebar.tsx +12 -11
- package/ux-demo/recipes/compact-toolbar.tsx +10 -10
- package/ux-demo/recipes/composer-form.tsx +8 -8
- package/ux-demo/recipes/list-detail-dialog.tsx +13 -13
- package/ux-demo/recipes/list-workspace-states.tsx +13 -13
- package/ux-demo/recipes/loading-inspector.tsx +10 -10
- package/ux-demo/recipes/navigation-sidebar.tsx +7 -7
- package/ux-demo/recipes/navigation-stack.tsx +5 -5
- package/ux-demo/recipes/workspace-header.tsx +5 -5
- package/dist/chunk-36TL6BCR.js.map +0 -1
- package/dist/chunk-4LOK4634.js +0 -21
- package/dist/chunk-4LOK4634.js.map +0 -1
- package/dist/chunk-6BRRUPJY.js.map +0 -1
- package/dist/chunk-6STQXRT4.js.map +0 -1
- package/dist/chunk-76PWEN5Y.js +0 -64
- package/dist/chunk-76PWEN5Y.js.map +0 -1
- package/dist/chunk-AYHC62OZ.js.map +0 -1
- package/dist/chunk-BU23Z5HZ.js.map +0 -1
- package/dist/chunk-DQDTZE4W.js +0 -70
- package/dist/chunk-DQDTZE4W.js.map +0 -1
- package/dist/chunk-FOQK4F3L.js.map +0 -1
- package/dist/chunk-GJYQ27C2.js.map +0 -1
- package/dist/chunk-JYD7G4RG.js.map +0 -1
- package/dist/chunk-KQDU7Z7P.js.map +0 -1
- package/dist/chunk-P74VXMB6.js.map +0 -1
- package/dist/chunk-PBT3CPPY.js.map +0 -1
- package/dist/chunk-PRG4CL3Q.js.map +0 -1
- package/dist/chunk-ROZLJULF.js.map +0 -1
- package/dist/chunk-U657IHFB.js.map +0 -1
- package/dist/chunk-V6W56V3N.js.map +0 -1
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { extname, relative } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import ts from 'typescript';
|
|
5
|
+
|
|
6
|
+
import { isUiTraversalExcluded } from '../traversal-exclusions.mjs';
|
|
7
|
+
|
|
8
|
+
// KUI-D040: visible labels that Kerf UI's capitalization rule (docs/writing.md)
|
|
9
|
+
// gives title style — button titles, menu items, segment and pop-up choices,
|
|
10
|
+
// tab labels, field labels, list/group headers (including catalog example
|
|
11
|
+
// labels), view/pane/dialog titles, panel toggle names, and region and
|
|
12
|
+
// landmark names — plus the accessible names of icon-only controls, checked
|
|
13
|
+
// against Apple's title-style rule. Only string and template literals passed
|
|
14
|
+
// to known title-style props are read (a template literal's static words, never
|
|
15
|
+
// its interpolated values), so prose, descriptions, placeholders, tooltips,
|
|
16
|
+
// checkbox/switch option text, status lines, LoadingSpinner and Skeleton
|
|
17
|
+
// labels, and dynamic data are never judged. The check reports lowercase words title style capitalizes; it
|
|
18
|
+
// does not report capitalized short words, because Apple capitalizes a
|
|
19
|
+
// preposition of any length inside a phrasal verb ("Log In to the Server").
|
|
20
|
+
|
|
21
|
+
const scriptExtensions = new Set(['.tsx', '.jsx']);
|
|
22
|
+
|
|
23
|
+
// Apple Style Guide, "capitalization": articles, coordinating conjunctions,
|
|
24
|
+
// `to` in infinitives, `as`, and prepositions of four letters or fewer stay
|
|
25
|
+
// lowercase unless they are the first or last word or follow a colon.
|
|
26
|
+
// `via`, `per`, and `vs` are added as other short prepositions.
|
|
27
|
+
export const TITLE_STYLE_MINOR_WORDS = Object.freeze(
|
|
28
|
+
new Set([
|
|
29
|
+
'a',
|
|
30
|
+
'an',
|
|
31
|
+
'the',
|
|
32
|
+
'and',
|
|
33
|
+
'but',
|
|
34
|
+
'or',
|
|
35
|
+
'nor',
|
|
36
|
+
'for',
|
|
37
|
+
'yet',
|
|
38
|
+
'so',
|
|
39
|
+
'as',
|
|
40
|
+
'at',
|
|
41
|
+
'by',
|
|
42
|
+
'from',
|
|
43
|
+
'in',
|
|
44
|
+
'into',
|
|
45
|
+
'of',
|
|
46
|
+
'off',
|
|
47
|
+
'on',
|
|
48
|
+
'onto',
|
|
49
|
+
'out',
|
|
50
|
+
'over',
|
|
51
|
+
'to',
|
|
52
|
+
'up',
|
|
53
|
+
'with',
|
|
54
|
+
'per',
|
|
55
|
+
'via',
|
|
56
|
+
'vs',
|
|
57
|
+
]),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Apple keeps the second part of these compounds lowercase.
|
|
61
|
+
const lowercaseCompoundTails = new Set(['built-in', 'plug-in']);
|
|
62
|
+
const longestTitle = 12;
|
|
63
|
+
|
|
64
|
+
const capitalize = (word) => word.charAt(0).toUpperCase() + word.slice(1);
|
|
65
|
+
const startsLowercase = (word) => /^\p{Ll}/u.test(word);
|
|
66
|
+
|
|
67
|
+
/** A token title style leaves alone: a number, code, path, URL, or an intentionally lowercase-led brand such as iOS or macOS. */
|
|
68
|
+
function exempt(core) {
|
|
69
|
+
if (!/\p{L}/u.test(core)) return true;
|
|
70
|
+
if (/\d/.test(core)) return true;
|
|
71
|
+
if (/[./\\@_#=<>{}[\]|`~^*+]/.test(core)) return true;
|
|
72
|
+
// Lowercase-led mixed case is a deliberate brand or identifier spelling.
|
|
73
|
+
if (/^\p{Ll}[\p{L}'’-]*\p{Lu}/u.test(core)) return true;
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Split a word into leading punctuation, its letters, and trailing punctuation. */
|
|
78
|
+
function parts(token) {
|
|
79
|
+
const match = /^([^\p{L}\p{N}]*)(.*?)([^\p{L}\p{N}]*)$/u.exec(token);
|
|
80
|
+
return { lead: match[1], core: match[2], trail: match[3] };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check one visible label against Apple's title-style capitalization.
|
|
85
|
+
*
|
|
86
|
+
* Returns `null` when the text is title style, is a complete sentence (it
|
|
87
|
+
* ends with `.`, `!`, or `?`, so sentence style applies), or is too long to
|
|
88
|
+
* be a label. Otherwise returns the lowercase words to capitalize and the
|
|
89
|
+
* repaired text.
|
|
90
|
+
*/
|
|
91
|
+
export function titleStyleViolation(text) {
|
|
92
|
+
const value = String(text).trim();
|
|
93
|
+
if (!value || /\n/.test(value)) return null;
|
|
94
|
+
// A complete sentence takes sentence style and ending punctuation; an
|
|
95
|
+
// ellipsis only marks a command that asks for more input.
|
|
96
|
+
if (/(?<![.…])[.!?]["'”’)\]]*$/u.test(value)) return null;
|
|
97
|
+
const tokens = value.split(/(\s+)/);
|
|
98
|
+
const words = tokens
|
|
99
|
+
.map((token, index) => ({ token, index }))
|
|
100
|
+
.filter(({ token }) => token.trim());
|
|
101
|
+
if (!words.some(({ token }) => /\p{L}/u.test(token))) return null;
|
|
102
|
+
if (words.length > longestTitle) return null;
|
|
103
|
+
const flagged = [];
|
|
104
|
+
let afterColon = false;
|
|
105
|
+
for (const [position, { token, index }] of words.entries()) {
|
|
106
|
+
const { lead, core, trail } = parts(token);
|
|
107
|
+
const first = position === 0 || afterColon;
|
|
108
|
+
const last = position === words.length - 1;
|
|
109
|
+
afterColon = /:$/.test(trail);
|
|
110
|
+
if (!core || exempt(core)) continue;
|
|
111
|
+
const segments = core.split('-');
|
|
112
|
+
const fixed = segments.map((segment, segmentIndex) => {
|
|
113
|
+
if (!segment || !startsLowercase(segment)) return segment;
|
|
114
|
+
if (segmentIndex === 0) {
|
|
115
|
+
const minor = TITLE_STYLE_MINOR_WORDS.has(segment.toLowerCase());
|
|
116
|
+
if (minor && !first && !last && segments.length === 1) return segment;
|
|
117
|
+
return capitalize(segment);
|
|
118
|
+
}
|
|
119
|
+
// Second word of a hyphenated compound, except Built-in and Plug-in,
|
|
120
|
+
// and except a single-letter prefix such as e-mail.
|
|
121
|
+
if (lowercaseCompoundTails.has(core.toLowerCase())) return segment;
|
|
122
|
+
if (segments[0].length === 1) return segment;
|
|
123
|
+
// A minor word inside a longer compound stays lowercase, as it would
|
|
124
|
+
// between separate words: Edge-to-Edge, Up-to-Date.
|
|
125
|
+
if (
|
|
126
|
+
segmentIndex < segments.length - 1 &&
|
|
127
|
+
TITLE_STYLE_MINOR_WORDS.has(segment.toLowerCase())
|
|
128
|
+
)
|
|
129
|
+
return segment;
|
|
130
|
+
return capitalize(segment);
|
|
131
|
+
});
|
|
132
|
+
const repaired = fixed.join('-');
|
|
133
|
+
if (repaired !== core) {
|
|
134
|
+
flagged.push(core);
|
|
135
|
+
tokens[index] = `${lead}${repaired}${trail}`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (!flagged.length) return null;
|
|
139
|
+
return { words: flagged, suggestion: tokens.join('') };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Component props whose string is a visible title-style label, or the
|
|
143
|
+
// accessible name of an icon-only control (docs/writing.md gives those title
|
|
144
|
+
// style too: "More Actions", "Clear Search"). `props` names a direct string
|
|
145
|
+
// prop; `entries` names an array prop whose object entries carry the label
|
|
146
|
+
// under one of `keys` (walked through nested arrays such as a PopupMenu
|
|
147
|
+
// `submenu`).
|
|
148
|
+
const titleStyleProps = new Map([
|
|
149
|
+
['ListHeader', { props: ['label'] }],
|
|
150
|
+
['ListActionRow', { props: ['label'] }],
|
|
151
|
+
// A catalog example or stack label renders as a ListHeader and names the
|
|
152
|
+
// specimen's region, so it is a heading.
|
|
153
|
+
['CatalogExample', { props: ['label'] }],
|
|
154
|
+
['CatalogExampleStack', { props: ['label'] }],
|
|
155
|
+
['EmptyState', { props: ['title'] }],
|
|
156
|
+
['StateBanner', { props: ['title'] }],
|
|
157
|
+
['SplitButton', { props: ['label', 'optionsLabel'] }],
|
|
158
|
+
['Combobox', { props: ['label'] }],
|
|
159
|
+
['IconButton', { props: ['label'] }],
|
|
160
|
+
['IconToggleButton', { props: ['label'] }],
|
|
161
|
+
['TokenSearchField', { props: ['clearLabel'] }],
|
|
162
|
+
// NavStack view titles and backText, plus the stack's region name and each
|
|
163
|
+
// view's toolbar and pane region names.
|
|
164
|
+
[
|
|
165
|
+
'NavStack',
|
|
166
|
+
{
|
|
167
|
+
props: ['backText', 'label'],
|
|
168
|
+
entries: {
|
|
169
|
+
views: ['title', 'label', 'contentLabel'],
|
|
170
|
+
bottomToolbar: ['label'],
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
['SplitView', { props: ['backLabel'] }],
|
|
175
|
+
['PopupMenu', { props: ['text', 'label'], entries: { items: ['label'] } }],
|
|
176
|
+
['SegmentedControl', { entries: { choices: ['label'] } }],
|
|
177
|
+
['Select', { entries: { choices: ['label'] } }],
|
|
178
|
+
['TabNavigator', { entries: { tabs: ['label'] } }],
|
|
179
|
+
// Region and landmark names: the accessible name of a pane, toolbar,
|
|
180
|
+
// control group, list, tab bar, or layout region names it, so it is title
|
|
181
|
+
// style ("Ticket Workbench", "View Actions"). Structured-config props carry
|
|
182
|
+
// the same names on their nested panels, toolbars, and panes.
|
|
183
|
+
['Pane', { props: ['label', 'contentLabel'] }],
|
|
184
|
+
[
|
|
185
|
+
'Workbench',
|
|
186
|
+
{
|
|
187
|
+
props: ['label'],
|
|
188
|
+
entries: {
|
|
189
|
+
leftRail: ['label', 'contentLabel'],
|
|
190
|
+
rightRail: ['label', 'contentLabel'],
|
|
191
|
+
bottomDrawer: ['label', 'contentLabel'],
|
|
192
|
+
mainToolbar: ['label'],
|
|
193
|
+
mainBottomToolbar: ['label'],
|
|
194
|
+
mainPane: ['contentLabel'],
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
['Toolbar', { props: ['label'] }],
|
|
199
|
+
['ToolbarControlGroup', { props: ['label'] }],
|
|
200
|
+
['SunkenPanel', { props: ['ariaLabel'] }],
|
|
201
|
+
['List', { props: ['ariaLabel'] }],
|
|
202
|
+
['TabBar', { props: ['label'] }],
|
|
203
|
+
[
|
|
204
|
+
'CollapsiblePanel',
|
|
205
|
+
{
|
|
206
|
+
props: ['label'],
|
|
207
|
+
entries: {
|
|
208
|
+
toolbar: ['label'],
|
|
209
|
+
bottomToolbar: ['label'],
|
|
210
|
+
pane: ['contentLabel'],
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
['FloatingToolbar', { props: ['label'] }],
|
|
215
|
+
['ResizableRegion', { props: ['label'] }],
|
|
216
|
+
]);
|
|
217
|
+
// Landmark elements and landmark roles: their `aria-label` names the region.
|
|
218
|
+
const landmarkTags = new Set([
|
|
219
|
+
'section',
|
|
220
|
+
'nav',
|
|
221
|
+
'aside',
|
|
222
|
+
'main',
|
|
223
|
+
'header',
|
|
224
|
+
'footer',
|
|
225
|
+
'form',
|
|
226
|
+
'search',
|
|
227
|
+
]);
|
|
228
|
+
const landmarkRoles = new Set([
|
|
229
|
+
'region',
|
|
230
|
+
'navigation',
|
|
231
|
+
'complementary',
|
|
232
|
+
'main',
|
|
233
|
+
'banner',
|
|
234
|
+
'contentinfo',
|
|
235
|
+
'form',
|
|
236
|
+
'search',
|
|
237
|
+
]);
|
|
238
|
+
// A raw Web Awesome field's or radio group's `label` is a field label. The
|
|
239
|
+
// option text of `wa-checkbox`, `wa-switch`, and `wa-radio` is sentence style
|
|
240
|
+
// and is not read.
|
|
241
|
+
const fieldLabelTags = new Set([
|
|
242
|
+
'wa-input',
|
|
243
|
+
'wa-select',
|
|
244
|
+
'wa-textarea',
|
|
245
|
+
'wa-radio-group',
|
|
246
|
+
]);
|
|
247
|
+
const titleToolbarTextSizes = new Set(['xlarge', 'xlarge-fixed', 'large']);
|
|
248
|
+
const buttonTags = new Set(['button', 'wa-button']);
|
|
249
|
+
const titleSlots = new Set(['title', 'leading', 'header']);
|
|
250
|
+
const statusToolbarTextSizes = new Set(['small', 'xsmall']);
|
|
251
|
+
|
|
252
|
+
function tagName(node) {
|
|
253
|
+
const name = node.tagName;
|
|
254
|
+
if (ts.isIdentifier(name)) return name.text;
|
|
255
|
+
if (ts.isPropertyAccessExpression(name)) return name.name.text;
|
|
256
|
+
return name.getText();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function attribute(element, name) {
|
|
260
|
+
return element.attributes.properties.find(
|
|
261
|
+
(item) =>
|
|
262
|
+
ts.isJsxAttribute(item) &&
|
|
263
|
+
ts.isIdentifier(item.name) &&
|
|
264
|
+
item.name.text === name,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// A template literal's interpolation stands in the checked text as a token
|
|
269
|
+
// with no letters, so title style never judges the interpolated data, and a
|
|
270
|
+
// word that touches an interpolation (`${count}s`) carries the token's digit
|
|
271
|
+
// and is skipped too. The static words keep their positions, so a lowercase
|
|
272
|
+
// word that ends the label (`Edit ${name} shortcut`) is still the last word.
|
|
273
|
+
const holeToken = (index) => `\u2045${index}\u2046`;
|
|
274
|
+
const holePattern = /\u2045(\d+)\u2046/g;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* A template literal with interpolations, read as a label whose static words
|
|
278
|
+
* are checked: `text` holds hole tokens for `title-style` judgment, `display`
|
|
279
|
+
* is the source spelling, and `restore` maps a repaired text back to source.
|
|
280
|
+
*/
|
|
281
|
+
function templateLabel(node) {
|
|
282
|
+
const holes = node.templateSpans.map(
|
|
283
|
+
(span) => `\${${span.expression.getText()}}`,
|
|
284
|
+
);
|
|
285
|
+
const text =
|
|
286
|
+
node.head.text +
|
|
287
|
+
node.templateSpans
|
|
288
|
+
.map((span, index) => holeToken(index) + span.literal.text)
|
|
289
|
+
.join('');
|
|
290
|
+
return {
|
|
291
|
+
text,
|
|
292
|
+
node,
|
|
293
|
+
display: text.replace(holePattern, (_, index) => holes[Number(index)]),
|
|
294
|
+
restore: (value) =>
|
|
295
|
+
value.replace(holePattern, (_, index) => holes[Number(index)]),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function literalOf(node) {
|
|
300
|
+
if (!node) return undefined;
|
|
301
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
|
|
302
|
+
return node;
|
|
303
|
+
if (ts.isTemplateExpression(node)) return templateLabel(node);
|
|
304
|
+
if (ts.isJsxExpression(node)) return literalOf(node.expression);
|
|
305
|
+
if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node))
|
|
306
|
+
return literalOf(node.expression);
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function attributeValueNode(item) {
|
|
311
|
+
if (!item?.initializer) return undefined;
|
|
312
|
+
return ts.isJsxExpression(item.initializer)
|
|
313
|
+
? item.initializer.expression
|
|
314
|
+
: item.initializer;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function textAttribute(element, name) {
|
|
318
|
+
const value = attributeValueNode(attribute(element, name));
|
|
319
|
+
return value
|
|
320
|
+
? ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)
|
|
321
|
+
? value.text
|
|
322
|
+
: undefined
|
|
323
|
+
: undefined;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Whether a ToolbarText renders a title rather than a status or count. */
|
|
327
|
+
function toolbarTextIsTitle(element) {
|
|
328
|
+
if (attribute(element, 'headingLevel')) return true;
|
|
329
|
+
if (titleToolbarTextSizes.has(textAttribute(element, 'size') ?? ''))
|
|
330
|
+
return true;
|
|
331
|
+
// `title`, `leading`, or `header` slot of a toolbar, pane, or panel: the
|
|
332
|
+
// identity position a toolbar title occupies.
|
|
333
|
+
let parent = element.parent;
|
|
334
|
+
while (
|
|
335
|
+
parent &&
|
|
336
|
+
(ts.isParenthesizedExpression(parent) || ts.isJsxExpression(parent))
|
|
337
|
+
)
|
|
338
|
+
parent = parent.parent;
|
|
339
|
+
if (
|
|
340
|
+
!parent ||
|
|
341
|
+
!(ts.isJsxAttribute(parent) || ts.isPropertyAssignment(parent)) ||
|
|
342
|
+
!ts.isIdentifier(parent.name) ||
|
|
343
|
+
!titleSlots.has(parent.name.text)
|
|
344
|
+
)
|
|
345
|
+
return false;
|
|
346
|
+
// A small `leading` text in a footer or bottom bar is a status line.
|
|
347
|
+
return !(
|
|
348
|
+
parent.name.text === 'leading' &&
|
|
349
|
+
statusToolbarTextSizes.has(textAttribute(element, 'size') ?? '')
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function constBindings(sourceFile) {
|
|
354
|
+
const bindings = new Map();
|
|
355
|
+
const visit = (node) => {
|
|
356
|
+
if (
|
|
357
|
+
ts.isVariableDeclaration(node) &&
|
|
358
|
+
ts.isIdentifier(node.name) &&
|
|
359
|
+
node.initializer &&
|
|
360
|
+
ts.isVariableDeclarationList(node.parent) &&
|
|
361
|
+
node.parent.flags & ts.NodeFlags.Const
|
|
362
|
+
)
|
|
363
|
+
bindings.set(node.name.text, node.initializer);
|
|
364
|
+
ts.forEachChild(node, visit);
|
|
365
|
+
};
|
|
366
|
+
visit(sourceFile);
|
|
367
|
+
return bindings;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function unwrap(node, bindings, seen) {
|
|
371
|
+
let current = node;
|
|
372
|
+
while (current) {
|
|
373
|
+
if (
|
|
374
|
+
ts.isParenthesizedExpression(current) ||
|
|
375
|
+
ts.isAsExpression(current) ||
|
|
376
|
+
ts.isSatisfiesExpression(current) ||
|
|
377
|
+
ts.isJsxExpression(current)
|
|
378
|
+
) {
|
|
379
|
+
current = current.expression;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (
|
|
383
|
+
ts.isIdentifier(current) &&
|
|
384
|
+
bindings.has(current.text) &&
|
|
385
|
+
!seen.has(current.text)
|
|
386
|
+
) {
|
|
387
|
+
seen.add(current.text);
|
|
388
|
+
current = bindings.get(current.text);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
return current;
|
|
392
|
+
}
|
|
393
|
+
return current;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Collect string literals stored under `keys` in an array of object literals, recursively. */
|
|
397
|
+
function entryLiterals(node, keys, bindings, seen = new Set(), found = []) {
|
|
398
|
+
const value = unwrap(node, bindings, seen);
|
|
399
|
+
if (!value) return found;
|
|
400
|
+
if (ts.isArrayLiteralExpression(value)) {
|
|
401
|
+
for (const element of value.elements)
|
|
402
|
+
entryLiterals(
|
|
403
|
+
ts.isSpreadElement(element) ? element.expression : element,
|
|
404
|
+
keys,
|
|
405
|
+
bindings,
|
|
406
|
+
seen,
|
|
407
|
+
found,
|
|
408
|
+
);
|
|
409
|
+
return found;
|
|
410
|
+
}
|
|
411
|
+
if (ts.isObjectLiteralExpression(value))
|
|
412
|
+
for (const property of value.properties) {
|
|
413
|
+
if (!ts.isPropertyAssignment(property)) continue;
|
|
414
|
+
const key =
|
|
415
|
+
ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)
|
|
416
|
+
? property.name.text
|
|
417
|
+
: undefined;
|
|
418
|
+
if (key && keys.includes(key)) {
|
|
419
|
+
const literal = literalOf(property.initializer);
|
|
420
|
+
if (literal) found.push(literal);
|
|
421
|
+
} else if (key && key !== 'attributes')
|
|
422
|
+
entryLiterals(property.initializer, keys, bindings, seen, found);
|
|
423
|
+
}
|
|
424
|
+
return found;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* A panel toolbar's `toggle: { action, name }` (Workbench, CollapsiblePanel,
|
|
429
|
+
* and catalog panels): `name` is the toggle group's accessible name and
|
|
430
|
+
* completes the composed "Show …" / "Hide …" label, so it is a title-style
|
|
431
|
+
* panel name. Requiring `action` keeps unrelated `toggle` objects out.
|
|
432
|
+
*/
|
|
433
|
+
function panelToggleName(node, bindings) {
|
|
434
|
+
const name =
|
|
435
|
+
(ts.isPropertyAssignment(node) || ts.isJsxAttribute(node)) &&
|
|
436
|
+
(ts.isIdentifier(node.name) || ts.isStringLiteral(node.name))
|
|
437
|
+
? node.name.text
|
|
438
|
+
: undefined;
|
|
439
|
+
if (name !== 'toggle') return undefined;
|
|
440
|
+
const value = unwrap(
|
|
441
|
+
ts.isJsxAttribute(node) ? attributeValueNode(node) : node.initializer,
|
|
442
|
+
bindings,
|
|
443
|
+
new Set(),
|
|
444
|
+
);
|
|
445
|
+
if (!value || !ts.isObjectLiteralExpression(value)) return undefined;
|
|
446
|
+
const property = (key) =>
|
|
447
|
+
value.properties.find(
|
|
448
|
+
(item) =>
|
|
449
|
+
ts.isPropertyAssignment(item) &&
|
|
450
|
+
(ts.isIdentifier(item.name) || ts.isStringLiteral(item.name)) &&
|
|
451
|
+
item.name.text === key,
|
|
452
|
+
);
|
|
453
|
+
if (!property('action')) return undefined;
|
|
454
|
+
return literalOf(property('name')?.initializer);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Visible title-style label literals rendered by one JSX/TSX source file. */
|
|
458
|
+
export function titleStyleLabels(fileName, source) {
|
|
459
|
+
const sourceFile = ts.createSourceFile(
|
|
460
|
+
fileName,
|
|
461
|
+
source,
|
|
462
|
+
ts.ScriptTarget.Latest,
|
|
463
|
+
true,
|
|
464
|
+
fileName.endsWith('.jsx') ? ts.ScriptKind.JSX : ts.ScriptKind.TSX,
|
|
465
|
+
);
|
|
466
|
+
const bindings = constBindings(sourceFile);
|
|
467
|
+
const labels = [];
|
|
468
|
+
const add = (literal, owner) => labels.push({ literal, owner });
|
|
469
|
+
const visit = (node) => {
|
|
470
|
+
if (ts.isJsxElement(node)) {
|
|
471
|
+
const name = tagName(node.openingElement);
|
|
472
|
+
if (buttonTags.has(name) || name === 'FieldLabel') {
|
|
473
|
+
const text = node.children
|
|
474
|
+
.filter((child) => ts.isJsxText(child))
|
|
475
|
+
.map((child) => child.text)
|
|
476
|
+
.join(' ')
|
|
477
|
+
.replace(/\s+/g, ' ')
|
|
478
|
+
.trim();
|
|
479
|
+
const onlyText = node.children.every(
|
|
480
|
+
(child) =>
|
|
481
|
+
ts.isJsxText(child) ||
|
|
482
|
+
(ts.isJsxSelfClosingElement(child) &&
|
|
483
|
+
/Icon$/.test(tagName(child))) ||
|
|
484
|
+
(ts.isJsxElement(child) &&
|
|
485
|
+
/Icon$/.test(tagName(child.openingElement))),
|
|
486
|
+
);
|
|
487
|
+
if (text && onlyText)
|
|
488
|
+
labels.push({
|
|
489
|
+
literal: { text, node: node.children.find(ts.isJsxText) },
|
|
490
|
+
owner: name === 'FieldLabel' ? 'FieldLabel' : `<${name}> text`,
|
|
491
|
+
});
|
|
492
|
+
// An icon-only button's accessible name is its label.
|
|
493
|
+
if (!text && onlyText && name !== 'FieldLabel') {
|
|
494
|
+
const literal = literalOf(
|
|
495
|
+
attributeValueNode(attribute(node.openingElement, 'aria-label')),
|
|
496
|
+
);
|
|
497
|
+
if (literal) add(literal, `<${name}> aria-label`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const toggleName = panelToggleName(node, bindings);
|
|
502
|
+
if (toggleName) add(toggleName, 'panel toggle name');
|
|
503
|
+
if (ts.isJsxSelfClosingElement(node) || ts.isJsxOpeningElement(node)) {
|
|
504
|
+
const name = tagName(node);
|
|
505
|
+
if (name === 'ToolbarText' && toolbarTextIsTitle(node)) {
|
|
506
|
+
const literal = literalOf(attributeValueNode(attribute(node, 'text')));
|
|
507
|
+
if (literal) add(literal, 'ToolbarText text');
|
|
508
|
+
}
|
|
509
|
+
if (
|
|
510
|
+
landmarkTags.has(name) ||
|
|
511
|
+
landmarkRoles.has(textAttribute(node, 'role') ?? '')
|
|
512
|
+
) {
|
|
513
|
+
const literal = literalOf(
|
|
514
|
+
attributeValueNode(attribute(node, 'aria-label')),
|
|
515
|
+
);
|
|
516
|
+
if (literal) add(literal, `<${name}> aria-label`);
|
|
517
|
+
}
|
|
518
|
+
if (fieldLabelTags.has(name)) {
|
|
519
|
+
const literal = literalOf(attributeValueNode(attribute(node, 'label')));
|
|
520
|
+
if (literal) add(literal, `<${name}> label`);
|
|
521
|
+
}
|
|
522
|
+
const config = titleStyleProps.get(name);
|
|
523
|
+
if (config) {
|
|
524
|
+
for (const prop of config.props ?? []) {
|
|
525
|
+
const literal = literalOf(attributeValueNode(attribute(node, prop)));
|
|
526
|
+
if (literal) add(literal, `${name} ${prop}`);
|
|
527
|
+
}
|
|
528
|
+
for (const [prop, keys] of Object.entries(config.entries ?? {})) {
|
|
529
|
+
const value = attributeValueNode(attribute(node, prop));
|
|
530
|
+
if (value)
|
|
531
|
+
for (const literal of entryLiterals(value, keys, bindings))
|
|
532
|
+
add(literal, `${name} ${prop}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
ts.forEachChild(node, visit);
|
|
537
|
+
};
|
|
538
|
+
visit(sourceFile);
|
|
539
|
+
return labels.map(({ literal, owner }) => {
|
|
540
|
+
const node = literal.node ?? literal;
|
|
541
|
+
const start = node.getStart(sourceFile);
|
|
542
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(start);
|
|
543
|
+
return {
|
|
544
|
+
text: literal.text,
|
|
545
|
+
...(literal.display === undefined
|
|
546
|
+
? {}
|
|
547
|
+
: { display: literal.display, restore: literal.restore }),
|
|
548
|
+
owner,
|
|
549
|
+
line: line + 1,
|
|
550
|
+
column: character + 1,
|
|
551
|
+
};
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const testModule =
|
|
556
|
+
/(?:^|\/)(?:__tests__\/|[^/]*\.(?:test|spec)\.[cm]?[jt]sx?$)/;
|
|
557
|
+
|
|
558
|
+
export async function checkCopyCapitalization(
|
|
559
|
+
packageRoot,
|
|
560
|
+
inputs,
|
|
561
|
+
selectedPaths,
|
|
562
|
+
) {
|
|
563
|
+
const diagnostics = [];
|
|
564
|
+
for (const file of inputs) {
|
|
565
|
+
if (!scriptExtensions.has(extname(file))) continue;
|
|
566
|
+
if (isUiTraversalExcluded(packageRoot, file)) continue;
|
|
567
|
+
const path = relative(packageRoot, file).replaceAll('\\', '/');
|
|
568
|
+
if (testModule.test(path)) continue;
|
|
569
|
+
if (selectedPaths?.length && !selectedPaths.includes(path)) continue;
|
|
570
|
+
const source = await readFile(file, 'utf8');
|
|
571
|
+
for (const label of titleStyleLabels(file, source)) {
|
|
572
|
+
const violation = titleStyleViolation(label.text);
|
|
573
|
+
if (!violation) continue;
|
|
574
|
+
const shown = label.display ?? label.text;
|
|
575
|
+
const suggestion = label.restore
|
|
576
|
+
? label.restore(violation.suggestion)
|
|
577
|
+
: violation.suggestion;
|
|
578
|
+
diagnostics.push({
|
|
579
|
+
id: 'KUI-D040',
|
|
580
|
+
severity: 'warning',
|
|
581
|
+
stage: 'analyzer',
|
|
582
|
+
message: `${label.owner} "${shown}" is not title-style capitalized (${violation.words.map((word) => `"${word}"`).join(', ')}).`,
|
|
583
|
+
location: { file: path, line: label.line, column: label.column },
|
|
584
|
+
action: `Use "${suggestion}": capitalize every word except articles, coordinating conjunctions, and prepositions of four letters or fewer, and always capitalize the first and last word. A complete sentence ends with punctuation and stays sentence style. Suppress KUI-D040 for a lowercase brand or product name.`,
|
|
585
|
+
documentation: '@kerfjs/ui/docs/writing.md#capitalization',
|
|
586
|
+
meaning: 'Visible label is not title-style capitalized',
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return diagnostics;
|
|
591
|
+
}
|
package/doctor/index.mjs
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from '../analyzer/index.mjs';
|
|
29
29
|
import { evaluateUi, UI_EVALUATION_RULES } from '../evaluator/index.mjs';
|
|
30
30
|
import { checkComponentStyles } from './component-style-checks.mjs';
|
|
31
|
+
import { checkCopyCapitalization } from './copy-capitalization-checks.mjs';
|
|
31
32
|
import {
|
|
32
33
|
isForeignRuleDefinitionDiagnostic,
|
|
33
34
|
projectConsumerCoreConfig,
|
|
@@ -66,6 +67,10 @@ export const UI_DOCTOR_RULES = Object.freeze({
|
|
|
66
67
|
severity: 'error',
|
|
67
68
|
title: 'Stylesheet selects another component class',
|
|
68
69
|
},
|
|
70
|
+
'KUI-D040': {
|
|
71
|
+
severity: 'warning',
|
|
72
|
+
title: 'Visible label is not title-style capitalized',
|
|
73
|
+
},
|
|
69
74
|
});
|
|
70
75
|
|
|
71
76
|
const sourceExtensions = new Set([
|
|
@@ -534,14 +539,13 @@ async function cacheKey(root, packageRoot, mode, paths, config) {
|
|
|
534
539
|
}),
|
|
535
540
|
)
|
|
536
541
|
.update(await readFile(fileURLToPath(import.meta.url)));
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
),
|
|
543
|
-
)
|
|
544
|
-
);
|
|
542
|
+
for (const module of [
|
|
543
|
+
'component-style-checks.mjs',
|
|
544
|
+
'copy-capitalization-checks.mjs',
|
|
545
|
+
])
|
|
546
|
+
hash.update(
|
|
547
|
+
await readFile(resolve(dirname(fileURLToPath(import.meta.url)), module)),
|
|
548
|
+
);
|
|
545
549
|
for (const file of files)
|
|
546
550
|
hash.update(portablePath(root, file)).update(await readFile(file));
|
|
547
551
|
return hash.digest('hex');
|
|
@@ -1265,6 +1269,11 @@ export async function runUiDoctor({
|
|
|
1265
1269
|
selectedPaths,
|
|
1266
1270
|
root,
|
|
1267
1271
|
)),
|
|
1272
|
+
...(await checkCopyCapitalization(
|
|
1273
|
+
packageRoot,
|
|
1274
|
+
await collectInputs(packageRoot),
|
|
1275
|
+
selectedPaths,
|
|
1276
|
+
)),
|
|
1268
1277
|
],
|
|
1269
1278
|
}))(),
|
|
1270
1279
|
signal,
|