@mindees/atlas 0.5.0 → 0.7.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/dist/index.d.ts CHANGED
@@ -14,7 +14,7 @@ import { Maturity, NotImplementedError, PackageInfo, notImplemented } from "@min
14
14
  /** The npm package name. */
15
15
  declare const name = "@mindees/atlas";
16
16
  /** The package version. All `@mindees/*` packages share one locked version line. */
17
- declare const VERSION = "0.5.0";
17
+ declare const VERSION = "0.7.0";
18
18
  /** Current maturity of this package. See the repository `STATUS.md`. */
19
19
  declare const maturity: Maturity;
20
20
  /**
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ import { NotImplementedError, notImplemented } from "@mindees/core";
13
13
  /** The npm package name. */
14
14
  const name = "@mindees/atlas";
15
15
  /** The package version. All `@mindees/*` packages share one locked version line. */
16
- const VERSION = "0.5.0";
16
+ const VERSION = "0.7.0";
17
17
  /** Current maturity of this package. See the repository `STATUS.md`. */
18
18
  const maturity = "experimental";
19
19
  /**
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * `@mindees/atlas` (Atlas) — accessible, signals-native UI primitives. Function components\n * over `@mindees/core`'s `createElement` that return renderer-agnostic `MindeesNode` trees:\n * web rendering is real via the Helix DOM backend; native is a labeled 🔬 research track (the\n * same serializable tree, interpreted by a native host later). A curated cross-platform\n * `StyleObject`, typed accessibility, and a structural theme (on the `@mindees/atlas/theme`\n * subpath). The virtualized recycling `List` is on the `@mindees/atlas/list` subpath.\n *\n * @module\n */\n\nimport type { Maturity, PackageInfo } from '@mindees/core'\nimport { NotImplementedError, notImplemented } from '@mindees/core'\n\n/** The npm package name. */\nexport const name = '@mindees/atlas'\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.5.0'\n\n/** Current maturity of this package. See the repository `STATUS.md`. */\nexport const maturity: Maturity = 'experimental'\n\n/**\n * Static identity + maturity metadata for this package. Frozen so the\n * self-reported identity tooling introspects cannot be mutated at runtime,\n * matching the `readonly` fields of {@link PackageInfo}.\n */\nexport const info: PackageInfo = Object.freeze({ name, version: VERSION, maturity })\n\nexport { type A11yProps, type A11yState, type Role, toA11yProps } from './a11y'\nexport {\n ActivityIndicator,\n type ActivityIndicatorProps,\n Avatar,\n type AvatarProps,\n Badge,\n type BadgeProps,\n Card,\n type CardProps,\n Chip,\n type ChipProps,\n Divider,\n type DividerProps,\n KeyboardAvoidingView,\n type KeyboardAvoidingViewProps,\n ProgressBar,\n type ProgressBarProps,\n SafeAreaView,\n type SafeAreaViewProps,\n Switch,\n type SwitchProps,\n} from './components'\nexport {\n type ColorScheme,\n getEnvironment,\n type KeyboardState,\n type PlatformEnvironment,\n type SafeAreaInsets,\n setEnvironment,\n useColorScheme,\n useKeyboard,\n useSafeAreaInsets,\n useWindowDimensions,\n type WindowDimensions,\n} from './environment'\nexport { type AttachableGesture, GestureView, type GestureViewProps } from './gesture'\nexport { type BaseProps, type Reactive, resolveStyle, toHostProps } from './host'\nexport { animateTo, motion } from './motion'\nexport {\n FocusScope,\n type FocusScopeProps,\n Modal,\n type ModalProps,\n} from './overlay'\nexport {\n Button,\n type ButtonProps,\n Column,\n Image,\n type ImageProps,\n type InteractionState,\n Pressable,\n type PressableProps,\n Row,\n ScrollView,\n type ScrollViewProps,\n Spacer,\n type SpacerProps,\n Stack,\n type StackProps,\n Text,\n TextInput,\n type TextInputProps,\n type TextProps,\n usePressable,\n View,\n type ViewProps,\n} from './primitives'\nexport { flattenStyle, type StyleInput, type StyleObject, type StyleValue } from './style'\nexport {\n duration,\n easing,\n fontSize,\n fontWeight,\n getTheme,\n lineHeight,\n palette,\n radius,\n space,\n type Theme,\n type ThemeColors,\n tokens,\n useTheme,\n} from './tokens'\nexport type { Maturity, PackageInfo }\nexport { NotImplementedError, notImplemented }\n"],"mappings":";;;;;;;;;;;;;AAeA,MAAa,OAAO;;AAGpB,MAAa,UAAU;;AAGvB,MAAa,WAAqB;;;;;;AAOlC,MAAa,OAAoB,OAAO,OAAO;CAAE;CAAM,SAAS;CAAS;AAAS,CAAC"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * `@mindees/atlas` (Atlas) — accessible, signals-native UI primitives. Function components\n * over `@mindees/core`'s `createElement` that return renderer-agnostic `MindeesNode` trees:\n * web rendering is real via the Helix DOM backend; native is a labeled 🔬 research track (the\n * same serializable tree, interpreted by a native host later). A curated cross-platform\n * `StyleObject`, typed accessibility, and a structural theme (on the `@mindees/atlas/theme`\n * subpath). The virtualized recycling `List` is on the `@mindees/atlas/list` subpath.\n *\n * @module\n */\n\nimport type { Maturity, PackageInfo } from '@mindees/core'\nimport { NotImplementedError, notImplemented } from '@mindees/core'\n\n/** The npm package name. */\nexport const name = '@mindees/atlas'\n\n/** The package version. All `@mindees/*` packages share one locked version line. */\nexport const VERSION = '0.7.0'\n\n/** Current maturity of this package. See the repository `STATUS.md`. */\nexport const maturity: Maturity = 'experimental'\n\n/**\n * Static identity + maturity metadata for this package. Frozen so the\n * self-reported identity tooling introspects cannot be mutated at runtime,\n * matching the `readonly` fields of {@link PackageInfo}.\n */\nexport const info: PackageInfo = Object.freeze({ name, version: VERSION, maturity })\n\nexport { type A11yProps, type A11yState, type Role, toA11yProps } from './a11y'\nexport {\n ActivityIndicator,\n type ActivityIndicatorProps,\n Avatar,\n type AvatarProps,\n Badge,\n type BadgeProps,\n Card,\n type CardProps,\n Chip,\n type ChipProps,\n Divider,\n type DividerProps,\n KeyboardAvoidingView,\n type KeyboardAvoidingViewProps,\n ProgressBar,\n type ProgressBarProps,\n SafeAreaView,\n type SafeAreaViewProps,\n Switch,\n type SwitchProps,\n} from './components'\nexport {\n type ColorScheme,\n getEnvironment,\n type KeyboardState,\n type PlatformEnvironment,\n type SafeAreaInsets,\n setEnvironment,\n useColorScheme,\n useKeyboard,\n useSafeAreaInsets,\n useWindowDimensions,\n type WindowDimensions,\n} from './environment'\nexport { type AttachableGesture, GestureView, type GestureViewProps } from './gesture'\nexport { type BaseProps, type Reactive, resolveStyle, toHostProps } from './host'\nexport { animateTo, motion } from './motion'\nexport {\n FocusScope,\n type FocusScopeProps,\n Modal,\n type ModalProps,\n} from './overlay'\nexport {\n Button,\n type ButtonProps,\n Column,\n Image,\n type ImageProps,\n type InteractionState,\n Pressable,\n type PressableProps,\n Row,\n ScrollView,\n type ScrollViewProps,\n Spacer,\n type SpacerProps,\n Stack,\n type StackProps,\n Text,\n TextInput,\n type TextInputProps,\n type TextProps,\n usePressable,\n View,\n type ViewProps,\n} from './primitives'\nexport { flattenStyle, type StyleInput, type StyleObject, type StyleValue } from './style'\nexport {\n duration,\n easing,\n fontSize,\n fontWeight,\n getTheme,\n lineHeight,\n palette,\n radius,\n space,\n type Theme,\n type ThemeColors,\n tokens,\n useTheme,\n} from './tokens'\nexport type { Maturity, PackageInfo }\nexport { NotImplementedError, notImplemented }\n"],"mappings":";;;;;;;;;;;;;AAeA,MAAa,OAAO;;AAGpB,MAAa,UAAU;;AAGvB,MAAa,WAAqB;;;;;;AAOlC,MAAa,OAAoB,OAAO,OAAO;CAAE;CAAM,SAAS;CAAS;AAAS,CAAC"}
@@ -0,0 +1,33 @@
1
+ import { StyleObject } from "./style.js";
2
+ import { Component } from "@mindees/core";
3
+ import { Router } from "@mindees/router";
4
+
5
+ //#region src/stack.d.ts
6
+ /** Which role a card plays in the running transition. */
7
+ type StackLayer = 'entering' | 'leaving';
8
+ /** A custom card-style interpolator (RN `cardStyleInterpolator` parity). */
9
+ type StackInterpolator = (progress: () => number, layer: StackLayer, width: () => number) => () => StyleObject;
10
+ /** Built-in transition presets. */
11
+ type TransitionPreset = 'slide' | 'fade' | 'none';
12
+ /** Options for {@link createStackNavigator} (factory defaults) and the returned component (per-render). */
13
+ interface StackNavigatorOptions {
14
+ readonly notFound?: Component;
15
+ readonly transition?: TransitionPreset | StackInterpolator;
16
+ readonly gestureEnabled?: boolean;
17
+ readonly edgeWidth?: number;
18
+ readonly popThreshold?: number;
19
+ readonly flingVelocity?: number;
20
+ readonly width?: () => number;
21
+ }
22
+ /**
23
+ * Create a stack navigator {@link Component} bound to `router`. Render it via `createElement` (so the
24
+ * renderer owns its reactive scope and disposes it on unmount) instead of `createRouterView(router)`.
25
+ *
26
+ * @example
27
+ * const Stack = createStackNavigator(router, { transition: 'slide' })
28
+ * render(createElement(Stack, { notFound: NotFound }), backend, root)
29
+ */
30
+ declare function createStackNavigator(router: Router, defaults?: StackNavigatorOptions): Component<StackNavigatorOptions>;
31
+ //#endregion
32
+ export { StackInterpolator, StackLayer, StackNavigatorOptions, TransitionPreset, createStackNavigator };
33
+ //# sourceMappingURL=stack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stack.d.ts","names":[],"sources":["../src/stack.ts"],"mappings":";;;;;AAuDsB;AAAA,KAPV,UAAA;;KAGA,iBAAA,IACV,QAAA,gBACA,KAAA,EAAO,UAAA,EACP,KAAA,yBACS,WAAW;;KAGV,gBAAA;AAGZ;AAAA,UAAiB,qBAAA;EAAA,SACN,QAAA,GAAW,SAAA;EAAA,SACX,UAAA,GAAa,gBAAA,GAAmB,iBAAA;EAAA,SAChC,cAAA;EAAA,SACA,SAAA;EAAA,SACA,YAAA;EAAA,SACA,aAAA;EAAA,SACA,KAAA;AAAA;;;;;;;;;iBAyDK,oBAAA,CACd,MAAA,EAAQ,MAAA,EACR,QAAA,GAAU,qBAAA,GACT,SAAA,CAAU,qBAAA"}
package/dist/stack.js ADDED
@@ -0,0 +1,238 @@
1
+ import { useWindowDimensions } from "./environment.js";
2
+ import { View } from "./primitives.js";
3
+ import { GestureView } from "./gesture.js";
4
+ import { animateTo } from "./motion.js";
5
+ import { animate, createElement, effect, interpolate, keyedRegion, pan, signal, spring, untrack } from "@mindees/core";
6
+ import { createHref } from "@mindees/router";
7
+ //#region src/stack.ts
8
+ /**
9
+ * Atlas `createStackNavigator` — animated stack navigation over the Quantum router, composing the
10
+ * keyed reconciler + animation engine + gesture system. A drop-in superset of `createRouterView`:
11
+ * pushing a route slides/fades the new screen in over the old; back reverses it; an edge swipe-back
12
+ * gesture drives the pop interactively (release past a threshold completes it, else cancels).
13
+ *
14
+ * Mechanism (no new core/renderer/router surface): screens live in a {@link keyedRegion} keyed by a
15
+ * per-entry key, so a surviving screen is reused (state/scroll preserved) and a departed one is
16
+ * disposed exactly when its key leaves the rendered set. ONE progress {@link animate}d value drives
17
+ * both cards' transforms via {@link interpolate} (one batch/frame → glitch-free). With no frame
18
+ * source (SSR/headless) the transition jumps to its end, so output is the destination instantly.
19
+ *
20
+ * v1 limitation: a navigation that changes only params/search of the CURRENT screen (same route) is a
21
+ * `snap` (instant) and remounts the screen — full in-screen param-state preservation is a follow-up.
22
+ *
23
+ * @module
24
+ */
25
+ const PRESETS = {
26
+ slide: (progress, layer, width) => {
27
+ if (layer === "entering") {
28
+ const clamp = (p) => p < 0 ? 0 : p > 1 ? 1 : p;
29
+ return () => ({ transform: `translateX(${(1 - clamp(progress())) * width()}px)` });
30
+ }
31
+ const clamp = (p) => p < 0 ? 0 : p > 1 ? 1 : p;
32
+ return () => ({ transform: `translateX(${-clamp(progress()) * width() * .3}px)` });
33
+ },
34
+ fade: (progress, layer) => {
35
+ const o = layer === "entering" ? interpolate(progress, [0, 1], [0, 1]) : interpolate(progress, [0, 1], [1, 0]);
36
+ return () => ({ opacity: o() });
37
+ },
38
+ none: () => () => ({})
39
+ };
40
+ const resolvePreset = (t) => typeof t === "function" ? t : PRESETS[t ?? "slide"];
41
+ /** Render a route-match chain FROZEN to `matches` (the leaving card keeps its own content). */
42
+ function renderChain(matches, router, notFound) {
43
+ const build = (depth) => {
44
+ const m = matches[depth];
45
+ if (!m) return depth === 0 && notFound ? createElement(notFound, {}) : null;
46
+ const child = build(depth + 1);
47
+ const component = m.route.component;
48
+ if (component === void 0) return child;
49
+ return createElement(component, {
50
+ router,
51
+ params: () => m.params,
52
+ search: () => m.search,
53
+ data: () => router.loaderData(m),
54
+ children: child
55
+ });
56
+ };
57
+ return build(0);
58
+ }
59
+ /**
60
+ * Create a stack navigator {@link Component} bound to `router`. Render it via `createElement` (so the
61
+ * renderer owns its reactive scope and disposes it on unmount) instead of `createRouterView(router)`.
62
+ *
63
+ * @example
64
+ * const Stack = createStackNavigator(router, { transition: 'slide' })
65
+ * render(createElement(Stack, { notFound: NotFound }), backend, root)
66
+ */
67
+ function createStackNavigator(router, defaults = {}) {
68
+ return (props = {}) => {
69
+ const opts = {
70
+ ...defaults,
71
+ ...props
72
+ };
73
+ const interp = resolvePreset(opts.transition);
74
+ const gestureEnabled = opts.gestureEnabled !== false;
75
+ const edgeWidth = opts.edgeWidth ?? 30;
76
+ const popThreshold = opts.popThreshold ?? .5;
77
+ const flingVelocity = opts.flingVelocity ?? .3;
78
+ const dims = useWindowDimensions();
79
+ const width = opts.width ?? (() => dims().width || 360);
80
+ const progress = animate(1);
81
+ const stack = signal([]);
82
+ const anim = signal(null);
83
+ let navCounter = 0;
84
+ let gen = 0;
85
+ const hrefOf = () => createHref(router.location());
86
+ const entryFor = (href, matches) => ({
87
+ key: `${href}#${++navCounter}`,
88
+ href,
89
+ matches
90
+ });
91
+ const commitTo = (next) => {
92
+ anim.set(null);
93
+ stack.set(next);
94
+ progress.set(1);
95
+ };
96
+ const classify = () => {
97
+ const matches = router.matches();
98
+ const href = hrefOf();
99
+ const cur = stack();
100
+ if (cur.length === 0) {
101
+ stack.set([entryFor(href, matches)]);
102
+ progress.set(1);
103
+ return;
104
+ }
105
+ const top = cur[cur.length - 1];
106
+ if (anim() === null && top.href === href) return;
107
+ const below = cur[cur.length - 2];
108
+ if (below && below.href === href) {
109
+ const g = ++gen;
110
+ anim.set({
111
+ lower: below,
112
+ upper: top,
113
+ enteringIsUpper: false
114
+ });
115
+ progress.set(1);
116
+ animateTo(progress, 0, { onComplete: (finished) => {
117
+ if (g === gen && finished) commitTo(cur.slice(0, -1));
118
+ } });
119
+ } else if (!cur.some((e) => e.href === href)) {
120
+ const entering = entryFor(href, matches);
121
+ const g = ++gen;
122
+ anim.set({
123
+ lower: top,
124
+ upper: entering,
125
+ enteringIsUpper: true
126
+ });
127
+ progress.set(0);
128
+ animateTo(progress, 1, { onComplete: (finished) => {
129
+ if (g === gen && finished) commitTo([...cur, entering]);
130
+ } });
131
+ } else {
132
+ ++gen;
133
+ commitTo([entryFor(href, matches)]);
134
+ }
135
+ };
136
+ effect(() => {
137
+ router.matches();
138
+ router.location();
139
+ untrack(classify);
140
+ });
141
+ const visibleEntries = () => {
142
+ const a = anim();
143
+ if (a) return [a.lower, a.upper];
144
+ const s = stack();
145
+ const t = s[s.length - 1];
146
+ return t ? [t] : [];
147
+ };
148
+ const layerFor = (entry) => {
149
+ const a = anim();
150
+ if (!a) return "entering";
151
+ return entry.key === a.upper.key === a.enteringIsUpper ? "entering" : "leaving";
152
+ };
153
+ let swiping = false;
154
+ const startSwipe = () => {
155
+ const cur = stack();
156
+ if (cur.length < 2) return;
157
+ const top = cur[cur.length - 1];
158
+ const below = cur[cur.length - 2];
159
+ ++gen;
160
+ swiping = true;
161
+ anim.set({
162
+ lower: below,
163
+ upper: top,
164
+ enteringIsUpper: false
165
+ });
166
+ };
167
+ const swipeGesture = pan({
168
+ axis: "x",
169
+ minDistance: 4,
170
+ onBegin: (e) => {
171
+ if (e.x - e.translationX > edgeWidth) return;
172
+ startSwipe();
173
+ },
174
+ onUpdate: (e) => {
175
+ if (!swiping) return;
176
+ const p = 1 - e.translationX / Math.max(width(), 1);
177
+ progress.set(p < 0 ? 0 : p > 1 ? 1 : p);
178
+ },
179
+ onEnd: (e) => {
180
+ if (!swiping) return;
181
+ swiping = false;
182
+ const cur = stack();
183
+ const shouldPop = progress() < popThreshold || e.velocityX > flingVelocity;
184
+ const g = ++gen;
185
+ if (shouldPop) spring(progress, {
186
+ to: 0,
187
+ velocity: -e.velocityX * 1e3 / Math.max(width(), 1),
188
+ onComplete: (finished) => {
189
+ if (g !== gen || !finished) return;
190
+ commitTo(cur.slice(0, -1));
191
+ router.history.back();
192
+ }
193
+ });
194
+ else spring(progress, {
195
+ to: 1,
196
+ velocity: -e.velocityX * 1e3 / Math.max(width(), 1),
197
+ onComplete: (finished) => {
198
+ if (g === gen && finished) anim.set(null);
199
+ }
200
+ });
201
+ }
202
+ });
203
+ const ScreenCard = (entry) => {
204
+ const enteringStyle = interp(() => progress(), "entering", width);
205
+ const leavingStyle = interp(() => progress(), "leaving", width);
206
+ const cardStyle = () => ({
207
+ position: "absolute",
208
+ top: 0,
209
+ right: 0,
210
+ bottom: 0,
211
+ left: 0,
212
+ ...layerFor(entry) === "entering" ? enteringStyle() : leavingStyle()
213
+ });
214
+ return createElement(View, { style: cardStyle }, renderChain(entry.matches, router, opts.notFound));
215
+ };
216
+ const region = keyedRegion({
217
+ each: visibleEntries,
218
+ key: (e) => e.key,
219
+ children: (item) => ScreenCard(item())
220
+ });
221
+ const containerStyle = () => ({
222
+ position: "relative",
223
+ width: "100%",
224
+ height: "100%",
225
+ overflow: "hidden"
226
+ });
227
+ if (gestureEnabled) return GestureView({
228
+ gesture: swipeGesture,
229
+ style: containerStyle,
230
+ children: region
231
+ });
232
+ return createElement(View, { style: containerStyle }, region);
233
+ };
234
+ }
235
+ //#endregion
236
+ export { createStackNavigator };
237
+
238
+ //# sourceMappingURL=stack.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stack.js","names":[],"sources":["../src/stack.ts"],"sourcesContent":["/**\n * Atlas `createStackNavigator` — animated stack navigation over the Quantum router, composing the\n * keyed reconciler + animation engine + gesture system. A drop-in superset of `createRouterView`:\n * pushing a route slides/fades the new screen in over the old; back reverses it; an edge swipe-back\n * gesture drives the pop interactively (release past a threshold completes it, else cancels).\n *\n * Mechanism (no new core/renderer/router surface): screens live in a {@link keyedRegion} keyed by a\n * per-entry key, so a surviving screen is reused (state/scroll preserved) and a departed one is\n * disposed exactly when its key leaves the rendered set. ONE progress {@link animate}d value drives\n * both cards' transforms via {@link interpolate} (one batch/frame → glitch-free). With no frame\n * source (SSR/headless) the transition jumps to its end, so output is the destination instantly.\n *\n * v1 limitation: a navigation that changes only params/search of the CURRENT screen (same route) is a\n * `snap` (instant) and remounts the screen — full in-screen param-state preservation is a follow-up.\n *\n * @module\n */\n\nimport {\n animate,\n type Component,\n createElement,\n effect,\n interpolate,\n keyedRegion,\n type MindeesNode,\n pan,\n type Signal,\n signal,\n spring,\n untrack,\n} from '@mindees/core'\nimport { createHref, type RouteMatch, type Router } from '@mindees/router'\nimport { useWindowDimensions } from './environment'\nimport { GestureView } from './gesture'\nimport type { Reactive } from './host'\nimport { animateTo } from './motion'\nimport { View } from './primitives'\nimport type { StyleInput, StyleObject } from './style'\n\n/** A frozen snapshot of one screen in the stack. */\ninterface StackEntry {\n readonly key: string\n readonly href: string\n readonly matches: readonly RouteMatch[]\n}\n\n/** Which role a card plays in the running transition. */\nexport type StackLayer = 'entering' | 'leaving'\n\n/** A custom card-style interpolator (RN `cardStyleInterpolator` parity). */\nexport type StackInterpolator = (\n progress: () => number,\n layer: StackLayer,\n width: () => number,\n) => () => StyleObject\n\n/** Built-in transition presets. */\nexport type TransitionPreset = 'slide' | 'fade' | 'none'\n\n/** Options for {@link createStackNavigator} (factory defaults) and the returned component (per-render). */\nexport interface StackNavigatorOptions {\n readonly notFound?: Component\n readonly transition?: TransitionPreset | StackInterpolator\n readonly gestureEnabled?: boolean\n readonly edgeWidth?: number\n readonly popThreshold?: number\n readonly flingVelocity?: number\n readonly width?: () => number\n}\n\nconst PRESETS: Record<TransitionPreset, StackInterpolator> = {\n slide: (progress, layer, width) => {\n // Read width() INSIDE the style fn so the transform tracks a window resize (not captured once).\n if (layer === 'entering') {\n const clamp = (p: number) => (p < 0 ? 0 : p > 1 ? 1 : p)\n return () => ({ transform: `translateX(${(1 - clamp(progress())) * width()}px)` })\n }\n const clamp = (p: number) => (p < 0 ? 0 : p > 1 ? 1 : p)\n return () => ({ transform: `translateX(${-clamp(progress()) * width() * 0.3}px)` })\n },\n fade: (progress, layer) => {\n const o =\n layer === 'entering'\n ? interpolate(progress, [0, 1], [0, 1])\n : interpolate(progress, [0, 1], [1, 0])\n return () => ({ opacity: o() })\n },\n none: () => () => ({}),\n}\n\nconst resolvePreset = (t: TransitionPreset | StackInterpolator | undefined): StackInterpolator =>\n typeof t === 'function' ? t : PRESETS[t ?? 'slide']\n\n/** Render a route-match chain FROZEN to `matches` (the leaving card keeps its own content). */\nfunction renderChain(\n matches: readonly RouteMatch[],\n router: Router,\n notFound?: Component,\n): MindeesNode {\n const build = (depth: number): MindeesNode => {\n const m = matches[depth]\n if (!m) return depth === 0 && notFound ? createElement(notFound, {}) : null\n const child = build(depth + 1)\n const component = m.route.component\n if (component === undefined) return child\n return createElement(component, {\n router,\n params: () => m.params,\n search: () => m.search,\n data: () => router.loaderData(m),\n children: child,\n })\n }\n return build(0)\n}\n\n/**\n * Create a stack navigator {@link Component} bound to `router`. Render it via `createElement` (so the\n * renderer owns its reactive scope and disposes it on unmount) instead of `createRouterView(router)`.\n *\n * @example\n * const Stack = createStackNavigator(router, { transition: 'slide' })\n * render(createElement(Stack, { notFound: NotFound }), backend, root)\n */\nexport function createStackNavigator(\n router: Router,\n defaults: StackNavigatorOptions = {},\n): Component<StackNavigatorOptions> {\n return (props: StackNavigatorOptions = {}) => {\n const opts = { ...defaults, ...props }\n const interp = resolvePreset(opts.transition)\n const gestureEnabled = opts.gestureEnabled !== false\n const edgeWidth = opts.edgeWidth ?? 30\n const popThreshold = opts.popThreshold ?? 0.5\n const flingVelocity = opts.flingVelocity ?? 0.3\n const dims = useWindowDimensions()\n const width = opts.width ?? (() => dims().width || 360)\n\n // Reactive state (the classify effect, animation drivers, the gesture) is owned by the renderer's\n // root — this is a COMPONENT, rendered via `createElement(Stack, props)` — so everything disposes\n // on unmount with no orphan root. (Animations auto-stop via their owner; GestureView resets the\n // gesture on cleanup.)\n const progress = animate(1) // 1 = top settled in, 0 = top settled out\n const stack: Signal<StackEntry[]> = signal([])\n // The running transition's two cards, or null when settled. `lower` paints under `upper`.\n const anim = signal<{\n lower: StackEntry\n upper: StackEntry\n enteringIsUpper: boolean\n } | null>(null)\n let navCounter = 0\n let gen = 0 // interruption generation: a stale onComplete no-ops\n\n const hrefOf = (): string => createHref(router.location())\n const entryFor = (href: string, matches: readonly RouteMatch[]): StackEntry => ({\n key: `${href}#${++navCounter}`,\n href,\n matches,\n })\n\n const commitTo = (next: StackEntry[]): void => {\n anim.set(null)\n stack.set(next)\n progress.set(1)\n }\n\n const classify = (): void => {\n const matches = router.matches()\n const href = hrefOf()\n const cur = stack()\n if (cur.length === 0) {\n stack.set([entryFor(href, matches)]) // seed (first render / deep-link), no animation\n progress.set(1)\n return\n }\n const top = cur[cur.length - 1] as StackEntry\n // Only treat same-location as a no-op when SETTLED. Mid-transition, a nav back to the\n // committed top (e.g. interrupting a push with a back-to-origin) must reconcile (→ SNAP via\n // the gen bump below), not be swallowed — otherwise the URL and the on-screen card desync.\n if (anim() === null && top.href === href) return\n const below = cur[cur.length - 2]\n if (below && below.href === href) {\n // POP (programmatic back): animate top out, then drop it.\n const g = ++gen\n anim.set({ lower: below, upper: top, enteringIsUpper: false })\n progress.set(1)\n animateTo(progress, 0, {\n onComplete: (finished) => {\n if (g === gen && finished) commitTo(cur.slice(0, -1))\n },\n })\n } else if (!cur.some((e) => e.href === href)) {\n // PUSH: animate the new screen in over the old, then keep only the new on screen.\n const entering = entryFor(href, matches)\n const g = ++gen\n anim.set({ lower: top, upper: entering, enteringIsUpper: true })\n progress.set(0)\n animateTo(progress, 1, {\n onComplete: (finished) => {\n if (g === gen && finished) commitTo([...cur, entering])\n },\n })\n } else {\n // Replace / go(±n) / ambiguous → SNAP (instant), also the SSR / 'none' path.\n ++gen\n commitTo([entryFor(href, matches)])\n }\n }\n\n effect(() => {\n router.matches() // track location/matches\n router.location()\n untrack(classify)\n })\n\n const visibleEntries = (): StackEntry[] => {\n const a = anim()\n if (a) return [a.lower, a.upper] // lower painted first (under), upper on top\n const s = stack()\n const t = s[s.length - 1]\n return t ? [t] : []\n }\n\n const layerFor = (entry: StackEntry): StackLayer => {\n const a = anim()\n if (!a) return 'entering'\n const isUpper = entry.key === a.upper.key\n return isUpper === a.enteringIsUpper ? 'entering' : 'leaving'\n }\n\n // --- swipe-back (edge pan → drive progress → spring complete/cancel) ---\n // `swiping` gates onUpdate/onEnd so a stray touch DURING a programmatic push/pop (anim is also\n // set then) can't hijack it — only an edge swipe that passed onBegin drives the gesture.\n let swiping = false\n const startSwipe = (): void => {\n const cur = stack()\n if (cur.length < 2) return\n const top = cur[cur.length - 1] as StackEntry\n const below = cur[cur.length - 2] as StackEntry\n ++gen // take over any running transition\n swiping = true\n anim.set({ lower: below, upper: top, enteringIsUpper: false })\n }\n const swipeGesture = pan({\n axis: 'x',\n minDistance: 4,\n onBegin: (e) => {\n if (e.x - e.translationX > edgeWidth) return // not an edge swipe — ignore\n startSwipe()\n },\n onUpdate: (e) => {\n if (!swiping) return\n const p = 1 - e.translationX / Math.max(width(), 1)\n progress.set(p < 0 ? 0 : p > 1 ? 1 : p)\n },\n onEnd: (e) => {\n if (!swiping) return\n swiping = false\n const cur = stack()\n const shouldPop = progress() < popThreshold || e.velocityX > flingVelocity\n const g = ++gen\n if (shouldPop) {\n spring(progress, {\n to: 0,\n velocity: (-e.velocityX * 1000) / Math.max(width(), 1),\n onComplete: (finished) => {\n if (g !== gen || !finished) return\n commitTo(cur.slice(0, -1)) // local commit first…\n router.history.back() // …then sync history (classify() then no-ops)\n },\n })\n } else {\n spring(progress, {\n to: 1,\n velocity: (-e.velocityX * 1000) / Math.max(width(), 1),\n onComplete: (finished) => {\n if (g === gen && finished) anim.set(null) // cancel: dispose the peeked card\n },\n })\n }\n },\n })\n\n const ScreenCard = (entry: StackEntry): MindeesNode => {\n // A reused card flips entering↔leaving during a transition, so the layer (and thus the\n // interpolator) must be read REACTIVELY — both style accessors are built once and switched.\n const enteringStyle = interp(() => progress(), 'entering', width)\n const leavingStyle = interp(() => progress(), 'leaving', width)\n const cardStyle: Reactive<StyleInput> = () => ({\n position: 'absolute',\n top: 0,\n right: 0,\n bottom: 0,\n left: 0,\n ...(layerFor(entry) === 'entering' ? enteringStyle() : leavingStyle()),\n })\n return createElement(\n View,\n { style: cardStyle },\n renderChain(entry.matches, router, opts.notFound),\n )\n }\n\n // The container holds the layered cards; the keyed region mounts/disposes them by key. The\n // swipe-back gesture lives on the CONTAINER (pointer events from any card bubble up), so it is\n // independent of which card is on top.\n const region = keyedRegion({\n each: visibleEntries,\n key: (e: StackEntry) => e.key,\n children: (item: () => StackEntry) => ScreenCard(item()),\n })\n const containerStyle: Reactive<StyleInput> = () => ({\n position: 'relative',\n width: '100%',\n height: '100%',\n overflow: 'hidden',\n })\n if (gestureEnabled) {\n return GestureView({ gesture: swipeGesture, style: containerStyle, children: region })\n }\n return createElement(View, { style: containerStyle }, region)\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindees/atlas",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "MindeesNative Atlas - accessible, signals-native UI primitives + a virtualized recycling list. Renderer-agnostic (web real, native research track).",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "type": "module",
@@ -24,6 +24,10 @@
24
24
  "./for": {
25
25
  "types": "./dist/for.d.ts",
26
26
  "import": "./dist/for.js"
27
+ },
28
+ "./stack": {
29
+ "types": "./dist/stack.d.ts",
30
+ "import": "./dist/stack.js"
27
31
  }
28
32
  },
29
33
  "publishConfig": {
@@ -35,11 +39,12 @@
35
39
  "directory": "packages/atlas"
36
40
  },
37
41
  "dependencies": {
38
- "@mindees/core": "0.5.0"
42
+ "@mindees/core": "0.7.0",
43
+ "@mindees/router": "0.7.0"
39
44
  },
40
45
  "devDependencies": {
41
46
  "happy-dom": "20.9.0",
42
- "@mindees/renderer": "0.5.0"
47
+ "@mindees/renderer": "0.7.0"
43
48
  },
44
49
  "scripts": {
45
50
  "build": "tsdown",