@birdapi/velinstyle 1.0.0 → 1.2.0

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