decantr 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (382) hide show
  1. package/AGENTS.md +868 -0
  2. package/CHANGELOG.md +255 -0
  3. package/CLAUDE.md +178 -0
  4. package/LICENSE +21 -0
  5. package/README.md +229 -0
  6. package/cli/art.js +127 -0
  7. package/cli/commands/a11y.js +61 -0
  8. package/cli/commands/audit.js +225 -0
  9. package/cli/commands/build.js +38 -0
  10. package/cli/commands/dev.js +18 -0
  11. package/cli/commands/doctor.js +197 -0
  12. package/cli/commands/figma-sync.js +48 -0
  13. package/cli/commands/figma-tokens.js +55 -0
  14. package/cli/commands/generate.js +26 -0
  15. package/cli/commands/init.js +116 -0
  16. package/cli/commands/lint.js +209 -0
  17. package/cli/commands/mcp.js +530 -0
  18. package/cli/commands/migrate.js +175 -0
  19. package/cli/commands/test.js +38 -0
  20. package/cli/commands/validate.js +354 -0
  21. package/cli/index.js +113 -0
  22. package/package.json +95 -0
  23. package/reference/atoms.md +517 -0
  24. package/reference/behaviors.md +384 -0
  25. package/reference/build-tooling.md +275 -0
  26. package/reference/color-guidelines.md +965 -0
  27. package/reference/component-lifecycle.md +137 -0
  28. package/reference/compound-spacing.md +95 -0
  29. package/reference/decantation-process.md +499 -0
  30. package/reference/dev-server-routes.md +93 -0
  31. package/reference/form-system.md +253 -0
  32. package/reference/i18n.md +336 -0
  33. package/reference/icons.md +576 -0
  34. package/reference/llm-primer.md +953 -0
  35. package/reference/plugins.md +252 -0
  36. package/reference/registry-consumption.md +76 -0
  37. package/reference/router.md +217 -0
  38. package/reference/shells.md +116 -0
  39. package/reference/spatial-guidelines.md +541 -0
  40. package/reference/ssr.md +234 -0
  41. package/reference/state-data.md +215 -0
  42. package/reference/state-patterns.md +166 -0
  43. package/reference/state.md +194 -0
  44. package/reference/style-system.md +110 -0
  45. package/reference/tokens.md +460 -0
  46. package/src/app.js +19 -0
  47. package/src/chart/_animate.js +266 -0
  48. package/src/chart/_base.js +109 -0
  49. package/src/chart/_data.js +209 -0
  50. package/src/chart/_format.js +106 -0
  51. package/src/chart/_interact.js +364 -0
  52. package/src/chart/_palette.js +105 -0
  53. package/src/chart/_renderer.js +52 -0
  54. package/src/chart/_scene.js +262 -0
  55. package/src/chart/_shared.js +371 -0
  56. package/src/chart/index.js +637 -0
  57. package/src/chart/layouts/_layout-base.js +328 -0
  58. package/src/chart/layouts/cartesian.js +148 -0
  59. package/src/chart/layouts/hierarchy.js +562 -0
  60. package/src/chart/layouts/polar.js +101 -0
  61. package/src/chart/renderers/canvas.js +179 -0
  62. package/src/chart/renderers/svg.js +256 -0
  63. package/src/chart/renderers/webgpu.js +715 -0
  64. package/src/chart/types/_type-base.js +26 -0
  65. package/src/chart/types/area.js +134 -0
  66. package/src/chart/types/bar.js +173 -0
  67. package/src/chart/types/box-plot.js +125 -0
  68. package/src/chart/types/bubble.js +63 -0
  69. package/src/chart/types/candlestick.js +115 -0
  70. package/src/chart/types/chord.js +85 -0
  71. package/src/chart/types/combination.js +108 -0
  72. package/src/chart/types/funnel.js +68 -0
  73. package/src/chart/types/gauge.js +163 -0
  74. package/src/chart/types/heatmap.js +98 -0
  75. package/src/chart/types/histogram.js +71 -0
  76. package/src/chart/types/line.js +111 -0
  77. package/src/chart/types/org-chart.js +93 -0
  78. package/src/chart/types/pie.js +81 -0
  79. package/src/chart/types/radar.js +96 -0
  80. package/src/chart/types/radial.js +68 -0
  81. package/src/chart/types/range-area.js +55 -0
  82. package/src/chart/types/range-bar.js +61 -0
  83. package/src/chart/types/sankey.js +73 -0
  84. package/src/chart/types/scatter.js +66 -0
  85. package/src/chart/types/sparkline.js +81 -0
  86. package/src/chart/types/sunburst.js +69 -0
  87. package/src/chart/types/swimlane.js +88 -0
  88. package/src/chart/types/treemap.js +62 -0
  89. package/src/chart/types/waterfall.js +100 -0
  90. package/src/components/_base.js +1658 -0
  91. package/src/components/_behaviors.js +1140 -0
  92. package/src/components/_primitives.js +534 -0
  93. package/src/components/_qr-encoder.js +539 -0
  94. package/src/components/accordion.js +207 -0
  95. package/src/components/affix.js +62 -0
  96. package/src/components/alert-dialog.js +75 -0
  97. package/src/components/alert.js +47 -0
  98. package/src/components/aspect-ratio.js +24 -0
  99. package/src/components/avatar-group.js +55 -0
  100. package/src/components/avatar.js +38 -0
  101. package/src/components/back-top.js +75 -0
  102. package/src/components/badge.js +74 -0
  103. package/src/components/banner.js +68 -0
  104. package/src/components/breadcrumb.js +162 -0
  105. package/src/components/button.js +115 -0
  106. package/src/components/calendar.js +131 -0
  107. package/src/components/card.js +192 -0
  108. package/src/components/carousel.js +98 -0
  109. package/src/components/cascader.js +261 -0
  110. package/src/components/checkbox.js +80 -0
  111. package/src/components/chip.js +81 -0
  112. package/src/components/code-block.js +82 -0
  113. package/src/components/collapsible.js +50 -0
  114. package/src/components/color-palette.js +438 -0
  115. package/src/components/color-picker.js +314 -0
  116. package/src/components/combobox.js +181 -0
  117. package/src/components/command.js +174 -0
  118. package/src/components/comment.js +206 -0
  119. package/src/components/context-menu.js +76 -0
  120. package/src/components/data-table.js +724 -0
  121. package/src/components/date-picker.js +217 -0
  122. package/src/components/date-range-picker.js +244 -0
  123. package/src/components/datetime-picker.js +271 -0
  124. package/src/components/descriptions.js +68 -0
  125. package/src/components/drawer.js +179 -0
  126. package/src/components/dropdown.js +88 -0
  127. package/src/components/empty.js +41 -0
  128. package/src/components/float-button.js +90 -0
  129. package/src/components/form.js +106 -0
  130. package/src/components/hover-card.js +49 -0
  131. package/src/components/icon.js +87 -0
  132. package/src/components/image.js +97 -0
  133. package/src/components/index.js +117 -0
  134. package/src/components/input-group.js +75 -0
  135. package/src/components/input-number.js +155 -0
  136. package/src/components/input-otp.js +178 -0
  137. package/src/components/input.js +91 -0
  138. package/src/components/kbd.js +36 -0
  139. package/src/components/label.js +25 -0
  140. package/src/components/list.js +118 -0
  141. package/src/components/masked-input.js +236 -0
  142. package/src/components/mentions.js +165 -0
  143. package/src/components/menu.js +259 -0
  144. package/src/components/message.js +80 -0
  145. package/src/components/modal.js +147 -0
  146. package/src/components/navigation-menu.js +166 -0
  147. package/src/components/notification.js +84 -0
  148. package/src/components/pagination.js +104 -0
  149. package/src/components/placeholder.js +132 -0
  150. package/src/components/popconfirm.js +70 -0
  151. package/src/components/popover.js +58 -0
  152. package/src/components/progress.js +61 -0
  153. package/src/components/qrcode.js +251 -0
  154. package/src/components/radiogroup.js +120 -0
  155. package/src/components/range-slider.js +176 -0
  156. package/src/components/rate.js +186 -0
  157. package/src/components/resizable.js +83 -0
  158. package/src/components/result.js +57 -0
  159. package/src/components/scroll-area.js +43 -0
  160. package/src/components/segmented.js +97 -0
  161. package/src/components/select.js +165 -0
  162. package/src/components/separator.js +31 -0
  163. package/src/components/shell.js +407 -0
  164. package/src/components/skeleton.js +39 -0
  165. package/src/components/slider.js +141 -0
  166. package/src/components/sortable-list.js +176 -0
  167. package/src/components/space.js +42 -0
  168. package/src/components/spinner.js +112 -0
  169. package/src/components/splitter.js +147 -0
  170. package/src/components/statistic.js +136 -0
  171. package/src/components/steps.js +99 -0
  172. package/src/components/switch.js +95 -0
  173. package/src/components/table.js +44 -0
  174. package/src/components/tabs.js +216 -0
  175. package/src/components/tag.js +115 -0
  176. package/src/components/textarea.js +82 -0
  177. package/src/components/time-picker.js +153 -0
  178. package/src/components/time-range-picker.js +170 -0
  179. package/src/components/timeline.js +226 -0
  180. package/src/components/toast.js +71 -0
  181. package/src/components/toggle.js +213 -0
  182. package/src/components/tooltip.js +57 -0
  183. package/src/components/tour.js +159 -0
  184. package/src/components/transfer.js +163 -0
  185. package/src/components/tree-select.js +274 -0
  186. package/src/components/tree.js +141 -0
  187. package/src/components/typography.js +136 -0
  188. package/src/components/upload.js +118 -0
  189. package/src/components/visually-hidden.js +20 -0
  190. package/src/components/watermark.js +124 -0
  191. package/src/core/index.js +539 -0
  192. package/src/core/lifecycle.js +69 -0
  193. package/src/css/atoms.js +651 -0
  194. package/src/css/components.js +940 -0
  195. package/src/css/derive.js +1296 -0
  196. package/src/css/index.js +265 -0
  197. package/src/css/runtime.js +268 -0
  198. package/src/css/styles/addons/bioluminescent.js +93 -0
  199. package/src/css/styles/addons/clay.js +70 -0
  200. package/src/css/styles/addons/clean.js +57 -0
  201. package/src/css/styles/addons/command-center.js +143 -0
  202. package/src/css/styles/addons/dopamine.js +83 -0
  203. package/src/css/styles/addons/editorial.js +80 -0
  204. package/src/css/styles/addons/glassmorphism.js +99 -0
  205. package/src/css/styles/addons/liquid-glass.js +105 -0
  206. package/src/css/styles/addons/prismatic.js +100 -0
  207. package/src/css/styles/addons/retro.js +63 -0
  208. package/src/css/styles/auradecantism.js +96 -0
  209. package/src/css/theme-registry.js +444 -0
  210. package/src/data/entity.js +281 -0
  211. package/src/data/index.js +13 -0
  212. package/src/data/persist.js +225 -0
  213. package/src/data/query.js +839 -0
  214. package/src/data/realtime.js +299 -0
  215. package/src/data/url.js +177 -0
  216. package/src/data/worker.js +134 -0
  217. package/src/explorer/archetypes.js +243 -0
  218. package/src/explorer/atoms.js +228 -0
  219. package/src/explorer/charts.js +497 -0
  220. package/src/explorer/components.js +129 -0
  221. package/src/explorer/foundations.js +949 -0
  222. package/src/explorer/icons.js +178 -0
  223. package/src/explorer/patterns.js +247 -0
  224. package/src/explorer/recipes.js +194 -0
  225. package/src/explorer/shared/pattern-examples.js +1337 -0
  226. package/src/explorer/shared/showcase-renderer.js +958 -0
  227. package/src/explorer/shared/spec-table.js +41 -0
  228. package/src/explorer/shared/usage-links.js +87 -0
  229. package/src/explorer/shell-config.js +10 -0
  230. package/src/explorer/shells.js +551 -0
  231. package/src/explorer/styles.js +161 -0
  232. package/src/explorer/tokens.js +262 -0
  233. package/src/explorer/tools.js +525 -0
  234. package/src/form/index.js +804 -0
  235. package/src/i18n/index.js +251 -0
  236. package/src/icons/essential.js +479 -0
  237. package/src/icons/index.js +53 -0
  238. package/src/plugins/index.js +282 -0
  239. package/src/registry/archetypes/content-site.json +71 -0
  240. package/src/registry/archetypes/docs-explorer.json +23 -0
  241. package/src/registry/archetypes/ecommerce.json +104 -0
  242. package/src/registry/archetypes/financial-dashboard.json +77 -0
  243. package/src/registry/archetypes/index.json +41 -0
  244. package/src/registry/archetypes/portfolio.json +82 -0
  245. package/src/registry/archetypes/recipe-community.json +159 -0
  246. package/src/registry/archetypes/saas-dashboard.json +86 -0
  247. package/src/registry/architect/cross-cutting.json +45 -0
  248. package/src/registry/architect/domains/ecommerce.json +294 -0
  249. package/src/registry/architect/domains/financial-services.json +302 -0
  250. package/src/registry/architect/index.json +26 -0
  251. package/src/registry/architect/traits.json +379 -0
  252. package/src/registry/atoms.json +16 -0
  253. package/src/registry/chart-showcase.json +160 -0
  254. package/src/registry/chart.json +136 -0
  255. package/src/registry/components.json +8616 -0
  256. package/src/registry/core.json +216 -0
  257. package/src/registry/css.json +319 -0
  258. package/src/registry/data.json +135 -0
  259. package/src/registry/foundations.json +11 -0
  260. package/src/registry/icons.json +463 -0
  261. package/src/registry/index.json +101 -0
  262. package/src/registry/patterns/activity-feed.json +37 -0
  263. package/src/registry/patterns/article-content.json +27 -0
  264. package/src/registry/patterns/auth-form.json +37 -0
  265. package/src/registry/patterns/author-card.json +20 -0
  266. package/src/registry/patterns/card-grid.json +127 -0
  267. package/src/registry/patterns/category-nav.json +26 -0
  268. package/src/registry/patterns/chart-grid.json +36 -0
  269. package/src/registry/patterns/chat-interface.json +37 -0
  270. package/src/registry/patterns/checklist-card.json +55 -0
  271. package/src/registry/patterns/comparison-panel.json +27 -0
  272. package/src/registry/patterns/component-showcase.json +24 -0
  273. package/src/registry/patterns/contact-form.json +31 -0
  274. package/src/registry/patterns/cta-section.json +20 -0
  275. package/src/registry/patterns/data-table.json +37 -0
  276. package/src/registry/patterns/detail-header.json +83 -0
  277. package/src/registry/patterns/detail-panel.json +27 -0
  278. package/src/registry/patterns/explorer-shell.json +22 -0
  279. package/src/registry/patterns/filter-bar.json +33 -0
  280. package/src/registry/patterns/filter-sidebar.json +27 -0
  281. package/src/registry/patterns/form-sections.json +110 -0
  282. package/src/registry/patterns/goal-tracker.json +27 -0
  283. package/src/registry/patterns/hero.json +107 -0
  284. package/src/registry/patterns/index.json +47 -0
  285. package/src/registry/patterns/kpi-grid.json +36 -0
  286. package/src/registry/patterns/media-gallery.json +20 -0
  287. package/src/registry/patterns/order-history.json +20 -0
  288. package/src/registry/patterns/pagination.json +19 -0
  289. package/src/registry/patterns/photo-to-recipe.json +36 -0
  290. package/src/registry/patterns/pipeline-tracker.json +28 -0
  291. package/src/registry/patterns/post-list.json +27 -0
  292. package/src/registry/patterns/pricing-table.json +32 -0
  293. package/src/registry/patterns/scorecard.json +28 -0
  294. package/src/registry/patterns/search-bar.json +20 -0
  295. package/src/registry/patterns/specimen-grid.json +19 -0
  296. package/src/registry/patterns/stat-card.json +55 -0
  297. package/src/registry/patterns/stats-bar.json +55 -0
  298. package/src/registry/patterns/steps-card.json +55 -0
  299. package/src/registry/patterns/table-of-contents.json +19 -0
  300. package/src/registry/patterns/testimonials.json +21 -0
  301. package/src/registry/patterns/timeline.json +27 -0
  302. package/src/registry/patterns/token-inspector.json +21 -0
  303. package/src/registry/patterns/wizard.json +27 -0
  304. package/src/registry/recipe-auradecantism.json +69 -0
  305. package/src/registry/recipe-clean.json +65 -0
  306. package/src/registry/recipe-command-center.json +78 -0
  307. package/src/registry/router.json +73 -0
  308. package/src/registry/schema/README.md +197 -0
  309. package/src/registry/skeletons.json +259 -0
  310. package/src/registry/state.json +137 -0
  311. package/src/registry/tokens.json +40 -0
  312. package/src/router/hash.js +17 -0
  313. package/src/router/history.js +18 -0
  314. package/src/router/index.js +598 -0
  315. package/src/ssr/index.js +922 -0
  316. package/src/state/arrays.js +181 -0
  317. package/src/state/devtools.js +647 -0
  318. package/src/state/index.js +498 -0
  319. package/src/state/middleware.js +288 -0
  320. package/src/state/scheduler.js +206 -0
  321. package/src/state/store.js +300 -0
  322. package/src/tags/index.js +19 -0
  323. package/src/tannins/auth.js +396 -0
  324. package/src/test/dom.js +352 -0
  325. package/src/test/index.js +62 -0
  326. package/src/test/state.js +306 -0
  327. package/tools/a11y-audit.js +487 -0
  328. package/tools/analyzer.js +315 -0
  329. package/tools/audit.js +706 -0
  330. package/tools/builder.js +1422 -0
  331. package/tools/css-extract.js +188 -0
  332. package/tools/dev-server.js +316 -0
  333. package/tools/dts-gen.js +1260 -0
  334. package/tools/figma-components.js +329 -0
  335. package/tools/figma-patterns.js +516 -0
  336. package/tools/figma-plugin/code.js +453 -0
  337. package/tools/figma-plugin/manifest.json +14 -0
  338. package/tools/figma-plugin/ui.html +268 -0
  339. package/tools/figma-render.js +293 -0
  340. package/tools/figma-tokens.js +712 -0
  341. package/tools/figma-upload.js +318 -0
  342. package/tools/generate.js +738 -0
  343. package/tools/icons.js +133 -0
  344. package/tools/init-templates.js +265 -0
  345. package/tools/install-hooks.sh +5 -0
  346. package/tools/migrations/0.5.0.js +53 -0
  347. package/tools/migrations/0.6.0.js +95 -0
  348. package/tools/minify.js +170 -0
  349. package/tools/pre-commit +4 -0
  350. package/tools/registry.js +662 -0
  351. package/tools/reset-playground.js +61 -0
  352. package/tools/starter-templates/content-site/app.js +49 -0
  353. package/tools/starter-templates/content-site/essence.js +19 -0
  354. package/tools/starter-templates/content-site/pages.js +31 -0
  355. package/tools/starter-templates/ecommerce/app.js +50 -0
  356. package/tools/starter-templates/ecommerce/essence.js +19 -0
  357. package/tools/starter-templates/ecommerce/pages.js +31 -0
  358. package/tools/starter-templates/landing-page/app.js +38 -0
  359. package/tools/starter-templates/landing-page/essence.js +18 -0
  360. package/tools/starter-templates/landing-page/pages.js +21 -0
  361. package/tools/starter-templates/portfolio/app.js +45 -0
  362. package/tools/starter-templates/portfolio/essence.js +19 -0
  363. package/tools/starter-templates/portfolio/pages.js +33 -0
  364. package/tools/starter-templates/saas-dashboard/app.js +70 -0
  365. package/tools/starter-templates/saas-dashboard/essence.js +19 -0
  366. package/tools/starter-templates/saas-dashboard/pages.js +31 -0
  367. package/tools/verify-pack.js +203 -0
  368. package/types/chart.d.ts +77 -0
  369. package/types/components.d.ts +587 -0
  370. package/types/core.d.ts +89 -0
  371. package/types/css.d.ts +149 -0
  372. package/types/data.d.ts +238 -0
  373. package/types/form.d.ts +164 -0
  374. package/types/i18n.d.ts +51 -0
  375. package/types/icons.d.ts +27 -0
  376. package/types/index.d.ts +13 -0
  377. package/types/router.d.ts +116 -0
  378. package/types/ssr.d.ts +102 -0
  379. package/types/state.d.ts +83 -0
  380. package/types/tags.d.ts +62 -0
  381. package/types/tannins.d.ts +63 -0
  382. package/types/test.d.ts +48 -0
@@ -0,0 +1,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
+ }