@valbuild/shared 0.150.10 → 0.150.11

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/client/dist/valbuild-shared-client.cjs.dev.js +16 -2
  3. package/client/dist/valbuild-shared-client.cjs.prod.js +16 -2
  4. package/client/dist/valbuild-shared-client.esm.js +2 -2
  5. package/dist/{acquireTypes-dd976839.cjs.prod.js → acquireTypes-6250a305.cjs.prod.js} +46 -46
  6. package/dist/{acquireTypes-6e2f7961.esm.js → acquireTypes-c2227b23.esm.js} +2 -2
  7. package/dist/{acquireTypes-84a4077a.cjs.dev.js → acquireTypes-c9e757a1.cjs.dev.js} +46 -46
  8. package/dist/{slicedToArray-22567815.esm.js → createForOfIteratorHelper-9b80f1a2.esm.js} +136 -106
  9. package/dist/{slicedToArray-d99b4e2e.cjs.prod.js → createForOfIteratorHelper-9ca2078d.cjs.prod.js} +142 -111
  10. package/dist/{slicedToArray-c8a29f6b.cjs.dev.js → createForOfIteratorHelper-f2d7ac6d.cjs.dev.js} +142 -111
  11. package/dist/declarations/src/client/index.d.ts +2 -0
  12. package/dist/declarations/src/internal/ApiRoutes.d.ts +1 -0
  13. package/dist/declarations/src/internal/index.d.ts +2 -0
  14. package/dist/declarations/src/internal/pageInspection.d.ts +117 -0
  15. package/dist/declarations/src/internal/quickPreview.d.ts +91 -0
  16. package/dist/declarations/src/internal/valCanvasProtocol.d.ts +53 -0
  17. package/dist/declarations/src/publish/buildProposal.d.ts +16 -1
  18. package/dist/declarations/src/publish/buildQuickPreview.d.ts +46 -0
  19. package/dist/declarations/src/publish/index.d.ts +3 -2
  20. package/dist/declarations/src/publish/runStudioDeploy.d.ts +10 -0
  21. package/dist/getSitemapTree-5354ce90.cjs.prod.js +1421 -0
  22. package/dist/getSitemapTree-97b3166f.esm.js +1381 -0
  23. package/dist/getSitemapTree-d934ff9c.cjs.dev.js +1421 -0
  24. package/dist/{proposalProject-689c24cc.esm.js → proposalProject-5da177d5.esm.js} +1 -2
  25. package/dist/{proposalProject-926f2c68.cjs.dev.js → proposalProject-76caa377.cjs.prod.js} +2 -3
  26. package/dist/{proposalProject-fab0c026.cjs.prod.js → proposalProject-81fee08f.cjs.dev.js} +2 -3
  27. package/dist/{protocol-7ddc1a71.esm.js → protocol-55cfa3cf.esm.js} +1 -1
  28. package/dist/{protocol-4e2a455b.cjs.prod.js → protocol-b1055ef0.cjs.prod.js} +6 -6
  29. package/dist/{protocol-ac1c3ec7.cjs.dev.js → protocol-d0efb8cd.cjs.dev.js} +6 -6
  30. package/dist/wrapNativeSuper-639b12b7.esm.js +107 -0
  31. package/dist/wrapNativeSuper-8673414f.cjs.prod.js +113 -0
  32. package/dist/wrapNativeSuper-933f991e.cjs.dev.js +113 -0
  33. package/internal/dist/valbuild-shared-internal.cjs.dev.js +53 -34
  34. package/internal/dist/valbuild-shared-internal.cjs.prod.js +53 -34
  35. package/internal/dist/valbuild-shared-internal.esm.js +11 -6
  36. package/package.json +1 -1
  37. package/publish/dist/valbuild-shared-publish.cjs.dev.js +589 -237
  38. package/publish/dist/valbuild-shared-publish.cjs.prod.js +589 -237
  39. package/publish/dist/valbuild-shared-publish.esm.js +504 -155
  40. package/typecheck/dist/valbuild-shared-typecheck.cjs.dev.js +34 -34
  41. package/typecheck/dist/valbuild-shared-typecheck.cjs.prod.js +34 -34
  42. package/typecheck/dist/valbuild-shared-typecheck.esm.js +5 -5
  43. package/typecheck/testing/dist/valbuild-shared-typecheck-testing.cjs.dev.js +7 -7
  44. package/typecheck/testing/dist/valbuild-shared-typecheck-testing.cjs.prod.js +7 -7
  45. package/typecheck/testing/dist/valbuild-shared-typecheck-testing.esm.js +3 -3
  46. package/dist/createForOfIteratorHelper-666a6d17.esm.js +0 -137
  47. package/dist/createForOfIteratorHelper-87a33eff.cjs.dev.js +0 -145
  48. package/dist/createForOfIteratorHelper-fef4fa7d.cjs.prod.js +0 -145
  49. package/dist/getSitemapTree-873f7266.cjs.prod.js +0 -743
  50. package/dist/getSitemapTree-b6161fd8.esm.js +0 -717
  51. package/dist/getSitemapTree-be70d9fc.cjs.dev.js +0 -743
@@ -0,0 +1,1381 @@
1
+ import { _ as _typeof, a as _slicedToArray, b as _asyncToGenerator, c as _regenerator, d as _objectSpread2, e as _toConsumableArray, f as _createForOfIteratorHelper } from './createForOfIteratorHelper-9b80f1a2.esm.js';
2
+ import { Internal, tanStackSegmentsOfRoutePath } from '@valbuild/core';
3
+
4
+ var VAL_CONFIG_SESSION_STORAGE_KEY = "val-config";
5
+ var VAL_THEME_SESSION_STORAGE_KEY = "val-theme";
6
+ var VAL_AI_SESSION_STORAGE_KEY = "val-ai-session";
7
+
8
+ /**
9
+ * A quick preview: a proposal's page, running the proposal's CLIENT code
10
+ * built in the design tab, without a build being uploaded or served
11
+ * (valbuild/home `docs/design-tab.md`).
12
+ *
13
+ * A full build of a proposal bundles the app for the server and the browser,
14
+ * uploads it, and has the platform render every page before it is served --
15
+ * minutes. Most of what the design tab changes is components and styles,
16
+ * which the browser half alone shows. So the tab bundles the app in its
17
+ * builder worker, and a service worker on the proposal's address hands the
18
+ * page that bundle instead of the served one:
19
+ *
20
+ * 1. The tab sends the files (`/_app/app.js`, its chunks, the stylesheet and
21
+ * assets) to the page in the preview, over the canvas protocol.
22
+ * 2. The page -- the bridge, which is the only thing at that origin the tab
23
+ * can talk to -- registers {@link QUICK_PREVIEW_SERVICE_WORKER_SOURCE}
24
+ * and stores the files in it (Cache Storage, so a worker the browser
25
+ * stopped still has them).
26
+ * 3. The tab opens the page with `?val_quick=<id>`. The worker serves that
27
+ * document's `/_app/` requests from the files, and everything else from
28
+ * the network: the HTML is the last build's, rendered by its server, and
29
+ * the new code takes it over in the browser.
30
+ *
31
+ * What it cannot show, and the tab says so: anything on the server. Server
32
+ * functions, API routes and loaders that run there are the last build's, as
33
+ * is the page's first render and its `head`. A new dependency is not there
34
+ * either. A build is still what decides, before Publish.
35
+ *
36
+ * Only a document opened with the parameter is served from the files, so a
37
+ * visitor to the proposal's address -- even in the same browser -- is never
38
+ * given them.
39
+ */
40
+
41
+ /** The parameter a quick preview's page is opened with. */
42
+ var QUICK_PREVIEW_PARAM = "val_quick";
43
+
44
+ /** Where Val's API serves the worker: under `/api/val`. */
45
+ var QUICK_PREVIEW_SERVICE_WORKER_PATH = "/quick-preview-sw.js";
46
+
47
+ /** What the page sends the worker to store a preview's files. */
48
+ var QUICK_PREVIEW_STORE_MESSAGE = "val-quick-preview-store";
49
+
50
+ /**
51
+ * A preview's files, by their path under `/_app/`: `app.js`, `c-<hash>.js`,
52
+ * `app.css`, `a/<hash>.png`. A binary file is base64.
53
+ */
54
+
55
+ /**
56
+ * The files of a quick preview: paths under `/_app/` with no way out of it,
57
+ * each a string body and a type.
58
+ */
59
+ function isQuickPreviewFiles(value) {
60
+ if (_typeof(value) !== "object" || value === null) return false;
61
+ return Object.entries(value).every(function (_ref) {
62
+ var _ref2 = _slicedToArray(_ref, 2),
63
+ path = _ref2[0],
64
+ file = _ref2[1];
65
+ return /^[A-Za-z0-9_][A-Za-z0-9_./-]*$/.test(path) && !path.includes("..") && _typeof(file) === "object" && file !== null && typeof file.body === "string" && typeof file.contentType === "string";
66
+ });
67
+ }
68
+
69
+ /**
70
+ * The worker, as the source it is served as: plain, and small, because it is
71
+ * served as a file of its own (`demoServiceWorker.ts` is the same).
72
+ */
73
+ var QUICK_PREVIEW_SERVICE_WORKER_SOURCE = "\n\"use strict\";\nconst PARAM = \"".concat(QUICK_PREVIEW_PARAM, "\";\nconst CACHE = \"val-quick-preview\";\nconst PREFIX = \"/__val_quick/\";\nconst INDEX = PREFIX + \"index\";\nconst KEEP = 4;\n\nself.addEventListener(\"install\", () => self.skipWaiting());\nself.addEventListener(\"activate\", (event) => {\n event.waitUntil(self.clients.claim());\n});\n\nself.addEventListener(\"message\", (event) => {\n const data = event.data;\n if (!data || data.type !== \"").concat(QUICK_PREVIEW_STORE_MESSAGE, "\") return;\n const port = event.ports && event.ports[0];\n event.waitUntil(\n store(data.id, data.files).then(\n () => port && port.postMessage({ ok: true }),\n (error) =>\n port &&\n port.postMessage({\n ok: false,\n message: String((error && error.message) || error),\n }),\n ),\n );\n});\n\nfunction keyOf(id, path) {\n return PREFIX + id + \"/\" + path;\n}\n\nasync function store(id, files) {\n const cache = await caches.open(CACHE);\n for (const path of Object.keys(files)) {\n const file = files[path];\n const body = file.base64\n ? Uint8Array.from(atob(file.body), (c) => c.charCodeAt(0))\n : file.body;\n await cache.put(\n keyOf(id, path),\n new Response(body, {\n headers: { \"Content-Type\": file.contentType, \"Cache-Control\": \"no-store\" },\n }),\n );\n }\n // The newest few previews are kept; older ones are deleted.\n const index = await cache.match(INDEX);\n const ids = [id].concat(\n (index ? await index.json() : []).filter((other) => other !== id),\n );\n const dropped = ids.slice(KEEP);\n if (dropped.length > 0) {\n for (const request of await cache.keys()) {\n const path = new URL(request.url).pathname;\n if (dropped.some((old) => path.startsWith(PREFIX + old + \"/\"))) {\n await cache.delete(request);\n }\n }\n }\n await cache.put(INDEX, new Response(JSON.stringify(ids.slice(0, KEEP))));\n}\n\n/** Which preview a document is: by its id, or by the address it was opened at. */\nconst byClient = new Map();\nasync function previewOf(clientId) {\n if (!clientId) return null;\n if (byClient.has(clientId)) return byClient.get(clientId);\n const client = await self.clients.get(clientId);\n const id = client ? new URL(client.url).searchParams.get(PARAM) : null;\n byClient.set(clientId, id);\n return id;\n}\n\nself.addEventListener(\"fetch\", (event) => {\n const url = new URL(event.request.url);\n if (url.origin !== self.location.origin) return;\n if (event.request.mode === \"navigate\") {\n if (event.resultingClientId) {\n byClient.set(event.resultingClientId, url.searchParams.get(PARAM));\n }\n return;\n }\n if (!url.pathname.startsWith(\"/_app/\")) return;\n event.respondWith(serve(event, url));\n});\n\nasync function serve(event, url) {\n const id = await previewOf(event.clientId);\n if (id) {\n const hit = await caches.match(keyOf(id, url.pathname.slice(6)), {\n cacheName: CACHE,\n });\n if (hit) return hit;\n }\n return fetch(event.request);\n}\n");
74
+
75
+ /** How long the worker is given to install and store a preview. */
76
+ var STORE_TIMEOUT_MS = 30000;
77
+
78
+ /**
79
+ * Store a preview's files in the worker, registering it first: what the
80
+ * bridge does when the tab sends `quickPreview`. `swUrl` is the worker's
81
+ * address on this origin (`/api/val` + {@link QUICK_PREVIEW_SERVICE_WORKER_PATH}).
82
+ */
83
+ function storeQuickPreview(_x, _x2, _x3) {
84
+ return _storeQuickPreview.apply(this, arguments);
85
+ }
86
+
87
+ /** The registration's worker, once it is active. */
88
+ function _storeQuickPreview() {
89
+ _storeQuickPreview = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(id, files, options) {
90
+ var _options$container;
91
+ var container, registration, worker;
92
+ return _regenerator().w(function (_context) {
93
+ while (1) switch (_context.n) {
94
+ case 0:
95
+ container = (_options$container = options.container) !== null && _options$container !== void 0 ? _options$container : typeof navigator !== "undefined" ? navigator.serviceWorker : undefined;
96
+ if (!(container === undefined)) {
97
+ _context.n = 1;
98
+ break;
99
+ }
100
+ throw new Error("This browser cannot show a quick preview here: it has no service workers on this address (a private window, or one that is not https).");
101
+ case 1:
102
+ _context.n = 2;
103
+ return container.register(options.swUrl, {
104
+ scope: "/"
105
+ });
106
+ case 2:
107
+ registration = _context.v;
108
+ _context.n = 3;
109
+ return activeWorker(registration);
110
+ case 3:
111
+ worker = _context.v;
112
+ _context.n = 4;
113
+ return new Promise(function (resolve, reject) {
114
+ var channel = new MessageChannel();
115
+ var timer = setTimeout(function () {
116
+ return reject(new Error("The quick preview's worker did not store the files."));
117
+ }, STORE_TIMEOUT_MS);
118
+ channel.port1.onmessage = function (event) {
119
+ clearTimeout(timer);
120
+ var answer = event.data;
121
+ if (_typeof(answer) === "object" && answer !== null && "ok" in answer && answer.ok === true) {
122
+ resolve();
123
+ } else {
124
+ reject(new Error(_typeof(answer) === "object" && answer !== null && "message" in answer && typeof answer.message === "string" ? answer.message : "The quick preview's worker could not store the files."));
125
+ }
126
+ };
127
+ worker.postMessage({
128
+ type: QUICK_PREVIEW_STORE_MESSAGE,
129
+ id: id,
130
+ files: files
131
+ }, [channel.port2]);
132
+ });
133
+ case 4:
134
+ return _context.a(2);
135
+ }
136
+ }, _callee);
137
+ }));
138
+ return _storeQuickPreview.apply(this, arguments);
139
+ }
140
+ function activeWorker(registration) {
141
+ var _registration$install;
142
+ var active = registration.active;
143
+ if (active !== null) return Promise.resolve(active);
144
+ var pending = (_registration$install = registration.installing) !== null && _registration$install !== void 0 ? _registration$install : registration.waiting;
145
+ if (pending === null) {
146
+ return Promise.reject(new Error("The quick preview's worker did not install."));
147
+ }
148
+ return new Promise(function (resolve, reject) {
149
+ var timer = setTimeout(function () {
150
+ return reject(new Error("The quick preview's worker did not start."));
151
+ }, STORE_TIMEOUT_MS);
152
+ pending.addEventListener("statechange", function () {
153
+ if (pending.state === "activated") {
154
+ clearTimeout(timer);
155
+ resolve(pending);
156
+ } else if (pending.state === "redundant") {
157
+ clearTimeout(timer);
158
+ reject(new Error("The quick preview's worker was replaced."));
159
+ }
160
+ });
161
+ });
162
+ }
163
+
164
+ /** The address of a page as a quick preview `id`. */
165
+ function withQuickPreviewParam(url, id) {
166
+ var parsed = new URL(url);
167
+ parsed.searchParams.set(QUICK_PREVIEW_PARAM, id);
168
+ return parsed.toString();
169
+ }
170
+
171
+ /**
172
+ * The one origin allowed to send this page a quick preview: the site's, where
173
+ * the design tab is, as this page's own Val server says it (the proposals
174
+ * list's `siteUrl`). `null` when it does not say.
175
+ *
176
+ * A quick preview is code that runs on this address, so the bridge takes one
177
+ * only from its parent, and only when that parent is the site: a page that
178
+ * framed this one could otherwise post a script into the worker and open
179
+ * the frame on it.
180
+ */
181
+ function quickPreviewSiteOrigin(_x4) {
182
+ return _quickPreviewSiteOrigin.apply(this, arguments);
183
+ }
184
+
185
+ /** The site's origin, as this page's server named it: asked once per page. */
186
+ function _quickPreviewSiteOrigin() {
187
+ _quickPreviewSiteOrigin = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2(api) {
188
+ var fetchImpl,
189
+ res,
190
+ body,
191
+ siteUrl,
192
+ _args2 = arguments;
193
+ return _regenerator().w(function (_context2) {
194
+ while (1) switch (_context2.p = _context2.n) {
195
+ case 0:
196
+ fetchImpl = _args2.length > 1 && _args2[1] !== undefined ? _args2[1] : fetch;
197
+ _context2.n = 1;
198
+ return fetchImpl("".concat(api.replace(/\/+$/, ""), "/proposals-api"), {
199
+ credentials: "same-origin",
200
+ headers: {
201
+ Accept: "application/json"
202
+ }
203
+ });
204
+ case 1:
205
+ res = _context2.v;
206
+ if (res.ok) {
207
+ _context2.n = 2;
208
+ break;
209
+ }
210
+ return _context2.a(2, null);
211
+ case 2:
212
+ _context2.n = 3;
213
+ return res.json();
214
+ case 3:
215
+ body = _context2.v;
216
+ if (!(_typeof(body) !== "object" || body === null || !("siteUrl" in body))) {
217
+ _context2.n = 4;
218
+ break;
219
+ }
220
+ return _context2.a(2, null);
221
+ case 4:
222
+ siteUrl = body.siteUrl;
223
+ if (!(typeof siteUrl !== "string")) {
224
+ _context2.n = 5;
225
+ break;
226
+ }
227
+ return _context2.a(2, null);
228
+ case 5:
229
+ _context2.p = 5;
230
+ return _context2.a(2, new URL(siteUrl).origin);
231
+ case 6:
232
+ _context2.p = 6;
233
+ _context2.v;
234
+ return _context2.a(2, null);
235
+ }
236
+ }, _callee2, null, [[5, 6]]);
237
+ }));
238
+ return _quickPreviewSiteOrigin.apply(this, arguments);
239
+ }
240
+ var siteOriginOnce = null;
241
+
242
+ /**
243
+ * Whether `origin` may ask this page for what only the design tab should
244
+ * have -- what is on it (`readPage`), or code to run (`quickPreview`): this
245
+ * page's own origin (the Studio's canvas), or the site's, where the design
246
+ * tab is ({@link quickPreviewSiteOrigin}). Asked of the server once, and
247
+ * again only while it has not said.
248
+ */
249
+ function isDesignTabOrigin(_x5, _x6) {
250
+ return _isDesignTabOrigin.apply(this, arguments);
251
+ }
252
+ function _isDesignTabOrigin() {
253
+ _isDesignTabOrigin = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3(origin, options) {
254
+ var site;
255
+ return _regenerator().w(function (_context3) {
256
+ while (1) switch (_context3.n) {
257
+ case 0:
258
+ if (!(origin === options.own)) {
259
+ _context3.n = 1;
260
+ break;
261
+ }
262
+ return _context3.a(2, true);
263
+ case 1:
264
+ siteOriginOnce !== null && siteOriginOnce !== void 0 ? siteOriginOnce : siteOriginOnce = quickPreviewSiteOrigin(options.api, options.fetchImpl)["catch"](function () {
265
+ return null;
266
+ });
267
+ _context3.n = 2;
268
+ return siteOriginOnce;
269
+ case 2:
270
+ site = _context3.v;
271
+ if (site === null) siteOriginOnce = null;
272
+ return _context3.a(2, site !== null && site === origin);
273
+ }
274
+ }, _callee3);
275
+ }));
276
+ return _isDesignTabOrigin.apply(this, arguments);
277
+ }
278
+
279
+ /** Marks a page load as the studio's canvas frame. */
280
+ var VAL_CANVAS_PARAM = "val_canvas";
281
+
282
+ /**
283
+ * Every message is tagged with this, and the tag is checked on receipt.
284
+ *
285
+ * A window receives messages from anything that has a handle on it —
286
+ * extensions, embeds, other frames — and `event.data` is whatever they chose to
287
+ * send. Checking a discriminator before reading anything else is what keeps a
288
+ * stray message from being read as a canvas instruction.
289
+ */
290
+ var VAL_CANVAS_MESSAGE = "val-canvas";
291
+
292
+ /** One element on the page that Val has a path for. */
293
+
294
+ /**
295
+ * The colours Val outlines the page's content in, when nobody has said
296
+ * otherwise.
297
+ *
298
+ * They mirror `--bg-page-selection` and `--bg-page-selection-soft` in
299
+ * `packages/ui/spa/index.css`, and they are literals here because the page the
300
+ * bridge runs in has none of Val's stylesheet — no `:host`, no tokens, nothing
301
+ * to inherit. A CSS custom property genuinely cannot reach across that
302
+ * boundary, which is why the colours are SENT (see the `theme` message below)
303
+ * rather than derived.
304
+ *
305
+ * They live beside the protocol rather than with the theme code on purpose:
306
+ * `@valbuild/shared/client` is what the bridge imports, and it exists to keep
307
+ * production bundles small (see the docblock on that entrypoint). Two strings
308
+ * cost nothing; the accent generator that produces the non-default values is
309
+ * ~350 lines of colour maths that no visitor to a Val site should download.
310
+ *
311
+ * `canvasSelectionDefaults.test.ts` holds them equal to the stylesheet.
312
+ */
313
+ var DEFAULT_CANVAS_SELECTION = "#079455";
314
+ var DEFAULT_CANVAS_SELECTION_SOFT = "rgba(7, 148, 85, 0.4)";
315
+
316
+ /** What the page tells the studio. */
317
+
318
+ /** What the studio tells the page. */
319
+
320
+ /** Narrow an unknown `event.data` to a message from the page. */
321
+ function isValCanvasPageMessage(data) {
322
+ if (_typeof(data) !== "object" || data === null) return false;
323
+ var message = data;
324
+ if (message.val !== VAL_CANVAS_MESSAGE) return false;
325
+ return message.type === "ready" || message.type === "elements" || message.type === "clicked" || message.type === "pinch" || message.type === "zoom" || message.type === "refreshing" || message.type === "elementPickingStopped" || message.type === "elementPicked" && isPickedElement(data.element) || message.type === "location" && isPageLocation(data) || message.type === "pageRead" && typeof data.requestId === "string" || message.type === "quickPreviewStored" && typeof data.id === "string";
326
+ }
327
+ function isPageLocation(value) {
328
+ var location = value;
329
+ return typeof location.path === "string" && location.path.startsWith("/") && typeof location.scrollX === "number" && typeof location.scrollY === "number";
330
+ }
331
+ function isPickedElement(value) {
332
+ if (_typeof(value) !== "object" || value === null) return false;
333
+ var element = value;
334
+ return typeof element.tag === "string" && typeof element.text === "string" && typeof element.selector === "string" && Array.isArray(element.classes) && element.classes.every(function (item) {
335
+ return typeof item === "string";
336
+ }) && (element.valPath === undefined || typeof element.valPath === "string");
337
+ }
338
+ var PICK_MODIFIERS = ["alt", "meta", "shift", "ctrl"];
339
+
340
+ /**
341
+ * Whether a value is a colour a `theme` message may carry.
342
+ *
343
+ * A whitelist rather than an escape, and the reason is where these two strings
344
+ * end up: the bridge writes them into a `<style>` element in the CUSTOMER's
345
+ * document with `dangerouslySetInnerHTML`, so a value containing `</style>` is
346
+ * a script tag away from running in someone else's page. A page can be
347
+ * `postMessage`d by any window holding a handle on it, and this guard is the
348
+ * only thing between such a message and that sink.
349
+ *
350
+ * The two shapes are exactly what `themeCustomProperties` produces — a hex, and
351
+ * the `rgba()` it derives the soft outline as. Narrowing the contract is easier
352
+ * to keep true than sanitising an open one, and a forged message that passes
353
+ * this can do no more than pick a different colour.
354
+ */
355
+ var CANVAS_SELECTION_COLOR = /^(?:#[0-9a-f]{3,8}|rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*(?:,\s*(?:0|1|0?\.\d+)\s*)?\))$/i;
356
+ function isCanvasSelectionColor(value) {
357
+ return typeof value === "string" && CANVAS_SELECTION_COLOR.test(value);
358
+ }
359
+
360
+ /** Narrow an unknown `event.data` to a message from the studio. */
361
+ function isValCanvasStudioMessage(data) {
362
+ if (_typeof(data) !== "object" || data === null) return false;
363
+ var message = data;
364
+ if (message.val !== VAL_CANVAS_MESSAGE) return false;
365
+ if (message.type === "theme") {
366
+ // Checked here and not in the bridge: a rejected message is never applied
367
+ // at all, and there is one guard rather than one per framework binding.
368
+ return isCanvasSelectionColor(message.selection) && isCanvasSelectionColor(message.selectionSoft);
369
+ }
370
+ if (message.type === "pickElements") {
371
+ var pick = data;
372
+ return typeof pick.enabled === "boolean" && typeof pick.active === "boolean" && typeof pick.keepPicked === "boolean" && PICK_MODIFIERS.includes(pick.modifier);
373
+ }
374
+ if (message.type === "scrollTo") {
375
+ var scroll = data;
376
+ return typeof scroll.x === "number" && Number.isFinite(scroll.x) && typeof scroll.y === "number" && Number.isFinite(scroll.y);
377
+ }
378
+ if (message.type === "quickPreview") {
379
+ var preview = data;
380
+ return typeof preview.id === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(preview.id) && isQuickPreviewFiles(preview.files);
381
+ }
382
+ if (message.type === "readPage") {
383
+ var read = data;
384
+ return typeof read.requestId === "string" && (read.selector === undefined || typeof read.selector === "string") && (read.maxItems === undefined || typeof read.maxItems === "number");
385
+ }
386
+ return message.type === "rescan" || message.type === "highlight" || message.type === "setPicking" || message.type === "sourceUpdate" || message.type === "sourcesSynced";
387
+ }
388
+
389
+ /**
390
+ * Whether this document is showing in the studio's canvas frame.
391
+ *
392
+ * The parameter marks the document the studio LOADED, and that is not always
393
+ * the document the frame ends up holding. A frame navigates on its own: a site
394
+ * with a login of its own redirects a signed-out canvas to its sign-in page,
395
+ * and the sign-in sends it back to wherever the site chooses. Each of those is
396
+ * a new document without the parameter, and it used to render Val's whole
397
+ * overlay inside the canvas — a studio in the studio, which never told the
398
+ * canvas it was there, so the canvas never went live again until reloaded.
399
+ *
400
+ * So where the frame SITS answers too: a document whose parent is the studio
401
+ * is in the canvas, whatever its own address says. `parentPathname` is that
402
+ * parent's path: `null` for a top-level document, and for a parent on another
403
+ * origin, whose `location` cannot be read and which is not the studio anyway.
404
+ */
405
+ function isValCanvasFrame(search) {
406
+ var parentPathname = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
407
+ if (parentPathname !== null && isValStudioPathname(parentPathname)) {
408
+ return true;
409
+ }
410
+ try {
411
+ return new URLSearchParams(search).get(VAL_CANVAS_PARAM) !== null;
412
+ } catch (_unused) {
413
+ return false;
414
+ }
415
+ }
416
+
417
+ /** Whether a path is the studio's own route, where the canvas lives. */
418
+ function isValStudioPathname(pathname) {
419
+ return pathname === "/val" || pathname.startsWith("/val/");
420
+ }
421
+
422
+ /** Add the canvas marker to a page URL. */
423
+ function withValCanvasParam(url) {
424
+ var separator = url.includes("?") ? "&" : "?";
425
+ return "".concat(url).concat(separator).concat(VAL_CANVAS_PARAM, "=1");
426
+ }
427
+
428
+ /**
429
+ * Pointing at anything on a page, as Lovable's picker does: hover outlines
430
+ * the element under the pointer, a click picks it, and the assistant is told
431
+ * which (valbuild/home `docs/design-tab.md`).
432
+ *
433
+ * ANY element, not only what Val marks. The design tab changes code, and the
434
+ * thing a person wants changed is as often a wrapper, a gap or a button as a
435
+ * field -- so a field's path is extra information when there is one
436
+ * (`valPath`), never the condition for picking.
437
+ *
438
+ * Picking is on while a MODIFIER is held, so the page stays a page: links
439
+ * follow, menus open, and nothing has to be switched back off. `active`
440
+ * turns it on without one, for a Select button or a touch screen.
441
+ *
442
+ * Plain DOM, nothing of React's or the Studio's, and in `shared` because
443
+ * both halves need it: the canvas bridge (`@valbuild/next`,
444
+ * `@valbuild/tanstack`) runs it inside the proposal's page, which is another
445
+ * origin than the design tab and the only place the page's elements can be
446
+ * reached; the Studio's stories run it on a page of their own. Only the
447
+ * `PickedElement` crosses the frame (`valCanvasProtocol.ts`).
448
+ */
449
+
450
+ var TEXT_LENGTH$1 = 80;
451
+ var CLASSES = 6;
452
+ function describeElement(element, root) {
453
+ var _element$textContent, _element$closest$getA, _element$closest;
454
+ var text = ((_element$textContent = element.textContent) !== null && _element$textContent !== void 0 ? _element$textContent : "").replace(/\s+/g, " ").trim();
455
+ var valPath = (_element$closest$getA = (_element$closest = element.closest("[data-val-path]")) === null || _element$closest === void 0 ? void 0 : _element$closest.getAttribute("data-val-path")) !== null && _element$closest$getA !== void 0 ? _element$closest$getA : undefined;
456
+ return _objectSpread2({
457
+ tag: element.tagName.toLowerCase(),
458
+ text: text.length > TEXT_LENGTH$1 ? "".concat(text.slice(0, TEXT_LENGTH$1 - 1), "\u2026") : text,
459
+ classes: Array.from(element.classList).slice(0, CLASSES),
460
+ selector: selectorOf$1(element, root)
461
+ }, valPath !== undefined ? {
462
+ valPath: valPath
463
+ } : {});
464
+ }
465
+ function selectorOf$1(element, root) {
466
+ var parts = [];
467
+ var at = element;
468
+ while (at !== null && at !== root) {
469
+ var tag = at.tagName.toLowerCase();
470
+ if (at.id !== "") {
471
+ parts.unshift("".concat(tag, "#").concat(at.id));
472
+ break;
473
+ }
474
+ var parent = at.parentElement;
475
+ var same = parent === null ? [] : Array.from(parent.children).filter(function (sibling) {
476
+ var _at;
477
+ return sibling.tagName === ((_at = at) === null || _at === void 0 ? void 0 : _at.tagName);
478
+ });
479
+ parts.unshift(same.length > 1 ? "".concat(tag, ":nth-of-type(").concat(same.indexOf(at) + 1, ")") : tag);
480
+ at = parent;
481
+ }
482
+ return parts.join(" > ");
483
+ }
484
+
485
+ /** What a person is told to hold, on their own keyboard. */
486
+ function modifierLabel(modifier, mac) {
487
+ switch (modifier) {
488
+ case "alt":
489
+ return mac ? "⌥" : "Alt";
490
+ case "meta":
491
+ return mac ? "⌘" : "Win";
492
+ case "shift":
493
+ return mac ? "⇧" : "Shift";
494
+ case "ctrl":
495
+ return mac ? "⌃" : "Ctrl";
496
+ }
497
+ }
498
+ function held(event, modifier) {
499
+ switch (modifier) {
500
+ case "alt":
501
+ return event.altKey;
502
+ case "meta":
503
+ return event.metaKey;
504
+ case "shift":
505
+ return event.shiftKey;
506
+ case "ctrl":
507
+ return event.ctrlKey;
508
+ }
509
+ }
510
+
511
+ /** How far a touch may move and still be a tap, not a scroll. */
512
+ var TAP_SLOP_PX = 10;
513
+ /** How long a touch may be held and still be a tap. */
514
+ var TAP_MS = 600;
515
+ /** How long after a tap its click may still arrive. */
516
+ var TAP_CLICK_MS = 800;
517
+ /**
518
+ * Listens on `root` (capturing, so the page's own handlers never see a pick)
519
+ * and returns what stops it.
520
+ */
521
+ function attachElementPicker(root, options) {
522
+ var win = root.ownerDocument.defaultView;
523
+ var last = null;
524
+ var picking = function picking(event) {
525
+ return options.active() || held(event, options.modifier);
526
+ };
527
+ var target = function target(event) {
528
+ return event.target instanceof Element && root.contains(event.target) ? event.target : null;
529
+ };
530
+ var onMove = function onMove(event) {
531
+ last = {
532
+ x: event.clientX,
533
+ y: event.clientY
534
+ };
535
+ options.onHover(picking(event) ? target(event) : null);
536
+ };
537
+ /*
538
+ * A tap. iOS Safari dispatches no click for a tap on an element nothing on
539
+ * the way to it listens to -- a listener on the body does not count -- so
540
+ * a tap is taken from the pointer events: a touch that went up where it
541
+ * came down, soon, picks. One that moved is a scroll, which the browser
542
+ * reports as `pointercancel` or as distance, and picks nothing.
543
+ */
544
+ var down = null;
545
+ /** When a tap last picked: the click a browser may still send is the same tap. */
546
+ var tappedAt = -Infinity;
547
+ var onPointerDown = function onPointerDown(event) {
548
+ down = event.pointerType !== "mouse" && options.active() ? {
549
+ x: event.clientX,
550
+ y: event.clientY,
551
+ at: Date.now()
552
+ } : null;
553
+ };
554
+ var onPointerCancel = function onPointerCancel() {
555
+ down = null;
556
+ };
557
+ var onPointerUp = function onPointerUp(event) {
558
+ var start = down;
559
+ down = null;
560
+ if (start === null || !options.active()) return;
561
+ var moved = Math.hypot(event.clientX - start.x, event.clientY - start.y);
562
+ if (moved > TAP_SLOP_PX || Date.now() - start.at > TAP_MS) return;
563
+ var element = target(event);
564
+ if (element === null) return;
565
+ event.preventDefault();
566
+ event.stopPropagation();
567
+ tappedAt = Date.now();
568
+ options.onHover(element);
569
+ options.onPick(element);
570
+ };
571
+ var onClick = function onClick(event) {
572
+ if (!picking(event)) return;
573
+ var element = target(event);
574
+ if (element === null) return;
575
+ event.preventDefault();
576
+ event.stopPropagation();
577
+ // Already picked by the tap this click comes from.
578
+ if (Date.now() - tappedAt < TAP_CLICK_MS) return;
579
+ options.onPick(element);
580
+ };
581
+ // Pressing the modifier with the pointer still: outline what is under it
582
+ // now, rather than after the next move.
583
+ var onKey = function onKey(event) {
584
+ if (!picking(event)) {
585
+ options.onHover(null);
586
+ return;
587
+ }
588
+ if (last === null) return;
589
+ var under = root.ownerDocument.elementFromPoint(last.x, last.y);
590
+ options.onHover(under !== null && root.contains(under) ? under : null);
591
+ };
592
+ var onLeave = function onLeave() {
593
+ last = null;
594
+ options.onHover(null);
595
+ };
596
+ root.addEventListener("mousemove", onMove, true);
597
+ root.addEventListener("click", onClick, true);
598
+ root.addEventListener("pointerdown", onPointerDown, true);
599
+ root.addEventListener("pointerup", onPointerUp, true);
600
+ root.addEventListener("pointercancel", onPointerCancel, true);
601
+ root.addEventListener("mouseleave", onLeave);
602
+ win === null || win === void 0 || win.addEventListener("keydown", onKey, true);
603
+ win === null || win === void 0 || win.addEventListener("keyup", onKey, true);
604
+ return function () {
605
+ root.removeEventListener("mousemove", onMove, true);
606
+ root.removeEventListener("click", onClick, true);
607
+ root.removeEventListener("pointerdown", onPointerDown, true);
608
+ root.removeEventListener("pointerup", onPointerUp, true);
609
+ root.removeEventListener("pointercancel", onPointerCancel, true);
610
+ root.removeEventListener("mouseleave", onLeave);
611
+ win === null || win === void 0 || win.removeEventListener("keydown", onKey, true);
612
+ win === null || win === void 0 || win.removeEventListener("keyup", onKey, true);
613
+ };
614
+ }
615
+
616
+ /**
617
+ * The outlines over the page while picking: the element under the pointer,
618
+ * named, and the one picked.
619
+ *
620
+ * Boxes `position: fixed` on top of everything, with `pointer-events: none`,
621
+ * appended to the page's body -- not outlines on the elements themselves.
622
+ * The canvas's field outlines are a stylesheet so they cannot disturb the
623
+ * page's layout (`ValCanvasBridge`); a fixed box is out of the flow, so it
624
+ * cannot either, and it is the one way to put a NAME beside any element, which
625
+ * a selector cannot reach.
626
+ *
627
+ * In a shadow root of one host element: the bridge re-measures the page on
628
+ * every `style` change under `body`, and a box that follows the pointer
629
+ * changes its style every frame. Inside a shadow root those changes are not
630
+ * the page's, so the host's one insertion is all the bridge sees.
631
+ */
632
+
633
+ var TOP = "2147483647";
634
+ function createPickerOverlay(doc, initialColor) {
635
+ var color = initialColor;
636
+ var shown = {
637
+ hover: null,
638
+ picked: null
639
+ };
640
+ var host = doc.createElement("div");
641
+ host.setAttribute("data-val-picker", "");
642
+ var shadow = host.attachShadow({
643
+ mode: "open"
644
+ });
645
+ doc.body.appendChild(host);
646
+ var box = function box(kind) {
647
+ var el = doc.createElement("div");
648
+ el.setAttribute("data-kind", kind);
649
+ Object.assign(el.style, {
650
+ position: "fixed",
651
+ pointerEvents: "none",
652
+ zIndex: TOP,
653
+ boxSizing: "border-box",
654
+ display: "none"
655
+ });
656
+ shadow.appendChild(el);
657
+ return el;
658
+ };
659
+ var boxes = {
660
+ hover: box("hover"),
661
+ picked: box("picked")
662
+ };
663
+ var label = doc.createElement("span");
664
+ Object.assign(label.style, {
665
+ position: "absolute",
666
+ left: "-2px",
667
+ padding: "1px 4px",
668
+ borderRadius: "2px",
669
+ font: "500 10px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace",
670
+ color: "#fff",
671
+ whiteSpace: "nowrap"
672
+ });
673
+ boxes.hover.appendChild(label);
674
+ // The pointer says it is picking, and a link under it is not a link.
675
+ var cursor = doc.createElement("style");
676
+ cursor.textContent = "[data-val-picking] * { cursor: crosshair !important; }";
677
+ doc.head.appendChild(cursor);
678
+ var place = function place(kind) {
679
+ var element = shown[kind];
680
+ var el = boxes[kind];
681
+ if (element === null || !element.isConnected) {
682
+ el.style.display = "none";
683
+ return;
684
+ }
685
+ var rect = element.getBoundingClientRect();
686
+ Object.assign(el.style, {
687
+ display: "block",
688
+ top: "".concat(rect.top - 2, "px"),
689
+ left: "".concat(rect.left - 2, "px"),
690
+ width: "".concat(rect.width + 4, "px"),
691
+ height: "".concat(rect.height + 4, "px"),
692
+ border: "2px ".concat(kind === "hover" ? "solid" : "dashed", " ").concat(color)
693
+ });
694
+ if (kind === "hover") {
695
+ label.textContent = elementLabel(element);
696
+ label.style.background = color;
697
+ // Above the box, unless that is off the top of the window.
698
+ if (rect.top >= 18) {
699
+ label.style.top = "";
700
+ label.style.bottom = "100%";
701
+ } else {
702
+ label.style.bottom = "";
703
+ label.style.top = "100%";
704
+ }
705
+ }
706
+ };
707
+ var frame = null;
708
+ var win = doc.defaultView;
709
+ var reposition = function reposition() {
710
+ if (frame !== null || win === null) return;
711
+ frame = win.requestAnimationFrame(function () {
712
+ frame = null;
713
+ place("hover");
714
+ place("picked");
715
+ });
716
+ };
717
+ win === null || win === void 0 || win.addEventListener("scroll", reposition, true);
718
+ win === null || win === void 0 || win.addEventListener("resize", reposition);
719
+ return {
720
+ show: function show(kind, element) {
721
+ shown[kind] = element;
722
+ if (kind === "hover") {
723
+ if (element !== null) {
724
+ doc.documentElement.setAttribute("data-val-picking", "");
725
+ } else {
726
+ doc.documentElement.removeAttribute("data-val-picking");
727
+ }
728
+ }
729
+ place(kind);
730
+ },
731
+ setColor: function setColor(next) {
732
+ color = next;
733
+ place("hover");
734
+ place("picked");
735
+ },
736
+ destroy: function destroy() {
737
+ if (frame !== null) win === null || win === void 0 || win.cancelAnimationFrame(frame);
738
+ win === null || win === void 0 || win.removeEventListener("scroll", reposition, true);
739
+ win === null || win === void 0 || win.removeEventListener("resize", reposition);
740
+ host.remove();
741
+ cursor.remove();
742
+ doc.documentElement.removeAttribute("data-val-picking");
743
+ }
744
+ };
745
+ }
746
+
747
+ /** `h2.mb-6.text-2xl`: what the hover box is named. */
748
+ function elementLabel(element) {
749
+ var classes = Array.from(element.classList).slice(0, 2);
750
+ return [element.tagName.toLowerCase()].concat(_toConsumableArray(classes)).join(".");
751
+ }
752
+ /**
753
+ * Picking on a page, whole: the listeners, the outlines, and Esc to stop
754
+ * picking without the modifier. What a canvas bridge mounts.
755
+ */
756
+ function startElementPicking(root, options) {
757
+ var _doc$defaultView;
758
+ var doc = root.ownerDocument;
759
+ var overlay = createPickerOverlay(doc, options.color);
760
+ var detach = attachElementPicker(root, {
761
+ modifier: options.modifier,
762
+ active: options.active,
763
+ onHover: function onHover(element) {
764
+ return overlay.show("hover", element);
765
+ },
766
+ onPick: function onPick(element) {
767
+ overlay.show("hover", null);
768
+ overlay.show("picked", element);
769
+ options.onPick(describeElement(element, root));
770
+ }
771
+ });
772
+ var onKey = function onKey(event) {
773
+ if (event.key === "Escape" && options.active()) options.onStop();
774
+ };
775
+ (_doc$defaultView = doc.defaultView) === null || _doc$defaultView === void 0 || _doc$defaultView.addEventListener("keydown", onKey);
776
+ return {
777
+ clearPicked: function clearPicked() {
778
+ return overlay.show("picked", null);
779
+ },
780
+ setColor: overlay.setColor,
781
+ stop: function stop() {
782
+ var _doc$defaultView2;
783
+ (_doc$defaultView2 = doc.defaultView) === null || _doc$defaultView2 === void 0 || _doc$defaultView2.removeEventListener("keydown", onKey);
784
+ detach();
785
+ overlay.destroy();
786
+ }
787
+ };
788
+ }
789
+
790
+ /** Where the page is: what a new build is opened at. */
791
+
792
+ /** A box on the page, in CSS pixels from the top of the document. */
793
+
794
+ /** One thing a reader would notice on the page, in document order. */
795
+
796
+ /** One element in detail: what `read_page` answers for a `selector`. */
797
+
798
+ var TEXT_LENGTH = 160;
799
+ var DEFAULT_MAX_ITEMS = 300;
800
+ var MAX_CHILDREN = 40;
801
+
802
+ /** The computed styles worth reading back: type, colour, spacing, layout. */
803
+ var STYLES = ["display", "position", "font-family", "font-size", "font-weight", "line-height", "letter-spacing", "text-align", "text-transform", "color", "background-color", "background-image", "margin", "padding", "gap", "border", "border-radius", "box-shadow", "width", "height", "max-width", "flex-direction", "justify-content", "align-items", "grid-template-columns", "opacity", "overflow"];
804
+
805
+ /** Where `win` is now. */
806
+ function pageLocationOf(win) {
807
+ var url = new URL(win.location.href);
808
+ // The canvas's own parameter is not part of where the page is.
809
+ url.searchParams["delete"](VAL_CANVAS_PARAM);
810
+ // Nor is which quick preview it shows: the next build is not that one.
811
+ url.searchParams["delete"](QUICK_PREVIEW_PARAM);
812
+ return {
813
+ path: "".concat(url.pathname).concat(url.search).concat(url.hash),
814
+ scrollX: Math.round(win.scrollX),
815
+ scrollY: Math.round(win.scrollY)
816
+ };
817
+ }
818
+
819
+ /**
820
+ * Report where the page is as it moves: on scroll, a little after it settles,
821
+ * and when it navigates -- which a client-side router does without an event,
822
+ * so the address is also looked at now and then. Returns the way to stop.
823
+ */
824
+ function watchPageLocation(win, onChange, options) {
825
+ var _options$settleMs, _options$pollMs;
826
+ var settleMs = (_options$settleMs = options === null || options === void 0 ? void 0 : options.settleMs) !== null && _options$settleMs !== void 0 ? _options$settleMs : 200;
827
+ var pollMs = (_options$pollMs = options === null || options === void 0 ? void 0 : options.pollMs) !== null && _options$pollMs !== void 0 ? _options$pollMs : 500;
828
+ var last = "";
829
+ var report = function report() {
830
+ var location = pageLocationOf(win);
831
+ var key = JSON.stringify(location);
832
+ if (key === last) return;
833
+ last = key;
834
+ onChange(location);
835
+ };
836
+ var settle = null;
837
+ var onScroll = function onScroll() {
838
+ if (settle !== null) clearTimeout(settle);
839
+ settle = setTimeout(function () {
840
+ settle = null;
841
+ report();
842
+ }, settleMs);
843
+ };
844
+ win.addEventListener("scroll", onScroll, {
845
+ passive: true
846
+ });
847
+ win.addEventListener("popstate", report);
848
+ var poll = setInterval(report, pollMs);
849
+ report();
850
+ return function () {
851
+ win.removeEventListener("scroll", onScroll);
852
+ win.removeEventListener("popstate", report);
853
+ clearInterval(poll);
854
+ if (settle !== null) clearTimeout(settle);
855
+ };
856
+ }
857
+
858
+ /**
859
+ * Scroll to where the last build was, once the page is tall enough to: a page
860
+ * that is still loading its content is shorter than it will be, so this tries
861
+ * again each frame for a while rather than stopping at the bottom of half a
862
+ * page. Returns the way to stop trying.
863
+ */
864
+ function restoreScroll(win, x, y, options) {
865
+ var _options$withinMs;
866
+ var until = Date.now() + ((_options$withinMs = options === null || options === void 0 ? void 0 : options.withinMs) !== null && _options$withinMs !== void 0 ? _options$withinMs : 2000);
867
+ var frame = null;
868
+ var _attempt = function attempt() {
869
+ frame = null;
870
+ win.scrollTo(x, y);
871
+ var reached = Math.abs(win.scrollY - y) < 2 && Math.abs(win.scrollX - x) < 2;
872
+ if (!reached && Date.now() < until) {
873
+ frame = win.requestAnimationFrame(_attempt);
874
+ }
875
+ };
876
+ _attempt();
877
+ return function () {
878
+ if (frame !== null) win.cancelAnimationFrame(frame);
879
+ };
880
+ }
881
+
882
+ /**
883
+ * What is on the page, as a reader would take it in: headings, text, links,
884
+ * buttons, images and inputs, in order, each with where it is. With a
885
+ * `selector`, that element in detail -- its computed styles, how many lines
886
+ * its text wraps to, and its children.
887
+ *
888
+ * `root` is what selectors are written from: the body, as `describeElement`
889
+ * writes them for a picked element.
890
+ */
891
+ function readPageSnapshot(doc, options) {
892
+ var _options$maxItems, _win$scrollY, _win$location$href, _win$innerWidth, _win$innerHeight;
893
+ var win = doc.defaultView;
894
+ var root = doc.body;
895
+ var maxItems = (_options$maxItems = options === null || options === void 0 ? void 0 : options.maxItems) !== null && _options$maxItems !== void 0 ? _options$maxItems : DEFAULT_MAX_ITEMS;
896
+ var outline = [];
897
+ var truncated = false;
898
+ var scope = (options === null || options === void 0 ? void 0 : options.selector) !== undefined ? findBySelector(root, options.selector) : null;
899
+ var walkRoot = scope !== null && scope !== void 0 ? scope : root;
900
+ // A hidden element is skipped with all that is in it.
901
+ var walker = doc.createTreeWalker(walkRoot, NodeFilter.SHOW_ELEMENT, {
902
+ acceptNode: function acceptNode(candidate) {
903
+ return candidate instanceof Element && isHidden(candidate, win) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
904
+ }
905
+ });
906
+ var node = walker.currentNode;
907
+ while (node !== null) {
908
+ if (node instanceof Element) {
909
+ var item = outlineItemOf(node, root, win);
910
+ if (item !== null) {
911
+ if (outline.length >= maxItems) {
912
+ truncated = true;
913
+ break;
914
+ }
915
+ outline.push(item);
916
+ }
917
+ }
918
+ node = walker.nextNode();
919
+ }
920
+ var scrollY = Math.round((_win$scrollY = win === null || win === void 0 ? void 0 : win.scrollY) !== null && _win$scrollY !== void 0 ? _win$scrollY : 0);
921
+ return _objectSpread2(_objectSpread2(_objectSpread2({
922
+ url: (_win$location$href = win === null || win === void 0 ? void 0 : win.location.href) !== null && _win$location$href !== void 0 ? _win$location$href : "",
923
+ title: doc.title,
924
+ viewport: {
925
+ width: (_win$innerWidth = win === null || win === void 0 ? void 0 : win.innerWidth) !== null && _win$innerWidth !== void 0 ? _win$innerWidth : 0,
926
+ height: (_win$innerHeight = win === null || win === void 0 ? void 0 : win.innerHeight) !== null && _win$innerHeight !== void 0 ? _win$innerHeight : 0,
927
+ scrollY: scrollY,
928
+ pageHeight: Math.round(doc.documentElement.scrollHeight)
929
+ },
930
+ outline: outline
931
+ }, truncated ? {
932
+ truncated: true
933
+ } : {}), scope !== null ? {
934
+ element: detailOf(scope, root, win)
935
+ } : {}), (options === null || options === void 0 ? void 0 : options.selector) !== undefined && scope === null ? {
936
+ notFound: options.selector
937
+ } : {});
938
+ }
939
+
940
+ /**
941
+ * The element a selector names, written as `describeElement` writes them --
942
+ * from the body down, or from an element with an id -- or any selector a
943
+ * person would write.
944
+ */
945
+ function findBySelector(root, selector) {
946
+ try {
947
+ var _root$querySelector;
948
+ return (_root$querySelector = root.querySelector(":scope > ".concat(selector))) !== null && _root$querySelector !== void 0 ? _root$querySelector : root.ownerDocument.querySelector(selector);
949
+ } catch (_unused) {
950
+ return null;
951
+ }
952
+ }
953
+ function outlineItemOf(element, root, win) {
954
+ var tag = element.tagName.toLowerCase();
955
+ var base = function base() {
956
+ return {
957
+ tag: tag,
958
+ selector: selectorOf(element, root),
959
+ box: boxOf(element, win)
960
+ };
961
+ };
962
+ var heading = /^h([1-6])$/.exec(tag);
963
+ if (heading !== null) {
964
+ return _objectSpread2({
965
+ kind: "heading",
966
+ level: Number(heading[1]),
967
+ text: textOf(element)
968
+ }, base());
969
+ }
970
+ if (tag === "a") {
971
+ var _element$getAttribute;
972
+ var href = (_element$getAttribute = element.getAttribute("href")) !== null && _element$getAttribute !== void 0 ? _element$getAttribute : undefined;
973
+ return _objectSpread2(_objectSpread2(_objectSpread2({
974
+ kind: "link"
975
+ }, textOf(element) !== "" ? {
976
+ text: textOf(element)
977
+ } : {}), href !== undefined ? {
978
+ href: href
979
+ } : {}), base());
980
+ }
981
+ if (tag === "button" || element.getAttribute("role") === "button") {
982
+ return _objectSpread2({
983
+ kind: "button",
984
+ text: textOf(element)
985
+ }, base());
986
+ }
987
+ if (tag === "img") {
988
+ var alt = element.getAttribute("alt");
989
+ var src = element.getAttribute("src");
990
+ return _objectSpread2(_objectSpread2(_objectSpread2({
991
+ kind: "image"
992
+ }, alt !== null ? {
993
+ alt: alt
994
+ } : {}), src !== null ? {
995
+ src: shorten(src)
996
+ } : {}), base());
997
+ }
998
+ if (tag === "input" || tag === "textarea" || tag === "select") {
999
+ var _ref, _ref2, _element$getAttribute2;
1000
+ var label = (_ref = (_ref2 = (_element$getAttribute2 = element.getAttribute("aria-label")) !== null && _element$getAttribute2 !== void 0 ? _element$getAttribute2 : element.getAttribute("placeholder")) !== null && _ref2 !== void 0 ? _ref2 : element.getAttribute("name")) !== null && _ref !== void 0 ? _ref : "";
1001
+ return _objectSpread2(_objectSpread2({
1002
+ kind: "input"
1003
+ }, label !== "" ? {
1004
+ text: label
1005
+ } : {}), base());
1006
+ }
1007
+ if (tag === "header" || tag === "nav" || tag === "main" || tag === "section" || tag === "footer" || tag === "aside") {
1008
+ return _objectSpread2({
1009
+ kind: "section"
1010
+ }, base());
1011
+ }
1012
+ // Text: an element that holds words of its own, not only through children
1013
+ // already listed (a heading, a link) -- a paragraph, a list item, a label.
1014
+ if (tag === "p" || tag === "li" || tag === "blockquote" || tag === "figcaption" || tag === "label" || tag === "td" || tag === "th" || hasOwnText(element)) {
1015
+ var text = textOf(element);
1016
+ if (text === "" || insideListed(element)) return null;
1017
+ return _objectSpread2({
1018
+ kind: "text",
1019
+ text: text
1020
+ }, base());
1021
+ }
1022
+ return null;
1023
+ }
1024
+
1025
+ /**
1026
+ * Already part of something the outline names: a link's or a button's words,
1027
+ * or a `<strong>` in a paragraph that is listed whole.
1028
+ */
1029
+ function insideListed(element) {
1030
+ var parent = element.parentElement;
1031
+ return parent !== null && parent.closest("a, button, h1, h2, h3, h4, h5, h6, [role='button'], p, li, blockquote, figcaption, label, td, th") !== null;
1032
+ }
1033
+ function hasOwnText(element) {
1034
+ for (var _i = 0, _Array$from = Array.from(element.childNodes); _i < _Array$from.length; _i++) {
1035
+ var _child$textContent;
1036
+ var child = _Array$from[_i];
1037
+ if (child.nodeType === 3 && ((_child$textContent = child.textContent) !== null && _child$textContent !== void 0 ? _child$textContent : "").trim() !== "") {
1038
+ return true;
1039
+ }
1040
+ }
1041
+ return false;
1042
+ }
1043
+ function detailOf(element, root, win) {
1044
+ var _win$getComputedStyle;
1045
+ var styles = {};
1046
+ var computed = (_win$getComputedStyle = win === null || win === void 0 ? void 0 : win.getComputedStyle(element)) !== null && _win$getComputedStyle !== void 0 ? _win$getComputedStyle : null;
1047
+ if (computed !== null) {
1048
+ var _iterator = _createForOfIteratorHelper(STYLES),
1049
+ _step;
1050
+ try {
1051
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
1052
+ var property = _step.value;
1053
+ var value = computed.getPropertyValue(property);
1054
+ if (value !== "" && value !== "none" && value !== "normal") {
1055
+ styles[property] = value;
1056
+ }
1057
+ }
1058
+ } catch (err) {
1059
+ _iterator.e(err);
1060
+ } finally {
1061
+ _iterator.f();
1062
+ }
1063
+ }
1064
+ var box = boxOf(element, win);
1065
+ var lineHeight = computed !== null ? parseFloat(computed.getPropertyValue("line-height")) : NaN;
1066
+ var lines = Number.isFinite(lineHeight) && lineHeight > 0 && box.height > 0 ? Math.max(1, Math.round(box.height / lineHeight)) : undefined;
1067
+ return _objectSpread2(_objectSpread2({
1068
+ selector: selectorOf(element, root),
1069
+ tag: element.tagName.toLowerCase(),
1070
+ text: textOf(element),
1071
+ box: box
1072
+ }, lines !== undefined ? {
1073
+ lines: lines
1074
+ } : {}), {}, {
1075
+ styles: styles,
1076
+ children: Array.from(element.children).slice(0, MAX_CHILDREN).map(function (child) {
1077
+ var text = textOf(child);
1078
+ return _objectSpread2(_objectSpread2({
1079
+ tag: child.tagName.toLowerCase(),
1080
+ selector: selectorOf(child, root)
1081
+ }, text !== "" ? {
1082
+ text: text
1083
+ } : {}), {}, {
1084
+ box: boxOf(child, win)
1085
+ });
1086
+ })
1087
+ });
1088
+ }
1089
+ function isHidden(element, win) {
1090
+ var tag = element.tagName.toLowerCase();
1091
+ if (tag === "script" || tag === "style" || tag === "noscript" || tag === "template") {
1092
+ return true;
1093
+ }
1094
+ // The picker's own outlines, and what the page hides from everyone.
1095
+ if (element.hasAttribute("data-val-picker") || element.hasAttribute("hidden") || element.getAttribute("aria-hidden") === "true") {
1096
+ return true;
1097
+ }
1098
+ var style = win === null || win === void 0 ? void 0 : win.getComputedStyle(element);
1099
+ return (style === null || style === void 0 ? void 0 : style.display) === "none" || (style === null || style === void 0 ? void 0 : style.visibility) === "hidden";
1100
+ }
1101
+ function boxOf(element, win) {
1102
+ var _win$scrollX, _win$scrollY2;
1103
+ var rect = element.getBoundingClientRect();
1104
+ return {
1105
+ x: Math.round(rect.left + ((_win$scrollX = win === null || win === void 0 ? void 0 : win.scrollX) !== null && _win$scrollX !== void 0 ? _win$scrollX : 0)),
1106
+ y: Math.round(rect.top + ((_win$scrollY2 = win === null || win === void 0 ? void 0 : win.scrollY) !== null && _win$scrollY2 !== void 0 ? _win$scrollY2 : 0)),
1107
+ width: Math.round(rect.width),
1108
+ height: Math.round(rect.height)
1109
+ };
1110
+ }
1111
+ function textOf(element) {
1112
+ var _element$textContent;
1113
+ return shorten(((_element$textContent = element.textContent) !== null && _element$textContent !== void 0 ? _element$textContent : "").replace(/\s+/g, " ").trim());
1114
+ }
1115
+ function shorten(text) {
1116
+ return text.length > TEXT_LENGTH ? "".concat(text.slice(0, TEXT_LENGTH - 1), "\u2026") : text;
1117
+ }
1118
+
1119
+ /** As `describeElement` writes it: from `root` down, or from an id. */
1120
+ function selectorOf(element, root) {
1121
+ var parts = [];
1122
+ var at = element;
1123
+ while (at !== null && at !== root) {
1124
+ var tag = at.tagName.toLowerCase();
1125
+ if (at.id !== "") {
1126
+ parts.unshift("".concat(tag, "#").concat(at.id));
1127
+ break;
1128
+ }
1129
+ var parent = at.parentElement;
1130
+ var same = parent === null ? [] : Array.from(parent.children).filter(function (sibling) {
1131
+ var _at;
1132
+ return sibling.tagName === ((_at = at) === null || _at === void 0 ? void 0 : _at.tagName);
1133
+ });
1134
+ parts.unshift(same.length > 1 ? "".concat(tag, ":nth-of-type(").concat(same.indexOf(at) + 1, ")") : tag);
1135
+ at = parent;
1136
+ }
1137
+ return parts.join(" > ");
1138
+ }
1139
+
1140
+ function parseRoutePattern(pattern) {
1141
+ if (pattern === "" || pattern === "/") {
1142
+ return [];
1143
+ }
1144
+ var p = pattern.startsWith("/") ? pattern.slice(1) : pattern;
1145
+ return p.split("/").map(function (part) {
1146
+ var isOptionalParam = part.startsWith("[[") && part.endsWith("]]");
1147
+ var isNonOptionalParam = part.startsWith("[") && part.endsWith("]");
1148
+ var nameStart = isOptionalParam ? 2 : isNonOptionalParam ? 1 : 0;
1149
+ var nameEnd = part.indexOf("]", nameStart);
1150
+ var isArrayParam = part.slice(nameStart, nameStart + 3) === "...";
1151
+ var paramName = part.slice(nameStart + (isArrayParam ? 3 : 0), nameEnd);
1152
+ if (isOptionalParam || isNonOptionalParam) {
1153
+ if (isArrayParam) {
1154
+ return {
1155
+ type: "array-param",
1156
+ paramName: paramName,
1157
+ optional: isOptionalParam
1158
+ };
1159
+ } else {
1160
+ return {
1161
+ type: "string-param",
1162
+ paramName: paramName,
1163
+ optional: isOptionalParam
1164
+ };
1165
+ }
1166
+ }
1167
+ return {
1168
+ type: "literal",
1169
+ name: part
1170
+ };
1171
+ });
1172
+ }
1173
+
1174
+ /**
1175
+ * A parsed pattern written back as segments: the inverse of
1176
+ * {@link parseRoutePattern}, in the vocabulary core's `matchRoutePattern`
1177
+ * reads.
1178
+ */
1179
+ function formatRoutePattern(pattern) {
1180
+ return pattern.map(function (part) {
1181
+ if (part.type === "literal") {
1182
+ return part.name;
1183
+ }
1184
+ var inner = part.type === "string-param" ? part.paramName : "...".concat(part.paramName);
1185
+ return part.optional ? "[[".concat(inner, "]]") : "[".concat(inner, "]");
1186
+ });
1187
+ }
1188
+
1189
+ function getNextAppRouterSourceFolder(moduleFilePath) {
1190
+ if (moduleFilePath.startsWith("/app")) {
1191
+ return "/app";
1192
+ } else if (moduleFilePath.startsWith("/src/app")) {
1193
+ return "/src/app";
1194
+ }
1195
+ return null;
1196
+ }
1197
+
1198
+ /**
1199
+ * The folder a TanStack Router app keeps its route files in.
1200
+ *
1201
+ * The same question as above for the other convention: `routes/` at the project
1202
+ * root, or under `src/`. A Val module for a route lives beside the route file
1203
+ * and is named the same way, so this is the prefix to strip before reading the
1204
+ * route pattern out of the rest.
1205
+ */
1206
+ function getTanStackRouterSourceFolder(moduleFilePath) {
1207
+ if (moduleFilePath.startsWith("/routes/")) {
1208
+ return "/routes";
1209
+ } else if (moduleFilePath.startsWith("/src/routes/")) {
1210
+ return "/src/routes";
1211
+ }
1212
+ return null;
1213
+ }
1214
+
1215
+ /**
1216
+ * The route folder of whichever page router this module belongs to.
1217
+ *
1218
+ * The two conventions have disjoint prefixes — `app/` versus `routes/` — so a
1219
+ * module file path answers this on its own, which is what the Studio needs: it
1220
+ * walks every schema in the project and has no framework to ask.
1221
+ */
1222
+ function getPageRouterSourceFolder(moduleFilePath) {
1223
+ var _getNextAppRouterSour;
1224
+ return (_getNextAppRouterSour = getNextAppRouterSourceFolder(moduleFilePath)) !== null && _getNextAppRouterSour !== void 0 ? _getNextAppRouterSour : getTanStackRouterSourceFolder(moduleFilePath);
1225
+ }
1226
+
1227
+ /**
1228
+ * Whether a route folder is a TanStack Router one.
1229
+ *
1230
+ * Exported so the pattern derivation and the source-folder detection cannot
1231
+ * drift apart about what `/src/routes` means.
1232
+ */
1233
+ function isTanStackRoutesFolder(srcFolder) {
1234
+ return srcFolder === "/routes" || srcFolder === "/src/routes";
1235
+ }
1236
+
1237
+ // Strictly speaking this should be in the framework packages but it's shared,
1238
+ // because we want to use it in the ui package. We want to resolve that somehow.
1239
+ //
1240
+ // It is one function for both page routers because the tree it builds is made
1241
+ // of URL paths, which is the same thing in either: only `srcFolder` and the
1242
+ // pattern derivation differ, and both of those are arguments (or the one call
1243
+ // below).
1244
+ function getPageRouterSitemapTree(srcFolder, paths) {
1245
+ var root = {
1246
+ type: "node",
1247
+ name: "/",
1248
+ children: []
1249
+ };
1250
+ var _iterator = _createForOfIteratorHelper(paths),
1251
+ _step;
1252
+ try {
1253
+ var _loop = function _loop() {
1254
+ var path = _step.value;
1255
+ var urlPath = path.urlPath,
1256
+ moduleFilePathS = path.moduleFilePath;
1257
+ var moduleFilePath = moduleFilePathS;
1258
+ if (!urlPath.startsWith("/")) {
1259
+ Internal.log.error("urlPath must start with /: ".concat(urlPath));
1260
+ return 0; // continue
1261
+ }
1262
+ var pattern = getPatternFromModuleFilePath(moduleFilePath, srcFolder);
1263
+ if (urlPath === "/") {
1264
+ var fullPath = "/";
1265
+ root.pattern = getPatternFromModuleFilePath(moduleFilePath, srcFolder);
1266
+ root.page = {
1267
+ fullPath: fullPath
1268
+ };
1269
+ root.sourcePath = Internal.joinModuleFilePathAndModulePath(moduleFilePath, "\"".concat(fullPath, "\""));
1270
+ root.moduleFilePath = moduleFilePath;
1271
+ return 0; // continue
1272
+ }
1273
+ var pathParts = urlPath.split("/").slice(1).filter(Boolean);
1274
+ var currentNode = root;
1275
+ pathParts.forEach(function (part, index) {
1276
+ var hasChildren = index < pathParts.length - 1;
1277
+ var isLast = index === pathParts.length - 1;
1278
+ var fullPath = "/" + pathParts.slice(0, index + 1).join("/");
1279
+ var sourcePath = Internal.joinModuleFilePathAndModulePath(moduleFilePath, "\"".concat(fullPath, "\""));
1280
+ var node = hasChildren ? _objectSpread2(_objectSpread2({
1281
+ type: "node",
1282
+ name: part,
1283
+ pattern: pattern,
1284
+ moduleFilePath: moduleFilePath
1285
+ }, isLast ? {
1286
+ page: {
1287
+ fullPath: fullPath
1288
+ },
1289
+ sourcePath: sourcePath
1290
+ } : {}), {}, {
1291
+ children: []
1292
+ }) : {
1293
+ type: "leaf",
1294
+ name: part,
1295
+ pattern: pattern,
1296
+ fullPath: fullPath,
1297
+ moduleFilePath: moduleFilePath,
1298
+ sourcePath: sourcePath,
1299
+ children: []
1300
+ };
1301
+ var existingNodeIndex = currentNode.children.findIndex(function (node) {
1302
+ return node.name === part;
1303
+ });
1304
+ if (existingNodeIndex === -1) {
1305
+ currentNode.children.push(node);
1306
+ if (currentNode.type === "node") {
1307
+ currentNode.isLinear = true;
1308
+ if (currentNode.children.length > 1) {
1309
+ delete currentNode.isLinear;
1310
+ }
1311
+ }
1312
+ currentNode = node;
1313
+ } else {
1314
+ var existingNode = currentNode.children[existingNodeIndex];
1315
+ if (existingNode.type === "leaf" && hasChildren) {
1316
+ var _sourcePath = existingNode.sourcePath;
1317
+ // convert leaf to node
1318
+ currentNode.children[existingNodeIndex] = {
1319
+ type: "node",
1320
+ name: part,
1321
+ pattern: pattern,
1322
+ page: {
1323
+ fullPath: fullPath
1324
+ },
1325
+ sourcePath: _sourcePath,
1326
+ children: existingNode.children,
1327
+ moduleFilePath: moduleFilePath
1328
+ };
1329
+ currentNode = currentNode.children[existingNodeIndex];
1330
+ } else if (existingNode.type === "node" && isLast) {
1331
+ // add page, source path and module file path
1332
+ existingNode.page = {
1333
+ fullPath: fullPath
1334
+ };
1335
+ existingNode.sourcePath = sourcePath;
1336
+ existingNode.moduleFilePath = moduleFilePath;
1337
+ existingNode.pattern = pattern;
1338
+ } else {
1339
+ currentNode = currentNode.children[existingNodeIndex];
1340
+ }
1341
+ }
1342
+ });
1343
+ },
1344
+ _ret;
1345
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
1346
+ _ret = _loop();
1347
+ if (_ret === 0) continue;
1348
+ }
1349
+ } catch (err) {
1350
+ _iterator.e(err);
1351
+ } finally {
1352
+ _iterator.f();
1353
+ }
1354
+ return root;
1355
+ }
1356
+
1357
+ /**
1358
+ * The route pattern a page-router module serves, e.g. `/blogs/[blog]`.
1359
+ *
1360
+ * Written in Next's vocabulary for both routers on purpose: the pattern is read
1361
+ * by `parseRoutePattern`, the sitemap and the Studio's key inputs, none of which
1362
+ * should have to know which framework the project uses. The TanStack branch
1363
+ * translates that framework's conventions into it; see
1364
+ * `tanStackSegmentsOfRoutePath` in `@valbuild/core` for the rules.
1365
+ */
1366
+ function getPatternFromModuleFilePath(moduleFilePath, srcFolder) {
1367
+ if (isTanStackRoutesFolder(srcFolder)) {
1368
+ var routePath = moduleFilePath.slice(srcFolder.length).replace(/\.val\.[tj]sx?$/, "");
1369
+ var segments = tanStackSegmentsOfRoutePath(routePath);
1370
+ // "" rather than "/" for the root, so it matches what the Next branch
1371
+ // returns for `/app/page.val.ts` — `parseRoutePattern` reads both as [].
1372
+ return segments.length === 0 ? "" : "/".concat(segments.join("/"));
1373
+ }
1374
+ return moduleFilePath.replace(srcFolder, "")
1375
+ // TODO: hacky!
1376
+ // remove route groups
1377
+ .replace(/\/\([^/]+\)/g, "") // not sure we want to do this?
1378
+ .replace("/page.val.ts", "").replace("/page.val.js", "");
1379
+ }
1380
+
1381
+ export { quickPreviewSiteOrigin as A, isDesignTabOrigin as B, parseRoutePattern as C, DEFAULT_CANVAS_SELECTION as D, formatRoutePattern as E, getNextAppRouterSourceFolder as F, getTanStackRouterSourceFolder as G, getPageRouterSourceFolder as H, isTanStackRoutesFolder as I, getPageRouterSitemapTree as J, getPatternFromModuleFilePath as K, QUICK_PREVIEW_PARAM as Q, VAL_CONFIG_SESSION_STORAGE_KEY as V, 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, describeElement as h, isValCanvasPageMessage as i, attachElementPicker as j, createPickerOverlay as k, elementLabel as l, modifierLabel as m, watchPageLocation as n, readPageSnapshot as o, pageLocationOf as p, findBySelector as q, restoreScroll as r, startElementPicking as s, QUICK_PREVIEW_SERVICE_WORKER_PATH as t, QUICK_PREVIEW_STORE_MESSAGE as u, isQuickPreviewFiles as v, withValCanvasParam as w, QUICK_PREVIEW_SERVICE_WORKER_SOURCE as x, storeQuickPreview as y, withQuickPreviewParam as z };