@birdapi/velinstyle 1.1.0 → 1.2.1

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.
Files changed (328) hide show
  1. package/README.de.md +89 -161
  2. package/README.md +187 -119
  3. package/cli/blueprint.js +98 -5
  4. package/cli/blueprints/app-chrome.html +18 -0
  5. package/cli/blueprints/benefits-grid.html +9 -0
  6. package/cli/blueprints/bottom-nav-mobile.html +2 -1
  7. package/cli/blueprints/cookie-consent.html +1 -1
  8. package/cli/blueprints/cta-band.html +6 -0
  9. package/cli/blueprints/faq-section.html +18 -0
  10. package/cli/blueprints/filter-bar.html +4 -4
  11. package/cli/blueprints/form-contact.html +6 -5
  12. package/cli/blueprints/hero-section.html +3 -3
  13. package/cli/blueprints/notification-center.html +1 -1
  14. package/cli/blueprints/onboarding.html +2 -1
  15. package/cli/blueprints/ops-console.html +29 -0
  16. package/cli/blueprints/pricing-band.html +37 -0
  17. package/cli/blueprints/process-steps.html +10 -0
  18. package/cli/blueprints/services-grid.html +9 -0
  19. package/cli/blueprints/settings-panel.html +8 -6
  20. package/cli/blueprints/split-hero.html +25 -0
  21. package/cli/blueprints/testimonials.html +8 -0
  22. package/cli/blueprints/trust-bar.html +10 -0
  23. package/cli/cli-manifest.json +81 -17
  24. package/cli/contrast-utils.js +97 -0
  25. package/cli/create.js +370 -0
  26. package/cli/docgen/extract-attributes.js +128 -10
  27. package/cli/docgen/extract-cli.js +3 -2
  28. package/cli/docgen/extract-components.js +61 -2
  29. package/cli/docgen/extract-utilities.js +10 -10
  30. package/cli/doctor.js +119 -0
  31. package/cli/index.js +560 -44
  32. package/cli/meta.js +84 -1
  33. package/cli/perf-audit.js +15 -2
  34. package/cli/prompt-engine.js +243 -0
  35. package/cli/review.js +268 -0
  36. package/cli/scaffold.js +119 -4
  37. package/cli/scanner-rules-data.js +7 -0
  38. package/cli/scanner.js +357 -4
  39. package/cli/serve.js +81 -0
  40. package/cli/skills.js +223 -0
  41. package/cli/transparency.js +221 -0
  42. package/cli/wc-api.js +89 -0
  43. package/cli/workflow.js +42 -0
  44. package/components/index.js +2 -0
  45. package/components/runtime/component-loaders.js +2 -0
  46. package/components/velin-accordion.js +62 -13
  47. package/components/velin-bottom-nav.js +10 -0
  48. package/components/velin-calendar.js +212 -0
  49. package/components/velin-command.js +18 -2
  50. package/components/velin-data-table.js +78 -1
  51. package/components/velin-dialog.js +4 -1
  52. package/components/velin-drawer.js +2 -4
  53. package/components/velin-file-dropzone.js +162 -0
  54. package/components/velin-icon.js +22 -1
  55. package/components/velin-lightbox.js +5 -3
  56. package/components/velin-modal.js +2 -4
  57. package/components/velin-search.js +1 -1
  58. package/components/velin-sheet.js +2 -4
  59. package/core/a11y/component-contracts.json +20 -3
  60. package/core/attributes/registry.js +14 -0
  61. package/core/meta/build.js +125 -5
  62. package/core/meta/design-constraints/contact.json +9 -0
  63. package/core/meta/design-constraints/faq.json +9 -0
  64. package/core/meta/design-constraints/hero.json +13 -0
  65. package/core/meta/knowledge/components.json +622 -0
  66. package/core/meta/knowledge/tokens.json +282 -0
  67. package/core/meta/pages/registry.json +294 -0
  68. package/core/meta/schema.js +13 -1
  69. package/core/meta/sections/registry.json +185 -0
  70. package/core/transparency/attach.js +74 -0
  71. package/core/transparency/claims.js +97 -0
  72. package/core/transparency/doctor.js +142 -0
  73. package/core/transparency/engine.js +75 -0
  74. package/core/transparency/export.js +98 -0
  75. package/core/transparency/index.js +30 -0
  76. package/core/transparency/migrate.js +130 -0
  77. package/core/transparency/normalize.js +125 -0
  78. package/core/transparency/policy.js +105 -0
  79. package/core/transparency/providers.js +235 -0
  80. package/core/transparency/registry.js +104 -0
  81. package/core/transparency/renderer.js +111 -0
  82. package/core/transparency/reporter.js +113 -0
  83. package/core/transparency/validator.js +112 -0
  84. package/dist/chunks/attach-H2ZSEAE6.js +365 -0
  85. package/dist/chunks/{attributes-STYCS3O5.js → attributes-A423NX5J.js} +3 -3
  86. package/dist/chunks/attributes-DZCXX2RZ.js +404 -0
  87. package/dist/chunks/{attributes-353XDOAX.js → attributes-F6UPIEVP.js} +1 -1
  88. package/dist/chunks/{attributes-ADOKLKA7.js → attributes-GLYXGGAL.js} +3 -3
  89. package/dist/chunks/{attributes-TPANLMLF.js → attributes-UFTYOSAU.js} +3 -3
  90. package/dist/chunks/attributes-XGLXLR2T.js +391 -0
  91. package/dist/chunks/attributes-YF4NI2QS.js +391 -0
  92. package/dist/chunks/browser-AUABBYUK.js +1544 -0
  93. package/dist/chunks/{chunk-RSMRR27R.js → chunk-2LSKX57P.js} +12 -10
  94. package/dist/chunks/{chunk-IEHKRARD.js → chunk-2TGCVDUL.js} +11 -9
  95. package/dist/chunks/{chunk-SRL6EWHP.js → chunk-5JWGUAPC.js} +17 -13
  96. package/dist/chunks/chunk-7YSO2OQ2.js +113 -0
  97. package/dist/chunks/{chunk-QVHYL3R4.js → chunk-BHR3ZG6U.js} +6 -6
  98. package/dist/chunks/chunk-CQMTCEI6.js +113 -0
  99. package/dist/chunks/{chunk-J472ERN2.js → chunk-JMSIHEYX.js} +15 -4
  100. package/dist/chunks/{chunk-MKZP2TBH.js → chunk-L7ZGUU4D.js} +2 -2
  101. package/dist/chunks/chunk-TUGIPKW5.js +113 -0
  102. package/dist/chunks/{highlight-GJHHV4KM.js → highlight-M2ELQNYK.js} +1 -1
  103. package/dist/chunks/runtime-entry.js +1 -1
  104. package/dist/chunks/{velin-accordion-FBIPAEXG.js → velin-accordion-WSY6BMLA.js} +64 -10
  105. package/dist/chunks/{velin-bottom-nav-7GEMPIM6.js → velin-bottom-nav-5RVRNGIP.js} +25 -0
  106. package/dist/chunks/{velin-bottom-nav-N3DFQR66.js → velin-bottom-nav-N5F4UO2I.js} +26 -1
  107. package/dist/chunks/velin-calendar-OFE6THUD.js +210 -0
  108. package/dist/chunks/velin-calendar-PGVNJTXK.js +217 -0
  109. package/dist/chunks/velin-calendar-XC7JRDSH.js +217 -0
  110. package/dist/chunks/{velin-code-block-CIAURDRJ.js → velin-code-block-ZGWG7JI5.js} +1 -1
  111. package/dist/chunks/{velin-combobox-NXS4QB4R.js → velin-combobox-56V2ZWT5.js} +1 -1
  112. package/dist/chunks/{velin-command-ONKO2A3J.js → velin-command-2HO2EKR6.js} +29 -2
  113. package/dist/chunks/{velin-command-TMLSRGIU.js → velin-command-ZJSLPK7B.js} +30 -3
  114. package/dist/chunks/{velin-copy-NYDBOKXT.js → velin-copy-XDQSV2XU.js} +1 -1
  115. package/dist/chunks/{velin-countdown-4ZVDGK4R.js → velin-countdown-V4Z5MA63.js} +1 -1
  116. package/dist/chunks/velin-data-table-UVR42UUM.js +372 -0
  117. package/dist/chunks/{velin-dialog-GTTZI564.js → velin-dialog-3YK2RPRV.js} +9 -2
  118. package/dist/chunks/{velin-dialog-ELFOZIXZ.js → velin-dialog-S24KAP4S.js} +3 -1
  119. package/dist/chunks/{velin-drawer-BE6TYN3H.js → velin-drawer-5PNU624J.js} +2 -4
  120. package/dist/chunks/{velin-drawer-DLL2GLT6.js → velin-drawer-CCM6S25D.js} +3 -5
  121. package/dist/chunks/{velin-email-RBX6ZR4S.js → velin-email-TNFONXPW.js} +1 -1
  122. package/dist/chunks/velin-file-dropzone-QRGF2JNA.js +154 -0
  123. package/dist/chunks/velin-file-dropzone-XIZMJ3NB.js +154 -0
  124. package/dist/chunks/{velin-icon-7M76DOPB.js → velin-icon-A6NCXGUK.js} +59 -17
  125. package/dist/chunks/{velin-icon-ZTSKUOVN.js → velin-icon-EC4B5XCM.js} +18 -5
  126. package/dist/chunks/{velin-lightbox-6E7CAATP.js → velin-lightbox-FFK4TUUP.js} +7 -6
  127. package/dist/chunks/{velin-lightbox-NXA3U2FM.js → velin-lightbox-P725FLTY.js} +5 -4
  128. package/dist/chunks/{velin-menubar-J66JXRWW.js → velin-menubar-6I2LM5HL.js} +1 -1
  129. package/dist/chunks/{velin-modal-5HYNNNUI.js → velin-modal-ICTMGRSU.js} +7 -8
  130. package/dist/chunks/{velin-modal-AEOTZ67O.js → velin-modal-JBNODH7D.js} +2 -4
  131. package/dist/chunks/{velin-popover-T72HK3DF.js → velin-popover-WZG2GHBL.js} +25 -4
  132. package/dist/chunks/{velin-rating-N4DI2XNL.js → velin-rating-QBBYRRBU.js} +1 -1
  133. package/dist/chunks/{velin-search-7B4KJ6TX.js → velin-search-B3L6J2RP.js} +2 -2
  134. package/dist/chunks/{velin-search-2XRQFBCS.js → velin-search-L3UG6NDQ.js} +5 -4
  135. package/dist/chunks/{velin-secure-field-6PV7RZDJ.js → velin-secure-field-DGYMP7OK.js} +1 -1
  136. package/dist/chunks/{velin-segmented-control-GYD5BIXQ.js → velin-segmented-control-P74GJPAR.js} +1 -1
  137. package/dist/chunks/{velin-sheet-R2Y67X6H.js → velin-sheet-M5GZZD7G.js} +2 -4
  138. package/dist/chunks/{velin-sheet-OUK6572T.js → velin-sheet-X7RXIWKV.js} +3 -5
  139. package/dist/chunks/{velin-stepper-X54WGHDG.js → velin-stepper-4YQJUHPC.js} +12 -5
  140. package/dist/chunks/{velin-toast-OMMOA2DK.js → velin-toast-UG5LWTG2.js} +5 -1
  141. package/dist/chunks/{velin-tooltip-BDHGRTQB.js → velin-tooltip-ZDH5SDLU.js} +1 -1
  142. package/dist/llms.txt +31 -8
  143. package/dist/search-index.json +182 -10
  144. package/dist/velin-agent.json +3980 -31
  145. package/dist/velinstyle-components.iife.js +1622 -286
  146. package/dist/velinstyle-components.js +1624 -286
  147. package/dist/velinstyle-components.min.js +262 -114
  148. package/dist/velinstyle.css +412 -67
  149. package/dist/velinstyle.d.ts +2 -0
  150. package/dist/velinstyle.min.css +1 -1
  151. package/package.json +21 -6
  152. package/packages/skill-engine/README.md +24 -0
  153. package/packages/skill-engine/package.json +7 -0
  154. package/packages/skill-engine/src/catalog.js +29 -0
  155. package/packages/skill-engine/src/index.js +5 -0
  156. package/packages/skill-engine/src/install.js +69 -0
  157. package/packages/skill-engine/src/registry.js +27 -0
  158. package/packages/skill-engine/src/resolve.js +68 -0
  159. package/packages/skill-engine/src/validate.js +40 -0
  160. package/packages/velinstyle-skills/README.md +6 -0
  161. package/packages/velinstyle-skills/adapter.json +8 -0
  162. package/packages/velinstyle-skills/bundles/landing-starter.bundle.yaml +12 -0
  163. package/packages/velinstyle-skills/bundles/review-suite.bundle.yaml +11 -0
  164. package/packages/velinstyle-skills/catalog.json +1004 -0
  165. package/packages/velinstyle-skills/graphs/component-ship.graph.yaml +14 -0
  166. package/packages/velinstyle-skills/graphs/landingpage.graph.yaml +21 -0
  167. package/packages/velinstyle-skills/graphs/release-gate.graph.yaml +15 -0
  168. package/packages/velinstyle-skills/index.js +22 -0
  169. package/packages/velinstyle-skills/package.json +7 -0
  170. package/packages/velinstyle-skills/packs/accessibility.pack.yaml +9 -0
  171. package/packages/velinstyle-skills/packs/design.pack.yaml +10 -0
  172. package/packages/velinstyle-skills/packs/documentation.pack.yaml +8 -0
  173. package/packages/velinstyle-skills/packs/enterprise.pack.yaml +10 -0
  174. package/packages/velinstyle-skills/packs/frontend.pack.yaml +10 -0
  175. package/packages/velinstyle-skills/packs/review.pack.yaml +9 -0
  176. package/packages/velinstyle-skills/projects/admin-panel.project.yaml +12 -0
  177. package/packages/velinstyle-skills/projects/blog.project.yaml +11 -0
  178. package/packages/velinstyle-skills/projects/documentation-portal.project.yaml +13 -0
  179. package/packages/velinstyle-skills/projects/ecommerce-shop.project.yaml +12 -0
  180. package/packages/velinstyle-skills/projects/landing-page.project.yaml +12 -0
  181. package/packages/velinstyle-skills/projects/laravel-dashboard.project.yaml +12 -0
  182. package/packages/velinstyle-skills/projects/marketing-website.project.yaml +14 -0
  183. package/packages/velinstyle-skills/projects/portfolio.project.yaml +12 -0
  184. package/packages/velinstyle-skills/registry.json +142 -0
  185. package/packages/velinstyle-skills/scripts/generate-assets.mjs +163 -0
  186. package/packages/velinstyle-skills/skills/velin-a11y-focus-keyboard/SKILL.md +57 -0
  187. package/packages/velinstyle-skills/skills/velin-a11y-page-checklist/SKILL.md +57 -0
  188. package/packages/velinstyle-skills/skills/velin-a11y-scan-fix/SKILL.md +57 -0
  189. package/packages/velinstyle-skills/skills/velin-ai-agent-briefing/SKILL.md +57 -0
  190. package/packages/velinstyle-skills/skills/velin-ai-plan-scaffold-review/SKILL.md +57 -0
  191. package/packages/velinstyle-skills/skills/velin-ai-skill-authoring/SKILL.md +57 -0
  192. package/packages/velinstyle-skills/skills/velin-components-accessibility-map/SKILL.md +57 -0
  193. package/packages/velinstyle-skills/skills/velin-components-api-contract/SKILL.md +57 -0
  194. package/packages/velinstyle-skills/skills/velin-components-recipe/SKILL.md +57 -0
  195. package/packages/velinstyle-skills/skills/velin-components-select-compose/SKILL.md +57 -0
  196. package/packages/velinstyle-skills/skills/velin-design-hero-compose/SKILL.md +57 -0
  197. package/packages/velinstyle-skills/skills/velin-design-section-rhythm/SKILL.md +57 -0
  198. package/packages/velinstyle-skills/skills/velin-design-surface-roles/SKILL.md +57 -0
  199. package/packages/velinstyle-skills/skills/velin-dev-component-wire/SKILL.md +57 -0
  200. package/packages/velinstyle-skills/skills/velin-dev-page-scaffold/SKILL.md +57 -0
  201. package/packages/velinstyle-skills/skills/velin-dev-plan-first/SKILL.md +57 -0
  202. package/packages/velinstyle-skills/skills/velin-docs-component-guide/SKILL.md +57 -0
  203. package/packages/velinstyle-skills/skills/velin-docs-generate-api/SKILL.md +57 -0
  204. package/packages/velinstyle-skills/skills/velin-dx-doctor-setup/SKILL.md +57 -0
  205. package/packages/velinstyle-skills/skills/velin-dx-project-init/SKILL.md +57 -0
  206. package/packages/velinstyle-skills/skills/velin-migration-legacy-wc/SKILL.md +57 -0
  207. package/packages/velinstyle-skills/skills/velin-migration-prefix-classes/SKILL.md +57 -0
  208. package/packages/velinstyle-skills/skills/velin-motion-reduced-safe/SKILL.md +57 -0
  209. package/packages/velinstyle-skills/skills/velin-motion-reveal-feedback/SKILL.md +57 -0
  210. package/packages/velinstyle-skills/skills/velin-perf-audit-fix/SKILL.md +57 -0
  211. package/packages/velinstyle-skills/skills/velin-perf-runtime-tree-shake/SKILL.md +57 -0
  212. package/packages/velinstyle-skills/skills/velin-release-changelog-sync/SKILL.md +57 -0
  213. package/packages/velinstyle-skills/skills/velin-release-meta-rebuild/SKILL.md +57 -0
  214. package/packages/velinstyle-skills/skills/velin-review-conversion-hero/SKILL.md +57 -0
  215. package/packages/velinstyle-skills/skills/velin-review-page-gate/SKILL.md +57 -0
  216. package/packages/velinstyle-skills/skills/velin-review-seo-meta/SKILL.md +57 -0
  217. package/packages/velinstyle-skills/skills/velin-security-sanitize-html/SKILL.md +57 -0
  218. package/packages/velinstyle-skills/skills/velin-security-scan-pii/SKILL.md +57 -0
  219. package/packages/velinstyle-skills/skills/velin-testing-a11y-suite/SKILL.md +57 -0
  220. package/packages/velinstyle-skills/skills/velin-testing-visual-smoke/SKILL.md +57 -0
  221. package/packages/velinstyle-skills/skills/velin-themes-apply-token/SKILL.md +57 -0
  222. package/packages/velinstyle-skills/skills/velin-themes-contrast-check/SKILL.md +57 -0
  223. package/packages/velinstyle-skills/skills/velin-utilities-layout-apply/SKILL.md +57 -0
  224. package/packages/velinstyle-skills/skills/velin-utilities-prefix-fix/SKILL.md +57 -0
  225. package/packages/velinstyle-skills/skills/velin-utilities-token-map/SKILL.md +57 -0
  226. package/packages/velinstyle-skills/templates/admin-panel.template.yaml +12 -0
  227. package/packages/velinstyle-skills/templates/blog.template.yaml +12 -0
  228. package/packages/velinstyle-skills/templates/documentation-portal.template.yaml +12 -0
  229. package/packages/velinstyle-skills/templates/ecommerce-shop.template.yaml +13 -0
  230. package/packages/velinstyle-skills/templates/landing-page.template.yaml +12 -0
  231. package/packages/velinstyle-skills/templates/laravel-dashboard.template.yaml +11 -0
  232. package/packages/velinstyle-skills/templates/marketing-website.template.yaml +13 -0
  233. package/packages/velinstyle-skills/templates/portfolio.template.yaml +12 -0
  234. package/schemas/component-knowledge.schema.json +33 -0
  235. package/schemas/design-constraint.schema.json +15 -0
  236. package/schemas/design-intelligence.schema.json +19 -0
  237. package/schemas/design-rule.schema.json +19 -0
  238. package/schemas/page-template.schema.json +24 -0
  239. package/schemas/review-report.schema.json +36 -0
  240. package/schemas/section.schema.json +22 -0
  241. package/schemas/skill-bundle.schema.json +15 -0
  242. package/schemas/skill-graph.schema.json +25 -0
  243. package/schemas/skill-pack.schema.json +14 -0
  244. package/schemas/skill-project.schema.json +16 -0
  245. package/schemas/skill-record.schema.json +27 -0
  246. package/schemas/skill-registry.schema.json +17 -0
  247. package/schemas/skill-template.schema.json +17 -0
  248. package/schemas/token-graph.schema.json +27 -0
  249. package/schemas/utility.schema.json +166 -0
  250. package/src/components/alert.css +22 -1
  251. package/src/components/calendar-dropzone.css +5 -0
  252. package/src/components/transparency.css +138 -0
  253. package/src/layout/app-shell.css +33 -0
  254. package/src/tokens/shadow.css +13 -0
  255. package/src/utilities/border.css +2 -0
  256. package/src/utilities/display.css +1 -24
  257. package/src/utilities/responsive.css +16 -1
  258. package/src/utilities/spacing.css +32 -0
  259. package/src/velinstyle.css +3 -0
  260. package/dist/chunks/attributes-3BZLI7XF.js +0 -364
  261. package/dist/chunks/attributes-4N5JP4CG.js +0 -364
  262. package/dist/chunks/attributes-5Q2WXGRC.js +0 -374
  263. package/dist/chunks/attributes-AIR7SVLK.js +0 -364
  264. package/dist/chunks/attributes-BGRN5XZO.js +0 -364
  265. package/dist/chunks/attributes-BVSQAYLR.js +0 -364
  266. package/dist/chunks/attributes-CDYFUY7Y.js +0 -364
  267. package/dist/chunks/attributes-JMYJZBPE.js +0 -364
  268. package/dist/chunks/attributes-R5JY3X4O.js +0 -374
  269. package/dist/chunks/attributes-TNSPQTHX.js +0 -374
  270. package/dist/chunks/chunk-2354NEBP.js +0 -109
  271. package/dist/chunks/chunk-2VNA7G7S.js +0 -109
  272. package/dist/chunks/chunk-AQLRJSGG.js +0 -109
  273. package/dist/chunks/chunk-EWPSDMQT.js +0 -109
  274. package/dist/chunks/chunk-LA7264K4.js +0 -85
  275. package/dist/chunks/chunk-QTJJ3DPO.js +0 -109
  276. package/dist/chunks/chunk-RZQVWFHW.js +0 -109
  277. package/dist/chunks/chunk-U4XHMZ7D.js +0 -109
  278. package/dist/chunks/chunk-V5YIUA6G.js +0 -109
  279. package/dist/chunks/chunk-XIET2VSZ.js +0 -109
  280. package/dist/chunks/chunk-XYLJ2GCB.js +0 -205
  281. package/dist/chunks/chunk-ZPSPKUYD.js +0 -109
  282. package/dist/chunks/highlight-XTHRKBA3.js +0 -37
  283. package/dist/chunks/velin-bottom-nav-2N2TYWOF.js +0 -86
  284. package/dist/chunks/velin-carousel-Y5VCOABZ.js +0 -213
  285. package/dist/chunks/velin-code-block-ASUBTAQV.js +0 -119
  286. package/dist/chunks/velin-code-block-TSUKPY4C.js +0 -123
  287. package/dist/chunks/velin-code-block-Y6CM726Q.js +0 -119
  288. package/dist/chunks/velin-combobox-2KDTCQ7N.js +0 -155
  289. package/dist/chunks/velin-command-M7RNHUPR.js +0 -136
  290. package/dist/chunks/velin-copy-T5KDTDXP.js +0 -81
  291. package/dist/chunks/velin-countdown-5XSTN3BH.js +0 -101
  292. package/dist/chunks/velin-counter-ISCTEAHI.js +0 -118
  293. package/dist/chunks/velin-dialog-D2GUHSCD.js +0 -144
  294. package/dist/chunks/velin-dialog-IJLFDXAJ.js +0 -144
  295. package/dist/chunks/velin-dialog-TNS3WXMB.js +0 -144
  296. package/dist/chunks/velin-drawer-MJS6WCF7.js +0 -130
  297. package/dist/chunks/velin-email-N5B5ZYTE.js +0 -90
  298. package/dist/chunks/velin-icon-KZO3IEHG.js +0 -164
  299. package/dist/chunks/velin-lightbox-I75U6JF4.js +0 -140
  300. package/dist/chunks/velin-lightbox-QOTB4PRL.js +0 -140
  301. package/dist/chunks/velin-lightbox-UCD6N7HF.js +0 -140
  302. package/dist/chunks/velin-live-dot-XHUZCIDZ.js +0 -67
  303. package/dist/chunks/velin-menubar-SFEXJE56.js +0 -86
  304. package/dist/chunks/velin-modal-2HOBXE6F.js +0 -176
  305. package/dist/chunks/velin-modal-GLI2XZZB.js +0 -176
  306. package/dist/chunks/velin-popover-EXINRIGJ.js +0 -145
  307. package/dist/chunks/velin-popover-OXX7MRZ6.js +0 -145
  308. package/dist/chunks/velin-popover-YKCCROJG.js +0 -145
  309. package/dist/chunks/velin-rating-XRQJV4LO.js +0 -91
  310. package/dist/chunks/velin-scroll-top-F2FCZNYD.js +0 -58
  311. package/dist/chunks/velin-search-4YHJWYSS.js +0 -556
  312. package/dist/chunks/velin-search-LWHY23Q5.js +0 -474
  313. package/dist/chunks/velin-secure-field-H7FSBUJW.js +0 -95
  314. package/dist/chunks/velin-segmented-control-PQX6LKOJ.js +0 -109
  315. package/dist/chunks/velin-sheet-3OGI4HD7.js +0 -119
  316. package/dist/chunks/velin-sparkline-GVEANJVW.js +0 -184
  317. package/dist/chunks/velin-sparkline-KDA3GQAN.js +0 -162
  318. package/dist/chunks/velin-stepper-2ZIHLKGM.js +0 -141
  319. package/dist/chunks/velin-stepper-FYEUEWE6.js +0 -141
  320. package/dist/chunks/velin-stepper-wc-QU3KRVQR.js +0 -133
  321. package/dist/chunks/velin-theme-toggle-BNNS7WLV.js +0 -285
  322. package/dist/chunks/velin-theme-toggle-NO7V4TWI.js +0 -285
  323. package/dist/chunks/velin-theme-toggle-QELCJM2Q.js +0 -291
  324. package/dist/chunks/velin-toast-RCZ4CSUC.js +0 -109
  325. package/dist/chunks/velin-toast-WEUBTJG6.js +0 -109
  326. package/dist/chunks/velin-tooltip-664IOHZA.js +0 -112
  327. package/dist/chunks/velin-tooltip-wc-LXDVT6DE.js +0 -103
  328. package/dist/chunks/worker-client-R6DBYPFT.js +0 -46
package/cli/scaffold.js CHANGED
@@ -1,11 +1,13 @@
1
1
  /**
2
- * Prompt-based scaffolding: map natural language to VelinStyle blueprint composition.
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 (0.8.0) — intent: ${intent.id}, confidence: ${intent.confidence} -->\n` +
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 = walkFiles(targetPath, ['.html', '.htm'], ignore);
457
- const cssFiles = walkFiles(targetPath, ['.css'], ignore);
458
- const jsFiles = walkFiles(targetPath, ['.js', '.mjs'], ignore);
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`);