foss-design 0.4.2 → 0.5.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 +1 -1
- package/dist/cli.js +2883 -2371
- package/dist/runtime/index.js +33 -6
- package/dist/viewer/assets/{index-k3bj-XTr.js → index-D6sHcSw6.js} +48 -48
- package/dist/viewer/index.html +1 -1
- package/dist/viewer-lib/index.js +98 -13
- package/dist/viewer-lib/types/src/shared/routes.d.ts +37 -0
- package/dist/viewer-lib/types/src/shared/types.d.ts +6 -0
- package/dist/viewer-lib/types/viewer/src/lib/frames.d.ts +6 -4
- package/package.json +1 -1
package/dist/viewer/index.html
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
document.documentElement.dataset.theme = dark ? 'dark' : 'light'
|
|
15
15
|
} catch (e) {}
|
|
16
16
|
</script>
|
|
17
|
-
<script type="module" crossorigin src="./assets/index-
|
|
17
|
+
<script type="module" crossorigin src="./assets/index-D6sHcSw6.js"></script>
|
|
18
18
|
<link rel="stylesheet" crossorigin href="./assets/index-BL2Cxj98.css">
|
|
19
19
|
</head>
|
|
20
20
|
<body>
|
package/dist/viewer-lib/index.js
CHANGED
|
@@ -7078,6 +7078,78 @@ async function copyText(text, label = "Copied") {
|
|
|
7078
7078
|
}
|
|
7079
7079
|
}
|
|
7080
7080
|
//#endregion
|
|
7081
|
+
//#region src/shared/routes.ts
|
|
7082
|
+
function split(path) {
|
|
7083
|
+
return path.split("/").filter(Boolean);
|
|
7084
|
+
}
|
|
7085
|
+
function compileRoute(pattern) {
|
|
7086
|
+
return {
|
|
7087
|
+
pattern,
|
|
7088
|
+
segments: split(pattern).map((part) => {
|
|
7089
|
+
if (part === "*") return { kind: "splat" };
|
|
7090
|
+
if (part.startsWith(":")) return {
|
|
7091
|
+
kind: "param",
|
|
7092
|
+
optional: part.endsWith("?")
|
|
7093
|
+
};
|
|
7094
|
+
return {
|
|
7095
|
+
kind: "static",
|
|
7096
|
+
value: part
|
|
7097
|
+
};
|
|
7098
|
+
})
|
|
7099
|
+
};
|
|
7100
|
+
}
|
|
7101
|
+
function decode(part) {
|
|
7102
|
+
try {
|
|
7103
|
+
return decodeURIComponent(part);
|
|
7104
|
+
} catch {
|
|
7105
|
+
return part;
|
|
7106
|
+
}
|
|
7107
|
+
}
|
|
7108
|
+
/** How well a route matches a path (higher is more specific), or null when it does not. */
|
|
7109
|
+
function scoreRoute(route, path) {
|
|
7110
|
+
const parts = split(path.split(/[?#]/)[0] ?? "").map(decode);
|
|
7111
|
+
const walk = (s, p) => {
|
|
7112
|
+
if (s === route.segments.length) return p === parts.length ? 0 : null;
|
|
7113
|
+
const segment = route.segments[s];
|
|
7114
|
+
if (segment.kind === "splat") return 0;
|
|
7115
|
+
if (segment.kind === "static") {
|
|
7116
|
+
if (parts[p] !== segment.value) return null;
|
|
7117
|
+
const rest = walk(s + 1, p + 1);
|
|
7118
|
+
return rest === null ? null : rest + 4;
|
|
7119
|
+
}
|
|
7120
|
+
if (p < parts.length) {
|
|
7121
|
+
const rest = walk(s + 1, p + 1);
|
|
7122
|
+
if (rest !== null) return rest + 2;
|
|
7123
|
+
}
|
|
7124
|
+
return segment.optional ? walk(s + 1, p) : null;
|
|
7125
|
+
};
|
|
7126
|
+
return walk(0, 0);
|
|
7127
|
+
}
|
|
7128
|
+
/** The most specific match for a path; the first one listed wins a tie. */
|
|
7129
|
+
function matchRoute(entries, path) {
|
|
7130
|
+
let best = null;
|
|
7131
|
+
for (const entry of entries) for (const route of entry.routes) {
|
|
7132
|
+
const score = scoreRoute(route, path);
|
|
7133
|
+
if (score !== null && (!best || score > best.score)) best = {
|
|
7134
|
+
score,
|
|
7135
|
+
value: entry.value
|
|
7136
|
+
};
|
|
7137
|
+
}
|
|
7138
|
+
return best?.value ?? null;
|
|
7139
|
+
}
|
|
7140
|
+
/**
|
|
7141
|
+
* The app path a link points at, from its `href` as written: an absolute path as it is, a
|
|
7142
|
+
* relative one against the route of the screen it is in (or `/`). Null for links that do not
|
|
7143
|
+
* lead to another page of the app: `#…`, other schemes, other sites.
|
|
7144
|
+
*/
|
|
7145
|
+
function linkPath(href, base = "/") {
|
|
7146
|
+
const raw = href.trim();
|
|
7147
|
+
if (!raw || raw.startsWith("#")) return null;
|
|
7148
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(raw) || raw.startsWith("//")) return null;
|
|
7149
|
+
const url = new URL(raw, `http://app.invalid${base.replace(/\/:[^/]+\??|\/\*$/g, "/x")}`);
|
|
7150
|
+
return url.pathname + url.search + url.hash;
|
|
7151
|
+
}
|
|
7152
|
+
//#endregion
|
|
7081
7153
|
//#region viewer/src/lib/frames.ts
|
|
7082
7154
|
/** Routes runtime messages to whoever listens to the frame that sent them. */
|
|
7083
7155
|
var handlers = /* @__PURE__ */ new Map();
|
|
@@ -7157,11 +7229,13 @@ function absoluteUrl(url) {
|
|
|
7157
7229
|
return new URL(url, document.baseURI).toString();
|
|
7158
7230
|
}
|
|
7159
7231
|
/**
|
|
7160
|
-
* A link or
|
|
7161
|
-
*
|
|
7162
|
-
*
|
|
7232
|
+
* A link, form or history push the runtime kept from leaving its frame. The path goes to the
|
|
7233
|
+
* screen whose `route` matches it best (a relative `href` resolves against the route of the
|
|
7234
|
+
* screen it is in); without routes, a path whose last part names a screen still opens it. A
|
|
7235
|
+
* link to another site opens in a new tab. A push nobody's route matches belongs to the
|
|
7236
|
+
* screen's own router; a link or form that leads nowhere stays put with a note.
|
|
7163
7237
|
*/
|
|
7164
|
-
function followLink(message, canvas, go) {
|
|
7238
|
+
function followLink(message, canvas, go, from) {
|
|
7165
7239
|
if (message.form) {
|
|
7166
7240
|
toast("Forms are not sent from a screen", void 0, "info");
|
|
7167
7241
|
return;
|
|
@@ -7172,14 +7246,25 @@ function followLink(message, canvas, go) {
|
|
|
7172
7246
|
else toast("Link to another site", message.href, "info");
|
|
7173
7247
|
return;
|
|
7174
7248
|
}
|
|
7175
|
-
const
|
|
7176
|
-
|
|
7249
|
+
const frames = canvas.pages.flatMap((page) => page.sections.flatMap((section) => section.items.filter((item) => item.kind === "screen" || item.kind === "url")));
|
|
7250
|
+
const here = frames.find((item) => item.id === from);
|
|
7251
|
+
const path = message.raw === void 0 ? message.path : linkPath(message.raw, here?.routes?.[0]);
|
|
7252
|
+
if (path === null) return;
|
|
7253
|
+
const routed = matchRoute(frames.map((item) => ({
|
|
7254
|
+
routes: (item.routes ?? []).map(compileRoute),
|
|
7255
|
+
value: item.id
|
|
7256
|
+
})), path);
|
|
7257
|
+
if (routed) {
|
|
7258
|
+
if (routed !== from) go(routed);
|
|
7259
|
+
return;
|
|
7260
|
+
}
|
|
7261
|
+
const bare = path.split(/[?#]/)[0].replace(/\/+$/, "");
|
|
7262
|
+
let name = bare.slice(bare.lastIndexOf("/") + 1).replace(/\.html?$/, "");
|
|
7177
7263
|
try {
|
|
7178
7264
|
name = decodeURIComponent(name);
|
|
7179
7265
|
} catch {}
|
|
7180
|
-
|
|
7181
|
-
if (
|
|
7182
|
-
else toast("Not a screen on this canvas", message.path, "info");
|
|
7266
|
+
if (name && name !== from && frames.some((item) => item.id === name)) go(name);
|
|
7267
|
+
else if (!message.pushed) toast("No screen for this link", path, "info");
|
|
7183
7268
|
}
|
|
7184
7269
|
//#endregion
|
|
7185
7270
|
//#region viewer/src/components/inspector.tsx
|
|
@@ -8854,7 +8939,7 @@ function FullPage({ canvasId, itemId }) {
|
|
|
8854
8939
|
else if (message.type === "go") {
|
|
8855
8940
|
const id = message.target.includes("/") ? message.target.split("/")[1] : message.target;
|
|
8856
8941
|
if (items.some((entry) => entry.id === id)) navigate(fullHref(canvasId, id, theme), { replace: true });
|
|
8857
|
-
} else if (message.type === "link" && canvas) followLink(message, canvas, (id) => navigate(fullHref(canvasId, id, theme), { replace: true }));
|
|
8942
|
+
} else if (message.type === "link" && canvas) followLink(message, canvas, (id) => navigate(fullHref(canvasId, id, theme), { replace: true }), itemId);
|
|
8858
8943
|
});
|
|
8859
8944
|
}, [url, item?.kind]);
|
|
8860
8945
|
if (error) return /* @__PURE__ */ jsx(NotFound, {
|
|
@@ -12160,7 +12245,7 @@ function CanvasView({ canvas, page }) {
|
|
|
12160
12245
|
console.warn(`[design] go("${target}"): no such screen`);
|
|
12161
12246
|
},
|
|
12162
12247
|
onLink(from, message) {
|
|
12163
|
-
followLink(message, canvas, (id) => this.onGo(from, id));
|
|
12248
|
+
followLink(message, canvas, (id) => this.onGo(from, id), from);
|
|
12164
12249
|
},
|
|
12165
12250
|
onWheel(frame, message) {
|
|
12166
12251
|
const root = frame.closest(".canvas-root");
|
|
@@ -12669,7 +12754,7 @@ function Player({ canvas, itemId }) {
|
|
|
12669
12754
|
} else if (message.type === "link") followLink(message, canvas, (id) => {
|
|
12670
12755
|
const target = all.find((e) => e.item.id === id);
|
|
12671
12756
|
if (target) navigate(playHref(canvas.id, target.item.id, target.pageId));
|
|
12672
|
-
});
|
|
12757
|
+
}, item?.id);
|
|
12673
12758
|
});
|
|
12674
12759
|
}, [
|
|
12675
12760
|
item,
|
|
@@ -12946,7 +13031,7 @@ function PhoneViewer({ canvas, pageId, itemId }) {
|
|
|
12946
13031
|
else if (message.type === "error") setErrors((list) => list.includes(message.message) ? list : [...list, message.message]);
|
|
12947
13032
|
else if (message.type === "updated") setErrors([]);
|
|
12948
13033
|
else if (message.type === "go") goTo(message.target.includes("/") ? message.target.split("/")[1] : message.target);
|
|
12949
|
-
else if (message.type === "link") followLink(message, canvas, goTo);
|
|
13034
|
+
else if (message.type === "link") followLink(message, canvas, goTo, item.id);
|
|
12950
13035
|
});
|
|
12951
13036
|
}, [frameEl, src]);
|
|
12952
13037
|
useEffect(() => {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screen routes: the app URL a screen stands for (`/databases/:id`), so a plain link in any
|
|
3
|
+
* screen (`href="/databases/orders-db"`) opens the screen that shows that URL. Patterns work
|
|
4
|
+
* like a router's: `:param` matches one segment, `:param?` an optional one, a trailing `*` the
|
|
5
|
+
* rest of the path.
|
|
6
|
+
*/
|
|
7
|
+
type Segment = {
|
|
8
|
+
kind: 'static';
|
|
9
|
+
value: string;
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'param';
|
|
12
|
+
optional: boolean;
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'splat';
|
|
15
|
+
};
|
|
16
|
+
export interface CompiledRoute {
|
|
17
|
+
pattern: string;
|
|
18
|
+
segments: Segment[];
|
|
19
|
+
}
|
|
20
|
+
/** What is wrong with a pattern, or null. */
|
|
21
|
+
export declare function routeError(pattern: string): string | null;
|
|
22
|
+
export declare function compileRoute(pattern: string): CompiledRoute;
|
|
23
|
+
/** How well a route matches a path (higher is more specific), or null when it does not. */
|
|
24
|
+
export declare function scoreRoute(route: CompiledRoute, path: string): number | null;
|
|
25
|
+
export interface Routed<T> {
|
|
26
|
+
routes: CompiledRoute[];
|
|
27
|
+
value: T;
|
|
28
|
+
}
|
|
29
|
+
/** The most specific match for a path; the first one listed wins a tie. */
|
|
30
|
+
export declare function matchRoute<T>(entries: Routed<T>[], path: string): T | null;
|
|
31
|
+
/**
|
|
32
|
+
* The app path a link points at, from its `href` as written: an absolute path as it is, a
|
|
33
|
+
* relative one against the route of the screen it is in (or `/`). Null for links that do not
|
|
34
|
+
* lead to another page of the app: `#…`, other schemes, other sites.
|
|
35
|
+
*/
|
|
36
|
+
export declare function linkPath(href: string, base?: string): string | null;
|
|
37
|
+
export {};
|
|
@@ -47,6 +47,8 @@ interface ItemBase {
|
|
|
47
47
|
id: string;
|
|
48
48
|
title: string;
|
|
49
49
|
description?: string;
|
|
50
|
+
/** Screens and url items: the app URL patterns they stand for (`/databases/:id`). */
|
|
51
|
+
routes?: string[];
|
|
50
52
|
/** Free-layout position on the page, in canvas pixels. */
|
|
51
53
|
x?: number;
|
|
52
54
|
y?: number;
|
|
@@ -296,6 +298,10 @@ export type RuntimeMessage = {
|
|
|
296
298
|
href: string;
|
|
297
299
|
path: string | null;
|
|
298
300
|
form: boolean;
|
|
301
|
+
/** The `href` as written (0.4.3+), resolved by the viewer against the screen's route. */
|
|
302
|
+
raw?: string;
|
|
303
|
+
/** Sent by the screen's own `history.pushState`, not a click: the screen already moved. */
|
|
304
|
+
pushed?: boolean;
|
|
299
305
|
} | {
|
|
300
306
|
source: 'design-runtime';
|
|
301
307
|
key: string;
|
|
@@ -23,11 +23,13 @@ export declare function frameSrc(url: string, theme: Theme, extra?: Record<strin
|
|
|
23
23
|
/** Absolute URL for opening a frame in a new tab. */
|
|
24
24
|
export declare function absoluteUrl(url: string): string;
|
|
25
25
|
/**
|
|
26
|
-
* A link or
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* A link, form or history push the runtime kept from leaving its frame. The path goes to the
|
|
27
|
+
* screen whose `route` matches it best (a relative `href` resolves against the route of the
|
|
28
|
+
* screen it is in); without routes, a path whose last part names a screen still opens it. A
|
|
29
|
+
* link to another site opens in a new tab. A push nobody's route matches belongs to the
|
|
30
|
+
* screen's own router; a link or form that leads nowhere stays put with a note.
|
|
29
31
|
*/
|
|
30
32
|
export declare function followLink(message: Extract<RuntimeMessage, {
|
|
31
33
|
type: 'link';
|
|
32
|
-
}>, canvas: CanvasDoc, go: (id: string) => void): void;
|
|
34
|
+
}>, canvas: CanvasDoc, go: (id: string) => void, from?: string): void;
|
|
33
35
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "foss-design",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Design canvases and design systems for coding agents: live, animated, clickable screens on an infinite canvas, served locally from a .design folder.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|