@valbuild/shared 0.146.2 → 0.147.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/CHANGELOG.md CHANGED
@@ -1,5 +1,63 @@
1
1
  # @valbuild/shared
2
2
 
3
+ ## 0.147.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#816](https://github.com/valbuild/val/pull/816) [`2f09cd5`](https://github.com/valbuild/val/commit/2f09cd5e2f4fc37f06917899f24f9ad2ee8d3a8b) Thanks [@freekh](https://github.com/freekh)! - Routers can give each route parameter a schema, and a URL can say which language a page is in.
8
+
9
+ - **`s.router(router, params, item)`** takes a schema per route parameter, by the name the route file gives it (`$slug`, `[slug]`, `{-$locale}`). Each key's parameters are validated against them, and a name the route does not have is a schema error.
10
+
11
+ ```ts
12
+ s.router(tanstackRouter, { locale: urlLocale, slug: s.string() }, item);
13
+ ```
14
+
15
+ - **`s.enum(...).locales({ ... })`** says which of the project's languages each value means, so `/nb/…` can mean `nb-NO`. The tags are checked against `locales.available` in the settings module, like `s.locale()`. On a nullable enum, a second argument names the language of a URL that leaves the segment out, which only a router parameter may use:
16
+
17
+ ```ts
18
+ const urlLocale = s
19
+ .enum("nb")
20
+ .nullable()
21
+ .locales({ nb: "nb-NO" }, { null: "en-US" });
22
+ ```
23
+
24
+ A router with a locale parameter makes each page a locale scope, so the Studio and validation know which language every page is in.
25
+
26
+ - **A route's language segment can no longer be forgotten.** In a project that declares `locales.available`, a page route with a `locale` or `lang` segment (`{-$locale}`, `[lang]`) whose module gives that parameter no schema is reported, saying what to add. Any schema answers it, so a `$lang` that is not a language is silenced with its own, e.g. `{ lang: s.enum("typescript", "python") }`. It is an ordinary validation error: the Studio still opens and `val validate` lists it, but a project that already has such a route will see it after upgrading, and it blocks publishing until the parameter has a schema.
27
+ - **The Studio's new-page form offers a language as a list.** A route parameter whose schema is a locale is picked from the project's languages, each named in its own language, with "no prefix" where the URL may leave the segment out. Other enum parameters get a list of their values, and every parameter's description is shown under it. The URL row previews what the choice puts there.
28
+ - **TanStack optional segments (`{-$param}`)** are now understood: route validation, `useValRoute` and the Studio's new-page form accept keys with and without the segment.
29
+
30
+ - [#876](https://github.com/valbuild/val/pull/876) [`cb04e58`](https://github.com/valbuild/val/commit/cb04e586db1e3ffd20ec1fee744561b1c8f8cffd) Thanks [@freekh](https://github.com/freekh)! - Demo mode: run the Val Studio with no account and no backend (TanStack Start).
31
+
32
+ `<ValApp demo>` starts Val's own server in the visitor's browser tab, and the Studio talks to it there instead of to `/api/val`. Edits show up in the page as they are typed and are held in the tab's memory: nothing is sent anywhere, and a reload starts over. Images and videos can be uploaded too: a small service worker, served by `initValDemoServer()`, hands the upload and the unpublished files to the server in the tab, so an uploaded photo or video shows in the page with nothing leaving the browser. Uploads are capped by their size after encoding. Pair `<ValApp demo>` with `<ValProvider demo>` on the site and `initValDemoServer()` from the new `@valbuild/tanstack/demo` entry on the API route. In a demo the Studio opens with a guided tour, Publish explains what it would do instead of publishing, and Preview in a new tab is shown but disabled.
33
+
34
+ Also:
35
+
36
+ - `@valbuild/server/browser` is a new entry: Val's request handling, bundleable for a browser. The TypeScript compiler, which only publishing and history need, is loaded when they ask for it, so a browser bundle of the server is about 140 KB rather than 3.6 MB.
37
+ - `s.image({ encode })` now scales an image down in browsers that cannot write WebP (Safari), as a JPEG or PNG in the image's own format, instead of uploading it at full size.
38
+ - `GET /profiles` answers with no profiles, rather than an error, when edits are stored locally and no project is configured.
39
+
40
+ ### Patch Changes
41
+
42
+ - [#876](https://github.com/valbuild/val/pull/876) [`cb04e58`](https://github.com/valbuild/val/commit/cb04e586db1e3ffd20ec1fee744561b1c8f8cffd) Thanks [@freekh](https://github.com/freekh)! - `s.image({ encode: { type: "webp" } })` now produces WebP in Safari too, including on iPhone and iPad.
43
+
44
+ Safari's canvas cannot write WebP, so the Studio used to fall back to a JPEG (or a PNG) there, or to the original file when `accept` allowed only WebP. Now it encodes those images with libwebp, compiled to WebAssembly, at the same quality and size as everywhere else. The encoder is downloaded only by a browser that needs it, once per page, from `content.val.build`, the same place the Studio's bundler is served from. Browsers that can already write WebP never download it. If it cannot be loaded, uploads still work: the image is scaled down as a JPEG or PNG as before, or the original is uploaded.
45
+
46
+ A deployment that cannot reach `content.val.build` can serve the encoder itself and point the Studio at it with `globalThis.__VAL_WEBP_SIMD_WASM_URL__` and `globalThis.__VAL_WEBP_WASM_URL__`, the same way `__VAL_ROLLDOWN_WASM_URL__` works for the bundler.
47
+
48
+ - Updated dependencies [[`2f09cd5`](https://github.com/valbuild/val/commit/2f09cd5e2f4fc37f06917899f24f9ad2ee8d3a8b)]:
49
+ - @valbuild/core@0.147.0
50
+
51
+ ## 0.146.4
52
+
53
+ ### Patch Changes
54
+
55
+ - [#870](https://github.com/valbuild/val/pull/870) [`81f2e80`](https://github.com/valbuild/val/commit/81f2e803c4cefdf58bbe6856c06e98ace95a8e0b) Thanks [@freekh](https://github.com/freekh)! - The canvas works on a page behind your site's own login.
56
+
57
+ A site with its own sign-in (a dashboard, an intranet, a members' area) can be edited in the canvas. The canvas is a frame on the same origin as the Studio, so it sends your site's cookies like any other tab: if you are signed in to the site, the canvas shows the page signed in as you.
58
+
59
+ If you were not signed in, the site redirected the canvas to its login page, and the canvas then broke. That page mounted Val's whole overlay inside the canvas, a Studio inside the Studio. The canvas stayed on "Preview is not ready yet" through the sign-in and after it, until you pressed Reload. Now a page shown in the Studio's canvas counts as the canvas wherever it navigated to, so you can sign in inside the canvas and keep editing.
60
+
3
61
  ## 0.146.2
4
62
 
5
63
  ### Patch Changes
@@ -2,7 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var getSitemapTree = require('../../dist/getSitemapTree-c732e09a.cjs.dev.js');
5
+ var getSitemapTree = require('../../dist/getSitemapTree-f6254b32.cjs.dev.js');
6
6
  require('@valbuild/core');
7
7
 
8
8
 
@@ -14,6 +14,7 @@ exports.VAL_CANVAS_MESSAGE = getSitemapTree.VAL_CANVAS_MESSAGE;
14
14
  exports.VAL_CANVAS_PARAM = getSitemapTree.VAL_CANVAS_PARAM;
15
15
  exports.VAL_CONFIG_SESSION_STORAGE_KEY = getSitemapTree.VAL_CONFIG_SESSION_STORAGE_KEY;
16
16
  exports.VAL_THEME_SESSION_STORAGE_KEY = getSitemapTree.VAL_THEME_SESSION_STORAGE_KEY;
17
+ exports.formatRoutePattern = getSitemapTree.formatRoutePattern;
17
18
  exports.getNextAppRouterSourceFolder = getSitemapTree.getNextAppRouterSourceFolder;
18
19
  exports.getPageRouterSitemapTree = getSitemapTree.getPageRouterSitemapTree;
19
20
  exports.getPageRouterSourceFolder = getSitemapTree.getPageRouterSourceFolder;
@@ -2,7 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var getSitemapTree = require('../../dist/getSitemapTree-0ed3deac.cjs.prod.js');
5
+ var getSitemapTree = require('../../dist/getSitemapTree-109be3b8.cjs.prod.js');
6
6
  require('@valbuild/core');
7
7
 
8
8
 
@@ -14,6 +14,7 @@ exports.VAL_CANVAS_MESSAGE = getSitemapTree.VAL_CANVAS_MESSAGE;
14
14
  exports.VAL_CANVAS_PARAM = getSitemapTree.VAL_CANVAS_PARAM;
15
15
  exports.VAL_CONFIG_SESSION_STORAGE_KEY = getSitemapTree.VAL_CONFIG_SESSION_STORAGE_KEY;
16
16
  exports.VAL_THEME_SESSION_STORAGE_KEY = getSitemapTree.VAL_THEME_SESSION_STORAGE_KEY;
17
+ exports.formatRoutePattern = getSitemapTree.formatRoutePattern;
17
18
  exports.getNextAppRouterSourceFolder = getSitemapTree.getNextAppRouterSourceFolder;
18
19
  exports.getPageRouterSitemapTree = getSitemapTree.getPageRouterSitemapTree;
19
20
  exports.getPageRouterSourceFolder = getSitemapTree.getPageRouterSourceFolder;
@@ -1,2 +1,2 @@
1
- export { D as DEFAULT_CANVAS_SELECTION, e as DEFAULT_CANVAS_SELECTION_SOFT, b as VAL_AI_SESSION_STORAGE_KEY, d as VAL_CANVAS_MESSAGE, c as VAL_CANVAS_PARAM, V as VAL_CONFIG_SESSION_STORAGE_KEY, a as VAL_THEME_SESSION_STORAGE_KEY, h as getNextAppRouterSourceFolder, m as getPageRouterSitemapTree, k as getPageRouterSourceFolder, n as getPatternFromModuleFilePath, j as getTanStackRouterSourceFolder, l as isTanStackRoutesFolder, g as isValCanvasFrame, i as isValCanvasPageMessage, f as isValCanvasStudioMessage, p as parseRoutePattern, w as withValCanvasParam } from '../../dist/getSitemapTree-6fcdf745.esm.js';
1
+ export { D as DEFAULT_CANVAS_SELECTION, e as DEFAULT_CANVAS_SELECTION_SOFT, b as VAL_AI_SESSION_STORAGE_KEY, d as VAL_CANVAS_MESSAGE, c as VAL_CANVAS_PARAM, V as VAL_CONFIG_SESSION_STORAGE_KEY, a as VAL_THEME_SESSION_STORAGE_KEY, h as formatRoutePattern, j as getNextAppRouterSourceFolder, n as getPageRouterSitemapTree, l as getPageRouterSourceFolder, o as getPatternFromModuleFilePath, k as getTanStackRouterSourceFolder, m as isTanStackRoutesFolder, g as isValCanvasFrame, i as isValCanvasPageMessage, f as isValCanvasStudioMessage, p as parseRoutePattern, w as withValCanvasParam } from '../../dist/getSitemapTree-c70ea145.esm.js';
2
2
  import '@valbuild/core';
@@ -1,4 +1,14 @@
1
1
  import { Api, ClientOf } from "./ApiRoutes.js";
2
2
  import { SharedValConfig } from "./SharedValConfig.js";
3
3
  export type ValClient = ClientOf<Api>;
4
- export declare const createValClient: (host: string, config: SharedValConfig | null) => ValClient;
4
+ export declare const createValClient: (host: string, config: SharedValConfig | null, // We want to use this in the future
5
+ options?: {
6
+ /**
7
+ * What sends a request. The network's `fetch` unless given.
8
+ *
9
+ * The demo passes one that hands the `Request` to Val's server running in
10
+ * the same tab (`@valbuild/server/browser`), so everything on both sides
11
+ * of the call is the real thing and only the network is gone.
12
+ */
13
+ fetch?: (input: string, init: RequestInit) => Promise<Response>;
14
+ }) => ValClient;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * A Val demo, as the page hands it to the Studio.
3
+ *
4
+ * In a demo the Studio talks to Val's own server running in the same tab
5
+ * (`@valbuild/server/browser`) instead of to an API route, and the page that
6
+ * starts that server is the app's (`<ValApp demo>`) while the Studio is a
7
+ * separate bundle loaded by a script tag. A window property is how the two
8
+ * meet, the same way `window.__VAL_MODULES__` hands the Studio the project's
9
+ * modules. See `docs/plans/demo-mode.md`.
10
+ */
11
+ export type ValDemoRuntime = {
12
+ /**
13
+ * Sends a Studio request to the in-tab server. Passed to `createValClient`.
14
+ *
15
+ * The server holds its edits in memory, so starting the demo over is
16
+ * reloading the page: there is nothing stored to clear.
17
+ */
18
+ fetch: (input: string, init: RequestInit) => Promise<Response>;
19
+ /**
20
+ * Whether uploads work here. They need the demo's service worker, which
21
+ * relays the upload and the unpublished files to this tab's server, and a
22
+ * service worker needs a secure context. Where it cannot run the Studio
23
+ * refuses uploads before sending anything, and says why.
24
+ */
25
+ uploads: boolean;
26
+ };
27
+ declare global {
28
+ var __VAL_DEMO__: ValDemoRuntime | undefined;
29
+ }
30
+ /** The demo this page was started with, or null outside a demo. */
31
+ export declare function getValDemo(): ValDemoRuntime | null;
@@ -3,6 +3,7 @@ export * from "./richtext/richTextImages.js";
3
3
  export * from "./server/types.js";
4
4
  export * from "./server/requestScopedMemo.js";
5
5
  export * from "./ValClient.js";
6
+ export * from "./demo.js";
6
7
  export * from "./ValUrls.js";
7
8
  export * from "./ApiRoutes.js";
8
9
  export * from "./schema/compatibility.js";
@@ -1,11 +1,12 @@
1
- import { type SourcePath } from "@valbuild/core";
1
+ import { type SerializedEnumSchema, type SerializedSchema, type SourcePath } from "@valbuild/core";
2
2
  import { type SchemaSourceSnapshot } from "./resolveSchemaSourceFixes.js";
3
3
  /**
4
4
  * Which of the project's languages governs a path, or `null` for none.
5
5
  *
6
6
  * THE question the whole feature turns on. A locale scope is a subtree in one
7
7
  * language, and three things open one: a `locale` field on an object, a record
8
- * keyed by `s.locale()`, and a locale segment in a segmented key. Once this can
8
+ * keyed by `s.locale()`, and a locale parameter in a router's key
9
+ * (`s.router(router, { locale: … }, item)`, where the URL names the language). Once this can
9
10
  * be answered from the schema and the source alone, everything downstream falls
10
11
  * out of it — the Studio's locale filter, deep links, `<html lang>`, and knowing
11
12
  * what to translate from and into.
@@ -17,3 +18,29 @@ import { type SchemaSourceSnapshot } from "./resolveSchemaSourceFixes.js";
17
18
  * in most projects, and for every project that has not declared any languages.
18
19
  */
19
20
  export declare function localeAt(path: SourcePath, snapshot: SchemaSourceSnapshot): string | null;
21
+ /**
22
+ * The language a locale schema's value MEANS, or `null` if it means none of
23
+ * the project's.
24
+ *
25
+ * `s.locale()` stores the tag itself. An enum with `.locales(...)` stores a
26
+ * value that stands for one — `nb` for `nb-NO` — and, on a router parameter,
27
+ * `null` for a URL that left the segment out, which `{ null: tag }` names.
28
+ * Anything else is not a locale and means nothing here.
29
+ *
30
+ * Callers reading a FIELD pass only strings: a field that holds `null` has no
31
+ * language, whatever its schema says about URLs.
32
+ */
33
+ export declare function localeMeantBy(schema: SerializedSchema, value: string | null, available: readonly string[]): string | null;
34
+ /**
35
+ * The tag an enum's `.locales(...)` maps a value to, as written — `null` is
36
+ * the `{ null }` mapping — or `undefined` where it maps none.
37
+ *
38
+ * An own property: a plain object inherits `toString` and the rest, and an
39
+ * enum value is allowed to be spelled like one.
40
+ */
41
+ export declare function enumLocaleTag(schema: SerializedEnumSchema, value: string | null): string | undefined;
42
+ /**
43
+ * Whether a schema is a locale: `s.locale()`, or an enum `.locales(...)` made
44
+ * one.
45
+ */
46
+ export declare function isLocaleSchema(schema: SerializedSchema): boolean;
@@ -65,6 +65,53 @@ export declare function fitWithin(width: number, height: number, maxWidth: numbe
65
65
  export declare function isMimeTypeAccepted(mimeType: string, accept: string | undefined): boolean;
66
66
  /** Formats left alone: the lossy list, plus the target type when it already fits. */
67
67
  export declare function isSkippedSource(sourceMimeType: string, targetMimeType: string, needsDownscale: boolean): boolean;
68
+ /**
69
+ * The types the Studio can write with WebAssembly when the canvas cannot.
70
+ *
71
+ * libwebp, compiled to WebAssembly (`@jsquash/webp`, loaded by
72
+ * `packages/ui/spa/utils/webpWasm.ts`). One entry because `encode` has one
73
+ * `type`; a second type is a second encoder to host, not a second string here.
74
+ */
75
+ export declare const WASM_ENCODED_MIME_TYPES: string[];
76
+ /**
77
+ * Whether to encode the canvas's pixels with WebAssembly, given what the
78
+ * canvas's own encoder handed back.
79
+ *
80
+ * `canvas.toBlob` answers a type it cannot write with a PNG instead of an error,
81
+ * and Safari -- the browser people take their photos in -- cannot write WebP.
82
+ * The pixels on that canvas are already decoded, oriented and scaled, so the
83
+ * only thing missing is the encoder, and libwebp in WebAssembly is that.
84
+ *
85
+ * Only then, and that is what keeps the WebAssembly lazy: a browser whose
86
+ * canvas wrote the type is never asked to download an encoder, even when its
87
+ * WebP then loses to the original on size -- a second encoder of the same
88
+ * format would lose for the same reason.
89
+ *
90
+ * `null` -- no blob at all -- is not "cannot write this type" either. It is a
91
+ * canvas that failed to serialize anything, under memory pressure for instance,
92
+ * and says nothing about WebP: a Chromium tab that hits it is still a browser
93
+ * that writes WebP, and must not start downloading an encoder because of it.
94
+ */
95
+ export declare function shouldEncodeWithWasm(input: {
96
+ targetMimeType: string;
97
+ canvasOutputType: string | null;
98
+ }): boolean;
99
+ /**
100
+ * What to scale an image down to when nothing could write the type asked for:
101
+ * the source's own format, when it is one every canvas can write.
102
+ *
103
+ * The second fallback, after {@link shouldEncodeWithWasm}: it is what a browser
104
+ * whose canvas cannot write WebP gets when the WebAssembly encoder could not be
105
+ * loaded either (offline, a blocked host, a failed instantiation). Without it,
106
+ * `chooseEncoded` would rightly refuse the canvas's PNG and upload the original
107
+ * -- every photo at the camera's full size, the one case `maxWidth` exists
108
+ * for. Scaling it down as a JPEG keeps the half of `encode` this browser can do.
109
+ *
110
+ * Only when a downscale is needed: re-encoding a JPEG that already fits, as a
111
+ * JPEG, is a second generation of loss for a few percent. And never into a
112
+ * type `accept` refuses, for the reason `isMimeTypeAccepted` gives.
113
+ */
114
+ export declare function fallbackMimeType(sourceMimeType: string, needsDownscale: boolean, accept: string | undefined): string | null;
68
115
  /**
69
116
  * Whether to upload what the encoder produced, or the file that was picked.
70
117
  *
@@ -11,3 +11,9 @@ export type RoutePattern = {
11
11
  optional: boolean;
12
12
  };
13
13
  export declare function parseRoutePattern(pattern: string): RoutePattern[];
14
+ /**
15
+ * A parsed pattern written back as segments: the inverse of
16
+ * {@link parseRoutePattern}, in the vocabulary core's `matchRoutePattern`
17
+ * reads.
18
+ */
19
+ export declare function formatRoutePattern(pattern: RoutePattern[]): string[];
@@ -261,12 +261,21 @@ export declare function isValCanvasPageMessage(data: unknown): data is ValCanvas
261
261
  /** Narrow an unknown `event.data` to a message from the studio. */
262
262
  export declare function isValCanvasStudioMessage(data: unknown): data is ValCanvasStudioMessage;
263
263
  /**
264
- * Whether this document was loaded as the studio's canvas frame.
264
+ * Whether this document is showing in the studio's canvas frame.
265
265
  *
266
- * Reads the location directly rather than taking it as an argument, because
267
- * every caller wants the answer for the document it is running in, and passing
268
- * it around is how one of them ends up asking about the wrong one.
266
+ * The parameter marks the document the studio LOADED, and that is not always
267
+ * the document the frame ends up holding. A frame navigates on its own: a site
268
+ * with a login of its own redirects a signed-out canvas to its sign-in page,
269
+ * and the sign-in sends it back to wherever the site chooses. Each of those is
270
+ * a new document without the parameter, and it used to render Val's whole
271
+ * overlay inside the canvas — a studio in the studio, which never told the
272
+ * canvas it was there, so the canvas never went live again until reloaded.
273
+ *
274
+ * So where the frame SITS answers too: a document whose parent is the studio
275
+ * is in the canvas, whatever its own address says. `parentPathname` is that
276
+ * parent's path: `null` for a top-level document, and for a parent on another
277
+ * origin, whose `location` cannot be read and which is not the studio anyway.
269
278
  */
270
- export declare function isValCanvasFrame(search: string): boolean;
279
+ export declare function isValCanvasFrame(search: string, parentPathname?: string | null): boolean;
271
280
  /** Add the canvas marker to a page URL. */
272
281
  export declare function withValCanvasParam(url: string): string;
@@ -121,13 +121,26 @@ function isValCanvasStudioMessage(data) {
121
121
  }
122
122
 
123
123
  /**
124
- * Whether this document was loaded as the studio's canvas frame.
124
+ * Whether this document is showing in the studio's canvas frame.
125
125
  *
126
- * Reads the location directly rather than taking it as an argument, because
127
- * every caller wants the answer for the document it is running in, and passing
128
- * it around is how one of them ends up asking about the wrong one.
126
+ * The parameter marks the document the studio LOADED, and that is not always
127
+ * the document the frame ends up holding. A frame navigates on its own: a site
128
+ * with a login of its own redirects a signed-out canvas to its sign-in page,
129
+ * and the sign-in sends it back to wherever the site chooses. Each of those is
130
+ * a new document without the parameter, and it used to render Val's whole
131
+ * overlay inside the canvas — a studio in the studio, which never told the
132
+ * canvas it was there, so the canvas never went live again until reloaded.
133
+ *
134
+ * So where the frame SITS answers too: a document whose parent is the studio
135
+ * is in the canvas, whatever its own address says. `parentPathname` is that
136
+ * parent's path: `null` for a top-level document, and for a parent on another
137
+ * origin, whose `location` cannot be read and which is not the studio anyway.
129
138
  */
130
139
  function isValCanvasFrame(search) {
140
+ var parentPathname = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
141
+ if (parentPathname !== null && isValStudioPathname(parentPathname)) {
142
+ return true;
143
+ }
131
144
  try {
132
145
  return new URLSearchParams(search).get(VAL_CANVAS_PARAM) !== null;
133
146
  } catch (_unused) {
@@ -135,6 +148,11 @@ function isValCanvasFrame(search) {
135
148
  }
136
149
  }
137
150
 
151
+ /** Whether a path is the studio's own route, where the canvas lives. */
152
+ function isValStudioPathname(pathname) {
153
+ return pathname === "/val" || pathname.startsWith("/val/");
154
+ }
155
+
138
156
  /** Add the canvas marker to a page URL. */
139
157
  function withValCanvasParam(url) {
140
158
  var separator = url.includes("?") ? "&" : "?";
@@ -175,6 +193,21 @@ function parseRoutePattern(pattern) {
175
193
  });
176
194
  }
177
195
 
196
+ /**
197
+ * A parsed pattern written back as segments: the inverse of
198
+ * {@link parseRoutePattern}, in the vocabulary core's `matchRoutePattern`
199
+ * reads.
200
+ */
201
+ function formatRoutePattern(pattern) {
202
+ return pattern.map(function (part) {
203
+ if (part.type === "literal") {
204
+ return part.name;
205
+ }
206
+ var inner = part.type === "string-param" ? part.paramName : "...".concat(part.paramName);
207
+ return part.optional ? "[[".concat(inner, "]]") : "[".concat(inner, "]");
208
+ });
209
+ }
210
+
178
211
  function getNextAppRouterSourceFolder(moduleFilePath) {
179
212
  if (moduleFilePath.startsWith("/app")) {
180
213
  return "/app";
@@ -491,6 +524,7 @@ exports._objectSpread2 = _objectSpread2;
491
524
  exports._toPropertyKey = _toPropertyKey;
492
525
  exports._typeof = _typeof;
493
526
  exports._unsupportedIterableToArray = _unsupportedIterableToArray;
527
+ exports.formatRoutePattern = formatRoutePattern;
494
528
  exports.getNextAppRouterSourceFolder = getNextAppRouterSourceFolder;
495
529
  exports.getPageRouterSitemapTree = getPageRouterSitemapTree;
496
530
  exports.getPageRouterSourceFolder = getPageRouterSourceFolder;
@@ -119,13 +119,26 @@ function isValCanvasStudioMessage(data) {
119
119
  }
120
120
 
121
121
  /**
122
- * Whether this document was loaded as the studio's canvas frame.
122
+ * Whether this document is showing in the studio's canvas frame.
123
123
  *
124
- * Reads the location directly rather than taking it as an argument, because
125
- * every caller wants the answer for the document it is running in, and passing
126
- * it around is how one of them ends up asking about the wrong one.
124
+ * The parameter marks the document the studio LOADED, and that is not always
125
+ * the document the frame ends up holding. A frame navigates on its own: a site
126
+ * with a login of its own redirects a signed-out canvas to its sign-in page,
127
+ * and the sign-in sends it back to wherever the site chooses. Each of those is
128
+ * a new document without the parameter, and it used to render Val's whole
129
+ * overlay inside the canvas — a studio in the studio, which never told the
130
+ * canvas it was there, so the canvas never went live again until reloaded.
131
+ *
132
+ * So where the frame SITS answers too: a document whose parent is the studio
133
+ * is in the canvas, whatever its own address says. `parentPathname` is that
134
+ * parent's path: `null` for a top-level document, and for a parent on another
135
+ * origin, whose `location` cannot be read and which is not the studio anyway.
127
136
  */
128
137
  function isValCanvasFrame(search) {
138
+ var parentPathname = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
139
+ if (parentPathname !== null && isValStudioPathname(parentPathname)) {
140
+ return true;
141
+ }
129
142
  try {
130
143
  return new URLSearchParams(search).get(VAL_CANVAS_PARAM) !== null;
131
144
  } catch (_unused) {
@@ -133,6 +146,11 @@ function isValCanvasFrame(search) {
133
146
  }
134
147
  }
135
148
 
149
+ /** Whether a path is the studio's own route, where the canvas lives. */
150
+ function isValStudioPathname(pathname) {
151
+ return pathname === "/val" || pathname.startsWith("/val/");
152
+ }
153
+
136
154
  /** Add the canvas marker to a page URL. */
137
155
  function withValCanvasParam(url) {
138
156
  var separator = url.includes("?") ? "&" : "?";
@@ -173,6 +191,21 @@ function parseRoutePattern(pattern) {
173
191
  });
174
192
  }
175
193
 
194
+ /**
195
+ * A parsed pattern written back as segments: the inverse of
196
+ * {@link parseRoutePattern}, in the vocabulary core's `matchRoutePattern`
197
+ * reads.
198
+ */
199
+ function formatRoutePattern(pattern) {
200
+ return pattern.map(function (part) {
201
+ if (part.type === "literal") {
202
+ return part.name;
203
+ }
204
+ var inner = part.type === "string-param" ? part.paramName : "...".concat(part.paramName);
205
+ return part.optional ? "[[".concat(inner, "]]") : "[".concat(inner, "]");
206
+ });
207
+ }
208
+
176
209
  function getNextAppRouterSourceFolder(moduleFilePath) {
177
210
  if (moduleFilePath.startsWith("/app")) {
178
211
  return "/app";
@@ -475,4 +508,4 @@ function getPatternFromModuleFilePath(moduleFilePath, srcFolder) {
475
508
  .replace("/page.val.ts", "").replace("/page.val.js", "");
476
509
  }
477
510
 
478
- export { DEFAULT_CANVAS_SELECTION as D, VAL_CONFIG_SESSION_STORAGE_KEY as V, _createForOfIteratorHelper as _, VAL_THEME_SESSION_STORAGE_KEY as a, VAL_AI_SESSION_STORAGE_KEY as b, VAL_CANVAS_PARAM as c, VAL_CANVAS_MESSAGE as d, DEFAULT_CANVAS_SELECTION_SOFT as e, isValCanvasStudioMessage as f, isValCanvasFrame as g, getNextAppRouterSourceFolder as h, isValCanvasPageMessage as i, getTanStackRouterSourceFolder as j, getPageRouterSourceFolder as k, isTanStackRoutesFolder as l, getPageRouterSitemapTree as m, getPatternFromModuleFilePath as n, _objectSpread2 as o, parseRoutePattern as p, _arrayLikeToArray as q, _unsupportedIterableToArray as r, _typeof as s, _defineProperty as t, _toPropertyKey as u, withValCanvasParam as w };
511
+ export { DEFAULT_CANVAS_SELECTION as D, VAL_CONFIG_SESSION_STORAGE_KEY as V, _createForOfIteratorHelper as _, VAL_THEME_SESSION_STORAGE_KEY as a, VAL_AI_SESSION_STORAGE_KEY as b, VAL_CANVAS_PARAM as c, VAL_CANVAS_MESSAGE as d, DEFAULT_CANVAS_SELECTION_SOFT as e, isValCanvasStudioMessage as f, isValCanvasFrame as g, formatRoutePattern as h, isValCanvasPageMessage as i, getNextAppRouterSourceFolder as j, getTanStackRouterSourceFolder as k, getPageRouterSourceFolder as l, isTanStackRoutesFolder as m, getPageRouterSitemapTree as n, getPatternFromModuleFilePath as o, parseRoutePattern as p, _objectSpread2 as q, _arrayLikeToArray as r, _unsupportedIterableToArray as s, _typeof as t, _defineProperty as u, _toPropertyKey as v, withValCanvasParam as w };
@@ -121,13 +121,26 @@ function isValCanvasStudioMessage(data) {
121
121
  }
122
122
 
123
123
  /**
124
- * Whether this document was loaded as the studio's canvas frame.
124
+ * Whether this document is showing in the studio's canvas frame.
125
125
  *
126
- * Reads the location directly rather than taking it as an argument, because
127
- * every caller wants the answer for the document it is running in, and passing
128
- * it around is how one of them ends up asking about the wrong one.
126
+ * The parameter marks the document the studio LOADED, and that is not always
127
+ * the document the frame ends up holding. A frame navigates on its own: a site
128
+ * with a login of its own redirects a signed-out canvas to its sign-in page,
129
+ * and the sign-in sends it back to wherever the site chooses. Each of those is
130
+ * a new document without the parameter, and it used to render Val's whole
131
+ * overlay inside the canvas — a studio in the studio, which never told the
132
+ * canvas it was there, so the canvas never went live again until reloaded.
133
+ *
134
+ * So where the frame SITS answers too: a document whose parent is the studio
135
+ * is in the canvas, whatever its own address says. `parentPathname` is that
136
+ * parent's path: `null` for a top-level document, and for a parent on another
137
+ * origin, whose `location` cannot be read and which is not the studio anyway.
129
138
  */
130
139
  function isValCanvasFrame(search) {
140
+ var parentPathname = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
141
+ if (parentPathname !== null && isValStudioPathname(parentPathname)) {
142
+ return true;
143
+ }
131
144
  try {
132
145
  return new URLSearchParams(search).get(VAL_CANVAS_PARAM) !== null;
133
146
  } catch (_unused) {
@@ -135,6 +148,11 @@ function isValCanvasFrame(search) {
135
148
  }
136
149
  }
137
150
 
151
+ /** Whether a path is the studio's own route, where the canvas lives. */
152
+ function isValStudioPathname(pathname) {
153
+ return pathname === "/val" || pathname.startsWith("/val/");
154
+ }
155
+
138
156
  /** Add the canvas marker to a page URL. */
139
157
  function withValCanvasParam(url) {
140
158
  var separator = url.includes("?") ? "&" : "?";
@@ -175,6 +193,21 @@ function parseRoutePattern(pattern) {
175
193
  });
176
194
  }
177
195
 
196
+ /**
197
+ * A parsed pattern written back as segments: the inverse of
198
+ * {@link parseRoutePattern}, in the vocabulary core's `matchRoutePattern`
199
+ * reads.
200
+ */
201
+ function formatRoutePattern(pattern) {
202
+ return pattern.map(function (part) {
203
+ if (part.type === "literal") {
204
+ return part.name;
205
+ }
206
+ var inner = part.type === "string-param" ? part.paramName : "...".concat(part.paramName);
207
+ return part.optional ? "[[".concat(inner, "]]") : "[".concat(inner, "]");
208
+ });
209
+ }
210
+
178
211
  function getNextAppRouterSourceFolder(moduleFilePath) {
179
212
  if (moduleFilePath.startsWith("/app")) {
180
213
  return "/app";
@@ -491,6 +524,7 @@ exports._objectSpread2 = _objectSpread2;
491
524
  exports._toPropertyKey = _toPropertyKey;
492
525
  exports._typeof = _typeof;
493
526
  exports._unsupportedIterableToArray = _unsupportedIterableToArray;
527
+ exports.formatRoutePattern = formatRoutePattern;
494
528
  exports.getNextAppRouterSourceFolder = getNextAppRouterSourceFolder;
495
529
  exports.getPageRouterSitemapTree = getPageRouterSitemapTree;
496
530
  exports.getPageRouterSourceFolder = getPageRouterSourceFolder;