@kerfjs/ui 5.0.0-beta.4 → 5.0.0-beta.41
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 +342 -108
- package/ai/application-ui-diagnostic-ids-v1.json +100 -0
- package/ai/application-ui-profile-sync.cjs +529 -0
- package/ai/application-ui-profile.d.mts +1 -0
- package/ai/application-ui-profile.d.ts +102 -0
- package/ai/application-ui-profile.defaults.json +41 -0
- package/ai/application-ui-profile.mjs +626 -0
- package/ai/application-ui-profile.schema.json +130 -0
- package/ai/catalog-authoring.json +25 -0
- package/ai/catalog-authoring.schema.json +61 -0
- package/ai/compile-time-contracts-v1.json +132 -0
- package/ai/compile-time-contracts-v1.schema.json +51 -0
- package/ai/component-catalog-extension-v2.d.ts +7 -0
- package/ai/component-catalog-extension-v2.schema.json +7 -0
- package/ai/component-catalog-extension.schema.json +79 -0
- package/ai/component-catalog-v2-overrides.json +496 -0
- package/ai/component-catalog-v2.d.ts +90 -0
- package/ai/component-catalog-v2.json +5678 -0
- package/ai/component-catalog-v2.schema.json +298 -0
- package/ai/component-catalog.json +2694 -417
- package/ai/component-catalog.schema.json +39 -0
- package/ai/public-api-signatures-v1.md +1591 -126
- package/ai/skill.md +140 -54
- package/ai/webawesome-jsx-signatures-v1.md +2 -1
- package/analyzer/cli.mjs +49 -0
- package/analyzer/index.d.mts +46 -0
- package/analyzer/index.mjs +1086 -0
- package/analyzer/report.schema.json +66 -0
- package/dist/app-tab.d.ts +15 -4
- package/dist/app-tab.js +3 -1
- package/dist/browser/app-tab.js +1 -0
- package/dist/browser/floating-toolbar.js +3 -0
- package/dist/browser/list-action-row.js +5 -0
- package/dist/browser/list-header.js +7 -0
- package/dist/browser/list-inset-control.js +3 -0
- package/dist/browser/list-inset-text.js +3 -0
- package/dist/browser/list-item.js +5 -0
- package/dist/browser/list.js +3 -0
- package/dist/browser/pane.js +3 -0
- package/dist/browser/row.js +3 -0
- package/dist/browser/segmented-control.js +1 -0
- package/dist/browser/select.js +1 -0
- package/dist/browser/skeleton.js +3 -0
- package/dist/browser/spacer.js +3 -0
- package/dist/browser/state-banner.js +1 -0
- package/dist/browser/sunken-panel.js +3 -0
- package/dist/browser/surface-scaffold.js +3 -0
- package/dist/browser/text.js +3 -0
- package/dist/browser/toolbar-text.js +1 -0
- package/dist/browser/value-table.js +1 -0
- package/dist/catalog-resources.d.ts +35 -0
- package/dist/catalog-resources.js +22 -0
- package/dist/catalog-resources.js.map +1 -0
- package/dist/catalog.d.ts +151 -0
- package/dist/catalog.js +396 -0
- package/dist/catalog.js.map +1 -0
- package/dist/chunk-2TYUFZUC.js +22 -0
- package/dist/chunk-2TYUFZUC.js.map +1 -0
- package/dist/chunk-6NDBE2OK.js +32 -0
- package/dist/chunk-6NDBE2OK.js.map +1 -0
- package/dist/chunk-6WTETUZ6.js +21 -0
- package/dist/chunk-6WTETUZ6.js.map +1 -0
- package/dist/chunk-AQFWBG23.js +83 -0
- package/dist/chunk-AQFWBG23.js.map +1 -0
- package/dist/chunk-B6JYRX3P.js +51 -0
- package/dist/chunk-B6JYRX3P.js.map +1 -0
- package/dist/chunk-C34NZOLJ.js +53 -0
- package/dist/chunk-C34NZOLJ.js.map +1 -0
- package/dist/chunk-CAB6NQ2T.js +102 -0
- package/dist/chunk-CAB6NQ2T.js.map +1 -0
- package/dist/chunk-CK6P5AUK.js +136 -0
- package/dist/chunk-CK6P5AUK.js.map +1 -0
- package/dist/chunk-DAO2VFTT.js +129 -0
- package/dist/chunk-DAO2VFTT.js.map +1 -0
- package/dist/chunk-DEYOGDE7.js +141 -0
- package/dist/chunk-DEYOGDE7.js.map +1 -0
- package/dist/chunk-DYBPI5O2.js +25 -0
- package/dist/chunk-DYBPI5O2.js.map +1 -0
- package/dist/{chunk-TRNBHPNW.js → chunk-E34TE2ZI.js} +12 -8
- package/dist/chunk-E34TE2ZI.js.map +1 -0
- package/dist/chunk-EYALJQQD.js +25 -0
- package/dist/chunk-EYALJQQD.js.map +1 -0
- package/dist/chunk-I45QO2IZ.js +88 -0
- package/dist/chunk-I45QO2IZ.js.map +1 -0
- package/dist/chunk-I4V3UDSA.js +79 -0
- package/dist/chunk-I4V3UDSA.js.map +1 -0
- package/dist/chunk-OGTJNHRG.js +42 -0
- package/dist/chunk-OGTJNHRG.js.map +1 -0
- package/dist/chunk-OL4TC3U2.js +156 -0
- package/dist/chunk-OL4TC3U2.js.map +1 -0
- package/dist/chunk-OMSRSL3B.js +120 -0
- package/dist/chunk-OMSRSL3B.js.map +1 -0
- package/dist/chunk-R3EBGEAW.js +46 -0
- package/dist/chunk-R3EBGEAW.js.map +1 -0
- package/dist/chunk-SOK5EUF7.js +25 -0
- package/dist/chunk-SOK5EUF7.js.map +1 -0
- package/dist/{chunk-7BR3DO2A.js → chunk-SRSJO5QE.js} +13 -4
- package/dist/chunk-SRSJO5QE.js.map +1 -0
- package/dist/chunk-T7DOWS6T.js +61 -0
- package/dist/chunk-T7DOWS6T.js.map +1 -0
- package/dist/chunk-TIGSG6BX.js +218 -0
- package/dist/chunk-TIGSG6BX.js.map +1 -0
- package/dist/chunk-UDHHKZWO.js +41 -0
- package/dist/chunk-UDHHKZWO.js.map +1 -0
- package/dist/{chunk-MQIZAD4A.js → chunk-UKKSMZYT.js} +76 -22
- package/dist/chunk-UKKSMZYT.js.map +1 -0
- package/dist/chunk-V6GQKOX2.js +39 -0
- package/dist/chunk-V6GQKOX2.js.map +1 -0
- package/dist/chunk-V6HCTCVD.js +46 -0
- package/dist/chunk-V6HCTCVD.js.map +1 -0
- package/dist/chunk-VDUJZF5G.js +515 -0
- package/dist/chunk-VDUJZF5G.js.map +1 -0
- package/dist/chunk-VXFGMDA7.js +31 -0
- package/dist/chunk-VXFGMDA7.js.map +1 -0
- package/dist/chunk-WN52USHV.js +115 -0
- package/dist/chunk-WN52USHV.js.map +1 -0
- package/dist/chunk-WSXUPSID.js +43 -0
- package/dist/chunk-WSXUPSID.js.map +1 -0
- package/dist/chunk-X5GRDQ7V.js +50 -0
- package/dist/chunk-X5GRDQ7V.js.map +1 -0
- package/dist/{chunk-56RJXPHG.js → chunk-Y6LYKFXY.js} +1 -1
- package/dist/{chunk-56RJXPHG.js.map → chunk-Y6LYKFXY.js.map} +1 -1
- package/dist/chunk-ZASJJMGV.js +17 -0
- package/dist/chunk-ZASJJMGV.js.map +1 -0
- package/dist/chunk-ZPWZIVI6.js +47 -0
- package/dist/chunk-ZPWZIVI6.js.map +1 -0
- package/dist/collapsible-panel.d.ts +77 -0
- package/dist/collapsible-panel.js +84 -0
- package/dist/collapsible-panel.js.map +1 -0
- package/dist/css-values.d.ts +75 -0
- package/dist/css-values.js +3 -0
- package/dist/css-values.js.map +1 -0
- package/dist/device-class.d.ts +62 -0
- package/dist/device-class.js +103 -0
- package/dist/device-class.js.map +1 -0
- package/dist/disclosure-arrow.js +1 -1
- package/dist/divider-sides-BzB6rphT.d.ts +6 -0
- package/dist/empty-state.d.ts +3 -2
- package/dist/empty-state.js +1 -1
- package/dist/flex-alignment-CF8NLj7i.d.ts +4 -0
- package/dist/floating-toolbar.d.ts +29 -0
- package/dist/floating-toolbar.js +3 -0
- package/dist/floating-toolbar.js.map +1 -0
- package/dist/index.d.ts +28 -14
- package/dist/index.js +34 -23
- package/dist/{menu-action-row.d.ts → list-action-row.d.ts} +19 -8
- package/dist/list-action-row.js +7 -0
- package/dist/list-action-row.js.map +1 -0
- package/dist/list-header.d.ts +80 -0
- package/dist/list-header.js +9 -0
- package/dist/list-header.js.map +1 -0
- package/dist/list-inset-control.d.ts +20 -0
- package/dist/list-inset-control.js +3 -0
- package/dist/list-inset-control.js.map +1 -0
- package/dist/list-inset-text.d.ts +29 -0
- package/dist/list-inset-text.js +3 -0
- package/dist/list-inset-text.js.map +1 -0
- package/dist/list-item.d.ts +45 -0
- package/dist/list-item.js +7 -0
- package/dist/list-item.js.map +1 -0
- package/dist/list.d.ts +30 -0
- package/dist/list.js +5 -0
- package/dist/list.js.map +1 -0
- package/dist/loading-spinner.js +1 -1
- package/dist/nav-stack.d.ts +41 -0
- package/dist/nav-stack.js +6 -0
- package/dist/nav-stack.js.map +1 -0
- package/dist/pane.d.ts +50 -0
- package/dist/pane.js +4 -0
- package/dist/pane.js.map +1 -0
- package/dist/resizable-region.d.ts +21 -3
- package/dist/resizable-region.js +1 -1
- package/dist/row.d.ts +28 -0
- package/dist/row.js +5 -0
- package/dist/row.js.map +1 -0
- package/dist/segmented-control.d.ts +11 -8
- package/dist/segmented-control.js +3 -1
- package/dist/select-register.d.ts +5 -1
- package/dist/select-register.js +129 -1
- package/dist/select-register.js.map +1 -1
- package/dist/select.d.ts +34 -8
- package/dist/select.js +3 -1
- package/dist/semantic-content-BbzjvSu9.d.ts +14 -0
- package/dist/skeleton.d.ts +25 -0
- package/dist/skeleton.js +4 -0
- package/dist/skeleton.js.map +1 -0
- package/dist/spacer.d.ts +16 -0
- package/dist/spacer.js +4 -0
- package/dist/spacer.js.map +1 -0
- package/dist/split-view.d.ts +43 -0
- package/dist/split-view.js +82 -0
- package/dist/split-view.js.map +1 -0
- package/dist/state-banner.d.ts +11 -5
- package/dist/state-banner.js +3 -1
- package/dist/styles/app-tab.css +26 -0
- package/dist/styles/catalog.css +582 -0
- package/dist/styles/collapsible-panel.css +212 -0
- package/dist/styles/floating-toolbar.css +38 -0
- package/dist/styles/foundation.css +80 -0
- package/dist/styles/layout.css +2 -20
- package/dist/styles/list-action-row.css +224 -0
- package/dist/styles/list-header.css +224 -0
- package/dist/styles/list-inset-control.css +23 -0
- package/dist/styles/list-inset-text.css +30 -0
- package/dist/styles/list-item.css +171 -0
- package/dist/styles/list.css +118 -0
- package/dist/styles/nav-stack.css +141 -0
- package/dist/styles/pane.css +62 -0
- package/dist/styles/resizable-region.css +158 -0
- package/dist/styles/row.css +92 -0
- package/dist/styles/segmented-control.css +10 -7
- package/dist/styles/select.css +115 -2
- package/dist/styles/skeleton.css +31 -0
- package/dist/styles/spacer.css +11 -0
- package/dist/styles/split-view.css +35 -0
- package/dist/styles/state-banner.css +49 -10
- package/dist/styles/styles.css +10 -5
- package/dist/styles/sunken-panel.css +22 -0
- package/dist/styles/surface-scaffold.css +51 -0
- package/dist/styles/tab-bar.css +24 -0
- package/dist/styles/tab-scaffold.css +85 -0
- package/dist/styles/text.css +6 -0
- package/dist/styles/token-search-field.css +14 -2
- package/dist/styles/toolbar-control-group.css +235 -8
- package/dist/styles/toolbar-text.css +65 -2
- package/dist/styles/toolbar.css +47 -2
- package/dist/styles/webawesome.css +171 -0
- package/dist/styles/workbench.css +187 -0
- package/dist/sunken-panel.d.ts +20 -0
- package/dist/sunken-panel.js +3 -0
- package/dist/sunken-panel.js.map +1 -0
- package/dist/surface-scaffold.d.ts +26 -0
- package/dist/surface-scaffold.js +3 -0
- package/dist/surface-scaffold.js.map +1 -0
- package/dist/tab-bar.d.ts +24 -6
- package/dist/tab-bar.js +1 -1
- package/dist/tab-scaffold.d.ts +31 -0
- package/dist/tab-scaffold.js +51 -0
- package/dist/tab-scaffold.js.map +1 -0
- package/dist/text.d.ts +20 -0
- package/dist/text.js +3 -0
- package/dist/text.js.map +1 -0
- package/dist/token-search-field.d.ts +30 -13
- package/dist/token-search-field.js +1 -1
- package/dist/toolbar-control-group.d.ts +33 -7
- package/dist/toolbar-control-group.js +1 -1
- package/dist/toolbar-text.d.ts +31 -4
- package/dist/toolbar-text.js +3 -1
- package/dist/toolbar.d.ts +10 -7
- package/dist/toolbar.js +1 -1
- package/dist/value-table.d.ts +7 -3
- package/dist/value-table.js +3 -1
- package/dist/webawesome.d.ts +1 -0
- package/dist/wire-catalog.d.ts +57 -0
- package/dist/wire-catalog.js +302 -0
- package/dist/wire-catalog.js.map +1 -0
- package/dist/wire-nav-stack.d.ts +18 -0
- package/dist/wire-nav-stack.js +228 -0
- package/dist/wire-nav-stack.js.map +1 -0
- package/dist/wire-resizable-regions.d.ts +1 -1
- package/dist/wire-resizable-regions.js +1 -1
- package/dist/wire-sidebar.d.ts +48 -0
- package/dist/wire-sidebar.js +147 -0
- package/dist/wire-sidebar.js.map +1 -0
- package/dist/wire-tab-bars.d.ts +19 -2
- package/dist/wire-tab-bars.js +1 -1
- package/dist/wire-tab-scaffold.d.ts +11 -0
- package/dist/wire-tab-scaffold.js +21 -0
- package/dist/wire-tab-scaffold.js.map +1 -0
- package/dist/wire-token-search-fields.d.ts +104 -4
- package/dist/wire-token-search-fields.js +1 -1
- package/dist/workbench.d.ts +44 -0
- package/dist/workbench.js +106 -0
- package/dist/workbench.js.map +1 -0
- package/docs/accessibility.md +155 -20
- package/docs/app-layouts.md +61 -0
- package/docs/catalog.md +364 -0
- package/docs/collapsible-panel.md +142 -0
- package/docs/component-contract.md +258 -28
- package/docs/component-integration.md +63 -0
- package/docs/component-selection.md +219 -57
- 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 +1 -0
- package/docs/design/templates/empty-state.svg +1 -0
- package/docs/design/templates/list/compact-dark.svg +1 -0
- package/docs/design/templates/list/compact.svg +1 -0
- package/docs/design/templates/list/stack-dark.svg +1 -0
- package/docs/design/templates/list/stack.svg +1 -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 +1 -0
- package/docs/design/templates/list-action-row.svg +1 -0
- package/docs/design/templates/list-dark.svg +1 -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/inline-action-dark.svg +1 -0
- package/docs/design/templates/list-header/inline-action.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 +1 -0
- package/docs/design/templates/list-header.svg +1 -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 +1 -0
- package/docs/design/templates/list-item.svg +1 -0
- package/docs/design/templates/list.svg +1 -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 +1 -0
- package/docs/design/templates/segmented-control.svg +1 -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 +1 -0
- package/docs/design/templates/skeleton.svg +1 -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-badge-dark.svg +1 -0
- package/docs/design/templates/state-banner/info-badge.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 +1 -0
- package/docs/design/templates/state-banner.svg +1 -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 +1 -0
- package/docs/design/templates/tab-bar.svg +1 -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 +1 -0
- package/docs/design/templates/token-search-field.svg +1 -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 +1 -0
- package/docs/design/templates/toolbar-control-group.svg +1 -0
- package/docs/design/templates/toolbar-dark.svg +1 -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 +1 -0
- package/docs/design/templates/toolbar-text.svg +1 -0
- package/docs/design/templates/toolbar.svg +1 -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 +1 -0
- package/docs/design/templates/value-table.svg +1 -0
- package/docs/design/templates.md +128 -0
- package/docs/design-philosophy.md +33 -11
- package/docs/device-class.md +54 -0
- package/docs/examples/application-ui-profile.json +60 -0
- package/docs/examples/command-palette-adapter.tsx +68 -17
- package/docs/examples/component-catalog-extension-v2.json +80 -0
- package/docs/examples/component-catalog-extension.json +45 -0
- package/docs/layout.md +169 -38
- package/docs/nav-stack.md +77 -0
- package/docs/recipes.md +64 -16
- package/docs/split-view.md +55 -0
- package/docs/sunken-panel.md +53 -0
- package/docs/surface-scaffold.md +25 -0
- package/docs/tab-scaffold.md +52 -0
- package/docs/type-contracts.md +127 -0
- package/docs/ui-analyzer.md +118 -0
- package/docs/ui-doctor.md +91 -0
- package/docs/ui-evaluator.md +122 -0
- package/docs/ux-demo.md +29 -10
- package/docs/webawesome-theme.md +82 -8
- package/docs/workbench.md +123 -0
- package/doctor/cli.mjs +115 -0
- package/doctor/config.schema.json +68 -0
- package/doctor/eslint-diagnostics.d.mts +24 -0
- package/doctor/eslint-diagnostics.mjs +24 -0
- package/doctor/index.d.mts +116 -0
- package/doctor/index.mjs +1272 -0
- package/doctor/report.schema.json +118 -0
- package/evaluator/cli.mjs +89 -0
- package/evaluator/index.d.mts +90 -0
- package/evaluator/index.mjs +1138 -0
- package/evaluator/report.schema.json +152 -0
- package/llms.txt +260 -32
- package/package.json +205 -38
- package/traversal-exclusions.mjs +27 -0
- package/ux-demo/recipes/app-shell.tsx +223 -13
- package/ux-demo/recipes/collapsible-sidebar.tsx +230 -0
- package/ux-demo/recipes/compact-toolbar.tsx +116 -12
- package/ux-demo/recipes/composer-form.tsx +120 -19
- package/ux-demo/recipes/list-detail-dialog.tsx +205 -0
- package/ux-demo/recipes/list-workspace-states.tsx +176 -9
- package/ux-demo/recipes/loaders.ts +13 -4
- package/ux-demo/recipes/loading-inspector.tsx +191 -0
- package/ux-demo/recipes/mount-recipe.ts +31 -5
- package/ux-demo/recipes/navigation-sidebar.tsx +123 -11
- package/ux-demo/recipes/navigation-stack.tsx +121 -0
- package/ux-demo/recipes/recipes.css +125 -16
- package/ux-demo/recipes/types.ts +9 -1
- package/ux-demo/recipes/workspace-header.tsx +72 -3
- package/dist/browser/dialog-header.js +0 -5
- package/dist/browser/menu-action-row.js +0 -3
- package/dist/browser/menu-header.js +0 -5
- 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-55DATV3A.js +0 -10
- package/dist/chunk-55DATV3A.js.map +0 -1
- package/dist/chunk-7BR3DO2A.js.map +0 -1
- package/dist/chunk-7LDCTU7D.js +0 -134
- package/dist/chunk-7LDCTU7D.js.map +0 -1
- package/dist/chunk-AQNKJRFH.js +0 -79
- package/dist/chunk-AQNKJRFH.js.map +0 -1
- package/dist/chunk-CTQKI2XL.js +0 -28
- package/dist/chunk-CTQKI2XL.js.map +0 -1
- package/dist/chunk-DAFFDO33.js +0 -14
- package/dist/chunk-DAFFDO33.js.map +0 -1
- package/dist/chunk-EGHL5S4P.js +0 -33
- package/dist/chunk-EGHL5S4P.js.map +0 -1
- package/dist/chunk-GY5WH7TO.js +0 -10
- package/dist/chunk-GY5WH7TO.js.map +0 -1
- package/dist/chunk-H5AGGVU5.js +0 -75
- package/dist/chunk-H5AGGVU5.js.map +0 -1
- package/dist/chunk-K3G72I6D.js +0 -24
- package/dist/chunk-K3G72I6D.js.map +0 -1
- package/dist/chunk-M5JRI6MN.js +0 -27
- package/dist/chunk-M5JRI6MN.js.map +0 -1
- package/dist/chunk-MBR3TYNL.js +0 -14
- package/dist/chunk-MBR3TYNL.js.map +0 -1
- package/dist/chunk-MFKMHK5W.js +0 -10
- package/dist/chunk-MFKMHK5W.js.map +0 -1
- package/dist/chunk-MQIZAD4A.js.map +0 -1
- package/dist/chunk-NPPD2A6O.js +0 -40
- package/dist/chunk-NPPD2A6O.js.map +0 -1
- package/dist/chunk-R6JHHBKQ.js +0 -19
- package/dist/chunk-R6JHHBKQ.js.map +0 -1
- package/dist/chunk-S3TPM2LP.js +0 -23
- package/dist/chunk-S3TPM2LP.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-WVPYVDXY.js +0 -15
- package/dist/chunk-WVPYVDXY.js.map +0 -1
- package/dist/chunk-XZXRLY5Q.js +0 -33
- package/dist/chunk-XZXRLY5Q.js.map +0 -1
- package/dist/dialog-header.d.ts +0 -15
- package/dist/dialog-header.js +0 -5
- package/dist/dialog-header.js.map +0 -1
- package/dist/menu-action-row.js +0 -4
- package/dist/menu-action-row.js.map +0 -1
- package/dist/menu-header.d.ts +0 -41
- package/dist/menu-header.js +0 -6
- package/dist/menu-header.js.map +0 -1
- package/dist/menu-item.d.ts +0 -31
- package/dist/menu-item.js +0 -4
- 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/dist/styles/dialog-header.css +0 -87
- package/dist/styles/menu-action-row.css +0 -160
- package/dist/styles/menu-header.css +0 -143
- package/dist/styles/menu-item.css +0 -123
- package/dist/styles/page-header.css +0 -33
- package/ux-demo/recipes/master-detail-dialog.tsx +0 -45
|
@@ -0,0 +1,1086 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import {
|
|
3
|
+
basename,
|
|
4
|
+
dirname,
|
|
5
|
+
extname,
|
|
6
|
+
isAbsolute,
|
|
7
|
+
relative,
|
|
8
|
+
resolve,
|
|
9
|
+
} from 'node:path';
|
|
10
|
+
import process from 'node:process';
|
|
11
|
+
|
|
12
|
+
import postcss from 'postcss';
|
|
13
|
+
import ts from 'typescript';
|
|
14
|
+
|
|
15
|
+
import { loadApplicationUiProfile } from '../ai/application-ui-profile.mjs';
|
|
16
|
+
import { isUiTraversalExcluded } from '../traversal-exclusions.mjs';
|
|
17
|
+
|
|
18
|
+
export const UI_ANALYSIS_SCHEMA_VERSION = 1;
|
|
19
|
+
|
|
20
|
+
export const UI_ANALYSIS_RULES = Object.freeze({
|
|
21
|
+
'KUI-L001': { severity: 'error', title: 'Private or unknown Kerf selector' },
|
|
22
|
+
'KUI-L002': { severity: 'error', title: 'Unknown Kerf token' },
|
|
23
|
+
'KUI-L003': { severity: 'error', title: 'Competing geometry owners' },
|
|
24
|
+
'KUI-L004': { severity: 'review', title: 'Repeated content inset' },
|
|
25
|
+
'KUI-L005': { severity: 'review', title: 'Forced component dimension' },
|
|
26
|
+
'KUI-L006': {
|
|
27
|
+
severity: 'review',
|
|
28
|
+
title: 'Hard-coded spacing outside the approved scale',
|
|
29
|
+
},
|
|
30
|
+
'KUI-L007': { severity: 'error', title: 'Nested scroll owners' },
|
|
31
|
+
'KUI-L008': { severity: 'review', title: 'Dynamic class requires review' },
|
|
32
|
+
'KUI-L009': { severity: 'error', title: 'Stylesheet could not be parsed' },
|
|
33
|
+
'KUI-L010': {
|
|
34
|
+
severity: 'error',
|
|
35
|
+
title: 'Private Kerf descendant override',
|
|
36
|
+
},
|
|
37
|
+
'KUI-L011': { severity: 'error', title: 'Uncataloged shadow part override' },
|
|
38
|
+
'KUI-L012': { severity: 'error', title: 'Private Kerf token assignment' },
|
|
39
|
+
'KUI-L013': { severity: 'error', title: 'Uncataloged CSS value literal' },
|
|
40
|
+
'KUI-L014': { severity: 'error', title: 'Wrong CSS value helper dimension' },
|
|
41
|
+
'KUI-L015': { severity: 'error', title: 'Non-standalone CSS expression' },
|
|
42
|
+
'KUI-L016': { severity: 'error', title: 'Forbidden declaration-list escape' },
|
|
43
|
+
'KUI-L017': { severity: 'review', title: 'Exceptional spacing shorthand' },
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const adoptionRules = new Set([
|
|
47
|
+
'KUI-L001',
|
|
48
|
+
'KUI-L002',
|
|
49
|
+
'KUI-L010',
|
|
50
|
+
'KUI-L011',
|
|
51
|
+
'KUI-L012',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
const sourceExtensions = new Set(['.js', '.jsx', '.mjs', '.ts', '.tsx']);
|
|
55
|
+
const spacingProperties = /^(?:margin|padding|gap|inset)(?:-|$)/;
|
|
56
|
+
const dimensionProperties =
|
|
57
|
+
/^(?:width|height|min-width|max-width|min-height|max-height)$/;
|
|
58
|
+
const approvedSpacing = new Set([0, 4, 8, 16, 24]);
|
|
59
|
+
async function collectFiles(root, paths) {
|
|
60
|
+
const files = [];
|
|
61
|
+
const visit = async (path) => {
|
|
62
|
+
if (isUiTraversalExcluded(root, path)) return;
|
|
63
|
+
const details = await stat(path);
|
|
64
|
+
if (details.isFile()) {
|
|
65
|
+
if (sourceExtensions.has(extname(path)) || path.endsWith('.css'))
|
|
66
|
+
files.push(path);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
const child = resolve(path, entry.name);
|
|
72
|
+
if (entry.isDirectory()) await visit(child);
|
|
73
|
+
else if (
|
|
74
|
+
sourceExtensions.has(extname(entry.name)) ||
|
|
75
|
+
entry.name.endsWith('.css')
|
|
76
|
+
)
|
|
77
|
+
files.push(child);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
for (const path of paths?.length ? paths : [root])
|
|
81
|
+
await visit(resolve(root, path));
|
|
82
|
+
return files.sort();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function location(file, node, source) {
|
|
86
|
+
if (!source) {
|
|
87
|
+
const point = node.source?.start;
|
|
88
|
+
return {
|
|
89
|
+
file,
|
|
90
|
+
line: point?.line ?? 1,
|
|
91
|
+
column: point?.column ?? point?.offset ?? 1,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const point = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
95
|
+
return {
|
|
96
|
+
file,
|
|
97
|
+
line: point.line + 1,
|
|
98
|
+
column: point.character + 1,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function diagnostic(ruleId, at, message, evidence, chain, adoption = false) {
|
|
103
|
+
const rule = UI_ANALYSIS_RULES[ruleId];
|
|
104
|
+
return {
|
|
105
|
+
ruleId,
|
|
106
|
+
severity: adoption && adoptionRules.has(ruleId) ? 'review' : rule.severity,
|
|
107
|
+
message,
|
|
108
|
+
location: at,
|
|
109
|
+
evidence,
|
|
110
|
+
chain,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isSuppressed(item, profile, root) {
|
|
115
|
+
const target = relative(root, item.location.file).replaceAll('\\', '/');
|
|
116
|
+
return (profile?.exceptions ?? []).some(
|
|
117
|
+
(exception) =>
|
|
118
|
+
exception.rules?.includes(item.ruleId) && exception.target === target,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function relativeStyleImports(file, source) {
|
|
123
|
+
const imports = [];
|
|
124
|
+
if (file.endsWith('.css')) {
|
|
125
|
+
let stylesheet;
|
|
126
|
+
try {
|
|
127
|
+
stylesheet = postcss.parse(source, { from: file });
|
|
128
|
+
} catch {
|
|
129
|
+
return imports;
|
|
130
|
+
}
|
|
131
|
+
stylesheet.walkAtRules('import', (rule) => {
|
|
132
|
+
const match = rule.params.match(
|
|
133
|
+
/^\s*(?:["']([^"']+)["']|url\(\s*(?:["']([^"']+)["']|([^\s)]+))\s*\))/,
|
|
134
|
+
);
|
|
135
|
+
const specifier = match?.[1] ?? match?.[2] ?? match?.[3];
|
|
136
|
+
if (specifier?.startsWith('.'))
|
|
137
|
+
imports.push(resolve(dirname(file), specifier));
|
|
138
|
+
});
|
|
139
|
+
return imports;
|
|
140
|
+
}
|
|
141
|
+
const syntax = file.endsWith('.tsx')
|
|
142
|
+
? ts.ScriptKind.TSX
|
|
143
|
+
: file.endsWith('.jsx')
|
|
144
|
+
? ts.ScriptKind.JSX
|
|
145
|
+
: file.endsWith('.ts')
|
|
146
|
+
? ts.ScriptKind.TS
|
|
147
|
+
: ts.ScriptKind.JS;
|
|
148
|
+
const module = ts.createSourceFile(
|
|
149
|
+
file,
|
|
150
|
+
source,
|
|
151
|
+
ts.ScriptTarget.Latest,
|
|
152
|
+
true,
|
|
153
|
+
syntax,
|
|
154
|
+
);
|
|
155
|
+
for (const statement of module.statements) {
|
|
156
|
+
if (
|
|
157
|
+
!ts.isImportDeclaration(statement) ||
|
|
158
|
+
!ts.isStringLiteral(statement.moduleSpecifier)
|
|
159
|
+
)
|
|
160
|
+
continue;
|
|
161
|
+
const specifier = statement.moduleSpecifier.text;
|
|
162
|
+
if (specifier.startsWith('.') && specifier.endsWith('.css'))
|
|
163
|
+
imports.push(resolve(dirname(file), specifier));
|
|
164
|
+
}
|
|
165
|
+
return imports;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function reachableStyleFacts(file, imports, styleFacts) {
|
|
169
|
+
const result = new Map();
|
|
170
|
+
for (const style of reachableStyleFiles(file, imports, styleFacts)) {
|
|
171
|
+
for (const [className, facts] of styleFacts.get(style)) {
|
|
172
|
+
const current = result.get(className) ?? { scroll: false, inset: false };
|
|
173
|
+
current.scroll ||= facts.scroll;
|
|
174
|
+
current.inset ||= facts.inset;
|
|
175
|
+
result.set(className, current);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function reachableStyleFiles(file, imports, styleFacts) {
|
|
182
|
+
const visited = new Set();
|
|
183
|
+
const visit = (style) => {
|
|
184
|
+
if (visited.has(style) || !styleFacts.has(style)) return;
|
|
185
|
+
visited.add(style);
|
|
186
|
+
for (const dependency of imports.get(style) ?? []) visit(dependency);
|
|
187
|
+
};
|
|
188
|
+
for (const style of imports.get(file) ?? []) visit(style);
|
|
189
|
+
return visited;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function loadCatalogs(profileResult) {
|
|
193
|
+
const entries = [];
|
|
194
|
+
for (const catalog of profileResult.profile?.catalogs ?? []) {
|
|
195
|
+
const owner = profileResult.provenance[`$catalogs.${catalog.package}`];
|
|
196
|
+
if (!owner) continue;
|
|
197
|
+
try {
|
|
198
|
+
const artifact = JSON.parse(
|
|
199
|
+
await readFile(
|
|
200
|
+
resolve(dirname(owner), catalog.composition.path),
|
|
201
|
+
'utf8',
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
entries.push(...(artifact.entries ?? []));
|
|
205
|
+
} catch {
|
|
206
|
+
// Profile validation reports the precise catalog load failure.
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return entries;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function catalogFacts(entries) {
|
|
213
|
+
const publicClasses = new Set();
|
|
214
|
+
const publicTokens = new Set();
|
|
215
|
+
const publicParts = new Map();
|
|
216
|
+
const classEntries = new Map();
|
|
217
|
+
const exportEntries = new Map();
|
|
218
|
+
for (const entry of entries) {
|
|
219
|
+
exportEntries.set(entry.name, entry);
|
|
220
|
+
for (const item of entry.publicExports ?? [])
|
|
221
|
+
exportEntries.set(typeof item === 'string' ? item : item.name, entry);
|
|
222
|
+
for (const className of entry.boundaries?.publicClasses ?? []) {
|
|
223
|
+
publicClasses.add(className);
|
|
224
|
+
const owners = classEntries.get(className) ?? [];
|
|
225
|
+
owners.push(entry);
|
|
226
|
+
classEntries.set(className, owners);
|
|
227
|
+
}
|
|
228
|
+
for (const token of entry.boundaries?.publicTokens ?? [])
|
|
229
|
+
publicTokens.add(token);
|
|
230
|
+
for (const part of entry.boundaries?.publicParts ?? []) {
|
|
231
|
+
const roots = publicParts.get(part) ?? new Set();
|
|
232
|
+
if (entry.boundaries?.rootClass) roots.add(entry.boundaries.rootClass);
|
|
233
|
+
publicParts.set(part, roots);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
publicClasses,
|
|
238
|
+
publicTokens,
|
|
239
|
+
publicParts,
|
|
240
|
+
classEntries,
|
|
241
|
+
exportEntries,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function spacingValues(value) {
|
|
246
|
+
const results = [];
|
|
247
|
+
for (const match of value.matchAll(/(-?\d*\.?\d+)(px|rem)\b/g)) {
|
|
248
|
+
const numeric = Number(match[1]) * (match[2] === 'rem' ? 16 : 1);
|
|
249
|
+
results.push({ text: match[0], pixels: numeric });
|
|
250
|
+
}
|
|
251
|
+
return results;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function escapeRegExp(value) {
|
|
255
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function inspectCss(
|
|
259
|
+
file,
|
|
260
|
+
source,
|
|
261
|
+
facts,
|
|
262
|
+
diagnostics,
|
|
263
|
+
cssFacts,
|
|
264
|
+
adoption = false,
|
|
265
|
+
) {
|
|
266
|
+
let root;
|
|
267
|
+
try {
|
|
268
|
+
root = postcss.parse(source, { from: file });
|
|
269
|
+
} catch (error) {
|
|
270
|
+
diagnostics.push(
|
|
271
|
+
diagnostic(
|
|
272
|
+
'KUI-L009',
|
|
273
|
+
{ file, line: error.line ?? 1, column: error.column ?? 1 },
|
|
274
|
+
error.reason ?? error.message,
|
|
275
|
+
),
|
|
276
|
+
);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
root.walkRules((rule) => {
|
|
280
|
+
const classes = [
|
|
281
|
+
...rule.selector.matchAll(/\.([_a-zA-Z]+[_a-zA-Z0-9-]*)/g),
|
|
282
|
+
].map((match) => match[1]);
|
|
283
|
+
const publicRootPattern = [...facts.publicClasses]
|
|
284
|
+
.map(
|
|
285
|
+
(className) => `\\.${className.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
|
|
286
|
+
)
|
|
287
|
+
.join('|');
|
|
288
|
+
const privateDescendants = new Set();
|
|
289
|
+
if (publicRootPattern)
|
|
290
|
+
for (const selector of rule.selectors ?? [rule.selector]) {
|
|
291
|
+
const rootMatch = new RegExp(
|
|
292
|
+
`(?:${publicRootPattern})(?:[.#:[\\w-]|\\s)*(?:>|\\s)`,
|
|
293
|
+
).exec(selector);
|
|
294
|
+
if (!rootMatch) continue;
|
|
295
|
+
const descendant = selector.slice(
|
|
296
|
+
rootMatch.index + rootMatch[0].length,
|
|
297
|
+
);
|
|
298
|
+
for (const match of descendant.matchAll(/\.(kui-[\w-]+)/g))
|
|
299
|
+
if (!facts.publicClasses.has(match[1]))
|
|
300
|
+
privateDescendants.add(match[1]);
|
|
301
|
+
}
|
|
302
|
+
for (const className of classes) {
|
|
303
|
+
if (privateDescendants.has(className))
|
|
304
|
+
diagnostics.push(
|
|
305
|
+
diagnostic(
|
|
306
|
+
'KUI-L010',
|
|
307
|
+
location(file, rule),
|
|
308
|
+
`.${className} is a private Kerf descendant; prefer the owning component's prop or variant, or catalog a deliberate public extension point.`,
|
|
309
|
+
{ selector: rule.selector, className },
|
|
310
|
+
undefined,
|
|
311
|
+
adoption,
|
|
312
|
+
),
|
|
313
|
+
);
|
|
314
|
+
else if (
|
|
315
|
+
className.startsWith('kui-') &&
|
|
316
|
+
!facts.publicClasses.has(className)
|
|
317
|
+
)
|
|
318
|
+
diagnostics.push(
|
|
319
|
+
diagnostic(
|
|
320
|
+
'KUI-L001',
|
|
321
|
+
location(file, rule),
|
|
322
|
+
`.${className} is not a cataloged public Kerf class; prefer a component prop or variant, or catalog the extension point.`,
|
|
323
|
+
{ selector: rule.selector, className },
|
|
324
|
+
undefined,
|
|
325
|
+
adoption,
|
|
326
|
+
),
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
for (const match of rule.selector.matchAll(/::part\(\s*([\w-]+)\s*\)/g)) {
|
|
330
|
+
const part = match[1];
|
|
331
|
+
const roots = facts.publicParts.get(part) ?? new Set();
|
|
332
|
+
const allowed = [...roots].some((className) =>
|
|
333
|
+
new RegExp(`\\.${escapeRegExp(className)}(?:[^\\w-]|$)`).test(
|
|
334
|
+
rule.selector.slice(0, match.index),
|
|
335
|
+
),
|
|
336
|
+
);
|
|
337
|
+
if (!allowed)
|
|
338
|
+
diagnostics.push(
|
|
339
|
+
diagnostic(
|
|
340
|
+
'KUI-L011',
|
|
341
|
+
location(file, rule),
|
|
342
|
+
`::part(${part}) is not a cataloged extension point; prefer the component's prop or variant, or add ${part} to boundaries.publicParts.`,
|
|
343
|
+
{ selector: rule.selector, part },
|
|
344
|
+
undefined,
|
|
345
|
+
adoption,
|
|
346
|
+
),
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
rule.walkDecls((decl) => {
|
|
350
|
+
const at = location(file, decl);
|
|
351
|
+
const tokens = [
|
|
352
|
+
...(decl.prop.match(/--kui-[a-z0-9-]+/g) ?? []),
|
|
353
|
+
...(decl.value.match(/--kui-[a-z0-9-]+/g) ?? []),
|
|
354
|
+
];
|
|
355
|
+
for (const token of new Set(tokens))
|
|
356
|
+
if (!facts.publicTokens.has(token)) {
|
|
357
|
+
const assignment = decl.prop === token;
|
|
358
|
+
diagnostics.push(
|
|
359
|
+
diagnostic(
|
|
360
|
+
assignment ? 'KUI-L012' : 'KUI-L002',
|
|
361
|
+
at,
|
|
362
|
+
assignment
|
|
363
|
+
? `${token} has no public configuration contract; prefer the component's prop or variant, or catalog the token explicitly.`
|
|
364
|
+
: `${token} is not a cataloged public Kerf token; prefer a documented token or component configuration.`,
|
|
365
|
+
{ property: decl.prop, value: decl.value, token },
|
|
366
|
+
undefined,
|
|
367
|
+
adoption,
|
|
368
|
+
),
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
if (
|
|
372
|
+
spacingProperties.test(decl.prop) &&
|
|
373
|
+
!/var\(|calc\(|remify\(/.test(decl.value)
|
|
374
|
+
)
|
|
375
|
+
for (const value of spacingValues(decl.value))
|
|
376
|
+
if (!approvedSpacing.has(value.pixels))
|
|
377
|
+
diagnostics.push(
|
|
378
|
+
diagnostic(
|
|
379
|
+
'KUI-L006',
|
|
380
|
+
at,
|
|
381
|
+
`${decl.prop}: ${value.text} is outside the approved Kerf spacing scale.`,
|
|
382
|
+
{
|
|
383
|
+
property: decl.prop,
|
|
384
|
+
value: decl.value,
|
|
385
|
+
pixels: value.pixels,
|
|
386
|
+
},
|
|
387
|
+
),
|
|
388
|
+
);
|
|
389
|
+
for (const className of classes) {
|
|
390
|
+
const record = cssFacts.get(className) ?? {
|
|
391
|
+
scroll: false,
|
|
392
|
+
inset: false,
|
|
393
|
+
};
|
|
394
|
+
if (
|
|
395
|
+
/^overflow(?:-|$)/.test(decl.prop) &&
|
|
396
|
+
/\b(?:auto|scroll)\b/.test(decl.value)
|
|
397
|
+
)
|
|
398
|
+
record.scroll = true;
|
|
399
|
+
if (/^padding(?:-|$)/.test(decl.prop) && decl.value !== '0')
|
|
400
|
+
record.inset = true;
|
|
401
|
+
cssFacts.set(className, record);
|
|
402
|
+
if (
|
|
403
|
+
facts.publicClasses.has(className) &&
|
|
404
|
+
dimensionProperties.test(decl.prop)
|
|
405
|
+
)
|
|
406
|
+
diagnostics.push(
|
|
407
|
+
diagnostic(
|
|
408
|
+
'KUI-L005',
|
|
409
|
+
at,
|
|
410
|
+
`.${className} forces ${decl.prop}; prefer the component's public sizing contract.`,
|
|
411
|
+
{
|
|
412
|
+
selector: rule.selector,
|
|
413
|
+
property: decl.prop,
|
|
414
|
+
value: decl.value,
|
|
415
|
+
},
|
|
416
|
+
),
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function literalClasses(attribute) {
|
|
424
|
+
if (!attribute?.initializer) return { values: [], dynamic: false };
|
|
425
|
+
const value = attribute.initializer;
|
|
426
|
+
if (ts.isStringLiteral(value))
|
|
427
|
+
return { values: value.text.split(/\s+/).filter(Boolean), dynamic: false };
|
|
428
|
+
if (
|
|
429
|
+
ts.isJsxExpression(value) &&
|
|
430
|
+
value.expression &&
|
|
431
|
+
ts.isStringLiteralLike(value.expression)
|
|
432
|
+
)
|
|
433
|
+
return {
|
|
434
|
+
values: value.expression.text.split(/\s+/).filter(Boolean),
|
|
435
|
+
dynamic: false,
|
|
436
|
+
};
|
|
437
|
+
return { values: [], dynamic: true };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function cssPreferred(contract) {
|
|
441
|
+
return (
|
|
442
|
+
[
|
|
443
|
+
...(contract.canonicalShorthands ?? []).map((item) => `\`${item}\``),
|
|
444
|
+
...(contract.helpers ?? []).map((item) => `\`${item}()\``),
|
|
445
|
+
].join(' or ') || 'a cataloged component prop'
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function objectProperty(object, name) {
|
|
450
|
+
return object?.properties.find(
|
|
451
|
+
(property) =>
|
|
452
|
+
ts.isPropertyAssignment(property) &&
|
|
453
|
+
property.name.getText().replaceAll(/["']/g, '') === name,
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function nestedCssValues(value, tail) {
|
|
458
|
+
if (!tail) return value ? [value] : [];
|
|
459
|
+
if (!value || !ts.isArrayLiteralExpression(value)) return [];
|
|
460
|
+
return value.elements.flatMap((element) => {
|
|
461
|
+
if (!ts.isObjectLiteralExpression(element)) return [];
|
|
462
|
+
const property = objectProperty(element, tail);
|
|
463
|
+
return property ? [property.initializer] : [];
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function jsxCssValues(opening, path) {
|
|
468
|
+
const [head, tail] = path.split('[].');
|
|
469
|
+
const attribute = opening.attributes.properties.find(
|
|
470
|
+
(item) => ts.isJsxAttribute(item) && item.name.getText() === head,
|
|
471
|
+
);
|
|
472
|
+
if (!attribute?.initializer) return [];
|
|
473
|
+
const value = ts.isJsxExpression(attribute.initializer)
|
|
474
|
+
? attribute.initializer.expression
|
|
475
|
+
: attribute.initializer;
|
|
476
|
+
return nestedCssValues(value, tail);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function callCssValues(call, path) {
|
|
480
|
+
const options = call.arguments[0];
|
|
481
|
+
if (!options || !ts.isObjectLiteralExpression(options)) return [];
|
|
482
|
+
const [head, tail] = path.split('[].');
|
|
483
|
+
const property = objectProperty(options, head);
|
|
484
|
+
return property ? nestedCssValues(property.initializer, tail) : [];
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function cssLiteral(value) {
|
|
488
|
+
if (
|
|
489
|
+
ts.isStringLiteralLike(value) ||
|
|
490
|
+
ts.isNoSubstitutionTemplateLiteral(value)
|
|
491
|
+
)
|
|
492
|
+
return value.text;
|
|
493
|
+
return undefined;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function importedName(expression, helperImports, namespaces, packageName) {
|
|
497
|
+
let helper;
|
|
498
|
+
if (ts.isIdentifier(expression)) helper = helperImports.get(expression.text);
|
|
499
|
+
else if (
|
|
500
|
+
ts.isPropertyAccessExpression(expression) &&
|
|
501
|
+
ts.isIdentifier(expression.expression) &&
|
|
502
|
+
namespaces.has(expression.expression.text)
|
|
503
|
+
)
|
|
504
|
+
helper = {
|
|
505
|
+
imported: expression.name.text,
|
|
506
|
+
source: namespaces.get(expression.expression.text),
|
|
507
|
+
};
|
|
508
|
+
else helper = undefined;
|
|
509
|
+
if (!helper) return undefined;
|
|
510
|
+
return helper.source === packageName ||
|
|
511
|
+
helper.source.startsWith(`${packageName}/`)
|
|
512
|
+
? helper.imported
|
|
513
|
+
: undefined;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function inspectCssValues(
|
|
517
|
+
file,
|
|
518
|
+
source,
|
|
519
|
+
entry,
|
|
520
|
+
valuesFor,
|
|
521
|
+
helperImports,
|
|
522
|
+
namespaces,
|
|
523
|
+
diagnostics,
|
|
524
|
+
) {
|
|
525
|
+
for (const contract of entry.cssValueProps ?? []) {
|
|
526
|
+
const preferred = cssPreferred(contract);
|
|
527
|
+
for (const value of valuesFor(contract.path)) {
|
|
528
|
+
const at = location(file, value, source);
|
|
529
|
+
if (
|
|
530
|
+
contract.grammar === 'declarations' &&
|
|
531
|
+
contract.rawPolicy !== 'allow'
|
|
532
|
+
) {
|
|
533
|
+
diagnostics.push(
|
|
534
|
+
diagnostic(
|
|
535
|
+
'KUI-L016',
|
|
536
|
+
at,
|
|
537
|
+
`\`${entry.name}.${contract.path}\` is a forbidden declaration-list escape; use \`className\`, public tokens, or cataloged props.`,
|
|
538
|
+
{ component: entry.key, path: contract.path },
|
|
539
|
+
),
|
|
540
|
+
);
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const literal = cssLiteral(value);
|
|
544
|
+
if (literal !== undefined) {
|
|
545
|
+
if (contract.exceptionalShorthands?.includes(literal)) {
|
|
546
|
+
diagnostics.push(
|
|
547
|
+
diagnostic(
|
|
548
|
+
'KUI-L017',
|
|
549
|
+
at,
|
|
550
|
+
`\`${literal}\` is exceptional for \`${entry.name}.${contract.path}\`; prefer ${preferred} unless the off-scale choice is deliberate.`,
|
|
551
|
+
{ component: entry.key, path: contract.path, value: literal },
|
|
552
|
+
),
|
|
553
|
+
);
|
|
554
|
+
} else if (
|
|
555
|
+
!contract.shorthands?.includes(literal) &&
|
|
556
|
+
contract.rawPolicy !== 'allow'
|
|
557
|
+
) {
|
|
558
|
+
diagnostics.push(
|
|
559
|
+
diagnostic(
|
|
560
|
+
'KUI-L013',
|
|
561
|
+
at,
|
|
562
|
+
`\`${entry.name}.${contract.path}\` uses ${contract.grammar} grammar; replace raw \`${literal}\` with ${preferred}.`,
|
|
563
|
+
{ component: entry.key, path: contract.path, value: literal },
|
|
564
|
+
),
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (!ts.isCallExpression(value)) continue;
|
|
570
|
+
const helper = importedName(
|
|
571
|
+
value.expression,
|
|
572
|
+
helperImports,
|
|
573
|
+
namespaces,
|
|
574
|
+
entry.package,
|
|
575
|
+
);
|
|
576
|
+
if (!helper) continue;
|
|
577
|
+
if (contract.nonStandaloneHelpers?.includes(helper))
|
|
578
|
+
diagnostics.push(
|
|
579
|
+
diagnostic(
|
|
580
|
+
'KUI-L015',
|
|
581
|
+
at,
|
|
582
|
+
`\`${helper}()\` is not standalone for \`${entry.name}.${contract.path}\`; wrap it with an accepted composer such as \`calc()\`.`,
|
|
583
|
+
{ component: entry.key, path: contract.path, helper },
|
|
584
|
+
),
|
|
585
|
+
);
|
|
586
|
+
else if (
|
|
587
|
+
!contract.helpers?.includes(helper) &&
|
|
588
|
+
contract.unsafeHelper !== helper
|
|
589
|
+
)
|
|
590
|
+
diagnostics.push(
|
|
591
|
+
diagnostic(
|
|
592
|
+
'KUI-L014',
|
|
593
|
+
at,
|
|
594
|
+
`\`${helper}()\` has the wrong grammar for \`${entry.name}.${contract.path}\`; use ${preferred}.`,
|
|
595
|
+
{ component: entry.key, path: contract.path, helper },
|
|
596
|
+
),
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function inspectTsx(file, sourceText, facts, cssFacts, diagnostics) {
|
|
603
|
+
const source = ts.createSourceFile(
|
|
604
|
+
file,
|
|
605
|
+
sourceText,
|
|
606
|
+
ts.ScriptTarget.Latest,
|
|
607
|
+
true,
|
|
608
|
+
ts.ScriptKind.TSX,
|
|
609
|
+
);
|
|
610
|
+
const imports = new Map();
|
|
611
|
+
const helperImports = new Map();
|
|
612
|
+
const namespaces = new Map();
|
|
613
|
+
for (const statement of source.statements) {
|
|
614
|
+
if (
|
|
615
|
+
!ts.isImportDeclaration(statement) ||
|
|
616
|
+
!ts.isStringLiteral(statement.moduleSpecifier)
|
|
617
|
+
)
|
|
618
|
+
continue;
|
|
619
|
+
const module = statement.moduleSpecifier.text;
|
|
620
|
+
const importClause = statement.importClause;
|
|
621
|
+
if (importClause?.name)
|
|
622
|
+
helperImports.set(importClause.name.text, {
|
|
623
|
+
imported: 'default',
|
|
624
|
+
source: module,
|
|
625
|
+
});
|
|
626
|
+
const bindings = statement.importClause?.namedBindings;
|
|
627
|
+
if (bindings && ts.isNamespaceImport(bindings)) {
|
|
628
|
+
namespaces.set(bindings.name.text, module);
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
if (!bindings || !ts.isNamedImports(bindings)) continue;
|
|
632
|
+
for (const item of bindings.elements) {
|
|
633
|
+
const exported = item.propertyName?.text ?? item.name.text;
|
|
634
|
+
helperImports.set(item.name.text, { imported: exported, source: module });
|
|
635
|
+
const entry = facts.exportEntries.get(exported);
|
|
636
|
+
if (
|
|
637
|
+
entry &&
|
|
638
|
+
(module === entry.package || module.startsWith(`${entry.package}/`))
|
|
639
|
+
)
|
|
640
|
+
imports.set(item.name.text, entry);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const stack = [];
|
|
644
|
+
const visit = (node) => {
|
|
645
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
646
|
+
const opening = ts.isJsxElement(node) ? node.openingElement : node;
|
|
647
|
+
const tag = opening.tagName.getText(source);
|
|
648
|
+
const namespaceEntry =
|
|
649
|
+
ts.isPropertyAccessExpression(opening.tagName) &&
|
|
650
|
+
ts.isIdentifier(opening.tagName.expression)
|
|
651
|
+
? facts.exportEntries.get(opening.tagName.name.text)
|
|
652
|
+
: undefined;
|
|
653
|
+
const namespaceSource =
|
|
654
|
+
ts.isPropertyAccessExpression(opening.tagName) &&
|
|
655
|
+
ts.isIdentifier(opening.tagName.expression)
|
|
656
|
+
? namespaces.get(opening.tagName.expression.text)
|
|
657
|
+
: undefined;
|
|
658
|
+
const entry =
|
|
659
|
+
imports.get(tag) ??
|
|
660
|
+
(namespaceEntry &&
|
|
661
|
+
(namespaceSource === namespaceEntry.package ||
|
|
662
|
+
namespaceSource?.startsWith(`${namespaceEntry.package}/`))
|
|
663
|
+
? namespaceEntry
|
|
664
|
+
: undefined);
|
|
665
|
+
const classAttribute = opening.attributes.properties.find(
|
|
666
|
+
(item) =>
|
|
667
|
+
ts.isJsxAttribute(item) &&
|
|
668
|
+
['class', 'className'].includes(item.name.getText(source)),
|
|
669
|
+
);
|
|
670
|
+
const classes = literalClasses(classAttribute);
|
|
671
|
+
const at = location(file, opening, source);
|
|
672
|
+
if (entry)
|
|
673
|
+
inspectCssValues(
|
|
674
|
+
file,
|
|
675
|
+
source,
|
|
676
|
+
entry,
|
|
677
|
+
(path) => jsxCssValues(opening, path),
|
|
678
|
+
helperImports,
|
|
679
|
+
namespaces,
|
|
680
|
+
diagnostics,
|
|
681
|
+
);
|
|
682
|
+
if (classes.dynamic)
|
|
683
|
+
diagnostics.push(
|
|
684
|
+
diagnostic(
|
|
685
|
+
'KUI-L008',
|
|
686
|
+
at,
|
|
687
|
+
'Dynamic class expression was left unclassified; review it against cataloged public classes.',
|
|
688
|
+
{ tag },
|
|
689
|
+
),
|
|
690
|
+
);
|
|
691
|
+
const owners = classes.values.flatMap(
|
|
692
|
+
(className) => facts.classEntries.get(className) ?? [],
|
|
693
|
+
);
|
|
694
|
+
for (const property of ['margin', 'border', 'padding']) {
|
|
695
|
+
const selfOwners = owners.filter(
|
|
696
|
+
(owner) => owner.layout?.geometry?.[property] === 'self',
|
|
697
|
+
);
|
|
698
|
+
if (new Set(selfOwners.map((owner) => owner.key)).size > 1)
|
|
699
|
+
diagnostics.push(
|
|
700
|
+
diagnostic(
|
|
701
|
+
'KUI-L003',
|
|
702
|
+
at,
|
|
703
|
+
`One element combines multiple ${property} owners: ${selfOwners.map((owner) => owner.key).join(', ')}.`,
|
|
704
|
+
{ property, classes: classes.values },
|
|
705
|
+
selfOwners.map((owner) => owner.key),
|
|
706
|
+
),
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
const scroll = classes.values.some(
|
|
710
|
+
(className) => cssFacts.get(className)?.scroll,
|
|
711
|
+
);
|
|
712
|
+
const inset = classes.values.some(
|
|
713
|
+
(className) => cssFacts.get(className)?.inset,
|
|
714
|
+
);
|
|
715
|
+
const parent = stack.at(-1);
|
|
716
|
+
if (scroll && parent?.scroll)
|
|
717
|
+
diagnostics.push(
|
|
718
|
+
diagnostic(
|
|
719
|
+
'KUI-L007',
|
|
720
|
+
at,
|
|
721
|
+
'A scroll-owning element is nested inside another declared scroll owner.',
|
|
722
|
+
{ classes: classes.values },
|
|
723
|
+
[parent.label, entry?.key ?? tag],
|
|
724
|
+
),
|
|
725
|
+
);
|
|
726
|
+
if (inset && parent?.inset)
|
|
727
|
+
diagnostics.push(
|
|
728
|
+
diagnostic(
|
|
729
|
+
'KUI-L004',
|
|
730
|
+
at,
|
|
731
|
+
'Nested elements both add content padding; confirm that the repeated inset is intentional.',
|
|
732
|
+
{ classes: classes.values },
|
|
733
|
+
[parent.label, entry?.key ?? tag],
|
|
734
|
+
),
|
|
735
|
+
);
|
|
736
|
+
stack.push({
|
|
737
|
+
scroll: scroll || parent?.scroll,
|
|
738
|
+
inset: inset || parent?.inset,
|
|
739
|
+
label: entry?.key ?? tag,
|
|
740
|
+
});
|
|
741
|
+
if (ts.isJsxElement(node))
|
|
742
|
+
for (const child of node.children) visit(child);
|
|
743
|
+
stack.pop();
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (ts.isCallExpression(node)) {
|
|
747
|
+
const namespaceEntry = ts.isPropertyAccessExpression(node.expression)
|
|
748
|
+
? facts.exportEntries.get(node.expression.name.text)
|
|
749
|
+
: undefined;
|
|
750
|
+
const namespaceSource =
|
|
751
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
752
|
+
ts.isIdentifier(node.expression.expression)
|
|
753
|
+
? namespaces.get(node.expression.expression.text)
|
|
754
|
+
: undefined;
|
|
755
|
+
const entry = ts.isIdentifier(node.expression)
|
|
756
|
+
? imports.get(node.expression.text)
|
|
757
|
+
: namespaceEntry &&
|
|
758
|
+
(namespaceSource === namespaceEntry.package ||
|
|
759
|
+
namespaceSource?.startsWith(`${namespaceEntry.package}/`))
|
|
760
|
+
? namespaceEntry
|
|
761
|
+
: undefined;
|
|
762
|
+
if (entry)
|
|
763
|
+
inspectCssValues(
|
|
764
|
+
file,
|
|
765
|
+
source,
|
|
766
|
+
entry,
|
|
767
|
+
(path) => callCssValues(node, path),
|
|
768
|
+
helperImports,
|
|
769
|
+
namespaces,
|
|
770
|
+
diagnostics,
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
ts.forEachChild(node, visit);
|
|
774
|
+
};
|
|
775
|
+
visit(source);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
export async function analyzeUiProject({
|
|
779
|
+
root: requestedRoot = process.cwd(),
|
|
780
|
+
paths,
|
|
781
|
+
knownRules = [],
|
|
782
|
+
adoption = false,
|
|
783
|
+
profile: packageProfile = resolve(
|
|
784
|
+
import.meta.dirname,
|
|
785
|
+
'../ai/application-ui-profile.defaults.json',
|
|
786
|
+
),
|
|
787
|
+
} = {}) {
|
|
788
|
+
const root = resolve(requestedRoot);
|
|
789
|
+
const files = await collectFiles(root, paths);
|
|
790
|
+
const diagnosticDecisions = new Map();
|
|
791
|
+
const contents = new Map(
|
|
792
|
+
await Promise.all(
|
|
793
|
+
files.map(async (file) => [file, await readFile(file, 'utf8')]),
|
|
794
|
+
),
|
|
795
|
+
);
|
|
796
|
+
const discoveredFiles = new Set(files);
|
|
797
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
798
|
+
const file = files[index];
|
|
799
|
+
for (const dependency of relativeStyleImports(file, contents.get(file))) {
|
|
800
|
+
const projectPath = relative(root, dependency);
|
|
801
|
+
if (
|
|
802
|
+
discoveredFiles.has(dependency) ||
|
|
803
|
+
projectPath === '..' ||
|
|
804
|
+
projectPath.startsWith('../')
|
|
805
|
+
)
|
|
806
|
+
continue;
|
|
807
|
+
try {
|
|
808
|
+
const source = await readFile(dependency, 'utf8');
|
|
809
|
+
discoveredFiles.add(dependency);
|
|
810
|
+
files.push(dependency);
|
|
811
|
+
contents.set(dependency, source);
|
|
812
|
+
} catch {
|
|
813
|
+
// TypeScript or the CSS toolchain reports missing imports; analysis stays read-only.
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
files.sort();
|
|
818
|
+
const profileContexts = new Map();
|
|
819
|
+
const loadContext = async (file) => {
|
|
820
|
+
const startDirectory = file === root ? root : dirname(file);
|
|
821
|
+
if (!profileContexts.has(startDirectory))
|
|
822
|
+
profileContexts.set(
|
|
823
|
+
startDirectory,
|
|
824
|
+
(async () => {
|
|
825
|
+
const profileResult = await loadApplicationUiProfile({
|
|
826
|
+
workspaceRoot: root,
|
|
827
|
+
startDirectory,
|
|
828
|
+
packageProfile,
|
|
829
|
+
knownRules: [...Object.keys(UI_ANALYSIS_RULES), ...knownRules],
|
|
830
|
+
});
|
|
831
|
+
const entries = await loadCatalogs(profileResult);
|
|
832
|
+
return {
|
|
833
|
+
profileResult,
|
|
834
|
+
facts: catalogFacts(entries),
|
|
835
|
+
startDirectory,
|
|
836
|
+
};
|
|
837
|
+
})(),
|
|
838
|
+
);
|
|
839
|
+
return profileContexts.get(startDirectory);
|
|
840
|
+
};
|
|
841
|
+
const contexts = new Map(
|
|
842
|
+
await Promise.all(
|
|
843
|
+
files.map(async (file) => [file, await loadContext(file)]),
|
|
844
|
+
),
|
|
845
|
+
);
|
|
846
|
+
if (!files.length) await loadContext(root);
|
|
847
|
+
const imports = new Map(
|
|
848
|
+
files.map((file) => [file, relativeStyleImports(file, contents.get(file))]),
|
|
849
|
+
);
|
|
850
|
+
const styleFacts = new Map();
|
|
851
|
+
const recordDiagnostics = (items, context) => {
|
|
852
|
+
for (const item of items) {
|
|
853
|
+
const key = JSON.stringify([
|
|
854
|
+
item.ruleId,
|
|
855
|
+
item.location,
|
|
856
|
+
item.message,
|
|
857
|
+
item.evidence,
|
|
858
|
+
item.chain,
|
|
859
|
+
]);
|
|
860
|
+
const decision = diagnosticDecisions.get(key) ?? {
|
|
861
|
+
item,
|
|
862
|
+
active: false,
|
|
863
|
+
suppressed: false,
|
|
864
|
+
activeConsumers: new Set(),
|
|
865
|
+
suppressedConsumers: new Set(),
|
|
866
|
+
};
|
|
867
|
+
const consumer =
|
|
868
|
+
relative(root, context.startDirectory).replaceAll('\\', '/') || '.';
|
|
869
|
+
if (isSuppressed(item, context.profileResult.profile, root)) {
|
|
870
|
+
decision.suppressed = true;
|
|
871
|
+
decision.suppressedConsumers.add(consumer);
|
|
872
|
+
} else {
|
|
873
|
+
decision.active = true;
|
|
874
|
+
decision.activeConsumers.add(consumer);
|
|
875
|
+
}
|
|
876
|
+
diagnosticDecisions.set(key, decision);
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
for (const file of files.filter((item) => item.endsWith('.css'))) {
|
|
880
|
+
const context = contexts.get(file);
|
|
881
|
+
const fileFacts = new Map();
|
|
882
|
+
await inspectCss(
|
|
883
|
+
file,
|
|
884
|
+
contents.get(file),
|
|
885
|
+
context.facts,
|
|
886
|
+
[],
|
|
887
|
+
fileFacts,
|
|
888
|
+
adoption,
|
|
889
|
+
);
|
|
890
|
+
styleFacts.set(file, fileFacts);
|
|
891
|
+
}
|
|
892
|
+
const styleConsumers = new Map(
|
|
893
|
+
[...styleFacts.keys()].map((file) => [file, new Set()]),
|
|
894
|
+
);
|
|
895
|
+
for (const file of files.filter((item) =>
|
|
896
|
+
sourceExtensions.has(extname(item)),
|
|
897
|
+
)) {
|
|
898
|
+
const context = contexts.get(file);
|
|
899
|
+
for (const style of reachableStyleFiles(file, imports, styleFacts))
|
|
900
|
+
styleConsumers.get(style).add(context);
|
|
901
|
+
const fileDiagnostics = [];
|
|
902
|
+
inspectTsx(
|
|
903
|
+
file,
|
|
904
|
+
contents.get(file),
|
|
905
|
+
context.facts,
|
|
906
|
+
reachableStyleFacts(file, imports, styleFacts),
|
|
907
|
+
fileDiagnostics,
|
|
908
|
+
);
|
|
909
|
+
recordDiagnostics(fileDiagnostics, context);
|
|
910
|
+
}
|
|
911
|
+
for (const [file, consumers] of styleConsumers) {
|
|
912
|
+
const applicableContexts = consumers.size
|
|
913
|
+
? consumers
|
|
914
|
+
: new Set([contexts.get(file)]);
|
|
915
|
+
for (const context of applicableContexts) {
|
|
916
|
+
const fileDiagnostics = [];
|
|
917
|
+
await inspectCss(
|
|
918
|
+
file,
|
|
919
|
+
contents.get(file),
|
|
920
|
+
context.facts,
|
|
921
|
+
fileDiagnostics,
|
|
922
|
+
new Map(),
|
|
923
|
+
adoption,
|
|
924
|
+
);
|
|
925
|
+
recordDiagnostics(fileDiagnostics, context);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const active = [...diagnosticDecisions.values()]
|
|
930
|
+
.filter((decision) => decision.active)
|
|
931
|
+
.map((decision) => {
|
|
932
|
+
if (
|
|
933
|
+
!decision.suppressedConsumers.size &&
|
|
934
|
+
decision.activeConsumers.size <= 1
|
|
935
|
+
)
|
|
936
|
+
return decision.item;
|
|
937
|
+
return {
|
|
938
|
+
...decision.item,
|
|
939
|
+
evidence: {
|
|
940
|
+
...decision.item.evidence,
|
|
941
|
+
consumers: {
|
|
942
|
+
active: [...decision.activeConsumers].sort(),
|
|
943
|
+
suppressed: [...decision.suppressedConsumers].sort(),
|
|
944
|
+
},
|
|
945
|
+
},
|
|
946
|
+
};
|
|
947
|
+
});
|
|
948
|
+
const suppressed = [...diagnosticDecisions.values()].filter(
|
|
949
|
+
(decision) => !decision.active && decision.suppressed,
|
|
950
|
+
).length;
|
|
951
|
+
active.sort(
|
|
952
|
+
(a, b) =>
|
|
953
|
+
a.location.file.localeCompare(b.location.file) ||
|
|
954
|
+
a.location.line - b.location.line ||
|
|
955
|
+
a.ruleId.localeCompare(b.ruleId),
|
|
956
|
+
);
|
|
957
|
+
const portablePath = (file) => {
|
|
958
|
+
if (!file || !isAbsolute(file)) return file;
|
|
959
|
+
const path = relative(root, file).replaceAll('\\', '/');
|
|
960
|
+
return path === '..' || path.startsWith('../')
|
|
961
|
+
? `<external>/${basename(file)}`
|
|
962
|
+
: path;
|
|
963
|
+
};
|
|
964
|
+
const resolvedContexts = await Promise.all(profileContexts.values());
|
|
965
|
+
const profileFiles = [
|
|
966
|
+
...new Set(
|
|
967
|
+
resolvedContexts.flatMap(
|
|
968
|
+
({ profileResult }) => profileResult.files ?? [],
|
|
969
|
+
),
|
|
970
|
+
),
|
|
971
|
+
];
|
|
972
|
+
const profilePackageRoot = dirname(profileFiles[0] ?? root);
|
|
973
|
+
const portableMessage = (message) =>
|
|
974
|
+
String(message)
|
|
975
|
+
.replaceAll(root, '<repo-root>')
|
|
976
|
+
.replaceAll(profilePackageRoot, '<package-root>');
|
|
977
|
+
const profileDiagnostics = [
|
|
978
|
+
...new Map(
|
|
979
|
+
resolvedContexts
|
|
980
|
+
.flatMap(({ profileResult }) => profileResult.diagnostics ?? [])
|
|
981
|
+
.map((item) => [
|
|
982
|
+
JSON.stringify([item.code, item.source, item.path, item.message]),
|
|
983
|
+
{
|
|
984
|
+
...item,
|
|
985
|
+
source: portablePath(item.source),
|
|
986
|
+
message: portableMessage(item.message),
|
|
987
|
+
},
|
|
988
|
+
]),
|
|
989
|
+
).values(),
|
|
990
|
+
];
|
|
991
|
+
return {
|
|
992
|
+
schemaVersion: UI_ANALYSIS_SCHEMA_VERSION,
|
|
993
|
+
tool: { name: '@kerfjs/ui analyzer', version: 1 },
|
|
994
|
+
root: '.',
|
|
995
|
+
files: files.map((file) => relative(root, file).replaceAll('\\', '/')),
|
|
996
|
+
profile: {
|
|
997
|
+
files: profileFiles.map(portablePath),
|
|
998
|
+
diagnostics: profileDiagnostics,
|
|
999
|
+
},
|
|
1000
|
+
diagnostics: active.map((item) => ({
|
|
1001
|
+
...item,
|
|
1002
|
+
location: {
|
|
1003
|
+
...item.location,
|
|
1004
|
+
file: portablePath(item.location.file),
|
|
1005
|
+
},
|
|
1006
|
+
})),
|
|
1007
|
+
summary: {
|
|
1008
|
+
errors:
|
|
1009
|
+
active.filter((item) => item.severity === 'error').length +
|
|
1010
|
+
profileDiagnostics.length,
|
|
1011
|
+
review: active.filter((item) => item.severity === 'review').length,
|
|
1012
|
+
suppressed,
|
|
1013
|
+
},
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
export function formatUiAnalysisText(report) {
|
|
1018
|
+
const lines = report.profile.diagnostics.map(
|
|
1019
|
+
(item) =>
|
|
1020
|
+
`${item.source ?? '<profile>'}:1:1 error ${item.code} ${item.message}`,
|
|
1021
|
+
);
|
|
1022
|
+
lines.push(
|
|
1023
|
+
...report.diagnostics.map(
|
|
1024
|
+
(item) =>
|
|
1025
|
+
`${item.location.file}:${item.location.line}:${item.location.column} ${item.severity} ${item.ruleId} ${item.message}`,
|
|
1026
|
+
),
|
|
1027
|
+
);
|
|
1028
|
+
lines.push(
|
|
1029
|
+
`Kerf UI analysis: ${report.summary.errors} error(s), ${report.summary.review} review finding(s), ${report.summary.suppressed} suppressed.`,
|
|
1030
|
+
);
|
|
1031
|
+
return lines.join('\n');
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
export function formatUiAnalysisSarif(report) {
|
|
1035
|
+
return {
|
|
1036
|
+
version: '2.1.0',
|
|
1037
|
+
$schema: 'https://json.schemastore.org/sarif-2.1.0.json',
|
|
1038
|
+
runs: [
|
|
1039
|
+
{
|
|
1040
|
+
tool: {
|
|
1041
|
+
driver: {
|
|
1042
|
+
name: report.tool.name,
|
|
1043
|
+
version: String(report.tool.version),
|
|
1044
|
+
rules: Object.entries(UI_ANALYSIS_RULES).map(([id, rule]) => ({
|
|
1045
|
+
id,
|
|
1046
|
+
shortDescription: { text: rule.title },
|
|
1047
|
+
})),
|
|
1048
|
+
},
|
|
1049
|
+
},
|
|
1050
|
+
results: [
|
|
1051
|
+
...report.profile.diagnostics.map((item) => ({
|
|
1052
|
+
ruleId: item.code,
|
|
1053
|
+
level: 'error',
|
|
1054
|
+
message: { text: item.message },
|
|
1055
|
+
locations: [
|
|
1056
|
+
{
|
|
1057
|
+
physicalLocation: {
|
|
1058
|
+
artifactLocation: { uri: item.source ?? '<profile>' },
|
|
1059
|
+
region: { startLine: 1, startColumn: 1 },
|
|
1060
|
+
},
|
|
1061
|
+
},
|
|
1062
|
+
],
|
|
1063
|
+
properties: { path: item.path },
|
|
1064
|
+
})),
|
|
1065
|
+
...report.diagnostics.map((item) => ({
|
|
1066
|
+
ruleId: item.ruleId,
|
|
1067
|
+
level: item.severity === 'error' ? 'error' : 'warning',
|
|
1068
|
+
message: { text: item.message },
|
|
1069
|
+
locations: [
|
|
1070
|
+
{
|
|
1071
|
+
physicalLocation: {
|
|
1072
|
+
artifactLocation: { uri: item.location.file },
|
|
1073
|
+
region: {
|
|
1074
|
+
startLine: item.location.line,
|
|
1075
|
+
startColumn: item.location.column,
|
|
1076
|
+
},
|
|
1077
|
+
},
|
|
1078
|
+
},
|
|
1079
|
+
],
|
|
1080
|
+
properties: { evidence: item.evidence, chain: item.chain },
|
|
1081
|
+
})),
|
|
1082
|
+
],
|
|
1083
|
+
},
|
|
1084
|
+
],
|
|
1085
|
+
};
|
|
1086
|
+
}
|