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.
- package/AGENTS.md +868 -0
- package/CHANGELOG.md +255 -0
- package/CLAUDE.md +178 -0
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/cli/art.js +127 -0
- package/cli/commands/a11y.js +61 -0
- package/cli/commands/audit.js +225 -0
- package/cli/commands/build.js +38 -0
- package/cli/commands/dev.js +18 -0
- package/cli/commands/doctor.js +197 -0
- package/cli/commands/figma-sync.js +48 -0
- package/cli/commands/figma-tokens.js +55 -0
- package/cli/commands/generate.js +26 -0
- package/cli/commands/init.js +116 -0
- package/cli/commands/lint.js +209 -0
- package/cli/commands/mcp.js +530 -0
- package/cli/commands/migrate.js +175 -0
- package/cli/commands/test.js +38 -0
- package/cli/commands/validate.js +354 -0
- package/cli/index.js +113 -0
- package/package.json +95 -0
- package/reference/atoms.md +517 -0
- package/reference/behaviors.md +384 -0
- package/reference/build-tooling.md +275 -0
- package/reference/color-guidelines.md +965 -0
- package/reference/component-lifecycle.md +137 -0
- package/reference/compound-spacing.md +95 -0
- package/reference/decantation-process.md +499 -0
- package/reference/dev-server-routes.md +93 -0
- package/reference/form-system.md +253 -0
- package/reference/i18n.md +336 -0
- package/reference/icons.md +576 -0
- package/reference/llm-primer.md +953 -0
- package/reference/plugins.md +252 -0
- package/reference/registry-consumption.md +76 -0
- package/reference/router.md +217 -0
- package/reference/shells.md +116 -0
- package/reference/spatial-guidelines.md +541 -0
- package/reference/ssr.md +234 -0
- package/reference/state-data.md +215 -0
- package/reference/state-patterns.md +166 -0
- package/reference/state.md +194 -0
- package/reference/style-system.md +110 -0
- package/reference/tokens.md +460 -0
- package/src/app.js +19 -0
- package/src/chart/_animate.js +266 -0
- package/src/chart/_base.js +109 -0
- package/src/chart/_data.js +209 -0
- package/src/chart/_format.js +106 -0
- package/src/chart/_interact.js +364 -0
- package/src/chart/_palette.js +105 -0
- package/src/chart/_renderer.js +52 -0
- package/src/chart/_scene.js +262 -0
- package/src/chart/_shared.js +371 -0
- package/src/chart/index.js +637 -0
- package/src/chart/layouts/_layout-base.js +328 -0
- package/src/chart/layouts/cartesian.js +148 -0
- package/src/chart/layouts/hierarchy.js +562 -0
- package/src/chart/layouts/polar.js +101 -0
- package/src/chart/renderers/canvas.js +179 -0
- package/src/chart/renderers/svg.js +256 -0
- package/src/chart/renderers/webgpu.js +715 -0
- package/src/chart/types/_type-base.js +26 -0
- package/src/chart/types/area.js +134 -0
- package/src/chart/types/bar.js +173 -0
- package/src/chart/types/box-plot.js +125 -0
- package/src/chart/types/bubble.js +63 -0
- package/src/chart/types/candlestick.js +115 -0
- package/src/chart/types/chord.js +85 -0
- package/src/chart/types/combination.js +108 -0
- package/src/chart/types/funnel.js +68 -0
- package/src/chart/types/gauge.js +163 -0
- package/src/chart/types/heatmap.js +98 -0
- package/src/chart/types/histogram.js +71 -0
- package/src/chart/types/line.js +111 -0
- package/src/chart/types/org-chart.js +93 -0
- package/src/chart/types/pie.js +81 -0
- package/src/chart/types/radar.js +96 -0
- package/src/chart/types/radial.js +68 -0
- package/src/chart/types/range-area.js +55 -0
- package/src/chart/types/range-bar.js +61 -0
- package/src/chart/types/sankey.js +73 -0
- package/src/chart/types/scatter.js +66 -0
- package/src/chart/types/sparkline.js +81 -0
- package/src/chart/types/sunburst.js +69 -0
- package/src/chart/types/swimlane.js +88 -0
- package/src/chart/types/treemap.js +62 -0
- package/src/chart/types/waterfall.js +100 -0
- package/src/components/_base.js +1658 -0
- package/src/components/_behaviors.js +1140 -0
- package/src/components/_primitives.js +534 -0
- package/src/components/_qr-encoder.js +539 -0
- package/src/components/accordion.js +207 -0
- package/src/components/affix.js +62 -0
- package/src/components/alert-dialog.js +75 -0
- package/src/components/alert.js +47 -0
- package/src/components/aspect-ratio.js +24 -0
- package/src/components/avatar-group.js +55 -0
- package/src/components/avatar.js +38 -0
- package/src/components/back-top.js +75 -0
- package/src/components/badge.js +74 -0
- package/src/components/banner.js +68 -0
- package/src/components/breadcrumb.js +162 -0
- package/src/components/button.js +115 -0
- package/src/components/calendar.js +131 -0
- package/src/components/card.js +192 -0
- package/src/components/carousel.js +98 -0
- package/src/components/cascader.js +261 -0
- package/src/components/checkbox.js +80 -0
- package/src/components/chip.js +81 -0
- package/src/components/code-block.js +82 -0
- package/src/components/collapsible.js +50 -0
- package/src/components/color-palette.js +438 -0
- package/src/components/color-picker.js +314 -0
- package/src/components/combobox.js +181 -0
- package/src/components/command.js +174 -0
- package/src/components/comment.js +206 -0
- package/src/components/context-menu.js +76 -0
- package/src/components/data-table.js +724 -0
- package/src/components/date-picker.js +217 -0
- package/src/components/date-range-picker.js +244 -0
- package/src/components/datetime-picker.js +271 -0
- package/src/components/descriptions.js +68 -0
- package/src/components/drawer.js +179 -0
- package/src/components/dropdown.js +88 -0
- package/src/components/empty.js +41 -0
- package/src/components/float-button.js +90 -0
- package/src/components/form.js +106 -0
- package/src/components/hover-card.js +49 -0
- package/src/components/icon.js +87 -0
- package/src/components/image.js +97 -0
- package/src/components/index.js +117 -0
- package/src/components/input-group.js +75 -0
- package/src/components/input-number.js +155 -0
- package/src/components/input-otp.js +178 -0
- package/src/components/input.js +91 -0
- package/src/components/kbd.js +36 -0
- package/src/components/label.js +25 -0
- package/src/components/list.js +118 -0
- package/src/components/masked-input.js +236 -0
- package/src/components/mentions.js +165 -0
- package/src/components/menu.js +259 -0
- package/src/components/message.js +80 -0
- package/src/components/modal.js +147 -0
- package/src/components/navigation-menu.js +166 -0
- package/src/components/notification.js +84 -0
- package/src/components/pagination.js +104 -0
- package/src/components/placeholder.js +132 -0
- package/src/components/popconfirm.js +70 -0
- package/src/components/popover.js +58 -0
- package/src/components/progress.js +61 -0
- package/src/components/qrcode.js +251 -0
- package/src/components/radiogroup.js +120 -0
- package/src/components/range-slider.js +176 -0
- package/src/components/rate.js +186 -0
- package/src/components/resizable.js +83 -0
- package/src/components/result.js +57 -0
- package/src/components/scroll-area.js +43 -0
- package/src/components/segmented.js +97 -0
- package/src/components/select.js +165 -0
- package/src/components/separator.js +31 -0
- package/src/components/shell.js +407 -0
- package/src/components/skeleton.js +39 -0
- package/src/components/slider.js +141 -0
- package/src/components/sortable-list.js +176 -0
- package/src/components/space.js +42 -0
- package/src/components/spinner.js +112 -0
- package/src/components/splitter.js +147 -0
- package/src/components/statistic.js +136 -0
- package/src/components/steps.js +99 -0
- package/src/components/switch.js +95 -0
- package/src/components/table.js +44 -0
- package/src/components/tabs.js +216 -0
- package/src/components/tag.js +115 -0
- package/src/components/textarea.js +82 -0
- package/src/components/time-picker.js +153 -0
- package/src/components/time-range-picker.js +170 -0
- package/src/components/timeline.js +226 -0
- package/src/components/toast.js +71 -0
- package/src/components/toggle.js +213 -0
- package/src/components/tooltip.js +57 -0
- package/src/components/tour.js +159 -0
- package/src/components/transfer.js +163 -0
- package/src/components/tree-select.js +274 -0
- package/src/components/tree.js +141 -0
- package/src/components/typography.js +136 -0
- package/src/components/upload.js +118 -0
- package/src/components/visually-hidden.js +20 -0
- package/src/components/watermark.js +124 -0
- package/src/core/index.js +539 -0
- package/src/core/lifecycle.js +69 -0
- package/src/css/atoms.js +651 -0
- package/src/css/components.js +940 -0
- package/src/css/derive.js +1296 -0
- package/src/css/index.js +265 -0
- package/src/css/runtime.js +268 -0
- package/src/css/styles/addons/bioluminescent.js +93 -0
- package/src/css/styles/addons/clay.js +70 -0
- package/src/css/styles/addons/clean.js +57 -0
- package/src/css/styles/addons/command-center.js +143 -0
- package/src/css/styles/addons/dopamine.js +83 -0
- package/src/css/styles/addons/editorial.js +80 -0
- package/src/css/styles/addons/glassmorphism.js +99 -0
- package/src/css/styles/addons/liquid-glass.js +105 -0
- package/src/css/styles/addons/prismatic.js +100 -0
- package/src/css/styles/addons/retro.js +63 -0
- package/src/css/styles/auradecantism.js +96 -0
- package/src/css/theme-registry.js +444 -0
- package/src/data/entity.js +281 -0
- package/src/data/index.js +13 -0
- package/src/data/persist.js +225 -0
- package/src/data/query.js +839 -0
- package/src/data/realtime.js +299 -0
- package/src/data/url.js +177 -0
- package/src/data/worker.js +134 -0
- package/src/explorer/archetypes.js +243 -0
- package/src/explorer/atoms.js +228 -0
- package/src/explorer/charts.js +497 -0
- package/src/explorer/components.js +129 -0
- package/src/explorer/foundations.js +949 -0
- package/src/explorer/icons.js +178 -0
- package/src/explorer/patterns.js +247 -0
- package/src/explorer/recipes.js +194 -0
- package/src/explorer/shared/pattern-examples.js +1337 -0
- package/src/explorer/shared/showcase-renderer.js +958 -0
- package/src/explorer/shared/spec-table.js +41 -0
- package/src/explorer/shared/usage-links.js +87 -0
- package/src/explorer/shell-config.js +10 -0
- package/src/explorer/shells.js +551 -0
- package/src/explorer/styles.js +161 -0
- package/src/explorer/tokens.js +262 -0
- package/src/explorer/tools.js +525 -0
- package/src/form/index.js +804 -0
- package/src/i18n/index.js +251 -0
- package/src/icons/essential.js +479 -0
- package/src/icons/index.js +53 -0
- package/src/plugins/index.js +282 -0
- package/src/registry/archetypes/content-site.json +71 -0
- package/src/registry/archetypes/docs-explorer.json +23 -0
- package/src/registry/archetypes/ecommerce.json +104 -0
- package/src/registry/archetypes/financial-dashboard.json +77 -0
- package/src/registry/archetypes/index.json +41 -0
- package/src/registry/archetypes/portfolio.json +82 -0
- package/src/registry/archetypes/recipe-community.json +159 -0
- package/src/registry/archetypes/saas-dashboard.json +86 -0
- package/src/registry/architect/cross-cutting.json +45 -0
- package/src/registry/architect/domains/ecommerce.json +294 -0
- package/src/registry/architect/domains/financial-services.json +302 -0
- package/src/registry/architect/index.json +26 -0
- package/src/registry/architect/traits.json +379 -0
- package/src/registry/atoms.json +16 -0
- package/src/registry/chart-showcase.json +160 -0
- package/src/registry/chart.json +136 -0
- package/src/registry/components.json +8616 -0
- package/src/registry/core.json +216 -0
- package/src/registry/css.json +319 -0
- package/src/registry/data.json +135 -0
- package/src/registry/foundations.json +11 -0
- package/src/registry/icons.json +463 -0
- package/src/registry/index.json +101 -0
- package/src/registry/patterns/activity-feed.json +37 -0
- package/src/registry/patterns/article-content.json +27 -0
- package/src/registry/patterns/auth-form.json +37 -0
- package/src/registry/patterns/author-card.json +20 -0
- package/src/registry/patterns/card-grid.json +127 -0
- package/src/registry/patterns/category-nav.json +26 -0
- package/src/registry/patterns/chart-grid.json +36 -0
- package/src/registry/patterns/chat-interface.json +37 -0
- package/src/registry/patterns/checklist-card.json +55 -0
- package/src/registry/patterns/comparison-panel.json +27 -0
- package/src/registry/patterns/component-showcase.json +24 -0
- package/src/registry/patterns/contact-form.json +31 -0
- package/src/registry/patterns/cta-section.json +20 -0
- package/src/registry/patterns/data-table.json +37 -0
- package/src/registry/patterns/detail-header.json +83 -0
- package/src/registry/patterns/detail-panel.json +27 -0
- package/src/registry/patterns/explorer-shell.json +22 -0
- package/src/registry/patterns/filter-bar.json +33 -0
- package/src/registry/patterns/filter-sidebar.json +27 -0
- package/src/registry/patterns/form-sections.json +110 -0
- package/src/registry/patterns/goal-tracker.json +27 -0
- package/src/registry/patterns/hero.json +107 -0
- package/src/registry/patterns/index.json +47 -0
- package/src/registry/patterns/kpi-grid.json +36 -0
- package/src/registry/patterns/media-gallery.json +20 -0
- package/src/registry/patterns/order-history.json +20 -0
- package/src/registry/patterns/pagination.json +19 -0
- package/src/registry/patterns/photo-to-recipe.json +36 -0
- package/src/registry/patterns/pipeline-tracker.json +28 -0
- package/src/registry/patterns/post-list.json +27 -0
- package/src/registry/patterns/pricing-table.json +32 -0
- package/src/registry/patterns/scorecard.json +28 -0
- package/src/registry/patterns/search-bar.json +20 -0
- package/src/registry/patterns/specimen-grid.json +19 -0
- package/src/registry/patterns/stat-card.json +55 -0
- package/src/registry/patterns/stats-bar.json +55 -0
- package/src/registry/patterns/steps-card.json +55 -0
- package/src/registry/patterns/table-of-contents.json +19 -0
- package/src/registry/patterns/testimonials.json +21 -0
- package/src/registry/patterns/timeline.json +27 -0
- package/src/registry/patterns/token-inspector.json +21 -0
- package/src/registry/patterns/wizard.json +27 -0
- package/src/registry/recipe-auradecantism.json +69 -0
- package/src/registry/recipe-clean.json +65 -0
- package/src/registry/recipe-command-center.json +78 -0
- package/src/registry/router.json +73 -0
- package/src/registry/schema/README.md +197 -0
- package/src/registry/skeletons.json +259 -0
- package/src/registry/state.json +137 -0
- package/src/registry/tokens.json +40 -0
- package/src/router/hash.js +17 -0
- package/src/router/history.js +18 -0
- package/src/router/index.js +598 -0
- package/src/ssr/index.js +922 -0
- package/src/state/arrays.js +181 -0
- package/src/state/devtools.js +647 -0
- package/src/state/index.js +498 -0
- package/src/state/middleware.js +288 -0
- package/src/state/scheduler.js +206 -0
- package/src/state/store.js +300 -0
- package/src/tags/index.js +19 -0
- package/src/tannins/auth.js +396 -0
- package/src/test/dom.js +352 -0
- package/src/test/index.js +62 -0
- package/src/test/state.js +306 -0
- package/tools/a11y-audit.js +487 -0
- package/tools/analyzer.js +315 -0
- package/tools/audit.js +706 -0
- package/tools/builder.js +1422 -0
- package/tools/css-extract.js +188 -0
- package/tools/dev-server.js +316 -0
- package/tools/dts-gen.js +1260 -0
- package/tools/figma-components.js +329 -0
- package/tools/figma-patterns.js +516 -0
- package/tools/figma-plugin/code.js +453 -0
- package/tools/figma-plugin/manifest.json +14 -0
- package/tools/figma-plugin/ui.html +268 -0
- package/tools/figma-render.js +293 -0
- package/tools/figma-tokens.js +712 -0
- package/tools/figma-upload.js +318 -0
- package/tools/generate.js +738 -0
- package/tools/icons.js +133 -0
- package/tools/init-templates.js +265 -0
- package/tools/install-hooks.sh +5 -0
- package/tools/migrations/0.5.0.js +53 -0
- package/tools/migrations/0.6.0.js +95 -0
- package/tools/minify.js +170 -0
- package/tools/pre-commit +4 -0
- package/tools/registry.js +662 -0
- package/tools/reset-playground.js +61 -0
- package/tools/starter-templates/content-site/app.js +49 -0
- package/tools/starter-templates/content-site/essence.js +19 -0
- package/tools/starter-templates/content-site/pages.js +31 -0
- package/tools/starter-templates/ecommerce/app.js +50 -0
- package/tools/starter-templates/ecommerce/essence.js +19 -0
- package/tools/starter-templates/ecommerce/pages.js +31 -0
- package/tools/starter-templates/landing-page/app.js +38 -0
- package/tools/starter-templates/landing-page/essence.js +18 -0
- package/tools/starter-templates/landing-page/pages.js +21 -0
- package/tools/starter-templates/portfolio/app.js +45 -0
- package/tools/starter-templates/portfolio/essence.js +19 -0
- package/tools/starter-templates/portfolio/pages.js +33 -0
- package/tools/starter-templates/saas-dashboard/app.js +70 -0
- package/tools/starter-templates/saas-dashboard/essence.js +19 -0
- package/tools/starter-templates/saas-dashboard/pages.js +31 -0
- package/tools/verify-pack.js +203 -0
- package/types/chart.d.ts +77 -0
- package/types/components.d.ts +587 -0
- package/types/core.d.ts +89 -0
- package/types/css.d.ts +149 -0
- package/types/data.d.ts +238 -0
- package/types/form.d.ts +164 -0
- package/types/i18n.d.ts +51 -0
- package/types/icons.d.ts +27 -0
- package/types/index.d.ts +13 -0
- package/types/router.d.ts +116 -0
- package/types/ssr.d.ts +102 -0
- package/types/state.d.ts +83 -0
- package/types/tags.d.ts +62 -0
- package/types/tannins.d.ts +63 -0
- package/types/test.d.ts +48 -0
package/reference/ssr.md
ADDED
|
@@ -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
|
+
```
|