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,647 @@
1
+ /**
2
+ * @module devtools
3
+ * Development-only reactive debugging tools for the Decantr framework.
4
+ *
5
+ * All exports are no-ops when `globalThis.__DECANTR_DEVTOOLS__` is not set,
6
+ * ensuring zero production cost. Call `enableDevTools()` to activate.
7
+ *
8
+ * @example
9
+ * import { enableDevTools, inspectSignal, label, snapshot, restore } from './devtools.js';
10
+ * import { createSignal, createEffect } from './index.js';
11
+ *
12
+ * enableDevTools();
13
+ * const [count, setCount] = createSignal(0);
14
+ * label(count, 'count');
15
+ * console.log(inspectSignal(count));
16
+ * // { value: 0, subscriberCount: 0, id: 1, label: 'count' }
17
+ */
18
+
19
+ import { createSignal, createEffect, createMemo } from './index.js';
20
+
21
+ // ─── Internal State ─────────────────────────────────────────────────────────
22
+
23
+ /**
24
+ * Metadata for reactive nodes (signal getters, effect disposers, memo getters).
25
+ * WeakMap keyed on the public-facing function so entries don't prevent GC.
26
+ * @type {WeakMap<Function, { id: number, label: string, type: string }>}
27
+ */
28
+ const _meta = new WeakMap();
29
+
30
+ /**
31
+ * Strong references for iteration (WeakMap is not enumerable).
32
+ * @type {Map<number, { ref: Function, type: string, effectObj?: object }>}
33
+ */
34
+ const _nodes = new Map();
35
+
36
+ /**
37
+ * Reverse lookup: internal effect/memo object -> node id.
38
+ * Used by getReactiveGraph() to resolve subscriber-set members back to nodes.
39
+ * @type {WeakMap<object, number>}
40
+ */
41
+ const _effectToNodeId = new WeakMap();
42
+
43
+ /**
44
+ * Signal getter -> setter, so restore() can write values back.
45
+ * @type {WeakMap<Function, Function>}
46
+ */
47
+ const _setters = new WeakMap();
48
+
49
+ /**
50
+ * Stored snapshots keyed by string id.
51
+ * @type {Map<string, { id: string, timestamp: number, values: Map<string, any> }>}
52
+ */
53
+ const _snapshots = new Map();
54
+
55
+ /** Auto-incrementing node id counter. */
56
+ let _nextId = 1;
57
+
58
+ /**
59
+ * Circular buffer for execution traces.
60
+ * @type {Array<TraceEntry>}
61
+ */
62
+ let _traceBuffer = [];
63
+
64
+ /**
65
+ * @typedef {{ id: number, label: string, type: string, timestamp: number, duration: number, trigger: string|null }} TraceEntry
66
+ */
67
+
68
+ /** Write cursor in the circular buffer. */
69
+ let _traceIndex = 0;
70
+
71
+ /** Total trace entries ever recorded (may exceed buffer capacity). */
72
+ let _traceTotal = 0;
73
+
74
+ /** Maximum trace history entries. */
75
+ let _maxTraceEntries = 1000;
76
+
77
+ /** Maximum stored snapshots. */
78
+ let _maxSnapshots = 50;
79
+
80
+ /**
81
+ * The most recent signal setter that fired, used to attribute effect triggers.
82
+ * @type {{ label: string, id: number }|null}
83
+ */
84
+ let _lastTrigger = null;
85
+
86
+ // ─── Helpers ────────────────────────────────────────────────────────────────
87
+
88
+ /**
89
+ * Check whether devtools are currently active.
90
+ * @returns {boolean}
91
+ */
92
+ function isActive() {
93
+ return !!globalThis.__DECANTR_DEVTOOLS__;
94
+ }
95
+
96
+ /**
97
+ * Register a reactive node in both _meta (WeakMap) and _nodes (Map).
98
+ * @param {Function} target - Public-facing getter or dispose function.
99
+ * @param {string} type - 'signal' | 'effect' | 'memo'
100
+ * @param {string} [name] - Optional human-readable label.
101
+ * @param {object} [effectObj] - Internal effect object (for graph edge resolution).
102
+ * @returns {number} Assigned id.
103
+ */
104
+ function registerNode(target, type, name, effectObj) {
105
+ const id = _nextId++;
106
+ const entry = { id, label: name || `${type}#${id}`, type };
107
+ _meta.set(target, entry);
108
+ const nodeEntry = { ref: target, type };
109
+ if (effectObj) {
110
+ nodeEntry.effectObj = effectObj;
111
+ _effectToNodeId.set(effectObj, id);
112
+ }
113
+ _nodes.set(id, nodeEntry);
114
+ return id;
115
+ }
116
+
117
+ /**
118
+ * Write a trace entry into the circular buffer.
119
+ * @param {number} id
120
+ * @param {string} lbl
121
+ * @param {string} type
122
+ * @param {number} duration - Execution time in ms.
123
+ * @param {string|null} trigger - Label of the triggering signal.
124
+ */
125
+ function recordTrace(id, lbl, type, duration, trigger) {
126
+ /** @type {TraceEntry} */
127
+ const entry = { id, label: lbl, type, timestamp: Date.now(), duration, trigger };
128
+
129
+ if (_traceBuffer.length < _maxTraceEntries) {
130
+ _traceBuffer.push(entry);
131
+ } else {
132
+ _traceBuffer[_traceIndex] = entry;
133
+ }
134
+ _traceIndex = (_traceIndex + 1) % _maxTraceEntries;
135
+ _traceTotal++;
136
+ }
137
+
138
+ /**
139
+ * Read trace entries in chronological order.
140
+ * @param {number} limit - Max entries to return.
141
+ * @returns {TraceEntry[]}
142
+ */
143
+ function readTrace(limit) {
144
+ const len = _traceBuffer.length;
145
+ const count = Math.min(len, limit);
146
+ if (count === 0) return [];
147
+
148
+ if (len < _maxTraceEntries) {
149
+ // Buffer has not wrapped — entries are already chronological
150
+ return _traceBuffer.slice(len - count);
151
+ }
152
+
153
+ // Buffer has wrapped — _traceIndex points to the oldest slot
154
+ const result = new Array(count);
155
+ const start = _traceIndex; // oldest entry position
156
+ const offset = len - count;
157
+ for (let i = 0; i < count; i++) {
158
+ result[i] = _traceBuffer[(start + offset + i) % len];
159
+ }
160
+ return result;
161
+ }
162
+
163
+ // ─── Public API ─────────────────────────────────────────────────────────────
164
+
165
+ /**
166
+ * Activate devtools instrumentation.
167
+ *
168
+ * Sets `globalThis.__DECANTR_DEVTOOLS__ = true` and configures limits.
169
+ * After calling this, use the module's `label()`, `registerSignalSetter()`,
170
+ * and the `*Tracked` factory wrappers to register reactive nodes for
171
+ * inspection, tracing, and snapshotting.
172
+ *
173
+ * Safe to call multiple times — subsequent calls update options only.
174
+ *
175
+ * @param {{ maxTraceEntries?: number, maxSnapshots?: number }} [options]
176
+ * @returns {void}
177
+ */
178
+ export function enableDevTools(options) {
179
+ globalThis.__DECANTR_DEVTOOLS__ = true;
180
+
181
+ if (options) {
182
+ if (typeof options.maxTraceEntries === 'number' && options.maxTraceEntries > 0) {
183
+ _maxTraceEntries = options.maxTraceEntries;
184
+ }
185
+ if (typeof options.maxSnapshots === 'number' && options.maxSnapshots > 0) {
186
+ _maxSnapshots = options.maxSnapshots;
187
+ }
188
+ }
189
+
190
+ // Shrink trace buffer if new limit is smaller
191
+ if (_traceBuffer.length > _maxTraceEntries) {
192
+ _traceBuffer = readTrace(_maxTraceEntries);
193
+ _traceIndex = 0;
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Inspect a signal getter and return its current debug state.
199
+ *
200
+ * Also works on memo getters (they expose `_subscribers` the same way).
201
+ * Returns `null` if devtools are not enabled or the function is not a
202
+ * recognized reactive getter.
203
+ *
204
+ * @param {Function} getter - A signal or memo getter.
205
+ * @returns {{ value: any, subscriberCount: number, id: number, label: string }|null}
206
+ */
207
+ export function inspectSignal(getter) {
208
+ if (!isActive()) return null;
209
+ if (typeof getter !== 'function') return null;
210
+
211
+ const meta = _meta.get(getter);
212
+ const subscribers = getter._subscribers;
213
+ const subscriberCount = subscribers instanceof Set ? subscribers.size : 0;
214
+
215
+ if (meta) {
216
+ return {
217
+ value: getter(),
218
+ subscriberCount,
219
+ id: meta.id,
220
+ label: meta.label
221
+ };
222
+ }
223
+
224
+ // Unregistered signal — still return useful info if it has _subscribers
225
+ if (subscribers instanceof Set) {
226
+ return {
227
+ value: getter(),
228
+ subscriberCount,
229
+ id: -1,
230
+ label: '(unlabeled)'
231
+ };
232
+ }
233
+
234
+ return null;
235
+ }
236
+
237
+ /**
238
+ * Assign a human-readable debug label to a reactive node.
239
+ *
240
+ * Works on signal getters, memo getters, and effect dispose functions.
241
+ * If the node was not previously registered (e.g. created before
242
+ * `enableDevTools()`), it is registered on first `label()` call.
243
+ *
244
+ * @param {Function} target - Signal getter, memo getter, or effect dispose.
245
+ * @param {string} name - Debug label.
246
+ * @returns {void}
247
+ */
248
+ export function label(target, name) {
249
+ if (!isActive()) return;
250
+ if (typeof target !== 'function' || typeof name !== 'string') return;
251
+
252
+ const existing = _meta.get(target);
253
+ if (existing) {
254
+ existing.label = name;
255
+ return;
256
+ }
257
+
258
+ // Infer node type from shape
259
+ let type = 'unknown';
260
+ if (target._subscribers instanceof Set) {
261
+ // Both signals and memos expose _subscribers — distinguish by checking
262
+ // whether there is a setter registered (signals have one, memos don't).
263
+ type = _setters.has(target) ? 'signal' : 'signal';
264
+ // Conservative: default to 'signal' — label() is called before we can
265
+ // reliably distinguish. Callers can use registerSignalSetter to clarify.
266
+ } else if (target.name === 'dispose') {
267
+ type = 'effect';
268
+ }
269
+ registerNode(target, type, name);
270
+ }
271
+
272
+ /**
273
+ * Register a signal's setter so `snapshot()`/`restore()` can capture and
274
+ * write values. Also registers the getter as a 'signal' node if not already
275
+ * tracked.
276
+ *
277
+ * @param {Function} getter - Signal getter from `createSignal`.
278
+ * @param {Function} setter - Signal setter from `createSignal`.
279
+ * @returns {void}
280
+ */
281
+ export function registerSignalSetter(getter, setter) {
282
+ if (!isActive()) return;
283
+ _setters.set(getter, setter);
284
+ // Ensure the getter is registered as a node
285
+ if (!_meta.has(getter)) {
286
+ registerNode(getter, 'signal');
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Build the full reactive dependency graph from all registered nodes.
292
+ *
293
+ * Nodes represent signals, effects, and memos. Edges flow from source
294
+ * (signal/memo) to subscriber (effect/memo).
295
+ *
296
+ * Edge resolution works by iterating each signal/memo's `_subscribers` set
297
+ * and matching each internal effect object back to a registered node via
298
+ * `_effectToNodeId`.
299
+ *
300
+ * @returns {{ nodes: Array<{ id: number, type: string, label: string, value: any, level: number }>, edges: Array<{ from: number, to: number }> }|null}
301
+ */
302
+ export function getReactiveGraph() {
303
+ if (!isActive()) return null;
304
+
305
+ /** @type {Array<{ id: number, type: string, label: string, value: any, level: number }>} */
306
+ const nodes = [];
307
+ /** @type {Array<{ from: number, to: number }>} */
308
+ const edges = [];
309
+
310
+ // Collect all nodes
311
+ for (const [nodeId, entry] of _nodes) {
312
+ const meta = _meta.get(entry.ref);
313
+ if (!meta) continue;
314
+
315
+ let value;
316
+ let level = 0;
317
+
318
+ if (entry.type === 'signal' || entry.type === 'memo') {
319
+ try {
320
+ // Safe to call getter here — no currentEffect is set at module scope
321
+ value = entry.ref();
322
+ } catch (_) {
323
+ value = '<error>';
324
+ }
325
+ }
326
+
327
+ if (entry.effectObj && typeof entry.effectObj.level === 'number') {
328
+ level = entry.effectObj.level;
329
+ }
330
+
331
+ nodes.push({ id: nodeId, type: entry.type, label: meta.label, value, level });
332
+ }
333
+
334
+ // Build edges: for each signal/memo, walk its subscribers and resolve
335
+ for (const [nodeId, entry] of _nodes) {
336
+ if (entry.type !== 'signal' && entry.type !== 'memo') continue;
337
+
338
+ const subs = entry.ref._subscribers;
339
+ if (!(subs instanceof Set)) continue;
340
+
341
+ for (const subscriber of subs) {
342
+ const targetId = _effectToNodeId.get(subscriber);
343
+ if (targetId !== undefined && targetId !== nodeId) {
344
+ edges.push({ from: nodeId, to: targetId });
345
+ }
346
+ }
347
+ }
348
+
349
+ return { nodes, edges };
350
+ }
351
+
352
+ /**
353
+ * Retrieve the execution trace log.
354
+ *
355
+ * Returns an array of trace entries in chronological order, recording what
356
+ * effects and memos ran, when, how long they took, and what signal triggered
357
+ * each execution.
358
+ *
359
+ * @param {{ limit?: number }} [options]
360
+ * @returns {TraceEntry[]|null} Returns `null` if devtools are not enabled.
361
+ */
362
+ export function getTrace(options) {
363
+ if (!isActive()) return null;
364
+
365
+ const limit = (options && typeof options.limit === 'number')
366
+ ? options.limit
367
+ : _traceBuffer.length;
368
+ return readTrace(Math.max(0, limit));
369
+ }
370
+
371
+ /**
372
+ * Capture a snapshot of all labeled signals that have a registered setter.
373
+ *
374
+ * Returns a snapshot object whose `id` can be passed to `restore()` to
375
+ * reinstate the captured values.
376
+ *
377
+ * @returns {{ id: string, timestamp: number, values: Map<string, any> }|null}
378
+ */
379
+ export function snapshot() {
380
+ if (!isActive()) return null;
381
+
382
+ /** @type {Map<string, any>} */
383
+ const values = new Map();
384
+
385
+ for (const [, entry] of _nodes) {
386
+ if (entry.type !== 'signal') continue;
387
+ if (!_setters.has(entry.ref)) continue;
388
+
389
+ const meta = _meta.get(entry.ref);
390
+ if (!meta) continue;
391
+
392
+ try {
393
+ values.set(meta.label, entry.ref());
394
+ } catch (_) {
395
+ // Skip signals that throw on read
396
+ }
397
+ }
398
+
399
+ const id = `snap_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
400
+ const ts = Date.now();
401
+ const snap = { id, timestamp: ts, values };
402
+
403
+ // Enforce max snapshots — evict oldest
404
+ if (_snapshots.size >= _maxSnapshots) {
405
+ const oldest = _snapshots.keys().next().value;
406
+ _snapshots.delete(oldest);
407
+ }
408
+
409
+ _snapshots.set(id, snap);
410
+ return snap;
411
+ }
412
+
413
+ /**
414
+ * Restore a previously captured snapshot by writing all captured signal
415
+ * values back through their registered setters.
416
+ *
417
+ * @param {string} snapshotId - The `id` from a previous `snapshot()` call.
418
+ * @returns {boolean} `true` if at least one signal was restored.
419
+ */
420
+ export function restore(snapshotId) {
421
+ if (!isActive()) return false;
422
+ if (typeof snapshotId !== 'string') return false;
423
+
424
+ const snap = _snapshots.get(snapshotId);
425
+ if (!snap) return false;
426
+
427
+ // Build label -> setter lookup
428
+ /** @type {Map<string, Function>} */
429
+ const labelToSetter = new Map();
430
+ for (const [, entry] of _nodes) {
431
+ if (entry.type !== 'signal') continue;
432
+ const meta = _meta.get(entry.ref);
433
+ if (!meta) continue;
434
+ const setter = _setters.get(entry.ref);
435
+ if (!setter) continue;
436
+ labelToSetter.set(meta.label, setter);
437
+ }
438
+
439
+ let restored = false;
440
+ for (const [lbl, value] of snap.values) {
441
+ const setter = labelToSetter.get(lbl);
442
+ if (setter) {
443
+ setter(value);
444
+ restored = true;
445
+ }
446
+ }
447
+ return restored;
448
+ }
449
+
450
+ /**
451
+ * Detect potential memory leaks in the reactive graph.
452
+ *
453
+ * Scans all registered signal/memo subscriber sets for:
454
+ * 1. **Orphaned effects** — subscribers marked `.disposed` but still present
455
+ * in a subscriber set (should have been cleaned up).
456
+ * 2. **Growing subscribers** — subscriber sets whose size exceeds a threshold,
457
+ * which may indicate effects subscribing in a loop without cleanup.
458
+ *
459
+ * @param {{ subscriberThreshold?: number }} [options]
460
+ * @returns {{ orphanedEffects: number, growingSubscribers: Array<{ label: string, count: number }> }|null}
461
+ */
462
+ export function detectLeaks(options) {
463
+ if (!isActive()) return null;
464
+
465
+ const threshold = (options && typeof options.subscriberThreshold === 'number')
466
+ ? options.subscriberThreshold
467
+ : 10;
468
+
469
+ let orphanedEffects = 0;
470
+ /** @type {Array<{ label: string, count: number }>} */
471
+ const growingSubscribers = [];
472
+
473
+ for (const [, entry] of _nodes) {
474
+ if (entry.type !== 'signal' && entry.type !== 'memo') continue;
475
+
476
+ const subs = entry.ref._subscribers;
477
+ if (!(subs instanceof Set)) continue;
478
+
479
+ const meta = _meta.get(entry.ref);
480
+ const lbl = meta ? meta.label : '(unlabeled)';
481
+
482
+ // Count orphaned subscribers
483
+ for (const subscriber of subs) {
484
+ if (subscriber.disposed) {
485
+ orphanedEffects++;
486
+ }
487
+ }
488
+
489
+ // Flag suspiciously large subscriber sets
490
+ if (subs.size > threshold) {
491
+ growingSubscribers.push({ label: lbl, count: subs.size });
492
+ }
493
+ }
494
+
495
+ return { orphanedEffects, growingSubscribers };
496
+ }
497
+
498
+ // ─── Instrumented Factory Wrappers ──────────────────────────────────────────
499
+ //
500
+ // Drop-in replacements for createSignal / createEffect / createMemo that
501
+ // automatically register nodes, record traces, and store effect object
502
+ // references for graph edge resolution.
503
+ //
504
+ // For nodes created *before* devtools are enabled, use label() +
505
+ // registerSignalSetter() to register them manually.
506
+
507
+ /**
508
+ * Instrumented `createSignal`.
509
+ *
510
+ * Returns the same `[getter, setter]` tuple but registers the signal for
511
+ * inspection and wraps the setter to attribute trigger sources.
512
+ *
513
+ * @template T
514
+ * @param {T} initialValue
515
+ * @param {{ label?: string }} [options]
516
+ * @returns {[() => T, (v: T | ((prev: T) => T)) => void]}
517
+ */
518
+ export function createSignalTracked(initialValue, options) {
519
+ const [getter, setter] = createSignal(initialValue);
520
+ if (!isActive()) return [getter, setter];
521
+
522
+ const name = (options && options.label) ? options.label : undefined;
523
+ registerNode(getter, 'signal', name);
524
+ _setters.set(getter, setter);
525
+
526
+ const meta = _meta.get(getter);
527
+
528
+ /**
529
+ * Wrapped setter that records trigger attribution before delegating.
530
+ * @param {T | ((prev: T) => T)} next
531
+ */
532
+ function trackedSetter(next) {
533
+ _lastTrigger = meta;
534
+ try {
535
+ setter(next);
536
+ } finally {
537
+ _lastTrigger = null;
538
+ }
539
+ }
540
+
541
+ return [getter, trackedSetter];
542
+ }
543
+
544
+ /**
545
+ * Instrumented `createEffect`.
546
+ *
547
+ * Wraps the user function to record execution duration and trigger source
548
+ * into the trace buffer.
549
+ *
550
+ * @param {Function} fn
551
+ * @param {{ label?: string }} [options]
552
+ * @returns {Function} dispose
553
+ */
554
+ export function createEffectTracked(fn, options) {
555
+ if (!isActive()) return createEffect(fn);
556
+
557
+ /** @type {{ id: number, label: string, type: string }|null} */
558
+ let meta = null;
559
+
560
+ // We need the internal effect object for graph resolution. The scheduler
561
+ // creates it inside createEffect — we capture it through the _subscribers
562
+ // sets that get populated when the effect first runs.
563
+ const dispose = createEffect(() => {
564
+ const start = typeof performance !== 'undefined' ? performance.now() : Date.now();
565
+ const result = fn();
566
+ const elapsed = typeof performance !== 'undefined'
567
+ ? performance.now() - start
568
+ : Date.now() - start;
569
+
570
+ if (meta) {
571
+ const triggerLabel = _lastTrigger ? _lastTrigger.label : null;
572
+ recordTrace(meta.id, meta.label, 'effect', elapsed, triggerLabel);
573
+ }
574
+
575
+ return result;
576
+ });
577
+
578
+ const name = (options && options.label) ? options.label : undefined;
579
+ registerNode(dispose, 'effect', name);
580
+ meta = _meta.get(dispose);
581
+
582
+ return dispose;
583
+ }
584
+
585
+ /**
586
+ * Instrumented `createMemo`.
587
+ *
588
+ * Wraps the computation to record execution traces.
589
+ *
590
+ * @template T
591
+ * @param {() => T} fn
592
+ * @param {{ label?: string }} [options]
593
+ * @returns {() => T}
594
+ */
595
+ export function createMemoTracked(fn, options) {
596
+ if (!isActive()) return createMemo(fn);
597
+
598
+ /** @type {{ id: number, label: string, type: string }|null} */
599
+ let meta = null;
600
+
601
+ const getter = createMemo(() => {
602
+ const start = typeof performance !== 'undefined' ? performance.now() : Date.now();
603
+ const result = fn();
604
+ const elapsed = typeof performance !== 'undefined'
605
+ ? performance.now() - start
606
+ : Date.now() - start;
607
+
608
+ if (meta) {
609
+ const triggerLabel = _lastTrigger ? _lastTrigger.label : null;
610
+ recordTrace(meta.id, meta.label, 'memo', elapsed, triggerLabel);
611
+ }
612
+
613
+ return result;
614
+ });
615
+
616
+ const name = (options && options.label) ? options.label : undefined;
617
+ registerNode(getter, 'memo', name);
618
+ meta = _meta.get(getter);
619
+
620
+ return getter;
621
+ }
622
+
623
+ // ─── Reset / Teardown ───────────────────────────────────────────────────────
624
+
625
+ /**
626
+ * Reset all devtools internal state. Intended for test teardown.
627
+ * Does not clear `globalThis.__DECANTR_DEVTOOLS__`.
628
+ * @returns {void}
629
+ */
630
+ export function _reset() {
631
+ _nodes.clear();
632
+ _snapshots.clear();
633
+ _traceBuffer = [];
634
+ _traceIndex = 0;
635
+ _traceTotal = 0;
636
+ _nextId = 1;
637
+ _lastTrigger = null;
638
+ }
639
+
640
+ /**
641
+ * Disable devtools and clear all state.
642
+ * @returns {void}
643
+ */
644
+ export function disableDevTools() {
645
+ globalThis.__DECANTR_DEVTOOLS__ = false;
646
+ _reset();
647
+ }