@tileflow/dev 0.1.0-alpha.18 → 0.1.0-alpha.19

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 (46) hide show
  1. package/GENERATED_OUTPUT_LICENSE.md +21 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +9 -0
  4. package/README.md +397 -41
  5. package/THIRD_PARTY_NOTICES.md +65 -7
  6. package/TRADEMARKS.md +10 -0
  7. package/dist/artifacts.d.ts +152 -0
  8. package/dist/artifacts.js +62 -0
  9. package/dist/chunk-BALG4DSG.js +844 -0
  10. package/dist/chunk-BKHIZ7H2.js +287 -0
  11. package/dist/chunk-BL3FK4GO.js +170 -0
  12. package/dist/chunk-D4OE4ITL.js +13 -0
  13. package/dist/chunk-GWUMIHL6.js +4208 -0
  14. package/dist/chunk-KDFTE4Q5.js +156 -0
  15. package/dist/chunk-O7PE6FB2.js +1210 -0
  16. package/dist/chunk-OEABXOJH.js +1991 -0
  17. package/dist/chunk-VEB43KDS.js +670 -0
  18. package/dist/chunk-ZOYFZ3MT.js +801 -0
  19. package/dist/chunk-ZRI6PNFL.js +211 -0
  20. package/dist/config.d.ts +33 -0
  21. package/dist/config.js +27 -0
  22. package/dist/fonts.d.ts +47 -0
  23. package/dist/fonts.js +16 -0
  24. package/dist/icons.d.ts +136 -0
  25. package/dist/icons.js +17 -0
  26. package/dist/index.d.ts +12 -320
  27. package/dist/index.js +128 -2566
  28. package/dist/inspect.d.ts +147 -0
  29. package/dist/inspect.js +16 -0
  30. package/dist/preview-assets-6LYL7GP7.js +145 -0
  31. package/dist/preview.d.ts +69 -0
  32. package/dist/preview.js +18 -0
  33. package/dist/server.d.ts +77 -0
  34. package/dist/server.js +47 -0
  35. package/dist/validation.d.ts +57 -0
  36. package/dist/validation.js +22 -0
  37. package/package.json +61 -7
  38. package/assets/streets-poi/cafe.svg +0 -10
  39. package/assets/streets-poi/hospital.svg +0 -10
  40. package/assets/streets-poi/hotel.svg +0 -10
  41. package/assets/streets-poi/museum.svg +0 -10
  42. package/assets/streets-poi/restaurant.svg +0 -10
  43. package/assets/streets-poi/school.svg +0 -10
  44. package/assets/streets-poi/services.svg +0 -10
  45. package/assets/streets-poi/shopping.svg +0 -10
  46. package/assets/streets-poi/train.svg +0 -10
@@ -0,0 +1,1991 @@
1
+ import {
2
+ createTileflowBuildArtifacts,
3
+ tileflowArtifactSessionSchemaVersion
4
+ } from "./chunk-O7PE6FB2.js";
5
+ import {
6
+ normalizeTileflowBasePath
7
+ } from "./chunk-VEB43KDS.js";
8
+ import {
9
+ TileflowStyleValidationError
10
+ } from "./chunk-KDFTE4Q5.js";
11
+ import {
12
+ TileflowValidationError,
13
+ defaultTileflowApiUrl,
14
+ defaultTileflowConfigPath
15
+ } from "./chunk-BKHIZ7H2.js";
16
+
17
+ // src/server.ts
18
+ import {
19
+ getTileflowStyleFontFaces,
20
+ tileflowMapIdSchema,
21
+ tileflowThemeNameSchema
22
+ } from "@tileflow/core";
23
+
24
+ // src/comparison.ts
25
+ import { randomBytes } from "crypto";
26
+
27
+ // src/comparison-html.ts
28
+ function renderTileflowComparisonHtml(options) {
29
+ const documentOptions = serializeInlineJson(options);
30
+ const nonceAttribute = options.cspNonce ? ` nonce="${escapeHtml(options.cspNonce)}"` : "";
31
+ return `<!doctype html>
32
+ <html lang="en" data-tileflow-state="loading">
33
+ <head>
34
+ <meta charset="utf-8" />
35
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
36
+ <title>${escapeHtml(options.title)}</title>
37
+ <style${nonceAttribute}>
38
+ :root {
39
+ color-scheme: light;
40
+ --accent: #3157d5;
41
+ --border: #d8dbe2;
42
+ --ink: #202124;
43
+ --muted: #656b76;
44
+ --paper: #f6f7f9;
45
+ --split: 50%;
46
+ --overlay-alpha: .5;
47
+ }
48
+ * { box-sizing: border-box; }
49
+ html, body { height: 100%; margin: 0; }
50
+ body {
51
+ display: grid;
52
+ grid-template-rows: auto minmax(0, 1fr);
53
+ overflow: hidden;
54
+ background: var(--paper);
55
+ color: var(--ink);
56
+ font: 13px/1.4 ui-sans-serif, system-ui, sans-serif;
57
+ }
58
+ button, input, select { font: inherit; }
59
+ button {
60
+ border: 1px solid var(--border);
61
+ border-radius: 6px;
62
+ background: white;
63
+ color: var(--ink);
64
+ cursor: pointer;
65
+ padding: 5px 9px;
66
+ }
67
+ button:hover { border-color: #a8afbd; }
68
+ button[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }
69
+ button:focus-visible, input:focus-visible, select:focus-visible, .inspector:focus-visible {
70
+ outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent);
71
+ outline-offset: 2px;
72
+ }
73
+ .toolbar {
74
+ z-index: 10;
75
+ display: flex;
76
+ min-height: 48px;
77
+ align-items: center;
78
+ gap: 8px;
79
+ padding: 7px 10px;
80
+ border-bottom: 1px solid var(--border);
81
+ background: rgba(255, 255, 255, .97);
82
+ flex-wrap: wrap;
83
+ }
84
+ .toolbar-title { font-weight: 750; margin-right: 4px; }
85
+ .toolbar-group { display: inline-flex; min-width: 0; align-items: center; gap: 8px; flex-wrap: wrap; }
86
+ .toolbar-separator { width: 1px; height: 25px; flex: 0 0 auto; background: var(--border); }
87
+ .toolbar label { display: inline-flex; align-items: center; gap: 5px; color: var(--muted); }
88
+ .toolbar input[type="range"] { width: 96px; }
89
+ .toolbar input[type="text"] { width: 150px; padding: 5px 7px; border: 1px solid var(--border); border-radius: 6px; }
90
+ .workspace { position: relative; min-height: 0; display: grid; grid-template-columns: minmax(0, 1fr) auto; }
91
+ .stage { position: relative; min-width: 0; min-height: 0; overflow: hidden; background: #d8dadd; }
92
+ .pane { position: absolute; inset: 0; min-width: 0; min-height: 0; overflow: hidden; background: #d8dadd; }
93
+ .pane iframe { width: 100%; height: 100%; display: block; border: 0; background: #d8dadd; }
94
+ .pane-label {
95
+ position: absolute;
96
+ z-index: 3;
97
+ top: 10px;
98
+ left: 10px;
99
+ display: flex;
100
+ max-width: calc(100% - 20px);
101
+ align-items: center;
102
+ gap: 7px;
103
+ border: 1px solid rgba(0, 0, 0, .12);
104
+ border-radius: 999px;
105
+ background: rgba(255, 255, 255, .93);
106
+ box-shadow: 0 3px 14px rgba(0, 0, 0, .12);
107
+ padding: 5px 9px;
108
+ pointer-events: none;
109
+ }
110
+ .pane-label-text { overflow: hidden; text-overflow: ellipsis; }
111
+ .side-state-text { flex: 0 0 auto; color: var(--muted); font-size: 10px; text-transform: uppercase; letter-spacing: .03em; }
112
+ .side-state { width: 8px; height: 8px; flex: 0 0 auto; border-radius: 50%; background: #8d929b; }
113
+ .side-state[data-status="ready"] { background: #248a4a; }
114
+ .side-state[data-status="building"] { background: #cf8114; }
115
+ .side-state[data-status="invalid"], .side-state[data-status="error"] { background: #ba3838; }
116
+ .stage[data-mode="side-by-side"] { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 1px; }
117
+ .stage[data-mode="side-by-side"] .pane { position: relative; inset: auto; }
118
+ .stage[data-mode="side-by-side"] .pane-right { grid-column: 2; }
119
+ .stage:not([data-mode="side-by-side"]) .pane-right { z-index: 2; }
120
+ .stage[data-mode="split"] .pane-right { clip-path: inset(0 0 0 var(--split)); }
121
+ .stage[data-mode="overlay"] .pane-right { opacity: var(--overlay-alpha); }
122
+ .stage[data-mode="blink"] .pane-right { opacity: 0; }
123
+ .stage[data-mode="blink"][data-blink-side="right"] .pane-right { opacity: 1; }
124
+ .stage[data-mode="blink"][data-blink-side="right"] .pane-left { opacity: 0; }
125
+ .stage:not([data-mode="side-by-side"]) .pane-right { pointer-events: none; }
126
+ .stage[data-driver="right"]:not([data-mode="side-by-side"]) .pane-right { pointer-events: auto; }
127
+ .stage[data-driver="right"]:not([data-mode="side-by-side"]) .pane-left { pointer-events: none; }
128
+ .stage[data-driver="left"]:not([data-mode="side-by-side"]) .pane-left { pointer-events: auto; }
129
+ .split-line { display: none; position: absolute; z-index: 4; inset-block: 0; left: var(--split); width: 1px; background: rgba(255,255,255,.9); box-shadow: 0 0 0 1px rgba(0,0,0,.35); pointer-events: none; }
130
+ .stage[data-mode="split"] .split-line { display: block; }
131
+ .inspector {
132
+ width: min(390px, 42vw);
133
+ border-left: 1px solid var(--border);
134
+ background: white;
135
+ overflow: auto;
136
+ padding: 12px;
137
+ }
138
+ .inspector[hidden] { display: none; }
139
+ .inspector h2 { margin: 0 0 10px; font-size: 15px; }
140
+ .inspector-heading { position: sticky; z-index: 2; top: -12px; display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: -12px -12px 10px; padding: 12px; border-bottom: 1px solid var(--border); background: white; }
141
+ .inspector-heading h2 { margin: 0; }
142
+ .inspector h3 { margin: 16px 0 6px; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
143
+ .inspector pre { max-height: 240px; margin: 6px 0; overflow: auto; border-radius: 6px; background: var(--paper); padding: 8px; font: 11px/1.45 ui-monospace, SFMono-Regular, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
144
+ .inspector select { max-width: 100%; padding: 5px 7px; border: 1px solid var(--border); border-radius: 6px; }
145
+ .feature-picker { display: grid; gap: 4px; margin: 8px 0; color: var(--muted); }
146
+ .feature-picker select { width: 100%; color: var(--ink); }
147
+ .curve-meta { margin: 5px 0; color: var(--muted); font-size: 11px; }
148
+ .empty { color: var(--muted); }
149
+ .curve { width: 100%; height: 116px; display: block; border: 1px solid var(--border); border-radius: 6px; background: #fbfbfc; }
150
+ .color-curve { display: grid; grid-template-columns: repeat(97, 1fr); height: 42px; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
151
+ .sprite-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(92px, 1fr)); gap: 7px; margin-top: 8px; }
152
+ .sprite-card { min-width: 0; border: 1px solid var(--border); border-radius: 7px; padding: 7px; background: var(--paper); text-align: center; }
153
+ .sprite-card canvas { display: block; margin: auto; max-width: 64px; max-height: 64px; image-rendering: auto; }
154
+ .sprite-name { display: block; margin-top: 4px; overflow: hidden; text-overflow: ellipsis; font-size: 10px; }
155
+ .feedback { color: var(--muted); min-width: 64px; }
156
+ .visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
157
+ @media (max-width: 760px) {
158
+ .toolbar { align-content: center; gap: 6px; padding: 6px; }
159
+ .toolbar-title { flex: 1 0 calc(100% - 12px); }
160
+ .toolbar-separator { display: none; }
161
+ .toolbar-group { flex: 1 1 100%; gap: 5px; }
162
+ .toolbar-group button { padding-inline: 7px; }
163
+ .toolbar input[type="range"] { width: min(24vw, 86px); }
164
+ .toolbar input[type="text"] { width: min(34vw, 130px); }
165
+ .workspace { grid-template-columns: minmax(0, 1fr); }
166
+ .inspector { position: absolute; z-index: 20; right: 0; inset-block: 0; width: min(92vw, 390px); box-shadow: -8px 0 28px rgba(0,0,0,.18); }
167
+ }
168
+ @media (prefers-reduced-motion: reduce) {
169
+ *, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
170
+ }
171
+ </style>
172
+ </head>
173
+ <body>
174
+ <header class="toolbar" aria-label="Comparison controls">
175
+ <span class="toolbar-title" id="title"></span>
176
+ <div class="toolbar-group" role="group" aria-label="Comparison mode">
177
+ <button type="button" data-mode="side-by-side">Side by side</button>
178
+ <button type="button" data-mode="split">Split</button>
179
+ <button type="button" data-mode="overlay">Overlay</button>
180
+ <button type="button" data-mode="blink">Blink</button>
181
+ </div>
182
+ <span class="toolbar-separator"></span>
183
+ <div class="toolbar-group">
184
+ <label id="split-control">Split <input id="split" type="range" min="5" max="95" value="50" aria-label="Split position" /></label>
185
+ <label id="alpha-control">Opacity <input id="alpha" type="range" min="0" max="100" value="50" aria-label="Overlay opacity" /></label>
186
+ <button type="button" id="driver">Interact: left</button>
187
+ <button type="button" id="blink-pause">Pause blink</button>
188
+ <button type="button" id="inspect-toggle" aria-controls="inspector" aria-expanded="false" aria-pressed="false">Inspect</button>
189
+ </div>
190
+ <span class="toolbar-separator"></span>
191
+ <div class="toolbar-group">
192
+ <input id="scene-name" type="text" value="comparison-view" aria-label="Scene name" />
193
+ <label>DPR <select id="scene-dpr"><option value="1">1</option><option value="2">2</option></select></label>
194
+ <button type="button" id="copy-scene">Copy scene</button>
195
+ <button type="button" id="copy-command">Copy command</button>
196
+ </div>
197
+ <span class="feedback" id="feedback" role="status"></span>
198
+ </header>
199
+ <div class="workspace">
200
+ <main class="stage" id="stage" data-driver="left" data-tileflow-capture-id="tileflow-comparison" data-tileflow-state="loading">
201
+ <section class="pane pane-left" data-side="left">
202
+ <div class="pane-label"><span class="side-state" id="left-state" role="status" aria-live="polite"></span><span class="pane-label-text" id="left-label"></span><span class="side-state-text" id="left-status-text">Building</span></div>
203
+ <iframe id="left-frame" title="Left Tileflow preview" referrerpolicy="no-referrer"></iframe>
204
+ </section>
205
+ <section class="pane pane-right" data-side="right">
206
+ <div class="pane-label"><span class="side-state" id="right-state" role="status" aria-live="polite"></span><span class="pane-label-text" id="right-label"></span><span class="side-state-text" id="right-status-text">Building</span></div>
207
+ <iframe id="right-frame" title="Right Tileflow preview" referrerpolicy="no-referrer"></iframe>
208
+ </section>
209
+ <div class="split-line" aria-hidden="true"></div>
210
+ </main>
211
+ <aside class="inspector" id="inspector" tabindex="-1" aria-labelledby="inspector-title" hidden>
212
+ <div class="inspector-heading"><h2 id="inspector-title">Cartographic inspector</h2><button type="button" id="inspector-close" aria-label="Close inspector">Close</button></div>
213
+ <div id="inspection" class="empty">Enable Inspect, then click either map.</div>
214
+ <h3>Zoom curve</h3>
215
+ <select id="curve-property" disabled><option>Select a rendered layer first</option></select>
216
+ <div id="curve-output" class="empty">Paint and layout values appear here.</div>
217
+ <h3>Compiler sidecar</h3>
218
+ <pre id="sidecar-output">No layer selected.</pre>
219
+ <h3>Sprite atlas</h3>
220
+ <button type="button" id="load-sprites">Load active side sprites</button>
221
+ <div id="sprite-output" class="sprite-grid"></div>
222
+ </aside>
223
+ </div>
224
+ <script type="module"${nonceAttribute}>
225
+ const options = ${documentOptions};
226
+ const bridgeSchemaVersion = 1;
227
+ const bridgeSetCameraType = "tileflow:comparison-set-camera";
228
+ const bridgeSetInspectionType = "tileflow:comparison-set-inspection";
229
+ const bridgeCameraType = "tileflow:comparison-camera";
230
+ const bridgeInspectionType = "tileflow:comparison-inspection";
231
+ const bridgeReadyType = "tileflow:comparison-ready";
232
+ const bridgeRuntimeErrorType = "tileflow:comparison-runtime-error";
233
+ const stage = document.getElementById("stage");
234
+ const inspector = document.getElementById("inspector");
235
+ const inspectionOutput = document.getElementById("inspection");
236
+ const curveProperty = document.getElementById("curve-property");
237
+ const curveOutput = document.getElementById("curve-output");
238
+ const sidecarOutput = document.getElementById("sidecar-output");
239
+ const spriteOutput = document.getElementById("sprite-output");
240
+ const feedback = document.getElementById("feedback");
241
+ const splitInput = document.getElementById("split");
242
+ const alphaInput = document.getElementById("alpha");
243
+ const driverButton = document.getElementById("driver");
244
+ const blinkPauseButton = document.getElementById("blink-pause");
245
+ const inspectButton = document.getElementById("inspect-toggle");
246
+ const inspectorCloseButton = document.getElementById("inspector-close");
247
+ const sceneNameInput = document.getElementById("scene-name");
248
+ const sceneDprInput = document.getElementById("scene-dpr");
249
+ const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
250
+ const states = Object.fromEntries(["left", "right"].map((id) => {
251
+ const definition = options[id];
252
+ return [id, {
253
+ id,
254
+ definition,
255
+ frame: document.getElementById(id + "-frame"),
256
+ stateElement: document.getElementById(id + "-state"),
257
+ statusTextElement: document.getElementById(id + "-status-text"),
258
+ map: undefined,
259
+ camera: undefined,
260
+ bridgeReady: false,
261
+ artifactStatus: "building",
262
+ artifactGeneration: -1,
263
+ artifactLastGoodGeneration: undefined,
264
+ sidecar: undefined,
265
+ sidecarPromise: undefined,
266
+ sidecarAbort: undefined,
267
+ spriteAbort: undefined,
268
+ loadEpoch: 0,
269
+ lastCameraRequestId: undefined,
270
+ cleanupBridge: undefined,
271
+ runtimeErrors: [],
272
+ runtimeFailed: false
273
+ }];
274
+ }));
275
+ let canonicalCamera = cameraFromUrl();
276
+ let activeSideId = "left";
277
+ let inspectionEnabled = false;
278
+ let selectedInspection;
279
+ let requestSequence = 0;
280
+ let blinkTimer;
281
+ let blinkPaused = reducedMotion.matches;
282
+ let blinkSide = "left";
283
+ let spriteLoadSequence = 0;
284
+ let spriteOwnerId;
285
+
286
+ document.getElementById("title").textContent = options.title;
287
+ const initialUi = readUiFromUrl();
288
+ splitInput.value = String(initialUi.split);
289
+ alphaInput.value = String(initialUi.alpha);
290
+ applySliders();
291
+ setMode(initialUi.mode || options.initialMode);
292
+ updateBlinkMotionPreference();
293
+ for (const id of ["left", "right"]) {
294
+ const side = states[id];
295
+ document.getElementById(id + "-label").textContent = side.definition.label;
296
+ side.frame.src = side.definition.previewUrl;
297
+ side.frame.addEventListener("load", () => connectSide(side));
298
+ side.frame.addEventListener("focus", () => selectSide(side.id));
299
+ startStatus(side);
300
+ }
301
+
302
+ for (const button of document.querySelectorAll("[data-mode]")) {
303
+ button.addEventListener("click", () => setMode(button.dataset.mode));
304
+ }
305
+ splitInput.addEventListener("input", () => { applySliders(); writeUrlState(); });
306
+ alphaInput.addEventListener("input", () => { applySliders(); writeUrlState(); });
307
+ driverButton.addEventListener("click", () => {
308
+ selectSide(stage.dataset.driver === "left" ? "right" : "left");
309
+ });
310
+ blinkPauseButton.addEventListener("click", () => {
311
+ blinkPaused = !blinkPaused;
312
+ updateBlinkMotionPreference();
313
+ restartBlink();
314
+ });
315
+ inspectButton.addEventListener("click", () => setInspectionEnabled(!inspectionEnabled, true));
316
+ inspectorCloseButton.addEventListener("click", () => setInspectionEnabled(false, true));
317
+ document.getElementById("copy-scene").addEventListener("click", () => copyScene(false));
318
+ document.getElementById("copy-command").addEventListener("click", () => copyScene(true));
319
+ document.getElementById("load-sprites").addEventListener("click", () => loadSprites(states[activeSideId]));
320
+ curveProperty.addEventListener("change", renderCurve);
321
+ reducedMotion.addEventListener?.("change", () => {
322
+ blinkPaused = reducedMotion.matches;
323
+ updateBlinkMotionPreference();
324
+ restartBlink();
325
+ });
326
+ addEventListener("keydown", (event) => {
327
+ if (event.key === "Escape" && inspectionEnabled) setInspectionEnabled(false, true);
328
+ });
329
+ addEventListener("message", receiveBridgeMessage);
330
+ addEventListener("beforeunload", () => {
331
+ clearInterval(blinkTimer);
332
+ for (const side of Object.values(states)) {
333
+ side.eventSource?.close();
334
+ side.sidecarAbort?.abort();
335
+ side.spriteAbort?.abort();
336
+ side.cleanupBridge?.();
337
+ }
338
+ });
339
+
340
+ function selectSide(id, writeState = true) {
341
+ if (id !== "left" && id !== "right") return;
342
+ const changed = activeSideId !== id || stage.dataset.driver !== id;
343
+ const previousSpriteOwner = spriteOwnerId;
344
+ activeSideId = id;
345
+ stage.dataset.driver = id;
346
+ driverButton.textContent = "Interact: " + id;
347
+ driverButton.setAttribute("aria-label", "Interact with " + states[id].definition.label);
348
+ if (changed && previousSpriteOwner && previousSpriteOwner !== id) {
349
+ states[previousSpriteOwner].spriteAbort?.abort();
350
+ spriteLoadSequence += 1;
351
+ spriteOwnerId = undefined;
352
+ spriteOutput.replaceChildren();
353
+ }
354
+ if (writeState) writeUrlState();
355
+ if (!canonicalCamera) maybeEstablishInitialCamera();
356
+ }
357
+
358
+ function setInspectionEnabled(enabled, manageFocus = false) {
359
+ inspectionEnabled = enabled;
360
+ inspectButton.setAttribute("aria-pressed", String(enabled));
361
+ inspectButton.setAttribute("aria-expanded", String(enabled));
362
+ inspector.hidden = !enabled;
363
+ for (const side of Object.values(states)) sendInspectionState(side);
364
+ if (manageFocus) {
365
+ if (enabled) inspector.focus();
366
+ else inspectButton.focus();
367
+ }
368
+ }
369
+
370
+ function updateBlinkMotionPreference() {
371
+ const motionBlocked = reducedMotion.matches;
372
+ blinkPauseButton.disabled = motionBlocked;
373
+ blinkPauseButton.textContent = motionBlocked
374
+ ? "Blink paused (reduced motion)"
375
+ : blinkPaused ? "Resume blink" : "Pause blink";
376
+ }
377
+
378
+ function setMode(mode) {
379
+ if (!["side-by-side", "split", "overlay", "blink"].includes(mode)) return;
380
+ stage.dataset.mode = mode;
381
+ for (const button of document.querySelectorAll("[data-mode]")) {
382
+ button.setAttribute("aria-pressed", String(button.dataset.mode === mode));
383
+ }
384
+ document.getElementById("split-control").hidden = mode !== "split";
385
+ document.getElementById("alpha-control").hidden = mode !== "overlay";
386
+ blinkPauseButton.hidden = mode !== "blink";
387
+ restartBlink();
388
+ writeUrlState();
389
+ setTimeout(() => {
390
+ for (const side of Object.values(states)) side.map?.resize?.();
391
+ }, 0);
392
+ }
393
+
394
+ function applySliders() {
395
+ const split = boundedNumber(Number(splitInput.value), 5, 95, 50);
396
+ const alpha = boundedNumber(Number(alphaInput.value), 0, 100, 50);
397
+ stage.style.setProperty("--split", split + "%");
398
+ stage.style.setProperty("--overlay-alpha", String(alpha / 100));
399
+ }
400
+
401
+ function restartBlink() {
402
+ clearInterval(blinkTimer);
403
+ blinkTimer = undefined;
404
+ blinkSide = "left";
405
+ stage.dataset.blinkSide = blinkSide;
406
+ if (stage.dataset.mode !== "blink" || blinkPaused) return;
407
+ blinkTimer = setInterval(() => {
408
+ blinkSide = blinkSide === "left" ? "right" : "left";
409
+ stage.dataset.blinkSide = blinkSide;
410
+ }, 700);
411
+ }
412
+
413
+ function connectSide(side) {
414
+ side.loadEpoch += 1;
415
+ const epoch = side.loadEpoch;
416
+ side.cleanupBridge?.();
417
+ side.cleanupBridge = undefined;
418
+ side.sidecarAbort?.abort();
419
+ side.spriteAbort?.abort();
420
+ side.sidecarAbort = undefined;
421
+ side.spriteAbort = undefined;
422
+ side.map = undefined;
423
+ side.bridgeReady = false;
424
+ side.camera = undefined;
425
+ side.sidecar = undefined;
426
+ side.sidecarPromise = undefined;
427
+ side.runtimeErrors = [];
428
+ side.runtimeFailed = false;
429
+ side.lastCameraRequestId = undefined;
430
+ if (spriteOwnerId === side.id) {
431
+ spriteLoadSequence += 1;
432
+ spriteOwnerId = undefined;
433
+ spriteOutput.replaceChildren();
434
+ spriteOutput.textContent = "The selected map reloaded; load its sprite atlas again.";
435
+ }
436
+ if (selectedInspection?.side === side) {
437
+ selectedInspection = undefined;
438
+ inspectionOutput.className = "empty";
439
+ inspectionOutput.textContent = "The selected map reloaded; click a rendered feature again.";
440
+ curveProperty.disabled = true;
441
+ curveProperty.replaceChildren(new Option("Select a rendered layer first"));
442
+ curveOutput.className = "empty";
443
+ curveOutput.textContent = "Paint and layout values appear here.";
444
+ sidecarOutput.textContent = "No layer selected.";
445
+ }
446
+ renderSideStatus(side);
447
+ updateDocumentReadiness();
448
+ const started = performance.now();
449
+ const findMap = () => {
450
+ if (side.loadEpoch !== epoch) return;
451
+ let childWindow;
452
+ try {
453
+ childWindow = side.frame.contentWindow;
454
+ const map = childWindow?.__tileflowPreviewMap;
455
+ if (map) {
456
+ side.map = map;
457
+ side.cleanupBridge = installChildBridge(side, childWindow, map, epoch);
458
+ return;
459
+ }
460
+ } catch (error) {
461
+ markSideRuntimeError(side, error);
462
+ return;
463
+ }
464
+ if (performance.now() - started < 10_000) {
465
+ setTimeout(findMap, 50);
466
+ } else {
467
+ markSideRuntimeError(side, "Preview map did not initialize.");
468
+ maybeEstablishInitialCamera();
469
+ }
470
+ };
471
+ findMap();
472
+ }
473
+
474
+ function installChildBridge(side, childWindow, map, epoch) {
475
+ let applyingRequestId;
476
+ let inspectionActive = inspectionEnabled;
477
+ let movementFrame;
478
+ let queuedUserMovement = false;
479
+ let releaseFrame;
480
+ let releaseFrameAfterPaint;
481
+ let readyTimer;
482
+ let readyAnnounced = false;
483
+ const targetOrigin = childWindow.location.origin;
484
+ // The bridge is installed by the same-origin parent, so callbacks created here
485
+ // belong to the parent realm. Dispatch directly with an explicit child source;
486
+ // commands still cross the iframe boundary through validated postMessage events.
487
+ const postToParent = (message) => receiveBridgeMessage({
488
+ data: message,
489
+ origin: targetOrigin,
490
+ source: childWindow
491
+ });
492
+ let userMovementActive = false;
493
+ const postCamera = (phase, requestId, userInitiated = false) => {
494
+ postToParent({
495
+ type: bridgeCameraType,
496
+ schemaVersion: bridgeSchemaVersion,
497
+ side: side.id,
498
+ phase,
499
+ ...(requestId === undefined ? {} : {requestId}),
500
+ ...(userInitiated ? {userInitiated: true} : {}),
501
+ camera: readMapCamera(map)
502
+ });
503
+ };
504
+ const handleMoveStart = (event) => {
505
+ if (applyingRequestId === undefined && event?.originalEvent) userMovementActive = true;
506
+ };
507
+ const handleMove = (event) => {
508
+ if (applyingRequestId !== undefined) return;
509
+ if (event?.originalEvent) userMovementActive = true;
510
+ queuedUserMovement = queuedUserMovement || userMovementActive;
511
+ if (movementFrame !== undefined) return;
512
+ movementFrame = childWindow.requestAnimationFrame(() => {
513
+ movementFrame = undefined;
514
+ const userInitiated = queuedUserMovement;
515
+ queuedUserMovement = false;
516
+ postCamera("move", undefined, userInitiated);
517
+ });
518
+ };
519
+ const handleMoveEnd = (event) => {
520
+ if (applyingRequestId !== undefined) {
521
+ postCamera("end", applyingRequestId);
522
+ scheduleApplyingRelease(applyingRequestId);
523
+ return;
524
+ }
525
+ if (event?.originalEvent) userMovementActive = true;
526
+ if (movementFrame !== undefined) {
527
+ childWindow.cancelAnimationFrame(movementFrame);
528
+ movementFrame = undefined;
529
+ }
530
+ queuedUserMovement = false;
531
+ postCamera("end", undefined, userMovementActive);
532
+ userMovementActive = false;
533
+ };
534
+ const handleClick = (event) => {
535
+ if (!inspectionActive) return;
536
+ const features = map.queryRenderedFeatures(event.point).slice(0, 16).map(projectFeature);
537
+ postToParent({
538
+ type: bridgeInspectionType,
539
+ schemaVersion: bridgeSchemaVersion,
540
+ side: side.id,
541
+ coordinate: [event.lngLat.lng, event.lngLat.lat],
542
+ features
543
+ });
544
+ };
545
+ const handleError = (event) => {
546
+ postToParent({
547
+ type: bridgeRuntimeErrorType,
548
+ schemaVersion: bridgeSchemaVersion,
549
+ side: side.id,
550
+ message: sanitizeRuntimeMessage(event?.error?.message || event?.message || "Map runtime error")
551
+ });
552
+ };
553
+ const handleCommand = (event) => {
554
+ if (event.source !== childWindow.parent || event.origin !== targetOrigin) return;
555
+ const message = event.data;
556
+ if (!message || message.schemaVersion !== bridgeSchemaVersion) return;
557
+ if (message.type === bridgeSetInspectionType && typeof message.enabled === "boolean") {
558
+ inspectionActive = message.enabled;
559
+ map.getCanvas().style.cursor = inspectionActive ? "crosshair" : "";
560
+ return;
561
+ }
562
+ if (message.type !== bridgeSetCameraType || !validCamera(message.camera)) return;
563
+ if (movementFrame !== undefined) {
564
+ childWindow.cancelAnimationFrame(movementFrame);
565
+ movementFrame = undefined;
566
+ }
567
+ queuedUserMovement = false;
568
+ userMovementActive = false;
569
+ cancelApplyingRelease();
570
+ applyingRequestId = typeof message.requestId === "number" ? message.requestId : 0;
571
+ try {
572
+ map.jumpTo(message.camera);
573
+ postCamera("end", applyingRequestId);
574
+ scheduleApplyingRelease(applyingRequestId);
575
+ } catch (error) {
576
+ applyingRequestId = undefined;
577
+ handleError({error});
578
+ }
579
+ };
580
+ const announceReady = () => {
581
+ if (readyAnnounced || side.loadEpoch !== epoch) return;
582
+ readyAnnounced = true;
583
+ clearTimeout(readyTimer);
584
+ postToParent({
585
+ type: bridgeReadyType,
586
+ schemaVersion: bridgeSchemaVersion,
587
+ side: side.id,
588
+ camera: readMapCamera(map)
589
+ });
590
+ };
591
+ const handleReadyTimeout = () => {
592
+ if (readyAnnounced || side.loadEpoch !== epoch) return;
593
+ markSideRuntimeError(side, "Preview map did not become ready within 10 seconds.");
594
+ maybeEstablishInitialCamera();
595
+ };
596
+ const cancelApplyingRelease = () => {
597
+ if (releaseFrame !== undefined) childWindow.cancelAnimationFrame(releaseFrame);
598
+ if (releaseFrameAfterPaint !== undefined) childWindow.cancelAnimationFrame(releaseFrameAfterPaint);
599
+ releaseFrame = undefined;
600
+ releaseFrameAfterPaint = undefined;
601
+ };
602
+ const scheduleApplyingRelease = (requestId) => {
603
+ cancelApplyingRelease();
604
+ releaseFrame = childWindow.requestAnimationFrame(() => {
605
+ releaseFrame = undefined;
606
+ releaseFrameAfterPaint = childWindow.requestAnimationFrame(() => {
607
+ releaseFrameAfterPaint = undefined;
608
+ if (applyingRequestId === requestId) applyingRequestId = undefined;
609
+ });
610
+ });
611
+ };
612
+ map.on("movestart", handleMoveStart);
613
+ map.on("move", handleMove);
614
+ map.on("moveend", handleMoveEnd);
615
+ map.on("click", handleClick);
616
+ map.on("error", handleError);
617
+ childWindow.addEventListener("message", handleCommand);
618
+ if (map.loaded?.()) announceReady();
619
+ else {
620
+ map.once("load", announceReady);
621
+ map.once("idle", announceReady);
622
+ readyTimer = setTimeout(handleReadyTimeout, 10_000);
623
+ }
624
+ return () => {
625
+ if (movementFrame !== undefined) childWindow.cancelAnimationFrame(movementFrame);
626
+ cancelApplyingRelease();
627
+ clearTimeout(readyTimer);
628
+ map.off("movestart", handleMoveStart);
629
+ map.off("move", handleMove);
630
+ map.off("moveend", handleMoveEnd);
631
+ map.off("click", handleClick);
632
+ map.off("error", handleError);
633
+ map.off("load", announceReady);
634
+ map.off("idle", announceReady);
635
+ childWindow.removeEventListener("message", handleCommand);
636
+ };
637
+ }
638
+
639
+ function receiveBridgeMessage(event) {
640
+ if (event.origin !== location.origin || !event.data || event.data.schemaVersion !== bridgeSchemaVersion) return;
641
+ const side = Object.values(states).find((candidate) => candidate.frame.contentWindow === event.source);
642
+ if (!side || event.data.side !== side.id) return;
643
+ const message = event.data;
644
+ if (message.type === bridgeReadyType && validCamera(message.camera)) {
645
+ side.bridgeReady = true;
646
+ side.camera = message.camera;
647
+ renderSideStatus(side);
648
+ maybeEstablishInitialCamera();
649
+ sendInspectionState(side);
650
+ void loadSidecar(side);
651
+ updateDocumentReadiness();
652
+ return;
653
+ }
654
+ if (message.type === bridgeCameraType && validCamera(message.camera)) {
655
+ if (message.requestId !== undefined) {
656
+ if (message.requestId !== side.lastCameraRequestId) return;
657
+ side.camera = message.camera;
658
+ if (
659
+ side.id === stage.dataset.driver &&
660
+ canonicalCamera &&
661
+ !camerasEqual(message.camera, canonicalCamera)
662
+ ) {
663
+ canonicalCamera = message.camera;
664
+ sendCamera(states[side.id === "left" ? "right" : "left"], canonicalCamera);
665
+ writeUrlState();
666
+ }
667
+ if (message.phase === "end" && selectedInspection?.side === side) renderCurve();
668
+ return;
669
+ }
670
+ side.camera = message.camera;
671
+ if (message.userInitiated !== true) {
672
+ if (canonicalCamera && !camerasEqual(message.camera, canonicalCamera)) {
673
+ sendCamera(side, canonicalCamera);
674
+ }
675
+ return;
676
+ }
677
+ selectSide(side.id, false);
678
+ canonicalCamera = message.camera;
679
+ const other = states[side.id === "left" ? "right" : "left"];
680
+ sendCamera(other, canonicalCamera);
681
+ if (message.phase === "end") {
682
+ writeUrlState();
683
+ if (selectedInspection?.side === side) renderCurve();
684
+ }
685
+ return;
686
+ }
687
+ if (message.type === bridgeInspectionType && Array.isArray(message.features)) {
688
+ selectSide(side.id);
689
+ selectedInspection = {side, coordinate: message.coordinate, features: message.features.slice(0, 16), featureIndex: 0};
690
+ renderInspection();
691
+ return;
692
+ }
693
+ if (message.type === bridgeRuntimeErrorType) {
694
+ markSideRuntimeError(side, message.message);
695
+ }
696
+ }
697
+
698
+ function sendCamera(side, camera) {
699
+ if (!side.bridgeReady || !camera || !side.frame.contentWindow) return;
700
+ if (side.camera && camerasEqual(side.camera, camera)) return;
701
+ const requestId = ++requestSequence;
702
+ side.lastCameraRequestId = requestId;
703
+ side.frame.contentWindow.postMessage({
704
+ type: bridgeSetCameraType,
705
+ schemaVersion: bridgeSchemaVersion,
706
+ requestId,
707
+ camera
708
+ }, location.origin);
709
+ }
710
+
711
+ function maybeEstablishInitialCamera() {
712
+ if (canonicalCamera) {
713
+ for (const side of Object.values(states)) sendCamera(side, canonicalCamera);
714
+ writeUrlState();
715
+ return;
716
+ }
717
+ const driver = states[stage.dataset.driver];
718
+ let source = driver.bridgeReady && driver.camera ? driver : undefined;
719
+ const driverUnavailable = driver.runtimeFailed || (
720
+ driver.artifactStatus === "invalid" && !Number.isInteger(driver.artifactLastGoodGeneration)
721
+ );
722
+ if (!source && driverUnavailable && states.left.bridgeReady && states.left.camera) {
723
+ source = states.left;
724
+ }
725
+ if (!source) return;
726
+ canonicalCamera = source.camera;
727
+ for (const side of Object.values(states)) {
728
+ if (side !== source) sendCamera(side, canonicalCamera);
729
+ }
730
+ writeUrlState();
731
+ }
732
+
733
+ function sendInspectionState(side) {
734
+ if (!side.bridgeReady || !side.frame.contentWindow) return;
735
+ side.frame.contentWindow.postMessage({
736
+ type: bridgeSetInspectionType,
737
+ schemaVersion: bridgeSchemaVersion,
738
+ enabled: inspectionEnabled
739
+ }, location.origin);
740
+ }
741
+
742
+ function startStatus(side) {
743
+ fetch(side.definition.statusUrl, {cache: "no-store"})
744
+ .then((response) => response.ok ? response.json() : Promise.reject(new Error("Status unavailable")))
745
+ .then((status) => applyArtifactStatus(side, status))
746
+ .catch(() => undefined);
747
+ const events = new EventSource(side.definition.eventsUrl);
748
+ side.eventSource = events;
749
+ for (const name of ["building", "ready", "invalid"]) {
750
+ events.addEventListener(name, (event) => {
751
+ try { applyArtifactStatus(side, JSON.parse(event.data)); } catch {}
752
+ });
753
+ }
754
+ }
755
+
756
+ function applyArtifactStatus(side, status) {
757
+ if (!status || !["building", "ready", "invalid"].includes(status.status)) return;
758
+ const generation = Number.isInteger(status.generation) && status.generation >= 0 ? status.generation : 0;
759
+ if (generation < side.artifactGeneration) return;
760
+ if (
761
+ generation === side.artifactGeneration &&
762
+ status.status === "building" &&
763
+ side.artifactStatus !== "building"
764
+ ) return;
765
+ side.artifactGeneration = generation;
766
+ side.artifactStatus = status.status;
767
+ side.artifactLastGoodGeneration = Number.isInteger(status.lastGoodGeneration)
768
+ ? status.lastGoodGeneration
769
+ : undefined;
770
+ side.artifactDiagnostic = status.diagnostics?.[0]?.message;
771
+ renderSideStatus(side);
772
+ maybeEstablishInitialCamera();
773
+ updateDocumentReadiness();
774
+ }
775
+
776
+ function updateDocumentReadiness() {
777
+ const sides = Object.values(states);
778
+ const failed = sides.some((side) => side.runtimeFailed || (
779
+ side.artifactStatus === "invalid" && !Number.isInteger(side.artifactLastGoodGeneration)
780
+ ));
781
+ const ready = sides.every((side) => side.bridgeReady && (
782
+ side.artifactStatus === "ready" ||
783
+ (side.artifactStatus === "invalid" && Number.isInteger(side.artifactLastGoodGeneration))
784
+ ));
785
+ const value = failed ? "error" : ready ? "idle" : "loading";
786
+ document.documentElement.dataset.tileflowState = value;
787
+ stage.dataset.tileflowState = value;
788
+ }
789
+
790
+ function renderSideStatus(side) {
791
+ const diagnostic = side.artifactDiagnostic;
792
+ const message = side.runtimeFailed
793
+ ? "Runtime error: " + (side.runtimeErrors.at(-1) || "Preview runtime failed")
794
+ : side.artifactStatus === "invalid"
795
+ ? "Invalid generation; showing last valid preview" + (diagnostic ? ": " + diagnostic : "")
796
+ : "Generation " + String(Math.max(0, side.artifactGeneration)) + " \xB7 " + side.artifactStatus;
797
+ side.stateElement.dataset.status = side.runtimeFailed ? "error" : side.artifactStatus;
798
+ side.stateElement.title = message;
799
+ side.stateElement.setAttribute("aria-label", side.definition.label + ": " + message);
800
+ side.statusTextElement.textContent = side.runtimeFailed ? "Runtime error" : side.artifactStatus;
801
+ }
802
+
803
+ async function loadSidecar(side) {
804
+ if (!side.definition.sidecarUrl) return undefined;
805
+ if (side.sidecar !== undefined) return side.sidecar;
806
+ if (side.sidecarPromise) return side.sidecarPromise;
807
+ const epoch = side.loadEpoch;
808
+ const controller = new AbortController();
809
+ side.sidecarAbort?.abort();
810
+ side.sidecarAbort = controller;
811
+ const promise = fetch(currentSidecarUrl(side), {cache: "no-store", signal: controller.signal})
812
+ .then((response) => response.ok ? response.json() : Promise.reject(new Error("Compiler sidecar unavailable")))
813
+ .then((document) => {
814
+ if (side.loadEpoch !== epoch || controller.signal.aborted) return undefined;
815
+ side.sidecar = document;
816
+ if (selectedInspection?.side === side) renderInspection();
817
+ return document;
818
+ })
819
+ .catch((error) => {
820
+ if (error?.name !== "AbortError" && selectedInspection?.side === side) {
821
+ sidecarOutput.textContent = "Unable to load compiler sidecar; reload or select the feature to retry.";
822
+ }
823
+ return undefined;
824
+ })
825
+ .finally(() => {
826
+ if (side.sidecarPromise === promise) side.sidecarPromise = undefined;
827
+ if (side.sidecarAbort === controller) side.sidecarAbort = undefined;
828
+ });
829
+ side.sidecarPromise = promise;
830
+ return promise;
831
+ }
832
+
833
+ function currentSidecarUrl(side) {
834
+ const configured = new URL(side.definition.sidecarUrl, location.href);
835
+ const marker = "/__inspection/";
836
+ const markerIndex = configured.pathname.lastIndexOf(marker);
837
+ if (markerIndex < 0) return configured.href;
838
+ const suffixParts = configured.pathname.slice(markerIndex + marker.length).split("/");
839
+ const configuredMap = suffixParts[0];
840
+ const configuredThemeFile = suffixParts[1];
841
+ const configuredTheme = configuredThemeFile?.endsWith(".json") ? configuredThemeFile.slice(0, -5) : undefined;
842
+ if (suffixParts.length !== 2 || !validPortableName(configuredMap) || !validPortableName(configuredTheme)) {
843
+ return configured.href;
844
+ }
845
+ const metadata = side.map?.getStyle?.().metadata;
846
+ const map = metadata?.["tileflow:map"];
847
+ const theme = metadata?.["tileflow:theme"];
848
+ if (!validPortableName(map) || !validPortableName(theme)) {
849
+ return configured.href;
850
+ }
851
+ configured.pathname = configured.pathname.slice(0, markerIndex) + marker + map + "/" + theme + ".json";
852
+ return configured.href;
853
+ }
854
+
855
+ function renderInspection() {
856
+ if (!selectedInspection) return;
857
+ const side = selectedInspection.side;
858
+ const feature = selectedInspection.features[selectedInspection.featureIndex];
859
+ inspectionOutput.replaceChildren();
860
+ if (!feature) {
861
+ inspectionOutput.className = "empty";
862
+ inspectionOutput.textContent = side.definition.label + ": no rendered feature at this point.";
863
+ curveProperty.disabled = true;
864
+ curveOutput.className = "empty";
865
+ curveOutput.textContent = "Paint and layout values appear here.";
866
+ sidecarOutput.textContent = "No layer selected.";
867
+ return;
868
+ }
869
+ inspectionOutput.className = "";
870
+ const layer = side.map?.getStyle?.().layers?.find((candidate) => candidate.id === feature.layerId);
871
+ const title = document.createElement("strong");
872
+ title.textContent = side.definition.label + " \xB7 " + feature.layerId;
873
+ const pickerLabel = document.createElement("label");
874
+ pickerLabel.className = "feature-picker";
875
+ pickerLabel.append(document.createTextNode("Rendered feature (" + selectedInspection.features.length + ")"));
876
+ const picker = document.createElement("select");
877
+ picker.setAttribute("aria-label", "Rendered feature at inspected point");
878
+ selectedInspection.features.forEach((candidate, index) => {
879
+ const option = document.createElement("option");
880
+ option.value = String(index);
881
+ option.textContent = String(index + 1) + ". " + candidate.layerId + " \xB7 " + candidate.geometryType +
882
+ (candidate.id === null ? "" : " \xB7 " + String(candidate.id));
883
+ picker.append(option);
884
+ });
885
+ picker.value = String(selectedInspection.featureIndex);
886
+ picker.addEventListener("change", () => {
887
+ selectedInspection.featureIndex = boundedInteger(Number(picker.value), 0, selectedInspection.features.length - 1, 0);
888
+ renderInspection();
889
+ });
890
+ pickerLabel.append(picker);
891
+ const summary = document.createElement("pre");
892
+ summary.textContent = boundedJson({
893
+ coordinate: selectedInspection.coordinate,
894
+ feature: {id: feature.id, geometryType: feature.geometryType, properties: feature.properties},
895
+ layer: layer ? {
896
+ id: layer.id,
897
+ type: layer.type,
898
+ source: layer.source,
899
+ sourceLayer: layer["source-layer"],
900
+ minzoom: layer.minzoom,
901
+ maxzoom: layer.maxzoom,
902
+ filter: layer.filter
903
+ } : undefined
904
+ });
905
+ inspectionOutput.append(title, pickerLabel, summary);
906
+ populateCurveProperties(layer);
907
+ const sidecarLayer = findSidecarLayer(side.sidecar, feature.layerId);
908
+ sidecarOutput.textContent = sidecarLayer
909
+ ? boundedJson(sidecarLayerWithSemanticAttribution(sidecarLayer))
910
+ : "No compiler sidecar entry for this layer.";
911
+ if (side.sidecar === undefined && !side.sidecarPromise) void loadSidecar(side);
912
+ }
913
+
914
+ function populateCurveProperties(layer) {
915
+ const previousValue = curveProperty.value;
916
+ curveProperty.replaceChildren();
917
+ if (!layer) {
918
+ curveProperty.disabled = true;
919
+ return;
920
+ }
921
+ const entries = [];
922
+ for (const section of ["paint", "layout"]) {
923
+ for (const name of Object.keys(layer[section] || {}).sort()) entries.push({section, name});
924
+ }
925
+ if (entries.length === 0) {
926
+ curveProperty.disabled = true;
927
+ curveOutput.textContent = "This layer has no paint or layout values.";
928
+ return;
929
+ }
930
+ for (const entry of entries) {
931
+ const option = document.createElement("option");
932
+ option.value = entry.section + ":" + entry.name;
933
+ option.textContent = entry.section + "." + entry.name;
934
+ curveProperty.append(option);
935
+ }
936
+ if (Array.from(curveProperty.options).some((option) => option.value === previousValue)) {
937
+ curveProperty.value = previousValue;
938
+ }
939
+ curveProperty.disabled = false;
940
+ renderCurve();
941
+ }
942
+
943
+ function renderCurve() {
944
+ if (!selectedInspection) return;
945
+ const side = selectedInspection.side;
946
+ const feature = selectedInspection.features[selectedInspection.featureIndex];
947
+ if (!feature) return;
948
+ const layer = side.map?.getStyle?.().layers?.find((candidate) => candidate.id === feature.layerId);
949
+ const [section, name] = curveProperty.value.split(":");
950
+ const raw = layer?.[section]?.[name];
951
+ curveOutput.replaceChildren();
952
+ if (raw === undefined) return;
953
+ const maximumZoom = 24;
954
+ const samples = Array.from({length: maximumZoom * 4 + 1}, (_, index) => {
955
+ const zoom = index / 4;
956
+ return {zoom, value: evaluateBasicZoomValue(raw, zoom)};
957
+ });
958
+ if (samples.some((sample) => sample.value === unsupportedValue)) {
959
+ const pre = document.createElement("pre");
960
+ pre.textContent = "Unsupported or data-driven expression; no approximate curve is shown.\\n" + boundedJson(raw);
961
+ curveOutput.append(pre);
962
+ return;
963
+ }
964
+ const currentZoom = boundedNumber(side.camera?.zoom ?? canonicalCamera?.zoom, 0, maximumZoom, 0);
965
+ const currentValue = evaluateBasicZoomValue(raw, currentZoom);
966
+ const meta = document.createElement("div");
967
+ meta.className = "curve-meta";
968
+ meta.textContent = "Current z" + formatNumber(currentZoom) + ": " + formatCurveValue(currentValue) + " \xB7 sampled every 0.25 zoom";
969
+ curveOutput.append(meta);
970
+ if (samples.every((sample) => typeof sample.value === "number" && Number.isFinite(sample.value))) {
971
+ curveOutput.append(renderNumericCurve(samples, currentZoom));
972
+ return;
973
+ }
974
+ if (samples.every((sample) => parseColor(sample.value))) {
975
+ const strip = document.createElement("div");
976
+ strip.className = "color-curve";
977
+ for (const sample of samples) {
978
+ const swatch = document.createElement("span");
979
+ swatch.style.background = colorToCss(parseColor(sample.value));
980
+ swatch.title = "z" + formatNumber(sample.zoom) + ": " + String(sample.value);
981
+ strip.append(swatch);
982
+ }
983
+ curveOutput.append(strip);
984
+ return;
985
+ }
986
+ const pre = document.createElement("pre");
987
+ pre.textContent = samples.map((sample) => "z" + sample.zoom + " " + String(sample.value)).join("\\n");
988
+ curveOutput.append(pre);
989
+ }
990
+
991
+ const unsupportedValue = Symbol("unsupported");
992
+ function evaluateBasicZoomValue(value, zoom) {
993
+ if (!Array.isArray(value)) {
994
+ return value && typeof value === "object" && ("stops" in value || "property" in value)
995
+ ? unsupportedValue
996
+ : value;
997
+ }
998
+ if (value[0] === "literal") return value[1];
999
+ if (value[0] === "step" && isZoomInput(value[1])) {
1000
+ let output = value[2];
1001
+ for (let index = 3; index + 1 < value.length; index += 2) {
1002
+ if (zoom < value[index]) break;
1003
+ output = value[index + 1];
1004
+ }
1005
+ return evaluateBasicZoomValue(output, zoom);
1006
+ }
1007
+ if (value[0] === "interpolate" && isZoomInput(value[2])) {
1008
+ const stops = [];
1009
+ for (let index = 3; index + 1 < value.length; index += 2) stops.push([value[index], value[index + 1]]);
1010
+ if (!stops.length) return unsupportedValue;
1011
+ if (zoom <= stops[0][0]) return evaluateBasicZoomValue(stops[0][1], zoom);
1012
+ if (zoom >= stops.at(-1)[0]) return evaluateBasicZoomValue(stops.at(-1)[1], zoom);
1013
+ const upperIndex = stops.findIndex((stop) => zoom < stop[0]);
1014
+ const lower = stops[upperIndex - 1];
1015
+ const upper = stops[upperIndex];
1016
+ const interpolation = value[1];
1017
+ let base;
1018
+ if (Array.isArray(interpolation) && interpolation.length === 1 && interpolation[0] === "linear") {
1019
+ base = 1;
1020
+ } else if (
1021
+ Array.isArray(interpolation) && interpolation.length === 2 &&
1022
+ interpolation[0] === "exponential" && Number.isFinite(Number(interpolation[1])) &&
1023
+ Number(interpolation[1]) > 0
1024
+ ) {
1025
+ base = Number(interpolation[1]);
1026
+ } else {
1027
+ return unsupportedValue;
1028
+ }
1029
+ const progress = interpolationProgress(zoom, lower[0], upper[0], base);
1030
+ const from = evaluateBasicZoomValue(lower[1], zoom);
1031
+ const to = evaluateBasicZoomValue(upper[1], zoom);
1032
+ if (typeof from === "number" && typeof to === "number") return from + (to - from) * progress;
1033
+ const fromColor = parseColor(from);
1034
+ const toColor = parseColor(to);
1035
+ return fromColor && toColor ? colorToCss(fromColor.map((part, index) => part + (toColor[index] - part) * progress)) : unsupportedValue;
1036
+ }
1037
+ return unsupportedValue;
1038
+ }
1039
+
1040
+ function isZoomInput(value) { return Array.isArray(value) && value.length === 1 && value[0] === "zoom"; }
1041
+ function interpolationProgress(value, lower, upper, base) {
1042
+ const distance = upper - lower;
1043
+ if (!distance) return 0;
1044
+ if (!Number.isFinite(base) || base === 1) return (value - lower) / distance;
1045
+ return (Math.pow(base, value - lower) - 1) / (Math.pow(base, distance) - 1);
1046
+ }
1047
+
1048
+ function renderNumericCurve(samples, currentZoom) {
1049
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1050
+ svg.setAttribute("class", "curve");
1051
+ svg.setAttribute("viewBox", "0 0 320 116");
1052
+ const values = samples.map((sample) => sample.value);
1053
+ const minimum = Math.min(...values);
1054
+ const maximum = Math.max(...values);
1055
+ const span = maximum - minimum || 1;
1056
+ const points = samples.map((sample) => {
1057
+ const x = 10 + sample.zoom / 24 * 300;
1058
+ const y = 104 - (sample.value - minimum) / span * 92;
1059
+ return x.toFixed(2) + "," + y.toFixed(2);
1060
+ }).join(" ");
1061
+ const polyline = document.createElementNS(svg.namespaceURI, "polyline");
1062
+ polyline.setAttribute("points", points);
1063
+ polyline.setAttribute("fill", "none");
1064
+ polyline.setAttribute("stroke", "#3157d5");
1065
+ polyline.setAttribute("stroke-width", "2");
1066
+ svg.append(polyline);
1067
+ const marker = document.createElementNS(svg.namespaceURI, "line");
1068
+ const markerX = 10 + currentZoom / 24 * 300;
1069
+ marker.setAttribute("x1", String(markerX));
1070
+ marker.setAttribute("x2", String(markerX));
1071
+ marker.setAttribute("y1", "8");
1072
+ marker.setAttribute("y2", "108");
1073
+ marker.setAttribute("stroke", "#ba3838");
1074
+ marker.setAttribute("stroke-width", "1.5");
1075
+ marker.setAttribute("stroke-dasharray", "3 3");
1076
+ svg.append(marker);
1077
+ const title = document.createElementNS(svg.namespaceURI, "title");
1078
+ title.textContent = "minimum " + minimum + ", maximum " + maximum + ", current zoom " + formatNumber(currentZoom);
1079
+ svg.append(title);
1080
+ svg.setAttribute("role", "img");
1081
+ svg.setAttribute("aria-label", title.textContent);
1082
+ return svg;
1083
+ }
1084
+
1085
+ async function loadSprites(side) {
1086
+ const epoch = side.loadEpoch;
1087
+ const sequence = ++spriteLoadSequence;
1088
+ spriteOwnerId = side.id;
1089
+ side.spriteAbort?.abort();
1090
+ const controller = new AbortController();
1091
+ side.spriteAbort = controller;
1092
+ spriteOutput.replaceChildren();
1093
+ const style = side.map?.getStyle?.();
1094
+ if (typeof style?.sprite !== "string") {
1095
+ spriteOutput.textContent = "The active style has no single served sprite atlas.";
1096
+ if (side.spriteAbort === controller) side.spriteAbort = undefined;
1097
+ return;
1098
+ }
1099
+ let base;
1100
+ try {
1101
+ base = new URL(style.sprite, side.frame.contentWindow.location.href);
1102
+ } catch {
1103
+ spriteOutput.textContent = "Invalid sprite URL.";
1104
+ if (side.spriteAbort === controller) side.spriteAbort = undefined;
1105
+ return;
1106
+ }
1107
+ if (!urlBelongsToSide(base, side)) {
1108
+ spriteOutput.textContent = "Only sprite atlases served by the active comparison side can be opened.";
1109
+ if (side.spriteAbort === controller) side.spriteAbort = undefined;
1110
+ return;
1111
+ }
1112
+ try {
1113
+ const atlasUrl = spriteAssetUrl(base, ".json");
1114
+ const imageUrl = spriteAssetUrl(base, ".png");
1115
+ const [atlasResponse, image] = await Promise.all([
1116
+ fetch(atlasUrl, {cache: "no-store", signal: controller.signal}),
1117
+ loadImage(imageUrl, controller.signal)
1118
+ ]);
1119
+ if (!atlasResponse.ok) throw new Error("Sprite metadata request failed");
1120
+ const atlas = await atlasResponse.json();
1121
+ if (
1122
+ controller.signal.aborted ||
1123
+ side.loadEpoch !== epoch ||
1124
+ sequence !== spriteLoadSequence ||
1125
+ activeSideId !== side.id
1126
+ ) return;
1127
+ const entries = Object.entries(atlas).sort(([left], [right]) => left.localeCompare(right)).slice(0, 256);
1128
+ for (const [name, item] of entries) {
1129
+ if (!validSpriteEntry(item, image)) continue;
1130
+ const card = document.createElement("div");
1131
+ card.className = "sprite-card";
1132
+ const canvas = document.createElement("canvas");
1133
+ const ratio = item.pixelRatio || 1;
1134
+ canvas.width = Math.max(1, Math.ceil(item.width / ratio));
1135
+ canvas.height = Math.max(1, Math.ceil(item.height / ratio));
1136
+ canvas.getContext("2d").drawImage(image, item.x, item.y, item.width, item.height, 0, 0, canvas.width, canvas.height);
1137
+ const label = document.createElement("span");
1138
+ label.className = "sprite-name";
1139
+ label.textContent = name;
1140
+ label.title = name;
1141
+ card.append(canvas, label);
1142
+ spriteOutput.append(card);
1143
+ }
1144
+ if (!spriteOutput.childElementCount) spriteOutput.textContent = "The atlas is empty.";
1145
+ } catch (error) {
1146
+ if (error?.name !== "AbortError" && sequence === spriteLoadSequence) {
1147
+ spriteOutput.textContent = "Unable to load the served sprite atlas: " + safeErrorMessage(error);
1148
+ }
1149
+ } finally {
1150
+ if (side.spriteAbort === controller) side.spriteAbort = undefined;
1151
+ }
1152
+ }
1153
+
1154
+ function loadImage(url, signal) {
1155
+ return new Promise((resolve, reject) => {
1156
+ const image = new Image();
1157
+ const cleanup = () => signal.removeEventListener("abort", handleAbort);
1158
+ const handleAbort = () => {
1159
+ image.src = "";
1160
+ cleanup();
1161
+ reject(new DOMException("Sprite image request aborted", "AbortError"));
1162
+ };
1163
+ image.onload = () => { cleanup(); resolve(image); };
1164
+ image.onerror = () => { cleanup(); reject(new Error("Sprite image request failed")); };
1165
+ if (signal.aborted) return handleAbort();
1166
+ signal.addEventListener("abort", handleAbort, {once: true});
1167
+ image.src = url;
1168
+ });
1169
+ }
1170
+
1171
+ function urlBelongsToSide(url, side) {
1172
+ const basePath = side.definition.basePath;
1173
+ return url.origin === location.origin &&
1174
+ (url.pathname === basePath || url.pathname.startsWith(basePath + "/"));
1175
+ }
1176
+
1177
+ function spriteAssetUrl(base, suffix) {
1178
+ const asset = new URL(base.href);
1179
+ asset.pathname += suffix;
1180
+ return asset.href;
1181
+ }
1182
+
1183
+ function validSpriteEntry(value, image) {
1184
+ return value && [value.x, value.y, value.width, value.height].every(Number.isFinite) &&
1185
+ value.x >= 0 && value.y >= 0 && value.width > 0 && value.height > 0 &&
1186
+ value.x + value.width <= image.naturalWidth && value.y + value.height <= image.naturalHeight;
1187
+ }
1188
+
1189
+ async function copyScene(commandOnly) {
1190
+ const name = sceneNameInput.value;
1191
+ if (!validPortableName(name)) {
1192
+ setFeedback("Use a portable kebab-case scene name.");
1193
+ return;
1194
+ }
1195
+ const side = states[activeSideId];
1196
+ const camera = side.camera || canonicalCamera;
1197
+ if (!camera) {
1198
+ setFeedback("Wait for the maps to become ready.");
1199
+ return;
1200
+ }
1201
+ const style = side.map?.getStyle?.();
1202
+ const map = style?.metadata?.["tileflow:map"];
1203
+ const theme = style?.metadata?.["tileflow:theme"];
1204
+ if (!validPortableName(map) || !validConcreteThemeName(theme)) {
1205
+ setFeedback("The active preview does not expose a concrete map and theme.");
1206
+ return;
1207
+ }
1208
+ const dpr = Number(sceneDprInput.value) === 2 ? 2 : 1;
1209
+ const viewport = readMapViewport(side, dpr);
1210
+ const {width, height} = viewport;
1211
+ if (commandOnly) {
1212
+ const command = [
1213
+ "tileflow capture",
1214
+ ...(side.definition.captureConfig
1215
+ ? ["--config " + quoteCliArgument(side.definition.captureConfig)]
1216
+ : []),
1217
+ "--map " + quoteCliArgument(map),
1218
+ "--theme " + quoteCliArgument(theme),
1219
+ "--center=" + formatNumber(camera.center[0]) + "," + formatNumber(camera.center[1]),
1220
+ "--zoom=" + formatNumber(camera.zoom),
1221
+ "--bearing=" + formatNumber(camera.bearing),
1222
+ "--pitch=" + formatNumber(camera.pitch),
1223
+ "--width=" + width,
1224
+ "--height=" + height,
1225
+ "--dpr=" + dpr,
1226
+ "--out=" + quoteCliArgument(name + ".png")
1227
+ ].join(" ");
1228
+ if (!await copyText(command)) {
1229
+ setFeedback("Clipboard access is unavailable.");
1230
+ return;
1231
+ }
1232
+ setFeedback("Exploratory capture command copied.");
1233
+ return;
1234
+ }
1235
+ const definition = {
1236
+ theme,
1237
+ camera: {
1238
+ type: "center",
1239
+ center: [roundNumber(camera.center[0]), roundNumber(camera.center[1])],
1240
+ zoom: roundNumber(camera.zoom),
1241
+ bearing: roundNumber(camera.bearing),
1242
+ pitch: roundNumber(camera.pitch)
1243
+ },
1244
+ viewport: {width, height, dpr},
1245
+ target: {kind: "map"}
1246
+ };
1247
+ const serialized = JSON.stringify(definition, null, 2).replace(/^/gm, " ");
1248
+ if (!await copyText("'" + name + "': " + serialized.trimStart() + ",")) {
1249
+ setFeedback("Clipboard access is unavailable.");
1250
+ return;
1251
+ }
1252
+ setFeedback("Scene entry copied.");
1253
+ }
1254
+
1255
+ function readMapViewport(side, dpr) {
1256
+ const container = side.map?.getContainer?.();
1257
+ const canvas = side.map?.getCanvas?.();
1258
+ const maximum = dpr === 2 ? 2048 : 4096;
1259
+ const width = boundedInteger(container?.clientWidth || canvas?.clientWidth, 64, maximum, 1280);
1260
+ const height = boundedInteger(container?.clientHeight || canvas?.clientHeight, 64, maximum, 800);
1261
+ return {width, height};
1262
+ }
1263
+
1264
+ async function copyText(value) {
1265
+ try {
1266
+ await navigator.clipboard.writeText(value);
1267
+ return true;
1268
+ } catch {
1269
+ const textarea = document.createElement("textarea");
1270
+ textarea.value = value;
1271
+ textarea.style.position = "fixed";
1272
+ textarea.style.opacity = "0";
1273
+ document.body.append(textarea);
1274
+ textarea.select();
1275
+ const copied = document.execCommand("copy");
1276
+ textarea.remove();
1277
+ return copied;
1278
+ }
1279
+ }
1280
+
1281
+ function writeUrlState() {
1282
+ const url = new URL(location.href);
1283
+ url.searchParams.set("mode", stage.dataset.mode);
1284
+ url.searchParams.set("split", String(Math.round(Number(splitInput.value))));
1285
+ url.searchParams.set("alpha", String(Math.round(Number(alphaInput.value))));
1286
+ url.searchParams.set("driver", stage.dataset.driver);
1287
+ if (canonicalCamera) {
1288
+ const values = {
1289
+ lng: canonicalCamera.center[0], lat: canonicalCamera.center[1], zoom: canonicalCamera.zoom,
1290
+ bearing: canonicalCamera.bearing, pitch: canonicalCamera.pitch
1291
+ };
1292
+ for (const [name, value] of Object.entries(values)) url.searchParams.set(name, formatNumber(value));
1293
+ }
1294
+ history.replaceState(history.state, "", url.href);
1295
+ }
1296
+
1297
+ function readUiFromUrl() {
1298
+ const query = new URL(location.href).searchParams;
1299
+ const modeValue = singleQueryValue(query, "mode");
1300
+ const mode = ["side-by-side", "split", "overlay", "blink"].includes(modeValue) ? modeValue : undefined;
1301
+ const driver = singleQueryValue(query, "driver");
1302
+ selectSide(driver === "right" ? "right" : "left", false);
1303
+ const split = singleQueryValue(query, "split");
1304
+ const alpha = singleQueryValue(query, "alpha");
1305
+ return {
1306
+ mode,
1307
+ split: split === undefined ? 50 : boundedNumber(Number(split), 5, 95, 50),
1308
+ alpha: alpha === undefined ? 50 : boundedNumber(Number(alpha), 0, 100, 50)
1309
+ };
1310
+ }
1311
+
1312
+ function cameraFromUrl() {
1313
+ const query = new URL(location.href).searchParams;
1314
+ if (!["lng", "lat", "zoom", "bearing", "pitch"].every((name) => query.getAll(name).length === 1)) return undefined;
1315
+ const camera = {
1316
+ center: [Number(query.get("lng")), Number(query.get("lat"))],
1317
+ zoom: Number(query.get("zoom")), bearing: Number(query.get("bearing")), pitch: Number(query.get("pitch"))
1318
+ };
1319
+ return validCamera(camera) ? camera : undefined;
1320
+ }
1321
+
1322
+ function readMapCamera(map) {
1323
+ const center = map.getCenter();
1324
+ return {
1325
+ center: [wrapLongitude(center.lng), center.lat], zoom: map.getZoom(),
1326
+ bearing: map.getBearing(), pitch: map.getPitch()
1327
+ };
1328
+ }
1329
+
1330
+ function validCamera(camera) {
1331
+ return camera && Array.isArray(camera.center) && camera.center.length === 2 &&
1332
+ camera.center.every(Number.isFinite) && camera.center[0] >= -180 && camera.center[0] <= 180 &&
1333
+ camera.center[1] >= -90 && camera.center[1] <= 90 && Number.isFinite(camera.zoom) &&
1334
+ camera.zoom >= 0 && camera.zoom <= 24 && Number.isFinite(camera.bearing) &&
1335
+ camera.bearing >= -180 && camera.bearing <= 180 && Number.isFinite(camera.pitch) &&
1336
+ camera.pitch >= 0 && camera.pitch <= 85;
1337
+ }
1338
+
1339
+ function projectFeature(feature) {
1340
+ return {
1341
+ layerId: String(feature.layer?.id || "").slice(0, 256),
1342
+ id: ["number", "string"].includes(typeof feature.id) ? feature.id : null,
1343
+ geometryType: String(feature.geometry?.type || "Unknown").slice(0, 32),
1344
+ properties: sanitizeProperties(feature.properties)
1345
+ };
1346
+ }
1347
+
1348
+ function sanitizeProperties(input) {
1349
+ const result = {};
1350
+ for (const key of Object.keys(input || {}).sort().slice(0, 64)) {
1351
+ if (key.length > 128) continue;
1352
+ const value = input[key];
1353
+ if (typeof value === "boolean" || (typeof value === "number" && Number.isFinite(value))) result[key] = value;
1354
+ else if (typeof value === "string") result[key] = value.slice(0, 512);
1355
+ }
1356
+ return result;
1357
+ }
1358
+
1359
+ function findSidecarLayer(document, layerId) {
1360
+ const layers = document?.layers;
1361
+ if (Array.isArray(layers)) return layers.find((layer) => layer?.id === layerId || layer?.layerId === layerId);
1362
+ if (layers && typeof layers === "object") return layers[layerId];
1363
+ return undefined;
1364
+ }
1365
+
1366
+ function sidecarLayerWithSemanticAttribution(layer) {
1367
+ // Sidecar v1 proves semantic provenance, not token provenance. Keep explicit
1368
+ // tokens when present, but never infer them by matching compiled values.
1369
+ const contributions = Array.isArray(layer?.contributions) ? layer.contributions : [];
1370
+ const semanticAttribution = contributions.flatMap((contribution) => {
1371
+ const semanticOwner = typeof contribution?.owner === "string" ? contribution.owner : undefined;
1372
+ const semanticTarget = typeof contribution?.target === "string" ? contribution.target : undefined;
1373
+ if (!semanticOwner || !semanticTarget) return [];
1374
+ const effects = Array.isArray(contribution.effects) ? contribution.effects : [];
1375
+ const authoringPaths = [...new Set([
1376
+ "modules." + semanticOwner,
1377
+ ...causalStringArray(contribution.authoringPaths),
1378
+ ...effects.flatMap((effect) =>
1379
+ (effect?.kind === "add" || effect?.kind === "patch") && typeof effect.target === "string"
1380
+ ? ["compilerEffects." + effect.target + "." + effect.kind]
1381
+ : []
1382
+ )
1383
+ ])];
1384
+ const themeTokens = causalArray(contribution.themeTokens);
1385
+ return [{
1386
+ semanticOwner,
1387
+ semanticTarget,
1388
+ authoringPaths,
1389
+ ...(themeTokens.length > 0 ? {themeTokens} : {})
1390
+ }];
1391
+ });
1392
+ return {...layer, semanticAttribution};
1393
+ }
1394
+
1395
+ function causalStringArray(value) {
1396
+ return causalArray(value).filter((entry) => typeof entry === "string");
1397
+ }
1398
+
1399
+ function causalArray(value) {
1400
+ return Array.isArray(value) ? value.slice(0, 64) : [];
1401
+ }
1402
+
1403
+ function markSideRuntimeError(side, input) {
1404
+ const message = sanitizeRuntimeMessage(safeErrorMessage(input));
1405
+ if (!side.runtimeErrors.includes(message)) side.runtimeErrors.push(message);
1406
+ side.runtimeErrors = side.runtimeErrors.slice(-16);
1407
+ side.runtimeFailed = true;
1408
+ renderSideStatus(side);
1409
+ updateDocumentReadiness();
1410
+ }
1411
+
1412
+ function sanitizeRuntimeMessage(value) {
1413
+ return String(value).replace(/https?:\\/\\/[^\\s)]+/gu, (url) => {
1414
+ try { return new URL(url).origin; } catch { return "[url]"; }
1415
+ }).slice(0, 512);
1416
+ }
1417
+
1418
+ function parseColor(value) {
1419
+ if (typeof value !== "string") return undefined;
1420
+ const hex = /^#([a-f\\d]{3}|[a-f\\d]{6}|[a-f\\d]{8})$/iu.exec(value);
1421
+ if (hex) {
1422
+ let digits = hex[1];
1423
+ if (digits.length === 3) digits = digits.split("").map((part) => part + part).join("");
1424
+ const alpha = digits.length === 8 ? parseInt(digits.slice(6, 8), 16) / 255 : 1;
1425
+ return [parseInt(digits.slice(0, 2), 16), parseInt(digits.slice(2, 4), 16), parseInt(digits.slice(4, 6), 16), alpha];
1426
+ }
1427
+ const rgb = /^rgba?\\(\\s*([\\d.]+)\\s*,\\s*([\\d.]+)\\s*,\\s*([\\d.]+)(?:\\s*,\\s*([\\d.]+))?\\s*\\)$/iu.exec(value);
1428
+ return rgb ? [Number(rgb[1]), Number(rgb[2]), Number(rgb[3]), rgb[4] === undefined ? 1 : Number(rgb[4])] : undefined;
1429
+ }
1430
+
1431
+ function colorToCss(color) {
1432
+ return "rgba(" + color.slice(0, 3).map((part) => Math.round(part)).join(",") + "," + Math.max(0, Math.min(1, color[3])) + ")";
1433
+ }
1434
+
1435
+ function boundedJson(value) {
1436
+ let result;
1437
+ try { result = JSON.stringify(value, null, 2); } catch { result = "[unserializable]"; }
1438
+ if (typeof result !== "string") result = String(value);
1439
+ return result.length > 12_000 ? result.slice(0, 12_000) + "\\n\u2026" : result;
1440
+ }
1441
+ function camerasEqual(left, right) {
1442
+ return Boolean(left && right) &&
1443
+ Math.abs(left.center[0] - right.center[0]) < 1e-7 &&
1444
+ Math.abs(left.center[1] - right.center[1]) < 1e-7 &&
1445
+ Math.abs(left.zoom - right.zoom) < 1e-7 &&
1446
+ Math.abs(left.bearing - right.bearing) < 1e-7 &&
1447
+ Math.abs(left.pitch - right.pitch) < 1e-7;
1448
+ }
1449
+ function formatCurveValue(value) {
1450
+ if (typeof value === "number" && Number.isFinite(value)) return formatNumber(value);
1451
+ return typeof value === "string" ? value : boundedJson(value);
1452
+ }
1453
+ function singleQueryValue(query, name) {
1454
+ const values = query.getAll(name);
1455
+ return values.length === 1 ? values[0] : undefined;
1456
+ }
1457
+ function validPortableName(value) {
1458
+ return typeof value === "string" &&
1459
+ /^[a-z][a-z0-9-]{0,63}$/u.test(value) &&
1460
+ value !== "constructor" && value !== "prototype" &&
1461
+ !/^(?:aux|con|nul|prn|com[1-9]|lpt[1-9])$/u.test(value);
1462
+ }
1463
+ function validConcreteThemeName(value) { return validPortableName(value) && value !== "system"; }
1464
+ function quoteCliArgument(value) {
1465
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/u.test(value)) return value;
1466
+ const quote = String.fromCharCode(39);
1467
+ const escapedQuote = quote + '"' + quote + '"' + quote;
1468
+ return quote + value.replaceAll(quote, escapedQuote) + quote;
1469
+ }
1470
+ function safeErrorMessage(error) { return error instanceof Error ? error.message : String(error || "Unknown error"); }
1471
+ function boundedNumber(value, minimum, maximum, fallback) { return Number.isFinite(value) ? Math.max(minimum, Math.min(maximum, value)) : fallback; }
1472
+ function boundedInteger(value, minimum, maximum, fallback) { return Number.isInteger(value) && value >= minimum && value <= maximum ? value : fallback; }
1473
+ function roundNumber(value) { return Number(value.toFixed(6)); }
1474
+ function formatNumber(value) { return String(roundNumber(value)); }
1475
+ function wrapLongitude(value) { return ((value + 180) % 360 + 360) % 360 - 180; }
1476
+ function setFeedback(value) { feedback.textContent = value; setTimeout(() => { if (feedback.textContent === value) feedback.textContent = ""; }, 2500); }
1477
+ </script>
1478
+ </body>
1479
+ </html>`;
1480
+ }
1481
+ function serializeInlineJson(value) {
1482
+ return JSON.stringify(value).replace(
1483
+ /[<>&\u2028\u2029]/gu,
1484
+ (character) => `\\u${character.codePointAt(0).toString(16).padStart(4, "0")}`
1485
+ );
1486
+ }
1487
+ function escapeHtml(value) {
1488
+ return value.replace(/[&<>"']/gu, (character) => {
1489
+ switch (character) {
1490
+ case "&":
1491
+ return "&amp;";
1492
+ case "<":
1493
+ return "&lt;";
1494
+ case ">":
1495
+ return "&gt;";
1496
+ case '"':
1497
+ return "&quot;";
1498
+ default:
1499
+ return "&#39;";
1500
+ }
1501
+ });
1502
+ }
1503
+
1504
+ // src/comparison.ts
1505
+ var tileflowComparisonSchemaVersion = 1;
1506
+ function createTileflowComparisonRequestHandler(options) {
1507
+ const comparisonBasePath = normalizeTileflowBasePath(options.basePath);
1508
+ const left = normalizeSide(options.left, "left", comparisonBasePath);
1509
+ const right = normalizeSide(options.right, "right", comparisonBasePath);
1510
+ assertDisjointSideRoutes(left.basePath, right.basePath);
1511
+ const title = normalizeLabel(options.title ?? "Tileflow comparison", "title");
1512
+ const initialMode = options.initialMode ?? "side-by-side";
1513
+ return async function handleTileflowComparisonRequest(request) {
1514
+ const url = new URL(request.url);
1515
+ const pathname = url.pathname;
1516
+ if (matchesBasePath(pathname, left.basePath)) return await left.handler(request);
1517
+ if (matchesBasePath(pathname, right.basePath)) return await right.handler(request);
1518
+ if (isComparisonRoot(pathname, comparisonBasePath)) {
1519
+ if (request.method !== "GET" && request.method !== "HEAD") {
1520
+ return jsonResponse({ error: "Method not allowed" }, 405);
1521
+ }
1522
+ const cspNonce = randomBytes(18).toString("base64");
1523
+ const html = renderTileflowComparisonHtml({
1524
+ basePath: comparisonBasePath,
1525
+ cspNonce,
1526
+ initialMode,
1527
+ left: publicSide(left),
1528
+ right: publicSide(right),
1529
+ schemaVersion: tileflowComparisonSchemaVersion,
1530
+ title
1531
+ });
1532
+ return new Response(request.method === "HEAD" ? null : html, {
1533
+ headers: {
1534
+ "Cache-Control": "no-store",
1535
+ "Content-Security-Policy": comparisonContentSecurityPolicy(cspNonce),
1536
+ "Content-Type": "text/html; charset=utf-8",
1537
+ "Referrer-Policy": "no-referrer",
1538
+ "X-Content-Type-Options": "nosniff"
1539
+ }
1540
+ });
1541
+ }
1542
+ return jsonResponse({ error: "Not found" }, 404);
1543
+ };
1544
+ }
1545
+ function comparisonContentSecurityPolicy(nonce) {
1546
+ return [
1547
+ "default-src 'none'",
1548
+ "base-uri 'none'",
1549
+ "connect-src 'self'",
1550
+ "font-src 'self'",
1551
+ "form-action 'none'",
1552
+ "frame-ancestors 'none'",
1553
+ "frame-src 'self'",
1554
+ "img-src 'self' blob: data:",
1555
+ "object-src 'none'",
1556
+ `script-src 'nonce-${nonce}'`,
1557
+ `style-src-elem 'nonce-${nonce}'`,
1558
+ "style-src-attr 'unsafe-inline'"
1559
+ ].join("; ");
1560
+ }
1561
+ function normalizeSide(side, name, comparisonBasePath) {
1562
+ const basePath = normalizeTileflowBasePath(side.basePath);
1563
+ if (!basePath) {
1564
+ throw new TypeError(`Tileflow comparison ${name} basePath must not own the server root.`);
1565
+ }
1566
+ if (comparisonBasePath && !matchesBasePath(basePath, comparisonBasePath)) {
1567
+ throw new TypeError(
1568
+ `Tileflow comparison ${name} basePath must be inside ${comparisonBasePath}.`
1569
+ );
1570
+ }
1571
+ if (typeof side.handler !== "function") {
1572
+ throw new TypeError(`Tileflow comparison ${name} handler must be a function.`);
1573
+ }
1574
+ return {
1575
+ ...side,
1576
+ basePath,
1577
+ ...side.captureConfig === void 0 ? {} : { captureConfig: normalizeCaptureConfig(side.captureConfig, `${name} captureConfig`) },
1578
+ eventsUrl: `${basePath}/__events`,
1579
+ label: normalizeLabel(side.label, `${name} label`),
1580
+ previewUrl: normalizeOwnedUrl(side.previewUrl, basePath, `${name} previewUrl`),
1581
+ ...side.sidecarUrl ? { sidecarUrl: normalizeOwnedUrl(side.sidecarUrl, basePath, `${name} sidecarUrl`) } : {},
1582
+ statusUrl: `${basePath}/__status`
1583
+ };
1584
+ }
1585
+ function publicSide(side) {
1586
+ return {
1587
+ basePath: side.basePath,
1588
+ ...side.captureConfig ? { captureConfig: side.captureConfig } : {},
1589
+ eventsUrl: side.eventsUrl,
1590
+ label: side.label,
1591
+ previewUrl: side.previewUrl,
1592
+ ...side.sidecarUrl ? { sidecarUrl: side.sidecarUrl } : {},
1593
+ statusUrl: side.statusUrl
1594
+ };
1595
+ }
1596
+ function normalizeCaptureConfig(value, label) {
1597
+ if (typeof value !== "string" || value.length === 0 || value.length > 4096 || value !== value.trim() || value.startsWith("/") || value.includes("\\") || /^[A-Za-z]:/u.test(value) || /[\p{Cc}]/u.test(value)) {
1598
+ throw new TypeError(`Invalid Tileflow comparison ${label}.`);
1599
+ }
1600
+ const segments = value.split("/");
1601
+ if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
1602
+ throw new TypeError(
1603
+ `Tileflow comparison ${label} must be a normalized path inside the current working directory.`
1604
+ );
1605
+ }
1606
+ return value;
1607
+ }
1608
+ function normalizeOwnedUrl(value, basePath, label) {
1609
+ if (typeof value !== "string" || value.length === 0 || value.length > 4096 || value !== value.trim() || !value.startsWith("/") || value.includes("\\") || /[\p{Cc}]/u.test(value)) {
1610
+ throw new TypeError(`Invalid Tileflow comparison ${label}.`);
1611
+ }
1612
+ let parsed;
1613
+ try {
1614
+ parsed = new URL(value, "http://tileflow.local");
1615
+ } catch {
1616
+ throw new TypeError(`Invalid Tileflow comparison ${label}.`);
1617
+ }
1618
+ if (parsed.origin !== "http://tileflow.local" || parsed.hash || !matchesBasePath(parsed.pathname, basePath)) {
1619
+ throw new TypeError(`Tileflow comparison ${label} must remain below ${basePath}.`);
1620
+ }
1621
+ return `${parsed.pathname}${parsed.search}`;
1622
+ }
1623
+ function normalizeLabel(value, label) {
1624
+ if (typeof value !== "string" || value.length === 0 || value.length > 160 || value !== value.trim() || /[\p{Cc}]/u.test(value)) {
1625
+ throw new TypeError(`Invalid Tileflow comparison ${label}.`);
1626
+ }
1627
+ return value.split(" \xB7 ").map((part, index) => index > 0 && /[\\/]/u.test(part) ? part.split(/[\\/]/u).at(-1) : part).join(" \xB7 ");
1628
+ }
1629
+ function assertDisjointSideRoutes(left, right) {
1630
+ if (matchesBasePath(left, right) || matchesBasePath(right, left)) {
1631
+ throw new TypeError("Tileflow comparison side base paths must be disjoint.");
1632
+ }
1633
+ }
1634
+ function isComparisonRoot(pathname, basePath) {
1635
+ return basePath ? pathname === basePath || pathname === `${basePath}/` : pathname === "/";
1636
+ }
1637
+ function matchesBasePath(pathname, basePath) {
1638
+ return pathname === basePath || pathname.startsWith(`${basePath}/`);
1639
+ }
1640
+ function jsonResponse(body, status) {
1641
+ return new Response(`${JSON.stringify(body, null, 2)}
1642
+ `, {
1643
+ status,
1644
+ headers: {
1645
+ "Cache-Control": "no-store",
1646
+ "Content-Type": "application/json; charset=utf-8",
1647
+ "X-Content-Type-Options": "nosniff"
1648
+ }
1649
+ });
1650
+ }
1651
+
1652
+ // src/server.ts
1653
+ function getTileflowStyleSelection(path) {
1654
+ const match = /^\/styles\/([a-z][a-z0-9-]{0,63})\/([a-z][a-z0-9-]{0,63})\.json$/u.exec(path);
1655
+ return match?.[1] && match[2] && tileflowMapIdSchema.safeParse(match[1]).success && tileflowThemeNameSchema.safeParse(match[2]).success ? { mapName: match[1], themeName: match[2] } : void 0;
1656
+ }
1657
+ function getTileflowStyleInspectionSelection(path) {
1658
+ const match = /^\/__inspection\/([a-z][a-z0-9-]{0,63})\/([a-z][a-z0-9-]{0,63})\.json$/u.exec(
1659
+ path
1660
+ );
1661
+ return match?.[1] && match[2] && tileflowMapIdSchema.safeParse(match[1]).success && tileflowThemeNameSchema.safeParse(match[2]).success ? { mapName: match[1], themeName: match[2] } : void 0;
1662
+ }
1663
+ function isTileflowRequestUrl(url, basePath) {
1664
+ return getTileflowRequestPath(url, basePath) !== null;
1665
+ }
1666
+ function getTileflowRequestPath(url, basePath) {
1667
+ const pathname = getRequestPathname(url);
1668
+ if (pathname === null) return null;
1669
+ const path = stripTileflowBasePath(pathname, normalizeTileflowBasePath(basePath));
1670
+ return path !== null && isOwnedTileflowRequestPath(path) ? path : null;
1671
+ }
1672
+ function createTileflowNodeRequest(request) {
1673
+ const protocol = getNodeHeader(request, "x-forwarded-proto") ?? "http";
1674
+ const host = getNodeHeader(request, "host") ?? "localhost";
1675
+ return new Request(`${protocol}://${host}${request.url ?? "/"}`, {
1676
+ method: request.method
1677
+ });
1678
+ }
1679
+ async function writeTileflowNodeResponse(response, tileflowResponse) {
1680
+ response.statusCode = tileflowResponse.status;
1681
+ tileflowResponse.headers.forEach((value, key) => {
1682
+ response.setHeader(key, value);
1683
+ });
1684
+ if (!tileflowResponse.body) {
1685
+ response.end();
1686
+ return;
1687
+ }
1688
+ const reader = tileflowResponse.body.getReader();
1689
+ try {
1690
+ while (true) {
1691
+ const { done, value } = await reader.read();
1692
+ if (done) break;
1693
+ if (response.destroyed) {
1694
+ await reader.cancel();
1695
+ return;
1696
+ }
1697
+ response.write(Buffer.from(value));
1698
+ }
1699
+ response.end();
1700
+ } finally {
1701
+ reader.releaseLock();
1702
+ }
1703
+ }
1704
+ function createTileflowDevRequestHandler(options = {}) {
1705
+ const basePath = normalizeTileflowBasePath(options.basePath);
1706
+ const configPath = options.config ?? defaultTileflowConfigPath;
1707
+ const cwd = options.cwd ?? process.cwd();
1708
+ const apiBaseUrl = options.apiBaseUrl ?? defaultTileflowApiUrl;
1709
+ return async function handleTileflowDevRequest(request) {
1710
+ if (request.method !== "GET" && request.method !== "HEAD") {
1711
+ return jsonResponse2({ error: "Method not allowed" }, 405);
1712
+ }
1713
+ const url = new URL(request.url);
1714
+ const path = getTileflowRequestPath(url.pathname, basePath);
1715
+ if (path === null) {
1716
+ return jsonResponse2({ error: "Not found" }, 404);
1717
+ }
1718
+ try {
1719
+ if (path === "/__events") {
1720
+ return options.session ? eventStreamResponse(options.session) : jsonResponse2({ error: "Live events require a development session." }, 404);
1721
+ }
1722
+ if (path.startsWith("/__runtime/")) {
1723
+ const runtimeResponse = (await import("./preview-assets-6LYL7GP7.js")).getTileflowPreviewRuntimeResponse(path);
1724
+ if (runtimeResponse) return runtimeResponse;
1725
+ }
1726
+ let artifacts;
1727
+ let state;
1728
+ if (options.session) {
1729
+ artifacts = getSessionArtifacts(options.session);
1730
+ state = options.session.getState();
1731
+ } else {
1732
+ artifacts = await createTileflowBuildArtifacts({
1733
+ assetBaseUrl: `${url.origin}${basePath}`,
1734
+ config: configPath,
1735
+ cwd,
1736
+ inspection: options.inspection,
1737
+ styleBaseUrl: options.styleBaseUrl ?? `${url.origin}${basePath}`,
1738
+ apiBaseUrl
1739
+ });
1740
+ state = {
1741
+ artifacts,
1742
+ generation: 1,
1743
+ lastGoodGeneration: 1,
1744
+ status: "ready"
1745
+ };
1746
+ }
1747
+ if (path === "/__status") {
1748
+ return jsonResponse2(createTileflowArtifactStatus(state));
1749
+ }
1750
+ if (path === "/" || path === "") {
1751
+ const { renderTileflowPreviewHtml, resolveTileflowPreview } = await import("./preview.js");
1752
+ const requestedMaps = url.searchParams.getAll("map");
1753
+ const requestedThemes = url.searchParams.getAll("theme");
1754
+ if (requestedMaps.length > 1) {
1755
+ return jsonResponse2({ error: "Tileflow map query must appear at most once." }, 400);
1756
+ }
1757
+ if (requestedThemes.length > 1) {
1758
+ return jsonResponse2({ error: "Tileflow theme query must appear at most once." }, 400);
1759
+ }
1760
+ const requestedMap = requestedMaps.length === 1 ? requestedMaps[0] : options.map;
1761
+ const requestedTheme = requestedThemes.length === 1 ? requestedThemes[0] : options.theme;
1762
+ const preview = artifacts ? resolveTileflowPreview(artifacts.project, {
1763
+ map: requestedMap,
1764
+ scene: options.scene,
1765
+ theme: requestedTheme
1766
+ }) : void 0;
1767
+ const previewStyle = preview ? artifacts?.styles[preview.mapName]?.[preview.themeName] : void 0;
1768
+ const isStreetsPreview = preview !== void 0 && previewStyle?.metadata?.["tileflow:root"] === "streets";
1769
+ const fontFaces = previewStyle ? getTileflowStyleFontFaces(previewStyle) : [];
1770
+ return htmlResponse(
1771
+ renderTileflowPreviewHtml(
1772
+ preview,
1773
+ basePath,
1774
+ createTileflowArtifactStatus(state),
1775
+ isStreetsPreview,
1776
+ fontFaces
1777
+ )
1778
+ );
1779
+ }
1780
+ if (!artifacts) return unavailableArtifactsResponse(state);
1781
+ if (path.startsWith("/generations/") && "files" in artifacts) {
1782
+ const fileName = path.replace(/^\/+/, "");
1783
+ const asset = artifacts.files?.find((candidate) => candidate.fileName === fileName);
1784
+ if (!asset) return jsonResponse2({ error: `Unknown generation asset: ${fileName}` }, 404);
1785
+ return assetResponse(asset);
1786
+ }
1787
+ if (path === "/manifest.json") {
1788
+ return jsonResponse2(artifacts.manifest);
1789
+ }
1790
+ if (path === "/build-manifest.json") {
1791
+ return jsonResponse2(artifacts.buildManifest);
1792
+ }
1793
+ if (path.startsWith("/__inspection/")) {
1794
+ const selection = getTileflowStyleInspectionSelection(path);
1795
+ const inspection = selection ? artifacts.styleInspections?.[selection.mapName]?.[selection.themeName] : void 0;
1796
+ if (!selection || !inspection) {
1797
+ return jsonResponse2({ error: "Compiler inspection is unavailable." }, 404);
1798
+ }
1799
+ return compactJsonResponse(inspection);
1800
+ }
1801
+ if (path.startsWith("/styles/")) {
1802
+ const selection = getTileflowStyleSelection(path);
1803
+ const style = selection ? artifacts.styles[selection.mapName]?.[selection.themeName] : void 0;
1804
+ if (!selection || !style) {
1805
+ return jsonResponse2({ error: "Unknown map theme style." }, 404);
1806
+ }
1807
+ return compactJsonResponse(style);
1808
+ }
1809
+ if (path.startsWith("/icons/")) {
1810
+ const assetName = path.replace(/^\/+/, "");
1811
+ const asset = artifacts.assets.find((candidate) => candidate.fileName === assetName);
1812
+ if (!asset) {
1813
+ return jsonResponse2({ error: `Unknown icon asset: ${assetName}` }, 404);
1814
+ }
1815
+ return assetResponse(asset);
1816
+ }
1817
+ if (path.startsWith("/fonts/")) {
1818
+ const assetName = path.replace(/^\/+/, "");
1819
+ const asset = artifacts.assets.find((candidate) => candidate.fileName === assetName);
1820
+ if (!asset) {
1821
+ return jsonResponse2({ error: `Unknown font asset: ${assetName}` }, 404);
1822
+ }
1823
+ return assetResponse(asset);
1824
+ }
1825
+ return jsonResponse2({ error: "Not found" }, 404);
1826
+ } catch (error) {
1827
+ options.onError?.(error);
1828
+ return tileflowErrorResponse(error);
1829
+ }
1830
+ };
1831
+ }
1832
+ function createTileflowArtifactStatus(state) {
1833
+ return {
1834
+ schemaVersion: tileflowArtifactSessionSchemaVersion,
1835
+ generation: state.generation,
1836
+ status: state.status,
1837
+ ..."lastGoodGeneration" in state && state.lastGoodGeneration !== void 0 ? { lastGoodGeneration: state.lastGoodGeneration } : {},
1838
+ ...state.status === "invalid" ? { diagnostics: state.diagnostics } : {}
1839
+ };
1840
+ }
1841
+ function getSessionArtifacts(session) {
1842
+ const state = session.getState();
1843
+ return state.status === "ready" ? state.artifacts : session.getLastGoodArtifacts();
1844
+ }
1845
+ function unavailableArtifactsResponse(state) {
1846
+ return jsonResponse2(
1847
+ {
1848
+ error: "No valid Tileflow artifact generation is available.",
1849
+ ...createTileflowArtifactStatus(state)
1850
+ },
1851
+ 409
1852
+ );
1853
+ }
1854
+ function eventStreamResponse(session) {
1855
+ const encoder = new TextEncoder();
1856
+ let unsubscribe = () => {
1857
+ };
1858
+ const stream = new ReadableStream({
1859
+ start(controller) {
1860
+ const enqueue = (state) => {
1861
+ const status = createTileflowArtifactStatus(state);
1862
+ controller.enqueue(
1863
+ encoder.encode(`event: ${status.status}
1864
+ data: ${JSON.stringify(status)}
1865
+
1866
+ `)
1867
+ );
1868
+ };
1869
+ enqueue(session.getState());
1870
+ unsubscribe = session.subscribe(enqueue);
1871
+ },
1872
+ cancel() {
1873
+ unsubscribe();
1874
+ }
1875
+ });
1876
+ return new Response(stream, {
1877
+ headers: {
1878
+ "Access-Control-Allow-Origin": "*",
1879
+ "Cache-Control": "no-cache, no-transform",
1880
+ Connection: "keep-alive",
1881
+ "Content-Type": "text/event-stream; charset=utf-8",
1882
+ "X-Accel-Buffering": "no"
1883
+ }
1884
+ });
1885
+ }
1886
+ function getNodeHeader(request, name) {
1887
+ const value = request.headers[name];
1888
+ return Array.isArray(value) ? value[0] : value;
1889
+ }
1890
+ function stripTileflowBasePath(pathname, basePath) {
1891
+ if (!basePath) {
1892
+ return pathname;
1893
+ }
1894
+ if (pathname === basePath) {
1895
+ return "/";
1896
+ }
1897
+ if (pathname.startsWith(`${basePath}/`)) {
1898
+ return pathname.slice(basePath.length);
1899
+ }
1900
+ return null;
1901
+ }
1902
+ function getRequestPathname(url) {
1903
+ try {
1904
+ return new URL(url ?? "/", "http://tileflow.local").pathname;
1905
+ } catch {
1906
+ return null;
1907
+ }
1908
+ }
1909
+ function isOwnedTileflowRequestPath(path) {
1910
+ return path === "/" || path === "/build-manifest.json" || path === "/manifest.json" || path === "/__events" || path === "/__status" || getTileflowStyleInspectionSelection(path) !== void 0 || path.startsWith("/generations/") || path.startsWith("/icons/") || getTileflowStyleSelection(path) !== void 0 || path.startsWith("/fonts/") || path.startsWith("/__runtime/");
1911
+ }
1912
+ function tileflowErrorResponse(error) {
1913
+ if (error instanceof Error && "code" in error && error.code === "PREVIEW_SELECTION_INVALID") {
1914
+ return jsonResponse2({ error: error.message }, 400);
1915
+ }
1916
+ if (error instanceof TileflowStyleValidationError) {
1917
+ return jsonResponse2(
1918
+ {
1919
+ error: error.message,
1920
+ issues: error.issues
1921
+ },
1922
+ 400
1923
+ );
1924
+ }
1925
+ if (error instanceof TileflowValidationError) {
1926
+ return jsonResponse2(
1927
+ {
1928
+ error: error.message,
1929
+ messages: error.messages
1930
+ },
1931
+ 400
1932
+ );
1933
+ }
1934
+ const message = error instanceof Error ? error.message : "Unknown error";
1935
+ return jsonResponse2({ error: message }, 500);
1936
+ }
1937
+ function jsonResponse2(body, status = 200) {
1938
+ return new Response(`${JSON.stringify(body, null, 2)}
1939
+ `, {
1940
+ headers: {
1941
+ "Access-Control-Allow-Origin": "*",
1942
+ "Cache-Control": "no-store",
1943
+ "Content-Type": "application/json; charset=utf-8"
1944
+ },
1945
+ status
1946
+ });
1947
+ }
1948
+ function compactJsonResponse(body, status = 200) {
1949
+ return new Response(`${JSON.stringify(body)}
1950
+ `, {
1951
+ headers: {
1952
+ "Access-Control-Allow-Origin": "*",
1953
+ "Cache-Control": "no-store",
1954
+ "Content-Type": "application/json; charset=utf-8"
1955
+ },
1956
+ status
1957
+ });
1958
+ }
1959
+ function htmlResponse(body, status = 200) {
1960
+ return new Response(body, {
1961
+ headers: {
1962
+ "Access-Control-Allow-Origin": "*",
1963
+ "Cache-Control": "no-store",
1964
+ "Content-Type": "text/html; charset=utf-8"
1965
+ },
1966
+ status
1967
+ });
1968
+ }
1969
+ function assetResponse(asset) {
1970
+ const body = typeof asset.source === "string" ? asset.source : Buffer.from(asset.source);
1971
+ return new Response(body, {
1972
+ headers: {
1973
+ "Access-Control-Allow-Origin": "*",
1974
+ "Cache-Control": "no-store",
1975
+ "Content-Type": asset.contentType
1976
+ }
1977
+ });
1978
+ }
1979
+
1980
+ export {
1981
+ tileflowComparisonSchemaVersion,
1982
+ createTileflowComparisonRequestHandler,
1983
+ getTileflowStyleSelection,
1984
+ getTileflowStyleInspectionSelection,
1985
+ isTileflowRequestUrl,
1986
+ getTileflowRequestPath,
1987
+ createTileflowNodeRequest,
1988
+ writeTileflowNodeResponse,
1989
+ createTileflowDevRequestHandler,
1990
+ createTileflowArtifactStatus
1991
+ };