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,234 @@
1
+ # Server-Side Rendering (SSR)
2
+
3
+ Decantr supports server-side rendering via a dedicated `decantr/ssr` module that works in pure Node.js without DOM globals.
4
+
5
+ ## When to Use SSR
6
+
7
+ - **SEO**: Search engines need HTML content to index pages
8
+ - **Initial load performance**: Users see content before JavaScript loads
9
+ - **Social sharing**: Open Graph scrapers need static HTML
10
+ - **Accessibility**: Content available before JS executes
11
+
12
+ ## Architecture
13
+
14
+ The SSR module is a **separate entry point** that never imports `document` at module level. It provides SSR-safe versions of Decantr's core primitives:
15
+
16
+ | Client | SSR |
17
+ |--------|-----|
18
+ | `h(tag, props, ...children)` → HTMLElement | `ssrH(tag, props, ...children)` → VNode |
19
+ | `text(getter)` → reactive Text node | `ssrText(getter)` → TextVNode (evaluated once) |
20
+ | `cond(pred, trueFn, falseFn)` → reactive branch | `ssrCond(pred, trueFn, falseFn)` → static branch |
21
+ | `list(items, keyFn, renderFn)` → reactive list | `ssrList(items, keyFn, renderFn)` → static list |
22
+ | `css(...atoms)` → class string + DOM injection | `ssrCss(...atoms)` → class string only |
23
+ | `onMount(fn)` → runs after mount | `ssrOnMount(fn)` → no-op |
24
+ | `onDestroy(fn)` → runs on teardown | `ssrOnDestroy(fn)` → no-op |
25
+
26
+ ## Quick Start
27
+
28
+ ### Server
29
+
30
+ ```js
31
+ import { renderToString } from 'decantr/ssr';
32
+ import { ssrH, ssrText, ssrCond, ssrList, ssrCss } from 'decantr/ssr';
33
+ import { createSignal } from 'decantr/state';
34
+
35
+ function App() {
36
+ const [count] = createSignal(0);
37
+ return ssrH('div', { class: ssrCss('_flex _col _gap4 _p6') },
38
+ ssrH('h1', null, 'Hello from SSR'),
39
+ ssrH('p', null, ssrText(() => `Count: ${count()}`))
40
+ );
41
+ }
42
+
43
+ const html = renderToString(() => App());
44
+ // => '<div data-d-id="0" class="_flex _col _gap4 _p6"><h1 data-d-id="1">Hello from SSR</h1>...'
45
+ ```
46
+
47
+ ### Client (Hydration)
48
+
49
+ ```js
50
+ import { hydrate, installHydrationRuntime } from 'decantr/ssr';
51
+ import { createEffect } from 'decantr/state';
52
+ import { pushScope, popScope, drainMountQueue, runDestroyFns } from 'decantr/core';
53
+ import { h, text, cond, list, onMount } from 'decantr/core';
54
+
55
+ // Install runtime once before hydrating
56
+ installHydrationRuntime(
57
+ { createEffect },
58
+ { pushScope, popScope, drainMountQueue, runDestroyFns }
59
+ );
60
+
61
+ // Hydrate — reuses existing DOM, attaches event listeners + signal bindings
62
+ hydrate(document.getElementById('app'), () => App());
63
+ ```
64
+
65
+ ## API Reference
66
+
67
+ ### `renderToString(component)`
68
+
69
+ Renders a component function to an HTML string.
70
+
71
+ - Signals are read once without creating subscriptions
72
+ - Effects are not created
73
+ - `onMount`/`onDestroy` callbacks are ignored
74
+ - Each element gets a `data-d-id` attribute for hydration matching
75
+ - Returns a complete HTML string
76
+
77
+ ```js
78
+ const html = renderToString(() => App());
79
+ res.send(`<!DOCTYPE html><html><body><div id="app">${html}</div></body></html>`);
80
+ ```
81
+
82
+ ### `renderToStream(component)`
83
+
84
+ Same as `renderToString` but returns a `ReadableStream` that yields HTML chunks incrementally. Useful for large pages where you want to start sending HTML before the entire tree is serialized.
85
+
86
+ ```js
87
+ const stream = renderToStream(() => App());
88
+
89
+ // Node.js HTTP response
90
+ const reader = stream.getReader();
91
+ while (true) {
92
+ const { done, value } = await reader.read();
93
+ if (done) break;
94
+ res.write(value);
95
+ }
96
+ res.end();
97
+
98
+ // Or pipe to a Response (Deno, Bun, edge functions)
99
+ return new Response(stream, {
100
+ headers: { 'Content-Type': 'text/html' }
101
+ });
102
+ ```
103
+
104
+ ### `hydrate(root, component)`
105
+
106
+ Walks the existing server-rendered DOM and attaches:
107
+ - Event listeners from component props
108
+ - Signal subscriptions for reactive text/attribute updates
109
+ - Conditional (`cond`) and list (`list`) reactivity
110
+ - `onMount` callbacks
111
+
112
+ The existing DOM is **reused**, not recreated. Matching is done by position (depth-first tree walk), not by `data-d-id`.
113
+
114
+ ```js
115
+ hydrate(document.getElementById('app'), () => App());
116
+ ```
117
+
118
+ ### `installHydrationRuntime(stateMod, lifecycleMod)`
119
+
120
+ Must be called once on the client before `hydrate()`. Provides the reactive runtime:
121
+
122
+ ```js
123
+ installHydrationRuntime(
124
+ { createEffect }, // from decantr/state
125
+ { pushScope, popScope, drainMountQueue, runDestroyFns } // from decantr/core
126
+ );
127
+ ```
128
+
129
+ ### `isSSR()`
130
+
131
+ Returns `true` during `renderToString`/`renderToStream` execution, `false` otherwise. Use this to conditionally skip browser-only code:
132
+
133
+ ```js
134
+ function App() {
135
+ if (!isSSR()) {
136
+ // Browser-only initialization
137
+ window.addEventListener('resize', handleResize);
138
+ }
139
+ return ssrH('div', null, 'content');
140
+ }
141
+ ```
142
+
143
+ ## Signal State During SSR
144
+
145
+ During SSR, signals are **evaluated once** without creating reactive subscriptions:
146
+
147
+ ```js
148
+ const [count, setCount] = createSignal(0);
149
+
150
+ renderToString(() => {
151
+ // count() reads the current value (0) but does NOT subscribe
152
+ return ssrH('span', null, ssrText(() => count()));
153
+ });
154
+
155
+ setCount(5);
156
+ // The rendered HTML still shows "0" — no reactive updates during SSR
157
+ ```
158
+
159
+ This is by design: SSR produces a static snapshot of your UI. Reactivity only activates after hydration on the client.
160
+
161
+ ## Limitations
162
+
163
+ 1. **No browser APIs during SSR**: `window`, `document`, `localStorage`, `fetch` (unless polyfilled), `requestAnimationFrame` are not available
164
+ 2. **No effects**: `createEffect` callbacks are not executed during SSR
165
+ 3. **No lifecycle**: `onMount`/`onDestroy` are no-ops during SSR
166
+ 4. **No DOM manipulation**: Components must use `ssrH`/`ssrText`/`ssrCond`/`ssrList` instead of `h`/`text`/`cond`/`list`
167
+ 5. **No Portals**: `Portal` components are not supported in SSR
168
+ 6. **Static routing**: The router must be configured to resolve the correct page for the request URL before calling `renderToString`
169
+ 7. **CSS is not injected**: The atomic CSS runtime does not inject styles during SSR. Include your CSS in the HTML shell or use build-time extraction
170
+
171
+ ## Hydration Mismatch
172
+
173
+ If the SSR HTML doesn't match the client-side render, hydration will still work but may produce visual glitches. Common causes:
174
+
175
+ - Different signal values between server and client
176
+ - Browser-only branches (`if (typeof window !== 'undefined')`)
177
+ - Date/time-dependent content
178
+ - Random values
179
+
180
+ Guard against mismatches by ensuring the component produces identical output for the same inputs on both server and client.
181
+
182
+ ## Integration Example: Express
183
+
184
+ ```js
185
+ import express from 'express';
186
+ import { renderToString, ssrH, ssrText, ssrCss } from 'decantr/ssr';
187
+ import { createSignal } from 'decantr/state';
188
+
189
+ const app = express();
190
+
191
+ app.get('/', (req, res) => {
192
+ const html = renderToString(() =>
193
+ ssrH('div', { id: 'app', class: ssrCss('_flex _col _gap4') },
194
+ ssrH('h1', null, 'Server-Rendered Decantr App'),
195
+ ssrH('p', null, 'Hydration will activate interactivity.')
196
+ )
197
+ );
198
+
199
+ res.send(`<!DOCTYPE html>
200
+ <html>
201
+ <head>
202
+ <meta charset="utf-8">
203
+ <title>My App</title>
204
+ <link rel="stylesheet" href="/styles.css">
205
+ </head>
206
+ <body>
207
+ <div id="app">${html}</div>
208
+ <script type="module" src="/client.js"></script>
209
+ </body>
210
+ </html>`);
211
+ });
212
+
213
+ app.listen(3000);
214
+ ```
215
+
216
+ ## Integration Example: Hono (Edge)
217
+
218
+ ```js
219
+ import { Hono } from 'hono';
220
+ import { renderToStream, ssrH } from 'decantr/ssr';
221
+
222
+ const app = new Hono();
223
+
224
+ app.get('/', (c) => {
225
+ const stream = renderToStream(() =>
226
+ ssrH('div', { id: 'app' }, ssrH('h1', null, 'Edge SSR'))
227
+ );
228
+ return new Response(stream, {
229
+ headers: { 'Content-Type': 'text/html' }
230
+ });
231
+ });
232
+
233
+ export default app;
234
+ ```
@@ -0,0 +1,215 @@
1
+ # Data Layer Reference
2
+
3
+ `import { createQuery, createInfiniteQuery, createMutation, queryClient, createEntityStore, createURLSignal, createURLStore, parsers, createWebSocket, createEventSource, createPersisted, createIndexedDB, createCrossTab, createOfflineQueue, createWorkerSignal, createWorkerQuery } from 'decantr/data';`
4
+
5
+ ## createQuery(key, fetcher, options?)
6
+
7
+ Server state management with caching, deduplication, and stale-while-revalidate. Replaces `createResource`.
8
+
9
+ ```js
10
+ const users = createQuery('users', ({ signal }) =>
11
+ fetch('/api/users', { signal }).then(r => r.json())
12
+ );
13
+ // users.data(), users.isLoading(), users.error()
14
+ // users.refetch(), users.setData([])
15
+ ```
16
+
17
+ **Dynamic key** — re-fetches when key changes (previous request cancelled via AbortController):
18
+ ```js
19
+ const user = createQuery(() => `user-${id()}`, ({ key, signal }) =>
20
+ fetch(`/api/users/${id()}`, { signal }).then(r => r.json())
21
+ );
22
+ ```
23
+
24
+ | Option | Type | Default | Description |
25
+ |--------|------|---------|-------------|
26
+ | `staleTime` | `number` | `0` | ms before cached data is stale |
27
+ | `cacheTime` | `number` | `300000` | ms to keep inactive cache (5 min) |
28
+ | `retry` | `number` | `3` | Retry count with exponential backoff |
29
+ | `refetchInterval` | `number` | — | Auto background refetch ms |
30
+ | `refetchOnWindowFocus` | `boolean` | `true` | Refetch on tab focus |
31
+ | `enabled` | `() => boolean` | — | Signal getter; false = idle |
32
+ | `select` | `(data) => T` | — | Transform raw data |
33
+ | `initialData` | `T` | — | Initial data value |
34
+ | `placeholderData` | `T` | — | Placeholder while loading |
35
+
36
+ | Return | Type | Description |
37
+ |--------|------|-------------|
38
+ | `data` | `() => T \| undefined` | Signal getter |
39
+ | `status` | `() => string` | 'idle' \| 'loading' \| 'success' \| 'error' |
40
+ | `error` | `() => Error \| null` | Signal getter |
41
+ | `isLoading` | `() => boolean` | True during initial load |
42
+ | `isStale` | `() => boolean` | True when cached data is stale |
43
+ | `isFetching` | `() => boolean` | True during any fetch (including background) |
44
+ | `refetch` | `() => Promise` | Manually trigger refetch |
45
+ | `setData` | `(value: T) => void` | Manually set data |
46
+
47
+ ## createInfiniteQuery(key, fetcher, options?)
48
+
49
+ Cursor-based or offset pagination.
50
+
51
+ ```js
52
+ const posts = createInfiniteQuery('posts',
53
+ ({ pageParam = 0, signal }) => fetch(`/api/posts?offset=${pageParam}`, { signal }).then(r => r.json()),
54
+ { getNextPageParam: (lastPage) => lastPage.nextCursor }
55
+ );
56
+ // posts.pages(), posts.allItems(), posts.hasNextPage(), posts.fetchNextPage()
57
+ ```
58
+
59
+ ## createMutation(mutationFn, options?)
60
+
61
+ Mutations with optimistic updates and rollback.
62
+
63
+ ```js
64
+ const addUser = createMutation(
65
+ (user) => fetch('/api/users', { method: 'POST', body: JSON.stringify(user) }).then(r => r.json()),
66
+ {
67
+ onMutate: (user) => {
68
+ const prev = queryClient.getCache('users');
69
+ queryClient.setCache('users', [...(prev || []), user]);
70
+ return { prev }; // rollback context
71
+ },
72
+ onError: (err, user, ctx) => queryClient.setCache('users', ctx.prev),
73
+ onSuccess: () => queryClient.invalidate('users')
74
+ }
75
+ );
76
+ addUser.mutate({ name: 'Alice' });
77
+ ```
78
+
79
+ ## queryClient
80
+
81
+ Singleton cache manager.
82
+
83
+ | Method | Description |
84
+ |--------|-------------|
85
+ | `invalidate(keyPrefix)` | Mark matching queries stale, refetch active ones |
86
+ | `prefetch(key, fetcher)` | Warm cache without subscribing |
87
+ | `setCache(key, data)` | Manual cache write |
88
+ | `getCache(key)` | Read cache |
89
+ | `clear()` | Reset everything |
90
+
91
+ ## createEntityStore(options)
92
+
93
+ Normalized collections with per-entity reactivity.
94
+
95
+ ```js
96
+ const users = createEntityStore({ getId: u => u.id });
97
+ users.addMany([{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]);
98
+ users.get(1)(); // { id: 1, name: 'Alice' } — per-entity signal
99
+ users.all(); // full array — collection signal
100
+ users.filter(u => u.name.startsWith('A'))(); // derived view
101
+ ```
102
+
103
+ | Method | Description |
104
+ |--------|-------------|
105
+ | `addMany(entities)` | Batch add |
106
+ | `upsert(entity)` | Add or update |
107
+ | `update(id, partial)` | Partial merge |
108
+ | `remove(id)` | Remove |
109
+ | `clear()` | Remove all |
110
+ | `get(id)` | Per-entity signal getter (lazy, only fires for that entity) |
111
+ | `all()` | Signal: all entities as array |
112
+ | `count()` | Signal: count |
113
+ | `filter(pred)` | Derived filtered memo |
114
+ | `sorted(cmp)` | Derived sorted memo |
115
+ | `paginated({page, size})` | Derived paginated memo |
116
+
117
+ ## createURLSignal(key, parser, options?)
118
+
119
+ Signal backed by URL search params. Routing-mode aware (hash/history).
120
+
121
+ ```js
122
+ const [filter, setFilter] = createURLSignal('filter', parsers.string, { defaultValue: '' });
123
+ const [page, setPage] = createURLSignal('page', parsers.integer, { defaultValue: 1 });
124
+ ```
125
+
126
+ ## createURLStore(schema)
127
+
128
+ Multiple URL params as reactive store.
129
+
130
+ ```js
131
+ const url = createURLStore({
132
+ page: { parser: parsers.integer, defaultValue: 1 },
133
+ sort: { parser: parsers.string, defaultValue: 'name' }
134
+ });
135
+ url.page(); // 1
136
+ url.setPage(2); // updates URL
137
+ url.values(); // { page: 2, sort: 'name' }
138
+ url.reset(); // reset all to defaults
139
+ ```
140
+
141
+ ## Built-in parsers
142
+
143
+ `parsers.string`, `parsers.integer`, `parsers.float`, `parsers.boolean`, `parsers.json`, `parsers.date`, `parsers.enum(['a','b','c'])`
144
+
145
+ ## createWebSocket(url, options?)
146
+
147
+ WebSocket with auto-reconnect, exponential backoff, and send buffering.
148
+
149
+ ```js
150
+ const ws = createWebSocket('wss://api.example.com/ws', { parse: JSON.parse });
151
+ ws.on(msg => users.upsert(msg.payload));
152
+ ws.send({ type: 'subscribe', channel: 'updates' });
153
+ // ws.status(), ws.lastMessage(), ws.close()
154
+ ```
155
+
156
+ ## createEventSource(url, options?)
157
+
158
+ Server-Sent Events with typed event listeners.
159
+
160
+ ```js
161
+ const sse = createEventSource('/api/events', { events: ['update', 'delete'] });
162
+ sse.on('update', data => { ... });
163
+ ```
164
+
165
+ ## createPersisted(key, init, options?)
166
+
167
+ Signal backed by localStorage/sessionStorage with cross-tab sync via `storage` event.
168
+
169
+ ```js
170
+ const [theme, setTheme] = createPersisted('app-theme', 'light');
171
+ ```
172
+
173
+ ## createIndexedDB(dbName, storeName)
174
+
175
+ Reactive IndexedDB binding. All methods return Promises.
176
+
177
+ ```js
178
+ const db = createIndexedDB('myapp', 'items');
179
+ await db.set('key1', { name: 'Alice' });
180
+ const item = await db.get('key1');
181
+ ```
182
+
183
+ ## createCrossTab(channel, signal)
184
+
185
+ BroadcastChannel sync for a signal across browser tabs.
186
+
187
+ ```js
188
+ const cleanup = createCrossTab('counter', [count, setCount]);
189
+ ```
190
+
191
+ ## createOfflineQueue(options)
192
+
193
+ Queue operations while offline, auto-flush on reconnect.
194
+
195
+ ```js
196
+ const queue = createOfflineQueue({
197
+ process: async (item) => await fetch(item.url, item.options),
198
+ persist: true
199
+ });
200
+ queue.add({ url: '/api/sync', options: { method: 'POST', body: '...' } });
201
+ ```
202
+
203
+ ## createWorkerSignal(worker) / createWorkerQuery(worker, input)
204
+
205
+ Web Worker integration.
206
+
207
+ ```js
208
+ const ws = createWorkerSignal(new Worker('./compute.js'));
209
+ ws.send({ type: 'process', data: largeArray });
210
+ // ws.result(), ws.busy(), ws.error()
211
+ ```
212
+
213
+ ---
214
+
215
+ **See also:** `reference/state.md`, `reference/state-patterns.md`
@@ -0,0 +1,166 @@
1
+ # State Patterns — LLM-Optimized Recipes
2
+
3
+ Common patterns for combining Decantr state primitives. Each recipe is a complete, copy-pasteable solution.
4
+
5
+ ## Server Data + UI State
6
+
7
+ ```js
8
+ import { createQuery } from 'decantr/data';
9
+ import { createSignal } from 'decantr/state';
10
+
11
+ const [filter, setFilter] = createSignal('');
12
+ const users = createQuery(
13
+ () => `users-${filter()}`,
14
+ ({ signal }) => fetch(`/api/users?q=${filter()}`, { signal }).then(r => r.json())
15
+ );
16
+ // users.data(), users.isLoading(), users.error()
17
+ ```
18
+
19
+ ## Entity CRUD + Optimistic Updates
20
+
21
+ ```js
22
+ import { createEntityStore, createMutation, queryClient } from 'decantr/data';
23
+
24
+ const users = createEntityStore({ getId: u => u.id });
25
+ const addUser = createMutation(
26
+ (user) => fetch('/api/users', { method: 'POST', body: JSON.stringify(user) }).then(r => r.json()),
27
+ {
28
+ onMutate: (user) => { users.upsert({ ...user, id: Date.now() }); },
29
+ onSuccess: (data) => { users.upsert(data); },
30
+ onError: () => { queryClient.invalidate('users'); }
31
+ }
32
+ );
33
+ ```
34
+
35
+ ## Real-Time Feed
36
+
37
+ ```js
38
+ import { createWebSocket, createEntityStore } from 'decantr/data';
39
+
40
+ const messages = createEntityStore({ getId: m => m.id });
41
+ const ws = createWebSocket('wss://api.example.com/ws', { parse: JSON.parse });
42
+ ws.on(msg => {
43
+ if (msg.type === 'add') messages.upsert(msg.payload);
44
+ if (msg.type === 'remove') messages.remove(msg.payload.id);
45
+ });
46
+ ```
47
+
48
+ ## Offline Form
49
+
50
+ ```js
51
+ import { createOfflineQueue, createPersisted } from 'decantr/data';
52
+
53
+ const [draft, setDraft] = createPersisted('form-draft', { name: '', email: '' });
54
+ const queue = createOfflineQueue({
55
+ process: async (item) => fetch('/api/submit', { method: 'POST', body: JSON.stringify(item) }),
56
+ persist: true
57
+ });
58
+ // On submit: queue.add(draft()); setDraft({ name: '', email: '' });
59
+ ```
60
+
61
+ ## URL-Driven Filters + Query
62
+
63
+ ```js
64
+ import { createURLStore, parsers, createQuery } from 'decantr/data';
65
+
66
+ const url = createURLStore({
67
+ page: { parser: parsers.integer, defaultValue: 1 },
68
+ sort: { parser: parsers.string, defaultValue: 'name' },
69
+ filter: { parser: parsers.string, defaultValue: '' }
70
+ });
71
+
72
+ const results = createQuery(
73
+ () => `items-${url.page()}-${url.sort()}-${url.filter()}`,
74
+ ({ signal }) => fetch(`/api/items?page=${url.page()}&sort=${url.sort()}&q=${url.filter()}`, { signal }).then(r => r.json())
75
+ );
76
+ ```
77
+
78
+ ## Undo/Redo Editor
79
+
80
+ ```js
81
+ import { createSignal, createHistory } from 'decantr/state';
82
+
83
+ const [content, setContent] = createSignal('');
84
+ const { undo, redo, canUndo, canRedo } = createHistory([content, setContent], { maxLength: 50 });
85
+ ```
86
+
87
+ Or with middleware:
88
+ ```js
89
+ import { withMiddleware } from 'decantr/state/middleware';
90
+ import { undoMiddleware } from 'decantr/state/middleware';
91
+
92
+ const mw = undoMiddleware({ maxLength: 50 });
93
+ const [content, setContent] = withMiddleware(createSignal(''), [mw.middleware]);
94
+ // mw.undo(), mw.redo(), mw.canUndo(), mw.canRedo()
95
+ ```
96
+
97
+ ## Cross-Tab Sync
98
+
99
+ ```js
100
+ import { createPersisted, createCrossTab } from 'decantr/data';
101
+
102
+ const [theme, setTheme] = createPersisted('theme', 'light');
103
+ const cleanup = createCrossTab('theme-sync', [theme, setTheme]);
104
+ // Changes in one tab propagate to all others
105
+ ```
106
+
107
+ ## Large List Performance
108
+
109
+ ```js
110
+ import { createSignal } from 'decantr/state';
111
+ import { mapArray, createProjection } from 'decantr/state/arrays';
112
+
113
+ const [items, setItems] = createSignal(largeArray);
114
+ const rendered = mapArray(items, (item) => renderRow(item));
115
+ // Only added/removed items trigger scope creation — stable per-item references
116
+ ```
117
+
118
+ ## Worker Computation
119
+
120
+ ```js
121
+ import { createWorkerQuery } from 'decantr/data';
122
+ import { createSignal } from 'decantr/state';
123
+
124
+ const [data, setData] = createSignal([]);
125
+ const result = createWorkerQuery(new Worker('./analyze.js'), data);
126
+ // result.data() — computed result from worker
127
+ // result.loading() — true while worker is processing
128
+ ```
129
+
130
+ ## Deep Reactive Store
131
+
132
+ ```js
133
+ import { createDeepStore, produce, reconcile } from 'decantr/state/store';
134
+
135
+ const store = createDeepStore({
136
+ user: { name: 'Alice', settings: { theme: 'dark' } },
137
+ items: []
138
+ });
139
+
140
+ // Nested writes are reactive
141
+ store.user.settings.theme = 'light';
142
+
143
+ // Immer-style mutations
144
+ produce(store, draft => {
145
+ draft.items.push({ id: 1, name: 'New' });
146
+ draft.user.name = 'Bob';
147
+ });
148
+
149
+ // Efficient bulk update
150
+ reconcile(store, await fetch('/api/state').then(r => r.json()));
151
+ ```
152
+
153
+ ## Explicit Dependency Control
154
+
155
+ ```js
156
+ import { createSignal, on } from 'decantr/state';
157
+
158
+ const [search, setSearch] = createSignal('');
159
+ const [debounced, setDebounced] = createSignal('');
160
+
161
+ // Only re-runs when search changes, not when debounced changes
162
+ on(search, (value) => {
163
+ clearTimeout(timer);
164
+ timer = setTimeout(() => setDebounced(value), 300);
165
+ });
166
+ ```