@octane-xplat/ui 0.3.0 → 0.4.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/README.md +46 -0
- package/dist/native/octane-client-build.json +1 -1
- package/dist/native/ui.js +52 -40
- package/dist/native/ui10.js +20 -11
- package/dist/native/ui11.js +42 -109
- package/dist/native/ui12.js +17 -84
- package/dist/native/ui13.js +11 -41
- package/dist/native/ui14.js +118 -76
- package/dist/native/ui15.js +43 -92
- package/dist/native/ui16.js +106 -45
- package/dist/native/ui17.js +29 -42
- package/dist/native/ui18.js +34 -53
- package/dist/native/ui19.js +246 -32
- package/dist/native/ui20.js +27 -16
- package/dist/native/ui21.js +376 -86
- package/dist/native/ui22.js +26 -114
- package/dist/native/ui23.js +49 -97
- package/dist/native/ui24.js +78 -53
- package/dist/native/ui25.js +138 -16
- package/dist/native/ui26.js +8 -23
- package/dist/native/ui27.js +60 -34
- package/dist/native/ui28.js +36 -142
- package/dist/native/ui29.js +101 -21
- package/dist/native/ui30.js +14 -31
- package/dist/native/ui31.js +55 -36
- package/dist/native/ui32.js +49 -61
- package/dist/native/ui33.js +36 -28
- package/dist/native/ui34.js +51 -8
- package/dist/native/ui35.js +98 -20
- package/dist/native/ui36.js +9 -33
- package/dist/native/ui37.js +42 -15
- package/dist/native/ui38.js +186 -57
- package/dist/native/ui39.js +96 -24
- package/dist/native/ui4.js +22 -20
- package/dist/native/ui40.js +57 -13
- package/dist/native/ui41.js +15 -100
- package/dist/native/ui42.js +6 -146
- package/dist/native/ui43.js +37 -21
- package/dist/native/ui44.js +265 -6
- package/dist/native/ui45.js +28 -0
- package/dist/native/ui46.js +35 -0
- package/dist/native/ui47.js +41 -0
- package/dist/native/ui48.js +72 -0
- package/dist/native/ui49.js +66 -0
- package/dist/native/ui5.js +37 -77
- package/dist/native/ui50.js +27 -0
- package/dist/native/ui51.js +13 -0
- package/dist/native/ui52.js +92 -0
- package/dist/native/ui53.js +34 -0
- package/dist/native/ui54.js +17 -0
- package/dist/native/ui55.js +156 -0
- package/dist/native/ui56.js +33 -0
- package/dist/native/ui57.js +14 -0
- package/dist/native/ui58.js +22 -0
- package/dist/native/ui59.js +8 -0
- package/dist/native/ui6.js +52 -9
- package/dist/native/ui60.js +108 -0
- package/dist/native/ui61.js +76 -0
- package/dist/native/ui7.js +42 -26
- package/dist/native/ui8.js +22 -12
- package/dist/native/ui9.js +18 -9
- package/dist/web/octane-client-build.json +1 -1
- package/dist/web/ui.js +51 -40
- package/dist/web/ui10.js +33 -186
- package/dist/web/ui11.js +28 -409
- package/dist/web/ui12.js +21 -68
- package/dist/web/ui13.js +201 -97
- package/dist/web/ui14.js +85 -95
- package/dist/web/ui15.js +368 -25
- package/dist/web/ui16.js +13 -101
- package/dist/web/ui17.js +246 -42
- package/dist/web/ui18.js +237 -28
- package/dist/web/ui19.js +36 -10
- package/dist/web/ui20.js +73 -102
- package/dist/web/ui21.js +102 -73
- package/dist/web/ui22.js +95 -55
- package/dist/web/ui23.js +94 -55
- package/dist/web/ui24.js +31 -15
- package/dist/web/ui25.js +105 -19
- package/dist/web/ui26.js +29 -23
- package/dist/web/ui27.js +12 -123
- package/dist/web/ui28.js +9 -110
- package/dist/web/ui29.js +52 -102
- package/dist/web/ui3.js +29 -18
- package/dist/web/ui30.js +32 -41
- package/dist/web/ui31.js +120 -39
- package/dist/web/ui32.js +41 -74
- package/dist/web/ui33.js +148 -32
- package/dist/web/ui34.js +54 -51
- package/dist/web/ui35.js +55 -7
- package/dist/web/ui36.js +18 -41
- package/dist/web/ui37.js +6 -46
- package/dist/web/ui38.js +37 -16
- package/dist/web/ui39.js +135 -81
- package/dist/web/ui4.js +46 -152
- package/dist/web/ui40.js +39 -37
- package/dist/web/ui41.js +41 -16
- package/dist/web/ui42.js +74 -7
- package/dist/web/ui43.js +40 -20
- package/dist/web/ui44.js +60 -6
- package/dist/web/ui45.js +9 -0
- package/dist/web/ui46.js +45 -0
- package/dist/web/ui47.js +13 -0
- package/dist/web/ui48.js +63 -0
- package/dist/web/ui49.js +47 -0
- package/dist/web/ui5.js +131 -27
- package/dist/web/ui50.js +17 -0
- package/dist/web/ui51.js +149 -0
- package/dist/web/ui52.js +42 -0
- package/dist/web/ui53.js +17 -0
- package/dist/web/ui54.js +9 -0
- package/dist/web/ui55.js +22 -0
- package/dist/web/ui56.js +8 -0
- package/dist/web/ui57.js +44 -0
- package/dist/web/ui58.js +46 -0
- package/dist/web/ui6.js +81 -45
- package/dist/web/ui7.js +43 -21
- package/dist/web/ui8.js +27 -21
- package/dist/web/ui9.js +29 -18
- package/package.json +13 -4
- package/src/theme/tokens.css +369 -138
- package/types/index.d.ts +102 -58
- package/types/props.d.ts +233 -13
package/dist/web/ui17.js
CHANGED
|
@@ -1,45 +1,249 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
1
|
+
//#region src/route-table.ts
|
|
2
|
+
var EXT = /\.(tsrx|tsx|ts|mts|cts|js|mjs|cjs|jsx)$/;
|
|
3
|
+
var SUFFIX = /\.(web|native|ios|android)$/;
|
|
4
|
+
var PARAM = /^\[(.+)\]$/;
|
|
5
|
+
var PRESENT = /\+(modal|fade|push)$/;
|
|
6
|
+
/** Internal error carrier used by the public `redirect()` helper. Keeping
|
|
7
|
+
* this in the shared route table gives both platform leaves the same guard
|
|
8
|
+
* behavior without importing a platform runtime. */
|
|
9
|
+
var RouteRedirect = class extends Error {
|
|
10
|
+
route;
|
|
11
|
+
constructor(route) {
|
|
12
|
+
super("route redirect");
|
|
13
|
+
this.route = route;
|
|
14
|
+
this.name = "RouteRedirect";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
/** Component pick rule for route/layout modules: default export, then a
|
|
18
|
+
* `screen` named export, then a single function export. Route files use
|
|
19
|
+
* named exports (decision #13) — a lone component is unambiguous.
|
|
20
|
+
* route-config exports are reserved metadata, never the screen. */
|
|
21
|
+
function pick(mod, file) {
|
|
22
|
+
if (typeof mod?.default === "function") return mod.default;
|
|
23
|
+
if (typeof mod?.screen === "function") return mod.screen;
|
|
24
|
+
const fns = Object.keys(mod ?? {}).filter((k) => typeof mod[k] === "function" && ![
|
|
25
|
+
"loader",
|
|
26
|
+
"beforeLoad",
|
|
27
|
+
"head"
|
|
28
|
+
].includes(k));
|
|
29
|
+
if (fns.length === 1) return mod[fns[0]];
|
|
30
|
+
console.warn("[octane-xplat] route file " + file + (fns.length ? ` has ${fns.length} component-shaped exports (${fns.join(", ")}) — add a default or 'screen' export` : " has no component export") + " — skipped");
|
|
31
|
+
}
|
|
32
|
+
function deriveRouteManifest(files, prefer, dir = "app") {
|
|
33
|
+
const prefix = dir.replace(/^\.?\/?/, "").replace(/\/+$/, "") + "/";
|
|
34
|
+
const seen = /* @__PURE__ */ new Map();
|
|
35
|
+
const layoutRank = /* @__PURE__ */ new Map();
|
|
36
|
+
const layouts = {};
|
|
37
|
+
const loaders = {};
|
|
38
|
+
const warned = /* @__PURE__ */ new Set();
|
|
39
|
+
for (const key of Object.keys(files).sort()) {
|
|
40
|
+
let rel = key.replace(/^\.?\//, "");
|
|
41
|
+
if (rel.startsWith(prefix)) rel = rel.slice(prefix.length);
|
|
42
|
+
rel = rel.replace(EXT, "");
|
|
43
|
+
const parts = rel.split("/");
|
|
44
|
+
let base = parts[parts.length - 1];
|
|
45
|
+
const sm = SUFFIX.exec(base);
|
|
46
|
+
const suffix = sm?.[1];
|
|
47
|
+
if (sm) base = base.slice(0, base.length - sm[0].length);
|
|
48
|
+
if (suffix && !prefer.includes(suffix)) continue;
|
|
49
|
+
const rank = suffix ? prefer.indexOf(suffix) : prefer.length;
|
|
50
|
+
const pm = PRESENT.exec(base);
|
|
51
|
+
const presentation = pm?.[1];
|
|
52
|
+
if (pm) base = base.slice(0, base.length - pm[0].length);
|
|
53
|
+
if (base === "_layout") {
|
|
54
|
+
const d = parts.slice(0, -1).join("/");
|
|
55
|
+
const prev = layoutRank.get(d);
|
|
56
|
+
if (prev !== void 0 && prev <= rank) continue;
|
|
57
|
+
const component = pick(files[key], key);
|
|
58
|
+
if (component) {
|
|
59
|
+
layouts[d] = component;
|
|
60
|
+
layoutRank.set(d, rank);
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const segs = parts.slice(0, -1).concat(base);
|
|
65
|
+
if (segs[segs.length - 1] === "index") segs.pop();
|
|
66
|
+
const segments = segs.map((s) => {
|
|
67
|
+
const p = PARAM.exec(s);
|
|
68
|
+
return p ? ":" + p[1] : s;
|
|
69
|
+
});
|
|
70
|
+
const name = segments.join("/") || "index";
|
|
71
|
+
const meta = {
|
|
72
|
+
name,
|
|
73
|
+
segments,
|
|
74
|
+
params: segments.filter((s) => s.startsWith(":")).map((s) => s.slice(1)),
|
|
75
|
+
file: key,
|
|
76
|
+
presentation
|
|
77
|
+
};
|
|
78
|
+
const loader = files[key]?.loader;
|
|
79
|
+
if (typeof loader === "function") meta.loader = loader;
|
|
80
|
+
const beforeLoad = files[key]?.beforeLoad;
|
|
81
|
+
if (typeof beforeLoad === "function") meta.beforeLoad = beforeLoad;
|
|
82
|
+
if (files[key]?.head && (typeof files[key].head === "object" || typeof files[key].head === "function")) meta.head = files[key].head;
|
|
83
|
+
const prev = seen.get(name);
|
|
84
|
+
if (prev && prev.rank <= rank) {
|
|
85
|
+
if (prev.rank === rank && !warned.has(name)) {
|
|
86
|
+
warned.add(name);
|
|
87
|
+
console.warn(`[octane-xplat] route '${name}' is defined by both ${prev.meta.file} and ${key} — keeping ${prev.meta.file}`);
|
|
88
|
+
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const component = pick(files[key], key);
|
|
92
|
+
if (component) seen.set(name, {
|
|
93
|
+
rank,
|
|
94
|
+
meta,
|
|
95
|
+
component
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const screens = {};
|
|
99
|
+
const routes = [];
|
|
100
|
+
for (const { meta, component } of seen.values()) {
|
|
101
|
+
screens[meta.name] = component;
|
|
102
|
+
routes.push(meta);
|
|
103
|
+
if (meta.loader) loaders[meta.name] = meta.loader;
|
|
104
|
+
}
|
|
105
|
+
routes.sort((a, b) => b.segments.reduce((n, s) => n + (s.startsWith(":") ? 1 : 2), 0) - a.segments.reduce((n, s) => n + (s.startsWith(":") ? 1 : 2), 0) || a.name.localeCompare(b.name));
|
|
106
|
+
return {
|
|
107
|
+
screens,
|
|
108
|
+
routes,
|
|
109
|
+
layouts,
|
|
110
|
+
loaders
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** Match URL path segments against a manifest — returns the winning meta
|
|
114
|
+
* plus extracted params, or null. First hit wins (routes are pre-sorted
|
|
115
|
+
* by specificity). */
|
|
116
|
+
function matchRoute(routes, segs) {
|
|
117
|
+
for (const meta of routes) {
|
|
118
|
+
if (meta.segments.length !== segs.length) continue;
|
|
119
|
+
const params = {};
|
|
120
|
+
let ok = true;
|
|
121
|
+
for (let i = 0; i < segs.length; i++) {
|
|
122
|
+
const p = meta.segments[i];
|
|
123
|
+
if (p.startsWith(":")) params[p.slice(1)] = decodeRouteParam(decodeURIComponent(segs[i]));
|
|
124
|
+
else if (p !== segs[i]) {
|
|
125
|
+
ok = false;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
38
128
|
}
|
|
39
|
-
|
|
129
|
+
if (ok) return {
|
|
130
|
+
meta,
|
|
131
|
+
params
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
/** Serialize a Route to a URL path (no origin): manifest routes substitute
|
|
137
|
+
* `:param` segments from params (leftover params → query string);
|
|
138
|
+
* non-manifest names keep the legacy `/<stack>/<name>?params` shape. */
|
|
139
|
+
function buildRoutePath(routes, r) {
|
|
140
|
+
const meta = routes.find((m) => m.name === r.name);
|
|
141
|
+
let segs;
|
|
142
|
+
let rest = r.params;
|
|
143
|
+
if (meta) {
|
|
144
|
+
segs = meta.segments.map((s) => {
|
|
145
|
+
if (!s.startsWith(":")) return s;
|
|
146
|
+
const v = r.params[s.slice(1)];
|
|
147
|
+
if (v === void 0) console.warn(`[octane-xplat] route '${r.name}' pushed without path param ${s}`);
|
|
148
|
+
return encodeURIComponent(encodeRouteParam(v, r.name, s.slice(1)));
|
|
149
|
+
});
|
|
150
|
+
rest = Object.fromEntries(Object.entries(r.params).filter(([k]) => !meta.params.includes(k)));
|
|
151
|
+
} else segs = [r.name];
|
|
152
|
+
const q = Object.entries(rest).map(([k, v]) => encodeURIComponent(k) + "=" + encodeURIComponent(encodeRouteParam(v, r.name, k))).join("&");
|
|
153
|
+
return (((r.stack === "root" ? "" : "/" + r.stack) + "/" + segs.join("/")).replace(/\/+$/, "") || "/") + (q ? "?" + q : "");
|
|
154
|
+
}
|
|
155
|
+
var JSON_PARAM_PREFIX = "json:";
|
|
156
|
+
/** Route codegen exposes string params, but the low-level Route shape remains
|
|
157
|
+
* open for compatibility. Preserve accidental object params on web instead
|
|
158
|
+
* of silently turning them into `[object Object]`; generated APIs still keep
|
|
159
|
+
* callers on scalar strings. */
|
|
160
|
+
function encodeRouteParam(value, routeName, key) {
|
|
161
|
+
if (value === void 0 || value === null || [
|
|
162
|
+
"string",
|
|
163
|
+
"number",
|
|
164
|
+
"boolean"
|
|
165
|
+
].includes(typeof value)) return String(value ?? "");
|
|
166
|
+
console.warn(`[octane-xplat] route '${routeName}' param '${key}' is non-scalar; JSON-encoding it for the URL`);
|
|
167
|
+
try {
|
|
168
|
+
return JSON_PARAM_PREFIX + JSON.stringify(value);
|
|
169
|
+
} catch {
|
|
170
|
+
console.warn(`[octane-xplat] route '${routeName}' param '${key}' could not be JSON-encoded; using an empty value`);
|
|
171
|
+
return "";
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function decodeRouteParam(value) {
|
|
175
|
+
if (!value.startsWith(JSON_PARAM_PREFIX)) return value;
|
|
176
|
+
try {
|
|
177
|
+
return JSON.parse(value.slice(5));
|
|
178
|
+
} catch {
|
|
179
|
+
return value;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** `?a=1&b=2` → params — shared code carries no URLSearchParams
|
|
183
|
+
* (invariant 4: no DOM globals on the native path). */
|
|
184
|
+
function parseQueryString(qs) {
|
|
185
|
+
const params = {};
|
|
186
|
+
if (!qs) return params;
|
|
187
|
+
for (const pair of qs.split("&")) {
|
|
188
|
+
if (!pair) continue;
|
|
189
|
+
const eq = pair.indexOf("=");
|
|
190
|
+
const k = eq === -1 ? pair : pair.slice(0, eq);
|
|
191
|
+
const v = eq === -1 ? "" : pair.slice(eq + 1);
|
|
192
|
+
params[decodeURIComponent(k)] = decodeRouteParam(decodeURIComponent(v));
|
|
193
|
+
}
|
|
194
|
+
return params;
|
|
195
|
+
}
|
|
196
|
+
/** Full URL or path → canonical path for matching. `https://x/a/b?y` →
|
|
197
|
+
* `/a/b?y`; custom-scheme links keep their host as the first segment —
|
|
198
|
+
* `textcoral://post/5` → `/post/5` (the common app-scheme convention). */
|
|
199
|
+
function linkPath(url) {
|
|
200
|
+
const m = /^[a-z][a-z0-9+.-]*:(\/\/)?/i.exec(url);
|
|
201
|
+
let rest = m ? url.slice(m[0].length) : url;
|
|
202
|
+
if (m?.[1] && /^https?:\/\//i.test(m[0])) rest = rest.replace(/^[^/?#]*/, "");
|
|
203
|
+
return "/" + rest.replace(/^\/+/, "");
|
|
204
|
+
}
|
|
205
|
+
/** Match a URL path+query against the manifest into a Route — the shared
|
|
206
|
+
* half of web's URL parse and native's deep-link handling. Whole-path
|
|
207
|
+
* match wins ('/demo/x' → root 'demo/:id'); a non-matching first segment
|
|
208
|
+
* is the stack prefix ('/demos/demo/x' → stack 'demos'). Unmatched names
|
|
209
|
+
* fall through as literal routes for pre-manifest callers. */
|
|
210
|
+
function matchUrl(routes, url) {
|
|
211
|
+
const [p, qs] = url.split("?");
|
|
212
|
+
const segs = p.split("/").filter(Boolean);
|
|
213
|
+
const query = parseQueryString(qs);
|
|
214
|
+
if (!segs.length) return null;
|
|
215
|
+
const finish = (stack, m, fallbackName) => ({
|
|
216
|
+
stack,
|
|
217
|
+
name: m ? m.meta.name : fallbackName,
|
|
218
|
+
params: {
|
|
219
|
+
...query,
|
|
220
|
+
...m?.params
|
|
221
|
+
},
|
|
222
|
+
presentation: m?.meta.presentation
|
|
223
|
+
});
|
|
224
|
+
const root = matchRoute(routes, segs);
|
|
225
|
+
if (root) return finish("root", root, segs.join("/"));
|
|
226
|
+
if (segs.length > 1) {
|
|
227
|
+
const named = matchRoute(routes, segs.slice(1));
|
|
228
|
+
if (named) return finish(segs[0], named, segs[1]);
|
|
229
|
+
return finish(segs[0], null, segs[1]);
|
|
230
|
+
}
|
|
231
|
+
return finish("root", null, segs[0]);
|
|
232
|
+
}
|
|
233
|
+
/** Layout components wrapping a route name, outermost → innermost —
|
|
234
|
+
* 'chat/room' picks up `app/chat/_layout.tsrx`, 'a/b/c' chains 'a' then
|
|
235
|
+
* 'a/b'. The '' root layout is the app's own shell (entry renders it)
|
|
236
|
+
* and never joins a chain. Leaves apply the chain at render: native
|
|
237
|
+
* wraps each pushed Page's component, web wraps the outlet's resolved
|
|
238
|
+
* element. */
|
|
239
|
+
function layoutChain(layouts, name) {
|
|
240
|
+
const segs = name.split("/");
|
|
241
|
+
const chain = [];
|
|
242
|
+
for (let i = 1; i < segs.length; i++) {
|
|
243
|
+
const L = layouts[segs.slice(0, i).join("/")];
|
|
244
|
+
if (L) chain.push(L);
|
|
40
245
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
});
|
|
246
|
+
return chain;
|
|
247
|
+
}
|
|
44
248
|
//#endregion
|
|
45
|
-
export {
|
|
249
|
+
export { RouteRedirect, buildRoutePath, deriveRouteManifest, layoutChain, linkPath, matchRoute, matchUrl, parseQueryString };
|
package/dist/web/ui18.js
CHANGED
|
@@ -1,31 +1,240 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
var
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
import { RouteRedirect, buildRoutePath, layoutChain, linkPath, matchUrl } from "./ui17.js";
|
|
2
|
+
import { hookSlots, useSyncExternalStore } from "octane";
|
|
3
|
+
//#region src/route.web.ts
|
|
4
|
+
var screens = {};
|
|
5
|
+
var routes = [];
|
|
6
|
+
var routeLayouts = {};
|
|
7
|
+
var routeLoaders = {};
|
|
8
|
+
/** Register the app's name → screen table. On web the table feeds
|
|
9
|
+
* `screenFor` — the fallback outlet resolution in Tabs when no
|
|
10
|
+
* `resolveScreen` prop is given; on native `pushRoute` resolves
|
|
11
|
+
* `route.name` through it. `manifest` (from deriveRouteManifest) enables
|
|
12
|
+
* path-param matching — call once from the shared routes module. */
|
|
13
|
+
function registerScreens(table, manifest) {
|
|
14
|
+
screens = table;
|
|
15
|
+
routes = manifest ?? [];
|
|
16
|
+
if (current !== void 0) {
|
|
17
|
+
current = parse();
|
|
18
|
+
emit();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** One-call registration for route-dir apps — screens + URL patterns +
|
|
22
|
+
* layouts all come from deriveRouteManifest. */
|
|
23
|
+
function registerRoutes(manifest) {
|
|
24
|
+
registerScreens(manifest.screens, manifest.routes);
|
|
25
|
+
routeLayouts = manifest.layouts;
|
|
26
|
+
routeLoaders = manifest.loaders ?? {};
|
|
27
|
+
applyHead(read());
|
|
28
|
+
}
|
|
29
|
+
function screenFor(name) {
|
|
30
|
+
return screens[name];
|
|
31
|
+
}
|
|
32
|
+
/** Directory layouts wrapping a route, outermost → innermost — outlets
|
|
33
|
+
* wrap their resolved element with these (`_layout.tsrx` files). */
|
|
34
|
+
function layoutsFor(name) {
|
|
35
|
+
return layoutChain(routeLayouts, name);
|
|
36
|
+
}
|
|
37
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
38
|
+
var current;
|
|
39
|
+
var modalRoute = null;
|
|
40
|
+
var historyDepth = history.state?.__octaneXplatDepth ?? 0;
|
|
41
|
+
var ROUTE_HEAD_ATTR = "data-octane-xplat-route-head";
|
|
42
|
+
var scrollPositions = /* @__PURE__ */ new Map();
|
|
43
|
+
var lastKey = location.pathname + location.search;
|
|
44
|
+
var scrollKey = () => location.pathname + location.search;
|
|
45
|
+
var saveScroll = () => scrollPositions.set(lastKey, window.scrollY);
|
|
46
|
+
var restoreScroll = () => {
|
|
47
|
+
const y = scrollPositions.get(scrollKey());
|
|
48
|
+
if (y !== void 0) requestAnimationFrame(() => window.scrollTo(0, y));
|
|
49
|
+
};
|
|
50
|
+
function parse() {
|
|
51
|
+
return matchUrl(routes, location.pathname + location.search);
|
|
52
|
+
}
|
|
53
|
+
function read() {
|
|
54
|
+
if (current === void 0) current = parse();
|
|
55
|
+
return current;
|
|
56
|
+
}
|
|
57
|
+
function emit() {
|
|
58
|
+
listeners.forEach((l) => l());
|
|
59
|
+
}
|
|
60
|
+
function metaFor(name) {
|
|
61
|
+
return routes.find((meta) => meta.name === name);
|
|
62
|
+
}
|
|
63
|
+
function headFor(r) {
|
|
64
|
+
const head = r ? metaFor(r.name)?.head : void 0;
|
|
65
|
+
return typeof head === "function" ? head(r?.params ?? {}) : head;
|
|
66
|
+
}
|
|
67
|
+
function applyHead(r) {
|
|
68
|
+
if (typeof document === "undefined") return;
|
|
69
|
+
const head = headFor(r);
|
|
70
|
+
if (!head) {
|
|
71
|
+
document.querySelectorAll(`meta[${ROUTE_HEAD_ATTR}]`).forEach((node) => node.remove());
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (head.title !== void 0) document.title = head.title;
|
|
75
|
+
document.querySelectorAll(`meta[${ROUTE_HEAD_ATTR}]`).forEach((node) => node.remove());
|
|
76
|
+
for (const [name, content] of Object.entries(head.meta ?? {})) {
|
|
77
|
+
const tag = document.createElement("meta");
|
|
78
|
+
tag.setAttribute("name", name);
|
|
79
|
+
tag.setAttribute("content", content);
|
|
80
|
+
tag.setAttribute(ROUTE_HEAD_ATTR, "");
|
|
81
|
+
document.head.appendChild(tag);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function contextFor(r) {
|
|
85
|
+
return r.context ?? {};
|
|
86
|
+
}
|
|
87
|
+
async function prepareRoute(r, redirects = 0) {
|
|
88
|
+
const beforeLoad = metaFor(r.name)?.beforeLoad;
|
|
89
|
+
if (!beforeLoad) {
|
|
90
|
+
commitRoute(r);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (redirects > 16) {
|
|
94
|
+
console.warn(`[octane-xplat] beforeLoad redirect loop for '${r.name}'`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const returned = await beforeLoad({
|
|
99
|
+
params: r.params,
|
|
100
|
+
context: contextFor(r)
|
|
101
|
+
});
|
|
102
|
+
const context = returned ? {
|
|
103
|
+
...contextFor(r),
|
|
104
|
+
...returned
|
|
105
|
+
} : contextFor(r);
|
|
106
|
+
commitRoute({
|
|
107
|
+
...r,
|
|
108
|
+
context
|
|
109
|
+
});
|
|
110
|
+
} catch (e) {
|
|
111
|
+
if (e instanceof RouteRedirect) {
|
|
112
|
+
await prepareRoute(e.route, redirects + 1);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
console.warn(`[octane-xplat] beforeLoad('${r.name}') rejected: ${e?.message ?? e}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function redirect(r) {
|
|
119
|
+
throw new RouteRedirect(r);
|
|
120
|
+
}
|
|
121
|
+
function pushRoute(r) {
|
|
122
|
+
if (metaFor(r.name)?.beforeLoad) {
|
|
123
|
+
prepareRoute(r);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
commitRoute(r);
|
|
127
|
+
}
|
|
128
|
+
function commitRoute(r) {
|
|
129
|
+
const route = {
|
|
130
|
+
...r,
|
|
131
|
+
presentation: r.presentation ?? presentationFor(r.name)
|
|
16
132
|
};
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
133
|
+
const loader = routeLoaders[route.name] ?? routes.find((meta) => meta.name === route.name)?.loader;
|
|
134
|
+
if (loader && !Object.prototype.hasOwnProperty.call(route, "loaderData") && !Object.prototype.hasOwnProperty.call(route, "loaderError")) {
|
|
135
|
+
Promise.resolve().then(() => loader(route.params)).then((loaderData) => commitRoute({
|
|
136
|
+
...route,
|
|
137
|
+
loaderData
|
|
138
|
+
}), (loaderError) => commitRoute({
|
|
139
|
+
...route,
|
|
140
|
+
loaderError
|
|
141
|
+
}));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
saveScroll();
|
|
145
|
+
historyDepth += 1;
|
|
146
|
+
history.pushState({ __octaneXplatDepth: historyDepth }, "", buildRoutePath(routes, route));
|
|
147
|
+
lastKey = scrollKey();
|
|
148
|
+
if (route.presentation === "modal") modalRoute = route;
|
|
149
|
+
else {
|
|
150
|
+
current = route;
|
|
151
|
+
modalRoute = null;
|
|
152
|
+
}
|
|
153
|
+
applyHead(modalRoute ?? current ?? null);
|
|
154
|
+
emit();
|
|
155
|
+
}
|
|
156
|
+
function presentationFor(name) {
|
|
157
|
+
return routes.find((m) => m.name === name)?.presentation;
|
|
158
|
+
}
|
|
159
|
+
/** Web history is one linear stack — back pops whatever route is current;
|
|
160
|
+
* per-stack pops aren't expressible, so `stack` is accepted for parity
|
|
161
|
+
* and ignored. A pushed modal is the top entry, so back dismisses it. */
|
|
162
|
+
function popRoute(_stack = "root") {
|
|
163
|
+
history.back();
|
|
164
|
+
}
|
|
165
|
+
window.addEventListener("popstate", () => {
|
|
166
|
+
saveScroll();
|
|
167
|
+
historyDepth = history.state?.__octaneXplatDepth ?? 0;
|
|
168
|
+
lastKey = scrollKey();
|
|
169
|
+
modalRoute = null;
|
|
170
|
+
current = parse();
|
|
171
|
+
applyHead(current);
|
|
172
|
+
restoreScroll();
|
|
173
|
+
emit();
|
|
29
174
|
});
|
|
175
|
+
/** Current route if it targets `stack`, else null. */
|
|
176
|
+
function routeFor(stack) {
|
|
177
|
+
const c = read();
|
|
178
|
+
return c && c.stack === stack ? c : null;
|
|
179
|
+
}
|
|
180
|
+
/** Web has one linear browser history; expose its active route stack for parity. */
|
|
181
|
+
function routeStacks() {
|
|
182
|
+
const route = read();
|
|
183
|
+
return route ? [route.stack] : [];
|
|
184
|
+
}
|
|
185
|
+
/** The route active at boot — for deep-link tab selection. */
|
|
186
|
+
function currentRoute() {
|
|
187
|
+
return read();
|
|
188
|
+
}
|
|
189
|
+
/** The modal route overlaying the current one, if any. */
|
|
190
|
+
function currentModalRoute() {
|
|
191
|
+
return modalRoute;
|
|
192
|
+
}
|
|
193
|
+
/** Deep-link entry, web half — the URL *is* the route state, so this is a
|
|
194
|
+
* plain pushRoute of the matched path (parity with the native leaf, where
|
|
195
|
+
* apps wire `onDeepLink(pushDeepLink)`). */
|
|
196
|
+
function pushDeepLink(url) {
|
|
197
|
+
const r = matchUrl(routes, linkPath(url));
|
|
198
|
+
if (!r) {
|
|
199
|
+
console.warn(`[octane-xplat] pushDeepLink('${url}') dropped — no route matches.`);
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
pushRoute(r);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
/** URL for a Route — Link's href and shareable-path helper. */
|
|
206
|
+
function hrefFor(r) {
|
|
207
|
+
return buildRoutePath(routes, r);
|
|
208
|
+
}
|
|
209
|
+
function useRoute(stack) {
|
|
210
|
+
return useSyncExternalStore((cb) => {
|
|
211
|
+
listeners.add(cb);
|
|
212
|
+
return () => listeners.delete(cb);
|
|
213
|
+
}, () => routeFor(stack), _h$0);
|
|
214
|
+
}
|
|
215
|
+
/** Whether the selected conceptual stack has an in-app route to pop. A
|
|
216
|
+
* browser's pre-app history is intentionally not counted. */
|
|
217
|
+
function canGoBack(stack = "root") {
|
|
218
|
+
const route = read();
|
|
219
|
+
return historyDepth > 0 && route?.stack === stack;
|
|
220
|
+
}
|
|
221
|
+
function useCanGoBack(stack = "root") {
|
|
222
|
+
return useSyncExternalStore((cb) => {
|
|
223
|
+
listeners.add(cb);
|
|
224
|
+
return () => listeners.delete(cb);
|
|
225
|
+
}, () => canGoBack(stack), _h$1);
|
|
226
|
+
}
|
|
227
|
+
/** The modal route overlaying the shell — outlets render it above their
|
|
228
|
+
* normal content (URL preserved underneath). */
|
|
229
|
+
function useModalRoute() {
|
|
230
|
+
return useSyncExternalStore((cb) => {
|
|
231
|
+
listeners.add(cb);
|
|
232
|
+
return () => listeners.delete(cb);
|
|
233
|
+
}, () => modalRoute, _h$2);
|
|
234
|
+
}
|
|
235
|
+
var _hs$ = /* @__PURE__ */ hookSlots(3);
|
|
236
|
+
var _h$0 = /* @__PURE__ */ Symbol(_hs$);
|
|
237
|
+
var _h$1 = /* @__PURE__ */ Symbol(_hs$ + 1);
|
|
238
|
+
var _h$2 = /* @__PURE__ */ Symbol(_hs$ + 2);
|
|
30
239
|
//#endregion
|
|
31
|
-
export {
|
|
240
|
+
export { canGoBack, currentModalRoute, currentRoute, hrefFor, layoutsFor, popRoute, pushDeepLink, pushRoute, redirect, registerRoutes, registerScreens, routeFor, routeStacks, screenFor, useCanGoBack, useModalRoute, useRoute };
|
package/dist/web/ui19.js
CHANGED
|
@@ -1,11 +1,37 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { hrefFor, pushRoute, useRoute } from "./ui18.js";
|
|
2
|
+
import { __s, bag5, clone, delegateEvents, hookSlots, setEventHandler, template, withSlot } from "octane";
|
|
3
|
+
import { bindSignalAttribute, bindSignalChild, compilerMemoRegion, enableSignalBindings, hookMemoCreate, hookMemoEqual, hookMemoPublish1, setURLAttribute, updateFreshClassAttr } from "octane/internal/client";
|
|
4
|
+
//#region src/NavLink.web.tsrx
|
|
5
|
+
enableSignalBindings(1, true);
|
|
6
|
+
delegateEvents(["click"]);
|
|
7
|
+
var _t$0 = /* @__PURE__ */ template("<a></a>", 3);
|
|
8
|
+
var _hs$ = /* @__PURE__ */ hookSlots(1);
|
|
9
|
+
var _h$0 = /* @__PURE__ */ Symbol(_hs$);
|
|
10
|
+
var NavLink = /* @__PURE__ */ __s(function NavLink(props, __s, __extra) {
|
|
11
|
+
const __memoRegion = compilerMemoRegion(__s, _hs$ + 0);
|
|
12
|
+
let __hk = __memoRegion.hooks;
|
|
13
|
+
if (__hk === void 0) __memoRegion.hooks = __hk = hookMemoCreate(3);
|
|
14
|
+
let __hkd0;
|
|
15
|
+
const current = withSlot(_h$0, useRoute, props.route.stack);
|
|
16
|
+
const active = !!current && current.name === props.route.name && Object.keys(current.params).length === Object.keys(props.route.params).length && Object.entries(props.route.params).every(([key, value]) => String(current.params[key]) === String(value));
|
|
17
|
+
const href = (__hkd0 = props, __hk[0] !== true || !hookMemoEqual(__hk[1], __hkd0) ? hookMemoPublish1(__hk, 0, hrefFor(props.route), __hkd0) : __hk[2]);
|
|
18
|
+
const navigate = (event) => {
|
|
19
|
+
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
20
|
+
event.preventDefault();
|
|
21
|
+
pushRoute(props.route);
|
|
22
|
+
};
|
|
23
|
+
if (__s.block.pending && !__s.block.crossRenderUpdate) return;
|
|
24
|
+
let _b = __s.slots[0];
|
|
25
|
+
if (_b === void 0) {
|
|
26
|
+
let _m1;
|
|
27
|
+
const _root = clone(_t$0);
|
|
28
|
+
_m1 = _root;
|
|
29
|
+
setEventHandler(_root, "$$click", navigate);
|
|
30
|
+
_b = bag5(__s, _root, void 0, _m1, void 0, null, void 0);
|
|
31
|
+
} else setEventHandler(_b.b, "$$click", navigate);
|
|
32
|
+
_b.a = bindSignalAttribute(__s, _b.a, _b.b, "href", href, "b:3f52d87a", setURLAttribute);
|
|
33
|
+
_b.c = updateFreshClassAttr([props.className, active ? props.activeClassName : ""], _b.c, _b.b);
|
|
34
|
+
_b.f = bindSignalChild(__s, _b.f, 1, _b.b, props.children, "b:49899ddc", null, void 0, void 0, void 0, true);
|
|
35
|
+
});
|
|
10
36
|
//#endregion
|
|
11
|
-
export {
|
|
37
|
+
export { NavLink };
|