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,300 @@
1
+ /**
2
+ * Deep reactive store with per-property subscriptions,
3
+ * Immer-like produce(), and structural-sharing reconcile().
4
+ * @module state/store
5
+ */
6
+ import { batch } from './index.js';
7
+ import { currentEffect, isBatching, scheduleEffect } from './scheduler.js';
8
+
9
+ /** @type {WeakMap<object, object>} raw target -> proxy */
10
+ const proxyCache = new WeakMap();
11
+ /** @type {WeakMap<object, Map<string|symbol, Set>>} target -> prop -> subscribers */
12
+ const subMaps = new WeakMap();
13
+ /** @type {WeakMap<object, object>} proxy -> raw target */
14
+ const proxyToRaw = new WeakMap();
15
+
16
+ const MUTATORS = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse', 'fill', 'copyWithin'];
17
+
18
+ function getSubs(prop, target) {
19
+ let map = subMaps.get(target);
20
+ if (!map) { map = new Map(); subMaps.set(target, map); }
21
+ let s = map.get(prop);
22
+ if (!s) { s = new Set(); map.set(prop, s); }
23
+ return s;
24
+ }
25
+
26
+ function track(subs) {
27
+ if (!currentEffect) return;
28
+ subs.add(currentEffect);
29
+ if (currentEffect.sources) currentEffect.sources.add(subs);
30
+ }
31
+
32
+ function notify(subs) {
33
+ if (!subs || subs.size === 0) return;
34
+ if (isBatching()) {
35
+ for (const sub of subs) scheduleEffect(sub);
36
+ } else {
37
+ const arr = [...subs];
38
+ for (let i = 0; i < arr.length; i++) {
39
+ if (!arr[i].disposed) arr[i].run();
40
+ }
41
+ }
42
+ }
43
+
44
+ function notifyAll(target) {
45
+ const map = subMaps.get(target);
46
+ if (!map) return;
47
+ for (const subs of map.values()) notify(subs);
48
+ }
49
+
50
+ /** @param {*} v */
51
+ function isProxyable(v) {
52
+ return v !== null && typeof v === 'object' && !Object.isFrozen(v);
53
+ }
54
+
55
+ /** Unwrap proxy to raw target (identity if not proxied). */
56
+ function toRaw(v) {
57
+ return (v && proxyToRaw.get(v)) || v;
58
+ }
59
+
60
+ /** Wrap a value in a deep reactive proxy (cached per identity). */
61
+ function wrap(target) {
62
+ if (proxyCache.has(target)) return proxyCache.get(target);
63
+ const isArr = Array.isArray(target);
64
+
65
+ const proxy = new Proxy(target, {
66
+ get(target, prop, receiver) {
67
+ if (prop === '__raw') return target;
68
+ if (typeof prop === 'symbol') return Reflect.get(target, prop, receiver);
69
+
70
+ if (isArr && MUTATORS.includes(/** @type {string} */ (prop))) {
71
+ return (...args) => {
72
+ batch(() => {
73
+ Array.prototype[prop].apply(target, args.map(a => toRaw(a)));
74
+ notify(getSubs('length', target));
75
+ notifyAll(target);
76
+ });
77
+ return target.length;
78
+ };
79
+ }
80
+
81
+ track(getSubs(prop, target));
82
+ const value = Reflect.get(target, prop, receiver);
83
+ return isProxyable(value) ? wrap(value) : value;
84
+ },
85
+
86
+ set(target, prop, value) {
87
+ const raw = toRaw(value);
88
+ const prev = target[prop];
89
+ if (Object.is(prev, raw)) return true;
90
+ target[prop] = raw;
91
+ notify(getSubs(prop, target));
92
+ if (isArr && prop !== 'length') notify(getSubs('length', target));
93
+ return true;
94
+ },
95
+
96
+ deleteProperty(target, prop) {
97
+ const had = prop in target;
98
+ const result = Reflect.deleteProperty(target, prop);
99
+ if (had) notify(getSubs(prop, target));
100
+ return result;
101
+ },
102
+
103
+ has(target, prop) {
104
+ track(getSubs(prop, target));
105
+ return Reflect.has(target, prop);
106
+ },
107
+
108
+ ownKeys(target) {
109
+ track(getSubs('@@keys', target));
110
+ return Reflect.ownKeys(target);
111
+ }
112
+ });
113
+
114
+ proxyCache.set(target, proxy);
115
+ proxyToRaw.set(proxy, target);
116
+ return proxy;
117
+ }
118
+
119
+ // ─── createDeepStore ─────────────────────────────────────────
120
+
121
+ /**
122
+ * Create a deeply reactive store. Nested objects/arrays are lazily
123
+ * wrapped in reactive proxies with per-property subscription tracking.
124
+ * @template T
125
+ * @param {T} init - Plain object or array
126
+ * @returns {T} Deep reactive proxy
127
+ */
128
+ export function createDeepStore(init) {
129
+ if (!isProxyable(init)) {
130
+ throw new Error('createDeepStore requires a plain object or array');
131
+ }
132
+ return /** @type {T} */ (wrap(init));
133
+ }
134
+
135
+ // ─── produce ─────────────────────────────────────────────────
136
+
137
+ /**
138
+ * Immer-like mutation. Executes recipe against a draft proxy that records
139
+ * mutations, then fires notifications in a single batch.
140
+ * @template T
141
+ * @param {T} store - Deep reactive store
142
+ * @param {(draft: T) => void} recipe - Mutation function
143
+ */
144
+ export function produce(store, recipe) {
145
+ /** @type {Array<{target: object, prop: string|symbol, value: *, type: string}>} */
146
+ const patches = [];
147
+ const drafts = new WeakMap();
148
+
149
+ function createDraft(target) {
150
+ const raw = toRaw(target);
151
+ if (drafts.has(raw)) return drafts.get(raw);
152
+
153
+ const draft = new Proxy(raw, {
154
+ get(t, prop) {
155
+ if (prop === '__raw') return raw;
156
+ if (typeof prop === 'symbol') return Reflect.get(t, prop);
157
+ if (Array.isArray(t) && MUTATORS.includes(/** @type {string} */ (prop))) {
158
+ return (...args) => {
159
+ const unwrapped = args.map(a => toRaw(a));
160
+ patches.push({ target: t, prop, value: unwrapped, type: 'array' });
161
+ Array.prototype[prop].apply(t, unwrapped);
162
+ return t.length;
163
+ };
164
+ }
165
+ const value = Reflect.get(t, prop);
166
+ return isProxyable(value) ? createDraft(value) : value;
167
+ },
168
+ set(t, prop, value) {
169
+ const v = toRaw(value);
170
+ patches.push({ target: t, prop, value: v, type: 'set' });
171
+ t[prop] = v;
172
+ return true;
173
+ },
174
+ deleteProperty(t, prop) {
175
+ patches.push({ target: t, prop, value: undefined, type: 'delete' });
176
+ delete t[prop];
177
+ return true;
178
+ }
179
+ });
180
+
181
+ drafts.set(raw, draft);
182
+ return draft;
183
+ }
184
+
185
+ recipe(createDraft(store));
186
+
187
+ // Notify in a single batch — mutations already applied to raw targets
188
+ batch(() => {
189
+ const seen = new Set();
190
+ for (const { target, prop, type } of patches) {
191
+ if (type === 'array') {
192
+ const key = target.toString() + '::arr';
193
+ if (!seen.has(key)) {
194
+ seen.add(key);
195
+ notify(getSubs('length', target));
196
+ notifyAll(target);
197
+ }
198
+ } else if (type === 'delete') {
199
+ notify(getSubs(prop, target));
200
+ notify(getSubs('@@keys', target));
201
+ } else {
202
+ notify(getSubs(prop, target));
203
+ }
204
+ }
205
+ });
206
+ }
207
+
208
+ // ─── reconcile ───────────────────────────────────────────────
209
+
210
+ /**
211
+ * Efficient bulk update with structural sharing. Parallel-walks old and
212
+ * new data, only notifying properties that actually changed.
213
+ * @template T
214
+ * @param {T} store - Deep reactive store
215
+ * @param {T} data - New plain data to reconcile against
216
+ */
217
+ export function reconcile(store, data) {
218
+ const raw = toRaw(store);
219
+ if (!isProxyable(raw) || !isProxyable(data)) return;
220
+ batch(() => { _reconcile(raw, data); });
221
+ }
222
+
223
+ function _reconcile(target, next) {
224
+ const isArr = Array.isArray(target);
225
+ if (isArr !== Array.isArray(next)) return; // type mismatch
226
+ if (isArr) return _reconcileArray(target, next);
227
+
228
+ const oldKeys = Object.keys(target);
229
+ const newKeys = Object.keys(next);
230
+ let keysChanged = false;
231
+
232
+ for (let i = 0; i < newKeys.length; i++) {
233
+ const key = newKeys[i];
234
+ const oldVal = target[key];
235
+ const newVal = next[key];
236
+
237
+ if (!(key in target)) {
238
+ target[key] = newVal;
239
+ notify(getSubs(key, target));
240
+ keysChanged = true;
241
+ continue;
242
+ }
243
+ if (Object.is(oldVal, newVal)) continue;
244
+
245
+ if (isProxyable(oldVal) && isProxyable(newVal)
246
+ && Array.isArray(oldVal) === Array.isArray(newVal)) {
247
+ _reconcile(oldVal, newVal);
248
+ } else {
249
+ target[key] = newVal;
250
+ notify(getSubs(key, target));
251
+ }
252
+ }
253
+
254
+ for (let i = 0; i < oldKeys.length; i++) {
255
+ const key = oldKeys[i];
256
+ if (!(key in next)) {
257
+ delete target[key];
258
+ notify(getSubs(key, target));
259
+ keysChanged = true;
260
+ }
261
+ }
262
+ if (keysChanged) notify(getSubs('@@keys', target));
263
+ }
264
+
265
+ function _reconcileArray(target, next) {
266
+ const oldLen = target.length;
267
+ const newLen = next.length;
268
+ let changed = false;
269
+
270
+ for (let i = 0; i < Math.min(oldLen, newLen); i++) {
271
+ const oldVal = target[i];
272
+ const newVal = next[i];
273
+ if (Object.is(oldVal, newVal)) continue;
274
+ if (isProxyable(oldVal) && isProxyable(newVal)
275
+ && Array.isArray(oldVal) === Array.isArray(newVal)) {
276
+ _reconcile(oldVal, newVal);
277
+ } else {
278
+ target[i] = newVal;
279
+ notify(getSubs(String(i), target));
280
+ changed = true;
281
+ }
282
+ }
283
+
284
+ for (let i = oldLen; i < newLen; i++) {
285
+ target[i] = next[i];
286
+ notify(getSubs(String(i), target));
287
+ changed = true;
288
+ }
289
+
290
+ if (newLen < oldLen) {
291
+ for (let i = newLen; i < oldLen; i++) notify(getSubs(String(i), target));
292
+ target.length = newLen;
293
+ changed = true;
294
+ }
295
+
296
+ if (changed) {
297
+ notify(getSubs('length', target));
298
+ notify(getSubs('@@keys', target));
299
+ }
300
+ }
@@ -0,0 +1,19 @@
1
+ import { h } from '../core/index.js';
2
+
3
+ /**
4
+ * Proxy-based tag functions. Destructure what you need:
5
+ * @example const { div, p, h2, button } = tags;
6
+ * div({ class: 'card' }, h2('Title'), p('Content'))
7
+ * @type {Record<string, Function>}
8
+ */
9
+ export const tags = new Proxy({}, {
10
+ get(_, tag) {
11
+ return (first, ...rest) => {
12
+ if (first && typeof first === 'object' && !first.nodeType
13
+ && !Array.isArray(first) && typeof first !== 'function') {
14
+ return h(tag, first, ...rest);
15
+ }
16
+ return h(tag, null, ...(first != null ? [first, ...rest] : rest));
17
+ };
18
+ }
19
+ });
@@ -0,0 +1,396 @@
1
+ /**
2
+ * Decantr Auth Reference Tannin
3
+ *
4
+ * Provides token-based authentication with reactive signals,
5
+ * persistent cross-tab token storage, auto-refresh on 401,
6
+ * and a route guard helper.
7
+ *
8
+ * @module decantr/tannins/auth
9
+ */
10
+
11
+ import { createSignal, createMemo, createEffect, batch } from '../state/index.js';
12
+ import { createPersisted } from '../data/persist.js';
13
+
14
+ /**
15
+ * Create an auth instance with reactive signals, token persistence,
16
+ * login/logout/refresh flows, and fetch middleware.
17
+ *
18
+ * @param {{
19
+ * loginEndpoint?: string,
20
+ * refreshEndpoint?: string,
21
+ * logoutEndpoint?: string,
22
+ * tokenKey?: string,
23
+ * storage?: 'localStorage' | 'sessionStorage',
24
+ * onAuthChange?: (isAuthenticated: boolean) => void
25
+ * }} [config]
26
+ * @returns {{
27
+ * user: () => any,
28
+ * token: () => string | null,
29
+ * isAuthenticated: () => boolean,
30
+ * isLoading: () => boolean,
31
+ * error: () => any,
32
+ * login: (credentials: Record<string, any>) => Promise<any>,
33
+ * logout: () => Promise<void>,
34
+ * refresh: () => Promise<void>,
35
+ * setUser: (user: any) => void,
36
+ * setToken: (token: string | null) => void,
37
+ * destroy: () => void
38
+ * }}
39
+ */
40
+ export function createAuth(config = {}) {
41
+ const {
42
+ loginEndpoint = '/api/auth/login',
43
+ refreshEndpoint = '/api/auth/refresh',
44
+ logoutEndpoint = '/api/auth/logout',
45
+ tokenKey = 'decantr_auth_token',
46
+ storage = 'localStorage',
47
+ onAuthChange = null
48
+ } = config;
49
+
50
+ // Persisted token signal — cross-tab sync via storage events
51
+ const persistedStorage = storage === 'sessionStorage' ? 'session' : 'local';
52
+ const [token, setTokenRaw] = createPersisted(tokenKey, null, { storage: persistedStorage });
53
+
54
+ // User object signal (not persisted — re-fetched on refresh)
55
+ const [user, setUser] = createSignal(null);
56
+
57
+ // Loading state
58
+ const [isLoading, setIsLoading] = createSignal(false);
59
+
60
+ // Error state
61
+ const [error, setError] = createSignal(null);
62
+
63
+ // Derived: is authenticated when token exists
64
+ const isAuthenticated = createMemo(() => token() !== null);
65
+
66
+ // Track previous auth state for onAuthChange callback
67
+ let prevAuth = isAuthenticated();
68
+ let authChangeDispose = null;
69
+ if (typeof onAuthChange === 'function') {
70
+ authChangeDispose = createEffect(() => {
71
+ const current = isAuthenticated();
72
+ if (current !== prevAuth) {
73
+ prevAuth = current;
74
+ onAuthChange(current);
75
+ }
76
+ });
77
+ }
78
+
79
+ // Track whether a refresh is in progress (to avoid concurrent refreshes)
80
+ let refreshPromise = null;
81
+
82
+ // Store the original fetch so we can restore it on destroy
83
+ const originalFetch = typeof globalThis !== 'undefined' ? globalThis.fetch : null;
84
+ let middlewareInstalled = false;
85
+
86
+ /**
87
+ * Install fetch middleware that injects Bearer token and handles 401 auto-refresh.
88
+ */
89
+ function installMiddleware() {
90
+ if (middlewareInstalled) return;
91
+ if (typeof globalThis === 'undefined' || typeof globalThis.fetch !== 'function') return;
92
+
93
+ const baseFetch = globalThis.fetch;
94
+ middlewareInstalled = true;
95
+
96
+ globalThis.fetch = async function authFetch(input, init) {
97
+ const currentToken = token();
98
+ const opts = { ...init };
99
+
100
+ // Inject Bearer token if available
101
+ if (currentToken) {
102
+ opts.headers = new Headers(opts.headers || {});
103
+ if (!opts.headers.has('Authorization')) {
104
+ opts.headers.set('Authorization', `Bearer ${currentToken}`);
105
+ }
106
+ }
107
+
108
+ let response;
109
+ try {
110
+ response = await baseFetch(input, opts);
111
+ } catch (err) {
112
+ throw err;
113
+ }
114
+
115
+ // On 401, attempt token refresh and retry once
116
+ if (response.status === 401 && currentToken && refreshEndpoint) {
117
+ try {
118
+ await doRefresh(baseFetch);
119
+ // Retry with new token
120
+ const retryToken = token();
121
+ if (retryToken) {
122
+ const retryOpts = { ...init };
123
+ retryOpts.headers = new Headers(retryOpts.headers || {});
124
+ retryOpts.headers.set('Authorization', `Bearer ${retryToken}`);
125
+ return baseFetch(input, retryOpts);
126
+ }
127
+ } catch (_) {
128
+ // Refresh failed — return original 401 response
129
+ }
130
+ }
131
+
132
+ return response;
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Remove fetch middleware, restoring the original fetch.
138
+ */
139
+ function removeMiddleware() {
140
+ if (!middlewareInstalled) return;
141
+ if (typeof globalThis !== 'undefined' && originalFetch) {
142
+ globalThis.fetch = originalFetch;
143
+ }
144
+ middlewareInstalled = false;
145
+ }
146
+
147
+ // Install middleware immediately
148
+ installMiddleware();
149
+
150
+ /**
151
+ * Internal refresh using a specific fetch function (to avoid recursion through middleware).
152
+ * @param {Function} fetchFn
153
+ */
154
+ async function doRefresh(fetchFn) {
155
+ // Deduplicate concurrent refresh calls
156
+ if (refreshPromise) return refreshPromise;
157
+
158
+ refreshPromise = (async () => {
159
+ const currentToken = token();
160
+ try {
161
+ const res = await fetchFn(refreshEndpoint, {
162
+ method: 'POST',
163
+ headers: {
164
+ 'Content-Type': 'application/json',
165
+ ...(currentToken ? { 'Authorization': `Bearer ${currentToken}` } : {})
166
+ }
167
+ });
168
+ if (!res.ok) {
169
+ // Refresh failed — clear auth state
170
+ batch(() => {
171
+ setTokenRaw(null);
172
+ setUser(null);
173
+ });
174
+ throw new Error(`Refresh failed: ${res.status}`);
175
+ }
176
+ const data = await res.json();
177
+ batch(() => {
178
+ if (data.token !== undefined) setTokenRaw(data.token);
179
+ if (data.user !== undefined) setUser(data.user);
180
+ });
181
+ } finally {
182
+ refreshPromise = null;
183
+ }
184
+ })();
185
+
186
+ return refreshPromise;
187
+ }
188
+
189
+ /**
190
+ * Log in with credentials. POSTs to loginEndpoint.
191
+ * Expects response JSON: { token: string, user?: any }
192
+ * @param {Record<string, any>} credentials
193
+ * @returns {Promise<any>} The response data
194
+ */
195
+ async function login(credentials) {
196
+ setIsLoading(true);
197
+ setError(null);
198
+ try {
199
+ // Use originalFetch (or the base fetch before middleware) to avoid injecting a stale token
200
+ const fetchFn = originalFetch || globalThis.fetch;
201
+ const res = await fetchFn(loginEndpoint, {
202
+ method: 'POST',
203
+ headers: { 'Content-Type': 'application/json' },
204
+ body: JSON.stringify(credentials)
205
+ });
206
+ if (!res.ok) {
207
+ const errText = await res.text().catch(() => `Login failed: ${res.status}`);
208
+ let errData;
209
+ try { errData = JSON.parse(errText); } catch (_) { errData = { message: errText }; }
210
+ const loginError = new Error(errData.message || `Login failed: ${res.status}`);
211
+ loginError.status = res.status;
212
+ loginError.data = errData;
213
+ setError(loginError);
214
+ throw loginError;
215
+ }
216
+ const data = await res.json();
217
+ batch(() => {
218
+ if (data.token !== undefined) setTokenRaw(data.token);
219
+ if (data.user !== undefined) setUser(data.user);
220
+ setError(null);
221
+ });
222
+ return data;
223
+ } catch (err) {
224
+ if (!error()) setError(err);
225
+ throw err;
226
+ } finally {
227
+ setIsLoading(false);
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Log out. Optionally POSTs to logoutEndpoint, then clears local state.
233
+ * @returns {Promise<void>}
234
+ */
235
+ async function logout() {
236
+ setIsLoading(true);
237
+ setError(null);
238
+ try {
239
+ if (logoutEndpoint) {
240
+ const currentToken = token();
241
+ const fetchFn = originalFetch || globalThis.fetch;
242
+ try {
243
+ await fetchFn(logoutEndpoint, {
244
+ method: 'POST',
245
+ headers: {
246
+ 'Content-Type': 'application/json',
247
+ ...(currentToken ? { 'Authorization': `Bearer ${currentToken}` } : {})
248
+ }
249
+ });
250
+ } catch (_) {
251
+ // Best-effort — always clear local state even if server call fails
252
+ }
253
+ }
254
+ } finally {
255
+ batch(() => {
256
+ setTokenRaw(null);
257
+ setUser(null);
258
+ setError(null);
259
+ });
260
+ setIsLoading(false);
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Refresh the auth token. POSTs to refreshEndpoint.
266
+ * Expects response JSON: { token: string, user?: any }
267
+ * @returns {Promise<void>}
268
+ */
269
+ async function refresh() {
270
+ setIsLoading(true);
271
+ setError(null);
272
+ try {
273
+ const fetchFn = originalFetch || globalThis.fetch;
274
+ await doRefresh(fetchFn);
275
+ } catch (err) {
276
+ setError(err);
277
+ throw err;
278
+ } finally {
279
+ setIsLoading(false);
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Manually set the token (e.g., from an external auth provider).
285
+ * @param {string | null} newToken
286
+ */
287
+ function setToken(newToken) {
288
+ setTokenRaw(newToken);
289
+ }
290
+
291
+ /**
292
+ * Clean up effects and remove fetch middleware.
293
+ */
294
+ function destroy() {
295
+ removeMiddleware();
296
+ if (typeof authChangeDispose === 'function') {
297
+ authChangeDispose();
298
+ authChangeDispose = null;
299
+ }
300
+ }
301
+
302
+ return {
303
+ user,
304
+ token,
305
+ isAuthenticated,
306
+ isLoading,
307
+ error,
308
+ login,
309
+ logout,
310
+ refresh,
311
+ setUser,
312
+ setToken,
313
+ destroy
314
+ };
315
+ }
316
+
317
+ /**
318
+ * Install a beforeEach route guard that redirects unauthenticated users.
319
+ *
320
+ * @param {Object} router — A Decantr router instance (from createRouter)
321
+ * @param {{
322
+ * loginPath?: string,
323
+ * redirectParam?: string,
324
+ * isAuthenticated?: () => boolean
325
+ * }} [options]
326
+ */
327
+ export function requireAuth(router, options = {}) {
328
+ const {
329
+ loginPath = '/login',
330
+ redirectParam = 'redirect',
331
+ isAuthenticated = null
332
+ } = options;
333
+
334
+ if (!router || typeof router.onNavigate !== 'function') {
335
+ throw new Error('requireAuth expects a Decantr router instance');
336
+ }
337
+
338
+ // We use onNavigate + navigate pattern since the router's beforeEach
339
+ // is set at construction time. This guard intercepts navigations by
340
+ // hooking into the router's existing lifecycle.
341
+ // However, if the router exposes a _beforeEach slot, we can use it.
342
+ // Since createRouter accepts beforeEach in config, and we want to add
343
+ // a guard post-construction, we store a reference and use onNavigate
344
+ // to redirect after the fact.
345
+
346
+ // The most reliable approach: override the navigate function to add a check
347
+ const originalNavigate = router.navigate;
348
+ const routerCurrent = router.current;
349
+
350
+ router.navigate = function guardedNavigate(to, opts) {
351
+ // Resolve the target path
352
+ let targetPath;
353
+ if (typeof to === 'object' && to.name) {
354
+ // Named route — let the original resolve it; we need the path
355
+ // For named routes, we can't easily pre-check, so navigate and check via onNavigate
356
+ return originalNavigate(to, opts);
357
+ }
358
+ targetPath = typeof to === 'string' ? to : '/';
359
+
360
+ // Skip guard for the login page itself
361
+ if (targetPath === loginPath || targetPath.startsWith(loginPath + '?') || targetPath.startsWith(loginPath + '/')) {
362
+ return originalNavigate(to, opts);
363
+ }
364
+
365
+ // Check authentication
366
+ const authCheck = typeof isAuthenticated === 'function' ? isAuthenticated : null;
367
+ if (authCheck && !authCheck()) {
368
+ const redirectTo = loginPath + (redirectParam ? `?${redirectParam}=${encodeURIComponent(targetPath)}` : '');
369
+ return originalNavigate(redirectTo, { replace: true });
370
+ }
371
+
372
+ return originalNavigate(to, opts);
373
+ };
374
+
375
+ // Also handle initial navigation and popstate-driven navigation via onNavigate
376
+ const unsubscribe = router.onNavigate((to) => {
377
+ const authCheck = typeof isAuthenticated === 'function' ? isAuthenticated : null;
378
+ if (!authCheck) return;
379
+
380
+ // Skip if already on login page
381
+ if (to.path === loginPath || to.path.startsWith(loginPath + '/')) return;
382
+
383
+ // If not authenticated, redirect to login
384
+ if (!authCheck()) {
385
+ const redirectTo = loginPath + (redirectParam ? `?${redirectParam}=${encodeURIComponent(to.path)}` : '');
386
+ // Use setTimeout to avoid navigating during a navigation callback
387
+ setTimeout(() => originalNavigate(redirectTo, { replace: true }), 0);
388
+ }
389
+ });
390
+
391
+ // Return a cleanup function
392
+ return function removeGuard() {
393
+ router.navigate = originalNavigate;
394
+ unsubscribe();
395
+ };
396
+ }