@kerfjs/ui 4.5.0-beta.1 → 5.0.0-beta.100
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 +461 -116
- package/ai/application-ui-diagnostic-ids-v1.json +23 -1
- package/ai/application-ui-profile-sync.cjs +21 -2
- package/ai/application-ui-profile.d.ts +4 -0
- package/ai/application-ui-profile.defaults.json +63 -8
- package/ai/application-ui-profile.mjs +28 -1
- package/ai/application-ui-profile.schema.json +10 -0
- package/ai/catalog-authoring.json +13 -4
- package/ai/catalog-authoring.schema.json +52 -8
- package/ai/compile-time-contracts-v1.json +63 -16
- package/ai/compile-time-contracts-v1.schema.json +4 -1
- package/ai/component-catalog-extension.schema.json +25 -0
- package/ai/component-catalog.json +5585 -1175
- package/ai/component-catalog.schema.json +78 -1
- package/ai/component-class-owners.cjs +83 -0
- package/ai/component-composition-extension.d.ts +7 -0
- package/ai/component-composition-extension.schema.json +7 -0
- package/ai/{component-catalog-v2-overrides.json → component-composition-rules.json} +281 -49
- package/ai/component-composition.d.ts +132 -0
- package/ai/component-composition.json +8729 -0
- package/ai/component-composition.schema.json +412 -0
- package/ai/components/README.md +185 -0
- package/ai/components/anchored-surface.md +99 -0
- package/ai/components/application-tabs.md +65 -0
- package/ai/components/badge.md +76 -0
- package/ai/components/chip.md +76 -0
- package/ai/components/choice-chips.md +87 -0
- package/ai/components/collapsible-panel.md +103 -0
- package/ai/components/column-track.md +78 -0
- package/ai/components/combobox.md +88 -0
- package/ai/components/composer.md +83 -0
- package/ai/components/content-item.md +98 -0
- package/ai/components/content-swap.md +78 -0
- package/ai/components/corner-status.md +88 -0
- package/ai/components/disclosure-arrow.md +77 -0
- package/ai/components/document-baseline.md +77 -0
- package/ai/components/edge-activity-indicator.md +81 -0
- package/ai/components/empty-state.md +80 -0
- package/ai/components/feedback.md +71 -0
- package/ai/components/file-picker.md +88 -0
- package/ai/components/floating-toolbar.md +82 -0
- package/ai/components/foundation.md +65 -0
- package/ai/components/framed-list.md +80 -0
- package/ai/components/grid.md +101 -0
- package/ai/components/headers.md +63 -0
- package/ai/components/icon-button.md +82 -0
- package/ai/components/icon-toggle-button.md +80 -0
- package/ai/components/layout.md +75 -0
- package/ai/components/list-action-row.md +108 -0
- package/ai/components/list-header.md +91 -0
- package/ai/components/list-inset-control.md +74 -0
- package/ai/components/list-inset-text.md +72 -0
- package/ai/components/list-item.md +108 -0
- package/ai/components/list.md +125 -0
- package/ai/components/loading-spinner.md +73 -0
- package/ai/components/lucide-icon.md +97 -0
- package/ai/components/nav-stack.md +115 -0
- package/ai/components/pane.md +115 -0
- package/ai/components/popup-menu.md +171 -0
- package/ai/components/recipe-app-shell.md +57 -0
- package/ai/components/recipe-collapsible-sidebar.md +57 -0
- package/ai/components/recipe-compact-toolbar.md +57 -0
- package/ai/components/recipe-composer-form.md +57 -0
- package/ai/components/recipe-list-detail-dialog.md +57 -0
- package/ai/components/recipe-list-workspace-states.md +57 -0
- package/ai/components/recipe-loading-inspector.md +57 -0
- package/ai/components/recipe-navigation-sidebar.md +57 -0
- package/ai/components/recipe-navigation-stack.md +57 -0
- package/ai/components/recipe-workspace-header.md +57 -0
- package/ai/components/resize.md +106 -0
- package/ai/components/row.md +96 -0
- package/ai/components/segmented-control.md +82 -0
- package/ai/components/select.md +116 -0
- package/ai/components/skeleton.md +84 -0
- package/ai/components/spacer.md +86 -0
- package/ai/components/split-button.md +80 -0
- package/ai/components/split-view.md +105 -0
- package/ai/components/state-banner.md +96 -0
- package/ai/components/summary-row.md +85 -0
- package/ai/components/sunken-panel.md +79 -0
- package/ai/components/surface-scaffold.md +105 -0
- package/ai/components/tab-bar.md +114 -0
- package/ai/components/tab-navigator.md +100 -0
- package/ai/components/tabs.md +95 -0
- package/ai/components/text.md +90 -0
- package/ai/components/token-search-field.md +107 -0
- package/ai/components/toolbar-control-group.md +125 -0
- package/ai/components/toolbar-text.md +100 -0
- package/ai/components/toolbar.md +103 -0
- package/ai/components/value-table.md +80 -0
- package/ai/components/wa-accordion-item.md +55 -0
- package/ai/components/wa-accordion.md +65 -0
- package/ai/components/wa-animated-image.md +55 -0
- package/ai/components/wa-animation.md +55 -0
- package/ai/components/wa-avatar.md +55 -0
- package/ai/components/wa-badge.md +55 -0
- package/ai/components/wa-breadcrumb-item.md +53 -0
- package/ai/components/wa-breadcrumb.md +55 -0
- package/ai/components/wa-button-group.md +60 -0
- package/ai/components/wa-button.md +59 -0
- package/ai/components/wa-callout.md +58 -0
- package/ai/components/wa-card.md +59 -0
- package/ai/components/wa-carousel-item.md +53 -0
- package/ai/components/wa-carousel.md +55 -0
- package/ai/components/wa-checkbox-group.md +55 -0
- package/ai/components/wa-checkbox.md +55 -0
- package/ai/components/wa-color-picker.md +55 -0
- package/ai/components/wa-comparison.md +55 -0
- package/ai/components/wa-copy-button.md +55 -0
- package/ai/components/wa-details.md +61 -0
- package/ai/components/wa-dialog.md +59 -0
- package/ai/components/wa-divider.md +53 -0
- package/ai/components/wa-drawer.md +55 -0
- package/ai/components/wa-dropdown-item.md +60 -0
- package/ai/components/wa-dropdown.md +60 -0
- package/ai/components/wa-format-bytes.md +53 -0
- package/ai/components/wa-format-date.md +53 -0
- package/ai/components/wa-format-number.md +53 -0
- package/ai/components/wa-icon.md +58 -0
- package/ai/components/wa-include.md +53 -0
- package/ai/components/wa-input.md +59 -0
- package/ai/components/wa-intersection-observer.md +53 -0
- package/ai/components/wa-known-date.md +53 -0
- package/ai/components/wa-markdown.md +53 -0
- package/ai/components/wa-mutation-observer.md +53 -0
- package/ai/components/wa-number-input.md +55 -0
- package/ai/components/wa-option.md +59 -0
- package/ai/components/wa-otp-input.md +53 -0
- package/ai/components/wa-page.md +53 -0
- package/ai/components/wa-pagination.md +55 -0
- package/ai/components/wa-popover.md +55 -0
- package/ai/components/wa-popup.md +53 -0
- package/ai/components/wa-progress-bar.md +53 -0
- package/ai/components/wa-progress-ring.md +53 -0
- package/ai/components/wa-qr-code.md +53 -0
- package/ai/components/wa-radio-group.md +59 -0
- package/ai/components/wa-radio.md +55 -0
- package/ai/components/wa-random-content.md +53 -0
- package/ai/components/wa-rating.md +55 -0
- package/ai/components/wa-relative-time.md +53 -0
- package/ai/components/wa-resize-observer.md +53 -0
- package/ai/components/wa-scroller.md +53 -0
- package/ai/components/wa-select.md +63 -0
- package/ai/components/wa-skeleton.md +53 -0
- package/ai/components/wa-slider.md +55 -0
- package/ai/components/wa-spinner.md +58 -0
- package/ai/components/wa-split-panel.md +58 -0
- package/ai/components/wa-switch.md +53 -0
- package/ai/components/wa-tab-group.md +69 -0
- package/ai/components/wa-tab-panel.md +60 -0
- package/ai/components/wa-tab.md +59 -0
- package/ai/components/wa-tag.md +59 -0
- package/ai/components/wa-textarea.md +53 -0
- package/ai/components/wa-time-input.md +55 -0
- package/ai/components/wa-toast-item.md +60 -0
- package/ai/components/wa-toast.md +60 -0
- package/ai/components/wa-tooltip.md +55 -0
- package/ai/components/wa-tree-item.md +55 -0
- package/ai/components/wa-tree.md +55 -0
- package/ai/components/wa-zoomable-frame.md +55 -0
- package/ai/components/workbench.md +120 -0
- package/ai/public-api-signatures-v1.md +3851 -441
- package/ai/skill.md +402 -70
- package/ai/webawesome-jsx-signatures-v1.md +9 -2
- package/analyzer/cli.mjs +5 -2
- package/analyzer/component-ownership.d.mts +72 -0
- package/analyzer/component-ownership.mjs +429 -0
- package/analyzer/index.d.mts +8 -0
- package/analyzer/index.mjs +1687 -49
- package/analyzer/loud-pairs.d.mts +49 -0
- package/analyzer/loud-pairs.mjs +357 -0
- package/analyzer/selectors.d.mts +21 -0
- package/analyzer/selectors.mjs +212 -0
- package/catalog-projection-cli.mjs +96 -0
- package/catalog-projection.d.mts +55 -0
- package/catalog-projection.mjs +62 -0
- package/dist/anchored-surface.d.ts +69 -0
- package/dist/anchored-surface.js +3 -0
- package/dist/anchored-surface.js.map +1 -0
- package/dist/app-tab.d.ts +39 -4
- package/dist/app-tab.js +2 -1
- package/dist/badge.d.ts +83 -0
- package/dist/badge.js +4 -0
- package/dist/badge.js.map +1 -0
- package/dist/browser/anchored-surface.js +3 -0
- package/dist/browser/badge.js +3 -0
- package/dist/browser/catalog.js +28 -0
- package/dist/browser/chip.js +3 -0
- package/dist/browser/choice-chips.js +3 -0
- package/dist/browser/collapsible-panel.js +9 -0
- package/dist/browser/column-track.js +3 -0
- package/dist/browser/combobox.js +3 -0
- package/dist/browser/composer.js +4 -0
- package/dist/browser/content-item.js +3 -0
- package/dist/browser/content-swap.js +3 -0
- package/dist/browser/corner-status.js +3 -0
- package/dist/browser/edge-activity-indicator.js +3 -0
- package/dist/browser/file-picker.js +3 -0
- package/dist/browser/framed-list.js +3 -0
- package/dist/browser/grid.js +3 -0
- package/dist/browser/icon-button.js +4 -0
- package/dist/browser/icon-toggle-button.js +4 -0
- package/dist/browser/list-action-row.js +2 -0
- package/dist/browser/list-header.js +3 -1
- package/dist/browser/list-item.js +1 -0
- package/dist/browser/nav-stack.js +11 -0
- package/dist/browser/popup-menu.js +3 -0
- package/dist/browser/row.js +3 -0
- package/dist/browser/segmented-control.js +0 -1
- package/dist/browser/select.js +2 -1
- package/dist/browser/spacer.js +3 -0
- package/dist/browser/split-button.js +5 -0
- package/dist/browser/split-view.js +13 -0
- package/dist/browser/state-banner.js +1 -0
- package/dist/browser/summary-row.js +3 -0
- package/dist/browser/surface-scaffold.js +3 -0
- package/dist/browser/tab-navigator.js +4 -0
- package/dist/browser/text.js +3 -0
- package/dist/browser/toolbar-control-group.js +1 -0
- package/dist/browser/workbench.js +14 -0
- package/dist/catalog-resources.d.ts +17 -1
- package/dist/catalog-resources.js +5 -5
- package/dist/catalog-resources.js.map +1 -1
- package/dist/catalog.d.ts +123 -121
- package/dist/catalog.js +598 -269
- package/dist/catalog.js.map +1 -1
- package/dist/chip.d.ts +69 -0
- package/dist/chip.js +3 -0
- package/dist/chip.js.map +1 -0
- package/dist/choice-chips.d.ts +82 -0
- package/dist/choice-chips.js +3 -0
- package/dist/choice-chips.js.map +1 -0
- package/dist/chunk-2GOCOLQL.js +270 -0
- package/dist/chunk-2GOCOLQL.js.map +1 -0
- package/dist/chunk-2MSPNRWJ.js +82 -0
- package/dist/chunk-2MSPNRWJ.js.map +1 -0
- package/dist/chunk-34U4ANDJ.js +119 -0
- package/dist/chunk-34U4ANDJ.js.map +1 -0
- package/dist/chunk-3GLYIIFR.js +230 -0
- package/dist/chunk-3GLYIIFR.js.map +1 -0
- package/dist/{chunk-Y6LYKFXY.js → chunk-3VXDSIBJ.js} +7 -4
- package/dist/chunk-3VXDSIBJ.js.map +1 -0
- package/dist/chunk-3WYREXW4.js +120 -0
- package/dist/chunk-3WYREXW4.js.map +1 -0
- package/dist/chunk-4UB5Z3ZZ.js +68 -0
- package/dist/chunk-4UB5Z3ZZ.js.map +1 -0
- package/dist/chunk-6C6DLP3W.js +31 -0
- package/dist/chunk-6C6DLP3W.js.map +1 -0
- package/dist/chunk-7IUEBNCO.js +108 -0
- package/dist/chunk-7IUEBNCO.js.map +1 -0
- package/dist/{chunk-QO55FN2Y.js → chunk-AN5IWMTM.js} +7 -3
- package/dist/chunk-AN5IWMTM.js.map +1 -0
- package/dist/chunk-AV7CPAPT.js +82 -0
- package/dist/chunk-AV7CPAPT.js.map +1 -0
- package/dist/{chunk-PX3YSS5C.js → chunk-B53PS2LE.js} +5 -3
- package/dist/chunk-B53PS2LE.js.map +1 -0
- package/dist/chunk-CA7LNX3J.js +44 -0
- package/dist/chunk-CA7LNX3J.js.map +1 -0
- package/dist/chunk-CEJ5FK6G.js +288 -0
- package/dist/chunk-CEJ5FK6G.js.map +1 -0
- package/dist/chunk-CMETHPKR.js +175 -0
- package/dist/chunk-CMETHPKR.js.map +1 -0
- package/dist/chunk-CMLPFIC5.js +46 -0
- package/dist/chunk-CMLPFIC5.js.map +1 -0
- package/dist/{chunk-RKVQEH4J.js → chunk-CVD5IZGB.js} +10 -5
- package/dist/chunk-CVD5IZGB.js.map +1 -0
- package/dist/chunk-CXZXHZTR.js +514 -0
- package/dist/chunk-CXZXHZTR.js.map +1 -0
- package/dist/chunk-D5RGM3XA.js +38 -0
- package/dist/chunk-D5RGM3XA.js.map +1 -0
- package/dist/{chunk-KWWR5VMS.js → chunk-DZJ4GBSP.js} +19 -5
- package/dist/chunk-DZJ4GBSP.js.map +1 -0
- package/dist/chunk-EIET4QLK.js +51 -0
- package/dist/chunk-EIET4QLK.js.map +1 -0
- package/dist/chunk-ELCMSQNJ.js +49 -0
- package/dist/chunk-ELCMSQNJ.js.map +1 -0
- package/dist/chunk-EOO6PYGF.js +162 -0
- package/dist/chunk-EOO6PYGF.js.map +1 -0
- package/dist/chunk-ETCWQLJV.js +85 -0
- package/dist/chunk-ETCWQLJV.js.map +1 -0
- package/dist/chunk-FDZNYD4C.js +138 -0
- package/dist/chunk-FDZNYD4C.js.map +1 -0
- package/dist/chunk-FR76ZJQC.js +90 -0
- package/dist/chunk-FR76ZJQC.js.map +1 -0
- package/dist/chunk-G3J7JDP6.js +29 -0
- package/dist/chunk-G3J7JDP6.js.map +1 -0
- package/dist/chunk-GS2QD3N7.js +14 -0
- package/dist/chunk-GS2QD3N7.js.map +1 -0
- package/dist/chunk-HGV7J4T6.js +86 -0
- package/dist/chunk-HGV7J4T6.js.map +1 -0
- package/dist/chunk-HLDRH23D.js +84 -0
- package/dist/chunk-HLDRH23D.js.map +1 -0
- package/dist/chunk-HQZYKJ56.js +302 -0
- package/dist/chunk-HQZYKJ56.js.map +1 -0
- package/dist/chunk-HZPHCSCS.js +95 -0
- package/dist/chunk-HZPHCSCS.js.map +1 -0
- package/dist/chunk-IKS72C6P.js +53 -0
- package/dist/chunk-IKS72C6P.js.map +1 -0
- package/dist/chunk-IM3GKYOG.js +202 -0
- package/dist/chunk-IM3GKYOG.js.map +1 -0
- package/dist/{chunk-JRKSK2HX.js → chunk-INI4WDWC.js} +53 -7
- package/dist/chunk-INI4WDWC.js.map +1 -0
- package/dist/chunk-IQSRHXZR.js +164 -0
- package/dist/chunk-IQSRHXZR.js.map +1 -0
- package/dist/{chunk-M7JBER5U.js → chunk-JOR5LGYE.js} +18 -4
- package/dist/chunk-JOR5LGYE.js.map +1 -0
- package/dist/chunk-K4AT3V3J.js +545 -0
- package/dist/chunk-K4AT3V3J.js.map +1 -0
- package/dist/chunk-KCS6NJ3O.js +82 -0
- package/dist/chunk-KCS6NJ3O.js.map +1 -0
- package/dist/{chunk-SRSJO5QE.js → chunk-KH4KEMSP.js} +34 -3
- package/dist/chunk-KH4KEMSP.js.map +1 -0
- package/dist/chunk-LL45C7BB.js +122 -0
- package/dist/chunk-LL45C7BB.js.map +1 -0
- package/dist/chunk-LOSRXZAH.js +100 -0
- package/dist/chunk-LOSRXZAH.js.map +1 -0
- package/dist/{chunk-7KEUJIIC.js → chunk-MF6ZUUJZ.js} +93 -11
- package/dist/chunk-MF6ZUUJZ.js.map +1 -0
- package/dist/chunk-MHMWQ7RB.js +19 -0
- package/dist/chunk-MHMWQ7RB.js.map +1 -0
- package/dist/chunk-MJ5RU6XT.js +173 -0
- package/dist/chunk-MJ5RU6XT.js.map +1 -0
- package/dist/chunk-MXNIVEVL.js +48 -0
- package/dist/chunk-MXNIVEVL.js.map +1 -0
- package/dist/{chunk-IJBSZ4NX.js → chunk-NS2TBYM4.js} +8 -5
- package/dist/chunk-NS2TBYM4.js.map +1 -0
- package/dist/chunk-PIVY32AE.js +140 -0
- package/dist/chunk-PIVY32AE.js.map +1 -0
- package/dist/chunk-PYBBYSZ3.js +526 -0
- 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-QH5KOFNS.js +39 -0
- package/dist/chunk-QH5KOFNS.js.map +1 -0
- package/dist/chunk-QIG4IGUP.js +139 -0
- package/dist/chunk-QIG4IGUP.js.map +1 -0
- package/dist/chunk-QLA25V2K.js +493 -0
- package/dist/chunk-QLA25V2K.js.map +1 -0
- package/dist/chunk-QROYYGWU.js +52 -0
- package/dist/chunk-QROYYGWU.js.map +1 -0
- package/dist/chunk-QYMJHS4K.js +36 -0
- package/dist/chunk-QYMJHS4K.js.map +1 -0
- package/dist/chunk-RHD7RKGI.js +176 -0
- package/dist/chunk-RHD7RKGI.js.map +1 -0
- package/dist/chunk-S2XULQEK.js +52 -0
- package/dist/chunk-S2XULQEK.js.map +1 -0
- package/dist/{chunk-LPOMRC4C.js → chunk-S3ECMF23.js} +11 -3
- package/dist/chunk-S3ECMF23.js.map +1 -0
- package/dist/chunk-S4Y3YTAI.js +35 -0
- package/dist/chunk-S4Y3YTAI.js.map +1 -0
- package/dist/chunk-S4ZGTDSG.js +18 -0
- package/dist/chunk-S4ZGTDSG.js.map +1 -0
- package/dist/{chunk-PSKN344N.js → chunk-T4WJ5VGP.js} +63 -12
- package/dist/chunk-T4WJ5VGP.js.map +1 -0
- package/dist/chunk-TDMHA7C3.js +251 -0
- package/dist/chunk-TDMHA7C3.js.map +1 -0
- package/dist/{chunk-Y45PPBDV.js → chunk-TGLUDQP2.js} +47 -14
- package/dist/chunk-TGLUDQP2.js.map +1 -0
- package/dist/chunk-TLMMFQWM.js +222 -0
- package/dist/chunk-TLMMFQWM.js.map +1 -0
- package/dist/chunk-U6QYTMDB.js +85 -0
- package/dist/chunk-U6QYTMDB.js.map +1 -0
- package/dist/chunk-UMKV7YVQ.js +20 -0
- package/dist/chunk-UMKV7YVQ.js.map +1 -0
- package/dist/chunk-VFYO3UGZ.js +45 -0
- package/dist/chunk-VFYO3UGZ.js.map +1 -0
- package/dist/chunk-W4IK7AWD.js +73 -0
- package/dist/chunk-W4IK7AWD.js.map +1 -0
- package/dist/chunk-WVS6CUCT.js +48 -0
- package/dist/chunk-WVS6CUCT.js.map +1 -0
- package/dist/chunk-Y6KT7ZTR.js +109 -0
- package/dist/chunk-Y6KT7ZTR.js.map +1 -0
- package/dist/chunk-YMUIQRWK.js +44 -0
- package/dist/chunk-YMUIQRWK.js.map +1 -0
- package/dist/chunk-YOA5W5ZH.js +807 -0
- package/dist/chunk-YOA5W5ZH.js.map +1 -0
- package/dist/chunk-YQ2AC4E2.js +59 -0
- package/dist/chunk-YQ2AC4E2.js.map +1 -0
- package/dist/chunk-ZKKO4MHC.js +25 -0
- package/dist/chunk-ZKKO4MHC.js.map +1 -0
- package/dist/chunk-ZNROKDSZ.js +54 -0
- package/dist/chunk-ZNROKDSZ.js.map +1 -0
- package/dist/collapsible-panel.d.ts +96 -17
- package/dist/collapsible-panel.js +104 -27
- package/dist/collapsible-panel.js.map +1 -1
- package/dist/column-track.d.ts +41 -0
- package/dist/column-track.js +4 -0
- package/dist/column-track.js.map +1 -0
- package/dist/combobox.d.ts +40 -0
- package/dist/combobox.js +3 -0
- package/dist/combobox.js.map +1 -0
- package/dist/composer.d.ts +62 -0
- package/dist/composer.js +5 -0
- package/dist/composer.js.map +1 -0
- package/dist/content-item.d.ts +116 -0
- package/dist/content-item.js +4 -0
- package/dist/content-item.js.map +1 -0
- package/dist/content-swap.d.ts +34 -0
- package/dist/content-swap.js +3 -0
- package/dist/content-swap.js.map +1 -0
- package/dist/corner-status.d.ts +36 -0
- package/dist/corner-status.js +3 -0
- package/dist/corner-status.js.map +1 -0
- package/dist/css-values.d.ts +127 -0
- package/dist/css-values.js +3 -0
- package/dist/css-values.js.map +1 -0
- package/dist/device-class.js +1 -1
- package/dist/device-class.js.map +1 -1
- package/dist/disclosure-arrow.d.ts +6 -1
- package/dist/disclosure-arrow.js +2 -1
- package/dist/edge-activity-indicator.d.ts +28 -0
- package/dist/edge-activity-indicator.js +3 -0
- package/dist/edge-activity-indicator.js.map +1 -0
- package/dist/empty-state.d.ts +5 -2
- package/dist/empty-state.js +2 -1
- package/dist/extension-attributes-mu8MFUl0.d.ts +13 -0
- package/dist/file-picker.d.ts +47 -0
- package/dist/file-picker.js +3 -0
- package/dist/file-picker.js.map +1 -0
- package/dist/flex-alignment-4ms8ZbV8.d.ts +5 -0
- package/dist/floating-toolbar.d.ts +29 -6
- package/dist/floating-toolbar.js +1 -1
- package/dist/framed-list.d.ts +47 -0
- package/dist/framed-list.js +4 -0
- package/dist/framed-list.js.map +1 -0
- package/dist/grid.d.ts +58 -0
- package/dist/grid.js +6 -0
- package/dist/grid.js.map +1 -0
- package/dist/help-tags-register.d.ts +13 -0
- package/dist/help-tags-register.js +9 -0
- package/dist/help-tags-register.js.map +1 -0
- package/dist/icon-button.d.ts +46 -0
- package/dist/icon-button.js +6 -0
- package/dist/icon-button.js.map +1 -0
- package/dist/icon-toggle-button.d.ts +42 -0
- package/dist/icon-toggle-button.js +6 -0
- package/dist/icon-toggle-button.js.map +1 -0
- package/dist/index.d.ts +59 -20
- package/dist/index.js +66 -29
- package/dist/list-action-row.d.ts +69 -12
- package/dist/list-action-row.js +5 -1
- package/dist/list-header.d.ts +36 -2
- package/dist/list-header.js +4 -1
- package/dist/list-icon-tile-B8dr97OW.d.ts +18 -0
- package/dist/list-inset-control.d.ts +10 -4
- package/dist/list-inset-control.js +1 -1
- package/dist/list-inset-text.d.ts +17 -12
- package/dist/list-inset-text.js +2 -1
- package/dist/list-item.d.ts +49 -5
- package/dist/list-item.js +4 -1
- package/dist/list.d.ts +121 -12
- package/dist/list.js +5 -1
- package/dist/loading-spinner.d.ts +12 -2
- package/dist/loading-spinner.js +2 -1
- package/dist/lucide-icon.d.ts +15 -3
- package/dist/lucide-icon.js +2 -1
- package/dist/nav-stack-VXTumzKa.d.ts +158 -0
- package/dist/nav-stack.d.ts +15 -38
- package/dist/nav-stack.js +13 -1
- package/dist/pane.d.ts +111 -7
- package/dist/pane.js +2 -1
- package/dist/panel-toolbar-vg6Ir5Dy.d.ts +77 -0
- package/dist/popup-menu-register.d.ts +10 -0
- package/dist/popup-menu-register.js +336 -0
- package/dist/popup-menu-register.js.map +1 -0
- package/dist/popup-menu.d.ts +125 -0
- package/dist/popup-menu.js +3 -0
- package/dist/popup-menu.js.map +1 -0
- package/dist/resizable-region.d.ts +32 -3
- package/dist/resizable-region.js +1 -1
- package/dist/row.d.ts +53 -0
- package/dist/row.js +7 -0
- package/dist/row.js.map +1 -0
- package/dist/scroll-clearance-BikHljTc.d.ts +14 -0
- package/dist/segmented-control.d.ts +7 -4
- package/dist/segmented-control.js +1 -2
- package/dist/select-register.js +255 -3
- package/dist/select-register.js.map +1 -1
- package/dist/select.d.ts +95 -6
- package/dist/select.js +4 -1
- package/dist/semantic-content-BbzjvSu9.d.ts +14 -0
- package/dist/semantic-tone-CdW013KF.d.ts +4 -0
- package/dist/sides-BPSWde0A.d.ts +4 -0
- package/dist/skeleton.d.ts +14 -7
- package/dist/skeleton.js +2 -1
- package/dist/spacer.d.ts +18 -0
- package/dist/spacer.js +4 -0
- package/dist/spacer.js.map +1 -0
- package/dist/split-button.d.ts +57 -0
- package/dist/split-button.js +7 -0
- package/dist/split-button.js.map +1 -0
- package/dist/split-view.d.ts +63 -6
- package/dist/split-view.js +88 -9
- package/dist/split-view.js.map +1 -1
- package/dist/state-banner.d.ts +37 -6
- package/dist/state-banner.js +4 -1
- package/dist/styles/anchored-surface.css +41 -0
- package/dist/styles/app-tab.css +232 -0
- package/dist/styles/badge.css +145 -0
- package/dist/styles/catalog/components/catalog-example-stack.css +14 -0
- package/dist/styles/catalog/components/catalog-example.css +204 -0
- package/dist/styles/catalog/components/catalog-stage.css +61 -0
- package/dist/styles/catalog/components/catalog.css +26 -0
- package/dist/styles/catalog.css +5 -575
- package/dist/styles/chip.css +208 -0
- package/dist/styles/choice-chips.css +154 -0
- package/dist/styles/collapsible-panel.css +450 -16
- package/dist/styles/column-track.css +57 -0
- package/dist/styles/combobox.css +113 -0
- package/dist/styles/components/actions/floating-toolbar/floating-toolbar.css +119 -0
- package/dist/styles/components/actions/icon-button/icon-button.css +22 -0
- package/dist/styles/components/actions/icon-toggle-button/icon-toggle-button.css +83 -0
- package/dist/styles/components/actions/popup-menu/popup-menu.css +370 -0
- package/dist/styles/components/actions/split-button/split-button.css +95 -0
- package/dist/styles/components/actions/toolbar/toolbar.css +443 -0
- package/dist/styles/components/actions/toolbar-control-group/toolbar-control-group.css +1069 -0
- package/dist/styles/components/actions/toolbar-text/toolbar-text.css +269 -0
- package/dist/styles/components/collections/framed-list/framed-list.css +47 -0
- package/dist/styles/components/collections/list/list.css +250 -0
- package/dist/styles/components/collections/list-action-row/list-action-row.css +466 -0
- package/dist/styles/components/collections/list-header/list-header.css +229 -0
- package/dist/styles/components/collections/list-inset-control/list-inset-control.css +30 -0
- package/dist/styles/components/collections/list-inset-text/list-inset-text.css +29 -0
- package/dist/styles/components/collections/list-item/list-item.css +461 -0
- package/dist/styles/components/collections/summary-row/summary-row.css +161 -0
- package/dist/styles/components/data-display/value-table/value-table.css +183 -0
- package/dist/styles/components/feedback/anchored-surface/anchored-surface.css +41 -0
- package/dist/styles/components/feedback/badge/badge.css +145 -0
- package/dist/styles/components/feedback/chip/chip.css +208 -0
- package/dist/styles/components/feedback/corner-status/corner-status.css +60 -0
- package/dist/styles/components/feedback/edge-activity-indicator/edge-activity-indicator.css +127 -0
- package/dist/styles/components/feedback/empty-state/empty-state.css +47 -0
- package/dist/styles/components/feedback/loading-spinner/loading-spinner.css +25 -0
- package/dist/styles/components/feedback/skeleton/skeleton.css +92 -0
- package/dist/styles/components/feedback/state-banner/state-banner.css +297 -0
- package/dist/styles/components/forms/choice-chips/choice-chips.css +154 -0
- package/dist/styles/components/forms/combobox/combobox.css +113 -0
- package/dist/styles/components/forms/composer/composer.css +100 -0
- package/dist/styles/components/forms/file-picker/file-picker.css +81 -0
- package/dist/styles/components/forms/segmented-control/segmented-control.css +196 -0
- package/dist/styles/components/forms/select/select.css +455 -0
- package/dist/styles/components/forms/token-search-field/token-search-field.css +432 -0
- package/dist/styles/components/layout/collapsible-panel/collapsible-panel.css +580 -0
- package/dist/styles/components/layout/column-track/column-track.css +57 -0
- package/dist/styles/components/layout/content-swap/content-swap.css +154 -0
- package/dist/styles/components/layout/grid/grid.css +88 -0
- package/dist/styles/components/layout/pane/pane.css +382 -0
- package/dist/styles/components/layout/resizable-region/resizable-region.css +545 -0
- package/dist/styles/components/layout/row/row.css +114 -0
- package/dist/styles/components/layout/spacer/spacer.css +11 -0
- package/dist/styles/components/layout/split-view/split-view.css +146 -0
- package/dist/styles/components/layout/workbench/workbench.css +1086 -0
- package/dist/styles/components/media/lucide-icon/lucide-icon.css +15 -0
- package/dist/styles/components/navigation/app-tab/app-tab.css +384 -0
- package/dist/styles/components/navigation/disclosure-arrow/disclosure-arrow.css +23 -0
- package/dist/styles/components/navigation/nav-stack/nav-stack.css +342 -0
- package/dist/styles/components/navigation/tab-bar/tab-bar.css +197 -0
- package/dist/styles/components/navigation/tab-navigator/tab-navigator.css +325 -0
- package/dist/styles/components/surfaces/content-item/content-item.css +138 -0
- package/dist/styles/components/surfaces/sunken-panel/sunken-panel.css +31 -0
- package/dist/styles/components/surfaces/surface-scaffold/surface-scaffold.css +425 -0
- package/dist/styles/components/typography/text/text.css +220 -0
- package/dist/styles/composer.css +100 -0
- package/dist/styles/content-item.css +138 -0
- package/dist/styles/content-swap.css +154 -0
- package/dist/styles/corner-status.css +60 -0
- package/dist/styles/document.css +98 -0
- package/dist/styles/edge-activity-indicator.css +127 -0
- package/dist/styles/file-picker.css +81 -0
- package/dist/styles/floating-toolbar.css +87 -6
- package/dist/styles/foundation.css +254 -53
- package/dist/styles/framed-list.css +47 -0
- package/dist/styles/grid.css +88 -0
- package/dist/styles/icon-button.css +22 -0
- package/dist/styles/icon-toggle-button.css +83 -0
- package/dist/styles/layout.css +1 -21
- package/dist/styles/list-action-row.css +323 -17
- package/dist/styles/list-header.css +128 -59
- package/dist/styles/list-inset-control.css +25 -2
- package/dist/styles/list-inset-text.css +27 -12
- package/dist/styles/list-item.css +371 -33
- package/dist/styles/list.css +214 -7
- package/dist/styles/loading-spinner.css +4 -0
- package/dist/styles/lucide-icon.css +9 -0
- package/dist/styles/nav-stack.css +280 -49
- package/dist/styles/pane.css +322 -2
- package/dist/styles/popup-menu.css +370 -0
- package/dist/styles/resizable-region.css +438 -4
- package/dist/styles/row.css +114 -0
- package/dist/styles/segmented-control.css +34 -3
- package/dist/styles/select.css +338 -23
- package/dist/styles/skeleton.css +64 -3
- package/dist/styles/spacer.css +11 -0
- package/dist/styles/split-button.css +95 -0
- package/dist/styles/split-view.css +114 -3
- package/dist/styles/state-banner.css +141 -42
- package/dist/styles/styles.css +22 -1
- package/dist/styles/summary-row.css +161 -0
- package/dist/styles/sunken-panel.css +13 -4
- package/dist/styles/surface-scaffold.css +425 -0
- package/dist/styles/tab-bar.css +161 -9
- package/dist/styles/tab-navigator.css +325 -0
- package/dist/styles/text.css +220 -0
- package/dist/styles/token-search-field.css +202 -59
- package/dist/styles/toolbar-control-group.css +886 -42
- package/dist/styles/toolbar-text.css +184 -7
- package/dist/styles/toolbar.css +363 -17
- package/dist/styles/value-table.css +115 -7
- package/dist/styles/webawesome.css +282 -20
- package/dist/styles/workbench.css +999 -7
- package/dist/summary-row.d.ts +56 -0
- package/dist/summary-row.js +4 -0
- package/dist/summary-row.js.map +1 -0
- package/dist/sunken-panel.d.ts +23 -4
- package/dist/sunken-panel.js +2 -1
- package/dist/surface-scaffold-register.d.ts +9 -0
- package/dist/surface-scaffold-register.js +151 -0
- package/dist/surface-scaffold-register.js.map +1 -0
- package/dist/surface-scaffold.d.ts +170 -0
- package/dist/surface-scaffold.js +4 -0
- package/dist/surface-scaffold.js.map +1 -0
- package/dist/tab-bar.d.ts +31 -6
- package/dist/tab-bar.js +1 -1
- package/dist/tab-navigator.d.ts +114 -0
- package/dist/tab-navigator.js +102 -0
- package/dist/tab-navigator.js.map +1 -0
- package/dist/text.d.ts +98 -0
- package/dist/text.js +3 -0
- package/dist/text.js.map +1 -0
- package/dist/token-search-field.d.ts +3 -59
- package/dist/token-search-field.js +2 -1
- package/dist/token-search-model-aYR0YAei.d.ts +184 -0
- package/dist/token-search-model.d.ts +3 -0
- package/dist/token-search-model.js +3 -0
- package/dist/token-search-model.js.map +1 -0
- package/dist/toolbar-control-group.d.ts +87 -4
- package/dist/toolbar-control-group.js +3 -1
- package/dist/toolbar-text.d.ts +46 -12
- package/dist/toolbar-text.js +2 -1
- package/dist/toolbar.d.ts +71 -9
- package/dist/toolbar.js +1 -1
- package/dist/types-CVgIwSdq.d.ts +189 -0
- package/dist/value-table.d.ts +30 -4
- package/dist/value-table.js +2 -1
- package/dist/webawesome.d.ts +8 -1
- package/dist/wire-catalog.d.ts +88 -13
- package/dist/wire-catalog.js +240 -206
- package/dist/wire-catalog.js.map +1 -1
- package/dist/wire-comboboxes.d.ts +26 -0
- package/dist/wire-comboboxes.js +3 -0
- package/dist/wire-comboboxes.js.map +1 -0
- 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-content-items.d.ts +11 -0
- package/dist/wire-content-items.js +139 -0
- package/dist/wire-content-items.js.map +1 -0
- package/dist/wire-corner-status.d.ts +11 -0
- package/dist/wire-corner-status.js +3 -0
- package/dist/wire-corner-status.js.map +1 -0
- package/dist/wire-file-pickers.d.ts +26 -0
- package/dist/wire-file-pickers.js +3 -0
- package/dist/wire-file-pickers.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-list-reorder.d.ts +41 -0
- package/dist/wire-list-reorder.js +3 -0
- package/dist/wire-list-reorder.js.map +1 -0
- package/dist/wire-nav-stack.d.ts +4 -1
- package/dist/wire-nav-stack.js +312 -16
- package/dist/wire-nav-stack.js.map +1 -1
- package/dist/wire-resizable-regions.d.ts +2 -1
- package/dist/wire-resizable-regions.js +2 -1
- package/dist/wire-scroll-dividers.d.ts +87 -0
- package/dist/wire-scroll-dividers.js +3 -0
- package/dist/wire-scroll-dividers.js.map +1 -0
- package/dist/wire-sidebar.d.ts +24 -4
- package/dist/wire-sidebar.js +122 -19
- package/dist/wire-sidebar.js.map +1 -1
- package/dist/wire-tab-bars.d.ts +1 -0
- package/dist/wire-tab-bars.js +1 -1
- package/dist/wire-tab-navigator.d.ts +13 -0
- package/dist/wire-tab-navigator.js +153 -0
- package/dist/wire-tab-navigator.js.map +1 -0
- package/dist/wire-token-search-fields.d.ts +5 -1
- package/dist/wire-token-search-fields.js +2 -1
- package/dist/wire-toolbar-visibility.d.ts +13 -0
- package/dist/wire-toolbar-visibility.js +3 -0
- package/dist/wire-toolbar-visibility.js.map +1 -0
- package/dist/wire-workbench.d.ts +116 -0
- package/dist/wire-workbench.js +6 -0
- package/dist/wire-workbench.js.map +1 -0
- package/dist/workbench.d.ts +253 -10
- package/dist/workbench.js +20 -62
- package/dist/workbench.js.map +1 -1
- package/docs/accessibility.md +617 -32
- package/docs/anchored-surface.md +101 -0
- package/docs/app-layouts.md +159 -10
- package/docs/apple-hig.md +2 -0
- package/docs/catalog.md +403 -110
- package/docs/collapsible-panel.md +272 -16
- package/docs/component-contract.md +700 -79
- package/docs/component-integration.md +75 -0
- package/docs/component-selection.md +908 -79
- package/docs/composer.md +143 -0
- package/docs/design/templates/choice-chips/color-palette-dark.svg +1 -0
- package/docs/design/templates/choice-chips/color-palette.svg +1 -0
- package/docs/design/templates/choice-chips/compact-icons-dark.svg +1 -0
- package/docs/design/templates/choice-chips/compact-icons.svg +1 -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 -0
- package/docs/design/templates/choice-chips.svg +1 -0
- 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/stack-dark.svg +1 -1
- package/docs/design/templates/list/stack.svg +1 -1
- package/docs/design/templates/list-action-row/default-dark.svg +1 -1
- package/docs/design/templates/list-action-row/default.svg +1 -1
- package/docs/design/templates/list-action-row/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 -0
- package/docs/design/templates/list-action-row/trailing-menu.svg +1 -0
- 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/action-dark.svg +1 -1
- package/docs/design/templates/list-header/action.svg +1 -1
- package/docs/design/templates/list-header/count-dark.svg +1 -1
- package/docs/design/templates/list-header/count.svg +1 -1
- 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/size-l-dark.svg +1 -0
- package/docs/design/templates/list-header/size-l.svg +1 -0
- package/docs/design/templates/list-header/size-m-dark.svg +1 -0
- package/docs/design/templates/list-header/size-m.svg +1 -0
- package/docs/design/templates/list-header/size-xl-dark.svg +1 -0
- package/docs/design/templates/list-header/size-xl.svg +1 -0
- package/docs/design/templates/list-header/size-xs-dark.svg +1 -0
- package/docs/design/templates/list-header/size-xs.svg +1 -0
- package/docs/design/templates/list-header/toggle-collapsed-dark.svg +1 -1
- package/docs/design/templates/list-header/toggle-collapsed.svg +1 -1
- package/docs/design/templates/list-header/toggle-expanded-dark.svg +1 -1
- package/docs/design/templates/list-header/toggle-expanded.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/default-dark.svg +1 -1
- package/docs/design/templates/list-item/default.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/multiline-dark.svg +1 -1
- package/docs/design/templates/list-item/multiline.svg +1 -1
- package/docs/design/templates/list-item/selected-dark.svg +1 -1
- package/docs/design/templates/list-item/selected.svg +1 -1
- package/docs/design/templates/list-item/trailing-dark.svg +1 -1
- package/docs/design/templates/list-item/trailing.svg +1 -1
- package/docs/design/templates/list-item-dark.svg +1 -1
- package/docs/design/templates/list-item.svg +1 -1
- package/docs/design/templates/list.svg +1 -1
- package/docs/design/templates/segmented-control/equal-dark.svg +1 -1
- package/docs/design/templates/segmented-control/equal.svg +1 -1
- package/docs/design/templates/segmented-control/pill-small-dark.svg +1 -1
- package/docs/design/templates/segmented-control/pill-small.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 -0
- package/docs/design/templates/state-banner/strip.svg +1 -0
- 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/workspace-tabs-dark.svg +1 -1
- package/docs/design/templates/tab-bar/workspace-tabs.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/empty-dark.svg +1 -1
- package/docs/design/templates/token-search-field/empty.svg +1 -1
- package/docs/design/templates/token-search-field/tokens-dark.svg +1 -1
- package/docs/design/templates/token-search-field/tokens.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/title-and-actions-dark.svg +1 -1
- package/docs/design/templates/toolbar/title-and-actions.svg +1 -1
- package/docs/design/templates/toolbar/width-visibility-320-dark.svg +1 -0
- package/docs/design/templates/toolbar/width-visibility-320.svg +1 -0
- package/docs/design/templates/toolbar/width-visibility-600-dark.svg +1 -0
- package/docs/design/templates/toolbar/width-visibility-600.svg +1 -0
- package/docs/design/templates/toolbar-control-group/borderless-single-dark.svg +1 -1
- package/docs/design/templates/toolbar-control-group/borderless-single.svg +1 -1
- package/docs/design/templates/toolbar-control-group/icon-buttons-dark.svg +1 -1
- package/docs/design/templates/toolbar-control-group/icon-buttons.svg +1 -1
- package/docs/design/templates/toolbar-control-group/icon-tile-brand-dark.svg +1 -0
- package/docs/design/templates/toolbar-control-group/icon-tile-brand.svg +1 -0
- package/docs/design/templates/toolbar-control-group/icon-tile-danger-dark.svg +1 -0
- package/docs/design/templates/toolbar-control-group/icon-tile-danger.svg +1 -0
- package/docs/design/templates/toolbar-control-group/icon-tile-neutral-dark.svg +1 -0
- package/docs/design/templates/toolbar-control-group/icon-tile-neutral.svg +1 -0
- package/docs/design/templates/toolbar-control-group/icon-tile-success-dark.svg +1 -0
- package/docs/design/templates/toolbar-control-group/icon-tile-success.svg +1 -0
- package/docs/design/templates/toolbar-control-group/icon-tile-warning-dark.svg +1 -0
- package/docs/design/templates/toolbar-control-group/icon-tile-warning.svg +1 -0
- package/docs/design/templates/toolbar-control-group/push-buttons-dark.svg +1 -1
- package/docs/design/templates/toolbar-control-group/push-buttons.svg +1 -1
- package/docs/design/templates/toolbar-control-group/with-text-dark.svg +1 -1
- package/docs/design/templates/toolbar-control-group/with-text.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/dark-tone-dark.svg +1 -0
- package/docs/design/templates/toolbar-text/dark-tone.svg +1 -0
- 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/small-dark.svg +1 -1
- package/docs/design/templates/toolbar-text/small.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/templates.md +19 -9
- package/docs/design-philosophy.md +58 -11
- package/docs/device-class.md +2 -1
- package/docs/document-baseline.md +93 -0
- package/docs/examples/application-ui-profile.json +8 -8
- package/docs/examples/command-palette-adapter.tsx +9 -7
- package/docs/examples/component-catalog-extension.json +8 -1
- package/docs/examples/{component-catalog-extension-v2.json → component-composition-extension.json} +8 -9
- package/docs/layout.md +764 -40
- package/docs/nav-stack.md +275 -11
- package/docs/recipes.md +125 -69
- package/docs/split-view.md +94 -5
- package/docs/sunken-panel.md +54 -1
- package/docs/surface-scaffold.md +270 -0
- package/docs/tab-navigator.md +255 -0
- package/docs/type-contracts.md +57 -22
- package/docs/ui-analyzer.md +334 -14
- package/docs/ui-doctor.md +207 -10
- package/docs/ui-evaluator.md +17 -0
- package/docs/ux-demo.md +47 -16
- package/docs/webawesome-theme.md +148 -14
- package/docs/workbench.md +600 -11
- package/docs/writing.md +152 -0
- package/doctor/cli.mjs +5 -0
- package/doctor/component-style-checks.mjs +457 -0
- package/doctor/config.schema.json +26 -0
- package/doctor/copy-capitalization-checks.mjs +591 -0
- package/doctor/eslint-diagnostics.d.mts +16 -0
- package/doctor/eslint-diagnostics.mjs +17 -0
- package/doctor/index.d.mts +6 -0
- package/doctor/index.mjs +214 -47
- package/evaluator/index.mjs +73 -8
- package/llms.txt +702 -56
- package/package.json +256 -25
- package/scripts/component-catalog-extension-validation.mjs +28 -0
- package/traversal-exclusions.mjs +60 -1
- package/ux-demo/recipes/app-shell.tsx +181 -189
- package/ux-demo/recipes/collapsible-sidebar.tsx +177 -139
- package/ux-demo/recipes/compact-toolbar.tsx +120 -101
- package/ux-demo/recipes/composer-form.tsx +86 -62
- package/ux-demo/recipes/list-detail-dialog.tsx +219 -114
- package/ux-demo/recipes/list-workspace-states.tsx +95 -80
- package/ux-demo/recipes/loaders.ts +5 -2
- package/ux-demo/recipes/loading-inspector.tsx +157 -117
- package/ux-demo/recipes/mount-recipe.ts +0 -5
- package/ux-demo/recipes/navigation-sidebar.tsx +98 -83
- package/ux-demo/recipes/navigation-stack.tsx +42 -29
- package/ux-demo/recipes/types.ts +14 -1
- package/ux-demo/recipes/workspace-header.tsx +77 -51
- package/vite-ux-review.d.mts +83 -0
- package/vite-ux-review.mjs +120 -0
- package/ai/component-catalog-extension-v2.d.ts +0 -7
- package/ai/component-catalog-extension-v2.schema.json +0 -7
- package/ai/component-catalog-v2.d.ts +0 -67
- package/ai/component-catalog-v2.json +0 -4675
- package/ai/component-catalog-v2.schema.json +0 -246
- package/dist/browser/panel-header.js +0 -7
- package/dist/chunk-5VTB2BKP.js +0 -35
- package/dist/chunk-5VTB2BKP.js.map +0 -1
- package/dist/chunk-6WTETUZ6.js +0 -21
- package/dist/chunk-6WTETUZ6.js.map +0 -1
- package/dist/chunk-7KEUJIIC.js.map +0 -1
- package/dist/chunk-7XRDUG4Y.js +0 -58
- package/dist/chunk-7XRDUG4Y.js.map +0 -1
- package/dist/chunk-B3A3IVO5.js +0 -83
- package/dist/chunk-B3A3IVO5.js.map +0 -1
- package/dist/chunk-DPDBVKNA.js +0 -113
- package/dist/chunk-DPDBVKNA.js.map +0 -1
- package/dist/chunk-FEAMR7S4.js +0 -68
- package/dist/chunk-FEAMR7S4.js.map +0 -1
- package/dist/chunk-FEY65TBV.js +0 -15
- package/dist/chunk-FEY65TBV.js.map +0 -1
- package/dist/chunk-IJBSZ4NX.js.map +0 -1
- package/dist/chunk-J5BFYY7Q.js +0 -35
- package/dist/chunk-J5BFYY7Q.js.map +0 -1
- package/dist/chunk-JRKSK2HX.js.map +0 -1
- package/dist/chunk-KWWR5VMS.js.map +0 -1
- package/dist/chunk-LPOMRC4C.js.map +0 -1
- package/dist/chunk-M7JBER5U.js.map +0 -1
- package/dist/chunk-NENBUYX5.js +0 -79
- package/dist/chunk-NENBUYX5.js.map +0 -1
- package/dist/chunk-NWSEP5RT.js +0 -117
- package/dist/chunk-NWSEP5RT.js.map +0 -1
- package/dist/chunk-OJWWHFH3.js +0 -426
- package/dist/chunk-OJWWHFH3.js.map +0 -1
- package/dist/chunk-PSKN344N.js.map +0 -1
- package/dist/chunk-PX3YSS5C.js.map +0 -1
- package/dist/chunk-QO55FN2Y.js.map +0 -1
- package/dist/chunk-R7ZWUN64.js +0 -38
- package/dist/chunk-R7ZWUN64.js.map +0 -1
- package/dist/chunk-RKVQEH4J.js.map +0 -1
- package/dist/chunk-SRSJO5QE.js.map +0 -1
- package/dist/chunk-SYDMYBPG.js +0 -31
- package/dist/chunk-SYDMYBPG.js.map +0 -1
- package/dist/chunk-TB6DY7H5.js +0 -25
- package/dist/chunk-TB6DY7H5.js.map +0 -1
- package/dist/chunk-UFZKLBCF.js +0 -108
- package/dist/chunk-UFZKLBCF.js.map +0 -1
- package/dist/chunk-UKKSMZYT.js +0 -281
- package/dist/chunk-UKKSMZYT.js.map +0 -1
- package/dist/chunk-W2FK7ZNY.js +0 -26
- package/dist/chunk-W2FK7ZNY.js.map +0 -1
- package/dist/chunk-W5L2JSBY.js +0 -40
- package/dist/chunk-W5L2JSBY.js.map +0 -1
- package/dist/chunk-Y45PPBDV.js.map +0 -1
- package/dist/chunk-Y6LYKFXY.js.map +0 -1
- package/dist/divider-sides-267FA7sY.d.ts +0 -4
- package/dist/panel-header.d.ts +0 -42
- package/dist/panel-header.js +0 -7
- package/dist/panel-header.js.map +0 -1
- package/dist/styles/panel-header.css +0 -64
- package/dist/styles/tab-scaffold.css +0 -84
- package/dist/tab-scaffold.d.ts +0 -30
- package/dist/tab-scaffold.js +0 -51
- package/dist/tab-scaffold.js.map +0 -1
- package/dist/wire-tab-scaffold.d.ts +0 -11
- package/dist/wire-tab-scaffold.js +0 -21
- package/dist/wire-tab-scaffold.js.map +0 -1
- package/docs/design/templates/panel-header/icon-actions-dark.svg +0 -1
- package/docs/design/templates/panel-header/icon-actions.svg +0 -1
- package/docs/design/templates/panel-header/icon-summary-actions-dark.svg +0 -1
- package/docs/design/templates/panel-header/icon-summary-actions.svg +0 -1
- package/docs/design/templates/panel-header/no-icon-dark.svg +0 -1
- package/docs/design/templates/panel-header/no-icon.svg +0 -1
- package/docs/design/templates/panel-header/page-heading-dark.svg +0 -1
- package/docs/design/templates/panel-header/page-heading.svg +0 -1
- package/docs/design/templates/panel-header/title-only-dark.svg +0 -1
- package/docs/design/templates/panel-header/title-only.svg +0 -1
- package/docs/design/templates/panel-header-dark.svg +0 -1
- package/docs/design/templates/panel-header.svg +0 -1
- package/docs/tab-scaffold.md +0 -50
- package/ux-demo/recipes/recipes.css +0 -448
package/analyzer/index.mjs
CHANGED
|
@@ -13,7 +13,26 @@ import postcss from 'postcss';
|
|
|
13
13
|
import ts from 'typescript';
|
|
14
14
|
|
|
15
15
|
import { loadApplicationUiProfile } from '../ai/application-ui-profile.mjs';
|
|
16
|
-
import {
|
|
16
|
+
import { createUiTraversalFilter } from '../traversal-exclusions.mjs';
|
|
17
|
+
import {
|
|
18
|
+
classOwnerFor,
|
|
19
|
+
componentLabel,
|
|
20
|
+
componentName,
|
|
21
|
+
componentOwnershipFacts,
|
|
22
|
+
contextualComponents,
|
|
23
|
+
configurationFor,
|
|
24
|
+
ownsClassBlock,
|
|
25
|
+
privateVariableOwner,
|
|
26
|
+
reportGap,
|
|
27
|
+
restyledComponents,
|
|
28
|
+
typedPropForToken,
|
|
29
|
+
} from './component-ownership.mjs';
|
|
30
|
+
import { collectLoudDeclarations, inspectLoudPairs } from './loud-pairs.mjs';
|
|
31
|
+
import {
|
|
32
|
+
classNames,
|
|
33
|
+
complexSelectorParts,
|
|
34
|
+
withoutRelationalArguments,
|
|
35
|
+
} from './selectors.mjs';
|
|
17
36
|
|
|
18
37
|
export const UI_ANALYSIS_SCHEMA_VERSION = 1;
|
|
19
38
|
|
|
@@ -22,7 +41,10 @@ export const UI_ANALYSIS_RULES = Object.freeze({
|
|
|
22
41
|
'KUI-L002': { severity: 'error', title: 'Unknown Kerf token' },
|
|
23
42
|
'KUI-L003': { severity: 'error', title: 'Competing geometry owners' },
|
|
24
43
|
'KUI-L004': { severity: 'review', title: 'Repeated content inset' },
|
|
25
|
-
|
|
44
|
+
// KUI-L005 ('Forced component dimension') is retired: KUI-L019 reports any
|
|
45
|
+
// restyle of a cataloged component, dimensions included, and L005's only
|
|
46
|
+
// other shape (a public class as an ancestor) sizes an application element.
|
|
47
|
+
// The id stays in the diagnostic registry so existing exceptions still load.
|
|
26
48
|
'KUI-L006': {
|
|
27
49
|
severity: 'review',
|
|
28
50
|
title: 'Hard-coded spacing outside the approved scale',
|
|
@@ -30,17 +52,84 @@ export const UI_ANALYSIS_RULES = Object.freeze({
|
|
|
30
52
|
'KUI-L007': { severity: 'error', title: 'Nested scroll owners' },
|
|
31
53
|
'KUI-L008': { severity: 'review', title: 'Dynamic class requires review' },
|
|
32
54
|
'KUI-L009': { severity: 'error', title: 'Stylesheet could not be parsed' },
|
|
55
|
+
'KUI-L010': {
|
|
56
|
+
severity: 'error',
|
|
57
|
+
title: 'Private Kerf descendant override',
|
|
58
|
+
},
|
|
59
|
+
'KUI-L011': { severity: 'error', title: 'Uncataloged shadow part override' },
|
|
60
|
+
'KUI-L012': { severity: 'error', title: 'Private Kerf token assignment' },
|
|
61
|
+
'KUI-L013': { severity: 'error', title: 'Uncataloged CSS value literal' },
|
|
62
|
+
'KUI-L014': { severity: 'error', title: 'Wrong CSS value helper dimension' },
|
|
63
|
+
'KUI-L015': { severity: 'error', title: 'Non-standalone CSS expression' },
|
|
64
|
+
'KUI-L016': { severity: 'error', title: 'Forbidden declaration-list escape' },
|
|
65
|
+
'KUI-L017': { severity: 'review', title: 'Exceptional spacing shorthand' },
|
|
66
|
+
'KUI-L018': {
|
|
67
|
+
severity: 'review',
|
|
68
|
+
title: 'Loud fill override without its on-loud pair',
|
|
69
|
+
},
|
|
70
|
+
'KUI-L019': {
|
|
71
|
+
severity: 'error',
|
|
72
|
+
title: 'Application CSS restyles a cataloged component',
|
|
73
|
+
},
|
|
74
|
+
'KUI-L020': {
|
|
75
|
+
severity: 'error',
|
|
76
|
+
title: "Another component's private variable",
|
|
77
|
+
},
|
|
78
|
+
'KUI-L021': {
|
|
79
|
+
severity: 'error',
|
|
80
|
+
title: 'Component token overridden where a typed prop exists',
|
|
81
|
+
},
|
|
82
|
+
'KUI-L022': {
|
|
83
|
+
severity: 'error',
|
|
84
|
+
title: "Hook class restyles a cataloged component's root",
|
|
85
|
+
},
|
|
86
|
+
'KUI-L023': {
|
|
87
|
+
severity: 'error',
|
|
88
|
+
title: "Another component's public class is rendered as markup",
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const ownershipAction =
|
|
93
|
+
"Configure the component through its typed props, variants, or public tokens; style only your own elements (in the component's context when needed). If no configuration covers the need, report the component gap to its package instead of overriding it.";
|
|
94
|
+
|
|
95
|
+
/** The doctor's per-rule repair action, where it is more specific than the default. */
|
|
96
|
+
export const UI_ANALYSIS_ACTIONS = Object.freeze({
|
|
97
|
+
'KUI-L019': ownershipAction,
|
|
98
|
+
'KUI-L020': ownershipAction,
|
|
99
|
+
'KUI-L021':
|
|
100
|
+
'Set the typed prop the diagnostic names on the component instead of overriding its token.',
|
|
101
|
+
'KUI-L022': ownershipAction,
|
|
102
|
+
'KUI-L023': ownershipAction,
|
|
33
103
|
});
|
|
34
104
|
|
|
35
|
-
const
|
|
105
|
+
const adoptionRules = new Set([
|
|
106
|
+
'KUI-L001',
|
|
107
|
+
'KUI-L002',
|
|
108
|
+
'KUI-L010',
|
|
109
|
+
'KUI-L011',
|
|
110
|
+
'KUI-L012',
|
|
111
|
+
'KUI-L019',
|
|
112
|
+
'KUI-L020',
|
|
113
|
+
'KUI-L021',
|
|
114
|
+
'KUI-L022',
|
|
115
|
+
'KUI-L023',
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
const sourceExtensions = new Set(['.js', '.jsx', '.mjs', '.ts', '.tsx']);
|
|
119
|
+
function isTestModule(file) {
|
|
120
|
+
const path = file.replaceAll('\\', '/');
|
|
121
|
+
return (
|
|
122
|
+
/(?:^|\/)__tests__\//.test(path) ||
|
|
123
|
+
/\.(?:test|spec)\.(?:js|jsx|mjs|ts|tsx)$/.test(path)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
36
126
|
const spacingProperties = /^(?:margin|padding|gap|inset)(?:-|$)/;
|
|
37
|
-
const dimensionProperties =
|
|
38
|
-
/^(?:width|height|min-width|max-width|min-height|max-height)$/;
|
|
39
127
|
const approvedSpacing = new Set([0, 4, 8, 16, 24]);
|
|
40
128
|
async function collectFiles(root, paths) {
|
|
41
129
|
const files = [];
|
|
130
|
+
const excluded = await createUiTraversalFilter(root);
|
|
42
131
|
const visit = async (path) => {
|
|
43
|
-
if (
|
|
132
|
+
if (excluded(path)) return;
|
|
44
133
|
const details = await stat(path);
|
|
45
134
|
if (details.isFile()) {
|
|
46
135
|
if (sourceExtensions.has(extname(path)) || path.endsWith('.css'))
|
|
@@ -51,6 +140,7 @@ async function collectFiles(root, paths) {
|
|
|
51
140
|
for (const entry of entries) {
|
|
52
141
|
const child = resolve(path, entry.name);
|
|
53
142
|
if (entry.isDirectory()) await visit(child);
|
|
143
|
+
else if (excluded(child)) continue;
|
|
54
144
|
else if (
|
|
55
145
|
sourceExtensions.has(extname(entry.name)) ||
|
|
56
146
|
entry.name.endsWith('.css')
|
|
@@ -80,11 +170,11 @@ function location(file, node, source) {
|
|
|
80
170
|
};
|
|
81
171
|
}
|
|
82
172
|
|
|
83
|
-
function diagnostic(ruleId, at, message, evidence, chain) {
|
|
173
|
+
function diagnostic(ruleId, at, message, evidence, chain, adoption = false) {
|
|
84
174
|
const rule = UI_ANALYSIS_RULES[ruleId];
|
|
85
175
|
return {
|
|
86
176
|
ruleId,
|
|
87
|
-
severity: rule.severity,
|
|
177
|
+
severity: adoption && adoptionRules.has(ruleId) ? 'review' : rule.severity,
|
|
88
178
|
message,
|
|
89
179
|
location: at,
|
|
90
180
|
evidence,
|
|
@@ -119,7 +209,13 @@ function relativeStyleImports(file, source) {
|
|
|
119
209
|
});
|
|
120
210
|
return imports;
|
|
121
211
|
}
|
|
122
|
-
const syntax = file.endsWith('.tsx')
|
|
212
|
+
const syntax = file.endsWith('.tsx')
|
|
213
|
+
? ts.ScriptKind.TSX
|
|
214
|
+
: file.endsWith('.jsx')
|
|
215
|
+
? ts.ScriptKind.JSX
|
|
216
|
+
: file.endsWith('.ts')
|
|
217
|
+
? ts.ScriptKind.TS
|
|
218
|
+
: ts.ScriptKind.JS;
|
|
123
219
|
const module = ts.createSourceFile(
|
|
124
220
|
file,
|
|
125
221
|
source,
|
|
@@ -147,6 +243,7 @@ function reachableStyleFacts(file, imports, styleFacts) {
|
|
|
147
243
|
const current = result.get(className) ?? { scroll: false, inset: false };
|
|
148
244
|
current.scroll ||= facts.scroll;
|
|
149
245
|
current.inset ||= facts.inset;
|
|
246
|
+
current.styled ??= facts.styled;
|
|
150
247
|
result.set(className, current);
|
|
151
248
|
}
|
|
152
249
|
}
|
|
@@ -164,7 +261,7 @@ function reachableStyleFiles(file, imports, styleFacts) {
|
|
|
164
261
|
return visited;
|
|
165
262
|
}
|
|
166
263
|
|
|
167
|
-
async function loadCatalogs(profileResult) {
|
|
264
|
+
async function loadCatalogs(profileResult, ownership) {
|
|
168
265
|
const entries = [];
|
|
169
266
|
for (const catalog of profileResult.profile?.catalogs ?? []) {
|
|
170
267
|
const owner = profileResult.provenance[`$catalogs.${catalog.package}`];
|
|
@@ -176,7 +273,51 @@ async function loadCatalogs(profileResult) {
|
|
|
176
273
|
'utf8',
|
|
177
274
|
),
|
|
178
275
|
);
|
|
179
|
-
|
|
276
|
+
const selection = await loadSelectionFacts(owner, catalog);
|
|
277
|
+
const joined = new Set();
|
|
278
|
+
for (const entry of artifact.entries ?? []) {
|
|
279
|
+
const selected = selection.get(entry.key);
|
|
280
|
+
joined.add(entry.key);
|
|
281
|
+
const componentSource =
|
|
282
|
+
selected?.componentSource ??
|
|
283
|
+
(/\.[cm]?[jt]sx?$/.test(entry.source) ? entry.source : undefined);
|
|
284
|
+
entries.push(
|
|
285
|
+
selected
|
|
286
|
+
? {
|
|
287
|
+
...entry,
|
|
288
|
+
publicExports: entry.publicExports ?? selected.publicExports,
|
|
289
|
+
moduleImports: selected.moduleImports,
|
|
290
|
+
componentSource,
|
|
291
|
+
styleSources: entry.styleSources ?? selected.styleSources,
|
|
292
|
+
catalogDirectory: dirname(owner),
|
|
293
|
+
}
|
|
294
|
+
: { ...entry, componentSource, catalogDirectory: dirname(owner) },
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
for (const [key, selected] of selection) {
|
|
298
|
+
if (
|
|
299
|
+
ownership !== 'component' ||
|
|
300
|
+
joined.has(key) ||
|
|
301
|
+
!selected.componentSource
|
|
302
|
+
)
|
|
303
|
+
continue;
|
|
304
|
+
entries.push({
|
|
305
|
+
key,
|
|
306
|
+
package: catalog.package,
|
|
307
|
+
id: selected.id,
|
|
308
|
+
name: selected.name,
|
|
309
|
+
kind: selected.kind,
|
|
310
|
+
componentSource: selected.componentSource,
|
|
311
|
+
styleSources: selected.styleSources,
|
|
312
|
+
moduleImports: selected.moduleImports,
|
|
313
|
+
publicExports: selected.publicExports,
|
|
314
|
+
boundaries: {
|
|
315
|
+
publicClasses: selected.publicClasses ?? [],
|
|
316
|
+
rootElement: selected.rootElement,
|
|
317
|
+
},
|
|
318
|
+
catalogDirectory: dirname(owner),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
180
321
|
} catch {
|
|
181
322
|
// Profile validation reports the precise catalog load failure.
|
|
182
323
|
}
|
|
@@ -184,25 +325,342 @@ async function loadCatalogs(profileResult) {
|
|
|
184
325
|
return entries;
|
|
185
326
|
}
|
|
186
327
|
|
|
328
|
+
// The composition (v2) catalog carries no import facts, and several of its
|
|
329
|
+
// entries share a display name (the Kerf `Select` and the Web Awesome
|
|
330
|
+
// `wa-select` are both "Select"). The selection (v1) catalog is where each
|
|
331
|
+
// entry's real public exports and import subpaths live, so join it by key.
|
|
332
|
+
async function loadSelectionFacts(owner, catalog) {
|
|
333
|
+
const facts = new Map();
|
|
334
|
+
if (!catalog.selection?.path) return facts;
|
|
335
|
+
let artifact;
|
|
336
|
+
try {
|
|
337
|
+
artifact = JSON.parse(
|
|
338
|
+
await readFile(resolve(dirname(owner), catalog.selection.path), 'utf8'),
|
|
339
|
+
);
|
|
340
|
+
} catch {
|
|
341
|
+
return facts;
|
|
342
|
+
}
|
|
343
|
+
const packageName = artifact.package ?? catalog.package;
|
|
344
|
+
for (const entry of artifact.entries ?? []) {
|
|
345
|
+
const delivery = entry.delivery ?? {};
|
|
346
|
+
facts.set(`${packageName}:${entry.id}`, {
|
|
347
|
+
id: entry.id,
|
|
348
|
+
name: entry.name,
|
|
349
|
+
kind: entry.kind,
|
|
350
|
+
publicExports: entry.publicExports,
|
|
351
|
+
publicClasses: entry.publicClasses,
|
|
352
|
+
rootElement: entry.rootElement,
|
|
353
|
+
componentSource: entry.source,
|
|
354
|
+
styleSources: entry.styleSources,
|
|
355
|
+
moduleImports: [delivery.browserImport, delivery.moduleImport].filter(
|
|
356
|
+
Boolean,
|
|
357
|
+
),
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
return facts;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// A catalog entry's public exports mix the component render function with
|
|
364
|
+
// helpers its props consume (`Select` ships `uiColor`, `List` ships
|
|
365
|
+
// `px`/`rem`/`flex`). Only the render function stands for the entry, so only
|
|
366
|
+
// it may carry the entry's contracts. A component export is one whose name
|
|
367
|
+
// starts with an uppercase letter: JSX itself treats a lowercase tag as an
|
|
368
|
+
// intrinsic element, so a lowercase export can never be a component tag, and
|
|
369
|
+
// scripts/check-component-catalog.mjs enforces the converse for this package
|
|
370
|
+
// (every uppercase runtime export returns SafeHtml; no lowercase one does).
|
|
371
|
+
// eslint-plugin-kerfjs applies the same rule.
|
|
372
|
+
function isComponentExport(name) {
|
|
373
|
+
return /^[A-Z]/.test(name);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Pick the catalog entry an imported (or namespace-accessed) export name
|
|
377
|
+
// refers to. Names are not unique across a catalog, so candidates are ranked:
|
|
378
|
+
// an entry whose own import subpath is the module wins, then an entry that
|
|
379
|
+
// declares the name as a public export, then catalog order. A helper export
|
|
380
|
+
// never resolves to an entry.
|
|
381
|
+
function resolveExportEntry(
|
|
382
|
+
candidates,
|
|
383
|
+
name,
|
|
384
|
+
module,
|
|
385
|
+
file,
|
|
386
|
+
packageDirectory,
|
|
387
|
+
aliasSource,
|
|
388
|
+
) {
|
|
389
|
+
if (!isComponentExport(name)) return undefined;
|
|
390
|
+
const localSource =
|
|
391
|
+
packageDirectory && (module?.startsWith('.') || aliasSource)
|
|
392
|
+
? relative(
|
|
393
|
+
packageDirectory,
|
|
394
|
+
aliasSource ?? resolve(dirname(file), module),
|
|
395
|
+
)
|
|
396
|
+
.replaceAll('\\', '/')
|
|
397
|
+
.replace(/\.[cm]?[jt]sx?$/, '')
|
|
398
|
+
: undefined;
|
|
399
|
+
const eligible = (candidates ?? []).filter(
|
|
400
|
+
(entry) =>
|
|
401
|
+
module === entry.package ||
|
|
402
|
+
module?.startsWith(`${entry.package}/`) ||
|
|
403
|
+
(localSource &&
|
|
404
|
+
entry.componentSource?.replace(/\.[cm]?[jt]sx?$/, '') === localSource),
|
|
405
|
+
);
|
|
406
|
+
const exported = (entry) =>
|
|
407
|
+
(entry.publicExports ?? []).some(
|
|
408
|
+
(item) => (typeof item === 'string' ? item : item.name) === name,
|
|
409
|
+
);
|
|
410
|
+
return (
|
|
411
|
+
eligible.find((entry) => entry.moduleImports?.includes(module)) ??
|
|
412
|
+
eligible.find(exported) ??
|
|
413
|
+
eligible[0]
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Use the nearest project tsconfig without walking into a parent repository.
|
|
418
|
+
// TypeScript expands `extends` and resolves exact and wildcard `paths` here.
|
|
419
|
+
function compilerOptionsFor(file, root, cache) {
|
|
420
|
+
let directory = dirname(file);
|
|
421
|
+
const visited = [];
|
|
422
|
+
while (directory === root || !relative(root, directory).startsWith('..')) {
|
|
423
|
+
if (cache.has(directory)) break;
|
|
424
|
+
visited.push(directory);
|
|
425
|
+
const configPath = resolve(directory, 'tsconfig.json');
|
|
426
|
+
if (ts.sys.fileExists(configPath)) {
|
|
427
|
+
const config = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
428
|
+
cache.set(
|
|
429
|
+
directory,
|
|
430
|
+
config.error
|
|
431
|
+
? undefined
|
|
432
|
+
: ts.parseJsonConfigFileContent(
|
|
433
|
+
config.config,
|
|
434
|
+
ts.sys,
|
|
435
|
+
directory,
|
|
436
|
+
undefined,
|
|
437
|
+
configPath,
|
|
438
|
+
).options,
|
|
439
|
+
);
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
if (directory === root) break;
|
|
443
|
+
directory = dirname(directory);
|
|
444
|
+
}
|
|
445
|
+
const options = cache.get(directory);
|
|
446
|
+
for (const path of visited) cache.set(path, options);
|
|
447
|
+
return options;
|
|
448
|
+
}
|
|
449
|
+
|
|
187
450
|
function catalogFacts(entries) {
|
|
188
451
|
const publicClasses = new Set();
|
|
189
452
|
const publicTokens = new Set();
|
|
453
|
+
const publicParts = new Map();
|
|
190
454
|
const classEntries = new Map();
|
|
455
|
+
const blockEntries = new Map();
|
|
191
456
|
const exportEntries = new Map();
|
|
457
|
+
const addExport = (name, entry) => {
|
|
458
|
+
const candidates = exportEntries.get(name) ?? [];
|
|
459
|
+
if (!candidates.includes(entry)) candidates.push(entry);
|
|
460
|
+
exportEntries.set(name, candidates);
|
|
461
|
+
};
|
|
192
462
|
for (const entry of entries) {
|
|
193
|
-
|
|
194
|
-
for (const item of entry.publicExports ?? [])
|
|
195
|
-
|
|
196
|
-
|
|
463
|
+
addExport(entry.name, entry);
|
|
464
|
+
for (const item of entry.publicExports ?? []) {
|
|
465
|
+
const name = typeof item === 'string' ? item : item.name;
|
|
466
|
+
if (isComponentExport(name)) addExport(name, entry);
|
|
467
|
+
}
|
|
468
|
+
for (const className of new Set(
|
|
469
|
+
[
|
|
470
|
+
entry.boundaries?.rootClass,
|
|
471
|
+
...(entry.boundaries?.publicClasses ?? []),
|
|
472
|
+
].filter(Boolean),
|
|
473
|
+
)) {
|
|
197
474
|
publicClasses.add(className);
|
|
198
475
|
const owners = classEntries.get(className) ?? [];
|
|
199
476
|
owners.push(entry);
|
|
200
477
|
classEntries.set(className, owners);
|
|
478
|
+
const block = bemBlock(className);
|
|
479
|
+
const blockOwners = blockEntries.get(block) ?? [];
|
|
480
|
+
if (!blockOwners.includes(entry)) blockOwners.push(entry);
|
|
481
|
+
blockEntries.set(block, blockOwners);
|
|
201
482
|
}
|
|
202
483
|
for (const token of entry.boundaries?.publicTokens ?? [])
|
|
203
484
|
publicTokens.add(token);
|
|
485
|
+
for (const part of entry.boundaries?.publicParts ?? []) {
|
|
486
|
+
const roots = publicParts.get(part) ?? new Set();
|
|
487
|
+
if (entry.boundaries?.rootClass) roots.add(entry.boundaries.rootClass);
|
|
488
|
+
publicParts.set(part, roots);
|
|
489
|
+
}
|
|
204
490
|
}
|
|
205
|
-
return {
|
|
491
|
+
return {
|
|
492
|
+
entries,
|
|
493
|
+
publicClasses,
|
|
494
|
+
publicTokens,
|
|
495
|
+
publicParts,
|
|
496
|
+
classEntries,
|
|
497
|
+
blockEntries,
|
|
498
|
+
exportEntries,
|
|
499
|
+
ownership: componentOwnershipFacts(entries),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const bemBlock = (name) => name.split(/__|--/, 1)[0];
|
|
504
|
+
|
|
505
|
+
function renderedLiteralClasses(file, source) {
|
|
506
|
+
const names = new Set();
|
|
507
|
+
const parsed = ts.createSourceFile(
|
|
508
|
+
file,
|
|
509
|
+
source,
|
|
510
|
+
ts.ScriptTarget.Latest,
|
|
511
|
+
true,
|
|
512
|
+
file.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.JSX,
|
|
513
|
+
);
|
|
514
|
+
const visit = (node) => {
|
|
515
|
+
if (
|
|
516
|
+
ts.isJsxAttribute(node) &&
|
|
517
|
+
['class', 'className'].includes(node.name.text)
|
|
518
|
+
) {
|
|
519
|
+
const value =
|
|
520
|
+
node.initializer && ts.isStringLiteral(node.initializer)
|
|
521
|
+
? node.initializer.text
|
|
522
|
+
: node.initializer &&
|
|
523
|
+
ts.isJsxExpression(node.initializer) &&
|
|
524
|
+
node.initializer.expression &&
|
|
525
|
+
ts.isStringLiteral(node.initializer.expression)
|
|
526
|
+
? node.initializer.expression.text
|
|
527
|
+
: undefined;
|
|
528
|
+
for (const name of value?.split(/\s+/) ?? []) if (name) names.add(name);
|
|
529
|
+
}
|
|
530
|
+
ts.forEachChild(node, visit);
|
|
531
|
+
};
|
|
532
|
+
visit(parsed);
|
|
533
|
+
return names;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function inferOwnedClasses(entries, contents, imports) {
|
|
537
|
+
return entries.map((entry) => {
|
|
538
|
+
if (!entry.componentSource || !entry.catalogDirectory) return entry;
|
|
539
|
+
const sourceFile = resolve(entry.catalogDirectory, entry.componentSource);
|
|
540
|
+
const source = contents.get(sourceFile);
|
|
541
|
+
if (!source) return entry;
|
|
542
|
+
const styleFiles = entry.styleSources?.length
|
|
543
|
+
? entry.styleSources.map((path) => resolve(entry.catalogDirectory, path))
|
|
544
|
+
: [...(imports.get(sourceFile) ?? [])];
|
|
545
|
+
const styleSources = styleFiles.map((file) =>
|
|
546
|
+
relative(entry.catalogDirectory, file).replaceAll('\\', '/'),
|
|
547
|
+
);
|
|
548
|
+
const styledBlocks = new Set();
|
|
549
|
+
for (const style of styleFiles) {
|
|
550
|
+
const css = contents.get(style);
|
|
551
|
+
if (!css) continue;
|
|
552
|
+
try {
|
|
553
|
+
postcss.parse(css, { from: style }).walkRules((rule) => {
|
|
554
|
+
for (const name of subjectClasses(rule.selector))
|
|
555
|
+
styledBlocks.add(bemBlock(name));
|
|
556
|
+
});
|
|
557
|
+
} catch {
|
|
558
|
+
// The stylesheet parser reports malformed CSS in the normal pass.
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const inferred = [...renderedLiteralClasses(sourceFile, source)].filter(
|
|
562
|
+
(name) => styledBlocks.has(bemBlock(name)),
|
|
563
|
+
);
|
|
564
|
+
if (!inferred.length) return { ...entry, styleSources };
|
|
565
|
+
return {
|
|
566
|
+
...entry,
|
|
567
|
+
styleSources,
|
|
568
|
+
boundaries: {
|
|
569
|
+
...entry.boundaries,
|
|
570
|
+
publicClasses: [
|
|
571
|
+
...new Set([...(entry.boundaries?.publicClasses ?? []), ...inferred]),
|
|
572
|
+
],
|
|
573
|
+
},
|
|
574
|
+
};
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async function ownershipGroupEntries(groups, root, contents) {
|
|
579
|
+
if (!groups?.length) return [];
|
|
580
|
+
const manifest = JSON.parse(
|
|
581
|
+
await readFile(resolve(root, 'package.json'), 'utf8'),
|
|
582
|
+
);
|
|
583
|
+
return Promise.all(
|
|
584
|
+
groups.map(async (group, index) => {
|
|
585
|
+
const classes = new Set();
|
|
586
|
+
for (const style of group.styleSources) {
|
|
587
|
+
const file = resolve(root, style);
|
|
588
|
+
let css = contents.get(file);
|
|
589
|
+
if (css === undefined)
|
|
590
|
+
try {
|
|
591
|
+
css = await readFile(file, 'utf8');
|
|
592
|
+
} catch {
|
|
593
|
+
// A missing configured file owns no classes.
|
|
594
|
+
}
|
|
595
|
+
if (!css) continue;
|
|
596
|
+
try {
|
|
597
|
+
postcss.parse(css, { from: file }).walkRules((rule) => {
|
|
598
|
+
for (const name of subjectClasses(rule.selector)) classes.add(name);
|
|
599
|
+
});
|
|
600
|
+
} catch {
|
|
601
|
+
// The normal stylesheet pass reports malformed CSS.
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return {
|
|
605
|
+
key: `${manifest.name}:ownership-group-${index + 1}`,
|
|
606
|
+
package: manifest.name,
|
|
607
|
+
id: `ownership-group-${index + 1}`,
|
|
608
|
+
name: `Ownership group ${index + 1}`,
|
|
609
|
+
catalogDirectory: root,
|
|
610
|
+
styleSources: group.styleSources,
|
|
611
|
+
ownershipGroupSources: group.sources,
|
|
612
|
+
boundaries: { publicClasses: [...classes] },
|
|
613
|
+
};
|
|
614
|
+
}),
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async function implicitOwnershipEntries(files, imports, root) {
|
|
619
|
+
const found = [];
|
|
620
|
+
const packages = new Map();
|
|
621
|
+
const packageAt = async (directory) => {
|
|
622
|
+
if (packages.has(directory)) return packages.get(directory);
|
|
623
|
+
let result;
|
|
624
|
+
try {
|
|
625
|
+
const manifest = JSON.parse(
|
|
626
|
+
await readFile(resolve(directory, 'package.json'), 'utf8'),
|
|
627
|
+
);
|
|
628
|
+
if (typeof manifest.name === 'string')
|
|
629
|
+
result = { name: manifest.name, directory };
|
|
630
|
+
} catch {
|
|
631
|
+
// Search the containing directory for a package manifest.
|
|
632
|
+
}
|
|
633
|
+
if (!result && directory !== root) {
|
|
634
|
+
const parent = dirname(directory);
|
|
635
|
+
if (!relative(root, parent).startsWith('..'))
|
|
636
|
+
result = await packageAt(parent);
|
|
637
|
+
}
|
|
638
|
+
packages.set(directory, result);
|
|
639
|
+
return result;
|
|
640
|
+
};
|
|
641
|
+
for (const file of files) {
|
|
642
|
+
if (!sourceExtensions.has(extname(file))) continue;
|
|
643
|
+
const styles = (imports.get(file) ?? []).filter((path) =>
|
|
644
|
+
files.includes(path),
|
|
645
|
+
);
|
|
646
|
+
if (!styles.length) continue;
|
|
647
|
+
const owner = await packageAt(dirname(file));
|
|
648
|
+
if (!owner) continue;
|
|
649
|
+
const source = relative(owner.directory, file).replaceAll('\\', '/');
|
|
650
|
+
const id = `module:${source}`;
|
|
651
|
+
found.push({
|
|
652
|
+
key: `${owner.name}:${id}`,
|
|
653
|
+
package: owner.name,
|
|
654
|
+
id,
|
|
655
|
+
name: basename(file).replace(/\.[^.]+$/, ''),
|
|
656
|
+
componentSource: source,
|
|
657
|
+
catalogDirectory: owner.directory,
|
|
658
|
+
styleSources: styles.map((style) =>
|
|
659
|
+
relative(owner.directory, style).replaceAll('\\', '/'),
|
|
660
|
+
),
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
return found;
|
|
206
664
|
}
|
|
207
665
|
|
|
208
666
|
function spacingValues(value) {
|
|
@@ -214,7 +672,180 @@ function spacingValues(value) {
|
|
|
214
672
|
return results;
|
|
215
673
|
}
|
|
216
674
|
|
|
217
|
-
|
|
675
|
+
function escapeRegExp(value) {
|
|
676
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// A declaration that restyles, as opposed to one that only sets a custom
|
|
680
|
+
// property: setting a component's public token on its host is configuration,
|
|
681
|
+
// which the token rules (KUI-L002, KUI-L012, KUI-L020, KUI-L021) judge.
|
|
682
|
+
function restylingDeclarations(rule) {
|
|
683
|
+
return (rule.nodes ?? []).filter(
|
|
684
|
+
(node) => node.type === 'decl' && !node.prop.startsWith('--'),
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function inKeyframes(rule) {
|
|
689
|
+
return rule.parent?.type === 'atrule' && /keyframes$/i.test(rule.parent.name);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// The classes a rule makes its subject (the rightmost compound of each complex
|
|
693
|
+
// selector, outside :has()/:not()).
|
|
694
|
+
function subjectClasses(selector) {
|
|
695
|
+
return complexSelectorParts(selector).flatMap((parts) => {
|
|
696
|
+
const subject = parts.at(-1)?.compound;
|
|
697
|
+
return subject ? classNames(withoutRelationalArguments(subject)) : [];
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function inspectComponentOwnership(
|
|
702
|
+
file,
|
|
703
|
+
rule,
|
|
704
|
+
facts,
|
|
705
|
+
diagnostics,
|
|
706
|
+
adoption,
|
|
707
|
+
isForeign,
|
|
708
|
+
ownershipEvidence,
|
|
709
|
+
ownershipContext,
|
|
710
|
+
) {
|
|
711
|
+
if (inKeyframes(rule)) return;
|
|
712
|
+
const restyling = restylingDeclarations(rule);
|
|
713
|
+
if (restyling.length === 0) return;
|
|
714
|
+
const properties = [...new Set(restyling.map((decl) => decl.prop))];
|
|
715
|
+
const restyled = restyledComponents(
|
|
716
|
+
rule.selector,
|
|
717
|
+
facts.ownership,
|
|
718
|
+
isForeign,
|
|
719
|
+
ownershipEvidence,
|
|
720
|
+
);
|
|
721
|
+
for (const { entry, coOwners, via, name } of restyled)
|
|
722
|
+
diagnostics.push(
|
|
723
|
+
diagnostic(
|
|
724
|
+
'KUI-L019',
|
|
725
|
+
location(file, rule),
|
|
726
|
+
coOwners
|
|
727
|
+
? `\`${name}\` makes a class co-owned by ${coOwners.map((owner) => componentLabel(owner)).join(', ')} the subject of an application rule (${properties.join(', ')}); those components own their styles. Configure an owning component or style your own element instead.`
|
|
728
|
+
: via === 'descendant'
|
|
729
|
+
? `\`${name}\` reaches an unclassed descendant inside ${componentLabel(entry)} (${properties.join(', ')}); components own their styles. Use a child combinator or an own class to target your element, or configure the child through ${configurationFor(entry)}. ${reportGap(entry)}`
|
|
730
|
+
: `\`${name}\` makes ${componentLabel(entry)} the subject of an application rule (${properties.join(', ')}); components own their styles. Configure it through ${configurationFor(entry)}. To place your own content in its context, style your own element (\`${name} > .your-element\`). ${reportGap(entry)}`,
|
|
731
|
+
{
|
|
732
|
+
selector: rule.selector,
|
|
733
|
+
...(coOwners
|
|
734
|
+
? { components: coOwners.map((owner) => owner.key) }
|
|
735
|
+
: { component: entry.key }),
|
|
736
|
+
via,
|
|
737
|
+
target: name,
|
|
738
|
+
properties,
|
|
739
|
+
},
|
|
740
|
+
undefined,
|
|
741
|
+
adoption,
|
|
742
|
+
),
|
|
743
|
+
);
|
|
744
|
+
if (ownershipContext === 'subject' || !ownershipEvidence?.ownPackage) return;
|
|
745
|
+
for (const { entry, name, position } of contextualComponents(
|
|
746
|
+
rule.selector,
|
|
747
|
+
facts.ownership,
|
|
748
|
+
isForeign,
|
|
749
|
+
ownershipEvidence.ownPackage,
|
|
750
|
+
ownershipContext === 'any-package',
|
|
751
|
+
)) {
|
|
752
|
+
if (position === 'subject') continue;
|
|
753
|
+
if (
|
|
754
|
+
restyled.some(
|
|
755
|
+
(item) => item.entry.key === entry.key && item.name === name,
|
|
756
|
+
)
|
|
757
|
+
)
|
|
758
|
+
continue;
|
|
759
|
+
diagnostics.push(
|
|
760
|
+
diagnostic(
|
|
761
|
+
'KUI-L019',
|
|
762
|
+
location(file, rule),
|
|
763
|
+
`\`${name}\` uses ${componentLabel(entry)} as ${position} selector context (${properties.join(', ')}); foreign component handles cannot key this stylesheet in this ownership mode. Configure the component through ${configurationFor(entry)}. ${reportGap(entry)}`,
|
|
764
|
+
{
|
|
765
|
+
selector: rule.selector,
|
|
766
|
+
component: entry.key,
|
|
767
|
+
via: 'context',
|
|
768
|
+
position,
|
|
769
|
+
target: name,
|
|
770
|
+
properties,
|
|
771
|
+
},
|
|
772
|
+
undefined,
|
|
773
|
+
adoption,
|
|
774
|
+
),
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function inspectVariableOwnership(
|
|
780
|
+
file,
|
|
781
|
+
decl,
|
|
782
|
+
facts,
|
|
783
|
+
diagnostics,
|
|
784
|
+
adoption,
|
|
785
|
+
isForeign,
|
|
786
|
+
) {
|
|
787
|
+
const at = location(file, decl);
|
|
788
|
+
const privates = new Set([
|
|
789
|
+
...(decl.prop.match(/--_[a-z0-9_-]+/gi) ?? []),
|
|
790
|
+
...(decl.value.match(/--_[a-z0-9_-]+/gi) ?? []),
|
|
791
|
+
]);
|
|
792
|
+
for (const variable of privates) {
|
|
793
|
+
const owner = privateVariableOwner(variable, facts.ownership);
|
|
794
|
+
if (owner === undefined || (owner && !isForeign(owner))) continue;
|
|
795
|
+
const written = decl.prop === variable;
|
|
796
|
+
diagnostics.push(
|
|
797
|
+
diagnostic(
|
|
798
|
+
'KUI-L020',
|
|
799
|
+
at,
|
|
800
|
+
`\`${variable}\` is ${owner ? `${componentName(owner)}'s` : "a Kerf component's"} private variable; ${written ? 'writing it overrides' : 'reading it couples to'} that component's implementation. Configure it through ${configurationFor(owner)}. ${reportGap(owner)}`,
|
|
801
|
+
{
|
|
802
|
+
property: decl.prop,
|
|
803
|
+
value: decl.value,
|
|
804
|
+
variable,
|
|
805
|
+
...(owner ? { component: owner.key } : {}),
|
|
806
|
+
access: written ? 'write' : 'read',
|
|
807
|
+
},
|
|
808
|
+
undefined,
|
|
809
|
+
adoption,
|
|
810
|
+
),
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
if (!decl.prop.startsWith('--')) return;
|
|
814
|
+
const typed = typedPropForToken(decl.prop, facts.ownership);
|
|
815
|
+
if (!typed || !isForeign(typed.entry)) return;
|
|
816
|
+
const contract = (typed.entry.cssValueProps ?? []).find(
|
|
817
|
+
({ path }) => path === typed.path,
|
|
818
|
+
);
|
|
819
|
+
const example = contract?.examples?.[0];
|
|
820
|
+
diagnostics.push(
|
|
821
|
+
diagnostic(
|
|
822
|
+
'KUI-L021',
|
|
823
|
+
at,
|
|
824
|
+
`\`${decl.prop}\` is what ${typed.entry.name}'s typed \`${typed.path}\` prop sets; set the prop on ${typed.entry.name}${example ? ` (\`<${typed.entry.name} ${example} />\`)` : ''} instead of overriding the token. ${reportGap(typed.entry)}`,
|
|
825
|
+
{
|
|
826
|
+
property: decl.prop,
|
|
827
|
+
value: decl.value,
|
|
828
|
+
component: typed.entry.key,
|
|
829
|
+
prop: typed.path,
|
|
830
|
+
},
|
|
831
|
+
undefined,
|
|
832
|
+
adoption,
|
|
833
|
+
),
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
async function inspectCss(
|
|
838
|
+
file,
|
|
839
|
+
source,
|
|
840
|
+
facts,
|
|
841
|
+
diagnostics,
|
|
842
|
+
cssFacts,
|
|
843
|
+
adoption = false,
|
|
844
|
+
siblingOnLoud = [],
|
|
845
|
+
isForeign = () => true,
|
|
846
|
+
ownershipEvidence,
|
|
847
|
+
ownershipContext = 'subject',
|
|
848
|
+
) {
|
|
218
849
|
let root;
|
|
219
850
|
try {
|
|
220
851
|
root = postcss.parse(source, { from: file });
|
|
@@ -228,37 +859,139 @@ async function inspectCss(file, source, facts, diagnostics, cssFacts) {
|
|
|
228
859
|
);
|
|
229
860
|
return;
|
|
230
861
|
}
|
|
862
|
+
inspectLoudPairs(
|
|
863
|
+
file,
|
|
864
|
+
root,
|
|
865
|
+
(decl, message, evidence) =>
|
|
866
|
+
diagnostics.push(
|
|
867
|
+
diagnostic('KUI-L018', location(file, decl), message, evidence),
|
|
868
|
+
),
|
|
869
|
+
siblingOnLoud,
|
|
870
|
+
);
|
|
231
871
|
root.walkRules((rule) => {
|
|
872
|
+
const subjects = subjectClasses(rule.selector);
|
|
873
|
+
inspectComponentOwnership(
|
|
874
|
+
file,
|
|
875
|
+
rule,
|
|
876
|
+
facts,
|
|
877
|
+
diagnostics,
|
|
878
|
+
adoption,
|
|
879
|
+
isForeign,
|
|
880
|
+
ownershipEvidence,
|
|
881
|
+
ownershipContext,
|
|
882
|
+
);
|
|
883
|
+
if (!inKeyframes(rule) && restylingDeclarations(rule).length > 0)
|
|
884
|
+
for (const className of subjects) {
|
|
885
|
+
const record = cssFacts.get(className) ?? {
|
|
886
|
+
scroll: false,
|
|
887
|
+
inset: false,
|
|
888
|
+
};
|
|
889
|
+
record.styled ??= {
|
|
890
|
+
stylesheet: file,
|
|
891
|
+
line: rule.source?.start?.line ?? 1,
|
|
892
|
+
selector: rule.selector,
|
|
893
|
+
};
|
|
894
|
+
cssFacts.set(className, record);
|
|
895
|
+
}
|
|
232
896
|
const classes = [
|
|
233
897
|
...rule.selector.matchAll(/\.([_a-zA-Z]+[_a-zA-Z0-9-]*)/g),
|
|
234
898
|
].map((match) => match[1]);
|
|
899
|
+
const publicRootPattern = [...facts.publicClasses]
|
|
900
|
+
.map(
|
|
901
|
+
(className) => `\\.${className.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
|
|
902
|
+
)
|
|
903
|
+
.join('|');
|
|
904
|
+
const privateDescendants = new Set();
|
|
905
|
+
if (publicRootPattern)
|
|
906
|
+
for (const selector of rule.selectors ?? [rule.selector]) {
|
|
907
|
+
const rootMatch = new RegExp(
|
|
908
|
+
`(?:${publicRootPattern})(?:[.#:[\\w-]|\\s)*(?:>|\\s)`,
|
|
909
|
+
).exec(selector);
|
|
910
|
+
if (!rootMatch) continue;
|
|
911
|
+
const descendant = selector.slice(
|
|
912
|
+
rootMatch.index + rootMatch[0].length,
|
|
913
|
+
);
|
|
914
|
+
for (const match of descendant.matchAll(/\.(kui-[\w-]+)/g))
|
|
915
|
+
if (!facts.publicClasses.has(match[1]))
|
|
916
|
+
privateDescendants.add(match[1]);
|
|
917
|
+
}
|
|
235
918
|
for (const className of classes) {
|
|
236
|
-
if (
|
|
919
|
+
if (privateDescendants.has(className))
|
|
920
|
+
diagnostics.push(
|
|
921
|
+
diagnostic(
|
|
922
|
+
'KUI-L010',
|
|
923
|
+
location(file, rule),
|
|
924
|
+
`.${className} is a private Kerf descendant; prefer the owning component's prop or variant, or catalog a deliberate public extension point.`,
|
|
925
|
+
{ selector: rule.selector, className },
|
|
926
|
+
undefined,
|
|
927
|
+
adoption,
|
|
928
|
+
),
|
|
929
|
+
);
|
|
930
|
+
else if (
|
|
931
|
+
className.startsWith('kui-') &&
|
|
932
|
+
!facts.publicClasses.has(className)
|
|
933
|
+
)
|
|
237
934
|
diagnostics.push(
|
|
238
935
|
diagnostic(
|
|
239
936
|
'KUI-L001',
|
|
240
937
|
location(file, rule),
|
|
241
|
-
`.${className} is not a cataloged public Kerf class.`,
|
|
938
|
+
`.${className} is not a cataloged public Kerf class; prefer a component prop or variant, or catalog the extension point.`,
|
|
242
939
|
{ selector: rule.selector, className },
|
|
940
|
+
undefined,
|
|
941
|
+
adoption,
|
|
942
|
+
),
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
for (const match of rule.selector.matchAll(/::part\(\s*([\w-]+)\s*\)/g)) {
|
|
946
|
+
const part = match[1];
|
|
947
|
+
const roots = facts.publicParts.get(part) ?? new Set();
|
|
948
|
+
const allowed = [...roots].some((className) =>
|
|
949
|
+
new RegExp(`\\.${escapeRegExp(className)}(?:[^\\w-]|$)`).test(
|
|
950
|
+
rule.selector.slice(0, match.index),
|
|
951
|
+
),
|
|
952
|
+
);
|
|
953
|
+
if (!allowed)
|
|
954
|
+
diagnostics.push(
|
|
955
|
+
diagnostic(
|
|
956
|
+
'KUI-L011',
|
|
957
|
+
location(file, rule),
|
|
958
|
+
`::part(${part}) is not a cataloged extension point; prefer the component's prop or variant, or add ${part} to boundaries.publicParts.`,
|
|
959
|
+
{ selector: rule.selector, part },
|
|
960
|
+
undefined,
|
|
961
|
+
adoption,
|
|
243
962
|
),
|
|
244
963
|
);
|
|
245
964
|
}
|
|
246
965
|
rule.walkDecls((decl) => {
|
|
966
|
+
inspectVariableOwnership(
|
|
967
|
+
file,
|
|
968
|
+
decl,
|
|
969
|
+
facts,
|
|
970
|
+
diagnostics,
|
|
971
|
+
adoption,
|
|
972
|
+
isForeign,
|
|
973
|
+
);
|
|
247
974
|
const at = location(file, decl);
|
|
248
975
|
const tokens = [
|
|
249
976
|
...(decl.prop.match(/--kui-[a-z0-9-]+/g) ?? []),
|
|
250
977
|
...(decl.value.match(/--kui-[a-z0-9-]+/g) ?? []),
|
|
251
978
|
];
|
|
252
979
|
for (const token of new Set(tokens))
|
|
253
|
-
if (!facts.publicTokens.has(token))
|
|
980
|
+
if (!facts.publicTokens.has(token)) {
|
|
981
|
+
const assignment = decl.prop === token;
|
|
254
982
|
diagnostics.push(
|
|
255
983
|
diagnostic(
|
|
256
|
-
'KUI-L002',
|
|
984
|
+
assignment ? 'KUI-L012' : 'KUI-L002',
|
|
257
985
|
at,
|
|
258
|
-
|
|
986
|
+
assignment
|
|
987
|
+
? `${token} has no public configuration contract; prefer the component's prop or variant, or catalog the token explicitly.`
|
|
988
|
+
: `${token} is not a cataloged public Kerf token; prefer a documented token or component configuration.`,
|
|
259
989
|
{ property: decl.prop, value: decl.value, token },
|
|
990
|
+
undefined,
|
|
991
|
+
adoption,
|
|
260
992
|
),
|
|
261
993
|
);
|
|
994
|
+
}
|
|
262
995
|
if (
|
|
263
996
|
spacingProperties.test(decl.prop) &&
|
|
264
997
|
!/var\(|calc\(|remify\(/.test(decl.value)
|
|
@@ -277,7 +1010,7 @@ async function inspectCss(file, source, facts, diagnostics, cssFacts) {
|
|
|
277
1010
|
},
|
|
278
1011
|
),
|
|
279
1012
|
);
|
|
280
|
-
for (const className of
|
|
1013
|
+
for (const className of subjects) {
|
|
281
1014
|
const record = cssFacts.get(className) ?? {
|
|
282
1015
|
scroll: false,
|
|
283
1016
|
inset: false,
|
|
@@ -290,22 +1023,6 @@ async function inspectCss(file, source, facts, diagnostics, cssFacts) {
|
|
|
290
1023
|
if (/^padding(?:-|$)/.test(decl.prop) && decl.value !== '0')
|
|
291
1024
|
record.inset = true;
|
|
292
1025
|
cssFacts.set(className, record);
|
|
293
|
-
if (
|
|
294
|
-
facts.publicClasses.has(className) &&
|
|
295
|
-
dimensionProperties.test(decl.prop)
|
|
296
|
-
)
|
|
297
|
-
diagnostics.push(
|
|
298
|
-
diagnostic(
|
|
299
|
-
'KUI-L005',
|
|
300
|
-
at,
|
|
301
|
-
`.${className} forces ${decl.prop}; prefer the component's public sizing contract.`,
|
|
302
|
-
{
|
|
303
|
-
selector: rule.selector,
|
|
304
|
-
property: decl.prop,
|
|
305
|
-
value: decl.value,
|
|
306
|
-
},
|
|
307
|
-
),
|
|
308
|
-
);
|
|
309
1026
|
}
|
|
310
1027
|
});
|
|
311
1028
|
});
|
|
@@ -328,7 +1045,480 @@ function literalClasses(attribute) {
|
|
|
328
1045
|
return { values: [], dynamic: true };
|
|
329
1046
|
}
|
|
330
1047
|
|
|
331
|
-
function
|
|
1048
|
+
function literalClassValues(expression) {
|
|
1049
|
+
if (!expression) return [];
|
|
1050
|
+
if (ts.isStringLiteralLike(expression))
|
|
1051
|
+
return expression.text.split(/\s+/).filter(Boolean);
|
|
1052
|
+
if (ts.isParenthesizedExpression(expression))
|
|
1053
|
+
return literalClassValues(expression.expression);
|
|
1054
|
+
if (ts.isConditionalExpression(expression))
|
|
1055
|
+
return [
|
|
1056
|
+
...literalClassValues(expression.whenTrue),
|
|
1057
|
+
...literalClassValues(expression.whenFalse),
|
|
1058
|
+
];
|
|
1059
|
+
return [];
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function jsxOwnershipEvidence(style, facts, contents, root, compilerCache) {
|
|
1063
|
+
const hooks = new Map();
|
|
1064
|
+
const composedChildren = new Map();
|
|
1065
|
+
const composedDescendants = new Map();
|
|
1066
|
+
const ambiguousHooks = new Set();
|
|
1067
|
+
const ambiguousChildren = new Set();
|
|
1068
|
+
const ambiguousDescendants = new Set();
|
|
1069
|
+
const record = (map, ambiguous, key, entry) => {
|
|
1070
|
+
if (ambiguous.has(key)) return;
|
|
1071
|
+
if (map.has(key) && map.get(key).key !== entry.key) {
|
|
1072
|
+
map.delete(key);
|
|
1073
|
+
ambiguous.add(key);
|
|
1074
|
+
} else map.set(key, entry);
|
|
1075
|
+
};
|
|
1076
|
+
for (const owner of facts.entries) {
|
|
1077
|
+
if (!owner.componentSource || !owner.catalogDirectory) continue;
|
|
1078
|
+
const relativeStyle = relative(owner.catalogDirectory, style).replaceAll(
|
|
1079
|
+
'\\',
|
|
1080
|
+
'/',
|
|
1081
|
+
);
|
|
1082
|
+
if (!owner.styleSources?.includes(relativeStyle)) continue;
|
|
1083
|
+
const file = resolve(owner.catalogDirectory, owner.componentSource);
|
|
1084
|
+
const sourceText = contents.get(file);
|
|
1085
|
+
if (!sourceText) continue;
|
|
1086
|
+
const source = ts.createSourceFile(
|
|
1087
|
+
file,
|
|
1088
|
+
sourceText,
|
|
1089
|
+
ts.ScriptTarget.Latest,
|
|
1090
|
+
true,
|
|
1091
|
+
ts.ScriptKind.TSX,
|
|
1092
|
+
);
|
|
1093
|
+
const localComponents = new Map();
|
|
1094
|
+
for (const statement of source.statements) {
|
|
1095
|
+
if (
|
|
1096
|
+
ts.isFunctionDeclaration(statement) &&
|
|
1097
|
+
statement.name &&
|
|
1098
|
+
statement.body
|
|
1099
|
+
)
|
|
1100
|
+
localComponents.set(statement.name.text, statement.body);
|
|
1101
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
1102
|
+
for (const declaration of statement.declarationList.declarations)
|
|
1103
|
+
if (
|
|
1104
|
+
ts.isIdentifier(declaration.name) &&
|
|
1105
|
+
declaration.initializer &&
|
|
1106
|
+
(ts.isArrowFunction(declaration.initializer) ||
|
|
1107
|
+
ts.isFunctionExpression(declaration.initializer))
|
|
1108
|
+
)
|
|
1109
|
+
localComponents.set(
|
|
1110
|
+
declaration.name.text,
|
|
1111
|
+
declaration.initializer.body,
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
const imports = new Map();
|
|
1115
|
+
const namespaces = new Map();
|
|
1116
|
+
const compilerOptions = compilerOptionsFor(file, root, compilerCache);
|
|
1117
|
+
const aliasSource = (module) =>
|
|
1118
|
+
module && !module.startsWith('.') && compilerOptions?.paths
|
|
1119
|
+
? ts.resolveModuleName(module, file, compilerOptions, ts.sys)
|
|
1120
|
+
.resolvedModule?.resolvedFileName
|
|
1121
|
+
: undefined;
|
|
1122
|
+
for (const statement of source.statements) {
|
|
1123
|
+
if (
|
|
1124
|
+
!ts.isImportDeclaration(statement) ||
|
|
1125
|
+
!ts.isStringLiteral(statement.moduleSpecifier)
|
|
1126
|
+
)
|
|
1127
|
+
continue;
|
|
1128
|
+
const module = statement.moduleSpecifier.text;
|
|
1129
|
+
const bindings = statement.importClause?.namedBindings;
|
|
1130
|
+
if (bindings && ts.isNamespaceImport(bindings)) {
|
|
1131
|
+
namespaces.set(bindings.name.text, module);
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
if (!bindings || !ts.isNamedImports(bindings)) continue;
|
|
1135
|
+
for (const item of bindings.elements) {
|
|
1136
|
+
const exported = item.propertyName?.text ?? item.name.text;
|
|
1137
|
+
const entry = resolveExportEntry(
|
|
1138
|
+
facts.exportEntries.get(exported),
|
|
1139
|
+
exported,
|
|
1140
|
+
module,
|
|
1141
|
+
file,
|
|
1142
|
+
owner.catalogDirectory,
|
|
1143
|
+
aliasSource(module),
|
|
1144
|
+
);
|
|
1145
|
+
if (entry) imports.set(item.name.text, entry);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
const openingOf = (node) =>
|
|
1149
|
+
ts.isJsxElement(node) ? node.openingElement : node;
|
|
1150
|
+
const entryFor = (opening) => {
|
|
1151
|
+
const tag = opening.tagName.getText(source);
|
|
1152
|
+
if (imports.has(tag)) return imports.get(tag);
|
|
1153
|
+
if (
|
|
1154
|
+
ts.isPropertyAccessExpression(opening.tagName) &&
|
|
1155
|
+
ts.isIdentifier(opening.tagName.expression)
|
|
1156
|
+
) {
|
|
1157
|
+
const module = namespaces.get(opening.tagName.expression.text);
|
|
1158
|
+
return resolveExportEntry(
|
|
1159
|
+
facts.exportEntries.get(opening.tagName.name.text),
|
|
1160
|
+
opening.tagName.name.text,
|
|
1161
|
+
module,
|
|
1162
|
+
file,
|
|
1163
|
+
owner.catalogDirectory,
|
|
1164
|
+
aliasSource(module),
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
return undefined;
|
|
1168
|
+
};
|
|
1169
|
+
const classesOf = (opening) =>
|
|
1170
|
+
literalClasses(
|
|
1171
|
+
opening.attributes.properties.find(
|
|
1172
|
+
(item) =>
|
|
1173
|
+
ts.isJsxAttribute(item) &&
|
|
1174
|
+
['class', 'className'].includes(item.name.getText(source)),
|
|
1175
|
+
),
|
|
1176
|
+
).values;
|
|
1177
|
+
const returnedJsx = (body) => {
|
|
1178
|
+
if (!ts.isBlock(body)) return [body];
|
|
1179
|
+
const expressions = [];
|
|
1180
|
+
const visitReturns = (node) => {
|
|
1181
|
+
if (ts.isReturnStatement(node)) {
|
|
1182
|
+
if (node.expression) expressions.push(node.expression);
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
if (
|
|
1186
|
+
node !== body &&
|
|
1187
|
+
(ts.isFunctionDeclaration(node) ||
|
|
1188
|
+
ts.isFunctionExpression(node) ||
|
|
1189
|
+
ts.isArrowFunction(node))
|
|
1190
|
+
)
|
|
1191
|
+
return;
|
|
1192
|
+
ts.forEachChild(node, visitReturns);
|
|
1193
|
+
};
|
|
1194
|
+
visitReturns(body);
|
|
1195
|
+
return expressions;
|
|
1196
|
+
};
|
|
1197
|
+
const localReturns = (opening, seen) => {
|
|
1198
|
+
const name = opening.tagName.getText(source);
|
|
1199
|
+
const body = localComponents.get(name);
|
|
1200
|
+
if (!body || seen.has(name)) return undefined;
|
|
1201
|
+
return { name, expressions: returnedJsx(body) };
|
|
1202
|
+
};
|
|
1203
|
+
const composedBelow = (node, found, seen = new Set()) => {
|
|
1204
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
1205
|
+
const opening = openingOf(node);
|
|
1206
|
+
const entry = entryFor(opening);
|
|
1207
|
+
if (entry) {
|
|
1208
|
+
for (const type of [
|
|
1209
|
+
entry.boundaries?.rootElement,
|
|
1210
|
+
...(entry.boundaries?.descendantElements ?? []),
|
|
1211
|
+
]) {
|
|
1212
|
+
if (!type) continue;
|
|
1213
|
+
const entries = found.get(type) ?? new Set();
|
|
1214
|
+
entries.add(entry);
|
|
1215
|
+
found.set(type, entries);
|
|
1216
|
+
}
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
const local = localReturns(opening, seen);
|
|
1220
|
+
if (local) {
|
|
1221
|
+
const next = new Set([...seen, local.name]);
|
|
1222
|
+
for (const expression of local.expressions)
|
|
1223
|
+
composedBelow(expression, found, next);
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
if (ts.isJsxElement(node))
|
|
1227
|
+
for (const child of node.children) composedBelow(child, found, seen);
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
if (ts.isJsxFragment(node)) {
|
|
1231
|
+
for (const child of node.children) composedBelow(child, found, seen);
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
ts.forEachChild(node, (child) => composedBelow(child, found, seen));
|
|
1235
|
+
};
|
|
1236
|
+
const expandedRoots = (node, seen = new Set()) => {
|
|
1237
|
+
if (ts.isJsxFragment(node))
|
|
1238
|
+
return node.children.flatMap((child) => expandedRoots(child, seen));
|
|
1239
|
+
if (!ts.isJsxElement(node) && !ts.isJsxSelfClosingElement(node)) {
|
|
1240
|
+
const roots = [];
|
|
1241
|
+
ts.forEachChild(node, (child) =>
|
|
1242
|
+
roots.push(...expandedRoots(child, seen)),
|
|
1243
|
+
);
|
|
1244
|
+
return roots;
|
|
1245
|
+
}
|
|
1246
|
+
const local = localReturns(openingOf(node), seen);
|
|
1247
|
+
if (!local) return [node];
|
|
1248
|
+
const next = new Set([...seen, local.name]);
|
|
1249
|
+
return local.expressions.flatMap((expression) =>
|
|
1250
|
+
expandedRoots(expression, next),
|
|
1251
|
+
);
|
|
1252
|
+
};
|
|
1253
|
+
const visit = (node) => {
|
|
1254
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
1255
|
+
const opening = openingOf(node);
|
|
1256
|
+
const entry = entryFor(opening);
|
|
1257
|
+
if (entry)
|
|
1258
|
+
for (const name of classesOf(opening))
|
|
1259
|
+
record(hooks, ambiguousHooks, name, entry);
|
|
1260
|
+
if (ts.isJsxElement(node)) {
|
|
1261
|
+
const parentClasses = classesOf(opening);
|
|
1262
|
+
if (parentClasses.length) {
|
|
1263
|
+
const descendants = new Map();
|
|
1264
|
+
for (const child of node.children)
|
|
1265
|
+
composedBelow(child, descendants);
|
|
1266
|
+
for (const [type, entries] of descendants)
|
|
1267
|
+
for (const entry of entries)
|
|
1268
|
+
for (const name of parentClasses)
|
|
1269
|
+
record(
|
|
1270
|
+
composedDescendants,
|
|
1271
|
+
ambiguousDescendants,
|
|
1272
|
+
`${name}|${type}`,
|
|
1273
|
+
entry,
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
1276
|
+
const possible = new Map();
|
|
1277
|
+
let dynamic = false;
|
|
1278
|
+
for (const child of node.children) {
|
|
1279
|
+
if (ts.isJsxExpression(child) && child.expression) dynamic = true;
|
|
1280
|
+
if (!ts.isJsxElement(child) && !ts.isJsxSelfClosingElement(child))
|
|
1281
|
+
continue;
|
|
1282
|
+
for (const root of expandedRoots(child)) {
|
|
1283
|
+
const childOpening = openingOf(root);
|
|
1284
|
+
const childEntry = entryFor(childOpening);
|
|
1285
|
+
const type =
|
|
1286
|
+
childEntry?.boundaries?.rootElement ??
|
|
1287
|
+
(childEntry
|
|
1288
|
+
? undefined
|
|
1289
|
+
: childOpening.tagName.getText(source).toLowerCase());
|
|
1290
|
+
if (!type) continue;
|
|
1291
|
+
const candidates = possible.get(type) ?? [];
|
|
1292
|
+
candidates.push(childEntry);
|
|
1293
|
+
possible.set(type, candidates);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
if (!dynamic)
|
|
1297
|
+
for (const [type, candidates] of possible)
|
|
1298
|
+
if (
|
|
1299
|
+
candidates.length > 0 &&
|
|
1300
|
+
candidates.every(
|
|
1301
|
+
(candidate) => candidate?.key === candidates[0]?.key,
|
|
1302
|
+
) &&
|
|
1303
|
+
candidates[0]
|
|
1304
|
+
)
|
|
1305
|
+
for (const name of parentClasses)
|
|
1306
|
+
record(
|
|
1307
|
+
composedChildren,
|
|
1308
|
+
ambiguousChildren,
|
|
1309
|
+
`${name}|${type}`,
|
|
1310
|
+
candidates[0],
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
ts.forEachChild(node, visit);
|
|
1315
|
+
};
|
|
1316
|
+
visit(source);
|
|
1317
|
+
}
|
|
1318
|
+
return { componentMode: true, hooks, composedChildren, composedDescendants };
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
function cssPreferred(contract) {
|
|
1322
|
+
return (
|
|
1323
|
+
[
|
|
1324
|
+
...(contract.canonicalShorthands ?? []).map((item) => `\`${item}\``),
|
|
1325
|
+
...(contract.helpers ?? []).map((item) => `\`${item}()\``),
|
|
1326
|
+
].join(' or ') || 'a cataloged component prop'
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
function objectProperty(object, name) {
|
|
1331
|
+
return object?.properties.find(
|
|
1332
|
+
(property) =>
|
|
1333
|
+
ts.isPropertyAssignment(property) &&
|
|
1334
|
+
property.name.getText().replaceAll(/["']/g, '') === name,
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// A catalog `cssValueProps` path is dot-separated object members, each
|
|
1339
|
+
// optionally suffixed with `[]` to visit every array element: `gap`,
|
|
1340
|
+
// `choices[].swatch`, `iconTile.color`. A member that does not resolve to a
|
|
1341
|
+
// literal object or array (a tone string, a variable, a spread) is skipped:
|
|
1342
|
+
// dynamic values are the type system's job.
|
|
1343
|
+
function cssPathSegments(path) {
|
|
1344
|
+
return path
|
|
1345
|
+
.split('.')
|
|
1346
|
+
.map((segment) =>
|
|
1347
|
+
segment.endsWith('[]')
|
|
1348
|
+
? { name: segment.slice(0, -2), each: true }
|
|
1349
|
+
: { name: segment, each: false },
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function expandCssElements(values, each) {
|
|
1354
|
+
if (!each) return values;
|
|
1355
|
+
return values.flatMap((value) =>
|
|
1356
|
+
ts.isArrayLiteralExpression(value) ? value.elements : [],
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function nestedCssValues(value, segments) {
|
|
1361
|
+
let values = expandCssElements(value ? [value] : [], segments[0].each);
|
|
1362
|
+
for (const { name, each } of segments.slice(1))
|
|
1363
|
+
values = expandCssElements(
|
|
1364
|
+
values.flatMap((item) => {
|
|
1365
|
+
if (!ts.isObjectLiteralExpression(item)) return [];
|
|
1366
|
+
const property = objectProperty(item, name);
|
|
1367
|
+
return property ? [property.initializer] : [];
|
|
1368
|
+
}),
|
|
1369
|
+
each,
|
|
1370
|
+
);
|
|
1371
|
+
return values;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
function jsxCssValues(opening, path) {
|
|
1375
|
+
const segments = cssPathSegments(path);
|
|
1376
|
+
const attribute = opening.attributes.properties.find(
|
|
1377
|
+
(item) =>
|
|
1378
|
+
ts.isJsxAttribute(item) && item.name.getText() === segments[0].name,
|
|
1379
|
+
);
|
|
1380
|
+
if (!attribute?.initializer) return [];
|
|
1381
|
+
const value = ts.isJsxExpression(attribute.initializer)
|
|
1382
|
+
? attribute.initializer.expression
|
|
1383
|
+
: attribute.initializer;
|
|
1384
|
+
return nestedCssValues(value, segments);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
function callCssValues(call, path) {
|
|
1388
|
+
const options = call.arguments[0];
|
|
1389
|
+
if (!options || !ts.isObjectLiteralExpression(options)) return [];
|
|
1390
|
+
const segments = cssPathSegments(path);
|
|
1391
|
+
const property = objectProperty(options, segments[0].name);
|
|
1392
|
+
return property ? nestedCssValues(property.initializer, segments) : [];
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function cssLiteral(value) {
|
|
1396
|
+
if (
|
|
1397
|
+
ts.isStringLiteralLike(value) ||
|
|
1398
|
+
ts.isNoSubstitutionTemplateLiteral(value)
|
|
1399
|
+
)
|
|
1400
|
+
return value.text;
|
|
1401
|
+
return undefined;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function importedName(expression, helperImports, namespaces, packageName) {
|
|
1405
|
+
let helper;
|
|
1406
|
+
if (ts.isIdentifier(expression)) helper = helperImports.get(expression.text);
|
|
1407
|
+
else if (
|
|
1408
|
+
ts.isPropertyAccessExpression(expression) &&
|
|
1409
|
+
ts.isIdentifier(expression.expression) &&
|
|
1410
|
+
namespaces.has(expression.expression.text)
|
|
1411
|
+
)
|
|
1412
|
+
helper = {
|
|
1413
|
+
imported: expression.name.text,
|
|
1414
|
+
source: namespaces.get(expression.expression.text),
|
|
1415
|
+
};
|
|
1416
|
+
else helper = undefined;
|
|
1417
|
+
if (!helper) return undefined;
|
|
1418
|
+
return helper.source === packageName ||
|
|
1419
|
+
helper.source.startsWith(`${packageName}/`)
|
|
1420
|
+
? helper.imported
|
|
1421
|
+
: undefined;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function inspectCssValues(
|
|
1425
|
+
file,
|
|
1426
|
+
source,
|
|
1427
|
+
entry,
|
|
1428
|
+
valuesFor,
|
|
1429
|
+
helperImports,
|
|
1430
|
+
namespaces,
|
|
1431
|
+
diagnostics,
|
|
1432
|
+
) {
|
|
1433
|
+
for (const contract of entry.cssValueProps ?? []) {
|
|
1434
|
+
const preferred = cssPreferred(contract);
|
|
1435
|
+
for (const value of valuesFor(contract.path)) {
|
|
1436
|
+
const at = location(file, value, source);
|
|
1437
|
+
if (
|
|
1438
|
+
contract.grammar === 'declarations' &&
|
|
1439
|
+
contract.rawPolicy !== 'allow'
|
|
1440
|
+
) {
|
|
1441
|
+
diagnostics.push(
|
|
1442
|
+
diagnostic(
|
|
1443
|
+
'KUI-L016',
|
|
1444
|
+
at,
|
|
1445
|
+
`\`${entry.name}.${contract.path}\` is a forbidden declaration-list escape hatch; use \`className\`, public tokens, or cataloged props.`,
|
|
1446
|
+
{ component: entry.key, path: contract.path },
|
|
1447
|
+
),
|
|
1448
|
+
);
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
const literal = cssLiteral(value);
|
|
1452
|
+
if (literal !== undefined) {
|
|
1453
|
+
if (contract.exceptionalShorthands?.includes(literal)) {
|
|
1454
|
+
diagnostics.push(
|
|
1455
|
+
diagnostic(
|
|
1456
|
+
'KUI-L017',
|
|
1457
|
+
at,
|
|
1458
|
+
`\`${literal}\` is an exceptional shorthand for \`${entry.name}.${contract.path}\`; prefer ${preferred} unless the off-scale choice is deliberate.`,
|
|
1459
|
+
{ component: entry.key, path: contract.path, value: literal },
|
|
1460
|
+
),
|
|
1461
|
+
);
|
|
1462
|
+
} else if (
|
|
1463
|
+
!contract.shorthands?.includes(literal) &&
|
|
1464
|
+
contract.rawPolicy !== 'allow'
|
|
1465
|
+
) {
|
|
1466
|
+
diagnostics.push(
|
|
1467
|
+
diagnostic(
|
|
1468
|
+
'KUI-L013',
|
|
1469
|
+
at,
|
|
1470
|
+
`\`${entry.name}.${contract.path}\` uses ${contract.grammar} grammar; replace raw \`${literal}\` with ${preferred}.`,
|
|
1471
|
+
{ component: entry.key, path: contract.path, value: literal },
|
|
1472
|
+
),
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
if (!ts.isCallExpression(value)) continue;
|
|
1478
|
+
const helper = importedName(
|
|
1479
|
+
value.expression,
|
|
1480
|
+
helperImports,
|
|
1481
|
+
namespaces,
|
|
1482
|
+
entry.package,
|
|
1483
|
+
);
|
|
1484
|
+
if (!helper) continue;
|
|
1485
|
+
if (contract.nonStandaloneHelpers?.includes(helper))
|
|
1486
|
+
diagnostics.push(
|
|
1487
|
+
diagnostic(
|
|
1488
|
+
'KUI-L015',
|
|
1489
|
+
at,
|
|
1490
|
+
`\`${helper}()\` is not a standalone CSS value for \`${entry.name}.${contract.path}\`; wrap it with an accepted composer such as \`calc()\`.`,
|
|
1491
|
+
{ component: entry.key, path: contract.path, helper },
|
|
1492
|
+
),
|
|
1493
|
+
);
|
|
1494
|
+
else if (
|
|
1495
|
+
!contract.helpers?.includes(helper) &&
|
|
1496
|
+
contract.unsafeHelper !== helper
|
|
1497
|
+
)
|
|
1498
|
+
diagnostics.push(
|
|
1499
|
+
diagnostic(
|
|
1500
|
+
'KUI-L014',
|
|
1501
|
+
at,
|
|
1502
|
+
`\`${helper}()\` produces the wrong grammar for \`${entry.name}.${contract.path}\`; use ${preferred}.`,
|
|
1503
|
+
{ component: entry.key, path: contract.path, helper },
|
|
1504
|
+
),
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
function inspectTsx(
|
|
1511
|
+
file,
|
|
1512
|
+
sourceText,
|
|
1513
|
+
facts,
|
|
1514
|
+
cssFacts,
|
|
1515
|
+
diagnostics,
|
|
1516
|
+
adoption = false,
|
|
1517
|
+
isForeign = () => true,
|
|
1518
|
+
packageDirectory,
|
|
1519
|
+
compilerOptions,
|
|
1520
|
+
componentMode = false,
|
|
1521
|
+
) {
|
|
332
1522
|
const source = ts.createSourceFile(
|
|
333
1523
|
file,
|
|
334
1524
|
sourceText,
|
|
@@ -337,6 +1527,20 @@ function inspectTsx(file, sourceText, facts, cssFacts, diagnostics) {
|
|
|
337
1527
|
ts.ScriptKind.TSX,
|
|
338
1528
|
);
|
|
339
1529
|
const imports = new Map();
|
|
1530
|
+
const helperImports = new Map();
|
|
1531
|
+
const namespaces = new Map();
|
|
1532
|
+
const aliasSources = new Map();
|
|
1533
|
+
const aliasSource = (module) => {
|
|
1534
|
+
if (!module || module.startsWith('.') || !compilerOptions?.paths)
|
|
1535
|
+
return undefined;
|
|
1536
|
+
if (!aliasSources.has(module))
|
|
1537
|
+
aliasSources.set(
|
|
1538
|
+
module,
|
|
1539
|
+
ts.resolveModuleName(module, file, compilerOptions, ts.sys)
|
|
1540
|
+
.resolvedModule?.resolvedFileName,
|
|
1541
|
+
);
|
|
1542
|
+
return aliasSources.get(module);
|
|
1543
|
+
};
|
|
340
1544
|
for (const statement of source.statements) {
|
|
341
1545
|
if (
|
|
342
1546
|
!ts.isImportDeclaration(statement) ||
|
|
@@ -344,21 +1548,96 @@ function inspectTsx(file, sourceText, facts, cssFacts, diagnostics) {
|
|
|
344
1548
|
)
|
|
345
1549
|
continue;
|
|
346
1550
|
const module = statement.moduleSpecifier.text;
|
|
347
|
-
|
|
1551
|
+
const importClause = statement.importClause;
|
|
1552
|
+
if (importClause?.name)
|
|
1553
|
+
helperImports.set(importClause.name.text, {
|
|
1554
|
+
imported: 'default',
|
|
1555
|
+
source: module,
|
|
1556
|
+
});
|
|
348
1557
|
const bindings = statement.importClause?.namedBindings;
|
|
1558
|
+
if (bindings && ts.isNamespaceImport(bindings)) {
|
|
1559
|
+
namespaces.set(bindings.name.text, module);
|
|
1560
|
+
continue;
|
|
1561
|
+
}
|
|
349
1562
|
if (!bindings || !ts.isNamedImports(bindings)) continue;
|
|
350
1563
|
for (const item of bindings.elements) {
|
|
351
1564
|
const exported = item.propertyName?.text ?? item.name.text;
|
|
352
|
-
|
|
1565
|
+
helperImports.set(item.name.text, { imported: exported, source: module });
|
|
1566
|
+
const entry = resolveExportEntry(
|
|
1567
|
+
facts.exportEntries.get(exported),
|
|
1568
|
+
exported,
|
|
1569
|
+
module,
|
|
1570
|
+
file,
|
|
1571
|
+
packageDirectory,
|
|
1572
|
+
aliasSource(module),
|
|
1573
|
+
);
|
|
353
1574
|
if (entry) imports.set(item.name.text, entry);
|
|
354
1575
|
}
|
|
355
1576
|
}
|
|
356
1577
|
const stack = [];
|
|
1578
|
+
const inspectBorrowedMarkup = !isTestModule(file);
|
|
1579
|
+
const borrowed = (className, node, via) => {
|
|
1580
|
+
if (!inspectBorrowedMarkup) return;
|
|
1581
|
+
const owners =
|
|
1582
|
+
facts.classEntries.get(className) ??
|
|
1583
|
+
(componentMode && bemBlock(className) !== className
|
|
1584
|
+
? facts.blockEntries.get(bemBlock(className))
|
|
1585
|
+
: undefined) ??
|
|
1586
|
+
[];
|
|
1587
|
+
for (const owner of owners) {
|
|
1588
|
+
if (
|
|
1589
|
+
!isForeign(owner, className) ||
|
|
1590
|
+
owner.boundaries?.placeableClasses?.includes(className)
|
|
1591
|
+
)
|
|
1592
|
+
continue;
|
|
1593
|
+
diagnostics.push(
|
|
1594
|
+
diagnostic(
|
|
1595
|
+
'KUI-L023',
|
|
1596
|
+
location(file, node, source),
|
|
1597
|
+
`\`.${className}\` is rendered in this module's ${via} but belongs to ${componentLabel(owner)}. Render that component or configure it through ${configurationFor(owner)}. ${reportGap(owner)}`,
|
|
1598
|
+
{ className, component: owner.key, via },
|
|
1599
|
+
undefined,
|
|
1600
|
+
adoption,
|
|
1601
|
+
),
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1605
|
+
const borrowedValues = (values, node, via) => {
|
|
1606
|
+
for (const className of new Set(values)) borrowed(className, node, via);
|
|
1607
|
+
};
|
|
1608
|
+
const templateGap = '\uE000';
|
|
1609
|
+
const rawHtmlClasses = (value) => {
|
|
1610
|
+
const classes = [];
|
|
1611
|
+
for (const match of value.matchAll(
|
|
1612
|
+
/<[a-z][^>]*\sclass\s*=\s*["']([^"']+)["']/gi,
|
|
1613
|
+
)) {
|
|
1614
|
+
const pieces = match[1].split(templateGap);
|
|
1615
|
+
for (const [index, piece] of pieces.entries()) {
|
|
1616
|
+
const words = piece.split(/\s+/).filter(Boolean);
|
|
1617
|
+
if (index > 0 && !/^\s/.test(piece)) words.shift();
|
|
1618
|
+
if (index < pieces.length - 1 && !/\s$/.test(piece)) words.pop();
|
|
1619
|
+
classes.push(...words);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
return classes;
|
|
1623
|
+
};
|
|
357
1624
|
const visit = (node) => {
|
|
358
1625
|
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
359
1626
|
const opening = ts.isJsxElement(node) ? node.openingElement : node;
|
|
360
1627
|
const tag = opening.tagName.getText(source);
|
|
361
|
-
const
|
|
1628
|
+
const namespaceEntry =
|
|
1629
|
+
ts.isPropertyAccessExpression(opening.tagName) &&
|
|
1630
|
+
ts.isIdentifier(opening.tagName.expression)
|
|
1631
|
+
? resolveExportEntry(
|
|
1632
|
+
facts.exportEntries.get(opening.tagName.name.text),
|
|
1633
|
+
opening.tagName.name.text,
|
|
1634
|
+
namespaces.get(opening.tagName.expression.text),
|
|
1635
|
+
file,
|
|
1636
|
+
packageDirectory,
|
|
1637
|
+
aliasSource(namespaces.get(opening.tagName.expression.text)),
|
|
1638
|
+
)
|
|
1639
|
+
: undefined;
|
|
1640
|
+
const entry = imports.get(tag) ?? namespaceEntry;
|
|
362
1641
|
const classAttribute = opening.attributes.properties.find(
|
|
363
1642
|
(item) =>
|
|
364
1643
|
ts.isJsxAttribute(item) &&
|
|
@@ -366,6 +1645,55 @@ function inspectTsx(file, sourceText, facts, cssFacts, diagnostics) {
|
|
|
366
1645
|
);
|
|
367
1646
|
const classes = literalClasses(classAttribute);
|
|
368
1647
|
const at = location(file, opening, source);
|
|
1648
|
+
if ((!entry && /^[a-z]/.test(tag)) || (componentMode && entry))
|
|
1649
|
+
borrowedValues(
|
|
1650
|
+
classAttribute?.initializer &&
|
|
1651
|
+
ts.isJsxExpression(classAttribute.initializer)
|
|
1652
|
+
? literalClassValues(classAttribute.initializer.expression)
|
|
1653
|
+
: classes.values,
|
|
1654
|
+
classAttribute ?? opening,
|
|
1655
|
+
'JSX markup',
|
|
1656
|
+
);
|
|
1657
|
+
if (entry)
|
|
1658
|
+
inspectCssValues(
|
|
1659
|
+
file,
|
|
1660
|
+
source,
|
|
1661
|
+
entry,
|
|
1662
|
+
(path) => jsxCssValues(opening, path),
|
|
1663
|
+
helperImports,
|
|
1664
|
+
namespaces,
|
|
1665
|
+
diagnostics,
|
|
1666
|
+
);
|
|
1667
|
+
if (entry && isForeign(entry))
|
|
1668
|
+
for (const className of classes.values) {
|
|
1669
|
+
// A class owned by the surrounding application is still a hook when
|
|
1670
|
+
// placed on another component's root. Only the component's own
|
|
1671
|
+
// public class is safe to style here.
|
|
1672
|
+
if (
|
|
1673
|
+
classOwnerFor(facts.ownership, className, componentMode)?.key ===
|
|
1674
|
+
entry.key
|
|
1675
|
+
)
|
|
1676
|
+
continue;
|
|
1677
|
+
const styled = cssFacts.get(className)?.styled;
|
|
1678
|
+
if (!styled) continue;
|
|
1679
|
+
diagnostics.push(
|
|
1680
|
+
diagnostic(
|
|
1681
|
+
'KUI-L022',
|
|
1682
|
+
at,
|
|
1683
|
+
`\`${className}\` is placed on ${entry.name}'s root and styled by ${basename(styled.stylesheet)}:${styled.line} (\`${styled.selector}\`), which restyles ${componentLabel(entry)}; components own their styles. Configure it through ${configurationFor(entry)}, or wrap it in your own element and style that. ${reportGap(entry)}`,
|
|
1684
|
+
{
|
|
1685
|
+
tag,
|
|
1686
|
+
component: entry.key,
|
|
1687
|
+
className,
|
|
1688
|
+
stylesheet: styled.stylesheet,
|
|
1689
|
+
line: styled.line,
|
|
1690
|
+
selector: styled.selector,
|
|
1691
|
+
},
|
|
1692
|
+
undefined,
|
|
1693
|
+
adoption,
|
|
1694
|
+
),
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
369
1697
|
if (classes.dynamic)
|
|
370
1698
|
diagnostics.push(
|
|
371
1699
|
diagnostic(
|
|
@@ -425,11 +1753,92 @@ function inspectTsx(file, sourceText, facts, cssFacts, diagnostics) {
|
|
|
425
1753
|
inset: inset || parent?.inset,
|
|
426
1754
|
label: entry?.key ?? tag,
|
|
427
1755
|
});
|
|
1756
|
+
// JSX supplied through a prop (for example Toolbar.leading) is still
|
|
1757
|
+
// rendered markup and must be checked like ordinary children.
|
|
1758
|
+
ts.forEachChild(opening.attributes, visit);
|
|
428
1759
|
if (ts.isJsxElement(node))
|
|
429
1760
|
for (const child of node.children) visit(child);
|
|
430
1761
|
stack.pop();
|
|
431
1762
|
return;
|
|
432
1763
|
}
|
|
1764
|
+
if (ts.isCallExpression(node)) {
|
|
1765
|
+
if (ts.isPropertyAccessExpression(node.expression)) {
|
|
1766
|
+
const method = node.expression.name.text;
|
|
1767
|
+
const receiver = node.expression.expression;
|
|
1768
|
+
if (
|
|
1769
|
+
ts.isPropertyAccessExpression(receiver) &&
|
|
1770
|
+
receiver.name.text === 'classList'
|
|
1771
|
+
) {
|
|
1772
|
+
const written =
|
|
1773
|
+
method === 'add'
|
|
1774
|
+
? node.arguments
|
|
1775
|
+
: method === 'toggle'
|
|
1776
|
+
? node.arguments.slice(0, 1)
|
|
1777
|
+
: method === 'replace'
|
|
1778
|
+
? node.arguments.slice(1, 2)
|
|
1779
|
+
: [];
|
|
1780
|
+
for (const value of written)
|
|
1781
|
+
borrowedValues(literalClassValues(value), value, 'classList write');
|
|
1782
|
+
}
|
|
1783
|
+
if (
|
|
1784
|
+
method === 'setAttribute' &&
|
|
1785
|
+
node.arguments[0] &&
|
|
1786
|
+
ts.isStringLiteralLike(node.arguments[0]) &&
|
|
1787
|
+
node.arguments[0].text === 'class'
|
|
1788
|
+
)
|
|
1789
|
+
borrowedValues(
|
|
1790
|
+
literalClassValues(node.arguments[1]),
|
|
1791
|
+
node.arguments[1] ?? node,
|
|
1792
|
+
'setAttribute write',
|
|
1793
|
+
);
|
|
1794
|
+
}
|
|
1795
|
+
const entry = ts.isIdentifier(node.expression)
|
|
1796
|
+
? imports.get(node.expression.text)
|
|
1797
|
+
: ts.isPropertyAccessExpression(node.expression) &&
|
|
1798
|
+
ts.isIdentifier(node.expression.expression)
|
|
1799
|
+
? resolveExportEntry(
|
|
1800
|
+
facts.exportEntries.get(node.expression.name.text),
|
|
1801
|
+
node.expression.name.text,
|
|
1802
|
+
namespaces.get(node.expression.expression.text),
|
|
1803
|
+
file,
|
|
1804
|
+
packageDirectory,
|
|
1805
|
+
aliasSource(namespaces.get(node.expression.expression.text)),
|
|
1806
|
+
)
|
|
1807
|
+
: undefined;
|
|
1808
|
+
if (entry)
|
|
1809
|
+
inspectCssValues(
|
|
1810
|
+
file,
|
|
1811
|
+
source,
|
|
1812
|
+
entry,
|
|
1813
|
+
(path) => callCssValues(node, path),
|
|
1814
|
+
helperImports,
|
|
1815
|
+
namespaces,
|
|
1816
|
+
diagnostics,
|
|
1817
|
+
);
|
|
1818
|
+
}
|
|
1819
|
+
if (
|
|
1820
|
+
ts.isBinaryExpression(node) &&
|
|
1821
|
+
[ts.SyntaxKind.EqualsToken, ts.SyntaxKind.PlusEqualsToken].includes(
|
|
1822
|
+
node.operatorToken.kind,
|
|
1823
|
+
) &&
|
|
1824
|
+
ts.isPropertyAccessExpression(node.left) &&
|
|
1825
|
+
node.left.name.text === 'className'
|
|
1826
|
+
)
|
|
1827
|
+
borrowedValues(
|
|
1828
|
+
literalClassValues(node.right),
|
|
1829
|
+
node.right,
|
|
1830
|
+
'className assignment',
|
|
1831
|
+
);
|
|
1832
|
+
if (ts.isStringLiteralLike(node) && node.text.includes('<'))
|
|
1833
|
+
borrowedValues(rawHtmlClasses(node.text), node, 'raw HTML');
|
|
1834
|
+
if (ts.isTemplateExpression(node)) {
|
|
1835
|
+
const staticHtml = [
|
|
1836
|
+
node.head.text,
|
|
1837
|
+
...node.templateSpans.map((span) => span.literal.text),
|
|
1838
|
+
].join(templateGap);
|
|
1839
|
+
if (staticHtml.includes('<'))
|
|
1840
|
+
borrowedValues(rawHtmlClasses(staticHtml), node, 'raw HTML');
|
|
1841
|
+
}
|
|
433
1842
|
ts.forEachChild(node, visit);
|
|
434
1843
|
};
|
|
435
1844
|
visit(source);
|
|
@@ -439,6 +1848,11 @@ export async function analyzeUiProject({
|
|
|
439
1848
|
root: requestedRoot = process.cwd(),
|
|
440
1849
|
paths,
|
|
441
1850
|
knownRules = [],
|
|
1851
|
+
adoption = false,
|
|
1852
|
+
ownership = 'package',
|
|
1853
|
+
ownershipContext = 'subject',
|
|
1854
|
+
implicitComponentOwnership = false,
|
|
1855
|
+
ownershipGroups = [],
|
|
442
1856
|
profile: packageProfile = resolve(
|
|
443
1857
|
import.meta.dirname,
|
|
444
1858
|
'../ai/application-ui-profile.defaults.json',
|
|
@@ -474,6 +1888,22 @@ export async function analyzeUiProject({
|
|
|
474
1888
|
}
|
|
475
1889
|
}
|
|
476
1890
|
files.sort();
|
|
1891
|
+
const imports = new Map(
|
|
1892
|
+
files.map((file) => [file, relativeStyleImports(file, contents.get(file))]),
|
|
1893
|
+
);
|
|
1894
|
+
const implicitEntries =
|
|
1895
|
+
ownership === 'component' && implicitComponentOwnership
|
|
1896
|
+
? await implicitOwnershipEntries(files, imports, root)
|
|
1897
|
+
: [];
|
|
1898
|
+
const groupEntries =
|
|
1899
|
+
ownership === 'component'
|
|
1900
|
+
? await ownershipGroupEntries(ownershipGroups, root, contents)
|
|
1901
|
+
: [];
|
|
1902
|
+
const groupStyles = new Set(
|
|
1903
|
+
groupEntries.flatMap((entry) =>
|
|
1904
|
+
entry.styleSources.map((style) => resolve(entry.catalogDirectory, style)),
|
|
1905
|
+
),
|
|
1906
|
+
);
|
|
477
1907
|
const profileContexts = new Map();
|
|
478
1908
|
const loadContext = async (file) => {
|
|
479
1909
|
const startDirectory = file === root ? root : dirname(file);
|
|
@@ -487,10 +1917,51 @@ export async function analyzeUiProject({
|
|
|
487
1917
|
packageProfile,
|
|
488
1918
|
knownRules: [...Object.keys(UI_ANALYSIS_RULES), ...knownRules],
|
|
489
1919
|
});
|
|
490
|
-
const entries = await loadCatalogs(profileResult);
|
|
1920
|
+
const entries = await loadCatalogs(profileResult, ownership);
|
|
1921
|
+
const catalogedSources = new Set(
|
|
1922
|
+
entries
|
|
1923
|
+
.filter((entry) => entry.componentSource)
|
|
1924
|
+
.map((entry) =>
|
|
1925
|
+
resolve(entry.catalogDirectory, entry.componentSource),
|
|
1926
|
+
),
|
|
1927
|
+
);
|
|
1928
|
+
const ownershipEntries = [
|
|
1929
|
+
...entries,
|
|
1930
|
+
...groupEntries,
|
|
1931
|
+
...implicitEntries.flatMap((entry) => {
|
|
1932
|
+
const source = resolve(
|
|
1933
|
+
entry.catalogDirectory,
|
|
1934
|
+
entry.componentSource,
|
|
1935
|
+
);
|
|
1936
|
+
if (catalogedSources.has(source)) return [];
|
|
1937
|
+
if (
|
|
1938
|
+
groupEntries.some((group) => {
|
|
1939
|
+
const relativeSource = relative(
|
|
1940
|
+
group.catalogDirectory,
|
|
1941
|
+
source,
|
|
1942
|
+
).replaceAll('\\', '/');
|
|
1943
|
+
return group.ownershipGroupSources.some((path) =>
|
|
1944
|
+
path.endsWith('/')
|
|
1945
|
+
? relativeSource.startsWith(path)
|
|
1946
|
+
: relativeSource === path,
|
|
1947
|
+
);
|
|
1948
|
+
})
|
|
1949
|
+
)
|
|
1950
|
+
return [];
|
|
1951
|
+
const styleSources = entry.styleSources.filter(
|
|
1952
|
+
(style) =>
|
|
1953
|
+
!groupStyles.has(resolve(entry.catalogDirectory, style)),
|
|
1954
|
+
);
|
|
1955
|
+
return styleSources.length ? [{ ...entry, styleSources }] : [];
|
|
1956
|
+
}),
|
|
1957
|
+
];
|
|
491
1958
|
return {
|
|
492
1959
|
profileResult,
|
|
493
|
-
facts: catalogFacts(
|
|
1960
|
+
facts: catalogFacts(
|
|
1961
|
+
ownership === 'component'
|
|
1962
|
+
? inferOwnedClasses(ownershipEntries, contents, imports)
|
|
1963
|
+
: entries,
|
|
1964
|
+
),
|
|
494
1965
|
startDirectory,
|
|
495
1966
|
};
|
|
496
1967
|
})(),
|
|
@@ -503,9 +1974,106 @@ export async function analyzeUiProject({
|
|
|
503
1974
|
),
|
|
504
1975
|
);
|
|
505
1976
|
if (!files.length) await loadContext(root);
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
);
|
|
1977
|
+
// A stylesheet or source file belongs to the package whose manifest is
|
|
1978
|
+
// nearest it; entries of every other catalog package are foreign to it.
|
|
1979
|
+
const packages = new Map();
|
|
1980
|
+
const compilerOptions = new Map();
|
|
1981
|
+
const packageOf = async (directory) => {
|
|
1982
|
+
if (!packages.has(directory))
|
|
1983
|
+
packages.set(
|
|
1984
|
+
directory,
|
|
1985
|
+
(async () => {
|
|
1986
|
+
try {
|
|
1987
|
+
const manifest = JSON.parse(
|
|
1988
|
+
await readFile(resolve(directory, 'package.json'), 'utf8'),
|
|
1989
|
+
);
|
|
1990
|
+
if (typeof manifest.name === 'string')
|
|
1991
|
+
return { name: manifest.name, directory };
|
|
1992
|
+
} catch {
|
|
1993
|
+
// No readable manifest here; keep walking up.
|
|
1994
|
+
}
|
|
1995
|
+
const parent = dirname(directory);
|
|
1996
|
+
const inside = relative(root, parent);
|
|
1997
|
+
if (
|
|
1998
|
+
parent === directory ||
|
|
1999
|
+
inside === '..' ||
|
|
2000
|
+
inside.startsWith('../')
|
|
2001
|
+
)
|
|
2002
|
+
return undefined;
|
|
2003
|
+
return packageOf(parent);
|
|
2004
|
+
})(),
|
|
2005
|
+
);
|
|
2006
|
+
return packages.get(directory);
|
|
2007
|
+
};
|
|
2008
|
+
const styledBlocksByFile = new Map();
|
|
2009
|
+
const styledBlocksIn = (style) => {
|
|
2010
|
+
if (!styledBlocksByFile.has(style)) {
|
|
2011
|
+
const blocks = new Set();
|
|
2012
|
+
const css = contents.get(style);
|
|
2013
|
+
if (css)
|
|
2014
|
+
try {
|
|
2015
|
+
postcss.parse(css, { from: style }).walkRules((rule) => {
|
|
2016
|
+
for (const name of subjectClasses(rule.selector))
|
|
2017
|
+
blocks.add(bemBlock(name));
|
|
2018
|
+
});
|
|
2019
|
+
} catch {
|
|
2020
|
+
// Malformed CSS is reported by the normal inspection pass.
|
|
2021
|
+
}
|
|
2022
|
+
styledBlocksByFile.set(style, blocks);
|
|
2023
|
+
}
|
|
2024
|
+
return styledBlocksByFile.get(style);
|
|
2025
|
+
};
|
|
2026
|
+
const foreignTo = async (file, facts) => {
|
|
2027
|
+
const own = await packageOf(dirname(file));
|
|
2028
|
+
const source = own && relative(own.directory, file).replaceAll('\\', '/');
|
|
2029
|
+
const ownedStyles = new Set(imports.get(file) ?? []);
|
|
2030
|
+
if (ownership === 'component' && !file.endsWith('.css'))
|
|
2031
|
+
for (const entry of facts.entries) {
|
|
2032
|
+
if (
|
|
2033
|
+
entry.package !== own?.name ||
|
|
2034
|
+
!entry.componentSource ||
|
|
2035
|
+
!entry.catalogDirectory ||
|
|
2036
|
+
resolve(entry.catalogDirectory, entry.componentSource) !== file
|
|
2037
|
+
)
|
|
2038
|
+
continue;
|
|
2039
|
+
for (const style of entry.styleSources ?? [])
|
|
2040
|
+
ownedStyles.add(resolve(entry.catalogDirectory, style));
|
|
2041
|
+
}
|
|
2042
|
+
return (entry, className) => {
|
|
2043
|
+
if (entry.package !== own?.name) return true;
|
|
2044
|
+
if (ownership !== 'component') return false;
|
|
2045
|
+
if (entry.ownershipGroupSources) {
|
|
2046
|
+
const relativeFile = relative(entry.catalogDirectory, file).replaceAll(
|
|
2047
|
+
'\\',
|
|
2048
|
+
'/',
|
|
2049
|
+
);
|
|
2050
|
+
if (file.endsWith('.css'))
|
|
2051
|
+
return !entry.styleSources?.includes(relativeFile);
|
|
2052
|
+
return !entry.ownershipGroupSources.some((path) =>
|
|
2053
|
+
path.endsWith('/')
|
|
2054
|
+
? relativeFile.startsWith(path)
|
|
2055
|
+
: relativeFile === path,
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
if (
|
|
2059
|
+
!file.endsWith('.css') &&
|
|
2060
|
+
className &&
|
|
2061
|
+
entry.catalogDirectory &&
|
|
2062
|
+
entry.styleSources?.some((style) => {
|
|
2063
|
+
const path = resolve(entry.catalogDirectory, style);
|
|
2064
|
+
return (
|
|
2065
|
+
ownedStyles.has(path) &&
|
|
2066
|
+
styledBlocksIn(path).has(bemBlock(className))
|
|
2067
|
+
);
|
|
2068
|
+
})
|
|
2069
|
+
)
|
|
2070
|
+
return false;
|
|
2071
|
+
const ownedSources = file.endsWith('.css')
|
|
2072
|
+
? entry.styleSources
|
|
2073
|
+
: [entry.componentSource];
|
|
2074
|
+
return !ownedSources?.includes(source);
|
|
2075
|
+
};
|
|
2076
|
+
};
|
|
509
2077
|
const styleFacts = new Map();
|
|
510
2078
|
const recordDiagnostics = (items, context) => {
|
|
511
2079
|
for (const item of items) {
|
|
@@ -538,16 +2106,56 @@ export async function analyzeUiProject({
|
|
|
538
2106
|
for (const file of files.filter((item) => item.endsWith('.css'))) {
|
|
539
2107
|
const context = contexts.get(file);
|
|
540
2108
|
const fileFacts = new Map();
|
|
541
|
-
await inspectCss(
|
|
2109
|
+
await inspectCss(
|
|
2110
|
+
file,
|
|
2111
|
+
contents.get(file),
|
|
2112
|
+
context.facts,
|
|
2113
|
+
[],
|
|
2114
|
+
fileFacts,
|
|
2115
|
+
adoption,
|
|
2116
|
+
);
|
|
542
2117
|
styleFacts.set(file, fileFacts);
|
|
543
2118
|
}
|
|
544
2119
|
const styleConsumers = new Map(
|
|
545
2120
|
[...styleFacts.keys()].map((file) => [file, new Set()]),
|
|
546
2121
|
);
|
|
2122
|
+
// KUI-L018 accepts an on-loud from another stylesheet the same entry loads:
|
|
2123
|
+
// every stylesheet reachable from one file (a source module's CSS imports,
|
|
2124
|
+
// or a stylesheet with its @imports) is loaded together.
|
|
2125
|
+
const onLoudByStyle = new Map();
|
|
2126
|
+
for (const style of styleFacts.keys()) {
|
|
2127
|
+
let parsed;
|
|
2128
|
+
try {
|
|
2129
|
+
parsed = postcss.parse(contents.get(style), { from: style });
|
|
2130
|
+
} catch {
|
|
2131
|
+
continue;
|
|
2132
|
+
}
|
|
2133
|
+
onLoudByStyle.set(
|
|
2134
|
+
style,
|
|
2135
|
+
collectLoudDeclarations(style, parsed).filter(
|
|
2136
|
+
({ kind }) => kind === 'on-loud',
|
|
2137
|
+
),
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
2140
|
+
const coLoadedStyles = new Map(
|
|
2141
|
+
[...styleFacts.keys()].map((style) => [style, new Set()]),
|
|
2142
|
+
);
|
|
2143
|
+
for (const file of files) {
|
|
2144
|
+
const loaded = new Set(reachableStyleFiles(file, imports, styleFacts));
|
|
2145
|
+
if (styleFacts.has(file)) loaded.add(file);
|
|
2146
|
+
for (const style of loaded)
|
|
2147
|
+
for (const sibling of loaded)
|
|
2148
|
+
if (sibling !== style) coLoadedStyles.get(style).add(sibling);
|
|
2149
|
+
}
|
|
2150
|
+
const siblingOnLoud = (style) =>
|
|
2151
|
+
[...coLoadedStyles.get(style)]
|
|
2152
|
+
.sort()
|
|
2153
|
+
.flatMap((sibling) => onLoudByStyle.get(sibling) ?? []);
|
|
547
2154
|
for (const file of files.filter((item) =>
|
|
548
2155
|
sourceExtensions.has(extname(item)),
|
|
549
2156
|
)) {
|
|
550
2157
|
const context = contexts.get(file);
|
|
2158
|
+
const own = await packageOf(dirname(file));
|
|
551
2159
|
for (const style of reachableStyleFiles(file, imports, styleFacts))
|
|
552
2160
|
styleConsumers.get(style).add(context);
|
|
553
2161
|
const fileDiagnostics = [];
|
|
@@ -557,6 +2165,11 @@ export async function analyzeUiProject({
|
|
|
557
2165
|
context.facts,
|
|
558
2166
|
reachableStyleFacts(file, imports, styleFacts),
|
|
559
2167
|
fileDiagnostics,
|
|
2168
|
+
adoption,
|
|
2169
|
+
await foreignTo(file, context.facts),
|
|
2170
|
+
own?.directory,
|
|
2171
|
+
compilerOptionsFor(file, root, compilerOptions),
|
|
2172
|
+
ownership === 'component',
|
|
560
2173
|
);
|
|
561
2174
|
recordDiagnostics(fileDiagnostics, context);
|
|
562
2175
|
}
|
|
@@ -566,12 +2179,29 @@ export async function analyzeUiProject({
|
|
|
566
2179
|
: new Set([contexts.get(file)]);
|
|
567
2180
|
for (const context of applicableContexts) {
|
|
568
2181
|
const fileDiagnostics = [];
|
|
2182
|
+
const own = await packageOf(dirname(file));
|
|
569
2183
|
await inspectCss(
|
|
570
2184
|
file,
|
|
571
2185
|
contents.get(file),
|
|
572
2186
|
context.facts,
|
|
573
2187
|
fileDiagnostics,
|
|
574
2188
|
new Map(),
|
|
2189
|
+
adoption,
|
|
2190
|
+
siblingOnLoud(file),
|
|
2191
|
+
await foreignTo(file, context.facts),
|
|
2192
|
+
ownership === 'component'
|
|
2193
|
+
? {
|
|
2194
|
+
...jsxOwnershipEvidence(
|
|
2195
|
+
file,
|
|
2196
|
+
context.facts,
|
|
2197
|
+
contents,
|
|
2198
|
+
root,
|
|
2199
|
+
compilerOptions,
|
|
2200
|
+
),
|
|
2201
|
+
ownPackage: own?.name,
|
|
2202
|
+
}
|
|
2203
|
+
: undefined,
|
|
2204
|
+
ownershipContext,
|
|
575
2205
|
);
|
|
576
2206
|
recordDiagnostics(fileDiagnostics, context);
|
|
577
2207
|
}
|
|
@@ -654,6 +2284,14 @@ export async function analyzeUiProject({
|
|
|
654
2284
|
...item.location,
|
|
655
2285
|
file: portablePath(item.location.file),
|
|
656
2286
|
},
|
|
2287
|
+
...(item.evidence?.stylesheet
|
|
2288
|
+
? {
|
|
2289
|
+
evidence: {
|
|
2290
|
+
...item.evidence,
|
|
2291
|
+
stylesheet: portablePath(item.evidence.stylesheet),
|
|
2292
|
+
},
|
|
2293
|
+
}
|
|
2294
|
+
: {}),
|
|
657
2295
|
})),
|
|
658
2296
|
summary: {
|
|
659
2297
|
errors:
|