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,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
|
+
}
|