@birdapi/velinstyle 1.1.0 → 1.2.0

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