decantr 0.9.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 (382) hide show
  1. package/AGENTS.md +868 -0
  2. package/CHANGELOG.md +255 -0
  3. package/CLAUDE.md +178 -0
  4. package/LICENSE +21 -0
  5. package/README.md +229 -0
  6. package/cli/art.js +127 -0
  7. package/cli/commands/a11y.js +61 -0
  8. package/cli/commands/audit.js +225 -0
  9. package/cli/commands/build.js +38 -0
  10. package/cli/commands/dev.js +18 -0
  11. package/cli/commands/doctor.js +197 -0
  12. package/cli/commands/figma-sync.js +48 -0
  13. package/cli/commands/figma-tokens.js +55 -0
  14. package/cli/commands/generate.js +26 -0
  15. package/cli/commands/init.js +116 -0
  16. package/cli/commands/lint.js +209 -0
  17. package/cli/commands/mcp.js +530 -0
  18. package/cli/commands/migrate.js +175 -0
  19. package/cli/commands/test.js +38 -0
  20. package/cli/commands/validate.js +354 -0
  21. package/cli/index.js +113 -0
  22. package/package.json +95 -0
  23. package/reference/atoms.md +517 -0
  24. package/reference/behaviors.md +384 -0
  25. package/reference/build-tooling.md +275 -0
  26. package/reference/color-guidelines.md +965 -0
  27. package/reference/component-lifecycle.md +137 -0
  28. package/reference/compound-spacing.md +95 -0
  29. package/reference/decantation-process.md +499 -0
  30. package/reference/dev-server-routes.md +93 -0
  31. package/reference/form-system.md +253 -0
  32. package/reference/i18n.md +336 -0
  33. package/reference/icons.md +576 -0
  34. package/reference/llm-primer.md +953 -0
  35. package/reference/plugins.md +252 -0
  36. package/reference/registry-consumption.md +76 -0
  37. package/reference/router.md +217 -0
  38. package/reference/shells.md +116 -0
  39. package/reference/spatial-guidelines.md +541 -0
  40. package/reference/ssr.md +234 -0
  41. package/reference/state-data.md +215 -0
  42. package/reference/state-patterns.md +166 -0
  43. package/reference/state.md +194 -0
  44. package/reference/style-system.md +110 -0
  45. package/reference/tokens.md +460 -0
  46. package/src/app.js +19 -0
  47. package/src/chart/_animate.js +266 -0
  48. package/src/chart/_base.js +109 -0
  49. package/src/chart/_data.js +209 -0
  50. package/src/chart/_format.js +106 -0
  51. package/src/chart/_interact.js +364 -0
  52. package/src/chart/_palette.js +105 -0
  53. package/src/chart/_renderer.js +52 -0
  54. package/src/chart/_scene.js +262 -0
  55. package/src/chart/_shared.js +371 -0
  56. package/src/chart/index.js +637 -0
  57. package/src/chart/layouts/_layout-base.js +328 -0
  58. package/src/chart/layouts/cartesian.js +148 -0
  59. package/src/chart/layouts/hierarchy.js +562 -0
  60. package/src/chart/layouts/polar.js +101 -0
  61. package/src/chart/renderers/canvas.js +179 -0
  62. package/src/chart/renderers/svg.js +256 -0
  63. package/src/chart/renderers/webgpu.js +715 -0
  64. package/src/chart/types/_type-base.js +26 -0
  65. package/src/chart/types/area.js +134 -0
  66. package/src/chart/types/bar.js +173 -0
  67. package/src/chart/types/box-plot.js +125 -0
  68. package/src/chart/types/bubble.js +63 -0
  69. package/src/chart/types/candlestick.js +115 -0
  70. package/src/chart/types/chord.js +85 -0
  71. package/src/chart/types/combination.js +108 -0
  72. package/src/chart/types/funnel.js +68 -0
  73. package/src/chart/types/gauge.js +163 -0
  74. package/src/chart/types/heatmap.js +98 -0
  75. package/src/chart/types/histogram.js +71 -0
  76. package/src/chart/types/line.js +111 -0
  77. package/src/chart/types/org-chart.js +93 -0
  78. package/src/chart/types/pie.js +81 -0
  79. package/src/chart/types/radar.js +96 -0
  80. package/src/chart/types/radial.js +68 -0
  81. package/src/chart/types/range-area.js +55 -0
  82. package/src/chart/types/range-bar.js +61 -0
  83. package/src/chart/types/sankey.js +73 -0
  84. package/src/chart/types/scatter.js +66 -0
  85. package/src/chart/types/sparkline.js +81 -0
  86. package/src/chart/types/sunburst.js +69 -0
  87. package/src/chart/types/swimlane.js +88 -0
  88. package/src/chart/types/treemap.js +62 -0
  89. package/src/chart/types/waterfall.js +100 -0
  90. package/src/components/_base.js +1658 -0
  91. package/src/components/_behaviors.js +1140 -0
  92. package/src/components/_primitives.js +534 -0
  93. package/src/components/_qr-encoder.js +539 -0
  94. package/src/components/accordion.js +207 -0
  95. package/src/components/affix.js +62 -0
  96. package/src/components/alert-dialog.js +75 -0
  97. package/src/components/alert.js +47 -0
  98. package/src/components/aspect-ratio.js +24 -0
  99. package/src/components/avatar-group.js +55 -0
  100. package/src/components/avatar.js +38 -0
  101. package/src/components/back-top.js +75 -0
  102. package/src/components/badge.js +74 -0
  103. package/src/components/banner.js +68 -0
  104. package/src/components/breadcrumb.js +162 -0
  105. package/src/components/button.js +115 -0
  106. package/src/components/calendar.js +131 -0
  107. package/src/components/card.js +192 -0
  108. package/src/components/carousel.js +98 -0
  109. package/src/components/cascader.js +261 -0
  110. package/src/components/checkbox.js +80 -0
  111. package/src/components/chip.js +81 -0
  112. package/src/components/code-block.js +82 -0
  113. package/src/components/collapsible.js +50 -0
  114. package/src/components/color-palette.js +438 -0
  115. package/src/components/color-picker.js +314 -0
  116. package/src/components/combobox.js +181 -0
  117. package/src/components/command.js +174 -0
  118. package/src/components/comment.js +206 -0
  119. package/src/components/context-menu.js +76 -0
  120. package/src/components/data-table.js +724 -0
  121. package/src/components/date-picker.js +217 -0
  122. package/src/components/date-range-picker.js +244 -0
  123. package/src/components/datetime-picker.js +271 -0
  124. package/src/components/descriptions.js +68 -0
  125. package/src/components/drawer.js +179 -0
  126. package/src/components/dropdown.js +88 -0
  127. package/src/components/empty.js +41 -0
  128. package/src/components/float-button.js +90 -0
  129. package/src/components/form.js +106 -0
  130. package/src/components/hover-card.js +49 -0
  131. package/src/components/icon.js +87 -0
  132. package/src/components/image.js +97 -0
  133. package/src/components/index.js +117 -0
  134. package/src/components/input-group.js +75 -0
  135. package/src/components/input-number.js +155 -0
  136. package/src/components/input-otp.js +178 -0
  137. package/src/components/input.js +91 -0
  138. package/src/components/kbd.js +36 -0
  139. package/src/components/label.js +25 -0
  140. package/src/components/list.js +118 -0
  141. package/src/components/masked-input.js +236 -0
  142. package/src/components/mentions.js +165 -0
  143. package/src/components/menu.js +259 -0
  144. package/src/components/message.js +80 -0
  145. package/src/components/modal.js +147 -0
  146. package/src/components/navigation-menu.js +166 -0
  147. package/src/components/notification.js +84 -0
  148. package/src/components/pagination.js +104 -0
  149. package/src/components/placeholder.js +132 -0
  150. package/src/components/popconfirm.js +70 -0
  151. package/src/components/popover.js +58 -0
  152. package/src/components/progress.js +61 -0
  153. package/src/components/qrcode.js +251 -0
  154. package/src/components/radiogroup.js +120 -0
  155. package/src/components/range-slider.js +176 -0
  156. package/src/components/rate.js +186 -0
  157. package/src/components/resizable.js +83 -0
  158. package/src/components/result.js +57 -0
  159. package/src/components/scroll-area.js +43 -0
  160. package/src/components/segmented.js +97 -0
  161. package/src/components/select.js +165 -0
  162. package/src/components/separator.js +31 -0
  163. package/src/components/shell.js +407 -0
  164. package/src/components/skeleton.js +39 -0
  165. package/src/components/slider.js +141 -0
  166. package/src/components/sortable-list.js +176 -0
  167. package/src/components/space.js +42 -0
  168. package/src/components/spinner.js +112 -0
  169. package/src/components/splitter.js +147 -0
  170. package/src/components/statistic.js +136 -0
  171. package/src/components/steps.js +99 -0
  172. package/src/components/switch.js +95 -0
  173. package/src/components/table.js +44 -0
  174. package/src/components/tabs.js +216 -0
  175. package/src/components/tag.js +115 -0
  176. package/src/components/textarea.js +82 -0
  177. package/src/components/time-picker.js +153 -0
  178. package/src/components/time-range-picker.js +170 -0
  179. package/src/components/timeline.js +226 -0
  180. package/src/components/toast.js +71 -0
  181. package/src/components/toggle.js +213 -0
  182. package/src/components/tooltip.js +57 -0
  183. package/src/components/tour.js +159 -0
  184. package/src/components/transfer.js +163 -0
  185. package/src/components/tree-select.js +274 -0
  186. package/src/components/tree.js +141 -0
  187. package/src/components/typography.js +136 -0
  188. package/src/components/upload.js +118 -0
  189. package/src/components/visually-hidden.js +20 -0
  190. package/src/components/watermark.js +124 -0
  191. package/src/core/index.js +539 -0
  192. package/src/core/lifecycle.js +69 -0
  193. package/src/css/atoms.js +651 -0
  194. package/src/css/components.js +940 -0
  195. package/src/css/derive.js +1296 -0
  196. package/src/css/index.js +265 -0
  197. package/src/css/runtime.js +268 -0
  198. package/src/css/styles/addons/bioluminescent.js +93 -0
  199. package/src/css/styles/addons/clay.js +70 -0
  200. package/src/css/styles/addons/clean.js +57 -0
  201. package/src/css/styles/addons/command-center.js +143 -0
  202. package/src/css/styles/addons/dopamine.js +83 -0
  203. package/src/css/styles/addons/editorial.js +80 -0
  204. package/src/css/styles/addons/glassmorphism.js +99 -0
  205. package/src/css/styles/addons/liquid-glass.js +105 -0
  206. package/src/css/styles/addons/prismatic.js +100 -0
  207. package/src/css/styles/addons/retro.js +63 -0
  208. package/src/css/styles/auradecantism.js +96 -0
  209. package/src/css/theme-registry.js +444 -0
  210. package/src/data/entity.js +281 -0
  211. package/src/data/index.js +13 -0
  212. package/src/data/persist.js +225 -0
  213. package/src/data/query.js +839 -0
  214. package/src/data/realtime.js +299 -0
  215. package/src/data/url.js +177 -0
  216. package/src/data/worker.js +134 -0
  217. package/src/explorer/archetypes.js +243 -0
  218. package/src/explorer/atoms.js +228 -0
  219. package/src/explorer/charts.js +497 -0
  220. package/src/explorer/components.js +129 -0
  221. package/src/explorer/foundations.js +949 -0
  222. package/src/explorer/icons.js +178 -0
  223. package/src/explorer/patterns.js +247 -0
  224. package/src/explorer/recipes.js +194 -0
  225. package/src/explorer/shared/pattern-examples.js +1337 -0
  226. package/src/explorer/shared/showcase-renderer.js +958 -0
  227. package/src/explorer/shared/spec-table.js +41 -0
  228. package/src/explorer/shared/usage-links.js +87 -0
  229. package/src/explorer/shell-config.js +10 -0
  230. package/src/explorer/shells.js +551 -0
  231. package/src/explorer/styles.js +161 -0
  232. package/src/explorer/tokens.js +262 -0
  233. package/src/explorer/tools.js +525 -0
  234. package/src/form/index.js +804 -0
  235. package/src/i18n/index.js +251 -0
  236. package/src/icons/essential.js +479 -0
  237. package/src/icons/index.js +53 -0
  238. package/src/plugins/index.js +282 -0
  239. package/src/registry/archetypes/content-site.json +71 -0
  240. package/src/registry/archetypes/docs-explorer.json +23 -0
  241. package/src/registry/archetypes/ecommerce.json +104 -0
  242. package/src/registry/archetypes/financial-dashboard.json +77 -0
  243. package/src/registry/archetypes/index.json +41 -0
  244. package/src/registry/archetypes/portfolio.json +82 -0
  245. package/src/registry/archetypes/recipe-community.json +159 -0
  246. package/src/registry/archetypes/saas-dashboard.json +86 -0
  247. package/src/registry/architect/cross-cutting.json +45 -0
  248. package/src/registry/architect/domains/ecommerce.json +294 -0
  249. package/src/registry/architect/domains/financial-services.json +302 -0
  250. package/src/registry/architect/index.json +26 -0
  251. package/src/registry/architect/traits.json +379 -0
  252. package/src/registry/atoms.json +16 -0
  253. package/src/registry/chart-showcase.json +160 -0
  254. package/src/registry/chart.json +136 -0
  255. package/src/registry/components.json +8616 -0
  256. package/src/registry/core.json +216 -0
  257. package/src/registry/css.json +319 -0
  258. package/src/registry/data.json +135 -0
  259. package/src/registry/foundations.json +11 -0
  260. package/src/registry/icons.json +463 -0
  261. package/src/registry/index.json +101 -0
  262. package/src/registry/patterns/activity-feed.json +37 -0
  263. package/src/registry/patterns/article-content.json +27 -0
  264. package/src/registry/patterns/auth-form.json +37 -0
  265. package/src/registry/patterns/author-card.json +20 -0
  266. package/src/registry/patterns/card-grid.json +127 -0
  267. package/src/registry/patterns/category-nav.json +26 -0
  268. package/src/registry/patterns/chart-grid.json +36 -0
  269. package/src/registry/patterns/chat-interface.json +37 -0
  270. package/src/registry/patterns/checklist-card.json +55 -0
  271. package/src/registry/patterns/comparison-panel.json +27 -0
  272. package/src/registry/patterns/component-showcase.json +24 -0
  273. package/src/registry/patterns/contact-form.json +31 -0
  274. package/src/registry/patterns/cta-section.json +20 -0
  275. package/src/registry/patterns/data-table.json +37 -0
  276. package/src/registry/patterns/detail-header.json +83 -0
  277. package/src/registry/patterns/detail-panel.json +27 -0
  278. package/src/registry/patterns/explorer-shell.json +22 -0
  279. package/src/registry/patterns/filter-bar.json +33 -0
  280. package/src/registry/patterns/filter-sidebar.json +27 -0
  281. package/src/registry/patterns/form-sections.json +110 -0
  282. package/src/registry/patterns/goal-tracker.json +27 -0
  283. package/src/registry/patterns/hero.json +107 -0
  284. package/src/registry/patterns/index.json +47 -0
  285. package/src/registry/patterns/kpi-grid.json +36 -0
  286. package/src/registry/patterns/media-gallery.json +20 -0
  287. package/src/registry/patterns/order-history.json +20 -0
  288. package/src/registry/patterns/pagination.json +19 -0
  289. package/src/registry/patterns/photo-to-recipe.json +36 -0
  290. package/src/registry/patterns/pipeline-tracker.json +28 -0
  291. package/src/registry/patterns/post-list.json +27 -0
  292. package/src/registry/patterns/pricing-table.json +32 -0
  293. package/src/registry/patterns/scorecard.json +28 -0
  294. package/src/registry/patterns/search-bar.json +20 -0
  295. package/src/registry/patterns/specimen-grid.json +19 -0
  296. package/src/registry/patterns/stat-card.json +55 -0
  297. package/src/registry/patterns/stats-bar.json +55 -0
  298. package/src/registry/patterns/steps-card.json +55 -0
  299. package/src/registry/patterns/table-of-contents.json +19 -0
  300. package/src/registry/patterns/testimonials.json +21 -0
  301. package/src/registry/patterns/timeline.json +27 -0
  302. package/src/registry/patterns/token-inspector.json +21 -0
  303. package/src/registry/patterns/wizard.json +27 -0
  304. package/src/registry/recipe-auradecantism.json +69 -0
  305. package/src/registry/recipe-clean.json +65 -0
  306. package/src/registry/recipe-command-center.json +78 -0
  307. package/src/registry/router.json +73 -0
  308. package/src/registry/schema/README.md +197 -0
  309. package/src/registry/skeletons.json +259 -0
  310. package/src/registry/state.json +137 -0
  311. package/src/registry/tokens.json +40 -0
  312. package/src/router/hash.js +17 -0
  313. package/src/router/history.js +18 -0
  314. package/src/router/index.js +598 -0
  315. package/src/ssr/index.js +922 -0
  316. package/src/state/arrays.js +181 -0
  317. package/src/state/devtools.js +647 -0
  318. package/src/state/index.js +498 -0
  319. package/src/state/middleware.js +288 -0
  320. package/src/state/scheduler.js +206 -0
  321. package/src/state/store.js +300 -0
  322. package/src/tags/index.js +19 -0
  323. package/src/tannins/auth.js +396 -0
  324. package/src/test/dom.js +352 -0
  325. package/src/test/index.js +62 -0
  326. package/src/test/state.js +306 -0
  327. package/tools/a11y-audit.js +487 -0
  328. package/tools/analyzer.js +315 -0
  329. package/tools/audit.js +706 -0
  330. package/tools/builder.js +1422 -0
  331. package/tools/css-extract.js +188 -0
  332. package/tools/dev-server.js +316 -0
  333. package/tools/dts-gen.js +1260 -0
  334. package/tools/figma-components.js +329 -0
  335. package/tools/figma-patterns.js +516 -0
  336. package/tools/figma-plugin/code.js +453 -0
  337. package/tools/figma-plugin/manifest.json +14 -0
  338. package/tools/figma-plugin/ui.html +268 -0
  339. package/tools/figma-render.js +293 -0
  340. package/tools/figma-tokens.js +712 -0
  341. package/tools/figma-upload.js +318 -0
  342. package/tools/generate.js +738 -0
  343. package/tools/icons.js +133 -0
  344. package/tools/init-templates.js +265 -0
  345. package/tools/install-hooks.sh +5 -0
  346. package/tools/migrations/0.5.0.js +53 -0
  347. package/tools/migrations/0.6.0.js +95 -0
  348. package/tools/minify.js +170 -0
  349. package/tools/pre-commit +4 -0
  350. package/tools/registry.js +662 -0
  351. package/tools/reset-playground.js +61 -0
  352. package/tools/starter-templates/content-site/app.js +49 -0
  353. package/tools/starter-templates/content-site/essence.js +19 -0
  354. package/tools/starter-templates/content-site/pages.js +31 -0
  355. package/tools/starter-templates/ecommerce/app.js +50 -0
  356. package/tools/starter-templates/ecommerce/essence.js +19 -0
  357. package/tools/starter-templates/ecommerce/pages.js +31 -0
  358. package/tools/starter-templates/landing-page/app.js +38 -0
  359. package/tools/starter-templates/landing-page/essence.js +18 -0
  360. package/tools/starter-templates/landing-page/pages.js +21 -0
  361. package/tools/starter-templates/portfolio/app.js +45 -0
  362. package/tools/starter-templates/portfolio/essence.js +19 -0
  363. package/tools/starter-templates/portfolio/pages.js +33 -0
  364. package/tools/starter-templates/saas-dashboard/app.js +70 -0
  365. package/tools/starter-templates/saas-dashboard/essence.js +19 -0
  366. package/tools/starter-templates/saas-dashboard/pages.js +31 -0
  367. package/tools/verify-pack.js +203 -0
  368. package/types/chart.d.ts +77 -0
  369. package/types/components.d.ts +587 -0
  370. package/types/core.d.ts +89 -0
  371. package/types/css.d.ts +149 -0
  372. package/types/data.d.ts +238 -0
  373. package/types/form.d.ts +164 -0
  374. package/types/i18n.d.ts +51 -0
  375. package/types/icons.d.ts +27 -0
  376. package/types/index.d.ts +13 -0
  377. package/types/router.d.ts +116 -0
  378. package/types/ssr.d.ts +102 -0
  379. package/types/state.d.ts +83 -0
  380. package/types/tags.d.ts +62 -0
  381. package/types/tannins.d.ts +63 -0
  382. package/types/test.d.ts +48 -0
@@ -0,0 +1,487 @@
1
+ /**
2
+ * Decantr Accessibility Audit
3
+ *
4
+ * Static analysis for common WCAG violations.
5
+ * Parses JS source strings for component patterns and validates
6
+ * accessibility attributes. Zero third-party dependencies.
7
+ *
8
+ * Rules:
9
+ * button-label — Buttons must have text content, aria-label, or aria-labelledby
10
+ * input-label — Inputs must have associated label or aria-label
11
+ * img-alt — Images must have alt attribute
12
+ * focus-visible — Interactive elements need focus indicator
13
+ * keyboard-handler — Elements with onclick should also have onkeydown/onkeyup
14
+ * role-valid — Check role values against WAI-ARIA spec
15
+ * heading-order — No skipped heading levels
16
+ * contrast-ratio — Reminder about contrast when using color atoms
17
+ */
18
+
19
+ import { readFile } from 'node:fs/promises';
20
+
21
+ // ─── Valid WAI-ARIA Roles ────────────────────────────────────────
22
+
23
+ const VALID_ARIA_ROLES = new Set([
24
+ // Widget roles
25
+ 'alert', 'alertdialog', 'application', 'button', 'cell', 'checkbox',
26
+ 'columnheader', 'combobox', 'command', 'comment', 'complementary',
27
+ 'composite', 'definition', 'dialog', 'directory', 'document',
28
+ 'feed', 'figure', 'form', 'generic', 'grid', 'gridcell', 'group',
29
+ 'heading', 'img', 'input', 'insertion', 'landmark', 'link', 'list',
30
+ 'listbox', 'listitem', 'log', 'main', 'mark', 'marquee', 'math',
31
+ 'menu', 'menubar', 'menuitem', 'menuitemcheckbox', 'menuitemradio',
32
+ 'meter', 'navigation', 'none', 'note', 'option', 'paragraph',
33
+ 'presentation', 'progressbar', 'radio', 'radiogroup', 'range',
34
+ 'region', 'roletype', 'row', 'rowgroup', 'rowheader', 'scrollbar',
35
+ 'search', 'searchbox', 'section', 'sectionhead', 'select',
36
+ 'separator', 'slider', 'spinbutton', 'status', 'structure',
37
+ 'suggestion', 'switch', 'tab', 'table', 'tablist', 'tabpanel',
38
+ 'term', 'textbox', 'timer', 'toolbar', 'tooltip', 'tree',
39
+ 'treegrid', 'treeitem', 'widget', 'window',
40
+ // Landmark roles
41
+ 'banner', 'contentinfo',
42
+ ]);
43
+
44
+ // ─── Inherently keyboard-accessible elements ────────────────────
45
+
46
+ const KEYBOARD_ACCESSIBLE_ELEMENTS = new Set([
47
+ 'button', 'a', 'input', 'select', 'textarea', 'summary', 'details',
48
+ ]);
49
+
50
+ // ─── Helpers ─────────────────────────────────────────────────────
51
+
52
+ /**
53
+ * Find the line number for a character index in source.
54
+ * @param {string} source
55
+ * @param {number} index
56
+ * @returns {number}
57
+ */
58
+ function findLineNumber(source, index) {
59
+ return source.substring(0, index).split('\n').length;
60
+ }
61
+
62
+ /**
63
+ * Extract a context window around a match index.
64
+ * Returns the surrounding source from the current statement/call.
65
+ * @param {string} source
66
+ * @param {number} index
67
+ * @param {number} [before=200]
68
+ * @param {number} [after=300]
69
+ * @returns {string}
70
+ */
71
+ function getContext(source, index, before = 200, after = 300) {
72
+ const start = Math.max(0, index - before);
73
+ const end = Math.min(source.length, index + after);
74
+ return source.substring(start, end);
75
+ }
76
+
77
+ // ─── Rules ───────────────────────────────────────────────────────
78
+
79
+ /**
80
+ * Rule: button-label
81
+ * Buttons must have text content, aria-label, or aria-labelledby.
82
+ */
83
+ function ruleButtonLabel(source, filename) {
84
+ const issues = [];
85
+
86
+ // Detect Button({...}) component calls
87
+ const buttonCompRe = /\bButton\s*\(\s*\{/g;
88
+ let match;
89
+ while ((match = buttonCompRe.exec(source)) !== null) {
90
+ const ctx = getContext(source, match.index, 50, 400);
91
+ const hasAriaLabel = /['"]aria-label['"]/.test(ctx) || /aria-label\s*:/.test(ctx);
92
+ const hasAriaLabelledBy = /['"]aria-labelledby['"]/.test(ctx) || /aria-labelledby\s*:/.test(ctx);
93
+ // Check for text content: text('...') or children with text, or label prop
94
+ const hasTextChild = /\btext\s*\(/.test(ctx);
95
+ const hasLabel = /\blabel\s*:/.test(ctx);
96
+ const hasChildren = /\bchildren\s*:/.test(ctx);
97
+
98
+ if (!hasAriaLabel && !hasAriaLabelledBy && !hasTextChild && !hasLabel && !hasChildren) {
99
+ issues.push({
100
+ rule: 'button-label',
101
+ severity: 'error',
102
+ message: 'Button component missing accessible label (text content, label prop, aria-label, or aria-labelledby)',
103
+ file: filename,
104
+ line: findLineNumber(source, match.index),
105
+ });
106
+ }
107
+ }
108
+
109
+ // Detect h('button', {...}) calls
110
+ const hButtonRe = /\bh\s*\(\s*['"]button['"]\s*,/g;
111
+ while ((match = hButtonRe.exec(source)) !== null) {
112
+ const ctx = getContext(source, match.index, 50, 400);
113
+ const hasAriaLabel = /['"]aria-label['"]/.test(ctx) || /aria-label\s*:/.test(ctx);
114
+ const hasAriaLabelledBy = /['"]aria-labelledby['"]/.test(ctx) || /aria-labelledby\s*:/.test(ctx);
115
+ const hasTextChild = /\btext\s*\(/.test(ctx);
116
+
117
+ if (!hasAriaLabel && !hasAriaLabelledBy && !hasTextChild) {
118
+ issues.push({
119
+ rule: 'button-label',
120
+ severity: 'error',
121
+ message: 'h(\'button\') missing accessible label (text child, aria-label, or aria-labelledby)',
122
+ file: filename,
123
+ line: findLineNumber(source, match.index),
124
+ });
125
+ }
126
+ }
127
+
128
+ return issues;
129
+ }
130
+
131
+ /**
132
+ * Rule: input-label
133
+ * Inputs must have associated label or aria-label.
134
+ */
135
+ function ruleInputLabel(source, filename) {
136
+ const issues = [];
137
+
138
+ // Detect Input({...}) component calls
139
+ const inputCompRe = /\bInput\s*\(\s*\{/g;
140
+ let match;
141
+ while ((match = inputCompRe.exec(source)) !== null) {
142
+ const ctx = getContext(source, match.index, 50, 400);
143
+ const hasAriaLabel = /['"]aria-label['"]/.test(ctx) || /aria-label\s*:/.test(ctx);
144
+ const hasAriaLabelledBy = /['"]aria-labelledby['"]/.test(ctx) || /aria-labelledby\s*:/.test(ctx);
145
+ const hasLabel = /\blabel\s*:/.test(ctx);
146
+ const hasId = /\bid\s*:/.test(ctx);
147
+ const hasPlaceholder = /\bplaceholder\s*:/.test(ctx);
148
+
149
+ if (!hasAriaLabel && !hasAriaLabelledBy && !hasLabel && !hasId) {
150
+ issues.push({
151
+ rule: 'input-label',
152
+ severity: 'error',
153
+ message: 'Input component missing accessible label (label prop, aria-label, aria-labelledby, or id for external label)',
154
+ file: filename,
155
+ line: findLineNumber(source, match.index),
156
+ });
157
+ }
158
+ }
159
+
160
+ // Detect h('input', {...}) calls
161
+ const hInputRe = /\bh\s*\(\s*['"]input['"]\s*,/g;
162
+ while ((match = hInputRe.exec(source)) !== null) {
163
+ const ctx = getContext(source, match.index, 50, 400);
164
+ const hasAriaLabel = /['"]aria-label['"]/.test(ctx) || /aria-label\s*:/.test(ctx);
165
+ const hasAriaLabelledBy = /['"]aria-labelledby['"]/.test(ctx) || /aria-labelledby\s*:/.test(ctx);
166
+ const hasId = /\bid\s*:/.test(ctx);
167
+
168
+ if (!hasAriaLabel && !hasAriaLabelledBy && !hasId) {
169
+ issues.push({
170
+ rule: 'input-label',
171
+ severity: 'error',
172
+ message: 'h(\'input\') missing accessible label (aria-label, aria-labelledby, or id for external label)',
173
+ file: filename,
174
+ line: findLineNumber(source, match.index),
175
+ });
176
+ }
177
+ }
178
+
179
+ return issues;
180
+ }
181
+
182
+ /**
183
+ * Rule: img-alt
184
+ * Images must have alt attribute.
185
+ */
186
+ function ruleImgAlt(source, filename) {
187
+ const issues = [];
188
+
189
+ // Detect Image({...}) component calls
190
+ const imageCompRe = /\bImage\s*\(\s*\{/g;
191
+ let match;
192
+ while ((match = imageCompRe.exec(source)) !== null) {
193
+ const ctx = getContext(source, match.index, 50, 400);
194
+ const hasAlt = /\balt\s*:/.test(ctx);
195
+ const hasAriaLabel = /['"]aria-label['"]/.test(ctx) || /aria-label\s*:/.test(ctx);
196
+ const hasRole = /role\s*:\s*['"]presentation['"]/.test(ctx) || /role\s*:\s*['"]none['"]/.test(ctx);
197
+
198
+ if (!hasAlt && !hasAriaLabel && !hasRole) {
199
+ issues.push({
200
+ rule: 'img-alt',
201
+ severity: 'error',
202
+ message: 'Image component missing alt attribute (provide alt, aria-label, or role="presentation" for decorative images)',
203
+ file: filename,
204
+ line: findLineNumber(source, match.index),
205
+ });
206
+ }
207
+ }
208
+
209
+ // Detect h('img', {...}) calls
210
+ const hImgRe = /\bh\s*\(\s*['"]img['"]\s*,/g;
211
+ while ((match = hImgRe.exec(source)) !== null) {
212
+ const ctx = getContext(source, match.index, 50, 400);
213
+ const hasAlt = /\balt\s*:/.test(ctx);
214
+ const hasAriaLabel = /['"]aria-label['"]/.test(ctx) || /aria-label\s*:/.test(ctx);
215
+ const hasRole = /role\s*:\s*['"]presentation['"]/.test(ctx) || /role\s*:\s*['"]none['"]/.test(ctx);
216
+
217
+ if (!hasAlt && !hasAriaLabel && !hasRole) {
218
+ issues.push({
219
+ rule: 'img-alt',
220
+ severity: 'error',
221
+ message: 'h(\'img\') missing alt attribute (provide alt, aria-label, or role="presentation")',
222
+ file: filename,
223
+ line: findLineNumber(source, match.index),
224
+ });
225
+ }
226
+ }
227
+
228
+ return issues;
229
+ }
230
+
231
+ /**
232
+ * Rule: focus-visible
233
+ * Interactive elements with onclick should have focus indicator classes.
234
+ */
235
+ function ruleFocusVisible(source, filename) {
236
+ const issues = [];
237
+
238
+ // Find elements with onclick handlers
239
+ const onclickRe = /\bonclick\s*:/g;
240
+ let match;
241
+ while ((match = onclickRe.exec(source)) !== null) {
242
+ const ctx = getContext(source, match.index, 300, 300);
243
+ const hasFocusVisible = /_focusVisible/.test(ctx) || /_ring/.test(ctx) || /_focusRing/.test(ctx);
244
+ const hasOutline = /outline/.test(ctx);
245
+ const hasFocusClass = /focus/.test(ctx) && /class/.test(ctx);
246
+ // Skip if this is inside a Button/Input/etc. component (they handle focus internally)
247
+ const isComponentProp = /\b(Button|Input|Select|Checkbox|Switch|Radio)\s*\(\s*\{/.test(ctx);
248
+
249
+ if (!hasFocusVisible && !hasOutline && !hasFocusClass && !isComponentProp) {
250
+ issues.push({
251
+ rule: 'focus-visible',
252
+ severity: 'warning',
253
+ message: 'Element with onclick handler may need focus indicator (_focusVisible or _ring atom)',
254
+ file: filename,
255
+ line: findLineNumber(source, match.index),
256
+ });
257
+ }
258
+ }
259
+
260
+ return issues;
261
+ }
262
+
263
+ /**
264
+ * Rule: keyboard-handler
265
+ * Non-button/link elements with onclick should also have onkeydown/onkeyup.
266
+ */
267
+ function ruleKeyboardHandler(source, filename) {
268
+ const issues = [];
269
+
270
+ // Find onclick handlers and check context for element type and keyboard handler
271
+ const onclickRe = /\bonclick\s*:/g;
272
+ let match;
273
+ while ((match = onclickRe.exec(source)) !== null) {
274
+ const ctx = getContext(source, match.index, 300, 300);
275
+
276
+ // Check if this is inside a natively keyboard-accessible element
277
+ let isNativelyAccessible = false;
278
+ for (const el of KEYBOARD_ACCESSIBLE_ELEMENTS) {
279
+ // Check for h('button', or h('a', or Button( etc.
280
+ const hPattern = new RegExp(`\\bh\\s*\\(\\s*['"]${el}['"]`);
281
+ if (hPattern.test(ctx)) {
282
+ isNativelyAccessible = true;
283
+ break;
284
+ }
285
+ }
286
+ // Check for component calls like Button(, Select(, etc.
287
+ if (/\b(Button|Input|Select|Checkbox|Switch|Radio|Combobox|Textarea)\s*\(\s*\{/.test(ctx)) {
288
+ isNativelyAccessible = true;
289
+ }
290
+ // Check for role="button" which implies keyboard handling
291
+ if (/role\s*:\s*['"]button['"]/.test(ctx)) {
292
+ isNativelyAccessible = true;
293
+ }
294
+
295
+ if (isNativelyAccessible) continue;
296
+
297
+ const hasKeyboard = /\bonkeydown\s*:/.test(ctx) || /\bonkeyup\s*:/.test(ctx) || /\bonkeypress\s*:/.test(ctx);
298
+ const hasTabindex = /\btabindex\s*:/.test(ctx) || /\btabIndex\s*:/.test(ctx);
299
+
300
+ if (!hasKeyboard) {
301
+ issues.push({
302
+ rule: 'keyboard-handler',
303
+ severity: 'error',
304
+ message: 'Non-interactive element with onclick missing keyboard handler (add onkeydown/onkeyup for keyboard accessibility)',
305
+ file: filename,
306
+ line: findLineNumber(source, match.index),
307
+ });
308
+ }
309
+ }
310
+
311
+ return issues;
312
+ }
313
+
314
+ /**
315
+ * Rule: role-valid
316
+ * Check that role values are valid WAI-ARIA roles.
317
+ */
318
+ function ruleRoleValid(source, filename) {
319
+ const issues = [];
320
+
321
+ // Match role: 'value' or role: "value" or 'role': 'value'
322
+ const roleRe = /(?:['"]?role['"]?\s*:\s*)['"]([a-zA-Z]+)['"]/g;
323
+ let match;
324
+ while ((match = roleRe.exec(source)) !== null) {
325
+ const role = match[1].toLowerCase();
326
+ if (!VALID_ARIA_ROLES.has(role)) {
327
+ issues.push({
328
+ rule: 'role-valid',
329
+ severity: 'error',
330
+ message: `Invalid ARIA role "${match[1]}" — must be a valid WAI-ARIA role`,
331
+ file: filename,
332
+ line: findLineNumber(source, match.index),
333
+ });
334
+ }
335
+ }
336
+
337
+ return issues;
338
+ }
339
+
340
+ /**
341
+ * Rule: heading-order
342
+ * Heading levels should not be skipped (e.g. h1 -> h3 without h2).
343
+ */
344
+ function ruleHeadingOrder(source, filename) {
345
+ const issues = [];
346
+
347
+ // Collect all heading references in order of appearance
348
+ const headings = [];
349
+
350
+ // Match h('h1') through h('h6')
351
+ const hCallRe = /\bh\s*\(\s*['"]h([1-6])['"]/g;
352
+ let match;
353
+ while ((match = hCallRe.exec(source)) !== null) {
354
+ headings.push({
355
+ level: parseInt(match[1], 10),
356
+ index: match.index,
357
+ });
358
+ }
359
+
360
+ // Match tags.h1 through tags.h6
361
+ const tagsRe = /\btags\.h([1-6])\b/g;
362
+ while ((match = tagsRe.exec(source)) !== null) {
363
+ headings.push({
364
+ level: parseInt(match[1], 10),
365
+ index: match.index,
366
+ });
367
+ }
368
+
369
+ // Sort by position in source
370
+ headings.sort((a, b) => a.index - b.index);
371
+
372
+ // Check for skipped levels
373
+ for (let i = 1; i < headings.length; i++) {
374
+ const prev = headings[i - 1].level;
375
+ const curr = headings[i].level;
376
+ // Only flag when going deeper and skipping a level (e.g. h1 -> h3)
377
+ if (curr > prev && curr - prev > 1) {
378
+ issues.push({
379
+ rule: 'heading-order',
380
+ severity: 'warning',
381
+ message: `Heading level skipped: h${prev} to h${curr} (missing h${prev + 1})`,
382
+ file: filename,
383
+ line: findLineNumber(source, headings[i].index),
384
+ });
385
+ }
386
+ }
387
+
388
+ return issues;
389
+ }
390
+
391
+ /**
392
+ * Rule: contrast-ratio
393
+ * Info-level reminder when foreground and background color atoms are used together.
394
+ * Full contrast checking requires runtime; this is a static reminder.
395
+ */
396
+ function ruleContrastRatio(source, filename) {
397
+ const issues = [];
398
+
399
+ // Look for css() calls with both _fg* and _bg* atoms
400
+ const cssCallRe = /\bcss\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
401
+ let match;
402
+ while ((match = cssCallRe.exec(source)) !== null) {
403
+ const atomStr = match[1];
404
+ const hasFg = /_fg[a-zA-Z]/.test(atomStr);
405
+ const hasBg = /_bg[a-zA-Z]/.test(atomStr);
406
+ if (hasFg && hasBg) {
407
+ issues.push({
408
+ rule: 'contrast-ratio',
409
+ severity: 'info',
410
+ message: 'Foreground and background color atoms used together — verify WCAG AA contrast ratio (4.5:1 for text, 3:1 for large text)',
411
+ file: filename,
412
+ line: findLineNumber(source, match.index),
413
+ });
414
+ }
415
+ }
416
+
417
+ return issues;
418
+ }
419
+
420
+ // ─── Rule Registry ───────────────────────────────────────────────
421
+
422
+ const RULES = [
423
+ ruleButtonLabel,
424
+ ruleInputLabel,
425
+ ruleImgAlt,
426
+ ruleFocusVisible,
427
+ ruleKeyboardHandler,
428
+ ruleRoleValid,
429
+ ruleHeadingOrder,
430
+ ruleContrastRatio,
431
+ ];
432
+
433
+ // ─── Public API ──────────────────────────────────────────────────
434
+
435
+ /**
436
+ * Audit a single source string for accessibility issues.
437
+ * @param {string} source - JavaScript source code
438
+ * @param {string} [filename='unknown'] - Filename for issue reporting
439
+ * @returns {{rule: string, severity: 'error'|'warning'|'info', message: string, file: string, line?: number}[]}
440
+ */
441
+ export function auditSource(source, filename = 'unknown') {
442
+ const issues = [];
443
+ for (const rule of RULES) {
444
+ issues.push(...rule(source, filename));
445
+ }
446
+ return issues;
447
+ }
448
+
449
+ /**
450
+ * Audit multiple files from disk.
451
+ * @param {string[]} files - Array of absolute file paths
452
+ * @returns {Promise<{rule: string, severity: 'error'|'warning'|'info', message: string, file: string, line?: number}[]>}
453
+ */
454
+ export async function auditFiles(files) {
455
+ const allIssues = [];
456
+ for (const file of files) {
457
+ try {
458
+ const source = await readFile(file, 'utf-8');
459
+ allIssues.push(...auditSource(source, file));
460
+ } catch {
461
+ // Skip unreadable files
462
+ }
463
+ }
464
+ return allIssues;
465
+ }
466
+
467
+ /**
468
+ * Format audit issues into a human-readable report string.
469
+ * @param {{rule: string, severity: 'error'|'warning'|'info', message: string, file: string, line?: number}[]} issues
470
+ * @returns {string}
471
+ */
472
+ export function formatIssues(issues) {
473
+ if (issues.length === 0) return ' No accessibility issues found.\n';
474
+
475
+ const errors = issues.filter(i => i.severity === 'error');
476
+ const warnings = issues.filter(i => i.severity === 'warning');
477
+ const info = issues.filter(i => i.severity === 'info');
478
+
479
+ let output = '\n';
480
+ for (const issue of issues) {
481
+ const loc = issue.line ? `:${issue.line}` : '';
482
+ const icon = issue.severity === 'error' ? '\u2717' : issue.severity === 'warning' ? '\u26A0' : '\u2139';
483
+ output += ` ${icon} [${issue.rule}] ${issue.file}${loc}: ${issue.message}\n`;
484
+ }
485
+ output += `\n ${errors.length} error(s), ${warnings.length} warning(s), ${info.length} info\n`;
486
+ return output;
487
+ }