@birdapi/velinstyle 1.1.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 -162
- package/README.md +188 -119
- 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-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 +531 -44
- 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 +2 -0
- package/components/runtime/component-loaders.js +2 -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 +78 -1
- 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-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 +20 -3
- package/core/meta/build.js +125 -5
- 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-STYCS3O5.js → attributes-A423NX5J.js} +3 -3
- package/dist/chunks/{attributes-353XDOAX.js → attributes-F6UPIEVP.js} +1 -1
- package/dist/chunks/{attributes-ADOKLKA7.js → attributes-GLYXGGAL.js} +3 -3
- package/dist/chunks/{attributes-TPANLMLF.js → attributes-UFTYOSAU.js} +3 -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-RSMRR27R.js → chunk-2LSKX57P.js} +12 -10
- package/dist/chunks/{chunk-IEHKRARD.js → chunk-2TGCVDUL.js} +11 -9
- package/dist/chunks/{chunk-SRL6EWHP.js → chunk-5JWGUAPC.js} +17 -13
- package/dist/chunks/chunk-7YSO2OQ2.js +113 -0
- package/dist/chunks/{chunk-QVHYL3R4.js → chunk-BHR3ZG6U.js} +6 -6
- package/dist/chunks/{chunk-J472ERN2.js → chunk-JMSIHEYX.js} +15 -4
- package/dist/chunks/{chunk-MKZP2TBH.js → chunk-L7ZGUU4D.js} +2 -2
- package/dist/chunks/chunk-TUGIPKW5.js +113 -0
- package/dist/chunks/{highlight-GJHHV4KM.js → highlight-M2ELQNYK.js} +1 -1
- package/dist/chunks/runtime-entry.js +1 -1
- 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-N3DFQR66.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-ZGWG7JI5.js} +1 -1
- 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-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-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-NXA3U2FM.js → velin-lightbox-P725FLTY.js} +5 -4
- 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/llms.txt +29 -7
- package/dist/search-index.json +156 -10
- package/dist/velin-agent.json +3973 -28
- package/dist/velinstyle-components.iife.js +1211 -284
- package/dist/velinstyle-components.js +1213 -284
- package/dist/velinstyle-components.min.js +266 -118
- package/dist/velinstyle.css +252 -67
- package/dist/velinstyle.d.ts +2 -0
- package/dist/velinstyle.min.css +1 -1
- package/package.json +20 -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/components/alert.css +22 -1
- package/src/components/calendar-dropzone.css +5 -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 +2 -0
- package/dist/chunks/attributes-3BZLI7XF.js +0 -364
- package/dist/chunks/attributes-4N5JP4CG.js +0 -364
- package/dist/chunks/attributes-5Q2WXGRC.js +0 -374
- 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-XIET2VSZ.js +0 -109
- package/dist/chunks/chunk-XYLJ2GCB.js +0 -205
- package/dist/chunks/chunk-ZPSPKUYD.js +0 -109
- package/dist/chunks/highlight-XTHRKBA3.js +0 -37
- package/dist/chunks/velin-bottom-nav-2N2TYWOF.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-TSUKPY4C.js +0 -123
- 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-I75U6JF4.js +0 -140
- 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/meta.js
CHANGED
|
@@ -29,6 +29,31 @@ export async function buildMeta(options = {}) {
|
|
|
29
29
|
written.push(llmsFile);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// JSON twins for agents (pages, sections, knowledge, constraints)
|
|
33
|
+
const twinDir = join(PKG_ROOT, 'docs', 'generated', 'intelligence');
|
|
34
|
+
mkdirSync(twinDir, { recursive: true });
|
|
35
|
+
const twins = {
|
|
36
|
+
'pages.json': bundle.knowledgeGraph?.pages || [],
|
|
37
|
+
'sections.json': bundle.knowledgeGraph?.sections || [],
|
|
38
|
+
'components.json': bundle.knowledgeGraph?.components || [],
|
|
39
|
+
'design-constraints.json': bundle.knowledgeGraph?.designConstraints || [],
|
|
40
|
+
'index.json': {
|
|
41
|
+
version: bundle.framework.version,
|
|
42
|
+
generatedAt: bundle.generatedAt,
|
|
43
|
+
pageCount: bundle.knowledgeGraph?.pageCount || 0,
|
|
44
|
+
sectionCount: bundle.knowledgeGraph?.sectionCount || 0,
|
|
45
|
+
componentCount: bundle.knowledgeGraph?.componentCount || 0,
|
|
46
|
+
constraintCount: bundle.knowledgeGraph?.designConstraints?.length || 0,
|
|
47
|
+
agentBundle: 'dist/velin-agent.json',
|
|
48
|
+
llmsTxt: 'dist/llms.txt',
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
for (const [name, data] of Object.entries(twins)) {
|
|
52
|
+
const target = join(twinDir, name);
|
|
53
|
+
writeFileSync(target, JSON.stringify(data, null, 2), 'utf-8');
|
|
54
|
+
written.push(target);
|
|
55
|
+
}
|
|
56
|
+
|
|
32
57
|
return { ok: true, bundle, written, count: written.length };
|
|
33
58
|
}
|
|
34
59
|
|
|
@@ -43,7 +68,30 @@ export function metaPage(htmlPath, options = {}) {
|
|
|
43
68
|
}
|
|
44
69
|
const html = readFileSync(abs, 'utf-8');
|
|
45
70
|
const rel = htmlPath.replace(/\\/g, '/');
|
|
46
|
-
|
|
71
|
+
let meta = buildPageMeta(html, rel, PKG_ROOT);
|
|
72
|
+
|
|
73
|
+
// Merge curated fields from existing velin-meta so --write does not clobber goals/intent.
|
|
74
|
+
const existingMatch = html.match(
|
|
75
|
+
/<script[^>]*id=["']velin-meta["'][^>]*>([\s\S]*?)<\/script>/i,
|
|
76
|
+
);
|
|
77
|
+
if (existingMatch) {
|
|
78
|
+
try {
|
|
79
|
+
const prev = JSON.parse(existingMatch[1]);
|
|
80
|
+
meta = mergePageMeta(prev, meta);
|
|
81
|
+
if (options.write && meta._droppedKeys?.length) {
|
|
82
|
+
console.warn(
|
|
83
|
+
`velin-meta --write: dropping non-allowlisted keys: ${meta._droppedKeys.join(', ')} ` +
|
|
84
|
+
`(kept: ${[...META_MERGE_ALLOWLIST].join(', ')})`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
delete meta._droppedKeys;
|
|
88
|
+
} catch {
|
|
89
|
+
/* keep generated meta */
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
delete meta._droppedKeys;
|
|
93
|
+
}
|
|
94
|
+
|
|
47
95
|
const script = serializePageMetaScript(meta);
|
|
48
96
|
|
|
49
97
|
if (options.write) {
|
|
@@ -63,6 +111,41 @@ export function metaPage(htmlPath, options = {}) {
|
|
|
63
111
|
return { ok: true, meta, script };
|
|
64
112
|
}
|
|
65
113
|
|
|
114
|
+
/** Keys preserved from previous velin-meta on --write (plus nested page.*). */
|
|
115
|
+
const META_MERGE_ALLOWLIST = new Set([
|
|
116
|
+
'page', 'goals', 'intent', 'notes', 'curated', 'id', 'title',
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
/** Preserve curated page goals/intent while refreshing component inventory. */
|
|
120
|
+
function mergePageMeta(prev, next) {
|
|
121
|
+
const out = { ...next };
|
|
122
|
+
const dropped = [];
|
|
123
|
+
if (prev && typeof prev === 'object') {
|
|
124
|
+
for (const key of Object.keys(prev)) {
|
|
125
|
+
if (!META_MERGE_ALLOWLIST.has(key) && !(key in next)) {
|
|
126
|
+
dropped.push(key);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
out.page = { ...(next.page || {}), ...(prev.page || {}), ...(next.page || {}) };
|
|
130
|
+
if (prev.page?.intent) out.page.intent = prev.page.intent;
|
|
131
|
+
if (prev.page?.goals) out.page.goals = prev.page.goals;
|
|
132
|
+
if (prev.page?.title) out.page.title = prev.page.title;
|
|
133
|
+
if (prev.page?.id) out.page.id = prev.page.id;
|
|
134
|
+
if (prev.goals) out.goals = prev.goals;
|
|
135
|
+
if (prev.intent) out.intent = prev.intent;
|
|
136
|
+
if (prev.notes) out.notes = prev.notes;
|
|
137
|
+
if (prev.curated) out.curated = prev.curated;
|
|
138
|
+
if (prev.id && !out.id) out.id = prev.id;
|
|
139
|
+
}
|
|
140
|
+
out.allowed = next.allowed;
|
|
141
|
+
out.attributes = next.attributes;
|
|
142
|
+
out.a11y = next.a11y;
|
|
143
|
+
out.version = next.version;
|
|
144
|
+
out.mime = next.mime;
|
|
145
|
+
out._droppedKeys = dropped;
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
66
149
|
export async function metaMain(argv = process.argv.slice(2)) {
|
|
67
150
|
const pageIdx = argv.indexOf('page');
|
|
68
151
|
if (pageIdx !== -1 && argv[pageIdx + 1]) {
|
package/cli/perf-audit.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Performance audit for static HTML — images, scripts, inline styles.
|
|
3
3
|
*/
|
|
4
|
-
import { existsSync, readFileSync, writeFileSync, readdirSync } from 'fs';
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
|
|
5
5
|
import { join, extname, relative } from 'path';
|
|
6
6
|
|
|
7
7
|
const DEFAULT_IGNORE = ['node_modules', 'dist', '.git', '.next', 'build'];
|
|
@@ -18,6 +18,19 @@ export function walkHtmlFiles(dir, ignore = DEFAULT_IGNORE) {
|
|
|
18
18
|
return results;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/** Accept a single .html file or a directory. */
|
|
22
|
+
function collectHtmlFiles(targetPath, ignore = DEFAULT_IGNORE) {
|
|
23
|
+
if (!existsSync(targetPath)) return [];
|
|
24
|
+
const st = statSync(targetPath);
|
|
25
|
+
if (st.isFile()) {
|
|
26
|
+
return extname(targetPath).toLowerCase() === '.html' ? [targetPath] : [];
|
|
27
|
+
}
|
|
28
|
+
if (st.isDirectory()) {
|
|
29
|
+
return walkHtmlFiles(targetPath, ignore);
|
|
30
|
+
}
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
function lineOf(content, index) {
|
|
22
35
|
return content.slice(0, index).split('\n').length;
|
|
23
36
|
}
|
|
@@ -118,7 +131,7 @@ export function auditHtml(html, meta = {}) {
|
|
|
118
131
|
}
|
|
119
132
|
|
|
120
133
|
export function auditPath(targetPath) {
|
|
121
|
-
const files =
|
|
134
|
+
const files = collectHtmlFiles(targetPath);
|
|
122
135
|
const issues = [];
|
|
123
136
|
for (const file of files) {
|
|
124
137
|
const html = readFileSync(file, 'utf-8');
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt Engine — plan before HTML.
|
|
3
|
+
* Order: Mensch → Framework → KI (page registry + section graphs).
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, existsSync } from 'fs';
|
|
6
|
+
import { dirname, join } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { emitBlueprint } from './blueprint.js';
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const ROOT = join(__dirname, '..');
|
|
12
|
+
const PKG_VERSION = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8')).version;
|
|
13
|
+
|
|
14
|
+
function readJson(rel) {
|
|
15
|
+
const p = join(ROOT, rel);
|
|
16
|
+
if (!existsSync(p)) return null;
|
|
17
|
+
return JSON.parse(readFileSync(p, 'utf-8'));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function loadPages() {
|
|
21
|
+
return readJson('core/meta/pages/registry.json')?.pages || [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function loadSections() {
|
|
25
|
+
return readJson('core/meta/sections/registry.json')?.sections || [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function loadConstraintPack(id) {
|
|
29
|
+
const file = join(ROOT, 'core/meta/design-constraints', `${id}.json`);
|
|
30
|
+
if (!existsSync(file)) return null;
|
|
31
|
+
return JSON.parse(readFileSync(file, 'utf-8'));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function mapSectionToBlueprint(sectionId) {
|
|
35
|
+
const map = {
|
|
36
|
+
hero: 'hero-section',
|
|
37
|
+
'trust-bar': 'trust-bar',
|
|
38
|
+
benefits: 'benefits-grid',
|
|
39
|
+
services: 'services-grid',
|
|
40
|
+
process: 'process-steps',
|
|
41
|
+
testimonials: 'testimonials',
|
|
42
|
+
pricing: 'pricing-table',
|
|
43
|
+
faq: 'faq-section',
|
|
44
|
+
contact: 'form-contact',
|
|
45
|
+
cta: 'cta-band',
|
|
46
|
+
footer: 'footer-simple',
|
|
47
|
+
navbar: 'navbar-header',
|
|
48
|
+
};
|
|
49
|
+
return map[sectionId] || sectionId;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {string} prompt
|
|
54
|
+
*/
|
|
55
|
+
export function analyzePrompt(prompt) {
|
|
56
|
+
const lower = (prompt || '').toLowerCase();
|
|
57
|
+
const pages = loadPages();
|
|
58
|
+
let best = null;
|
|
59
|
+
let bestScore = 0;
|
|
60
|
+
for (const page of pages) {
|
|
61
|
+
let score = 0;
|
|
62
|
+
for (const kw of page.promptKeywords || []) {
|
|
63
|
+
if (lower.includes(kw.toLowerCase())) score += kw.length;
|
|
64
|
+
}
|
|
65
|
+
if (score > bestScore) {
|
|
66
|
+
bestScore = score;
|
|
67
|
+
best = page;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const audience =
|
|
72
|
+
/patient|clinic|healthcare|praxis/i.test(prompt) ? 'patients'
|
|
73
|
+
: /developer|docs|api/i.test(prompt) ? 'developers'
|
|
74
|
+
: /buyer|shop|store/i.test(prompt) ? 'buyers'
|
|
75
|
+
: 'prospects';
|
|
76
|
+
|
|
77
|
+
const conversionGoal =
|
|
78
|
+
/contact|beratung|consultation|anfrage|kontakt/i.test(prompt) ? 'contact form'
|
|
79
|
+
: /signup|trial|register/i.test(prompt) ? 'signup'
|
|
80
|
+
: best?.conversionGoal || 'lead';
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
intent: best ? `page:${best.id}` : 'page:landing',
|
|
84
|
+
targetAudience: audience,
|
|
85
|
+
businessType: best?.businessType || 'general',
|
|
86
|
+
pageGoal: best?.pageGoal || 'explain offer and convert',
|
|
87
|
+
conversionGoal,
|
|
88
|
+
pageId: best?.id || 'landing',
|
|
89
|
+
confidence: bestScore > 0 ? 'high' : 'low',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {ReturnType<typeof analyzePrompt>} analysis
|
|
95
|
+
* @param {string} prompt
|
|
96
|
+
*/
|
|
97
|
+
export function buildPlan(analysis, prompt) {
|
|
98
|
+
const pages = loadPages();
|
|
99
|
+
const sectionsMeta = loadSections();
|
|
100
|
+
const page = pages.find((p) => p.id === analysis.pageId) || pages.find((p) => p.id === 'landing');
|
|
101
|
+
const lower = (prompt || '').toLowerCase();
|
|
102
|
+
|
|
103
|
+
const sections = [...(page?.requiredSections || [])];
|
|
104
|
+
for (const opt of page?.optionalSections || []) {
|
|
105
|
+
if (opt === 'pricing' && /pric|tarif|abo/i.test(lower) && !sections.includes(opt)) {
|
|
106
|
+
sections.splice(Math.max(sections.length - 2, 0), 0, opt);
|
|
107
|
+
}
|
|
108
|
+
if (opt === 'trust-bar' && /trust|kunden|marken/i.test(lower) && !sections.includes(opt)) {
|
|
109
|
+
sections.splice(1, 0, opt);
|
|
110
|
+
}
|
|
111
|
+
if (opt === 'process' && /process|ablauf|how it works|schritte/i.test(lower) && !sections.includes(opt)) {
|
|
112
|
+
sections.splice(Math.min(4, sections.length), 0, opt);
|
|
113
|
+
}
|
|
114
|
+
if (opt === 'contact' && /contact|kontakt|formular/i.test(lower) && !sections.includes(opt)) {
|
|
115
|
+
sections.splice(Math.max(sections.length - 1, 0), 0, opt);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (/kontakt|contact|formular|steuerberater|leistungen/i.test(lower) && !sections.includes('contact')) {
|
|
120
|
+
sections.splice(Math.max(sections.length - 1, 0), 0, 'contact');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const uniqueSections = [...new Set(sections)];
|
|
124
|
+
const resolved = uniqueSections.map((id) => {
|
|
125
|
+
const meta = sectionsMeta.find((s) => s.id === id);
|
|
126
|
+
return {
|
|
127
|
+
id,
|
|
128
|
+
role: meta?.role || id,
|
|
129
|
+
blueprint: mapSectionToBlueprint(id),
|
|
130
|
+
designRules: meta?.designRules || [],
|
|
131
|
+
a11yRules: meta?.a11yRules || [],
|
|
132
|
+
seoRules: meta?.seoRules || [],
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const constraints = ['hero', 'faq', 'contact']
|
|
137
|
+
.map(loadConstraintPack)
|
|
138
|
+
.filter(Boolean);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
version: 1,
|
|
142
|
+
analysis,
|
|
143
|
+
page: {
|
|
144
|
+
id: page?.id,
|
|
145
|
+
name: page?.name,
|
|
146
|
+
seoIntent: page?.seoIntent,
|
|
147
|
+
defaultTone: page?.defaultTone,
|
|
148
|
+
recommendedComponents: page?.recommendedComponents || [],
|
|
149
|
+
},
|
|
150
|
+
sections: resolved,
|
|
151
|
+
layout: {
|
|
152
|
+
container: 'velin-container',
|
|
153
|
+
density: ['dashboard', 'crm', 'erp'].includes(page?.id) ? 'data-dense' : 'comfortable',
|
|
154
|
+
},
|
|
155
|
+
designConstraints: constraints,
|
|
156
|
+
accessibility: {
|
|
157
|
+
checklist: [
|
|
158
|
+
'One H1',
|
|
159
|
+
'Labeled form fields',
|
|
160
|
+
'Focus-visible controls',
|
|
161
|
+
'Respect prefers-reduced-motion',
|
|
162
|
+
],
|
|
163
|
+
risks: page?.accessibilityRisks || [],
|
|
164
|
+
},
|
|
165
|
+
seo: {
|
|
166
|
+
intent: page?.seoIntent,
|
|
167
|
+
notes: ['Meaningful H1', 'Section H2s', 'Descriptive link text'],
|
|
168
|
+
},
|
|
169
|
+
warnings: analysis.confidence === 'low'
|
|
170
|
+
? ['Low page-type confidence — defaulted to landing. Refine prompt with business type keywords.']
|
|
171
|
+
: [],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @param {object} plan
|
|
177
|
+
* @param {{ brand?: string, title?: string, cta?: string }} slots
|
|
178
|
+
*/
|
|
179
|
+
export function renderPlan(plan, slots = {}) {
|
|
180
|
+
const parts = [];
|
|
181
|
+
const missing = [];
|
|
182
|
+
for (const section of plan.sections || []) {
|
|
183
|
+
const emitId = section.blueprint || mapSectionToBlueprint(section.id);
|
|
184
|
+
const r = emitBlueprint(emitId);
|
|
185
|
+
if (!r.ok) {
|
|
186
|
+
missing.push(`${section.id}: ${r.error}`);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
parts.push(r.text);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let html = parts.join('\n\n');
|
|
193
|
+
if (slots.brand) html = html.replace(/\bBrand\b/g, slots.brand);
|
|
194
|
+
if (slots.title) html = html.replace(/>Headline</g, `>${slots.title}<`);
|
|
195
|
+
if (slots.cta) {
|
|
196
|
+
html = html.replace(/Primary CTA/g, slots.cta);
|
|
197
|
+
html = html.replace(/Request a consultation/g, slots.cta);
|
|
198
|
+
html = html.replace(/Kontakt aufnehmen/g, slots.cta);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const doc = `<!DOCTYPE html>
|
|
202
|
+
<html lang="de" data-velin-theme="light">
|
|
203
|
+
<head>
|
|
204
|
+
<meta charset="UTF-8">
|
|
205
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
206
|
+
<title>${slots.title || plan.page?.name || 'VelinStyle page'}</title>
|
|
207
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@birdapi/velinstyle@${PKG_VERSION}/dist/velinstyle.min.css">
|
|
208
|
+
</head>
|
|
209
|
+
<body>
|
|
210
|
+
${html}
|
|
211
|
+
<script type="module">
|
|
212
|
+
import { bootFromDOM } from 'https://cdn.jsdelivr.net/npm/@birdapi/velinstyle@${PKG_VERSION}/+esm';
|
|
213
|
+
bootFromDOM();
|
|
214
|
+
</script>
|
|
215
|
+
</body>
|
|
216
|
+
</html>
|
|
217
|
+
`;
|
|
218
|
+
|
|
219
|
+
return { html: doc, missing };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* @param {string} prompt
|
|
224
|
+
*/
|
|
225
|
+
export function planFromPrompt(prompt) {
|
|
226
|
+
if (!prompt?.trim()) return { ok: false, error: 'Prompt is required.' };
|
|
227
|
+
const analysis = analyzePrompt(prompt);
|
|
228
|
+
const plan = buildPlan(analysis, prompt);
|
|
229
|
+
const slots = {
|
|
230
|
+
brand: 'Brand',
|
|
231
|
+
title: /steuerberater/i.test(prompt) ? 'Steuerberatung mit Klarheit' : 'Headline',
|
|
232
|
+
cta: /kontakt|consultation|beratung/i.test(prompt) ? 'Kontakt aufnehmen' : 'Primary CTA',
|
|
233
|
+
};
|
|
234
|
+
const rendered = renderPlan(plan, slots);
|
|
235
|
+
return {
|
|
236
|
+
ok: true,
|
|
237
|
+
analysis,
|
|
238
|
+
plan,
|
|
239
|
+
html: rendered.html,
|
|
240
|
+
missing: rendered.missing,
|
|
241
|
+
warnings: plan.warnings,
|
|
242
|
+
};
|
|
243
|
+
}
|
package/cli/review.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review Engine — design / a11y / seo / performance / conversion / prompt score.
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync, existsSync } from 'fs';
|
|
5
|
+
import { dirname, join } from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const ROOT = join(__dirname, '..');
|
|
10
|
+
|
|
11
|
+
const PROFILES = new Set(['marketing', 'app', 'docs', 'fragment', 'ecommerce']);
|
|
12
|
+
|
|
13
|
+
function loadConstraint(id) {
|
|
14
|
+
const p = join(ROOT, 'core/meta/design-constraints', `${id}.json`);
|
|
15
|
+
if (!existsSync(p)) return null;
|
|
16
|
+
return JSON.parse(readFileSync(p, 'utf-8'));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Strip non-DOM noise so review heuristics do not count meta JSON or code demos. */
|
|
20
|
+
function stripNonRenderableHtml(html) {
|
|
21
|
+
return String(html || '')
|
|
22
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, ' ')
|
|
23
|
+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, ' ')
|
|
24
|
+
.replace(/<velin-code-block\b[^>]*>[\s\S]*?<\/velin-code-block>/gi, ' ')
|
|
25
|
+
.replace(/<!--[\s\S]*?-->/g, ' ');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function visibleWordCount(html) {
|
|
29
|
+
const clean = stripNonRenderableHtml(html)
|
|
30
|
+
.replace(/<[^>]+>/g, ' ')
|
|
31
|
+
.replace(/&[a-z]+;/gi, ' ')
|
|
32
|
+
.replace(/\s+/g, ' ')
|
|
33
|
+
.trim();
|
|
34
|
+
if (!clean) return 0;
|
|
35
|
+
return clean.split(' ').filter(Boolean).length;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function detectProfile(html, ctx = {}) {
|
|
39
|
+
if (ctx.profile && PROFILES.has(ctx.profile)) return ctx.profile;
|
|
40
|
+
const intent = ctx.plan?.page?.intent || ctx.plan?.page?.id || '';
|
|
41
|
+
const metaIntent = html.match(/"intent"\s*:\s*"([^"]+)"/)?.[1] || '';
|
|
42
|
+
const blob = `${intent} ${metaIntent}`.toLowerCase();
|
|
43
|
+
if (/dashboard|crm|erp|admin|app-shell|auth|login/.test(blob)) return 'app';
|
|
44
|
+
if (/docs|documentation|guide/.test(blob)) return 'docs';
|
|
45
|
+
if (/shop|ecommerce|store|checkout|cart/.test(blob)) return 'ecommerce';
|
|
46
|
+
if (ctx.fragment) return 'fragment';
|
|
47
|
+
// Short recipe-like HTML without document chrome → fragment
|
|
48
|
+
if (!/<html\b/i.test(html) && !/<main\b/i.test(html)) return 'fragment';
|
|
49
|
+
return 'marketing';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Hero scope: explicit hero markers first, else prefix until common section ids.
|
|
54
|
+
* Counts only real <a>/<button> tags with primary/outline CTA classes.
|
|
55
|
+
*/
|
|
56
|
+
function countHeroCtas(html) {
|
|
57
|
+
const clean = stripNonRenderableHtml(html);
|
|
58
|
+
let heroChunk = '';
|
|
59
|
+
const heroSection =
|
|
60
|
+
clean.match(/<section\b[^>]*(?:\bid=["']hero["']|data-velin-section=["']hero["'])[^>]*>[\s\S]*?<\/section>/i)
|
|
61
|
+
|| clean.match(/<(?:header|section)\b[^>]*\bid=["']top["'][^>]*>[\s\S]*?<\/(?:header|section)>/i);
|
|
62
|
+
if (heroSection) {
|
|
63
|
+
heroChunk = heroSection[0];
|
|
64
|
+
} else {
|
|
65
|
+
heroChunk = clean.split(/id=["'](?:services|faq|testimonials)["']/i)[0] || clean.slice(0, 2000);
|
|
66
|
+
}
|
|
67
|
+
const ctaRe = /<(?:a|button)\b[^>]*\bvelin-btn--(?:primary|outline)\b[^>]*>/gi;
|
|
68
|
+
return [...heroChunk.matchAll(ctaRe)].length;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {string} html
|
|
73
|
+
* @param {{ plan?: object, prompt?: string, profile?: string, fragment?: boolean }} [ctx]
|
|
74
|
+
*/
|
|
75
|
+
export function reviewHtml(html, ctx = {}) {
|
|
76
|
+
const issues = [];
|
|
77
|
+
const text = html || '';
|
|
78
|
+
const profile = detectProfile(text, ctx);
|
|
79
|
+
const isFragment = profile === 'fragment';
|
|
80
|
+
|
|
81
|
+
const isMarketing = profile === 'marketing' || profile === 'ecommerce';
|
|
82
|
+
|
|
83
|
+
const h1 = [...text.matchAll(/<h1\b/gi)];
|
|
84
|
+
if (!isFragment) {
|
|
85
|
+
if (h1.length === 0) {
|
|
86
|
+
const h1Fix = profile === 'app' || profile === 'docs'
|
|
87
|
+
? 'Add a single page H1 in the primary content (header/main) — not necessarily a marketing hero.'
|
|
88
|
+
: 'Add a single page H1 in the hero.';
|
|
89
|
+
issues.push({ code: 'a11y.missing-h1', severity: 'error', message: 'Missing H1', fix: h1Fix });
|
|
90
|
+
} else if (h1.length > 1) {
|
|
91
|
+
issues.push({ code: 'a11y.multiple-h1', severity: 'warning', message: `Found ${h1.length} H1 elements`, fix: 'Keep exactly one H1.' });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!isFragment && !/<html\b[^>]*\blang=/i.test(text) && /<html\b/i.test(text)) {
|
|
96
|
+
issues.push({
|
|
97
|
+
code: 'a11y.missing-lang',
|
|
98
|
+
severity: 'error',
|
|
99
|
+
message: 'Missing lang on <html>',
|
|
100
|
+
fix: 'Add lang="en" (or your locale) on <html>.',
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!isFragment) {
|
|
105
|
+
const imgs = [...text.matchAll(/<img\b[^>]*>/gi)];
|
|
106
|
+
for (const m of imgs) {
|
|
107
|
+
const tag = m[0];
|
|
108
|
+
if (/\brole=["']presentation["']/i.test(tag) || /\baria-hidden=["']true["']/i.test(tag)) continue;
|
|
109
|
+
if (!/\balt=/i.test(tag)) {
|
|
110
|
+
issues.push({
|
|
111
|
+
code: 'a11y.img-missing-alt',
|
|
112
|
+
severity: 'error',
|
|
113
|
+
message: 'Image without alt attribute',
|
|
114
|
+
fix: 'Add alt text (or alt="" for decorative images).',
|
|
115
|
+
});
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (/<input\b/i.test(text) && !isFragment) {
|
|
122
|
+
const inputs = [...text.matchAll(/<input\b[^>]*>/gi)];
|
|
123
|
+
let unlabeled = 0;
|
|
124
|
+
for (const m of inputs) {
|
|
125
|
+
const tag = m[0];
|
|
126
|
+
if (/\btype=["']hidden["']/i.test(tag)) continue;
|
|
127
|
+
if (/\baria-label=/i.test(tag) || /\baria-labelledby=/i.test(tag) || /\bid=/i.test(tag)) continue;
|
|
128
|
+
if (/\bplaceholder=/i.test(tag)) unlabeled += 1;
|
|
129
|
+
}
|
|
130
|
+
if (unlabeled > 0 && !/<label\b/i.test(text)) {
|
|
131
|
+
issues.push({
|
|
132
|
+
code: 'a11y.unlabeled-input',
|
|
133
|
+
severity: 'error',
|
|
134
|
+
message: 'Input(s) without associated label',
|
|
135
|
+
fix: 'Wire <label for="…"> to each input id (or use aria-label).',
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const ctaPrimary = [...text.matchAll(/velin-btn--primary/g)];
|
|
141
|
+
if (isMarketing && ctaPrimary.length > 4) {
|
|
142
|
+
issues.push({ code: 'design.too-many-primary-ctas', severity: 'warning', message: 'Many primary CTAs competing', fix: 'Limit primary CTAs; demote extras to outline/ghost.' });
|
|
143
|
+
}
|
|
144
|
+
if (/<form\b/i.test(text)) {
|
|
145
|
+
const skipFormSummary = profile === 'docs' && /type=["']search["']/i.test(text) && !/<textarea\b/i.test(text);
|
|
146
|
+
if (!skipFormSummary && !/velin-form-summary/i.test(text)) {
|
|
147
|
+
issues.push({ code: 'a11y.missing-form-summary', severity: 'error', message: 'Form without velin-form-summary', fix: 'Add <velin-form-summary> before fields.' });
|
|
148
|
+
}
|
|
149
|
+
if (/placeholder=/.test(text) && !/<label\b/i.test(text)) {
|
|
150
|
+
issues.push({ code: 'a11y.placeholder-only', severity: 'error', message: 'Possible placeholder-only fields', fix: 'Use visible <label> elements.' });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (/id="faq"|FAQ/i.test(text) && !/<(details|velin-accordion)\b/i.test(text)) {
|
|
155
|
+
issues.push({ code: 'design.faq-not-disclosure', severity: 'warning', message: 'FAQ section without disclosure pattern', fix: 'Use velin-accordion or details/summary.' });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!isFragment && !/<title\b/i.test(text)) {
|
|
159
|
+
issues.push({ code: 'seo.missing-title', severity: 'warning', message: 'Missing document title', fix: 'Add a descriptive <title>.' });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (/style="[^"]*background:[^"]*#|color:#[0-9a-f]{3,8}/i.test(text)) {
|
|
163
|
+
issues.push({ code: 'design.raw-hex', severity: 'warning', message: 'Raw hex colors detected', fix: 'Prefer --velin-color-* tokens.' });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (/animation:\s*[^;]*infinite/i.test(text)) {
|
|
167
|
+
issues.push({ code: 'perf.infinite-animation', severity: 'warning', message: 'Infinite animation found', fix: 'Avoid infinite motion; respect prefers-reduced-motion.' });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Thin / junk page floors (anti false-confidence)
|
|
171
|
+
const words = visibleWordCount(text);
|
|
172
|
+
const hasMain = /<main\b/i.test(text);
|
|
173
|
+
const mainEmpty = hasMain && /<main\b[^>]*>\s*<\/main>/i.test(text);
|
|
174
|
+
const thin = !isFragment && (words < 40 || !hasMain || mainEmpty || h1.length === 0);
|
|
175
|
+
|
|
176
|
+
if (!isFragment && words < 40) {
|
|
177
|
+
issues.push({
|
|
178
|
+
code: 'seo.thin-content',
|
|
179
|
+
severity: 'warning',
|
|
180
|
+
message: `Very little visible content (${words} words)`,
|
|
181
|
+
fix: 'Add meaningful page copy, headings, and sections.',
|
|
182
|
+
});
|
|
183
|
+
issues.push({
|
|
184
|
+
code: 'conversion.thin-page',
|
|
185
|
+
severity: 'warning',
|
|
186
|
+
message: 'Page is too thin to score conversion highly',
|
|
187
|
+
fix: 'Add a clear offer, proof, and primary CTA path.',
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (!isFragment && (!hasMain || mainEmpty)) {
|
|
191
|
+
issues.push({
|
|
192
|
+
code: 'seo.thin-content',
|
|
193
|
+
severity: 'warning',
|
|
194
|
+
message: hasMain ? 'Empty <main> landmark' : 'Missing <main> landmark',
|
|
195
|
+
fix: 'Wrap primary content in <main>.',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const plan = ctx.plan;
|
|
200
|
+
if (plan?.sections && isMarketing) {
|
|
201
|
+
const ids = plan.sections.map((s) => s.id);
|
|
202
|
+
for (const need of ['hero', 'footer']) {
|
|
203
|
+
if (!ids.includes(need) && /landing|lawyer|saas/i.test(plan.page?.id || '')) {
|
|
204
|
+
issues.push({ code: `plan.missing-${need}`, severity: 'error', message: `Plan missing section ${need}`, fix: `Include ${need} in page template.` });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (ctx.prompt && /steuerberater|lawyer|tax advisor/i.test(ctx.prompt)) {
|
|
210
|
+
for (const need of ['services', 'testimonials', 'faq', 'contact']) {
|
|
211
|
+
if (plan && !plan.sections.some((s) => s.id === need)) {
|
|
212
|
+
issues.push({ code: `conversion.missing-${need}`, severity: 'error', message: `Professional-services page missing ${need}`, fix: `Add ${need} section.` });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const heroRules = loadConstraint('hero');
|
|
218
|
+
if (heroRules && isMarketing && /velin-btn--primary/g.test(text)) {
|
|
219
|
+
const heroCtas = countHeroCtas(text);
|
|
220
|
+
if (heroCtas > 2) {
|
|
221
|
+
issues.push({ code: 'design.hero-cta-max', severity: 'error', message: 'Hero exceeds 2 CTAs', fix: 'Keep at most two CTAs in the hero (real <a>/<button> only; scripts and code samples are ignored).' });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const errors = issues.filter((i) => i.severity === 'error').length;
|
|
226
|
+
const warnings = issues.filter((i) => i.severity === 'warning').length;
|
|
227
|
+
|
|
228
|
+
const score = (base, penalty) => Math.max(0, Math.round((base - penalty) * 10) / 10);
|
|
229
|
+
|
|
230
|
+
let scores = {
|
|
231
|
+
design: score(10, errors * 1.5 + warnings * 0.5),
|
|
232
|
+
accessibility: score(10, issues.filter((i) => i.code.startsWith('a11y')).length * 2),
|
|
233
|
+
seo: score(10, issues.filter((i) => i.code.startsWith('seo')).length * 2),
|
|
234
|
+
performance: score(10, issues.filter((i) => i.code.startsWith('perf')).length * 2),
|
|
235
|
+
conversion: score(10, issues.filter((i) => i.code.startsWith('conversion')).length * 2),
|
|
236
|
+
visual: score(9, issues.filter((i) => i.code.startsWith('design')).length * 1.2),
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
if (thin) {
|
|
240
|
+
// Cap soft categories so junk pages cannot claim 10/10
|
|
241
|
+
scores.seo = Math.min(scores.seo, 4);
|
|
242
|
+
scores.conversion = Math.min(scores.conversion, 4);
|
|
243
|
+
scores.design = Math.min(scores.design, 6);
|
|
244
|
+
scores.visual = Math.min(scores.visual, 5);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (profile === 'app' || profile === 'docs') {
|
|
248
|
+
// Conversion/SEO marketing heuristics are not the primary signal for app/docs shells
|
|
249
|
+
scores.conversion = Math.min(scores.conversion, 6);
|
|
250
|
+
if (profile === 'app') scores.seo = Math.min(scores.seo, 7);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const avg = Object.values(scores).reduce((a, b) => a + b, 0) / Object.values(scores).length;
|
|
254
|
+
const promptScore = Math.max(0, Math.min(10, avg - (ctx.plan?.warnings?.length ? 1 : 0)));
|
|
255
|
+
|
|
256
|
+
const gate = errors > 0 ? 'fail' : warnings > 0 ? 'warn' : 'pass';
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
version: 1,
|
|
260
|
+
profile,
|
|
261
|
+
promptScore,
|
|
262
|
+
scores,
|
|
263
|
+
issues,
|
|
264
|
+
gate,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export { PROFILES, detectProfile };
|