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,352 @@
1
+ class EventTarget_ {
2
+ constructor() {
3
+ /** @type {Map<string, Set<Function>>} */
4
+ this._listeners = new Map();
5
+ }
6
+ addEventListener(type, fn) {
7
+ let set = this._listeners.get(type);
8
+ if (!set) { set = new Set(); this._listeners.set(type, set); }
9
+ set.add(fn);
10
+ }
11
+ removeEventListener(type, fn) {
12
+ const set = this._listeners.get(type);
13
+ if (set) set.delete(fn);
14
+ }
15
+ dispatchEvent(event) {
16
+ try { event.target = this; } catch (e) { /* target may be read-only */ }
17
+ let node = this;
18
+ while (node) {
19
+ try { event.currentTarget = node; } catch (e) { /* currentTarget may be read-only */ }
20
+ const set = node._listeners ? node._listeners.get(event.type) : null;
21
+ if (set) for (const fn of set) fn(event);
22
+ node = event.bubbles ? node.parentNode : null;
23
+ }
24
+ return true;
25
+ }
26
+ }
27
+
28
+ class Node_ extends EventTarget_ {
29
+ constructor(nodeType) {
30
+ super();
31
+ this.nodeType = nodeType;
32
+ this.parentNode = null;
33
+ this.childNodes = [];
34
+ this.nodeValue = null;
35
+ }
36
+ get textContent() {
37
+ if (this.nodeType === 3) return this.nodeValue;
38
+ if (this.nodeType === 8) return this.nodeValue;
39
+ return this.childNodes.map(c => c.textContent).join('');
40
+ }
41
+ set textContent(v) {
42
+ if (this.nodeType === 3 || this.nodeType === 8) { this.nodeValue = v; return; }
43
+ this.childNodes.forEach(c => { c.parentNode = null; });
44
+ this.childNodes = [];
45
+ if (v) this.appendChild(new Text_(v));
46
+ }
47
+ appendChild(child) {
48
+ if (child.parentNode) child.parentNode.removeChild(child);
49
+ child.parentNode = this;
50
+ this.childNodes.push(child);
51
+ return child;
52
+ }
53
+ removeChild(child) {
54
+ const i = this.childNodes.indexOf(child);
55
+ if (i !== -1) { this.childNodes.splice(i, 1); child.parentNode = null; }
56
+ return child;
57
+ }
58
+ insertBefore(newChild, ref) {
59
+ if (newChild.parentNode) newChild.parentNode.removeChild(newChild);
60
+ const i = ref ? this.childNodes.indexOf(ref) : this.childNodes.length;
61
+ newChild.parentNode = this;
62
+ this.childNodes.splice(i === -1 ? this.childNodes.length : i, 0, newChild);
63
+ return newChild;
64
+ }
65
+ replaceChildren(...nodes) {
66
+ this.childNodes.forEach(c => { c.parentNode = null; });
67
+ this.childNodes = [];
68
+ for (const n of nodes) this.appendChild(n);
69
+ }
70
+ get firstChild() { return this.childNodes[0] || null; }
71
+ get lastChild() { return this.childNodes[this.childNodes.length - 1] || null; }
72
+ get nextSibling() {
73
+ if (!this.parentNode) return null;
74
+ const i = this.parentNode.childNodes.indexOf(this);
75
+ return this.parentNode.childNodes[i + 1] || null;
76
+ }
77
+ get previousSibling() {
78
+ if (!this.parentNode) return null;
79
+ const i = this.parentNode.childNodes.indexOf(this);
80
+ return this.parentNode.childNodes[i - 1] || null;
81
+ }
82
+ contains(other) {
83
+ if (this === other) return true;
84
+ return this.childNodes.some(c => c.contains(other));
85
+ }
86
+ cloneNode(deep) {
87
+ const clone = new this.constructor(this.nodeType);
88
+ clone.nodeValue = this.nodeValue;
89
+ if (deep) {
90
+ for (const c of this.childNodes) clone.appendChild(c.cloneNode(true));
91
+ }
92
+ return clone;
93
+ }
94
+ }
95
+
96
+ class Text_ extends Node_ {
97
+ constructor(data) {
98
+ super(3);
99
+ this.nodeValue = String(data);
100
+ }
101
+ get data() { return this.nodeValue; }
102
+ set data(v) { this.nodeValue = v; }
103
+ }
104
+
105
+ class Comment_ extends Node_ {
106
+ constructor(data) {
107
+ super(8);
108
+ this.nodeValue = data || '';
109
+ }
110
+ }
111
+
112
+ class ClassList_ {
113
+ constructor(el) { this._el = el; }
114
+ _classes() { return (this._el._attrs.get('class') || '').split(/\s+/).filter(Boolean); }
115
+ _set(arr) { this._el._attrs.set('class', arr.join(' ')); }
116
+ add(...tokens) { const c = this._classes(); for (const t of tokens) if (!c.includes(t)) c.push(t); this._set(c); }
117
+ remove(...tokens) { this._set(this._classes().filter(c => !tokens.includes(c))); }
118
+ toggle(token, force) {
119
+ const has = this.contains(token);
120
+ if (force !== undefined) { if (force) { if (!has) this.add(token); } else { this.remove(token); } return force; }
121
+ if (has) { this.remove(token); return false; }
122
+ this.add(token); return true;
123
+ }
124
+ contains(token) { return this._classes().includes(token); }
125
+ get length() { return this._classes().length; }
126
+ }
127
+
128
+ class Element_ extends Node_ {
129
+ constructor(tagName) {
130
+ super(1);
131
+ this.tagName = tagName.toUpperCase();
132
+ this.localName = tagName.toLowerCase();
133
+ /** @type {Map<string, string>} */
134
+ this._attrs = new Map();
135
+ this.style = {};
136
+ this.classList = new ClassList_(this);
137
+ this._defineReflectedProps();
138
+ this._open = false;
139
+ }
140
+ _defineReflectedProps() {
141
+ for (const prop of ['disabled', 'checked', 'readOnly', 'required']) {
142
+ const attr = prop.toLowerCase();
143
+ Object.defineProperty(this, prop, {
144
+ get() { return this._attrs.has(attr); },
145
+ set(v) { if (v) this._attrs.set(attr, ''); else this._attrs.delete(attr); },
146
+ configurable: true,
147
+ });
148
+ }
149
+ }
150
+ get open() { return this._open; }
151
+ showModal() { this._open = true; }
152
+ close() {
153
+ if (this._open) {
154
+ this._open = false;
155
+ this.dispatchEvent(new Event_('close'));
156
+ }
157
+ }
158
+ showPopover() {
159
+ this._popoverOpen = true;
160
+ const e = new Event_('toggle'); e.newState = 'open'; e.oldState = 'closed';
161
+ this.dispatchEvent(e);
162
+ }
163
+ hidePopover() {
164
+ if (this._popoverOpen) {
165
+ this._popoverOpen = false;
166
+ const e = new Event_('toggle'); e.newState = 'closed'; e.oldState = 'open';
167
+ this.dispatchEvent(e);
168
+ }
169
+ }
170
+ click() { this.dispatchEvent(new Event_('click', { bubbles: true })); }
171
+ getAttribute(name) { return this._attrs.get(name) ?? null; }
172
+ setAttribute(name, value) { this._attrs.set(name, String(value)); }
173
+ removeAttribute(name) { this._attrs.delete(name); }
174
+ hasAttribute(name) { return this._attrs.has(name); }
175
+ get dataset() {
176
+ const el = this;
177
+ return new Proxy({}, {
178
+ get(_, prop) {
179
+ const attr = 'data-' + prop.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
180
+ return el._attrs.get(attr) ?? undefined;
181
+ },
182
+ set(_, prop, value) {
183
+ const attr = 'data-' + prop.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
184
+ el._attrs.set(attr, String(value));
185
+ return true;
186
+ }
187
+ });
188
+ }
189
+ get className() { return this._attrs.get('class') || ''; }
190
+ set className(v) { this._attrs.set('class', v); }
191
+ get id() { return this._attrs.get('id') || ''; }
192
+ set id(v) { this._attrs.set('id', v); }
193
+ get innerHTML() {
194
+ return this.childNodes.map(c => {
195
+ if (c.nodeType === 3) return c.nodeValue;
196
+ if (c.nodeType === 8) return `<!--${c.nodeValue}-->`;
197
+ if (c.nodeType === 1) return c.outerHTML;
198
+ return '';
199
+ }).join('');
200
+ }
201
+ set innerHTML(v) {
202
+ this.replaceChildren();
203
+ if (v) this.appendChild(new Text_(v));
204
+ }
205
+ get outerHTML() {
206
+ const tag = this.localName;
207
+ let attrs = '';
208
+ for (const [k, v] of this._attrs) attrs += ` ${k}="${v}"`;
209
+ return `<${tag}${attrs}>${this.innerHTML}</${tag}>`;
210
+ }
211
+ append(...nodes) {
212
+ for (const n of nodes) {
213
+ this.appendChild(typeof n === 'string' ? new Text_(n) : n);
214
+ }
215
+ }
216
+ prepend(...nodes) {
217
+ const first = this.firstChild;
218
+ for (const n of nodes) {
219
+ this.insertBefore(typeof n === 'string' ? new Text_(n) : n, first);
220
+ }
221
+ }
222
+ remove() {
223
+ if (this.parentNode) this.parentNode.removeChild(this);
224
+ }
225
+ querySelector(selector) {
226
+ return queryOne(this, selector);
227
+ }
228
+ querySelectorAll(selector) {
229
+ const results = [];
230
+ queryAll(this, selector, results);
231
+ return results;
232
+ }
233
+ get children() {
234
+ return this.childNodes.filter(c => c.nodeType === 1);
235
+ }
236
+ closest(selector) {
237
+ let node = this;
238
+ while (node) {
239
+ if (node.nodeType === 1 && matchesSelector(node, selector)) return node;
240
+ node = node.parentNode;
241
+ }
242
+ return null;
243
+ }
244
+ matches(selector) {
245
+ return matchesSelector(this, selector);
246
+ }
247
+ }
248
+
249
+ function matchesSelector(el, selector) {
250
+ if (selector.startsWith('#')) return el.id === selector.slice(1);
251
+ if (selector.startsWith('.')) return el.className.split(/\s+/).includes(selector.slice(1));
252
+ if (selector.startsWith('[')) {
253
+ const m = selector.match(/^\[([^\]=]+)(?:="([^"]*)")?\]$/);
254
+ if (m) return el.hasAttribute(m[1]) && (m[2] === undefined || el.getAttribute(m[1]) === m[2]);
255
+ return false;
256
+ }
257
+ return el.localName === selector.toLowerCase();
258
+ }
259
+
260
+ function queryOne(el, selector) {
261
+ for (const child of el.childNodes) {
262
+ if (child.nodeType === 1) {
263
+ if (matchesSelector(child, selector)) return child;
264
+ const found = queryOne(child, selector);
265
+ if (found) return found;
266
+ }
267
+ }
268
+ return null;
269
+ }
270
+
271
+ function queryAll(el, selector, results) {
272
+ for (const child of el.childNodes) {
273
+ if (child.nodeType === 1) {
274
+ if (matchesSelector(child, selector)) results.push(child);
275
+ queryAll(child, selector, results);
276
+ }
277
+ }
278
+ }
279
+
280
+ class Document_ extends Node_ {
281
+ constructor() {
282
+ super(9);
283
+ this.body = this.createElement('body');
284
+ this.head = this.createElement('head');
285
+ this.documentElement = this.createElement('html');
286
+ this.documentElement.appendChild(this.head);
287
+ this.documentElement.appendChild(this.body);
288
+ this.appendChild(this.documentElement);
289
+ }
290
+ createElement(tag) { return new Element_(tag); }
291
+ createElementNS(ns, tag) { return new Element_(tag); }
292
+ createTextNode(data) { return new Text_(data); }
293
+ createComment(data) { return new Comment_(data); }
294
+ createDocumentFragment() { return new Element_('d-fragment'); }
295
+ getElementById(id) { return queryOne(this.documentElement, `#${id}`); }
296
+ querySelector(sel) { return queryOne(this.documentElement, sel); }
297
+ querySelectorAll(sel) {
298
+ const results = [];
299
+ queryAll(this.documentElement, sel, results);
300
+ return results;
301
+ }
302
+ }
303
+
304
+ class Event_ {
305
+ constructor(type, opts = {}) {
306
+ this.type = type;
307
+ this.bubbles = opts.bubbles || false;
308
+ this.cancelable = opts.cancelable || false;
309
+ this.target = null;
310
+ this.defaultPrevented = false;
311
+ }
312
+ preventDefault() { this.defaultPrevented = true; }
313
+ stopPropagation() { this.bubbles = false; }
314
+ }
315
+
316
+ /**
317
+ * @returns {{ document: Document_, window: Object, cleanup: Function }}
318
+ */
319
+ export function createDOM() {
320
+ const doc = new Document_();
321
+ const win = {
322
+ document: doc,
323
+ Event: Event_,
324
+ location: { hash: '', pathname: '/', search: '', href: 'http://localhost/' },
325
+ history: {
326
+ _stack: [{ state: null, url: '/' }],
327
+ pushState(state, title, url) { this._stack.push({ state, url }); win.location.pathname = url; },
328
+ replaceState(state, title, url) { this._stack[this._stack.length - 1] = { state, url }; win.location.pathname = url; },
329
+ back() { if (this._stack.length > 1) this._stack.pop(); },
330
+ forward() { /* no-op in test DOM — no forward stack */ }
331
+ },
332
+ addEventListener: doc.addEventListener.bind(doc),
333
+ removeEventListener: doc.removeEventListener.bind(doc),
334
+ dispatchEvent: doc.dispatchEvent.bind(doc)
335
+ };
336
+ const prevDoc = globalThis.document;
337
+ const prevWin = globalThis.window;
338
+ globalThis.document = doc;
339
+ globalThis.window = win;
340
+ return {
341
+ document: doc,
342
+ window: win,
343
+ cleanup() {
344
+ if (prevDoc) globalThis.document = prevDoc;
345
+ else delete globalThis.document;
346
+ if (prevWin) globalThis.window = prevWin;
347
+ else delete globalThis.window;
348
+ }
349
+ };
350
+ }
351
+
352
+ export { Element_, Text_, Comment_, Document_, Event_ };
@@ -0,0 +1,62 @@
1
+ export { describe, it, test, before, after, beforeEach, afterEach, mock } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ export { assert };
4
+ export { createDOM } from './dom.js';
5
+ export { flushEffects, withScope, mockSignal, expectEffect, expectSignal, createTestStore, settled } from './state.js';
6
+ import { createDOM } from './dom.js';
7
+ import { flush as flushScheduler } from '../state/scheduler.js';
8
+
9
+ /**
10
+ * @param {Function} component
11
+ * @returns {{ container: Element, getByText: Function, getByClass: Function, getById: Function }}
12
+ */
13
+ export function render(component) {
14
+ const { document, cleanup } = createDOM();
15
+ const container = document.createElement('div');
16
+ document.body.appendChild(container);
17
+ const result = component();
18
+ if (result) container.appendChild(result);
19
+
20
+ function getByText(text) {
21
+ return findByText(container, text);
22
+ }
23
+
24
+ function getByClass(cls) {
25
+ return container.querySelector(`.${cls}`);
26
+ }
27
+
28
+ function getById(id) {
29
+ return container.querySelector(`#${id}`);
30
+ }
31
+
32
+ return { container, getByText, getByClass, getById, cleanup };
33
+ }
34
+
35
+ function findByText(el, text) {
36
+ for (const child of el.childNodes) {
37
+ if (child.nodeType === 1) {
38
+ if (child.textContent.trim() === text) return child;
39
+ const found = findByText(child, text);
40
+ if (found) return found;
41
+ }
42
+ }
43
+ return null;
44
+ }
45
+
46
+ /**
47
+ * @param {Element} el
48
+ * @param {string} event
49
+ * @param {Object} [detail]
50
+ */
51
+ export function fire(el, event, detail) {
52
+ const evt = { type: event, target: el, bubbles: true, cancelable: true, defaultPrevented: false, preventDefault() { this.defaultPrevented = true; }, stopPropagation() {}, ...detail };
53
+ el.dispatchEvent(evt);
54
+ }
55
+
56
+ /**
57
+ * @returns {Promise<void>}
58
+ */
59
+ export async function flush() {
60
+ flushScheduler();
61
+ await new Promise(r => setTimeout(r, 0));
62
+ }
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Testing utilities for Decantr's reactive state system.
3
+ *
4
+ * Provides helpers for synchronous effect flushing, scoped reactive contexts,
5
+ * mock signals with instrumentation, and assertion wrappers.
6
+ *
7
+ * @module test/state
8
+ */
9
+
10
+ import { createSignal, createEffect, createRoot, createStore } from '../state/index.js';
11
+ import { flush } from '../state/scheduler.js';
12
+
13
+ // ─── flushEffects ────────────────────────────────────────────
14
+
15
+ /**
16
+ * Synchronously flush all pending batched effects.
17
+ * Call after mutating signals inside `batch()` to force immediate propagation.
18
+ */
19
+ export function flushEffects() {
20
+ flush();
21
+ }
22
+
23
+ // ─── withScope ───────────────────────────────────────────────
24
+
25
+ /**
26
+ * Run `fn` inside an isolated reactive root that auto-disposes on completion.
27
+ * All signals, effects, and memos created within `fn` are cleaned up automatically.
28
+ *
29
+ * @template T
30
+ * @param {() => T} fn
31
+ * @returns {T}
32
+ */
33
+ export function withScope(fn) {
34
+ /** @type {T} */
35
+ let result;
36
+ createRoot((dispose) => {
37
+ try {
38
+ result = fn();
39
+ } finally {
40
+ dispose();
41
+ }
42
+ });
43
+ return result;
44
+ }
45
+
46
+ // ─── mockSignal ──────────────────────────────────────────────
47
+
48
+ /**
49
+ * Create an instrumented signal for testing.
50
+ * Tracks read/write counts and value history.
51
+ *
52
+ * @template T
53
+ * @param {T} initialValue
54
+ * @returns {{
55
+ * get: () => T,
56
+ * set: (v: T | ((prev: T) => T)) => void,
57
+ * getter: () => T,
58
+ * setter: (v: T | ((prev: T) => T)) => void,
59
+ * readCount: number,
60
+ * writeCount: number,
61
+ * history: T[],
62
+ * reset: () => void
63
+ * }}
64
+ */
65
+ export function mockSignal(initialValue) {
66
+ const [getter, setter] = createSignal(initialValue);
67
+
68
+ /** @type {T[]} */
69
+ const history = [initialValue];
70
+
71
+ const mock = {
72
+ readCount: 0,
73
+ writeCount: 0,
74
+ history,
75
+
76
+ /**
77
+ * Read the current value. Increments `readCount`.
78
+ * Participates in reactive tracking (safe inside effects).
79
+ * @returns {T}
80
+ */
81
+ get() {
82
+ mock.readCount++;
83
+ return getter();
84
+ },
85
+
86
+ /**
87
+ * Write a new value. Increments `writeCount` and appends to `history`.
88
+ * @param {T | ((prev: T) => T)} v
89
+ */
90
+ set(v) {
91
+ mock.writeCount++;
92
+ setter(v);
93
+ history.push(getter());
94
+ },
95
+
96
+ /** Alias — pass to APIs that expect a getter function. */
97
+ get getter() { return mock.get; },
98
+
99
+ /** Alias — pass to APIs that expect a setter function. */
100
+ get setter() { return mock.set; },
101
+
102
+ /**
103
+ * Reset to initial value, clear counters, and truncate history.
104
+ */
105
+ reset() {
106
+ mock.readCount = 0;
107
+ mock.writeCount = 0;
108
+ history.length = 0;
109
+ history.push(initialValue);
110
+ setter(initialValue);
111
+ }
112
+ };
113
+
114
+ return mock;
115
+ }
116
+
117
+ // ─── expectEffect ────────────────────────────────────────────
118
+
119
+ /**
120
+ * Run a reactive effect and return assertion helpers.
121
+ * The effect is tracked for run-count and can be disposed manually.
122
+ *
123
+ * @param {Function} fn — effect body (same signature as `createEffect`)
124
+ * @returns {{
125
+ * runCount: number,
126
+ * toHaveRun: () => void,
127
+ * toHaveRunTimes: (n: number) => void,
128
+ * dispose: () => void
129
+ * }}
130
+ */
131
+ export function expectEffect(fn) {
132
+ let runCount = 0;
133
+
134
+ const dispose = createEffect(() => {
135
+ runCount++;
136
+ return fn();
137
+ });
138
+
139
+ const handle = {
140
+ /** Current number of times the effect body has executed. */
141
+ get runCount() { return runCount; },
142
+
143
+ /**
144
+ * Assert that the effect has executed at least once.
145
+ * @throws {Error} if the effect never ran
146
+ */
147
+ toHaveRun() {
148
+ if (runCount === 0) {
149
+ throw new Error('Expected effect to have run, but it never executed');
150
+ }
151
+ },
152
+
153
+ /**
154
+ * Assert that the effect has executed exactly `n` times.
155
+ * @param {number} n
156
+ * @throws {Error} if count does not match
157
+ */
158
+ toHaveRunTimes(n) {
159
+ if (runCount !== n) {
160
+ throw new Error(
161
+ `Expected effect to have run ${n} time(s), but it ran ${runCount} time(s)`
162
+ );
163
+ }
164
+ },
165
+
166
+ /**
167
+ * Dispose the effect so it stops reacting to signal changes.
168
+ */
169
+ dispose
170
+ };
171
+
172
+ return handle;
173
+ }
174
+
175
+ // ─── expectSignal ────────────────────────────────────────────
176
+
177
+ /**
178
+ * Wrap a signal getter with assertion helpers.
179
+ *
180
+ * @template T
181
+ * @param {() => T} getter — signal getter function
182
+ * @returns {{
183
+ * toEqual: (expected: T) => void,
184
+ * toBeTruthy: () => void,
185
+ * toBeFalsy: () => void
186
+ * }}
187
+ */
188
+ export function expectSignal(getter) {
189
+ return {
190
+ /**
191
+ * Assert that the signal's current value equals `expected` (via `Object.is`).
192
+ * @param {T} expected
193
+ * @throws {Error} if values are not equal
194
+ */
195
+ toEqual(expected) {
196
+ const actual = getter();
197
+ if (!Object.is(actual, expected)) {
198
+ throw new Error(
199
+ `Expected signal to equal ${JSON.stringify(expected)}, but got ${JSON.stringify(actual)}`
200
+ );
201
+ }
202
+ },
203
+
204
+ /**
205
+ * Assert that the signal's current value is truthy.
206
+ * @throws {Error} if value is falsy
207
+ */
208
+ toBeTruthy() {
209
+ const actual = getter();
210
+ if (!actual) {
211
+ throw new Error(
212
+ `Expected signal to be truthy, but got ${JSON.stringify(actual)}`
213
+ );
214
+ }
215
+ },
216
+
217
+ /**
218
+ * Assert that the signal's current value is falsy.
219
+ * @throws {Error} if value is truthy
220
+ */
221
+ toBeFalsy() {
222
+ const actual = getter();
223
+ if (actual) {
224
+ throw new Error(
225
+ `Expected signal to be falsy, but got ${JSON.stringify(actual)}`
226
+ );
227
+ }
228
+ }
229
+ };
230
+ }
231
+
232
+ // ─── createTestStore ─────────────────────────────────────────
233
+
234
+ /**
235
+ * Create a reactive store with change recording.
236
+ * Every property mutation is captured in `changes` for later assertions.
237
+ *
238
+ * @template {Record<string, any>} T
239
+ * @param {T} init — initial store values
240
+ * @returns {{
241
+ * proxy: T,
242
+ * changes: Array<{ prop: string, prev: any, next: any }>,
243
+ * reset: () => void
244
+ * }}
245
+ */
246
+ export function createTestStore(init) {
247
+ const snapshot = { ...init };
248
+ const store = createStore({ ...init });
249
+
250
+ /** @type {Array<{ prop: string, prev: any, next: any }>} */
251
+ const changes = [];
252
+
253
+ const proxy = new Proxy(store, {
254
+ set(target, prop, value) {
255
+ const prev = target[prop];
256
+ target[prop] = value;
257
+ changes.push({ prop: /** @type {string} */ (prop), prev, next: value });
258
+ return true;
259
+ }
260
+ });
261
+
262
+ return {
263
+ proxy,
264
+ changes,
265
+
266
+ /**
267
+ * Reset all properties to initial values and clear the change log.
268
+ */
269
+ reset() {
270
+ changes.length = 0;
271
+ for (const key of Object.keys(snapshot)) {
272
+ store[key] = snapshot[key];
273
+ }
274
+ }
275
+ };
276
+ }
277
+
278
+ // ─── settled ─────────────────────────────────────────────────
279
+
280
+ /**
281
+ * Wait for all microtasks to settle. Useful after async signal updates.
282
+ *
283
+ * @param {{ timeout?: number }} [options]
284
+ * @returns {Promise<void>}
285
+ */
286
+ export function settled(options = {}) {
287
+ const { timeout } = options;
288
+
289
+ const tick = () => new Promise((resolve) => { queueMicrotask(resolve); });
290
+
291
+ if (timeout == null) {
292
+ // Drain two rounds of microtasks to catch cascading effects
293
+ return tick().then(tick);
294
+ }
295
+
296
+ return new Promise((resolve, reject) => {
297
+ const timer = setTimeout(() => {
298
+ reject(new Error(`settled() timed out after ${timeout}ms`));
299
+ }, timeout);
300
+
301
+ tick().then(tick).then(() => {
302
+ clearTimeout(timer);
303
+ resolve();
304
+ });
305
+ });
306
+ }