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
|
@@ -0,0 +1,949 @@
|
|
|
1
|
+
import { h, cond, list, text } from 'decantr/core';
|
|
2
|
+
import { createSignal, createEffect, createMemo, createStore, batch,
|
|
3
|
+
createRoot, on, createHistory, createContext, createSelector,
|
|
4
|
+
untrack } from 'decantr/state';
|
|
5
|
+
import { withMiddleware, validationMiddleware, undoMiddleware } from 'decantr/state/middleware.js';
|
|
6
|
+
import { createEntityStore } from 'decantr/data';
|
|
7
|
+
import { createForm, validators } from 'decantr/form';
|
|
8
|
+
import { css } from 'decantr/css';
|
|
9
|
+
import { tags } from 'decantr/tags';
|
|
10
|
+
import { Button, Input, Badge, Chip, Separator, CodeBlock } from 'decantr/components';
|
|
11
|
+
import { injectExplorerCSS } from './styles.js';
|
|
12
|
+
injectExplorerCSS();
|
|
13
|
+
|
|
14
|
+
const { div, h2, h3, h4, p, span, section, label, code, pre } = tags;
|
|
15
|
+
|
|
16
|
+
// ─── Shared section structure ───────────────────────────────
|
|
17
|
+
// Follows component-showcase pattern layout: _flex _col _gap6
|
|
18
|
+
|
|
19
|
+
function DemoSection(title, description, ...children) {
|
|
20
|
+
return div({ class: css('_flex _col _gap4') },
|
|
21
|
+
Separator({}),
|
|
22
|
+
div({ class: css('_flex _col _gap1') },
|
|
23
|
+
h3({ class: css('_heading5') }, title),
|
|
24
|
+
description ? p({ class: css('_body _fgmutedfg') }, description) : null
|
|
25
|
+
),
|
|
26
|
+
...children
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function DemoRow(...children) {
|
|
31
|
+
return div({ class: css('_flex _gap3 _wrap _aic') }, ...children);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function LogPanel() {
|
|
35
|
+
const el = div({ class: css('_fontmono _caption _fgmutedfg _p3 _bgmuted/10 _radius _oauto _mh[160px] _flex _col _gap1') });
|
|
36
|
+
const entries = [];
|
|
37
|
+
return {
|
|
38
|
+
el,
|
|
39
|
+
log(msg) {
|
|
40
|
+
entries.push(msg);
|
|
41
|
+
if (entries.length > 50) entries.shift();
|
|
42
|
+
el.textContent = '';
|
|
43
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
44
|
+
el.appendChild(div({}, entries[i]));
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
clear() {
|
|
48
|
+
entries.length = 0;
|
|
49
|
+
el.textContent = '';
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── Core Demos ─────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
function CoreDemo() {
|
|
57
|
+
return div({ class: css('_flex _col _gap6') },
|
|
58
|
+
DemoSection(
|
|
59
|
+
'h() — Element Creation',
|
|
60
|
+
'Creates DOM elements. Tags proxy provides the same capability with terser syntax.',
|
|
61
|
+
DemoRow(
|
|
62
|
+
Button({ variant: 'primary' }, 'Click me'),
|
|
63
|
+
span({ class: css('_body _fgmutedfg') }, 'Created with Button({variant:\'primary\'}, \'Click me\')')
|
|
64
|
+
),
|
|
65
|
+
CodeBlock({ language: 'javascript' },
|
|
66
|
+
`import { h } from 'decantr/core';\nimport { tags } from 'decantr/tags';\n\n// Using h()\nconst el = h('div', { class: 'my-class' }, 'Hello');\n\n// Using tags proxy (~25% fewer tokens)\nconst { div, span } = tags;\nconst el2 = div({ class: 'my-class' }, 'Hello');`
|
|
67
|
+
)
|
|
68
|
+
),
|
|
69
|
+
DemoSection(
|
|
70
|
+
'cond() — Conditional Rendering',
|
|
71
|
+
'Reactively toggles between two DOM branches.',
|
|
72
|
+
CondDemo(),
|
|
73
|
+
CodeBlock({ language: 'javascript' },
|
|
74
|
+
`const [show, setShow] = createSignal(true);\nconst el = cond(show,\n () => Badge({ variant: 'success' }, 'Visible'),\n () => Badge({ variant: 'error' }, 'Hidden')\n);`
|
|
75
|
+
)
|
|
76
|
+
),
|
|
77
|
+
DemoSection(
|
|
78
|
+
'list() — Keyed List Rendering',
|
|
79
|
+
'Renders a reactive list with keyed reconciliation.',
|
|
80
|
+
ListDemo(),
|
|
81
|
+
CodeBlock({ language: 'javascript' },
|
|
82
|
+
`const [items, setItems] = createSignal(['Alpha', 'Beta']);\nconst el = list(items, item => item, item =>\n Chip({ variant: 'primary', label: item })\n);`
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function CondDemo() {
|
|
89
|
+
const [show, setShow] = createSignal(true);
|
|
90
|
+
const toggle = Button({ variant: 'outline', size: 'sm', onclick: () => setShow(!show()) }, 'Toggle');
|
|
91
|
+
const conditional = cond(show,
|
|
92
|
+
() => Badge({ variant: 'success' }, 'Visible'),
|
|
93
|
+
() => Badge({ variant: 'error' }, 'Hidden')
|
|
94
|
+
);
|
|
95
|
+
return DemoRow(toggle, span({ class: css('_body') }, conditional));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function ListDemo() {
|
|
99
|
+
const [items, setItems] = createSignal(['Alpha', 'Beta', 'Gamma']);
|
|
100
|
+
const addBtn = Button({ variant: 'outline', size: 'sm', onclick: () => {
|
|
101
|
+
setItems([...items(), `Item-${items().length + 1}`]);
|
|
102
|
+
}}, 'Add Item');
|
|
103
|
+
const rendered = list(items, (item) => item, (item) =>
|
|
104
|
+
Chip({ variant: 'primary', label: item })
|
|
105
|
+
);
|
|
106
|
+
return div({ class: css('_flex _col _gap4') },
|
|
107
|
+
addBtn,
|
|
108
|
+
div({ class: css('_flex _gap2 _wrap') }, rendered)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── State Demos ────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
function StateDemo() {
|
|
115
|
+
return div({ class: css('_flex _col _gap6') },
|
|
116
|
+
DemoSection(
|
|
117
|
+
'createSignal — Reactive Primitive',
|
|
118
|
+
'Returns [getter, setter]. Reading the getter inside createEffect auto-tracks.',
|
|
119
|
+
SignalDemo(),
|
|
120
|
+
CodeBlock({ language: 'javascript' },
|
|
121
|
+
`const [count, setCount] = createSignal(0);\ncreateEffect(() => {\n console.log('Count:', count()); // auto-tracked\n});`
|
|
122
|
+
)
|
|
123
|
+
),
|
|
124
|
+
DemoSection(
|
|
125
|
+
'createMemo — Cached Derivation',
|
|
126
|
+
'Derives a cached value from other signals. Recomputes only when dependencies change.',
|
|
127
|
+
MemoDemo(),
|
|
128
|
+
CodeBlock({ language: 'javascript' },
|
|
129
|
+
`const sum = createMemo(() => a() + b());\nconst product = createMemo(() => a() * b());`
|
|
130
|
+
)
|
|
131
|
+
),
|
|
132
|
+
DemoSection(
|
|
133
|
+
'createStore — Reactive Object',
|
|
134
|
+
'Returns a Proxy that tracks per-property reads/writes. Direct assignment triggers effects.',
|
|
135
|
+
StoreDemo(),
|
|
136
|
+
CodeBlock({ language: 'javascript' },
|
|
137
|
+
`const store = createStore({ name: 'Decantr', stars: 0 });\nstore.stars = store.stars + 1; // triggers effects`
|
|
138
|
+
)
|
|
139
|
+
),
|
|
140
|
+
DemoSection(
|
|
141
|
+
'batch() — Batched Updates',
|
|
142
|
+
'Groups multiple signal writes into a single flush. Effects run once instead of per-write.',
|
|
143
|
+
BatchDemo(),
|
|
144
|
+
CodeBlock({ language: 'javascript' },
|
|
145
|
+
`const [x, setX] = createSignal(0);\nconst [y, setY] = createSignal(0);\n\n// Without batch: effect fires twice\nsetX(1); setY(1);\n\n// With batch: effect fires once\nbatch(() => { setX(2); setY(2); });`
|
|
146
|
+
)
|
|
147
|
+
),
|
|
148
|
+
DemoSection(
|
|
149
|
+
'on() — Explicit Dependency Tracking',
|
|
150
|
+
'Declares which signals to track explicitly. The effect body runs inside untrack().',
|
|
151
|
+
OnDemo(),
|
|
152
|
+
CodeBlock({ language: 'javascript' },
|
|
153
|
+
`const [search, setSearch] = createSignal('');\nconst [mode, setMode] = createSignal('all');\n\n// Only fires when search changes, not mode\non(search, (value, prev) => {\n console.log('Search:', value, 'Mode:', mode());\n});`
|
|
154
|
+
)
|
|
155
|
+
),
|
|
156
|
+
DemoSection(
|
|
157
|
+
'createHistory — Undo/Redo',
|
|
158
|
+
'Wraps a signal with an undo/redo stack. Time-travel for any reactive value.',
|
|
159
|
+
HistoryDemo(),
|
|
160
|
+
CodeBlock({ language: 'javascript' },
|
|
161
|
+
`const signal = createSignal('hello');\nconst { undo, redo, canUndo, canRedo } = createHistory(signal);\n\nsignal[1]('world'); // pushes to history\nundo(); // back to 'hello'\nredo(); // forward to 'world'`
|
|
162
|
+
)
|
|
163
|
+
),
|
|
164
|
+
DemoSection(
|
|
165
|
+
'createRoot — Ownership & Cleanup',
|
|
166
|
+
'Creates an isolated reactive scope. Disposing it stops all effects inside.',
|
|
167
|
+
RootDemo(),
|
|
168
|
+
CodeBlock({ language: 'javascript' },
|
|
169
|
+
`createRoot(dispose => {\n const [count, setCount] = createSignal(0);\n createEffect(() => console.log(count()));\n // Later: dispose() stops the effect\n});`
|
|
170
|
+
)
|
|
171
|
+
),
|
|
172
|
+
DemoSection(
|
|
173
|
+
'createSelector — Efficient Selection',
|
|
174
|
+
'Only notifies the previous and current matching items on change — O(1) instead of O(n).',
|
|
175
|
+
SelectorDemo(),
|
|
176
|
+
CodeBlock({ language: 'javascript' },
|
|
177
|
+
`const [selected, setSelected] = createSignal('a');\nconst isSelected = createSelector(selected);\n\n// Only 'a' and 'b' re-render when switching a→b\nisSelected('a'); // false (was true)\nisSelected('b'); // true (was false)\nisSelected('c'); // not notified at all`
|
|
178
|
+
)
|
|
179
|
+
),
|
|
180
|
+
DemoSection(
|
|
181
|
+
'createContext — Provider/Consumer',
|
|
182
|
+
'Dependency injection via context. Nested providers override for their subtree.',
|
|
183
|
+
ContextDemo(),
|
|
184
|
+
CodeBlock({ language: 'javascript' },
|
|
185
|
+
`const ThemeCtx = createContext('light');\n\n// Provide\nconst restore = ThemeCtx.Provider('dark');\n\n// Consume (reads nearest provider)\nThemeCtx.consume(); // 'dark'\n\nrestore(); // back to 'light'`
|
|
186
|
+
)
|
|
187
|
+
),
|
|
188
|
+
DemoSection(
|
|
189
|
+
'createEffect — Dependency Cleanup',
|
|
190
|
+
'Effects re-track on every run. Switching which signal is read drops stale subscriptions.',
|
|
191
|
+
EffectCleanupDemo(),
|
|
192
|
+
CodeBlock({ language: 'javascript' },
|
|
193
|
+
`const [useA, setUseA] = createSignal(true);\nconst [a, setA] = createSignal('A');\nconst [b, setB] = createSignal('B');\n\ncreateEffect(() => {\n // Only subscribes to whichever branch runs\n console.log(useA() ? a() : b());\n});\n// After setUseA(false): changing a() won't fire`
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function SignalDemo() {
|
|
200
|
+
const [count, setCount] = createSignal(0);
|
|
201
|
+
const [effectRuns, setEffectRuns] = createSignal(0);
|
|
202
|
+
const inc = Button({ variant: 'primary', size: 'sm', onclick: () => setCount(count() + 1) }, '+1');
|
|
203
|
+
const dec = Button({ variant: 'outline', size: 'sm', onclick: () => setCount(count() - 1) }, '-1');
|
|
204
|
+
const display = span({ class: css('_heading3') });
|
|
205
|
+
const runsDisplay = span({});
|
|
206
|
+
|
|
207
|
+
createEffect(() => {
|
|
208
|
+
display.textContent = String(count());
|
|
209
|
+
setEffectRuns(r => r + 1);
|
|
210
|
+
});
|
|
211
|
+
createEffect(() => { runsDisplay.textContent = String(effectRuns()); });
|
|
212
|
+
|
|
213
|
+
return div({ class: css('_flex _col _gap4') },
|
|
214
|
+
DemoRow(dec, display, inc),
|
|
215
|
+
DemoRow(
|
|
216
|
+
span({ class: css('_caption _fgmutedfg') }, 'Effects fired:'),
|
|
217
|
+
Badge({ variant: 'accent', count: effectRuns })
|
|
218
|
+
)
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function MemoDemo() {
|
|
223
|
+
const [a, setA] = createSignal(3);
|
|
224
|
+
const [b, setB] = createSignal(5);
|
|
225
|
+
const sum = createMemo(() => a() + b());
|
|
226
|
+
const product = createMemo(() => a() * b());
|
|
227
|
+
|
|
228
|
+
const aDisplay = span({});
|
|
229
|
+
const bDisplay = span({});
|
|
230
|
+
|
|
231
|
+
createEffect(() => { aDisplay.textContent = String(a()); });
|
|
232
|
+
createEffect(() => { bDisplay.textContent = String(b()); });
|
|
233
|
+
|
|
234
|
+
return div({ class: css('_flex _col _gap4') },
|
|
235
|
+
DemoRow(
|
|
236
|
+
Button({ size: 'sm', variant: 'outline', onclick: () => setA(a() + 1) }, 'a++'),
|
|
237
|
+
span({}, 'a='), aDisplay,
|
|
238
|
+
Button({ size: 'sm', variant: 'outline', onclick: () => setB(b() + 1) }, 'b++'),
|
|
239
|
+
span({}, 'b='), bDisplay
|
|
240
|
+
),
|
|
241
|
+
DemoRow(
|
|
242
|
+
span({ class: css('_flex _gap1 _aic') }, 'sum:', Badge({ variant: 'primary', count: sum })),
|
|
243
|
+
span({ class: css('_flex _gap1 _aic') }, 'product:', Badge({ variant: 'accent', count: product }))
|
|
244
|
+
)
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function StoreDemo() {
|
|
249
|
+
const store = createStore({ name: 'Decantr', version: '0.5.0', stars: 0 });
|
|
250
|
+
const nameEl = span({});
|
|
251
|
+
const versionEl = span({});
|
|
252
|
+
const starsEl = span({});
|
|
253
|
+
|
|
254
|
+
createEffect(() => { nameEl.textContent = store.name; });
|
|
255
|
+
createEffect(() => { versionEl.textContent = store.version; });
|
|
256
|
+
createEffect(() => { starsEl.textContent = String(store.stars); });
|
|
257
|
+
|
|
258
|
+
return div({ class: css('_flex _col _gap4') },
|
|
259
|
+
DemoRow(
|
|
260
|
+
span({ class: css('_fgmutedfg') }, 'name:'), nameEl,
|
|
261
|
+
Separator({ vertical: true }),
|
|
262
|
+
span({ class: css('_fgmutedfg') }, 'version:'), versionEl,
|
|
263
|
+
Separator({ vertical: true }),
|
|
264
|
+
span({ class: css('_fgmutedfg') }, 'stars:'), starsEl
|
|
265
|
+
),
|
|
266
|
+
DemoRow(
|
|
267
|
+
Button({ size: 'sm', variant: 'outline', onclick: () => { store.stars = store.stars + 1; } }, 'Star'),
|
|
268
|
+
Button({ size: 'sm', variant: 'outline', onclick: () => { store.name = 'Decantr Pro'; } }, 'Rename')
|
|
269
|
+
)
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function BatchDemo() {
|
|
274
|
+
const [x, setX] = createSignal(0);
|
|
275
|
+
const [y, setY] = createSignal(0);
|
|
276
|
+
const [effectRuns, setEffectRuns] = createSignal(0);
|
|
277
|
+
const log = LogPanel();
|
|
278
|
+
|
|
279
|
+
createEffect(() => {
|
|
280
|
+
const xv = x();
|
|
281
|
+
const yv = y();
|
|
282
|
+
setEffectRuns(r => r + 1);
|
|
283
|
+
log.log(`Effect fired — x=${xv}, y=${yv}`);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
return div({ class: css('_flex _col _gap4') },
|
|
287
|
+
DemoRow(
|
|
288
|
+
span({ class: css('_fgmutedfg _caption') }, 'x:'), Badge({ variant: 'primary', count: x }),
|
|
289
|
+
span({ class: css('_fgmutedfg _caption') }, 'y:'), Badge({ variant: 'primary', count: y }),
|
|
290
|
+
Separator({ vertical: true }),
|
|
291
|
+
span({ class: css('_caption _fgmutedfg') }, 'Effect runs:'),
|
|
292
|
+
Badge({ variant: 'accent', count: effectRuns })
|
|
293
|
+
),
|
|
294
|
+
DemoRow(
|
|
295
|
+
Button({ size: 'sm', variant: 'outline', onclick: () => {
|
|
296
|
+
setX(x() + 1);
|
|
297
|
+
setY(y() + 1);
|
|
298
|
+
}}, 'Update Both (no batch)'),
|
|
299
|
+
Button({ size: 'sm', variant: 'primary', onclick: () => {
|
|
300
|
+
batch(() => { setX(x() + 1); setY(y() + 1); });
|
|
301
|
+
}}, 'Update Both (batched)')
|
|
302
|
+
),
|
|
303
|
+
div({ class: css('_flex _col _gap2') },
|
|
304
|
+
span({ class: css('_caption _fgmutedfg') }, 'Effect log (newest first):'),
|
|
305
|
+
log.el
|
|
306
|
+
)
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function OnDemo() {
|
|
311
|
+
const [search, setSearch] = createSignal('');
|
|
312
|
+
const [filterMode, setFilterMode] = createSignal('all');
|
|
313
|
+
const log = LogPanel();
|
|
314
|
+
|
|
315
|
+
on(search, (value, prev) => {
|
|
316
|
+
const mode = filterMode();
|
|
317
|
+
log.log(`search changed: "${prev}" → "${value}" (mode: ${mode})`);
|
|
318
|
+
}, { defer: true });
|
|
319
|
+
|
|
320
|
+
const modeDisplay = span({ class: css('_fontmono') });
|
|
321
|
+
createEffect(() => { modeDisplay.textContent = filterMode(); });
|
|
322
|
+
|
|
323
|
+
return div({ class: css('_flex _col _gap4') },
|
|
324
|
+
DemoRow(
|
|
325
|
+
Input({
|
|
326
|
+
placeholder: 'Type to search...',
|
|
327
|
+
size: 'sm',
|
|
328
|
+
oninput: (e) => setSearch(e.target.value)
|
|
329
|
+
}),
|
|
330
|
+
Button({ size: 'sm', variant: 'outline', onclick: () => {
|
|
331
|
+
setFilterMode(filterMode() === 'all' ? 'active' : 'all');
|
|
332
|
+
}}, 'Toggle Mode'),
|
|
333
|
+
span({ class: css('_caption _fgmutedfg') }, 'Mode:'), modeDisplay
|
|
334
|
+
),
|
|
335
|
+
div({ class: css('_flex _col _gap2') },
|
|
336
|
+
span({ class: css('_caption _fgmutedfg') }, 'on(search, ...) log — changing mode does NOT fire:'),
|
|
337
|
+
log.el
|
|
338
|
+
),
|
|
339
|
+
DemoRow(
|
|
340
|
+
Button({ size: 'sm', variant: 'outline', onclick: () => {
|
|
341
|
+
setFilterMode(filterMode() === 'all' ? 'active' : 'all');
|
|
342
|
+
log.log(`(manually toggled mode to "${filterMode()}" — no effect fired)`);
|
|
343
|
+
}}, 'Toggle Mode + Log Proof')
|
|
344
|
+
)
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function HistoryDemo() {
|
|
349
|
+
const signal = createSignal('');
|
|
350
|
+
const [getText, setText] = signal;
|
|
351
|
+
const { undo, redo, canUndo, canRedo, clear } = createHistory(signal);
|
|
352
|
+
|
|
353
|
+
const display = span({ class: css('_heading3 _fontmono') });
|
|
354
|
+
const undoBtn = Button({ variant: 'outline', size: 'sm' }, 'Undo');
|
|
355
|
+
const redoBtn = Button({ variant: 'outline', size: 'sm' }, 'Redo');
|
|
356
|
+
|
|
357
|
+
createEffect(() => { display.textContent = getText() || '(empty)'; });
|
|
358
|
+
createEffect(() => { undoBtn.disabled = !canUndo(); });
|
|
359
|
+
createEffect(() => { redoBtn.disabled = !canRedo(); });
|
|
360
|
+
|
|
361
|
+
undoBtn.addEventListener('click', undo);
|
|
362
|
+
redoBtn.addEventListener('click', redo);
|
|
363
|
+
|
|
364
|
+
return div({ class: css('_flex _col _gap4') },
|
|
365
|
+
div({ class: css('_p4 _bgmuted/10 _radius _b1 _bcborder _flex _col _gap3') },
|
|
366
|
+
span({ class: css('_caption _fgmutedfg') }, 'Current value:'),
|
|
367
|
+
display
|
|
368
|
+
),
|
|
369
|
+
DemoRow(
|
|
370
|
+
Input({
|
|
371
|
+
placeholder: 'Type something...',
|
|
372
|
+
size: 'sm',
|
|
373
|
+
oninput: (e) => setText(e.target.value)
|
|
374
|
+
})
|
|
375
|
+
),
|
|
376
|
+
DemoRow(
|
|
377
|
+
undoBtn,
|
|
378
|
+
redoBtn,
|
|
379
|
+
Button({ variant: 'outline', size: 'sm', onclick: clear }, 'Clear History'),
|
|
380
|
+
Separator({ vertical: true }),
|
|
381
|
+
span({ class: css('_caption _fgmutedfg') }, 'Can undo:'),
|
|
382
|
+
Badge({ variant: 'primary', count: () => canUndo() ? 'Yes' : 'No' }),
|
|
383
|
+
span({ class: css('_caption _fgmutedfg') }, 'Can redo:'),
|
|
384
|
+
Badge({ variant: 'accent', count: () => canRedo() ? 'Yes' : 'No' })
|
|
385
|
+
)
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function RootDemo() {
|
|
390
|
+
const [activeScopes, setActiveScopes] = createSignal(0);
|
|
391
|
+
const container = div({ class: css('_flex _col _gap3') });
|
|
392
|
+
const disposers = [];
|
|
393
|
+
|
|
394
|
+
function spawnScope() {
|
|
395
|
+
const id = Date.now();
|
|
396
|
+
let scopeDispose;
|
|
397
|
+
|
|
398
|
+
createRoot(dispose => {
|
|
399
|
+
scopeDispose = dispose;
|
|
400
|
+
const [count, setCount] = createSignal(0);
|
|
401
|
+
const countDisplay = span({ class: css('_heading5 _fontmono') });
|
|
402
|
+
const statusEl = span({ class: css('_caption') });
|
|
403
|
+
|
|
404
|
+
createEffect(() => { countDisplay.textContent = String(count()); });
|
|
405
|
+
createEffect(() => { statusEl.textContent = 'active'; });
|
|
406
|
+
|
|
407
|
+
const interval = setInterval(() => setCount(c => c + 1), 500);
|
|
408
|
+
|
|
409
|
+
const row = div({ class: css('_flex _gap3 _aic _p3 _bgmuted/10 _radius _b1 _bcborder') },
|
|
410
|
+
Chip({ variant: 'primary', label: `Scope #${disposers.length + 1}` }),
|
|
411
|
+
span({ class: css('_caption _fgmutedfg') }, 'Counter:'),
|
|
412
|
+
countDisplay,
|
|
413
|
+
Badge({ variant: 'success' }, statusEl),
|
|
414
|
+
Button({ size: 'sm', variant: 'outline', onclick: () => {
|
|
415
|
+
clearInterval(interval);
|
|
416
|
+
scopeDispose();
|
|
417
|
+
statusEl.textContent = 'disposed';
|
|
418
|
+
row.style.opacity = '0.4';
|
|
419
|
+
setActiveScopes(n => n - 1);
|
|
420
|
+
}}, 'Dispose')
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
container.appendChild(row);
|
|
424
|
+
disposers.push({ dispose: scopeDispose, interval, row });
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
setActiveScopes(n => n + 1);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return div({ class: css('_flex _col _gap4') },
|
|
431
|
+
DemoRow(
|
|
432
|
+
Button({ variant: 'primary', size: 'sm', onclick: spawnScope }, 'Create Scope'),
|
|
433
|
+
Separator({ vertical: true }),
|
|
434
|
+
span({ class: css('_caption _fgmutedfg') }, 'Active scopes:'),
|
|
435
|
+
Badge({ variant: 'accent', count: activeScopes })
|
|
436
|
+
),
|
|
437
|
+
container
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function SelectorDemo() {
|
|
442
|
+
const items = ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon'];
|
|
443
|
+
const [selected, setSelected] = createSignal('Alpha');
|
|
444
|
+
const isSelected = createSelector(selected);
|
|
445
|
+
|
|
446
|
+
const renderCounts = {};
|
|
447
|
+
items.forEach(item => { renderCounts[item] = { count: 0 }; });
|
|
448
|
+
|
|
449
|
+
const itemEls = items.map(item => {
|
|
450
|
+
const countEl = span({ class: css('_caption _fontmono _fgmutedfg') });
|
|
451
|
+
const itemEl = div({ class: css('_flex _gap2 _aic _p3 _radius _pointer') });
|
|
452
|
+
|
|
453
|
+
createEffect(() => {
|
|
454
|
+
const sel = isSelected(item);
|
|
455
|
+
renderCounts[item].count++;
|
|
456
|
+
countEl.textContent = `renders: ${renderCounts[item].count}`;
|
|
457
|
+
itemEl.className = css(sel
|
|
458
|
+
? '_flex _gap2 _aic _p3 _radius _pointer _bgprimary _fgprimaryon'
|
|
459
|
+
: '_flex _gap2 _aic _p3 _radius _pointer _bgmuted/10 _fgfg'
|
|
460
|
+
);
|
|
461
|
+
itemEl.textContent = '';
|
|
462
|
+
itemEl.appendChild(span({ class: css('_body _fw600') }, item));
|
|
463
|
+
itemEl.appendChild(countEl);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
itemEl.addEventListener('click', () => setSelected(item));
|
|
467
|
+
return itemEl;
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
return div({ class: css('_flex _col _gap4') },
|
|
471
|
+
div({ class: css('_flex _gap2 _wrap') }, ...itemEls),
|
|
472
|
+
DemoRow(
|
|
473
|
+
span({ class: css('_caption _fgmutedfg') }, 'Selected:'),
|
|
474
|
+
Badge({ variant: 'primary', count: selected }),
|
|
475
|
+
span({ class: css('_caption _fgmutedfg') }, '— click an item. Only prev+current re-render.')
|
|
476
|
+
)
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function ContextDemo() {
|
|
481
|
+
const ThemeCtx = createContext('light');
|
|
482
|
+
const [activeTheme, setActiveTheme] = createSignal('(none)');
|
|
483
|
+
|
|
484
|
+
function readAndDisplay(label) {
|
|
485
|
+
const value = ThemeCtx.consume();
|
|
486
|
+
const el = div({ class: css('_p3 _radius _b1 _bcborder _flex _gap2 _aic') },
|
|
487
|
+
span({ class: css('_caption _fgmutedfg') }, label + ':'),
|
|
488
|
+
Chip({ variant: value === 'dark' ? 'accent' : 'primary', label: value })
|
|
489
|
+
);
|
|
490
|
+
if (value === 'dark') {
|
|
491
|
+
el.style.background = 'var(--d-surface-2)';
|
|
492
|
+
} else {
|
|
493
|
+
el.style.background = 'var(--d-surface-0)';
|
|
494
|
+
}
|
|
495
|
+
return el;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const outerBox = div({ class: css('_flex _col _gap3 _p4 _bgmuted/10 _radius _b1 _bcborder') });
|
|
499
|
+
const innerBox = div({ class: css('_flex _col _gap3 _p4 _radius _b1 _bcborder') });
|
|
500
|
+
|
|
501
|
+
function rebuild() {
|
|
502
|
+
outerBox.textContent = '';
|
|
503
|
+
innerBox.textContent = '';
|
|
504
|
+
|
|
505
|
+
// Outer: provide 'light'
|
|
506
|
+
const restoreOuter = ThemeCtx.Provider('light');
|
|
507
|
+
outerBox.appendChild(span({ class: css('_caption _fgmutedfg _fw600') }, 'Outer Provider: light'));
|
|
508
|
+
outerBox.appendChild(readAndDisplay('consume()'));
|
|
509
|
+
|
|
510
|
+
// Inner: provide 'dark'
|
|
511
|
+
const restoreInner = ThemeCtx.Provider('dark');
|
|
512
|
+
innerBox.appendChild(span({ class: css('_caption _fgmutedfg _fw600') }, 'Inner Provider: dark'));
|
|
513
|
+
innerBox.appendChild(readAndDisplay('consume()'));
|
|
514
|
+
innerBox.style.background = 'var(--d-surface-2)';
|
|
515
|
+
restoreInner();
|
|
516
|
+
|
|
517
|
+
outerBox.appendChild(innerBox);
|
|
518
|
+
outerBox.appendChild(readAndDisplay('After inner (restored)'));
|
|
519
|
+
restoreOuter();
|
|
520
|
+
|
|
521
|
+
setActiveTheme('demo rendered');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
rebuild();
|
|
525
|
+
|
|
526
|
+
return div({ class: css('_flex _col _gap4') },
|
|
527
|
+
outerBox,
|
|
528
|
+
DemoRow(
|
|
529
|
+
Button({ size: 'sm', variant: 'outline', onclick: rebuild }, 'Re-render Demo'),
|
|
530
|
+
span({ class: css('_caption _fgmutedfg') }, 'Nested providers override for their subtree only.')
|
|
531
|
+
)
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function EffectCleanupDemo() {
|
|
536
|
+
const [useA, setUseA] = createSignal(true);
|
|
537
|
+
const [a, setA] = createSignal('Alpha');
|
|
538
|
+
const [b, setB] = createSignal('Beta');
|
|
539
|
+
const log = LogPanel();
|
|
540
|
+
|
|
541
|
+
createEffect(() => {
|
|
542
|
+
const reading = useA() ? a() : b();
|
|
543
|
+
const source = useA() ? 'A' : 'B';
|
|
544
|
+
log.log(`Effect ran — reading ${source}: "${reading}"`);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
const branchDisplay = span({ class: css('_fontmono _fw600') });
|
|
548
|
+
createEffect(() => { branchDisplay.textContent = useA() ? 'Signal A' : 'Signal B'; });
|
|
549
|
+
|
|
550
|
+
return div({ class: css('_flex _col _gap4') },
|
|
551
|
+
DemoRow(
|
|
552
|
+
span({ class: css('_caption _fgmutedfg') }, 'Reading:'), branchDisplay,
|
|
553
|
+
Separator({ vertical: true }),
|
|
554
|
+
Button({ size: 'sm', variant: 'primary', onclick: () => setUseA(!useA()) }, 'Toggle Branch')
|
|
555
|
+
),
|
|
556
|
+
DemoRow(
|
|
557
|
+
Button({ size: 'sm', variant: 'outline', onclick: () => setA(a() + '+') }, 'Mutate A'),
|
|
558
|
+
Button({ size: 'sm', variant: 'outline', onclick: () => setB(b() + '+') }, 'Mutate B'),
|
|
559
|
+
span({ class: css('_caption _fgmutedfg') }, 'Only the active branch triggers the effect.')
|
|
560
|
+
),
|
|
561
|
+
div({ class: css('_flex _col _gap2') },
|
|
562
|
+
span({ class: css('_caption _fgmutedfg') }, 'Effect log (newest first):'),
|
|
563
|
+
log.el
|
|
564
|
+
)
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ─── Data Demos ─────────────────────────────────────────────
|
|
569
|
+
|
|
570
|
+
function DataDemo() {
|
|
571
|
+
return div({ class: css('_flex _col _gap6') },
|
|
572
|
+
DemoSection(
|
|
573
|
+
'createQuery — Server State (Simulated)',
|
|
574
|
+
'Manages async data fetching with loading/error/success states. Simulated with setTimeout.',
|
|
575
|
+
QueryDemo(),
|
|
576
|
+
CodeBlock({ language: 'javascript' },
|
|
577
|
+
`import { createQuery } from 'decantr/data';\n\nconst users = createQuery({\n key: 'users',\n fn: () => fetch('/api/users').then(r => r.json()),\n staleTime: 30_000\n});\n\n// users.data() — resolved value\n// users.loading() — boolean\n// users.error() — Error | null\n// users.refetch() — manual refresh`
|
|
578
|
+
)
|
|
579
|
+
),
|
|
580
|
+
DemoSection(
|
|
581
|
+
'createEntityStore — Entity Management',
|
|
582
|
+
'Normalized collection with O(1) lookups, per-entity reactivity, and derived views.',
|
|
583
|
+
EntityDemo(),
|
|
584
|
+
CodeBlock({ language: 'javascript' },
|
|
585
|
+
`import { createEntityStore } from 'decantr/data';\n\nconst users = createEntityStore({ getId: u => u.id });\nusers.addMany([{ id: '1', name: 'Alice' }]);\n\nconst alice = users.get('1'); // per-entity memo\nconst active = users.filter(u => u.active);\nconst sorted = users.sorted((a, b) => a.name.localeCompare(b.name));`
|
|
586
|
+
)
|
|
587
|
+
),
|
|
588
|
+
DemoSection(
|
|
589
|
+
'withMiddleware — Signal Middleware',
|
|
590
|
+
'Intercept signal reads/writes with composable middleware. Validation, undo, logging.',
|
|
591
|
+
MiddlewareDemo(),
|
|
592
|
+
CodeBlock({ language: 'javascript' },
|
|
593
|
+
`import { withMiddleware, validationMiddleware, undoMiddleware } from 'decantr/state/middleware';\n\nconst undo = undoMiddleware({ maxLength: 50 });\nconst [value, setValue] = withMiddleware(\n createSignal(50),\n [\n validationMiddleware(v => v >= 0 && v <= 100 ? true : 'Out of range'),\n undo.middleware\n ]\n);\n\nsetValue(150); // rejected — stays at 50\nundo.undo(); // time travel`
|
|
594
|
+
)
|
|
595
|
+
)
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function QueryDemo() {
|
|
600
|
+
const [data, setData] = createSignal(null);
|
|
601
|
+
const [loading, setLoading] = createSignal(false);
|
|
602
|
+
const [error, setError] = createSignal(null);
|
|
603
|
+
const [errorMode, setErrorMode] = createSignal(false);
|
|
604
|
+
const [fetchCount, setFetchCount] = createSignal(0);
|
|
605
|
+
|
|
606
|
+
function simulateFetch() {
|
|
607
|
+
setLoading(true);
|
|
608
|
+
setError(null);
|
|
609
|
+
setData(null);
|
|
610
|
+
setFetchCount(c => c + 1);
|
|
611
|
+
|
|
612
|
+
setTimeout(() => {
|
|
613
|
+
if (errorMode()) {
|
|
614
|
+
setLoading(false);
|
|
615
|
+
setError('NetworkError: simulated failure');
|
|
616
|
+
} else {
|
|
617
|
+
setLoading(false);
|
|
618
|
+
setData({
|
|
619
|
+
users: [
|
|
620
|
+
{ id: 1, name: 'Alice Chen', role: 'Engineer' },
|
|
621
|
+
{ id: 2, name: 'Bob Park', role: 'Designer' },
|
|
622
|
+
{ id: 3, name: 'Carol Wu', role: 'PM' }
|
|
623
|
+
],
|
|
624
|
+
fetchedAt: new Date().toLocaleTimeString()
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
}, 800);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Initial fetch
|
|
631
|
+
simulateFetch();
|
|
632
|
+
|
|
633
|
+
const statusEl = div({});
|
|
634
|
+
const dataEl = div({});
|
|
635
|
+
|
|
636
|
+
createEffect(() => {
|
|
637
|
+
statusEl.textContent = '';
|
|
638
|
+
if (loading()) {
|
|
639
|
+
statusEl.appendChild(
|
|
640
|
+
div({ class: css('_flex _gap2 _aic') },
|
|
641
|
+
Badge({ variant: 'warning' }, 'Loading...'),
|
|
642
|
+
span({ class: css('_caption _fgmutedfg _fontmono') }, 'Fetching data...')
|
|
643
|
+
)
|
|
644
|
+
);
|
|
645
|
+
} else if (error()) {
|
|
646
|
+
statusEl.appendChild(
|
|
647
|
+
div({ class: css('_flex _gap2 _aic') },
|
|
648
|
+
Badge({ variant: 'error' }, 'Error'),
|
|
649
|
+
span({ class: css('_caption _fgerror _fontmono') }, error())
|
|
650
|
+
)
|
|
651
|
+
);
|
|
652
|
+
} else {
|
|
653
|
+
statusEl.appendChild(
|
|
654
|
+
Badge({ variant: 'success' }, 'Success')
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
createEffect(() => {
|
|
660
|
+
dataEl.textContent = '';
|
|
661
|
+
const d = data();
|
|
662
|
+
if (d) {
|
|
663
|
+
const rows = d.users.map(u =>
|
|
664
|
+
div({ class: css('_flex _gap3 _aic _p2 _bgmuted/10 _radius') },
|
|
665
|
+
Chip({ variant: 'primary', label: u.name }),
|
|
666
|
+
span({ class: css('_caption _fgmutedfg') }, u.role)
|
|
667
|
+
)
|
|
668
|
+
);
|
|
669
|
+
dataEl.appendChild(
|
|
670
|
+
div({ class: css('_flex _col _gap2') },
|
|
671
|
+
...rows,
|
|
672
|
+
span({ class: css('_caption _fgmutedfg _fontmono') }, `Fetched at: ${d.fetchedAt}`)
|
|
673
|
+
)
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
return div({ class: css('_flex _col _gap4') },
|
|
679
|
+
div({ class: css('_p4 _bgmuted/10 _radius _b1 _bcborder _flex _col _gap3') },
|
|
680
|
+
statusEl,
|
|
681
|
+
dataEl
|
|
682
|
+
),
|
|
683
|
+
DemoRow(
|
|
684
|
+
Button({ size: 'sm', variant: 'primary', onclick: simulateFetch }, 'Refetch'),
|
|
685
|
+
Button({ size: 'sm', variant: errorMode() ? 'error' : 'outline', onclick: () => setErrorMode(!errorMode()) }, 'Toggle Error Mode'),
|
|
686
|
+
Separator({ vertical: true }),
|
|
687
|
+
span({ class: css('_caption _fgmutedfg') }, 'Fetches:'),
|
|
688
|
+
Badge({ variant: 'accent', count: fetchCount })
|
|
689
|
+
)
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function EntityDemo() {
|
|
694
|
+
const users = createEntityStore({ getId: u => u.id });
|
|
695
|
+
const [filterText, setFilterText] = createSignal('');
|
|
696
|
+
const [sortAsc, setSortAsc] = createSignal(true);
|
|
697
|
+
let nextId = 6;
|
|
698
|
+
|
|
699
|
+
// Seed data
|
|
700
|
+
users.addMany([
|
|
701
|
+
{ id: '1', name: 'Alice Chen', role: 'Engineer' },
|
|
702
|
+
{ id: '2', name: 'Bob Park', role: 'Designer' },
|
|
703
|
+
{ id: '3', name: 'Carol Wu', role: 'PM' },
|
|
704
|
+
{ id: '4', name: 'David Kim', role: 'Engineer' },
|
|
705
|
+
{ id: '5', name: 'Eve Singh', role: 'Designer' }
|
|
706
|
+
]);
|
|
707
|
+
|
|
708
|
+
const filtered = users.filter(u => {
|
|
709
|
+
const ft = filterText();
|
|
710
|
+
if (!ft) return true;
|
|
711
|
+
return u.name.toLowerCase().includes(ft.toLowerCase());
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
const sortedFiltered = createMemo(() => {
|
|
715
|
+
const items = filtered();
|
|
716
|
+
const asc = sortAsc();
|
|
717
|
+
return [...items].sort((a, b) => asc
|
|
718
|
+
? a.name.localeCompare(b.name)
|
|
719
|
+
: b.name.localeCompare(a.name)
|
|
720
|
+
);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
const listEl = div({ class: css('_flex _col _gap2') });
|
|
724
|
+
|
|
725
|
+
createEffect(() => {
|
|
726
|
+
listEl.textContent = '';
|
|
727
|
+
const items = sortedFiltered();
|
|
728
|
+
for (const user of items) {
|
|
729
|
+
listEl.appendChild(
|
|
730
|
+
div({ class: css('_flex _gap3 _aic _p2 _bgmuted/10 _radius') },
|
|
731
|
+
Chip({ variant: 'primary', label: user.name }),
|
|
732
|
+
span({ class: css('_caption _fgmutedfg') }, user.role),
|
|
733
|
+
Button({ size: 'sm', variant: 'outline', onclick: () => {
|
|
734
|
+
users.update(user.id, { role: user.role === 'Engineer' ? 'Lead' : 'Engineer' });
|
|
735
|
+
}}, 'Toggle Role'),
|
|
736
|
+
Button({ size: 'sm', variant: 'outline', onclick: () => {
|
|
737
|
+
users.remove(user.id);
|
|
738
|
+
}}, 'Remove')
|
|
739
|
+
)
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
if (items.length === 0) {
|
|
743
|
+
listEl.appendChild(span({ class: css('_caption _fgmutedfg _p3') }, 'No matching entities.'));
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
const names = ['Fay Li', 'Gus Roy', 'Hana Ito', 'Ivan Popov', 'Jade Moon', 'Kai Xu'];
|
|
748
|
+
const roles = ['Engineer', 'Designer', 'PM', 'QA'];
|
|
749
|
+
|
|
750
|
+
return div({ class: css('_flex _col _gap4') },
|
|
751
|
+
DemoRow(
|
|
752
|
+
span({ class: css('_caption _fgmutedfg') }, 'Count:'),
|
|
753
|
+
Badge({ variant: 'accent', count: users.count }),
|
|
754
|
+
Separator({ vertical: true }),
|
|
755
|
+
Input({
|
|
756
|
+
placeholder: 'Filter by name...',
|
|
757
|
+
size: 'sm',
|
|
758
|
+
oninput: (e) => setFilterText(e.target.value)
|
|
759
|
+
}),
|
|
760
|
+
Button({ size: 'sm', variant: 'outline', onclick: () => setSortAsc(!sortAsc()) },
|
|
761
|
+
() => sortAsc() ? 'Sort A→Z' : 'Sort Z→A'
|
|
762
|
+
)
|
|
763
|
+
),
|
|
764
|
+
div({ class: css('_p4 _bgmuted/10 _radius _b1 _bcborder') }, listEl),
|
|
765
|
+
DemoRow(
|
|
766
|
+
Button({ size: 'sm', variant: 'primary', onclick: () => {
|
|
767
|
+
const name = names[nextId % names.length];
|
|
768
|
+
const role = roles[nextId % roles.length];
|
|
769
|
+
users.upsert({ id: String(nextId++), name, role });
|
|
770
|
+
}}, 'Add Entity'),
|
|
771
|
+
Button({ size: 'sm', variant: 'outline', onclick: () => users.clear() }, 'Clear All')
|
|
772
|
+
)
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function MiddlewareDemo() {
|
|
777
|
+
const undoCtrl = undoMiddleware({ maxLength: 50 });
|
|
778
|
+
const [rejections, setRejections] = createSignal(0);
|
|
779
|
+
const [lastRejected, setLastRejected] = createSignal('');
|
|
780
|
+
|
|
781
|
+
const [value, setValue] = withMiddleware(
|
|
782
|
+
createSignal(50),
|
|
783
|
+
[
|
|
784
|
+
validationMiddleware(
|
|
785
|
+
v => (typeof v === 'number' && v >= 0 && v <= 100) ? true : `Out of range: ${v}`,
|
|
786
|
+
{ onError: (err, v) => { setRejections(r => r + 1); setLastRejected(String(v)); } }
|
|
787
|
+
),
|
|
788
|
+
undoCtrl.middleware
|
|
789
|
+
]
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
const display = span({ class: css('_heading3 _fontmono') });
|
|
793
|
+
const rangeBar = div({ class: css('_radius _h[8px] _bgprimary') });
|
|
794
|
+
|
|
795
|
+
createEffect(() => {
|
|
796
|
+
const v = value();
|
|
797
|
+
display.textContent = String(v);
|
|
798
|
+
rangeBar.style.width = v + '%';
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
const undoBtn = Button({ variant: 'outline', size: 'sm' }, 'Undo');
|
|
802
|
+
const redoBtn = Button({ variant: 'outline', size: 'sm' }, 'Redo');
|
|
803
|
+
createEffect(() => { undoBtn.disabled = !undoCtrl.canUndo(); });
|
|
804
|
+
createEffect(() => { redoBtn.disabled = !undoCtrl.canRedo(); });
|
|
805
|
+
undoBtn.addEventListener('click', () => undoCtrl.undo());
|
|
806
|
+
redoBtn.addEventListener('click', () => undoCtrl.redo());
|
|
807
|
+
|
|
808
|
+
return div({ class: css('_flex _col _gap4') },
|
|
809
|
+
div({ class: css('_p4 _bgmuted/10 _radius _b1 _bcborder _flex _col _gap3') },
|
|
810
|
+
DemoRow(
|
|
811
|
+
span({ class: css('_caption _fgmutedfg') }, 'Value (0–100):'),
|
|
812
|
+
display
|
|
813
|
+
),
|
|
814
|
+
div({ class: css('_bgmuted/10 _radius _h[8px] _w[100%]') }, rangeBar)
|
|
815
|
+
),
|
|
816
|
+
DemoRow(
|
|
817
|
+
Button({ size: 'sm', variant: 'outline', onclick: () => setValue(v => v - 10) }, '-10'),
|
|
818
|
+
Button({ size: 'sm', variant: 'outline', onclick: () => setValue(v => v + 10) }, '+10'),
|
|
819
|
+
Button({ size: 'sm', variant: 'outline', onclick: () => setValue(150) }, 'Set 150 (rejected)'),
|
|
820
|
+
Button({ size: 'sm', variant: 'outline', onclick: () => setValue(-20) }, 'Set -20 (rejected)')
|
|
821
|
+
),
|
|
822
|
+
DemoRow(
|
|
823
|
+
undoBtn,
|
|
824
|
+
redoBtn,
|
|
825
|
+
Separator({ vertical: true }),
|
|
826
|
+
span({ class: css('_caption _fgmutedfg') }, 'Rejections:'),
|
|
827
|
+
Badge({ variant: 'error', count: rejections }),
|
|
828
|
+
cond(
|
|
829
|
+
() => lastRejected() !== '',
|
|
830
|
+
() => span({ class: css('_caption _fgmutedfg') }, () => `Last rejected: ${lastRejected()}`)
|
|
831
|
+
)
|
|
832
|
+
)
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// ─── Router Demo ────────────────────────────────────────────
|
|
837
|
+
|
|
838
|
+
function RouterDemo() {
|
|
839
|
+
return div({ class: css('_flex _col _gap6') },
|
|
840
|
+
DemoSection(
|
|
841
|
+
'Client-Side Routing',
|
|
842
|
+
'Hash and History mode routing with nested routes, guards, and lazy loading.',
|
|
843
|
+
p({ class: css('_body _fgmutedfg') }, 'Router demos require full page context. The workbench itself uses hash routing.'),
|
|
844
|
+
CodeBlock({ language: 'javascript' },
|
|
845
|
+
`import { createRouter, navigate, link, useRoute } from 'decantr/router';\n\nconst router = createRouter({\n mode: 'hash',\n routes: [\n { path: '/', component: HomePage },\n { path: '/about', component: AboutPage },\n { path: '/user/:id', component: UserPage }\n ]\n});\n\n// Programmatic navigation\nnavigate('/about');\n\n// Route-aware links\nconst nav = link('/about', { class: 'nav-link' }, 'About');`
|
|
846
|
+
)
|
|
847
|
+
)
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// ─── Forms Demo ─────────────────────────────────────────────
|
|
852
|
+
|
|
853
|
+
function FormsDemo() {
|
|
854
|
+
return div({ class: css('_flex _col _gap6') },
|
|
855
|
+
DemoSection(
|
|
856
|
+
'Form System',
|
|
857
|
+
'createForm with validators, field-level reactivity, and fieldArray for dynamic fields.',
|
|
858
|
+
FormsInteractiveDemo(),
|
|
859
|
+
CodeBlock({ language: 'javascript' },
|
|
860
|
+
`import { createForm, validators } from 'decantr/form';\n\nconst form = createForm({\n fields: {\n email: { validators: [validators.required, validators.email] },\n password: { validators: [validators.required, validators.minLength(8)] }\n },\n onSubmit: (values) => console.log(values)\n});\n\n// Get field state\nform.field('email'); // { value, error, touched, dirty }`
|
|
861
|
+
)
|
|
862
|
+
)
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function FormsInteractiveDemo() {
|
|
867
|
+
const [submitted, setSubmitted] = createSignal('');
|
|
868
|
+
|
|
869
|
+
const form = createForm({
|
|
870
|
+
fields: {
|
|
871
|
+
email: { validators: [validators.required, validators.email] },
|
|
872
|
+
name: { validators: [validators.required] }
|
|
873
|
+
},
|
|
874
|
+
onSubmit: (values) => { setSubmitted(JSON.stringify(values, null, 2)); }
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
const emailField = form.field('email');
|
|
878
|
+
const nameField = form.field('name');
|
|
879
|
+
|
|
880
|
+
const emailError = span({ class: css('_caption _fgerror') });
|
|
881
|
+
const nameError = span({ class: css('_caption _fgerror') });
|
|
882
|
+
const resultEl = span({ class: css('_caption _fgprimary') });
|
|
883
|
+
|
|
884
|
+
createEffect(() => { emailError.textContent = emailField.error() || ''; });
|
|
885
|
+
createEffect(() => { nameError.textContent = nameField.error() || ''; });
|
|
886
|
+
createEffect(() => { resultEl.textContent = submitted(); });
|
|
887
|
+
|
|
888
|
+
return div({ class: css('_flex _col _gap4') },
|
|
889
|
+
div({ class: css('_flex _col _gap1') },
|
|
890
|
+
label({ class: css('_caption _fgmutedfg') }, 'Name'),
|
|
891
|
+
Input({
|
|
892
|
+
placeholder: 'Enter name...',
|
|
893
|
+
value: nameField.value,
|
|
894
|
+
oninput: (e) => nameField.setValue(e.target.value),
|
|
895
|
+
ref: (el) => { el.addEventListener('blur', () => nameField.setTouched(true)); }
|
|
896
|
+
}),
|
|
897
|
+
nameError
|
|
898
|
+
),
|
|
899
|
+
div({ class: css('_flex _col _gap1') },
|
|
900
|
+
label({ class: css('_caption _fgmutedfg') }, 'Email'),
|
|
901
|
+
Input({
|
|
902
|
+
placeholder: 'Enter email...',
|
|
903
|
+
value: emailField.value,
|
|
904
|
+
oninput: (e) => emailField.setValue(e.target.value),
|
|
905
|
+
ref: (el) => { el.addEventListener('blur', () => emailField.setTouched(true)); }
|
|
906
|
+
}),
|
|
907
|
+
emailError
|
|
908
|
+
),
|
|
909
|
+
DemoRow(
|
|
910
|
+
Button({ variant: 'primary', size: 'sm', onclick: () => form.submit() }, 'Submit'),
|
|
911
|
+
Button({ variant: 'outline', size: 'sm', onclick: () => form.reset() }, 'Reset')
|
|
912
|
+
),
|
|
913
|
+
submitted() ? div({ class: css('_p3 _bgmuted/10 _radius _caption _fgmutedfg') }, 'Submitted: ', resultEl) : null
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// ─── Subsection Registry ────────────────────────────────────
|
|
918
|
+
|
|
919
|
+
const SUBSECTIONS = {
|
|
920
|
+
core: { label: 'Core', render: CoreDemo },
|
|
921
|
+
state: { label: 'State', render: StateDemo },
|
|
922
|
+
data: { label: 'Data', render: DataDemo },
|
|
923
|
+
router: { label: 'Router', render: RouterDemo },
|
|
924
|
+
forms: { label: 'Forms', render: FormsDemo },
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
export function FoundationsExplorer(subsection) {
|
|
928
|
+
const sub = SUBSECTIONS[subsection] || SUBSECTIONS.core;
|
|
929
|
+
return section({ id: 'foundations-' + subsection, class: css('_flex _col _gap6') },
|
|
930
|
+
div({ class: css('_flex _col _gap1') },
|
|
931
|
+
h2({ class: css('_heading4') }, `Foundations — ${sub.label}`),
|
|
932
|
+
p({ class: css('_body _fgmutedfg') }, 'Interactive API playgrounds for Decantr primitives.')
|
|
933
|
+
),
|
|
934
|
+
sub.render()
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
export async function loadFoundationItems() {
|
|
939
|
+
try {
|
|
940
|
+
const resp = await fetch('/__decantr/registry/foundations.json');
|
|
941
|
+
const data = await resp.json();
|
|
942
|
+
return Object.entries(data.subsections || {}).map(([id, sub]) => ({
|
|
943
|
+
id, label: sub.label
|
|
944
|
+
}));
|
|
945
|
+
} catch {
|
|
946
|
+
// Fallback to local subsections
|
|
947
|
+
return Object.entries(SUBSECTIONS).map(([id, sub]) => ({ id, label: sub.label }));
|
|
948
|
+
}
|
|
949
|
+
}
|