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,922 @@
1
+ /**
2
+ * Decantr SSR — Server-Side Rendering + Hydration
3
+ *
4
+ * Separate entry point that works in Node.js without DOM globals.
5
+ * renderToString/renderToStream build a VNode tree and serialize to HTML.
6
+ * hydrate() walks existing DOM and attaches signal bindings + event listeners.
7
+ *
8
+ * IMPORTANT: This module must NOT import `document` at module level.
9
+ * renderToString and renderToStream work in pure Node.js.
10
+ * hydrate() requires a browser environment (it operates on existing DOM).
11
+ */
12
+
13
+ import { resolveAtomDecl, ALIASES } from '../css/atoms.js';
14
+
15
+ // ─── SSR Atom Resolution (no DOM injection) ────────────────────
16
+
17
+ /**
18
+ * Resolve atom classes for SSR without injecting CSS into DOM.
19
+ * Mirrors the logic of css() from ../css/index.js but skips inject().
20
+ * @param {...string} classes
21
+ * @returns {string}
22
+ */
23
+ function ssrCss(...classes) {
24
+ const result = [];
25
+ for (let i = 0; i < classes.length; i++) {
26
+ const cls = classes[i];
27
+ if (!cls) continue;
28
+ const parts = cls.split(/\s+/);
29
+ for (const part of parts) {
30
+ if (!part) continue;
31
+ if (part === '_group') { result.push('d-group'); continue; }
32
+ if (part === '_peer') { result.push('d-peer'); continue; }
33
+ // For SSR we pass through all class names — CSS is extracted at build time
34
+ // or injected by the client-side runtime during hydration.
35
+ result.push(part);
36
+ }
37
+ }
38
+ return result.join(' ');
39
+ }
40
+
41
+ // ─── HTML Escaping ──────────────────────────────────────────────
42
+
43
+ /**
44
+ * Escape HTML special characters in text content.
45
+ * @param {string} str
46
+ * @returns {string}
47
+ */
48
+ function escapeHTML(str) {
49
+ if (typeof str !== 'string') return '';
50
+ return str
51
+ .replace(/&/g, '&amp;')
52
+ .replace(/</g, '&lt;')
53
+ .replace(/>/g, '&gt;');
54
+ }
55
+
56
+ /**
57
+ * Escape HTML special characters in attribute values.
58
+ * @param {string} str
59
+ * @returns {string}
60
+ */
61
+ function escapeAttr(str) {
62
+ if (typeof str !== 'string') return '';
63
+ return str
64
+ .replace(/&/g, '&amp;')
65
+ .replace(/"/g, '&quot;')
66
+ .replace(/</g, '&lt;')
67
+ .replace(/>/g, '&gt;');
68
+ }
69
+
70
+ // ─── VNode Types ────────────────────────────────────────────────
71
+
72
+ /**
73
+ * @typedef {{ tag: string, props: Object|null, children: Array<VNode|TextVNode|string>, _id: number }} VNode
74
+ * @typedef {{ text: string, _id: number }} TextVNode
75
+ */
76
+
77
+ /** HTML void elements — self-closing, no children */
78
+ const VOID_ELEMENTS = new Set([
79
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
80
+ 'link', 'meta', 'param', 'source', 'track', 'wbr',
81
+ ]);
82
+
83
+ /** Boolean HTML attributes — rendered as `attr` not `attr="value"` */
84
+ const BOOLEAN_ATTRS = new Set([
85
+ 'allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked',
86
+ 'controls', 'default', 'defer', 'disabled', 'formnovalidate',
87
+ 'hidden', 'inert', 'ismap', 'itemscope', 'loop', 'multiple',
88
+ 'muted', 'nomodule', 'novalidate', 'open', 'playsinline',
89
+ 'readonly', 'required', 'reversed', 'selected',
90
+ ]);
91
+
92
+ // ─── SSR Context ────────────────────────────────────────────────
93
+
94
+ /** Global VNode ID counter — reset per render call */
95
+ let _nextId = 0;
96
+
97
+ /** Whether we are currently inside an SSR render pass */
98
+ let _ssrActive = false;
99
+
100
+ /**
101
+ * Evaluate a value without triggering signal subscriptions.
102
+ * In SSR mode signals are evaluated eagerly without tracking.
103
+ * @template T
104
+ * @param {() => T} fn
105
+ * @returns {T}
106
+ */
107
+ function ssrUntrack(fn) {
108
+ // In SSR we don't have reactive tracking, so just call the function
109
+ return fn();
110
+ }
111
+
112
+ // ─── SSR Primitives ─────────────────────────────────────────────
113
+
114
+ /**
115
+ * SSR version of h() — creates a VNode, not a DOM element.
116
+ * Event handlers (on*) are stored but not serialized to HTML.
117
+ * Functions in props are eagerly evaluated with ssrUntrack().
118
+ * @param {string} tag
119
+ * @param {Object|null} props
120
+ * @param {...*} children
121
+ * @returns {VNode}
122
+ */
123
+ function ssrH(tag, props, ...children) {
124
+ const id = _nextId++;
125
+ const resolvedProps = {};
126
+ const handlers = {};
127
+
128
+ if (props) {
129
+ for (const key in props) {
130
+ const val = props[key];
131
+
132
+ // Event handlers — store separately, do not serialize
133
+ if (key.startsWith('on') && typeof val === 'function') {
134
+ handlers[key] = val;
135
+ continue;
136
+ }
137
+
138
+ // ref callbacks — skip in SSR
139
+ if (key === 'ref') continue;
140
+
141
+ // Reactive props — evaluate eagerly
142
+ if (typeof val === 'function') {
143
+ const evaluated = ssrUntrack(val);
144
+ if (key === 'class' || key === 'className') {
145
+ resolvedProps['class'] = evaluated;
146
+ } else if (key === 'style' && typeof evaluated === 'object') {
147
+ resolvedProps['style'] = styleObjToString(evaluated);
148
+ } else if (evaluated !== false && evaluated != null) {
149
+ resolvedProps[key] = evaluated === true ? true : String(evaluated);
150
+ }
151
+ } else if (key === 'class' || key === 'className') {
152
+ resolvedProps['class'] = val;
153
+ } else if (key === 'style' && typeof val === 'object') {
154
+ resolvedProps['style'] = styleObjToString(val);
155
+ } else if (val !== false && val != null) {
156
+ resolvedProps[key] = val === true ? true : String(val);
157
+ }
158
+ }
159
+ }
160
+
161
+ // Flatten children
162
+ const flatChildren = flattenChildren(children);
163
+
164
+ return {
165
+ tag,
166
+ props: resolvedProps,
167
+ children: flatChildren,
168
+ _handlers: handlers,
169
+ _id: id,
170
+ };
171
+ }
172
+
173
+ /**
174
+ * SSR version of text() — evaluates getter once, returns TextVNode.
175
+ * @param {Function} getter
176
+ * @returns {TextVNode}
177
+ */
178
+ function ssrText(getter) {
179
+ const id = _nextId++;
180
+ const value = ssrUntrack(getter);
181
+ return { text: String(value), _id: id, _reactive: true };
182
+ }
183
+
184
+ /**
185
+ * SSR version of cond() — evaluates predicate once, returns the active branch.
186
+ * Wraps result in a d-cond VNode for hydration matching.
187
+ * @param {Function} condition
188
+ * @param {Function} thenFn
189
+ * @param {Function} [elseFn]
190
+ * @returns {VNode}
191
+ */
192
+ function ssrCond(condition, thenFn, elseFn) {
193
+ const id = _nextId++;
194
+ const result = ssrUntrack(condition);
195
+ const fn = result ? thenFn : elseFn;
196
+ const child = fn ? fn() : null;
197
+ const children = child != null ? [normalizeVNode(child)] : [];
198
+
199
+ return {
200
+ tag: 'd-cond',
201
+ props: {},
202
+ children,
203
+ _handlers: {},
204
+ _id: id,
205
+ };
206
+ }
207
+
208
+ /**
209
+ * SSR version of list() — evaluates items once, maps through renderFn.
210
+ * Wraps result in a d-list VNode for hydration matching.
211
+ * @param {Function} itemsGetter
212
+ * @param {Function} keyFn
213
+ * @param {Function} renderFn
214
+ * @returns {VNode}
215
+ */
216
+ function ssrList(itemsGetter, keyFn, renderFn) {
217
+ const id = _nextId++;
218
+ const items = ssrUntrack(itemsGetter);
219
+ const children = [];
220
+
221
+ if (Array.isArray(items)) {
222
+ for (let i = 0; i < items.length; i++) {
223
+ const child = renderFn(items[i], i);
224
+ if (child != null) {
225
+ children.push(normalizeVNode(child));
226
+ }
227
+ }
228
+ }
229
+
230
+ return {
231
+ tag: 'd-list',
232
+ props: {},
233
+ children,
234
+ _handlers: {},
235
+ _id: id,
236
+ };
237
+ }
238
+
239
+ /**
240
+ * SSR-safe onMount — no-op during SSR (mount callbacks don't run on server).
241
+ * @param {Function} _fn
242
+ */
243
+ function ssrOnMount(_fn) {
244
+ // No-op in SSR — mount callbacks run only on client
245
+ }
246
+
247
+ /**
248
+ * SSR-safe onDestroy — no-op during SSR.
249
+ * @param {Function} _fn
250
+ */
251
+ function ssrOnDestroy(_fn) {
252
+ // No-op in SSR — destroy callbacks run only on client
253
+ }
254
+
255
+ // ─── VNode Helpers ──────────────────────────────────────────────
256
+
257
+ /**
258
+ * Normalize a value into a VNode or TextVNode.
259
+ * @param {*} value
260
+ * @returns {VNode|TextVNode|null}
261
+ */
262
+ function normalizeVNode(value) {
263
+ if (value == null || value === false) return null;
264
+ if (typeof value === 'object' && (value.tag || value.text !== undefined)) return value;
265
+ if (typeof value === 'function') {
266
+ // Reactive text — evaluate eagerly
267
+ const text = String(ssrUntrack(value));
268
+ return { text, _id: _nextId++, _reactive: true };
269
+ }
270
+ return { text: String(value), _id: _nextId++ };
271
+ }
272
+
273
+ /**
274
+ * Flatten nested arrays and normalize children to VNodes.
275
+ * @param {Array} children
276
+ * @returns {Array<VNode|TextVNode>}
277
+ */
278
+ function flattenChildren(children) {
279
+ const result = [];
280
+ for (let i = 0; i < children.length; i++) {
281
+ const child = children[i];
282
+ if (child == null || child === false) continue;
283
+ if (Array.isArray(child)) {
284
+ const flat = flattenChildren(child);
285
+ for (let j = 0; j < flat.length; j++) result.push(flat[j]);
286
+ } else {
287
+ const normalized = normalizeVNode(child);
288
+ if (normalized) result.push(normalized);
289
+ }
290
+ }
291
+ return result;
292
+ }
293
+
294
+ /**
295
+ * Convert a style object to a CSS string.
296
+ * @param {Object} obj
297
+ * @returns {string}
298
+ */
299
+ function styleObjToString(obj) {
300
+ if (!obj || typeof obj !== 'object') return '';
301
+ const parts = [];
302
+ for (const key in obj) {
303
+ const value = obj[key];
304
+ if (value == null) continue;
305
+ // Convert camelCase to kebab-case
306
+ const cssKey = key.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
307
+ parts.push(`${cssKey}:${value}`);
308
+ }
309
+ return parts.join(';');
310
+ }
311
+
312
+ // ─── VNode Serialization ────────────────────────────────────────
313
+
314
+ /**
315
+ * Serialize a VNode tree to an HTML string.
316
+ * @param {VNode|TextVNode|null} node
317
+ * @returns {string}
318
+ */
319
+ function serializeVNode(node) {
320
+ if (!node) return '';
321
+
322
+ // Text node
323
+ if (node.text !== undefined) {
324
+ return escapeHTML(node.text);
325
+ }
326
+
327
+ const { tag, props, children, _id } = node;
328
+
329
+ // Build opening tag
330
+ let html = '<' + tag;
331
+
332
+ // Add hydration marker
333
+ html += ` data-d-id="${_id}"`;
334
+
335
+ // Serialize attributes
336
+ if (props) {
337
+ for (const key in props) {
338
+ const val = props[key];
339
+ if (val == null || val === false) continue;
340
+
341
+ if (BOOLEAN_ATTRS.has(key)) {
342
+ if (val) html += ' ' + key;
343
+ } else {
344
+ html += ` ${key}="${escapeAttr(String(val))}"`;
345
+ }
346
+ }
347
+ }
348
+
349
+ // Void element — self-closing
350
+ if (VOID_ELEMENTS.has(tag)) {
351
+ return html + '>';
352
+ }
353
+
354
+ html += '>';
355
+
356
+ // Serialize children
357
+ for (let i = 0; i < children.length; i++) {
358
+ html += serializeVNode(children[i]);
359
+ }
360
+
361
+ html += '</' + tag + '>';
362
+ return html;
363
+ }
364
+
365
+ /**
366
+ * Yield VNode chunks for streaming.
367
+ * @param {VNode|TextVNode|null} node
368
+ * @param {function(string): void} push — called for each chunk
369
+ */
370
+ function streamVNode(node, push) {
371
+ if (!node) return;
372
+
373
+ // Text node
374
+ if (node.text !== undefined) {
375
+ push(escapeHTML(node.text));
376
+ return;
377
+ }
378
+
379
+ const { tag, props, children, _id } = node;
380
+
381
+ // Build opening tag
382
+ let html = '<' + tag;
383
+ html += ` data-d-id="${_id}"`;
384
+
385
+ if (props) {
386
+ for (const key in props) {
387
+ const val = props[key];
388
+ if (val == null || val === false) continue;
389
+ if (BOOLEAN_ATTRS.has(key)) {
390
+ if (val) html += ' ' + key;
391
+ } else {
392
+ html += ` ${key}="${escapeAttr(String(val))}"`;
393
+ }
394
+ }
395
+ }
396
+
397
+ if (VOID_ELEMENTS.has(tag)) {
398
+ push(html + '>');
399
+ return;
400
+ }
401
+
402
+ push(html + '>');
403
+
404
+ // Stream children
405
+ for (let i = 0; i < children.length; i++) {
406
+ streamVNode(children[i], push);
407
+ }
408
+
409
+ push('</' + tag + '>');
410
+ }
411
+
412
+ // ─── SSR Execution Context ──────────────────────────────────────
413
+
414
+ /**
415
+ * Execute a function with SSR versions of h, text, cond, list, onMount, onDestroy.
416
+ * Uses import indirection: components call the SSR versions when _ssrActive is true.
417
+ *
418
+ * @template T
419
+ * @param {Function} componentFn — () => VNode tree
420
+ * @returns {VNode|TextVNode|null}
421
+ */
422
+ function runInSSRContext(componentFn) {
423
+ _nextId = 0;
424
+ _ssrActive = true;
425
+
426
+ // Store original globals if they exist
427
+ const prevH = globalThis.__d_ssr_h;
428
+ const prevText = globalThis.__d_ssr_text;
429
+ const prevCond = globalThis.__d_ssr_cond;
430
+ const prevList = globalThis.__d_ssr_list;
431
+ const prevCss = globalThis.__d_ssr_css;
432
+ const prevOnMount = globalThis.__d_ssr_onMount;
433
+ const prevOnDestroy = globalThis.__d_ssr_onDestroy;
434
+ const prevActive = globalThis.__d_ssr_active;
435
+
436
+ // Install SSR implementations on globalThis
437
+ globalThis.__d_ssr_h = ssrH;
438
+ globalThis.__d_ssr_text = ssrText;
439
+ globalThis.__d_ssr_cond = ssrCond;
440
+ globalThis.__d_ssr_list = ssrList;
441
+ globalThis.__d_ssr_css = ssrCss;
442
+ globalThis.__d_ssr_onMount = ssrOnMount;
443
+ globalThis.__d_ssr_onDestroy = ssrOnDestroy;
444
+ globalThis.__d_ssr_active = true;
445
+
446
+ try {
447
+ const result = componentFn();
448
+ return normalizeVNode(result);
449
+ } finally {
450
+ _ssrActive = false;
451
+
452
+ // Restore previous values
453
+ globalThis.__d_ssr_h = prevH;
454
+ globalThis.__d_ssr_text = prevText;
455
+ globalThis.__d_ssr_cond = prevCond;
456
+ globalThis.__d_ssr_list = prevList;
457
+ globalThis.__d_ssr_css = prevCss;
458
+ globalThis.__d_ssr_onMount = prevOnMount;
459
+ globalThis.__d_ssr_onDestroy = prevOnDestroy;
460
+ globalThis.__d_ssr_active = prevActive;
461
+ }
462
+ }
463
+
464
+ // ─── Public API: renderToString ─────────────────────────────────
465
+
466
+ /**
467
+ * Render a component to an HTML string.
468
+ *
469
+ * The component function is called in an SSR context where:
470
+ * - h() creates VNodes instead of DOM elements
471
+ * - text() evaluates getters once without reactive tracking
472
+ * - cond()/list() evaluate eagerly without creating effects
473
+ * - onMount()/onDestroy() are no-ops
474
+ * - Signals are read once without subscriptions
475
+ *
476
+ * Each element includes a `data-d-id` attribute for hydration matching.
477
+ *
478
+ * @param {Function} component — component function that returns a VNode tree
479
+ * @returns {string} HTML string
480
+ */
481
+ export function renderToString(component) {
482
+ const vnode = runInSSRContext(component);
483
+ return serializeVNode(vnode);
484
+ }
485
+
486
+ // ─── Public API: renderToStream ─────────────────────────────────
487
+
488
+ /**
489
+ * Render a component to a ReadableStream of HTML chunks.
490
+ *
491
+ * Same SSR semantics as renderToString but yields chunks incrementally.
492
+ * Uses the Web Streams API (available in Node.js 18+).
493
+ *
494
+ * @param {Function} component — component function that returns a VNode tree
495
+ * @returns {ReadableStream}
496
+ */
497
+ export function renderToStream(component) {
498
+ // Build VNode tree synchronously (same as renderToString)
499
+ const vnode = runInSSRContext(component);
500
+
501
+ return new ReadableStream({
502
+ start(controller) {
503
+ try {
504
+ streamVNode(vnode, chunk => {
505
+ controller.enqueue(chunk);
506
+ });
507
+ controller.close();
508
+ } catch (err) {
509
+ controller.error(err);
510
+ }
511
+ }
512
+ });
513
+ }
514
+
515
+ // ─── Public API: hydrate ────────────────────────────────────────
516
+
517
+ /**
518
+ * Hydrate an existing DOM tree produced by renderToString/renderToStream.
519
+ *
520
+ * This function:
521
+ * 1. Runs the component function in client mode (creates real DOM structures)
522
+ * 2. Walks existing DOM in parallel with component output
523
+ * 3. Reuses existing DOM nodes instead of creating new ones
524
+ * 4. Attaches event listeners, signal subscriptions, and reactive effects
525
+ * 5. Drains the onMount queue
526
+ *
527
+ * The existing DOM must match the component's initial render output structurally.
528
+ * Matching is done by position (depth-first walk), not by data-d-id.
529
+ *
530
+ * @param {HTMLElement} root — the DOM root containing SSR HTML
531
+ * @param {Function} component — the same component function used for SSR
532
+ */
533
+ export function hydrate(root, component) {
534
+ // Dynamically import core/state to avoid loading DOM globals at module level
535
+ // These must be available in the browser environment when hydrate() is called
536
+ const { createEffect } = _requireState();
537
+ const { pushScope, popScope, drainMountQueue, runDestroyFns } = _requireLifecycle();
538
+
539
+ pushScope();
540
+
541
+ // Run the component in client mode to get the real DOM tree
542
+ const clientTree = component();
543
+
544
+ const destroyFns = popScope();
545
+
546
+ // Walk the existing SSR DOM and the client-produced DOM in parallel
547
+ // to attach event listeners and reactive bindings
548
+ if (clientTree && root.firstChild) {
549
+ _hydrateNode(root.firstChild, clientTree, root, createEffect);
550
+ } else if (clientTree && !root.firstChild) {
551
+ // SSR HTML is empty but client produced content — append it
552
+ root.appendChild(clientTree);
553
+ }
554
+
555
+ // Drain mount queue
556
+ const mountFns = drainMountQueue();
557
+ for (const fn of mountFns) {
558
+ const cleanup = fn();
559
+ if (typeof cleanup === 'function') {
560
+ destroyFns.push(cleanup);
561
+ }
562
+ }
563
+
564
+ // Store destroy functions for unmount
565
+ root.__d_destroy = destroyFns;
566
+ }
567
+
568
+ // ─── Hydration Walker ───────────────────────────────────────────
569
+
570
+ /**
571
+ * Hydrate a single DOM node by reconciling it with the client-produced node.
572
+ * Attaches event listeners and reactive bindings from the client node
573
+ * onto the existing server-rendered DOM node.
574
+ *
575
+ * @param {Node} ssrNode — existing DOM node from SSR
576
+ * @param {Node} clientNode — freshly created DOM node from client render
577
+ * @param {Node} parent — parent of ssrNode
578
+ * @param {Function} createEffect — reactive effect factory
579
+ */
580
+ function _hydrateNode(ssrNode, clientNode, parent, createEffect) {
581
+ if (!ssrNode || !clientNode) return;
582
+
583
+ // Text node hydration
584
+ if (ssrNode.nodeType === 3 && clientNode.nodeType === 3) {
585
+ // If the client text node has reactive effects attached via createEffect,
586
+ // they will update ssrNode.nodeValue when signals change.
587
+ // We need to "redirect" client effects to update the SSR node instead.
588
+ // The simplest approach: copy any pending reactive subscription.
589
+ // Since the client node was created by text() which uses createEffect internally,
590
+ // the effect already observes the signal. We just need to ensure updates
591
+ // target the SSR node. We do this by patching nodeValue.
592
+ if (clientNode.nodeValue !== ssrNode.nodeValue) {
593
+ // Initial value mismatch — use client value (more recent)
594
+ ssrNode.nodeValue = clientNode.nodeValue;
595
+ }
596
+
597
+ // Redirect reactive updates: any effect that sets clientNode.nodeValue
598
+ // should instead set ssrNode.nodeValue. We achieve this by making
599
+ // clientNode.nodeValue a proxy to ssrNode.
600
+ _redirectTextUpdates(ssrNode, clientNode);
601
+ return;
602
+ }
603
+
604
+ // Element node hydration
605
+ if (ssrNode.nodeType === 1 && clientNode.nodeType === 1) {
606
+ // Attach event listeners from the client node to the SSR node
607
+ if (clientNode._listeners) {
608
+ for (const [type, fns] of clientNode._listeners) {
609
+ for (const fn of fns) {
610
+ ssrNode.addEventListener(type, fn);
611
+ }
612
+ }
613
+ }
614
+
615
+ // Copy over any special properties the client-side h() set up
616
+ // (reactive class, reactive attributes, etc.)
617
+ // The client-side createEffect calls will be trying to update clientNode.
618
+ // We redirect them to ssrNode.
619
+ _redirectElementUpdates(ssrNode, clientNode, createEffect);
620
+
621
+ // Recursively hydrate children
622
+ const ssrChildren = ssrNode.childNodes || [];
623
+ const clientChildren = clientNode.childNodes || [];
624
+ const maxLen = Math.max(ssrChildren.length, clientChildren.length);
625
+
626
+ for (let i = 0; i < maxLen; i++) {
627
+ const ssrChild = ssrChildren[i];
628
+ const clientChild = clientChildren[i];
629
+
630
+ if (ssrChild && clientChild) {
631
+ _hydrateNode(ssrChild, clientChild, ssrNode, createEffect);
632
+ } else if (!ssrChild && clientChild) {
633
+ // Client has extra node — append it (SSR missed it)
634
+ ssrNode.appendChild(clientChild);
635
+ }
636
+ // If SSR has extra node but client doesn't — leave it (stale SSR content)
637
+ }
638
+
639
+ return;
640
+ }
641
+
642
+ // Comment node (e.g., Portal placeholder) — skip
643
+ if (ssrNode.nodeType === 8) return;
644
+ }
645
+
646
+ /**
647
+ * Redirect text node updates from client node to SSR node.
648
+ * When createEffect updates clientNode.nodeValue, the update
649
+ * should also apply to ssrNode.
650
+ *
651
+ * @param {Text} ssrNode
652
+ * @param {Text} clientNode
653
+ */
654
+ function _redirectTextUpdates(ssrNode, clientNode) {
655
+ // Observe changes to clientNode.nodeValue and mirror them to ssrNode
656
+ const originalDescriptor = Object.getOwnPropertyDescriptor(
657
+ Object.getPrototypeOf(clientNode), 'nodeValue'
658
+ ) || Object.getOwnPropertyDescriptor(clientNode, 'nodeValue');
659
+
660
+ if (originalDescriptor && originalDescriptor.set) {
661
+ // If nodeValue is a setter (real DOM), intercept writes
662
+ Object.defineProperty(clientNode, 'nodeValue', {
663
+ get() {
664
+ return ssrNode.nodeValue;
665
+ },
666
+ set(v) {
667
+ ssrNode.nodeValue = v;
668
+ },
669
+ configurable: true,
670
+ });
671
+ } else {
672
+ // In test environments, nodeValue is a plain property.
673
+ // Use polling or direct assignment check.
674
+ // For simplicity in test DOM: replace clientNode in parent with ssrNode
675
+ // The effects that reference clientNode should be updated.
676
+ // Actually, in the test DOM nodeValue is just a field.
677
+ // We can define a setter on the instance.
678
+ let _val = clientNode.nodeValue;
679
+ Object.defineProperty(clientNode, 'nodeValue', {
680
+ get() { return ssrNode.nodeValue; },
681
+ set(v) { ssrNode.nodeValue = v; },
682
+ configurable: true,
683
+ });
684
+ }
685
+ }
686
+
687
+ /**
688
+ * Redirect element property updates from client node to SSR node.
689
+ * This covers reactive className, setAttribute, style updates
690
+ * that createEffect applies to the client-created element.
691
+ *
692
+ * @param {Element} ssrNode
693
+ * @param {Element} clientNode
694
+ * @param {Function} createEffect
695
+ */
696
+ function _redirectElementUpdates(ssrNode, clientNode, createEffect) {
697
+ // Intercept className writes
698
+ const classDesc = Object.getOwnPropertyDescriptor(clientNode, 'className') ||
699
+ Object.getOwnPropertyDescriptor(Object.getPrototypeOf(clientNode), 'className');
700
+
701
+ Object.defineProperty(clientNode, 'className', {
702
+ get() { return ssrNode.className; },
703
+ set(v) { ssrNode.className = v; },
704
+ configurable: true,
705
+ });
706
+
707
+ // Intercept setAttribute calls
708
+ const origSetAttribute = clientNode.setAttribute;
709
+ if (origSetAttribute) {
710
+ clientNode.setAttribute = function(name, value) {
711
+ ssrNode.setAttribute(name, value);
712
+ };
713
+ }
714
+
715
+ // Intercept style assignments
716
+ if (clientNode.style && ssrNode.style) {
717
+ const ssrStyle = ssrNode.style;
718
+ clientNode.style = new Proxy(ssrStyle, {
719
+ set(target, prop, value) {
720
+ target[prop] = value;
721
+ return true;
722
+ },
723
+ get(target, prop) {
724
+ return target[prop];
725
+ }
726
+ });
727
+ }
728
+ }
729
+
730
+ // ─── Lazy Module Resolution ─────────────────────────────────────
731
+ // These helpers lazily import core modules so that the SSR entry point
732
+ // does not pull in DOM-dependent code at module level.
733
+
734
+ let _stateModule = null;
735
+ let _lifecycleModule = null;
736
+
737
+ /**
738
+ * @returns {{ createEffect: Function }}
739
+ */
740
+ function _requireState() {
741
+ // When hydrate() is called in the browser, the state module is available.
742
+ // We import it lazily to avoid loading it during SSR (server-side).
743
+ if (!_stateModule) {
744
+ // In a browser/test environment, this will be available via the module graph
745
+ // We use a dynamic technique to avoid static analysis pulling it into SSR bundles
746
+ try {
747
+ // For Node.js test environments that have already loaded the module
748
+ _stateModule = { createEffect: globalThis.__d_state_createEffect };
749
+ if (!_stateModule.createEffect) {
750
+ throw new Error('Not cached');
751
+ }
752
+ } catch {
753
+ // Fallback: assume the module has been loaded and is available
754
+ _stateModule = { createEffect: function(fn) { fn(); return () => {}; } };
755
+ }
756
+ }
757
+ return _stateModule;
758
+ }
759
+
760
+ /**
761
+ * @returns {{ pushScope: Function, popScope: Function, drainMountQueue: Function, runDestroyFns: Function }}
762
+ */
763
+ function _requireLifecycle() {
764
+ if (!_lifecycleModule) {
765
+ try {
766
+ _lifecycleModule = {
767
+ pushScope: globalThis.__d_lifecycle_pushScope,
768
+ popScope: globalThis.__d_lifecycle_popScope,
769
+ drainMountQueue: globalThis.__d_lifecycle_drainMountQueue,
770
+ runDestroyFns: globalThis.__d_lifecycle_runDestroyFns,
771
+ };
772
+ if (!_lifecycleModule.pushScope) throw new Error('Not cached');
773
+ } catch {
774
+ // Fallback no-ops for environments where lifecycle isn't loaded
775
+ _lifecycleModule = {
776
+ pushScope: () => {},
777
+ popScope: () => [],
778
+ drainMountQueue: () => [],
779
+ runDestroyFns: () => {},
780
+ };
781
+ }
782
+ }
783
+ return _lifecycleModule;
784
+ }
785
+
786
+ // ─── Hydration Bootstrap ────────────────────────────────────────
787
+
788
+ /**
789
+ * Install hydration helpers that make core modules available to hydrate().
790
+ * Call this once on the client before calling hydrate().
791
+ *
792
+ * @param {{ createEffect: Function }} stateMod — the state module
793
+ * @param {{ pushScope: Function, popScope: Function, drainMountQueue: Function, runDestroyFns: Function }} lifecycleMod — lifecycle module
794
+ */
795
+ export function installHydrationRuntime(stateMod, lifecycleMod) {
796
+ _stateModule = stateMod;
797
+ _lifecycleModule = lifecycleMod;
798
+
799
+ // Also set globals for lazy resolution
800
+ if (stateMod.createEffect) globalThis.__d_state_createEffect = stateMod.createEffect;
801
+ if (lifecycleMod.pushScope) globalThis.__d_lifecycle_pushScope = lifecycleMod.pushScope;
802
+ if (lifecycleMod.popScope) globalThis.__d_lifecycle_popScope = lifecycleMod.popScope;
803
+ if (lifecycleMod.drainMountQueue) globalThis.__d_lifecycle_drainMountQueue = lifecycleMod.drainMountQueue;
804
+ if (lifecycleMod.runDestroyFns) globalThis.__d_lifecycle_runDestroyFns = lifecycleMod.runDestroyFns;
805
+ }
806
+
807
+ // ─── Direct SSR Builders (for components using SSR directly) ────
808
+
809
+ /**
810
+ * Check if we're currently in SSR mode.
811
+ * Components can use this to branch between SSR and client rendering.
812
+ * @returns {boolean}
813
+ */
814
+ export function isSSR() {
815
+ return _ssrActive || !!globalThis.__d_ssr_active;
816
+ }
817
+
818
+ /**
819
+ * Get the SSR-safe h() function.
820
+ * Returns ssrH during SSR, null otherwise.
821
+ * @returns {Function|null}
822
+ */
823
+ export function getSSRH() {
824
+ return _ssrActive ? ssrH : (globalThis.__d_ssr_h || null);
825
+ }
826
+
827
+ /**
828
+ * Get the SSR-safe text() function.
829
+ * Returns ssrText during SSR, null otherwise.
830
+ * @returns {Function|null}
831
+ */
832
+ export function getSSRText() {
833
+ return _ssrActive ? ssrText : (globalThis.__d_ssr_text || null);
834
+ }
835
+
836
+ /**
837
+ * Get the SSR-safe cond() function.
838
+ * @returns {Function|null}
839
+ */
840
+ export function getSSRCond() {
841
+ return _ssrActive ? ssrCond : (globalThis.__d_ssr_cond || null);
842
+ }
843
+
844
+ /**
845
+ * Get the SSR-safe list() function.
846
+ * @returns {Function|null}
847
+ */
848
+ export function getSSRList() {
849
+ return _ssrActive ? ssrList : (globalThis.__d_ssr_list || null);
850
+ }
851
+
852
+ /**
853
+ * Get the SSR-safe css() function.
854
+ * @returns {Function|null}
855
+ */
856
+ export function getSSRCss() {
857
+ return _ssrActive ? ssrCss : (globalThis.__d_ssr_css || null);
858
+ }
859
+
860
+ // ─── SSR Component Wrapper ──────────────────────────────────────
861
+
862
+ /**
863
+ * Create an SSR-compatible component that works in both server and client contexts.
864
+ * Returns a wrapper that delegates to SSR primitives during renderToString
865
+ * and to normal DOM primitives in the browser.
866
+ *
867
+ * Usage:
868
+ * ```js
869
+ * import { ssrComponent } from 'decantr/ssr';
870
+ *
871
+ * const MyComponent = ssrComponent((h, text, cond, list, css) => {
872
+ * return (props) => h('div', { class: css('_flex _p4') }, text(() => props.title));
873
+ * });
874
+ * ```
875
+ *
876
+ * @param {Function} factory — (h, text, cond, list, css) => componentFn
877
+ * @returns {Function}
878
+ */
879
+ export function ssrComponent(factory) {
880
+ return function(...args) {
881
+ if (isSSR()) {
882
+ const impl = factory(ssrH, ssrText, ssrCond, ssrList, ssrCss);
883
+ return impl(...args);
884
+ }
885
+ // In client mode, we need the real implementations
886
+ // They should be imported normally by the consuming code
887
+ throw new Error(
888
+ 'ssrComponent() called in client mode without real implementations. ' +
889
+ 'Import h, text, cond, list, css directly for client rendering.'
890
+ );
891
+ };
892
+ }
893
+
894
+ // ─── Exported SSR Primitives ────────────────────────────────────
895
+
896
+ // Export SSR primitives for direct use in universal components
897
+ export {
898
+ ssrH,
899
+ ssrText,
900
+ ssrCond,
901
+ ssrList,
902
+ ssrCss,
903
+ ssrOnMount,
904
+ ssrOnDestroy,
905
+ };
906
+
907
+ // Export internals for testing
908
+ export const _internals = {
909
+ serializeVNode,
910
+ streamVNode,
911
+ flattenChildren,
912
+ normalizeVNode,
913
+ escapeHTML,
914
+ escapeAttr,
915
+ styleObjToString,
916
+ VOID_ELEMENTS,
917
+ BOOLEAN_ATTRS,
918
+ _hydrateNode,
919
+ _redirectTextUpdates,
920
+ _redirectElementUpdates,
921
+ runInSSRContext,
922
+ };