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,281 @@
|
|
|
1
|
+
import { createSignal, createMemo, batch } from '../state/index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @template T
|
|
5
|
+
* @template {string|number} ID
|
|
6
|
+
* @typedef {Object} EntityStore
|
|
7
|
+
* @property {(entities: T[]) => void} addMany
|
|
8
|
+
* @property {(entity: T) => void} upsert
|
|
9
|
+
* @property {(id: ID, partial: Partial<T>) => void} update
|
|
10
|
+
* @property {(id: ID) => void} remove
|
|
11
|
+
* @property {() => void} clear
|
|
12
|
+
* @property {(id: ID) => () => T|undefined} get
|
|
13
|
+
* @property {() => T[]} all
|
|
14
|
+
* @property {() => number} count
|
|
15
|
+
* @property {(predicate: (entity: T) => boolean) => () => T[]} filter
|
|
16
|
+
* @property {(comparator: (a: T, b: T) => number) => () => T[]} sorted
|
|
17
|
+
* @property {(opts: { page: () => number, size: () => number }) => () => T[]} paginated
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @template T
|
|
22
|
+
* @template {string|number} [ID=string]
|
|
23
|
+
* @typedef {Object} EntityStoreOptions
|
|
24
|
+
* @property {(entity: T) => ID} getId - Extract the unique identifier from an entity
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a normalized entity collection store with per-entity reactivity.
|
|
29
|
+
*
|
|
30
|
+
* Entities are stored in a `Map<ID, T>` for O(1) lookups. A collection-level
|
|
31
|
+
* version signal drives derived views (`all`, `count`, `filter`, `sorted`,
|
|
32
|
+
* `paginated`). Per-entity signals are lazily created on first `.get(id)` call
|
|
33
|
+
* so that fine-grained subscriptions only pay for what they use.
|
|
34
|
+
*
|
|
35
|
+
* @template T
|
|
36
|
+
* @template {string|number} [ID=string]
|
|
37
|
+
* @param {EntityStoreOptions<T, ID>} options
|
|
38
|
+
* @returns {EntityStore<T, ID>}
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```js
|
|
42
|
+
* const users = createEntityStore({ getId: u => u.id });
|
|
43
|
+
* users.addMany([{ id: '1', name: 'Alice' }, { id: '2', name: 'Bob' }]);
|
|
44
|
+
* const alice = users.get('1'); // memo — only re-runs when entity '1' changes
|
|
45
|
+
* console.log(alice().name); // 'Alice'
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function createEntityStore(options) {
|
|
49
|
+
const { getId } = options;
|
|
50
|
+
|
|
51
|
+
// ── Internal storage ──────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/** @type {Map<ID, T>} */
|
|
54
|
+
const entities = new Map();
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Collection-level version counter. Incremented on any structural change
|
|
58
|
+
* (add, remove, clear). Derived memos read this to know when to recompute.
|
|
59
|
+
*/
|
|
60
|
+
const [version, setVersion] = createSignal(0);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Per-entity signal cache. Lazily populated by `.get(id)`.
|
|
64
|
+
* Each entry is a `[getter, setter]` tuple from `createSignal`.
|
|
65
|
+
* @type {Map<ID, [() => T|undefined, (v: T|undefined) => void]>}
|
|
66
|
+
*/
|
|
67
|
+
const entitySignals = new Map();
|
|
68
|
+
|
|
69
|
+
// ── Helpers ───────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
/** Bump the collection version so all derived views recompute. */
|
|
72
|
+
function bump() {
|
|
73
|
+
setVersion(v => v + 1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Notify the per-entity signal for `id`, if one exists.
|
|
78
|
+
* @param {ID} id
|
|
79
|
+
* @param {T|undefined} value
|
|
80
|
+
*/
|
|
81
|
+
function notifyEntity(id, value) {
|
|
82
|
+
const sig = entitySignals.get(id);
|
|
83
|
+
if (sig) sig[1](value);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Internal: set a single entity in the map and notify.
|
|
88
|
+
* Returns true if this was a new insertion (structural change).
|
|
89
|
+
* @param {T} entity
|
|
90
|
+
* @returns {boolean}
|
|
91
|
+
*/
|
|
92
|
+
function setEntity(entity) {
|
|
93
|
+
const id = getId(entity);
|
|
94
|
+
const existed = entities.has(id);
|
|
95
|
+
entities.set(id, entity);
|
|
96
|
+
notifyEntity(id, entity);
|
|
97
|
+
return !existed;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Public API ────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Batch-add multiple entities. Entities with duplicate IDs overwrite
|
|
104
|
+
* existing entries (upsert semantics).
|
|
105
|
+
* @param {T[]} items
|
|
106
|
+
*/
|
|
107
|
+
function addMany(items) {
|
|
108
|
+
if (items.length === 0) return;
|
|
109
|
+
batch(() => {
|
|
110
|
+
for (let i = 0; i < items.length; i++) {
|
|
111
|
+
setEntity(items[i]);
|
|
112
|
+
}
|
|
113
|
+
bump();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Add or replace a single entity.
|
|
119
|
+
* @param {T} entity
|
|
120
|
+
*/
|
|
121
|
+
function upsert(entity) {
|
|
122
|
+
batch(() => {
|
|
123
|
+
setEntity(entity);
|
|
124
|
+
bump();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Shallow-merge a partial update into an existing entity.
|
|
130
|
+
* No-op if the entity does not exist.
|
|
131
|
+
* @param {ID} id
|
|
132
|
+
* @param {Partial<T>} partial
|
|
133
|
+
*/
|
|
134
|
+
function update(id, partial) {
|
|
135
|
+
const existing = entities.get(id);
|
|
136
|
+
if (existing === undefined) return;
|
|
137
|
+
/** @type {T} */
|
|
138
|
+
const merged = { ...existing, ...partial };
|
|
139
|
+
batch(() => {
|
|
140
|
+
entities.set(id, merged);
|
|
141
|
+
notifyEntity(id, merged);
|
|
142
|
+
bump();
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Remove an entity by ID. No-op if the entity does not exist.
|
|
148
|
+
* @param {ID} id
|
|
149
|
+
*/
|
|
150
|
+
function remove(id) {
|
|
151
|
+
if (!entities.has(id)) return;
|
|
152
|
+
batch(() => {
|
|
153
|
+
entities.delete(id);
|
|
154
|
+
notifyEntity(id, undefined);
|
|
155
|
+
bump();
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Remove all entities.
|
|
161
|
+
*/
|
|
162
|
+
function clear() {
|
|
163
|
+
if (entities.size === 0) return;
|
|
164
|
+
batch(() => {
|
|
165
|
+
// Notify all existing per-entity signals
|
|
166
|
+
for (const [id] of entitySignals) {
|
|
167
|
+
notifyEntity(id, undefined);
|
|
168
|
+
}
|
|
169
|
+
entities.clear();
|
|
170
|
+
bump();
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get a per-entity memo. Returns a reactive getter that only recomputes
|
|
176
|
+
* when THIS specific entity changes — not when the collection changes.
|
|
177
|
+
*
|
|
178
|
+
* Signals are lazily created and cached in a Map. Subsequent calls with
|
|
179
|
+
* the same `id` return the same memo.
|
|
180
|
+
*
|
|
181
|
+
* @param {ID} id
|
|
182
|
+
* @returns {() => T|undefined}
|
|
183
|
+
*/
|
|
184
|
+
function get(id) {
|
|
185
|
+
let sig = entitySignals.get(id);
|
|
186
|
+
if (!sig) {
|
|
187
|
+
sig = createSignal(entities.get(id));
|
|
188
|
+
entitySignals.set(id, sig);
|
|
189
|
+
}
|
|
190
|
+
// Wrap in a memo so downstream effects only fire on value change
|
|
191
|
+
return createMemo(() => {
|
|
192
|
+
const s = entitySignals.get(id);
|
|
193
|
+
return s ? s[0]() : undefined;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Reactive getter: all entities as an array (snapshot order matches
|
|
199
|
+
* insertion order of the underlying Map).
|
|
200
|
+
* @type {() => T[]}
|
|
201
|
+
*/
|
|
202
|
+
const all = createMemo(() => {
|
|
203
|
+
version(); // subscribe to structural changes
|
|
204
|
+
return Array.from(entities.values());
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Reactive getter: entity count.
|
|
209
|
+
* @type {() => number}
|
|
210
|
+
*/
|
|
211
|
+
const count = createMemo(() => {
|
|
212
|
+
version();
|
|
213
|
+
return entities.size;
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Create a filtered derived view. The returned memo recomputes whenever
|
|
218
|
+
* the collection changes structurally.
|
|
219
|
+
*
|
|
220
|
+
* @param {(entity: T) => boolean} predicate
|
|
221
|
+
* @returns {() => T[]}
|
|
222
|
+
*/
|
|
223
|
+
function filter(predicate) {
|
|
224
|
+
return createMemo(() => {
|
|
225
|
+
version();
|
|
226
|
+
/** @type {T[]} */
|
|
227
|
+
const result = [];
|
|
228
|
+
for (const entity of entities.values()) {
|
|
229
|
+
if (predicate(entity)) result.push(entity);
|
|
230
|
+
}
|
|
231
|
+
return result;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Create a sorted derived view. The returned memo recomputes whenever
|
|
237
|
+
* the collection changes structurally.
|
|
238
|
+
*
|
|
239
|
+
* @param {(a: T, b: T) => number} comparator
|
|
240
|
+
* @returns {() => T[]}
|
|
241
|
+
*/
|
|
242
|
+
function sorted(comparator) {
|
|
243
|
+
return createMemo(() => {
|
|
244
|
+
version();
|
|
245
|
+
return Array.from(entities.values()).sort(comparator);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Create a paginated derived view. `page` is 1-indexed. Returns an
|
|
251
|
+
* empty array if the page is out of range.
|
|
252
|
+
* Accepts plain numbers or signal getters for page/size.
|
|
253
|
+
*
|
|
254
|
+
* @param {{ page: number|(() => number), size: number|(() => number) }} opts
|
|
255
|
+
* @returns {() => T[]}
|
|
256
|
+
*/
|
|
257
|
+
function paginated(opts) {
|
|
258
|
+
return createMemo(() => {
|
|
259
|
+
version();
|
|
260
|
+
const p = typeof opts.page === 'function' ? opts.page() : opts.page;
|
|
261
|
+
const s = typeof opts.size === 'function' ? opts.size() : opts.size;
|
|
262
|
+
const items = Array.from(entities.values());
|
|
263
|
+
const start = (p - 1) * s;
|
|
264
|
+
return items.slice(start, start + s);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
addMany,
|
|
270
|
+
upsert,
|
|
271
|
+
update,
|
|
272
|
+
remove,
|
|
273
|
+
clear,
|
|
274
|
+
get,
|
|
275
|
+
all,
|
|
276
|
+
count,
|
|
277
|
+
filter,
|
|
278
|
+
sorted,
|
|
279
|
+
paginated
|
|
280
|
+
};
|
|
281
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decantr Data Layer
|
|
3
|
+
* Server state, entity management, persistence, realtime, URL state, and worker integration.
|
|
4
|
+
*
|
|
5
|
+
* @module decantr/data
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { createQuery, createInfiniteQuery, createMutation, queryClient } from './query.js';
|
|
9
|
+
export { createEntityStore } from './entity.js';
|
|
10
|
+
export { createURLSignal, createURLStore, parsers } from './url.js';
|
|
11
|
+
export { createWebSocket, createEventSource } from './realtime.js';
|
|
12
|
+
export { createPersisted, createIndexedDB, createCrossTab, createOfflineQueue } from './persist.js';
|
|
13
|
+
export { createWorkerSignal, createWorkerQuery } from './worker.js';
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { createSignal, createEffect, batch } from '../state/index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Signal backed by localStorage or sessionStorage with cross-tab sync.
|
|
5
|
+
* @template T
|
|
6
|
+
* @param {string} key — storage key
|
|
7
|
+
* @param {T} init — fallback value when storage is empty
|
|
8
|
+
* @param {{ storage?: 'local' | 'session', serialize?: (v: T) => string, deserialize?: (s: string) => T }} [options]
|
|
9
|
+
* @returns {[() => T, (v: T | ((prev: T) => T)) => void]}
|
|
10
|
+
*/
|
|
11
|
+
export function createPersisted(key, init, options = {}) {
|
|
12
|
+
const storageType = options.storage || 'local';
|
|
13
|
+
const serialize = options.serialize || JSON.stringify;
|
|
14
|
+
const deserialize = options.deserialize || JSON.parse;
|
|
15
|
+
|
|
16
|
+
/** @returns {Storage | null} */
|
|
17
|
+
function getStore() {
|
|
18
|
+
if (typeof globalThis === 'undefined') return null;
|
|
19
|
+
try { return storageType === 'session' ? globalThis.sessionStorage : globalThis.localStorage; }
|
|
20
|
+
catch (_) { return null; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Read initial value from storage
|
|
24
|
+
let initial = init;
|
|
25
|
+
const store = getStore();
|
|
26
|
+
if (store) {
|
|
27
|
+
try {
|
|
28
|
+
const raw = store.getItem(key);
|
|
29
|
+
if (raw !== null) initial = deserialize(raw);
|
|
30
|
+
} catch (_) { /* corrupt data — use init */ }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const [get, set] = createSignal(initial);
|
|
34
|
+
|
|
35
|
+
// Persist on every change
|
|
36
|
+
createEffect(() => {
|
|
37
|
+
const value = get();
|
|
38
|
+
const s = getStore();
|
|
39
|
+
if (s) {
|
|
40
|
+
try { s.setItem(key, serialize(value)); }
|
|
41
|
+
catch (_) { /* storage full */ }
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Cross-tab sync via storage event (localStorage only)
|
|
46
|
+
if (storageType === 'local' && typeof globalThis !== 'undefined' && typeof globalThis.addEventListener === 'function') {
|
|
47
|
+
globalThis.addEventListener('storage', /** @param {StorageEvent} e */ (e) => {
|
|
48
|
+
if (e.key !== key || e.storageArea !== getStore()) return;
|
|
49
|
+
if (e.newValue === null) {
|
|
50
|
+
set(init);
|
|
51
|
+
} else {
|
|
52
|
+
try { set(deserialize(e.newValue)); }
|
|
53
|
+
catch (_) { /* corrupt cross-tab data */ }
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return [get, /** @param {T | ((prev: T) => T)} next */ (next) => set(next)];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Reactive IndexedDB binding with lazy connection.
|
|
63
|
+
* @param {string} dbName
|
|
64
|
+
* @param {string} storeName
|
|
65
|
+
* @returns {{ get: <T>(key: IDBValidKey) => Promise<T>, set: (key: IDBValidKey, value: any) => Promise<void>, delete: (key: IDBValidKey) => Promise<void>, getAll: <T>() => Promise<T[]>, clear: () => Promise<void> }}
|
|
66
|
+
*/
|
|
67
|
+
export function createIndexedDB(dbName, storeName) {
|
|
68
|
+
/** @type {IDBDatabase | null} */
|
|
69
|
+
let db = null;
|
|
70
|
+
/** @type {Promise<IDBDatabase> | null} */
|
|
71
|
+
let opening = null;
|
|
72
|
+
|
|
73
|
+
function open() {
|
|
74
|
+
if (db) return Promise.resolve(db);
|
|
75
|
+
if (opening) return opening;
|
|
76
|
+
if (typeof indexedDB === 'undefined') return Promise.reject(new Error('IndexedDB is not available'));
|
|
77
|
+
opening = new Promise((resolve, reject) => {
|
|
78
|
+
const req = indexedDB.open(dbName, 1);
|
|
79
|
+
req.onupgradeneeded = () => {
|
|
80
|
+
if (!req.result.objectStoreNames.contains(storeName)) req.result.createObjectStore(storeName);
|
|
81
|
+
};
|
|
82
|
+
req.onsuccess = () => { db = req.result; opening = null; resolve(db); };
|
|
83
|
+
req.onerror = () => { opening = null; reject(req.error); };
|
|
84
|
+
});
|
|
85
|
+
return opening;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** @param {IDBTransactionMode} mode @param {(s: IDBObjectStore) => IDBRequest} fn */
|
|
89
|
+
function tx(mode, fn) {
|
|
90
|
+
return open().then((d) => new Promise((resolve, reject) => {
|
|
91
|
+
const t = d.transaction(storeName, mode);
|
|
92
|
+
const req = fn(t.objectStore(storeName));
|
|
93
|
+
if (req) {
|
|
94
|
+
req.onsuccess = () => resolve(req.result);
|
|
95
|
+
req.onerror = () => reject(req.error);
|
|
96
|
+
} else {
|
|
97
|
+
t.oncomplete = () => resolve(undefined);
|
|
98
|
+
t.onerror = () => reject(t.error);
|
|
99
|
+
}
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
/** @template T @param {IDBValidKey} key @returns {Promise<T>} */
|
|
105
|
+
get(key) { return tx('readonly', (s) => s.get(key)); },
|
|
106
|
+
/** @param {IDBValidKey} key @param {any} value @returns {Promise<void>} */
|
|
107
|
+
set(key, value) { return tx('readwrite', (s) => s.put(value, key)); },
|
|
108
|
+
/** @param {IDBValidKey} key @returns {Promise<void>} */
|
|
109
|
+
delete(key) { return tx('readwrite', (s) => s.delete(key)); },
|
|
110
|
+
/** @template T @returns {Promise<T[]>} */
|
|
111
|
+
getAll() { return tx('readonly', (s) => s.getAll()); },
|
|
112
|
+
/** @returns {Promise<void>} */
|
|
113
|
+
clear() { return tx('readwrite', (s) => s.clear()); }
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* BroadcastChannel sync for a signal. Updates propagate across tabs.
|
|
119
|
+
* @template T
|
|
120
|
+
* @param {string} channel — channel name
|
|
121
|
+
* @param {[() => T, (v: T) => void]} signal — [getter, setter] tuple
|
|
122
|
+
* @returns {() => void} cleanup function
|
|
123
|
+
*/
|
|
124
|
+
export function createCrossTab(channel, signal) {
|
|
125
|
+
if (typeof BroadcastChannel === 'undefined') return () => {};
|
|
126
|
+
|
|
127
|
+
const [get, set] = signal;
|
|
128
|
+
const bc = new BroadcastChannel(channel);
|
|
129
|
+
const tabId = typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
130
|
+
? crypto.randomUUID()
|
|
131
|
+
: '__tab_' + Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
132
|
+
let skipEffect = false;
|
|
133
|
+
|
|
134
|
+
// Broadcast local changes to other tabs
|
|
135
|
+
createEffect(() => {
|
|
136
|
+
const value = get();
|
|
137
|
+
if (skipEffect) return;
|
|
138
|
+
try { bc.postMessage({ tabId, value }); }
|
|
139
|
+
catch (_) { /* channel closed or clone failed */ }
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Receive changes from other tabs
|
|
143
|
+
bc.onmessage = (e) => {
|
|
144
|
+
if (e.data && e.data.tabId !== tabId) {
|
|
145
|
+
skipEffect = true;
|
|
146
|
+
set(e.data.value);
|
|
147
|
+
skipEffect = false;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return () => { bc.close(); };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Queue operations while offline and flush on reconnect.
|
|
156
|
+
* @template T
|
|
157
|
+
* @param {{ process: (item: T) => Promise<any>, persist?: boolean, key?: string, retryDelay?: number }} options
|
|
158
|
+
* @returns {{ add: (item: T) => void, pending: () => T[], isOnline: () => boolean, flush: () => Promise<void> }}
|
|
159
|
+
*/
|
|
160
|
+
export function createOfflineQueue(options) {
|
|
161
|
+
const { process, persist = false, key = '__decantr_offline_queue', retryDelay = 1000 } = options;
|
|
162
|
+
|
|
163
|
+
// Restore persisted queue
|
|
164
|
+
let initial = [];
|
|
165
|
+
if (persist && typeof localStorage !== 'undefined') {
|
|
166
|
+
try {
|
|
167
|
+
const raw = localStorage.getItem(key);
|
|
168
|
+
if (raw) initial = JSON.parse(raw);
|
|
169
|
+
} catch (_) { /* corrupt — start fresh */ }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const [pending, setPending] = createSignal(initial);
|
|
173
|
+
const [isOnline, setIsOnline] = createSignal(
|
|
174
|
+
typeof navigator !== 'undefined' ? navigator.onLine : true
|
|
175
|
+
);
|
|
176
|
+
let flushing = false;
|
|
177
|
+
|
|
178
|
+
// Persist queue whenever it changes
|
|
179
|
+
if (persist) {
|
|
180
|
+
createEffect(() => {
|
|
181
|
+
const items = pending();
|
|
182
|
+
if (typeof localStorage !== 'undefined') {
|
|
183
|
+
try { localStorage.setItem(key, JSON.stringify(items)); }
|
|
184
|
+
catch (_) { /* storage full */ }
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Listen for online/offline events
|
|
190
|
+
if (typeof globalThis !== 'undefined' && typeof globalThis.addEventListener === 'function') {
|
|
191
|
+
globalThis.addEventListener('online', () => { setIsOnline(true); flush(); });
|
|
192
|
+
globalThis.addEventListener('offline', () => { setIsOnline(false); });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Process items sequentially (FIFO). Failed items stay in queue for retry. */
|
|
196
|
+
async function flush() {
|
|
197
|
+
if (flushing) return;
|
|
198
|
+
flushing = true;
|
|
199
|
+
try {
|
|
200
|
+
while (pending().length > 0 && isOnline()) {
|
|
201
|
+
const item = pending()[0];
|
|
202
|
+
try {
|
|
203
|
+
await process(item);
|
|
204
|
+
setPending((prev) => prev.slice(1));
|
|
205
|
+
} catch (_) {
|
|
206
|
+
await new Promise((r) => setTimeout(r, retryDelay));
|
|
207
|
+
if (!isOnline()) break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} finally {
|
|
211
|
+
flushing = false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
/** @param {T} item — add to queue; triggers flush if online */
|
|
217
|
+
add(item) {
|
|
218
|
+
setPending((prev) => [...prev, item]);
|
|
219
|
+
if (isOnline()) flush();
|
|
220
|
+
},
|
|
221
|
+
pending,
|
|
222
|
+
isOnline,
|
|
223
|
+
flush
|
|
224
|
+
};
|
|
225
|
+
}
|