@birdapi/velinstyle 1.0.0 → 1.2.0
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.de.md +87 -158
- package/README.md +188 -115
- package/cli/blueprint.js +98 -5
- package/cli/blueprints/app-chrome.html +18 -0
- package/cli/blueprints/benefits-grid.html +9 -0
- package/cli/blueprints/bottom-nav-mobile.html +2 -1
- package/cli/blueprints/cookie-consent.html +1 -1
- package/cli/blueprints/cta-band.html +6 -0
- package/cli/blueprints/faq-section.html +18 -0
- package/cli/blueprints/filter-bar.html +4 -4
- package/cli/blueprints/form-contact.html +6 -5
- package/cli/blueprints/hero-section.html +3 -3
- package/cli/blueprints/notification-center.html +1 -1
- package/cli/blueprints/onboarding.html +2 -1
- package/cli/blueprints/ops-console.html +29 -0
- package/cli/blueprints/pricing-band.html +37 -0
- package/cli/blueprints/process-steps.html +10 -0
- package/cli/blueprints/services-grid.html +9 -0
- package/cli/blueprints/settings-panel.html +8 -6
- package/cli/blueprints/split-hero.html +25 -0
- package/cli/blueprints/testimonials.html +8 -0
- package/cli/blueprints/trust-bar.html +10 -0
- package/cli/cli-manifest.json +81 -17
- package/cli/contrast-utils.js +97 -0
- package/cli/create.js +370 -0
- package/cli/docgen/extract-a11y.js +4 -2
- package/cli/docgen/extract-attributes.js +126 -10
- package/cli/docgen/extract-cli.js +3 -2
- package/cli/docgen/extract-components.js +61 -2
- package/cli/docgen/extract-utilities.js +10 -10
- package/cli/doctor.js +119 -0
- package/cli/index.js +535 -46
- package/cli/meta.js +84 -1
- package/cli/perf-audit.js +15 -2
- package/cli/prompt-engine.js +243 -0
- package/cli/review.js +268 -0
- package/cli/scaffold.js +119 -4
- package/cli/scanner-rules-data.js +7 -0
- package/cli/scanner.js +357 -4
- package/cli/serve.js +81 -0
- package/cli/skills.js +223 -0
- package/cli/wc-api.js +89 -0
- package/cli/workflow.js +42 -0
- package/components/index.js +4 -0
- package/components/runtime/component-loaders.js +4 -0
- package/components/velin-accordion.js +62 -13
- package/components/velin-bottom-nav.js +10 -0
- package/components/velin-calendar.js +212 -0
- package/components/velin-command.js +18 -2
- package/components/velin-data-table.js +438 -0
- package/components/velin-dialog.js +4 -1
- package/components/velin-drawer.js +2 -4
- package/components/velin-file-dropzone.js +162 -0
- package/components/velin-form-summary.js +348 -0
- package/components/velin-icon.js +22 -1
- package/components/velin-lightbox.js +5 -3
- package/components/velin-modal.js +2 -4
- package/components/velin-search.js +1 -1
- package/components/velin-sheet.js +2 -4
- package/core/a11y/component-contracts.json +35 -1
- package/core/highlight/languages/go.js +28 -0
- package/core/highlight/languages/python.js +29 -0
- package/core/highlight/languages/rust.js +32 -0
- package/core/highlight/languages/yaml.js +23 -0
- package/core/highlight/registry.js +13 -2
- package/core/meta/build.js +126 -6
- package/core/meta/design-constraints/contact.json +9 -0
- package/core/meta/design-constraints/faq.json +9 -0
- package/core/meta/design-constraints/hero.json +13 -0
- package/core/meta/knowledge/components.json +622 -0
- package/core/meta/knowledge/tokens.json +282 -0
- package/core/meta/pages/registry.json +294 -0
- package/core/meta/schema.js +3 -1
- package/core/meta/sections/registry.json +185 -0
- package/dist/chunks/{attributes-TPANLMLF.js → attributes-A423NX5J.js} +3 -3
- package/dist/chunks/{attributes-ADOKLKA7.js → attributes-F6UPIEVP.js} +2 -2
- package/dist/chunks/{attributes-STYCS3O5.js → attributes-GLYXGGAL.js} +3 -3
- package/dist/chunks/{attributes-5Q2WXGRC.js → attributes-UFTYOSAU.js} +20 -3
- package/dist/chunks/attributes-XGLXLR2T.js +391 -0
- package/dist/chunks/attributes-YF4NI2QS.js +391 -0
- package/dist/chunks/browser-AUABBYUK.js +1544 -0
- package/dist/chunks/{chunk-SRL6EWHP.js → chunk-2LSKX57P.js} +13 -11
- package/dist/chunks/{chunk-RSMRR27R.js → chunk-2TGCVDUL.js} +11 -9
- package/dist/chunks/{chunk-XIET2VSZ.js → chunk-5JWGUAPC.js} +17 -13
- package/dist/chunks/chunk-7YSO2OQ2.js +113 -0
- package/dist/chunks/{chunk-IEHKRARD.js → chunk-BHR3ZG6U.js} +10 -8
- package/dist/chunks/{chunk-J472ERN2.js → chunk-ERF5YVP4.js} +15 -4
- package/dist/chunks/{chunk-XYLJ2GCB.js → chunk-JMSIHEYX.js} +55 -7
- package/dist/chunks/{chunk-MKZP2TBH.js → chunk-L7ZGUU4D.js} +2 -2
- package/dist/chunks/chunk-TUGIPKW5.js +113 -0
- package/dist/chunks/go-HZ6XTFCK.js +31 -0
- package/dist/chunks/{highlight-GJHHV4KM.js → highlight-IBDX4XWS.js} +1 -1
- package/dist/chunks/{highlight-XTHRKBA3.js → highlight-M2ELQNYK.js} +1 -1
- package/dist/chunks/python-BMPKDKNO.js +32 -0
- package/dist/chunks/runtime-entry.js +1 -1
- package/dist/chunks/rust-2BWKI2MN.js +35 -0
- package/dist/chunks/{velin-accordion-FBIPAEXG.js → velin-accordion-WSY6BMLA.js} +64 -10
- package/dist/chunks/{velin-bottom-nav-7GEMPIM6.js → velin-bottom-nav-5RVRNGIP.js} +25 -0
- package/dist/chunks/{velin-bottom-nav-2N2TYWOF.js → velin-bottom-nav-N5F4UO2I.js} +26 -1
- package/dist/chunks/velin-calendar-OFE6THUD.js +210 -0
- package/dist/chunks/velin-calendar-PGVNJTXK.js +217 -0
- package/dist/chunks/velin-calendar-XC7JRDSH.js +217 -0
- package/dist/chunks/{velin-code-block-CIAURDRJ.js → velin-code-block-ITCLIC6T.js} +1 -1
- package/dist/chunks/{velin-code-block-TSUKPY4C.js → velin-code-block-ZGWG7JI5.js} +12 -3
- package/dist/chunks/{velin-combobox-NXS4QB4R.js → velin-combobox-56V2ZWT5.js} +1 -1
- package/dist/chunks/{velin-command-ONKO2A3J.js → velin-command-2HO2EKR6.js} +29 -2
- package/dist/chunks/{velin-command-TMLSRGIU.js → velin-command-ZJSLPK7B.js} +30 -3
- package/dist/chunks/{velin-copy-NYDBOKXT.js → velin-copy-XDQSV2XU.js} +1 -1
- package/dist/chunks/{velin-countdown-4ZVDGK4R.js → velin-countdown-V4Z5MA63.js} +1 -1
- package/dist/chunks/velin-data-table-KK7IDGTP.js +302 -0
- package/dist/chunks/velin-data-table-UVR42UUM.js +372 -0
- package/dist/chunks/{velin-dialog-GTTZI564.js → velin-dialog-3YK2RPRV.js} +9 -2
- package/dist/chunks/{velin-dialog-ELFOZIXZ.js → velin-dialog-S24KAP4S.js} +3 -1
- package/dist/chunks/{velin-drawer-BE6TYN3H.js → velin-drawer-5PNU624J.js} +2 -4
- package/dist/chunks/{velin-drawer-DLL2GLT6.js → velin-drawer-CCM6S25D.js} +3 -5
- package/dist/chunks/{velin-email-RBX6ZR4S.js → velin-email-TNFONXPW.js} +1 -1
- package/dist/chunks/velin-file-dropzone-QRGF2JNA.js +154 -0
- package/dist/chunks/velin-file-dropzone-XIZMJ3NB.js +154 -0
- package/dist/chunks/velin-form-summary-XRQ7COQH.js +273 -0
- package/dist/chunks/{velin-icon-7M76DOPB.js → velin-icon-A6NCXGUK.js} +59 -17
- package/dist/chunks/{velin-icon-ZTSKUOVN.js → velin-icon-EC4B5XCM.js} +18 -5
- package/dist/chunks/{velin-lightbox-6E7CAATP.js → velin-lightbox-FFK4TUUP.js} +7 -6
- package/dist/chunks/{velin-lightbox-I75U6JF4.js → velin-lightbox-P725FLTY.js} +15 -5
- package/dist/chunks/{velin-menubar-J66JXRWW.js → velin-menubar-6I2LM5HL.js} +1 -1
- package/dist/chunks/{velin-modal-5HYNNNUI.js → velin-modal-ICTMGRSU.js} +7 -8
- package/dist/chunks/{velin-modal-AEOTZ67O.js → velin-modal-JBNODH7D.js} +2 -4
- package/dist/chunks/{velin-popover-T72HK3DF.js → velin-popover-WZG2GHBL.js} +25 -4
- package/dist/chunks/{velin-rating-N4DI2XNL.js → velin-rating-QBBYRRBU.js} +1 -1
- package/dist/chunks/{velin-search-7B4KJ6TX.js → velin-search-B3L6J2RP.js} +2 -2
- package/dist/chunks/{velin-search-2XRQFBCS.js → velin-search-L3UG6NDQ.js} +5 -4
- package/dist/chunks/{velin-secure-field-6PV7RZDJ.js → velin-secure-field-DGYMP7OK.js} +1 -1
- package/dist/chunks/{velin-segmented-control-GYD5BIXQ.js → velin-segmented-control-P74GJPAR.js} +1 -1
- package/dist/chunks/{velin-sheet-R2Y67X6H.js → velin-sheet-M5GZZD7G.js} +2 -4
- package/dist/chunks/{velin-sheet-OUK6572T.js → velin-sheet-X7RXIWKV.js} +3 -5
- package/dist/chunks/{velin-stepper-X54WGHDG.js → velin-stepper-4YQJUHPC.js} +12 -5
- package/dist/chunks/{velin-toast-OMMOA2DK.js → velin-toast-UG5LWTG2.js} +5 -1
- package/dist/chunks/{velin-tooltip-BDHGRTQB.js → velin-tooltip-ZDH5SDLU.js} +1 -1
- package/dist/chunks/yaml-O67LEQYT.js +26 -0
- package/dist/llms.txt +29 -7
- package/dist/search-index.json +182 -10
- package/dist/velin-agent.json +3997 -26
- package/dist/velinstyle-components.iife.js +1952 -287
- package/dist/velinstyle-components.js +1956 -287
- package/dist/velinstyle-components.min.js +267 -119
- package/dist/velinstyle.css +381 -67
- package/dist/velinstyle.d.ts +4 -0
- package/dist/velinstyle.min.css +1 -1
- package/package.json +26 -6
- package/packages/skill-engine/README.md +24 -0
- package/packages/skill-engine/package.json +7 -0
- package/packages/skill-engine/src/catalog.js +29 -0
- package/packages/skill-engine/src/index.js +5 -0
- package/packages/skill-engine/src/install.js +69 -0
- package/packages/skill-engine/src/registry.js +27 -0
- package/packages/skill-engine/src/resolve.js +68 -0
- package/packages/skill-engine/src/validate.js +40 -0
- package/packages/velinstyle-skills/README.md +6 -0
- package/packages/velinstyle-skills/adapter.json +8 -0
- package/packages/velinstyle-skills/bundles/landing-starter.bundle.yaml +12 -0
- package/packages/velinstyle-skills/bundles/review-suite.bundle.yaml +11 -0
- package/packages/velinstyle-skills/catalog.json +1004 -0
- package/packages/velinstyle-skills/graphs/component-ship.graph.yaml +14 -0
- package/packages/velinstyle-skills/graphs/landingpage.graph.yaml +21 -0
- package/packages/velinstyle-skills/graphs/release-gate.graph.yaml +15 -0
- package/packages/velinstyle-skills/index.js +22 -0
- package/packages/velinstyle-skills/package.json +7 -0
- package/packages/velinstyle-skills/packs/accessibility.pack.yaml +9 -0
- package/packages/velinstyle-skills/packs/design.pack.yaml +10 -0
- package/packages/velinstyle-skills/packs/documentation.pack.yaml +8 -0
- package/packages/velinstyle-skills/packs/enterprise.pack.yaml +10 -0
- package/packages/velinstyle-skills/packs/frontend.pack.yaml +10 -0
- package/packages/velinstyle-skills/packs/review.pack.yaml +9 -0
- package/packages/velinstyle-skills/projects/admin-panel.project.yaml +12 -0
- package/packages/velinstyle-skills/projects/blog.project.yaml +11 -0
- package/packages/velinstyle-skills/projects/documentation-portal.project.yaml +13 -0
- package/packages/velinstyle-skills/projects/ecommerce-shop.project.yaml +12 -0
- package/packages/velinstyle-skills/projects/landing-page.project.yaml +12 -0
- package/packages/velinstyle-skills/projects/laravel-dashboard.project.yaml +12 -0
- package/packages/velinstyle-skills/projects/marketing-website.project.yaml +14 -0
- package/packages/velinstyle-skills/projects/portfolio.project.yaml +12 -0
- package/packages/velinstyle-skills/registry.json +142 -0
- package/packages/velinstyle-skills/scripts/generate-assets.mjs +163 -0
- package/packages/velinstyle-skills/skills/velin-a11y-focus-keyboard/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-a11y-page-checklist/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-a11y-scan-fix/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-ai-agent-briefing/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-ai-plan-scaffold-review/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-ai-skill-authoring/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-components-accessibility-map/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-components-api-contract/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-components-recipe/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-components-select-compose/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-design-hero-compose/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-design-section-rhythm/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-design-surface-roles/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-dev-component-wire/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-dev-page-scaffold/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-dev-plan-first/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-docs-component-guide/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-docs-generate-api/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-dx-doctor-setup/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-dx-project-init/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-migration-legacy-wc/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-migration-prefix-classes/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-motion-reduced-safe/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-motion-reveal-feedback/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-perf-audit-fix/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-perf-runtime-tree-shake/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-release-changelog-sync/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-release-meta-rebuild/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-review-conversion-hero/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-review-page-gate/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-review-seo-meta/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-security-sanitize-html/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-security-scan-pii/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-testing-a11y-suite/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-testing-visual-smoke/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-themes-apply-token/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-themes-contrast-check/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-utilities-layout-apply/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-utilities-prefix-fix/SKILL.md +57 -0
- package/packages/velinstyle-skills/skills/velin-utilities-token-map/SKILL.md +57 -0
- package/packages/velinstyle-skills/templates/admin-panel.template.yaml +12 -0
- package/packages/velinstyle-skills/templates/blog.template.yaml +12 -0
- package/packages/velinstyle-skills/templates/documentation-portal.template.yaml +12 -0
- package/packages/velinstyle-skills/templates/ecommerce-shop.template.yaml +13 -0
- package/packages/velinstyle-skills/templates/landing-page.template.yaml +12 -0
- package/packages/velinstyle-skills/templates/laravel-dashboard.template.yaml +11 -0
- package/packages/velinstyle-skills/templates/marketing-website.template.yaml +13 -0
- package/packages/velinstyle-skills/templates/portfolio.template.yaml +12 -0
- package/schemas/component-knowledge.schema.json +33 -0
- package/schemas/design-constraint.schema.json +15 -0
- package/schemas/design-intelligence.schema.json +19 -0
- package/schemas/design-rule.schema.json +19 -0
- package/schemas/page-template.schema.json +24 -0
- package/schemas/review-report.schema.json +36 -0
- package/schemas/section.schema.json +22 -0
- package/schemas/skill-bundle.schema.json +15 -0
- package/schemas/skill-graph.schema.json +25 -0
- package/schemas/skill-pack.schema.json +14 -0
- package/schemas/skill-project.schema.json +16 -0
- package/schemas/skill-record.schema.json +27 -0
- package/schemas/skill-registry.schema.json +17 -0
- package/schemas/skill-template.schema.json +17 -0
- package/schemas/token-graph.schema.json +27 -0
- package/schemas/utility.schema.json +166 -0
- package/src/base/wc-placeholder.css +7 -0
- package/src/components/alert.css +22 -1
- package/src/components/calendar-dropzone.css +5 -0
- package/src/components/data-table.css +93 -0
- package/src/components/form-validation.css +40 -0
- package/src/layout/app-shell.css +33 -0
- package/src/tokens/shadow.css +13 -0
- package/src/utilities/border.css +2 -0
- package/src/utilities/display.css +1 -24
- package/src/utilities/responsive.css +16 -1
- package/src/utilities/spacing.css +32 -0
- package/src/velinstyle.css +3 -0
- package/dist/chunks/attributes-3BZLI7XF.js +0 -364
- package/dist/chunks/attributes-4N5JP4CG.js +0 -364
- package/dist/chunks/attributes-AIR7SVLK.js +0 -364
- package/dist/chunks/attributes-BGRN5XZO.js +0 -364
- package/dist/chunks/attributes-BVSQAYLR.js +0 -364
- package/dist/chunks/attributes-CDYFUY7Y.js +0 -364
- package/dist/chunks/attributes-JMYJZBPE.js +0 -364
- package/dist/chunks/attributes-R5JY3X4O.js +0 -374
- package/dist/chunks/attributes-TNSPQTHX.js +0 -374
- package/dist/chunks/chunk-2354NEBP.js +0 -109
- package/dist/chunks/chunk-2VNA7G7S.js +0 -109
- package/dist/chunks/chunk-AQLRJSGG.js +0 -109
- package/dist/chunks/chunk-EWPSDMQT.js +0 -109
- package/dist/chunks/chunk-LA7264K4.js +0 -85
- package/dist/chunks/chunk-QTJJ3DPO.js +0 -109
- package/dist/chunks/chunk-RZQVWFHW.js +0 -109
- package/dist/chunks/chunk-U4XHMZ7D.js +0 -109
- package/dist/chunks/chunk-V5YIUA6G.js +0 -109
- package/dist/chunks/chunk-ZPSPKUYD.js +0 -109
- package/dist/chunks/velin-bottom-nav-N3DFQR66.js +0 -86
- package/dist/chunks/velin-carousel-Y5VCOABZ.js +0 -213
- package/dist/chunks/velin-code-block-ASUBTAQV.js +0 -119
- package/dist/chunks/velin-code-block-Y6CM726Q.js +0 -119
- package/dist/chunks/velin-combobox-2KDTCQ7N.js +0 -155
- package/dist/chunks/velin-command-M7RNHUPR.js +0 -136
- package/dist/chunks/velin-copy-T5KDTDXP.js +0 -81
- package/dist/chunks/velin-countdown-5XSTN3BH.js +0 -101
- package/dist/chunks/velin-counter-ISCTEAHI.js +0 -118
- package/dist/chunks/velin-dialog-D2GUHSCD.js +0 -144
- package/dist/chunks/velin-dialog-IJLFDXAJ.js +0 -144
- package/dist/chunks/velin-dialog-TNS3WXMB.js +0 -144
- package/dist/chunks/velin-drawer-MJS6WCF7.js +0 -130
- package/dist/chunks/velin-email-N5B5ZYTE.js +0 -90
- package/dist/chunks/velin-icon-KZO3IEHG.js +0 -164
- package/dist/chunks/velin-lightbox-QOTB4PRL.js +0 -140
- package/dist/chunks/velin-lightbox-UCD6N7HF.js +0 -140
- package/dist/chunks/velin-live-dot-XHUZCIDZ.js +0 -67
- package/dist/chunks/velin-menubar-SFEXJE56.js +0 -86
- package/dist/chunks/velin-modal-2HOBXE6F.js +0 -176
- package/dist/chunks/velin-modal-GLI2XZZB.js +0 -176
- package/dist/chunks/velin-popover-EXINRIGJ.js +0 -145
- package/dist/chunks/velin-popover-OXX7MRZ6.js +0 -145
- package/dist/chunks/velin-popover-YKCCROJG.js +0 -145
- package/dist/chunks/velin-rating-XRQJV4LO.js +0 -91
- package/dist/chunks/velin-scroll-top-F2FCZNYD.js +0 -58
- package/dist/chunks/velin-search-4YHJWYSS.js +0 -556
- package/dist/chunks/velin-search-LWHY23Q5.js +0 -474
- package/dist/chunks/velin-secure-field-H7FSBUJW.js +0 -95
- package/dist/chunks/velin-segmented-control-PQX6LKOJ.js +0 -109
- package/dist/chunks/velin-sheet-3OGI4HD7.js +0 -119
- package/dist/chunks/velin-sparkline-GVEANJVW.js +0 -184
- package/dist/chunks/velin-sparkline-KDA3GQAN.js +0 -162
- package/dist/chunks/velin-stepper-2ZIHLKGM.js +0 -141
- package/dist/chunks/velin-stepper-FYEUEWE6.js +0 -141
- package/dist/chunks/velin-stepper-wc-QU3KRVQR.js +0 -133
- package/dist/chunks/velin-theme-toggle-BNNS7WLV.js +0 -285
- package/dist/chunks/velin-theme-toggle-NO7V4TWI.js +0 -285
- package/dist/chunks/velin-theme-toggle-QELCJM2Q.js +0 -291
- package/dist/chunks/velin-toast-RCZ4CSUC.js +0 -109
- package/dist/chunks/velin-toast-WEUBTJG6.js +0 -109
- package/dist/chunks/velin-tooltip-664IOHZA.js +0 -112
- package/dist/chunks/velin-tooltip-wc-LXDVT6DE.js +0 -103
- package/dist/chunks/worker-client-R6DBYPFT.js +0 -46
package/cli/scaffold.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Prompt-based scaffolding:
|
|
2
|
+
* Prompt-based scaffolding: plan-first for pages; recipe fallback for fragments.
|
|
3
3
|
*/
|
|
4
4
|
import { readFileSync } from 'fs';
|
|
5
5
|
import { dirname, join } from 'path';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { emitBlueprint } from './blueprint.js';
|
|
8
8
|
import { auditHtml, suggestFromIssues } from './layout-audit.js';
|
|
9
|
+
import { planFromPrompt, analyzePrompt } from './prompt-engine.js';
|
|
10
|
+
import { reviewHtml } from './review.js';
|
|
9
11
|
|
|
10
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
13
|
const RECIPES_PATH = join(__dirname, 'scaffold-recipes.json');
|
|
@@ -14,10 +16,59 @@ function loadRecipes() {
|
|
|
14
16
|
return JSON.parse(readFileSync(RECIPES_PATH, 'utf-8'));
|
|
15
17
|
}
|
|
16
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Fragment intents (navbar, modal, …) stay recipe-based.
|
|
21
|
+
* Full pages go through the Prompt Engine (plan → render → review).
|
|
22
|
+
* Low-confidence hero-only fallback is removed in favor of page planning.
|
|
23
|
+
* @param {string} prompt
|
|
24
|
+
*/
|
|
25
|
+
function isPagePrompt(prompt) {
|
|
26
|
+
const recipes = loadRecipes();
|
|
27
|
+
const lower = (prompt || '').toLowerCase();
|
|
28
|
+
|
|
29
|
+
const pageHints = [
|
|
30
|
+
'landing page', 'landingpage', 'homepage', 'website', 'webseite',
|
|
31
|
+
'steuerberater', 'lawyer', 'kanzlei', 'tax advisor', 'attorney',
|
|
32
|
+
'clinic', 'healthcare', 'praxis', 'restaurant', 'saas', 'portfolio',
|
|
33
|
+
'agency', 'corporate', 'shop', 'online store', 'blog post page',
|
|
34
|
+
'course landing', 'community site', 'event page', 'page for',
|
|
35
|
+
];
|
|
36
|
+
if (pageHints.some((h) => lower.includes(h))) return true;
|
|
37
|
+
|
|
38
|
+
let bestRecipe = { id: null, score: 0 };
|
|
39
|
+
for (const [id, def] of Object.entries(recipes.intents)) {
|
|
40
|
+
let score = 0;
|
|
41
|
+
for (const kw of def.keywords) {
|
|
42
|
+
if (lower.includes(kw.toLowerCase())) score += kw.length;
|
|
43
|
+
}
|
|
44
|
+
if (score > bestRecipe.score) bestRecipe = { id, score };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const fragmentIds = new Set([
|
|
48
|
+
'navbar', 'modal', 'card', 'dashboard', 'login', 'footer', 'pricing', 'empty', 'table', 'onboarding',
|
|
49
|
+
]);
|
|
50
|
+
if (bestRecipe.id && fragmentIds.has(bestRecipe.id) && bestRecipe.score >= 5) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Hero-only recipe with "landing" keyword but no full-page phrasing → still plan a page
|
|
55
|
+
if (bestRecipe.id === 'hero' && /landing|homepage|website|webseite|seite/i.test(prompt)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const analysis = analyzePrompt(prompt);
|
|
60
|
+
return analysis.confidence === 'high' && !fragmentIds.has(analysis.pageId);
|
|
61
|
+
}
|
|
62
|
+
|
|
17
63
|
/**
|
|
18
64
|
* @param {string} prompt
|
|
19
65
|
*/
|
|
20
66
|
export function parseIntent(prompt) {
|
|
67
|
+
if (isPagePrompt(prompt)) {
|
|
68
|
+
const analysis = analyzePrompt(prompt);
|
|
69
|
+
return { id: analysis.pageId || 'landing', confidence: analysis.confidence, mode: 'plan' };
|
|
70
|
+
}
|
|
71
|
+
|
|
21
72
|
const recipes = loadRecipes();
|
|
22
73
|
const lower = prompt.toLowerCase();
|
|
23
74
|
let best = { id: recipes.fallback.intent, confidence: recipes.fallback.confidence, score: 0 };
|
|
@@ -32,7 +83,7 @@ export function parseIntent(prompt) {
|
|
|
32
83
|
}
|
|
33
84
|
}
|
|
34
85
|
|
|
35
|
-
return { id: best.id, confidence: best.score > 0 ? best.confidence : 'low' };
|
|
86
|
+
return { id: best.id, confidence: best.score > 0 ? best.confidence : 'low', mode: 'recipe' };
|
|
36
87
|
}
|
|
37
88
|
|
|
38
89
|
/**
|
|
@@ -108,7 +159,68 @@ export function scaffoldFromPrompt(prompt, options = {}) {
|
|
|
108
159
|
return { ok: false, error: 'Prompt is required.' };
|
|
109
160
|
}
|
|
110
161
|
|
|
162
|
+
if (isPagePrompt(prompt)) {
|
|
163
|
+
const planned = planFromPrompt(prompt);
|
|
164
|
+
if (!planned.ok) return planned;
|
|
165
|
+
|
|
166
|
+
const review = reviewHtml(planned.html, { plan: planned.plan, prompt });
|
|
167
|
+
const banner =
|
|
168
|
+
`<!-- Generated by velinstyle scaffold (1.2.0) — mode: plan, page: ${planned.plan.page?.id}, confidence: ${planned.analysis.confidence} -->\n` +
|
|
169
|
+
`<!-- Sections: ${(planned.plan.sections || []).map((s) => s.id).join(', ')} — run: velinstyle review -->\n`;
|
|
170
|
+
|
|
171
|
+
const fullHtml = banner + planned.html;
|
|
172
|
+
const issues = auditHtml(fullHtml);
|
|
173
|
+
const responsiveHints = suggestFromIssues(issues);
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
ok: true,
|
|
177
|
+
mode: 'plan',
|
|
178
|
+
intent: planned.plan.page?.id || planned.analysis.pageId,
|
|
179
|
+
confidence: planned.analysis.confidence,
|
|
180
|
+
blueprints: (planned.plan.sections || []).map((s) => s.blueprint),
|
|
181
|
+
plan: planned.plan,
|
|
182
|
+
analysis: planned.analysis,
|
|
183
|
+
review,
|
|
184
|
+
html: fullHtml,
|
|
185
|
+
slots: {
|
|
186
|
+
brand: 'Brand',
|
|
187
|
+
title: /steuerberater/i.test(prompt) ? 'Steuerberatung mit Klarheit' : 'Headline',
|
|
188
|
+
cta: /kontakt|consultation|beratung/i.test(prompt) ? 'Kontakt aufnehmen' : 'Primary CTA',
|
|
189
|
+
},
|
|
190
|
+
missing: planned.missing,
|
|
191
|
+
warnings: planned.warnings,
|
|
192
|
+
responsiveHints,
|
|
193
|
+
nextSteps: ['velinstyle review <file>', 'velinstyle layout suggest <file>', 'velinstyle scan <file>'],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
111
197
|
const intent = parseIntent(prompt);
|
|
198
|
+
// No silent hero-only fallback — unknown prompts plan a landing page.
|
|
199
|
+
if (intent.mode !== 'plan' && intent.id === 'hero' && intent.confidence === 'low') {
|
|
200
|
+
const planned = planFromPrompt(prompt);
|
|
201
|
+
if (!planned.ok) return planned;
|
|
202
|
+
const review = reviewHtml(planned.html, { plan: planned.plan, prompt });
|
|
203
|
+
const fullHtml =
|
|
204
|
+
`<!-- Generated by velinstyle scaffold (1.2.0) — mode: plan-fallback, page: ${planned.plan.page?.id} -->\n` +
|
|
205
|
+
planned.html;
|
|
206
|
+
return {
|
|
207
|
+
ok: true,
|
|
208
|
+
mode: 'plan',
|
|
209
|
+
intent: planned.plan.page?.id || 'landing',
|
|
210
|
+
confidence: planned.analysis.confidence,
|
|
211
|
+
blueprints: (planned.plan.sections || []).map((s) => s.blueprint),
|
|
212
|
+
plan: planned.plan,
|
|
213
|
+
analysis: planned.analysis,
|
|
214
|
+
review,
|
|
215
|
+
html: fullHtml,
|
|
216
|
+
slots: extractSlots(prompt),
|
|
217
|
+
missing: planned.missing,
|
|
218
|
+
warnings: [...(planned.warnings || []), 'Used page planner instead of silent hero fallback.'],
|
|
219
|
+
responsiveHints: suggestFromIssues(auditHtml(fullHtml)),
|
|
220
|
+
nextSteps: ['velinstyle review <file>', 'velinstyle layout suggest <file>', 'velinstyle scan <file>'],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
112
224
|
const slots = extractSlots(prompt);
|
|
113
225
|
const blueprintIds = resolveBlueprints(intent.id, { prompt, slots });
|
|
114
226
|
|
|
@@ -125,22 +237,25 @@ export function scaffoldFromPrompt(prompt, options = {}) {
|
|
|
125
237
|
html = applySlots(html, slots);
|
|
126
238
|
|
|
127
239
|
const banner =
|
|
128
|
-
`<!-- Generated by velinstyle scaffold (
|
|
240
|
+
`<!-- Generated by velinstyle scaffold (1.2.0) — mode: recipe, intent: ${intent.id}, confidence: ${intent.confidence} -->\n` +
|
|
129
241
|
`<!-- Blueprints: ${blueprintIds.join(', ')} — run: velinstyle layout suggest & velinstyle scan -->\n`;
|
|
130
242
|
|
|
131
243
|
const fullHtml = banner + html;
|
|
132
244
|
const issues = auditHtml(fullHtml);
|
|
133
245
|
const responsiveHints = suggestFromIssues(issues);
|
|
246
|
+
const review = reviewHtml(fullHtml, { prompt });
|
|
134
247
|
|
|
135
248
|
return {
|
|
136
249
|
ok: true,
|
|
250
|
+
mode: 'recipe',
|
|
137
251
|
intent: intent.id,
|
|
138
252
|
confidence: intent.confidence,
|
|
139
253
|
blueprints: blueprintIds,
|
|
140
254
|
html: fullHtml,
|
|
141
255
|
slots,
|
|
256
|
+
review,
|
|
142
257
|
responsiveHints,
|
|
143
|
-
nextSteps: ['velinstyle layout suggest <file>', 'velinstyle scan <file>'],
|
|
258
|
+
nextSteps: ['velinstyle review <file>', 'velinstyle layout suggest <file>', 'velinstyle scan <file>'],
|
|
144
259
|
};
|
|
145
260
|
}
|
|
146
261
|
|
|
@@ -44,6 +44,13 @@ export const SCANNER_RULES = [
|
|
|
44
44
|
{ id: 'a11y/iframe-title', category: 'a11y', severity: 'error', message: '<iframe> without title.', fixHint: 'Add descriptive title attribute.' },
|
|
45
45
|
{ id: 'a11y/autocomplete-auth', category: 'a11y', severity: 'warning', message: 'Auth field missing autocomplete (WCAG 2.2).', fixHint: 'Add autocomplete="username" or "current-password".' },
|
|
46
46
|
{ id: 'a11y/invalid-describedby', category: 'a11y', severity: 'warning', message: 'aria-invalid without aria-describedby.', fixHint: 'Link to error message element id.' },
|
|
47
|
+
{ id: 'a11y/duplicate-id', category: 'a11y', severity: 'error', message: 'Duplicate id attribute in the document.', fixHint: 'Make every id unique.' },
|
|
48
|
+
{ id: 'a11y/target-size-min', category: 'a11y', severity: 'warning', message: 'Interactive control appears smaller than ~24px (WCAG 2.5.8 heuristic).', fixHint: 'Increase hit target to at least 24×24 CSS px.' },
|
|
49
|
+
{ id: 'a11y/nested-interactive', category: 'a11y', severity: 'error', message: 'Nested interactive elements (button/a/role).', fixHint: 'Use a single control; do not nest button/a/role=button.' },
|
|
50
|
+
{ id: 'a11y/contrast-inline', category: 'a11y', severity: 'error', message: 'Inline or authored color pair fails WCAG AA contrast.', fixHint: 'Raise contrast to ≥4.5:1 (or ≥3:1 large text).' },
|
|
51
|
+
{ id: 'a11y/role-button-contract', category: 'a11y', severity: 'warning', message: 'role=button without tabindex and/or Enter/Space handling.', fixHint: 'Prefer native <button>, or tabindex="0" + keydown for Enter/Space.' },
|
|
52
|
+
{ id: 'css/unknown-velin-class', category: 'css', severity: 'warning', message: 'Unknown velin-* utility/component class.', fixHint: 'Use classes from the CSS bundle or blueprint --strict.' },
|
|
53
|
+
{ id: 'wc/invalid-attribute', category: 'wc', severity: 'warning', message: 'Attribute is not part of this Web Component API.', fixHint: 'Use observed / documented attributes only.' },
|
|
47
54
|
// CSS
|
|
48
55
|
{ id: 'css/var-fallback', category: 'css', severity: 'info', message: 'CSS variable without fallback in var().', fixHint: 'Use var(--velin-*, fallback).' },
|
|
49
56
|
{ id: 'css/z-index-token', category: 'css', severity: 'warning', message: 'Raw z-index instead of --velin-z-* token.', fixHint: 'Use tokens from z-index.css — --fix suggests token.' },
|
package/cli/scanner.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs';
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync, statSync } from 'fs';
|
|
2
2
|
import { join, relative, extname } from 'path';
|
|
3
3
|
import { applyFixes, fixSafeExternalLinkLine, fixZIndexLine } from './apply-fixes.js';
|
|
4
4
|
import { scanPIIHTML, scanPIIJS } from './pii-scanner.js';
|
|
5
|
+
import { loadKnownCssClasses } from './blueprint.js';
|
|
6
|
+
import { contrastFromInlineStyle, parseColor, contrastRatio, relLuminance } from './contrast-utils.js';
|
|
5
7
|
|
|
6
8
|
export { SCANNER_RULES, PERF_RULES } from './scanner-rules-data.js';
|
|
7
9
|
|
|
@@ -37,6 +39,19 @@ function walkFiles(dir, exts, ignore = DEFAULT_IGNORE) {
|
|
|
37
39
|
return results;
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
/** Accept a single file or a directory (same pattern as layout-audit). */
|
|
43
|
+
function collectFiles(targetPath, exts, ignore = DEFAULT_IGNORE) {
|
|
44
|
+
if (!existsSync(targetPath)) return [];
|
|
45
|
+
const st = statSync(targetPath);
|
|
46
|
+
if (st.isFile()) {
|
|
47
|
+
return exts.includes(extname(targetPath).toLowerCase()) ? [targetPath] : [];
|
|
48
|
+
}
|
|
49
|
+
if (st.isDirectory()) {
|
|
50
|
+
return walkFiles(targetPath, exts, ignore);
|
|
51
|
+
}
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
|
|
40
55
|
// ── Security Scanner ─────────────────────────────────────────────────────────
|
|
41
56
|
|
|
42
57
|
const INLINE_EVENTS = [
|
|
@@ -102,6 +117,15 @@ function scanSecurityHTML(content, file) {
|
|
|
102
117
|
});
|
|
103
118
|
}
|
|
104
119
|
|
|
120
|
+
if (/document\.write\s*\(/.test(line)) {
|
|
121
|
+
issues.push({
|
|
122
|
+
file, line: ln, severity: 0,
|
|
123
|
+
rule: 'security/no-document-write',
|
|
124
|
+
message: 'document.write() detected in HTML/script. This is dangerous and blocks parsing.',
|
|
125
|
+
fixable: false,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
105
129
|
if (/(?:href|src)\s*=\s*["']data:text\/html/i.test(line)) {
|
|
106
130
|
issues.push({
|
|
107
131
|
file, line: ln, severity: 0,
|
|
@@ -203,10 +227,311 @@ function issueCategory(rule) {
|
|
|
203
227
|
if (rule.startsWith('pii/')) return 'pii';
|
|
204
228
|
if (rule.startsWith('a11y/')) return 'a11y';
|
|
205
229
|
if (rule.startsWith('css/')) return 'css';
|
|
230
|
+
if (rule.startsWith('wc/')) return 'wc';
|
|
206
231
|
if (rule.startsWith('perf/')) return 'perf';
|
|
207
232
|
return 'other';
|
|
208
233
|
}
|
|
209
234
|
|
|
235
|
+
/** Author-facing attrs per tag (observed + documented aliases). Global HTML attrs ignored. */
|
|
236
|
+
const WC_ATTR_ALLOW = {
|
|
237
|
+
'velin-tooltip': new Set(['content', 'placement']),
|
|
238
|
+
'velin-copy': new Set(['value', 'text', 'label']),
|
|
239
|
+
'velin-icon': new Set(['name', 'size', 'label', 'provider', 'variant', 'sprite', 'aria-label', 'aria-hidden', 'class']),
|
|
240
|
+
'velin-sparkline': new Set(['values', 'width', 'height', 'min', 'max', 'area', 'glow', 'animate', 'label']),
|
|
241
|
+
'velin-data-table': new Set(['page-size', 'filter-input', 'empty-text', 'label', 'editable', 'sortable', 'class']),
|
|
242
|
+
'velin-form-summary': new Set([]),
|
|
243
|
+
'velin-theme-toggle': new Set(['themes-base', 'theme', 'themes']),
|
|
244
|
+
'velin-calendar': new Set(['value', 'min', 'max', 'label', 'class']),
|
|
245
|
+
'velin-file-dropzone': new Set(['accept', 'multiple', 'label', 'progress', 'class']),
|
|
246
|
+
'velin-code-block': new Set(['language', 'class']),
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const WC_GLOBAL_ATTRS = new Set([
|
|
250
|
+
'id', 'class', 'slot', 'part', 'style', 'hidden', 'title', 'role', 'tabindex',
|
|
251
|
+
'aria-label', 'aria-labelledby', 'aria-describedby', 'aria-hidden', 'aria-expanded',
|
|
252
|
+
'aria-controls', 'aria-live', 'aria-atomic', 'dir', 'lang', 'data-velin-component',
|
|
253
|
+
]);
|
|
254
|
+
|
|
255
|
+
function lineNumberAt(content, index) {
|
|
256
|
+
return content.slice(0, Math.max(0, index)).split('\n').length;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function scanDuplicateIds(content, file) {
|
|
260
|
+
const issues = [];
|
|
261
|
+
const seen = new Map();
|
|
262
|
+
for (const m of content.matchAll(/\bid\s*=\s*["']([^"']+)["']/gi)) {
|
|
263
|
+
const id = m[1];
|
|
264
|
+
const ln = lineNumberAt(content, m.index);
|
|
265
|
+
if (seen.has(id)) {
|
|
266
|
+
issues.push({
|
|
267
|
+
file,
|
|
268
|
+
line: ln,
|
|
269
|
+
severity: 0,
|
|
270
|
+
rule: 'a11y/duplicate-id',
|
|
271
|
+
message: `Duplicate id="${id}" (also at line ${seen.get(id)}).`,
|
|
272
|
+
fixable: false,
|
|
273
|
+
});
|
|
274
|
+
} else {
|
|
275
|
+
seen.set(id, ln);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return issues;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function scanUnknownVelinClasses(content, file, knownClasses) {
|
|
282
|
+
const issues = [];
|
|
283
|
+
if (!knownClasses) return issues;
|
|
284
|
+
for (const m of content.matchAll(/\bclass\s*=\s*["']([^"']+)["']/gi)) {
|
|
285
|
+
const ln = lineNumberAt(content, m.index);
|
|
286
|
+
for (const token of m[1].split(/\s+/)) {
|
|
287
|
+
if (!token.startsWith('velin-')) continue;
|
|
288
|
+
if (!knownClasses.has(token)) {
|
|
289
|
+
issues.push({
|
|
290
|
+
file,
|
|
291
|
+
line: ln,
|
|
292
|
+
severity: 1,
|
|
293
|
+
rule: 'css/unknown-velin-class',
|
|
294
|
+
message: `Unknown VelinStyle class "${token}".`,
|
|
295
|
+
fixable: false,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return issues;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function scanInvalidWcAttributes(content, file) {
|
|
304
|
+
const issues = [];
|
|
305
|
+
for (const m of content.matchAll(/<(velin-[a-z0-9-]+)\b([^>]*)>/gi)) {
|
|
306
|
+
const tag = m[1].toLowerCase();
|
|
307
|
+
const attrBlob = m[2] || '';
|
|
308
|
+
const allow = WC_ATTR_ALLOW[tag];
|
|
309
|
+
if (!allow) continue;
|
|
310
|
+
const ln = lineNumberAt(content, m.index);
|
|
311
|
+
for (const am of attrBlob.matchAll(/([:@]?[a-zA-Z_:][\w:.-]*)\s*=/g)) {
|
|
312
|
+
const name = am[1].toLowerCase();
|
|
313
|
+
if (name.startsWith('on')) continue;
|
|
314
|
+
if (name.startsWith('aria-') || name.startsWith('data-')) {
|
|
315
|
+
// data-velin-copy is wrong API for <velin-copy>; allow only data-* that we document via dataset.source → data-source
|
|
316
|
+
if (tag === 'velin-copy' && name !== 'data-source' && name.startsWith('data-')) {
|
|
317
|
+
issues.push({
|
|
318
|
+
file,
|
|
319
|
+
line: ln,
|
|
320
|
+
severity: 1,
|
|
321
|
+
rule: 'wc/invalid-attribute',
|
|
322
|
+
message: `<${tag}> does not use "${name}". Prefer value="…" or text="…".`,
|
|
323
|
+
fixable: false,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (WC_GLOBAL_ATTRS.has(name) || allow.has(name)) continue;
|
|
329
|
+
issues.push({
|
|
330
|
+
file,
|
|
331
|
+
line: ln,
|
|
332
|
+
severity: 1,
|
|
333
|
+
rule: 'wc/invalid-attribute',
|
|
334
|
+
message: `<${tag}> does not observe attribute "${name}". Allowed: ${[...allow].join(', ') || '(none besides globals)'}.`,
|
|
335
|
+
fixable: false,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return issues;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function collectTinyTapClasses(content) {
|
|
343
|
+
const tiny = new Set();
|
|
344
|
+
for (const block of content.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/gi)) {
|
|
345
|
+
const css = block[1];
|
|
346
|
+
for (const rule of css.matchAll(/\.([a-zA-Z_-][\w-]*)\s*\{([^}]*)\}/g)) {
|
|
347
|
+
const body = rule[2];
|
|
348
|
+
const w = body.match(/(?:^|;)\s*(?:width|inline-size)\s*:\s*([\d.]+)px/i);
|
|
349
|
+
const h = body.match(/(?:^|;)\s*(?:height|block-size)\s*:\s*([\d.]+)px/i);
|
|
350
|
+
const wp = w ? Number(w[1]) : Infinity;
|
|
351
|
+
const hp = h ? Number(h[1]) : Infinity;
|
|
352
|
+
if (wp <= 24 || hp <= 24) tiny.add(rule[1]);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return tiny;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function scanTargetSizeMin(content, file) {
|
|
359
|
+
const issues = [];
|
|
360
|
+
const tinyClasses = collectTinyTapClasses(content);
|
|
361
|
+
const interactiveRe = /<(button|a|input|select|textarea|summary)\b([^>]*)>/gi;
|
|
362
|
+
for (const m of content.matchAll(interactiveRe)) {
|
|
363
|
+
const attrs = m[2] || '';
|
|
364
|
+
const ln = lineNumberAt(content, m.index);
|
|
365
|
+
let tiny = false;
|
|
366
|
+
const styleM = attrs.match(/\bstyle\s*=\s*["']([^"']*)["']/i);
|
|
367
|
+
if (styleM) {
|
|
368
|
+
const s = styleM[1];
|
|
369
|
+
for (const dim of s.matchAll(/(?:width|height|inline-size|block-size)\s*:\s*([\d.]+)px/gi)) {
|
|
370
|
+
if (Number(dim[1]) <= 24) tiny = true;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const classM = attrs.match(/\bclass\s*=\s*["']([^"']*)["']/i);
|
|
374
|
+
if (classM) {
|
|
375
|
+
for (const c of classM[1].split(/\s+/)) {
|
|
376
|
+
if (tinyClasses.has(c)) tiny = true;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (tiny) {
|
|
380
|
+
issues.push({
|
|
381
|
+
file,
|
|
382
|
+
line: ln,
|
|
383
|
+
severity: 1,
|
|
384
|
+
rule: 'a11y/target-size-min',
|
|
385
|
+
message: `<${m[1]}> appears to have a hit target ≤24px (WCAG 2.5.8 heuristic).`,
|
|
386
|
+
fixable: false,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return issues;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function scanNestedInteractive(content, file) {
|
|
394
|
+
const issues = [];
|
|
395
|
+
const html = content
|
|
396
|
+
.replace(/<script\b[\s\S]*?<\/script>/gi, '')
|
|
397
|
+
.replace(/<style\b[\s\S]*?<\/style>/gi, '');
|
|
398
|
+
|
|
399
|
+
for (const m of html.matchAll(/<(button|a)\b([^>]*)>([\s\S]*?)<\/\1>/gi)) {
|
|
400
|
+
const outer = m[1].toLowerCase();
|
|
401
|
+
const innerHtml = m[3] || '';
|
|
402
|
+
const ln = lineNumberAt(content, m.index);
|
|
403
|
+
const nestedTag = innerHtml.match(/<(button|a)\b/i);
|
|
404
|
+
if (nestedTag) {
|
|
405
|
+
issues.push({
|
|
406
|
+
file,
|
|
407
|
+
line: ln,
|
|
408
|
+
severity: 0,
|
|
409
|
+
rule: 'a11y/nested-interactive',
|
|
410
|
+
message: `Nested interactive elements (<${outer}> contains <${nestedTag[1].toLowerCase()}>). Use a single control.`,
|
|
411
|
+
fixable: false,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
if (/\brole\s*=\s*["'](button|link|menuitem)["']/i.test(innerHtml)) {
|
|
415
|
+
const role = innerHtml.match(/\brole\s*=\s*["'](button|link|menuitem)["']/i)[1];
|
|
416
|
+
issues.push({
|
|
417
|
+
file,
|
|
418
|
+
line: ln,
|
|
419
|
+
severity: 0,
|
|
420
|
+
rule: 'a11y/nested-interactive',
|
|
421
|
+
message: `<${outer}> contains an element with role="${role}" (nested interactive).`,
|
|
422
|
+
fixable: false,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// role=button wrapping another interactive
|
|
428
|
+
for (const m of html.matchAll(/<([a-z][\w-]*)\b([^>]*\brole\s*=\s*["']button["'][^>]*)>([\s\S]*?)<\/\1>/gi)) {
|
|
429
|
+
const innerHtml = m[3] || '';
|
|
430
|
+
if (/<(button|a)\b/i.test(innerHtml) || /\brole\s*=\s*["'](button|link)["']/i.test(innerHtml)) {
|
|
431
|
+
issues.push({
|
|
432
|
+
file,
|
|
433
|
+
line: lineNumberAt(content, m.index),
|
|
434
|
+
severity: 0,
|
|
435
|
+
rule: 'a11y/nested-interactive',
|
|
436
|
+
message: `role="button" host contains nested interactive content.`,
|
|
437
|
+
fixable: false,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return issues;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function scanContrastInline(content, file) {
|
|
446
|
+
const issues = [];
|
|
447
|
+
for (const m of content.matchAll(/<([a-z][\w-]*)\b([^>]*?\bstyle\s*=\s*["']([^"']*)["'][^>]*)>/gi)) {
|
|
448
|
+
const style = m[3];
|
|
449
|
+
const result = contrastFromInlineStyle(style);
|
|
450
|
+
if (!result) continue;
|
|
451
|
+
if (!result.pass) {
|
|
452
|
+
issues.push({
|
|
453
|
+
file,
|
|
454
|
+
line: lineNumberAt(content, m.index),
|
|
455
|
+
severity: 0,
|
|
456
|
+
rule: 'a11y/contrast-inline',
|
|
457
|
+
message: `Inline color contrast ${result.ratio.toFixed(2)}:1 fails WCAG AA (${result.min}:1). color=${result.fg} on ${result.bg}.`,
|
|
458
|
+
fixable: false,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Authored <style> blocks: flag pairs of color + background-color in the same rule when both parse
|
|
464
|
+
for (const block of content.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/gi)) {
|
|
465
|
+
const css = block[1];
|
|
466
|
+
const blockStart = block.index ?? 0;
|
|
467
|
+
for (const rule of css.matchAll(/([^{]+)\{([^}]*)\}/g)) {
|
|
468
|
+
const body = rule[2];
|
|
469
|
+
const colorM = body.match(/(?:^|;)\s*color\s*:\s*([^;]+)/i);
|
|
470
|
+
const bgM = body.match(/(?:^|;)\s*(?:background-color|background)\s*:\s*([^;]+)/i);
|
|
471
|
+
if (!colorM || !bgM) continue;
|
|
472
|
+
const fg = parseColor(colorM[1].trim());
|
|
473
|
+
const bgRaw = bgM[1].trim();
|
|
474
|
+
if (/gradient|url\(/i.test(bgRaw)) continue;
|
|
475
|
+
const bg = parseColor(bgRaw.split(/\s+/)[0]);
|
|
476
|
+
if (!fg || !bg) continue;
|
|
477
|
+
const ratio = contrastRatio(relLuminance(fg), relLuminance(bg));
|
|
478
|
+
if (ratio < 4.5) {
|
|
479
|
+
issues.push({
|
|
480
|
+
file,
|
|
481
|
+
line: lineNumberAt(content, blockStart + (rule.index || 0)),
|
|
482
|
+
severity: 0,
|
|
483
|
+
rule: 'a11y/contrast-inline',
|
|
484
|
+
message: `Authored CSS contrast ${ratio.toFixed(2)}:1 fails WCAG AA (4.5:1) in rule "${rule[1].trim().slice(0, 40)}".`,
|
|
485
|
+
fixable: false,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return issues;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const VELIN_WC_WITH_KEYBOARD = new Set([
|
|
494
|
+
'velin-popover', 'velin-collapse', 'velin-dropdown', 'velin-menubar', 'velin-command',
|
|
495
|
+
'velin-tabs', 'velin-accordion', 'velin-dialog', 'velin-modal', 'velin-drawer', 'velin-sheet',
|
|
496
|
+
]);
|
|
497
|
+
|
|
498
|
+
function scanRoleButtonContract(content, file) {
|
|
499
|
+
const issues = [];
|
|
500
|
+
for (const m of content.matchAll(/<([a-z][\w-]*)\b([^>]*\brole\s*=\s*["']button["'][^>]*)>/gi)) {
|
|
501
|
+
const tag = m[1].toLowerCase();
|
|
502
|
+
const attrs = m[2] || '';
|
|
503
|
+
const ln = lineNumberAt(content, m.index);
|
|
504
|
+
if (tag === 'button' || tag === 'summary') continue;
|
|
505
|
+
if (VELIN_WC_WITH_KEYBOARD.has(tag)) continue;
|
|
506
|
+
|
|
507
|
+
const hasTab = /\btabindex\s*=\s*["']?(?:0|-?\d+)/i.test(attrs);
|
|
508
|
+
const hasKeyHandler = /\bon(?:keydown|keyup|keypress)\s*=/i.test(attrs);
|
|
509
|
+
// Look ahead ~800 chars for a nearby script attaching keys (weak); prefer explicit warning
|
|
510
|
+
if (!hasTab) {
|
|
511
|
+
issues.push({
|
|
512
|
+
file,
|
|
513
|
+
line: ln,
|
|
514
|
+
severity: 1,
|
|
515
|
+
rule: 'a11y/role-button-contract',
|
|
516
|
+
message: `role="button" on <${tag}> without tabindex. Use <button> or add tabindex="0" and Enter/Space handlers.`,
|
|
517
|
+
fixable: false,
|
|
518
|
+
});
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (!hasKeyHandler) {
|
|
522
|
+
issues.push({
|
|
523
|
+
file,
|
|
524
|
+
line: ln,
|
|
525
|
+
severity: 1,
|
|
526
|
+
rule: 'a11y/role-button-contract',
|
|
527
|
+
message: `role="button" on <${tag}> lacks Enter/Space keyboard handling. Prefer <button> or handle keydown.`,
|
|
528
|
+
fixable: false,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return issues;
|
|
533
|
+
}
|
|
534
|
+
|
|
210
535
|
// ── Accessibility Scanner ────────────────────────────────────────────────────
|
|
211
536
|
|
|
212
537
|
function scanA11yHTML(content, file) {
|
|
@@ -453,18 +778,26 @@ export function scan(targetPath, options = {}) {
|
|
|
453
778
|
? options.only.split(',').map((s) => s.trim().toLowerCase())
|
|
454
779
|
: null;
|
|
455
780
|
|
|
456
|
-
const htmlFiles =
|
|
457
|
-
const cssFiles =
|
|
458
|
-
const jsFiles =
|
|
781
|
+
const htmlFiles = collectFiles(targetPath, ['.html', '.htm'], ignore);
|
|
782
|
+
const cssFiles = collectFiles(targetPath, ['.css'], ignore);
|
|
783
|
+
const jsFiles = collectFiles(targetPath, ['.js', '.mjs'], ignore);
|
|
459
784
|
|
|
460
785
|
let allIssues = [];
|
|
461
786
|
|
|
462
787
|
const fixEmailPlaceholder = options.fixEmailPlaceholder || 'user@example.com';
|
|
788
|
+
const knownClasses = htmlFiles.length ? loadKnownCssClasses({ includeDist: true }) : null;
|
|
463
789
|
|
|
464
790
|
for (const file of htmlFiles) {
|
|
465
791
|
const content = readFileSync(file, 'utf-8');
|
|
466
792
|
allIssues.push(...scanSecurityHTML(content, file));
|
|
467
793
|
allIssues.push(...scanA11yHTML(content, file));
|
|
794
|
+
allIssues.push(...scanDuplicateIds(content, file));
|
|
795
|
+
allIssues.push(...scanTargetSizeMin(content, file));
|
|
796
|
+
allIssues.push(...scanNestedInteractive(content, file));
|
|
797
|
+
allIssues.push(...scanContrastInline(content, file));
|
|
798
|
+
allIssues.push(...scanRoleButtonContract(content, file));
|
|
799
|
+
allIssues.push(...scanInvalidWcAttributes(content, file));
|
|
800
|
+
allIssues.push(...scanUnknownVelinClasses(content, file, knownClasses));
|
|
468
801
|
allIssues.push(...scanPIIHTML(content, file, { fixEmailPlaceholder }));
|
|
469
802
|
}
|
|
470
803
|
|
|
@@ -520,7 +853,27 @@ export function scan(targetPath, options = {}) {
|
|
|
520
853
|
changedFiles: fixSummary.changedRelPaths,
|
|
521
854
|
};
|
|
522
855
|
}
|
|
856
|
+
if (options.returnData) {
|
|
857
|
+
return { exitCode: errors > 0 ? 1 : 0, ...payload };
|
|
858
|
+
}
|
|
523
859
|
console.log(JSON.stringify(payload, null, 2));
|
|
860
|
+
} else if (options.returnData) {
|
|
861
|
+
return {
|
|
862
|
+
exitCode: errors > 0 ? 1 : 0,
|
|
863
|
+
total: allIssues.length,
|
|
864
|
+
errors,
|
|
865
|
+
warnings,
|
|
866
|
+
infos,
|
|
867
|
+
issues: allIssues.map((i) => ({
|
|
868
|
+
file: relative(targetPath, i.file),
|
|
869
|
+
line: i.line,
|
|
870
|
+
severity: SEVERITY_LABEL[i.severity],
|
|
871
|
+
category: issueCategory(i.rule),
|
|
872
|
+
rule: i.rule,
|
|
873
|
+
message: i.message,
|
|
874
|
+
fixable: !!i.fixable,
|
|
875
|
+
})),
|
|
876
|
+
};
|
|
524
877
|
} else {
|
|
525
878
|
console.log(`\n ${C.bold('VelinStyle Scanner Report')}\n`);
|
|
526
879
|
console.log(` Scanned: ${htmlFiles.length} HTML, ${cssFiles.length} CSS, ${jsFiles.length} JS files\n`);
|