@tileflow/dev 0.1.0-alpha.4 → 0.1.0-alpha.40

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 (45) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +257 -5
  3. package/THIRD_PARTY_NOTICES.md +309 -0
  4. package/dist/artifacts.d.ts +269 -0
  5. package/dist/artifacts.js +98 -0
  6. package/dist/chunk-AQUGMUZW.js +324 -0
  7. package/dist/chunk-AVJMTXW5.js +3147 -0
  8. package/dist/chunk-C4VPHZ3F.js +4062 -0
  9. package/dist/chunk-D4OE4ITL.js +13 -0
  10. package/dist/chunk-FOMTSCOP.js +671 -0
  11. package/dist/chunk-KDFTE4Q5.js +156 -0
  12. package/dist/chunk-ONYQNLNA.js +1108 -0
  13. package/dist/chunk-RTU7TD4M.js +172 -0
  14. package/dist/chunk-XHJBCVBB.js +241 -0
  15. package/dist/config.d.ts +33 -0
  16. package/dist/config.js +27 -0
  17. package/dist/fonts.d.ts +47 -0
  18. package/dist/fonts.js +16 -0
  19. package/dist/icons-BIHpw5Zy.d.ts +417 -0
  20. package/dist/icons.d.ts +3 -0
  21. package/dist/icons.js +41 -0
  22. package/dist/inspect.d.ts +149 -0
  23. package/dist/inspect.js +805 -0
  24. package/dist/inspector-browser.js +16955 -0
  25. package/dist/inspector-observer.js +95 -0
  26. package/dist/preview-assets-ATLLYY72.js +103 -0
  27. package/dist/preview.d.ts +31 -0
  28. package/dist/preview.js +1972 -0
  29. package/dist/server.d.ts +89 -0
  30. package/dist/server.js +3657 -0
  31. package/dist/tilesets.d.ts +89 -0
  32. package/dist/tilesets.js +6 -0
  33. package/dist/validation.d.ts +65 -0
  34. package/dist/validation.js +24 -0
  35. package/package.json +77 -13
  36. package/ui-fonts/hubot-sans-OFL.txt +93 -0
  37. package/ui-fonts/hubot-sans.woff2 +0 -0
  38. package/ui-fonts/inter-OFL.txt +92 -0
  39. package/ui-fonts/inter.woff2 +0 -0
  40. package/ui-fonts/jetbrains-mono-OFL.txt +93 -0
  41. package/ui-fonts/jetbrains-mono.woff2 +0 -0
  42. package/ui-fonts/mona-sans-OFL.txt +93 -0
  43. package/ui-fonts/mona-sans.woff2 +0 -0
  44. package/dist/index.d.ts +0 -54
  45. package/dist/index.js +0 -314
package/dist/server.js ADDED
@@ -0,0 +1,3657 @@
1
+ import {
2
+ createTileflowArtifactDiagnostics,
3
+ createTileflowArtifactSession,
4
+ createTileflowLineBackgroundGlyphResponder,
5
+ parseTileflowLineBackgroundGlyphPath,
6
+ tileflowArtifactSessionSchemaVersion,
7
+ tileflowLineBackgroundGlyphRoute
8
+ } from "./chunk-AVJMTXW5.js";
9
+ import {
10
+ getTileflowAssetBasePath,
11
+ getTileflowAssetFileName,
12
+ joinTileflowPublicUrl,
13
+ normalizeTileflowBasePath,
14
+ resolveTileflowArtifactPublicUrls
15
+ } from "./chunk-FOMTSCOP.js";
16
+ import "./chunk-C4VPHZ3F.js";
17
+ import "./chunk-RTU7TD4M.js";
18
+ import {
19
+ TileflowStyleValidationError
20
+ } from "./chunk-KDFTE4Q5.js";
21
+ import {
22
+ TileflowValidationError,
23
+ defaultTileflowApiUrl,
24
+ defaultTileflowConfigPath
25
+ } from "./chunk-AQUGMUZW.js";
26
+ import "./chunk-D4OE4ITL.js";
27
+ import "./chunk-ONYQNLNA.js";
28
+
29
+ // src/server.ts
30
+ import { open } from "fs/promises";
31
+ import {
32
+ getTileflowStyleFontFaces,
33
+ tileflowMapIdSchema,
34
+ tileflowThemeNameSchema
35
+ } from "@tileflow/core";
36
+
37
+ // src/preview-controls.ts
38
+ import { parseResolvedTileflowMap, tileflowStyleFontFacesMetadataKey } from "@tileflow/core";
39
+ import { createStyleFromCatalogWithInspection } from "@tileflow/core/build";
40
+ var moduleNames = [
41
+ "land",
42
+ "water",
43
+ "nautical",
44
+ "roads",
45
+ "boundaries",
46
+ "labels",
47
+ "poi",
48
+ "aeroways",
49
+ "transit",
50
+ "addresses",
51
+ "landforms",
52
+ "buildings",
53
+ "vegetation"
54
+ ];
55
+ var fieldRules = {
56
+ "buildings.mode": ["flat", "3d"],
57
+ "buildings.threeDimensional.lighting": ["style", "sun"],
58
+ "buildings.threeDimensional.shadows": "boolean",
59
+ "buildings.threeDimensional.occlusion": "boolean",
60
+ "buildings.threeDimensional.windows": "boolean",
61
+ "buildings.threeDimensional.edges": [0, 1],
62
+ "buildings.threeDimensional.colorVariation": [0, 1],
63
+ "buildings.threeDimensional.nightLight": [0, 2],
64
+ "vegetation.mode": ["3d", "flat", "auto"],
65
+ "vegetation.fallback": ["flat", "none"],
66
+ "vegetation.visible": "boolean",
67
+ "vegetation.threeDimensional.finish": ["faceted", "smooth", "toon"],
68
+ "vegetation.threeDimensional.edges": [0, 1],
69
+ "vegetation.threeDimensional.heightScale": [0, 10],
70
+ "vegetation.threeDimensional.crownScale": [0, 10]
71
+ };
72
+ function record(value) {
73
+ return typeof value === "object" && value !== null && !Array.isArray(value);
74
+ }
75
+ function parsePreviewControls(text) {
76
+ if (text.length > 8192) throw new Error("Map controls exceed 8 KiB.");
77
+ const input = JSON.parse(text);
78
+ if (!record(input)) throw new Error("Map controls must be an object.");
79
+ for (const [module, patch] of Object.entries(input)) {
80
+ if (!moduleNames.includes(module) || !record(patch))
81
+ throw new Error("Unknown map control module: " + module);
82
+ const validate = (path, value) => {
83
+ const rule = path === module + ".enabled" ? "boolean" : fieldRules[path];
84
+ const valid = rule === "boolean" ? typeof value === "boolean" : rule && (typeof rule[0] === "number" ? typeof value === "number" && Number.isFinite(value) && value >= rule[0] && value <= rule[1] : typeof value === "string" && rule.includes(value));
85
+ if (!valid) throw new Error("Invalid map control: " + path);
86
+ };
87
+ for (const [key, value] of Object.entries(patch)) {
88
+ if (key === "threeDimensional" && record(value) && ["buildings", "vegetation"].includes(module)) {
89
+ for (const [nested, entry] of Object.entries(value))
90
+ validate(module + "." + key + "." + nested, entry);
91
+ } else validate(module + "." + key, value);
92
+ }
93
+ }
94
+ return input;
95
+ }
96
+ function describePreviewControls(artifacts, map, theme) {
97
+ const definition = artifacts.project.maps[map];
98
+ const style = artifacts.styles[map]?.[theme];
99
+ if (!definition || !style) throw new Error("Unknown map theme.");
100
+ const modules = definition.modules ?? {};
101
+ const metadata = style.metadata ?? {};
102
+ return {
103
+ map,
104
+ theme,
105
+ modules: moduleNames.map((id) => {
106
+ const config = modules[id] ?? { type: id };
107
+ const fields = [];
108
+ const record2 = config;
109
+ const add = (path, label, value, choices, min, max, step, inherit = false) => fields.push({
110
+ path,
111
+ label,
112
+ value,
113
+ ...choices ? { choices } : {},
114
+ ...inherit ? { inherit: true } : {},
115
+ ...min !== void 0 ? { min, max, step } : {}
116
+ });
117
+ if (id === "buildings") {
118
+ const design = metadata["tileflow:buildings"];
119
+ const authored = record2.threeDimensional;
120
+ const value = (key, fallback) => {
121
+ const resolved = design?.[key] ?? authored?.[key];
122
+ return typeof resolved === typeof fallback ? resolved : fallback;
123
+ };
124
+ add("threeDimensional.lighting", "Building light", value("lighting", "style"), [
125
+ "style",
126
+ "sun"
127
+ ]);
128
+ for (const [key, label] of [
129
+ ["shadows", "Cast shadows"],
130
+ ["occlusion", "Contact shadows"],
131
+ ["windows", "Night windows"]
132
+ ])
133
+ add("threeDimensional." + key, label, value(key, false));
134
+ for (const [key, label] of [
135
+ ["edges", "Edge definition"],
136
+ ["colorVariation", "Color variation"],
137
+ ["nightLight", "Night glow"]
138
+ ])
139
+ add(
140
+ "threeDimensional." + key,
141
+ label,
142
+ value(key, 0),
143
+ void 0,
144
+ 0,
145
+ key === "nightLight" ? 2 : 1,
146
+ 0.05
147
+ );
148
+ }
149
+ if (id === "vegetation") {
150
+ add(
151
+ "mode",
152
+ "Tree representation",
153
+ String(record2.mode ?? "3d"),
154
+ ["3d", "auto", "flat"],
155
+ void 0,
156
+ void 0,
157
+ void 0,
158
+ true
159
+ );
160
+ add("fallback", "Without 3D support", String(record2.fallback ?? "flat"), ["flat", "none"]);
161
+ add("visible", "Visible by default", record2.visible !== false);
162
+ const authored = record2.threeDimensional;
163
+ const design = metadata["tileflow:vegetation"];
164
+ add("threeDimensional.finish", "Tree finish", String(authored?.finish ?? "faceted"), [
165
+ "faceted",
166
+ "smooth",
167
+ "toon"
168
+ ]);
169
+ for (const [key, label] of [
170
+ ["heightScale", "Tree height"],
171
+ ["crownScale", "Crown size"]
172
+ ]) {
173
+ const value = design?.[key] ?? authored?.[key];
174
+ add(
175
+ "threeDimensional." + key,
176
+ label,
177
+ typeof value === "number" ? value : 1,
178
+ void 0,
179
+ 0,
180
+ 10,
181
+ 0.1
182
+ );
183
+ }
184
+ }
185
+ return { id, enabled: record2.enabled !== false, fields };
186
+ })
187
+ };
188
+ }
189
+ function compilePreviewControls(artifacts, map, theme, text, apiBaseUrl) {
190
+ if (artifacts.nativeBuild) throw new Error("Map controls require a web preview.");
191
+ const overrides = parsePreviewControls(text);
192
+ const definition = artifacts.project.maps[map];
193
+ const base = artifacts.styles[map]?.[theme];
194
+ if (!definition || !base) throw new Error("Unknown map theme.");
195
+ const modules = { ...definition.modules };
196
+ for (const [id, patch] of Object.entries(overrides)) {
197
+ const key = id;
198
+ const original = modules[key] ?? { type: id };
199
+ const merged = { ...original, ...patch };
200
+ if ("threeDimensional" in patch && patch.threeDimensional)
201
+ Object.assign(merged, {
202
+ threeDimensional: {
203
+ ..."threeDimensional" in original ? original.threeDimensional : {},
204
+ ...patch.threeDimensional
205
+ }
206
+ });
207
+ Object.assign(modules, { [id]: merged });
208
+ }
209
+ const resolved = parseResolvedTileflowMap({ ...definition, modules });
210
+ const result = createStyleFromCatalogWithInspection(
211
+ { ...artifacts.project, maps: { ...artifacts.project.maps, [map]: resolved } },
212
+ map,
213
+ {
214
+ theme,
215
+ apiBaseUrl,
216
+ preparedAssets: artifacts.mapAssets?.[map]
217
+ }
218
+ );
219
+ result.style.sources = Object.fromEntries(
220
+ Object.entries(result.style.sources).map(([id, source]) => [id, base.sources[id] ?? source])
221
+ );
222
+ if (base.glyphs) result.style.glyphs = base.glyphs;
223
+ if (base.sprite) result.style.sprite = base.sprite;
224
+ if (base.metadata?.[tileflowStyleFontFacesMetadataKey])
225
+ result.style.metadata = {
226
+ ...result.style.metadata,
227
+ [tileflowStyleFontFacesMetadataKey]: base.metadata[tileflowStyleFontFacesMetadataKey]
228
+ };
229
+ return result;
230
+ }
231
+
232
+ // src/comparison.ts
233
+ import { randomBytes } from "crypto";
234
+ import { readFile } from "fs/promises";
235
+
236
+ // src/inspector/card-styles.ts
237
+ var cardStyles = `
238
+ .inspector-ready .inspector-shell {
239
+ --inspector-page:color-mix(in srgb,var(--surface) 80%,var(--border));
240
+ --inspector-card-heading:color-mix(in srgb,var(--paper) 55%,var(--surface));
241
+ --inspector-content-gap:12px;
242
+ }
243
+ .inspector-ready .inspector-shell :is(.inspector-card-page,.inspector-layer-content,.inspector-tile-content,.inspector-diagnostics-list) {
244
+ padding:14px; background:var(--inspector-page);
245
+ }
246
+ .inspector-ready .inspector-shell :is(.inspector-card,.inspector-layer-evidence,.inspector-layer-neighbors,.inspector-layer-section,.inspector-layer-origin,.inspector-layer-samples,.inspector-tile-content > .inspector-kv,.inspector-tile-feature,.inspector-diagnostic,.inspector-diagnostics-empty) {
247
+ min-width:0; margin:0 0 var(--inspector-content-gap); border:1px solid var(--border); border-radius:10px; background:var(--paper);
248
+ }
249
+ .inspector-ready .inspector-shell .inspector-card { padding:0; overflow:hidden; }
250
+ .inspector-ready .inspector-shell :is(.inspector-card-page,.inspector-layer-content,.inspector-tile-content,.inspector-diagnostics-list) > :last-child { margin-bottom:0; }
251
+ .inspector-ready .inspector-shell :is(.inspector-card > .inspector-section-heading,.inspector-card > .controls-section-title,.inspector-layer-neighbors > strong) {
252
+ display:block; margin:0; padding:10px 14px; border:0; border-bottom:1px solid var(--border); background:var(--inspector-card-heading);
253
+ color:var(--ink); font:600 12px/1.4 var(--ui); letter-spacing:0; text-transform:none;
254
+ }
255
+ .inspector-ready .inspector-shell .inspector-card-table { overflow:auto; }
256
+ .inspector-ready .inspector-shell .inspector-card-table :is(.inspector-table,.inspector-layer-rules) { margin:0; }
257
+ .inspector-ready .inspector-shell .inspector-card-table th { background:var(--inspector-card-heading); padding:9px 14px; text-transform:none; letter-spacing:0; }
258
+ .inspector-ready .inspector-shell .inspector-card-table td { padding:9px 14px; height:36px; border-bottom-color:var(--border); }
259
+ .inspector-ready .inspector-shell :is(.inspector-card-table,.inspector-layer-samples) tr:last-child td { border-bottom:0; }
260
+ .inspector-ready .inspector-shell .inspector-subtabs { padding-inline:14px; background:var(--paper); }
261
+
262
+ /* Map: each existing section is one card, with the enabled module fields kept inline. */
263
+ .inspector-ready .inspector-shell .inspector-scene-content { max-width:none; background:var(--inspector-page); }
264
+ .inspector-ready .inspector-shell .map-controls { padding:14px!important; background:var(--inspector-page); }
265
+ .inspector-ready .inspector-shell .controls-grid { display:block; border:0; padding:0; }
266
+ .inspector-ready .inspector-shell :is(.controls-selection,.controls-runtime) { border:1px solid var(--border); padding:0; }
267
+ .inspector-ready .inspector-shell .controls-selection[hidden] { display:none; }
268
+ .inspector-ready .inspector-shell .controls-section-title { margin:0; }
269
+ .inspector-ready .inspector-shell .map-controls .control-field {
270
+ grid-template-columns:minmax(120px,180px) minmax(0,1fr); min-height:44px; padding:9px 18px; gap:16px;
271
+ }
272
+ .inspector-ready .inspector-shell .map-controls .control-field.inspector-palette-field { grid-template-columns:minmax(120px,180px) minmax(0,1fr); }
273
+ .inspector-ready .inspector-shell :is(.controls-selection,.controls-runtime) > .control-field:last-child { border-bottom:0; }
274
+ .inspector-ready .inspector-shell .control-module { background:transparent; }
275
+ .inspector-ready .inspector-shell .control-module:last-child { border-bottom:0; }
276
+ .inspector-ready .inspector-shell .control-module-header { min-height:44px; padding:10px 18px; }
277
+ .inspector-ready .inspector-shell .control-module-header label { gap:10px; }
278
+ .inspector-ready .inspector-shell .control-module-header input { margin-right:0; }
279
+ .inspector-ready .inspector-shell .control-module fieldset { padding:0; border-top:1px solid var(--border); background:color-mix(in srgb,var(--paper) 65%,var(--surface)); }
280
+ .inspector-ready .inspector-shell .control-module .control-field { padding-left:54px; border-bottom:1px solid var(--border); }
281
+ .inspector-ready .inspector-shell .control-module .control-field:last-child { border-bottom:0; }
282
+ .inspector-ready .inspector-shell .map-controls :is(select,input[type=number]) { border-radius:5px; }
283
+ .inspector-ready .inspector-shell :is(.controls-segments,.inspector-palettes button) { border-radius:5px; }
284
+ .inspector-ready .inspector-shell .inspector-palettes { gap:6px; flex-wrap:wrap; }
285
+ .inspector-ready .inspector-shell .inspector-palettes button { height:auto; min-height:30px; padding:4px 8px; }
286
+
287
+ /* Layers: preserve the list and tabs, frame each detail block as a card. */
288
+ .inspector-ready .inspector-shell .inspector-layer-content { container-type:inline-size; }
289
+ .inspector-ready .inspector-shell :is(.inspector-layer-evidence,.inspector-layer-neighbors) { padding:0; overflow:hidden; }
290
+ .inspector-ready .inspector-shell .inspector-layer-evidence-row { padding:10px 14px; grid-template-columns:16px 110px minmax(0,1fr); }
291
+ .inspector-ready .inspector-shell .inspector-layer-evidence-row:last-child { border-bottom:0; }
292
+ .inspector-ready .inspector-shell .inspector-layer-neighbors > div { margin:0; padding:9px 14px; border-bottom:1px solid var(--border); }
293
+ .inspector-ready .inspector-shell .inspector-layer-neighbors > div:last-child { border-bottom:0; }
294
+ .inspector-ready .inspector-shell :is(.inspector-layer-section,.inspector-layer-origin) { padding:12px 14px; }
295
+ .inspector-ready .inspector-shell .inspector-layer-section h4 { color:var(--ink); font:600 12px/1.4 var(--ui); text-transform:none; letter-spacing:0; }
296
+ .inspector-ready .inspector-shell .inspector-layer-content > .inspector-tools { margin-bottom:12px; padding:0 0 8px; border:0; background:transparent; }
297
+ .inspector-ready .inspector-shell .inspector-layer-content > .inspector-note { padding:0 2px 12px; }
298
+ .inspector-ready .inspector-shell .inspector-layer-samples { max-height:240px; }
299
+ .inspector-ready .inspector-shell .inspector-layer-samples td { padding:8px 12px; }
300
+ .inspector-ready .inspector-shell .inspector-layer-group-title { height:30px; min-height:30px; background:var(--inspector-card-heading)!important; }
301
+ .inspector-ready .inspector-shell .inspector-layer-group-title strong { text-transform:capitalize; letter-spacing:0; }
302
+ .inspector-ready .inspector-shell .inspector-layer-row { grid-template-columns:18px 26px minmax(0,1fr) 52px; height:36px; min-height:36px; gap:6px; }
303
+ .inspector-ready .inspector-shell .inspector-layer-glyph { display:grid; place-items:center; width:26px; height:26px; border:1px solid var(--border); border-radius:6px; background:var(--surface); }
304
+ @container(max-width:400px) {
305
+ .inspector-ready .inspector-shell .inspector-layer-evidence-row { grid-template-columns:16px minmax(0,1fr); gap:4px 8px; }
306
+ .inspector-ready .inspector-shell .inspector-layer-evidence-row > :last-child { grid-column:2; }
307
+ }
308
+
309
+ /* Tiles: keep whole tables together and allow long layer names to wrap. */
310
+ .inspector-ready .inspector-shell .inspector-tile-content > .inspector-note { padding:0 2px 10px; }
311
+ .inspector-ready .inspector-shell .inspector-tile-content .inspector-table td { vertical-align:top; white-space:normal; overflow-wrap:anywhere; }
312
+ .inspector-ready .inspector-shell .inspector-tile-content .inspector-tile-chip { display:inline-block; margin:2px 4px 2px 0; max-width:100%; white-space:normal; overflow-wrap:anywhere; text-align:left; border-radius:4px; font-size:10px; }
313
+ .inspector-ready .inspector-shell .inspector-tile-content > .inspector-kv { padding:0; gap:0; overflow:hidden; grid-template-columns:minmax(100px,35%) minmax(0,1fr); }
314
+ .inspector-ready .inspector-shell .inspector-tile-content > .inspector-kv > * { padding:10px 14px; border-bottom:1px solid var(--border); }
315
+ .inspector-ready .inspector-shell .inspector-tile-content > .inspector-kv > :nth-last-child(-n+2) { border-bottom:0; }
316
+ .inspector-ready .inspector-shell .inspector-tile-feature { padding:10px 14px; }
317
+ .inspector-ready .inspector-shell .inspector-tile-content > :is(.inspector-tile-chip-list,.inspector-tile-paging) { padding:0 0 10px; margin-bottom:10px; background:transparent; }
318
+
319
+ /* Debug: overlay controls and resolution controls retain their existing actions. */
320
+ .inspector-ready .inspector-shell .inspector-card > .inspector-debug-overlay { grid-template-columns:76px minmax(0,1fr) auto; padding:10px 18px; gap:14px; }
321
+ .inspector-ready .inspector-shell .inspector-debug-overlay:last-child { border-bottom:0; }
322
+ .inspector-ready .inspector-shell .inspector-debug-preview { width:76px; height:50px; border-radius:6px; }
323
+ .inspector-ready .inspector-shell .inspector-debug-overlay strong { font-weight:500; }
324
+ .inspector-ready .inspector-shell .inspector-debug-overlay small { margin-top:3px; font-size:11px; }
325
+ .inspector-ready .inspector-shell .inspector-overdraw-help { max-width:none; padding:10px 18px 12px 108px; }
326
+ .inspector-ready .inspector-shell .inspector-debug-scale { padding:12px 18px; gap:12px; }
327
+ .inspector-ready .inspector-shell .inspector-debug-scale > span:first-child { flex:0 0 180px; }
328
+ .inspector-ready .inspector-shell .inspector-debug-scale > input[type=range] { width:108px; min-width:60px; }
329
+ .inspector-ready .inspector-shell .inspector-debug-scale > .inspector-debug-specimen { flex:1; min-width:160px; }
330
+ .inspector-ready .inspector-shell .inspector-card > .inspector-note { padding:0 18px 12px; }
331
+ .inspector-ready .inspector-shell .inspector-debug-scale:last-child { border-top:1px solid var(--border); }
332
+
333
+ /* Assets and findings use the same spacing and surfaces as the other pages. */
334
+ .inspector-ready .inspector-shell .inspector-assets-content { background:var(--inspector-page); }
335
+ .inspector-ready .inspector-shell .inspector-asset-main { padding:14px; }
336
+ .inspector-ready .inspector-shell :is(.inspector-sprite-grid,.inspector-model-grid,.inspector-font-list) { gap:10px; background:transparent; }
337
+ .inspector-ready .inspector-shell :is(.inspector-sprite-card,.inspector-model-card,.inspector-font-card) { border:1px solid var(--border); border-radius:8px; background:var(--paper); overflow:hidden; }
338
+ .inspector-ready .inspector-shell .inspector-sprite-card { padding:10px 8px; }
339
+ .inspector-ready .inspector-shell .inspector-font-list { padding:14px; max-width:none; }
340
+ .inspector-ready .inspector-shell .inspector-font-card { padding:12px; }
341
+ .inspector-ready .inspector-shell .inspector-asset-aside { margin:14px 14px 14px 0; border:1px solid var(--border); border-radius:10px; background:var(--paper); }
342
+ .inspector-ready .inspector-shell .inspector-asset-aside h3 { margin:0; padding:12px 14px; background:var(--inspector-card-heading); border-bottom:1px solid var(--border); }
343
+ .inspector-ready .inspector-shell .inspector-diagnostic { overflow:hidden; }
344
+ .inspector-ready .inspector-shell .inspector-diagnostic > summary { padding:12px 14px; }
345
+ .inspector-ready .inspector-shell .inspector-diagnostic[open] > summary { background:var(--inspector-card-heading); border-bottom:1px solid var(--border); }
346
+ .inspector-ready .inspector-shell .inspector-diagnostic-detail { padding:12px 14px 14px 39px; }
347
+ .inspector-ready .inspector-shell .inspector-diagnostics-empty { padding:20px; max-width:none; }
348
+
349
+ @container(max-width:760px) {
350
+ .inspector-ready .inspector-shell .inspector-asset-aside { margin:0 14px 14px; }
351
+ }
352
+ @container(max-width:480px) {
353
+ .inspector-ready .inspector-shell :is(.inspector-card-page,.inspector-layer-content,.inspector-tile-content,.inspector-diagnostics-list,.inspector-asset-main,.inspector-font-list) { padding:10px; }
354
+ .inspector-ready .inspector-shell .map-controls { padding:10px!important; }
355
+ .inspector-ready .inspector-shell .map-controls :is(.control-field,.control-field.inspector-palette-field) { grid-template-columns:110px minmax(0,1fr); gap:10px; padding-inline:12px; }
356
+ .inspector-ready .inspector-shell .control-module .control-field { padding-left:24px; }
357
+ .inspector-ready .inspector-shell .inspector-debug-scale > span:first-child { flex-basis:100%; }
358
+ .inspector-ready .inspector-shell .inspector-card > .inspector-debug-overlay { grid-template-columns:60px minmax(0,1fr) auto; padding:10px 12px; gap:10px; }
359
+ .inspector-ready .inspector-shell .inspector-debug-preview { width:60px; height:40px; }
360
+ .inspector-ready .inspector-shell .inspector-overdraw-help { padding-left:12px; }
361
+ }
362
+ `;
363
+
364
+ // src/inspector/inspector-styles.ts
365
+ var inspectorStyles = `
366
+ body.inspector-ready {
367
+ --paper:#FFFFFF; --surface:#F5F5F5; --ink:#171717; --muted:#666666; --faint:#8C8C8C;
368
+ --border:#E5E5E5; --border-strong:#C8C8C8; --accent:#404040; --inspector-selected:#EBEBEB;
369
+ --hover:#EEEEEE; --accent-soft:#E8E8E8; --ok:#2F704B; --ok-soft:#E3EBDB; --warn:#815D21; --warn-soft:#F3E6CF;
370
+ --danger:#B13C34; --inspector-string:var(--ink); --inspector-number:var(--accent); --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
371
+ --ui:-apple-system,BlinkMacSystemFont,'Segoe UI',Arial,sans-serif;
372
+ --inspector-control-height:22px; --inspector-row-height:22px; --inspector-nav-height:27px;
373
+ --inspector-header-height:44px; --inspector-camera-control-height:30px;
374
+ color-scheme:light; font:12px/1.35 var(--ui); -webkit-font-smoothing:antialiased;
375
+ }
376
+ body.inspector-ready[data-inspector-theme=dark] {
377
+ --paper:#171717; --surface:#111111; --ink:#F5F5F5; --muted:#B3B3B3; --faint:#858585; --border:#333333; --border-strong:#525252;
378
+ --accent:#D4D4D4; --inspector-selected:#303030; --hover:#262626; --accent-soft:#303030; --ok:#91B887; --ok-soft:#24301F; --warn:#E2B36F; --warn-soft:#3A2F20; --danger:#F1746B; color-scheme:dark;
379
+ }
380
+ @media(prefers-color-scheme:dark) { body.inspector-ready[data-inspector-theme=system] {
381
+ --paper:#171717; --surface:#111111; --ink:#F5F5F5; --muted:#B3B3B3; --faint:#858585; --border:#333333; --border-strong:#525252;
382
+ --accent:#D4D4D4; --inspector-selected:#303030; --hover:#262626; --accent-soft:#303030; --ok:#91B887; --ok-soft:#24301F; --warn:#E2B36F; --warn-soft:#3A2F20; --danger:#F1746B; color-scheme:dark;
383
+ }}
384
+ .inspector-ready button,.inspector-ready input,.inspector-ready select { font:inherit; }
385
+ .inspector-ready button { border:1px solid var(--border); border-radius:6px; min-height:28px; padding:4px 10px; cursor:pointer; }
386
+ .inspector-ready button:disabled { cursor:default; opacity:.45; }
387
+ .inspector-ready button:hover:not(:disabled) { background:var(--hover); }
388
+ .inspector-ready input[type=range] { accent-color:var(--accent); }
389
+ .inspector-ready code,.inspector-ready pre { font-family:var(--mono); }
390
+ .inspector-ready .inspector-head { height:40px; min-height:40px; flex-wrap:nowrap; gap:2px; padding:0 8px; background:var(--paper); }
391
+ .inspector-ready .inspector-head-main { display:flex; align-items:center; flex:1; min-width:0; }
392
+ .inspector-ready .inspector-tabs { flex:1; align-items:center; gap:2px; }
393
+ .inspector-ready .inspector-tabs button { border:0; border-radius:6px; min-height:28px; padding:5px 10px; color:var(--muted); background:transparent; }
394
+ .inspector-ready .inspector-tabs button[aria-selected=true] { color:var(--ink); background:var(--surface); }
395
+ .inspector-ready .inspector-head > .inspector-icon { width:28px; height:28px; margin:0; }
396
+ .inspector-ready .inspector-icon[aria-pressed=true] { background:var(--accent-soft); color:var(--accent); }
397
+ .inspector-ready .inspector-camera { display:flex; align-items:center; flex:0 0 auto; flex-wrap:wrap; gap:6px; margin:0; padding:8px 12px; border:0; border-bottom:1px solid var(--border); background:var(--paper); overflow:visible; }
398
+ .inspector-ready .inspector-camera-field { display:flex; flex:0 0 auto; gap:0; height:30px; border:1px solid var(--border); border-radius:6px; background:var(--surface); overflow:hidden; }
399
+ .inspector-ready .inspector-camera-field > span:first-child { padding:0 4px 0 9px; color:var(--faint); font-size:10px; text-transform:uppercase; letter-spacing:.04em; }
400
+ .inspector-ready .inspector-camera-field input[type=number] { width:58px; min-height:0; padding:0 5px; border:0; background:transparent; font:12px var(--mono); appearance:textfield; color:var(--ink); }
401
+ .inspector-ready .inspector-camera-field input::-webkit-inner-spin-button { appearance:none; }
402
+ .inspector-ready .inspector-camera-field input[type=range] { width:110px; padding:0; margin:0 10px 0 2px; min-height:0; }
403
+ .inspector-ready .inspector-unit { padding-right:9px; color:var(--faint); }
404
+ .inspector-ready .inspector-copy-agent { margin-left:auto; background:var(--ink); color:var(--paper); border-color:var(--ink); font-weight:500; }
405
+ .inspector-ready .inspector-copy-agent:hover { background:var(--ink); opacity:.88; }
406
+ .inspector-ready .inspector-menu { top:38px; right:8px; width:236px; padding:10px; border-radius:10px; box-shadow:0 8px 28px #0002; }
407
+ .inspector-ready .inspector-dock-options { display:grid; grid-template-columns:repeat(3,1fr); gap:6px; margin-bottom:8px; }
408
+ .inspector-ready .inspector-dock-options button { width:auto; height:34px; border:1px solid var(--border); justify-content:center; }
409
+ .inspector-ready .inspector-menu-label { font-size:11px; }
410
+ .inspector-ready .inspector-segments,.inspector-ready .inspector-layer-modes,.inspector-ready .controls-segments { display:inline-flex; padding:2px; background:var(--surface); border:1px solid var(--border); border-radius:6px; gap:2px; }
411
+ .inspector-ready .inspector-segments button,.inspector-ready .inspector-layer-modes button,.inspector-ready .controls-segments button { min-height:23px; border:0; border-radius:4px; padding:3px 9px; font-size:12px; background:transparent; color:var(--muted); }
412
+ .inspector-ready .inspector-segments button[aria-pressed=true],.inspector-ready .inspector-layer-modes button[aria-pressed=true],.inspector-ready .controls-segments button[aria-checked=true] { color:var(--ink); background:var(--paper); box-shadow:0 1px 2px #0001; }
413
+ .inspector-ready .inspector-menu .inspector-segments { display:flex; }.inspector-ready .inspector-menu .inspector-segments button { flex:1; }
414
+ .inspector-ready .inspector-tools { padding:10px 12px; gap:8px; background:var(--paper); }
415
+ .inspector-ready .inspector-tools input[type=search] { height:30px; padding:0 9px; background:var(--surface); border-radius:6px; width:auto; }
416
+ .inspector-ready .inspector-subtabs { padding:0 20px; gap:18px; }
417
+ .inspector-ready .inspector-subtabs button { background:transparent; border:0; border-radius:0; border-bottom:2px solid transparent; padding:8px 0 9px; color:var(--muted); min-height:36px; }
418
+ .inspector-ready .inspector-subtabs button[aria-selected=true] { color:var(--ink); border-bottom-color:var(--ink); }
419
+ .inspector-ready input[role=switch] { appearance:none; width:26px!important; height:15px!important; min-height:0; flex:none; border:0; border-radius:8px; background:var(--faint); position:relative; padding:0; margin:0; cursor:pointer; }
420
+ .inspector-ready input[role=switch]::before { content:''; position:absolute; width:11px; height:11px; left:2px; top:2px; border-radius:50%; background:#fff; transition:transform .15s; }
421
+ .inspector-ready input[role=switch]:checked { background:var(--accent); }
422
+ .inspector-ready input[role=switch]:checked::before { transform:translateX(11px); }
423
+ .inspector-ready .inspector-layer-split { display:grid; grid-template-columns:minmax(0,min(38%,360px)) minmax(0,1fr); }
424
+ .inspector-ready .inspector-layer-sidebar { display:flex; flex-direction:column; min-width:0; min-height:0; overflow:hidden; border-right:1px solid var(--border); }
425
+ .inspector-ready .inspector-layer-list { flex:1; border:0; }.inspector-ready .inspector-layer-tools { flex-direction:column; align-items:stretch; }
426
+ .inspector-ready .inspector-layer-tools-row { display:flex; align-items:center; gap:8px; }.inspector-ready .inspector-layer-total { margin-left:auto; font:11px var(--mono); }
427
+ .inspector-ready .inspector-layer-visible { font-size:12px; }.inspector-ready .inspector-layer-group-title { height:32px; font-size:11px; padding:0 12px 0 8px!important; }
428
+ .inspector-ready .inspector-layer-group-title strong { text-transform:uppercase; letter-spacing:.05em; }
429
+ .inspector-ready .inspector-layer-row { display:grid; grid-template-columns:20px 18px minmax(0,1fr) 64px; height:30px; padding:0 12px 0 6px; gap:8px; border-left:2px solid transparent; }
430
+ .inspector-ready .inspector-layer-row[aria-pressed=true] { background:var(--inspector-selected); border-left-color:var(--accent); color:var(--ink); }
431
+ .inspector-ready .inspector-layer-glyph { color:var(--muted); font-size:15px; }.inspector-ready .inspector-layer-label { font-size:12px; }
432
+ .inspector-ready .inspector-layer-prefix { color:var(--faint); }.inspector-ready .inspector-layer-name { font-weight:500; }
433
+ .inspector-ready .inspector-eye { padding:0; border:0; width:20px; height:20px; min-height:20px; color:var(--faint); background:transparent; display:grid; place-items:center; opacity:0; }
434
+ .inspector-ready .inspector-eye svg { width:14px; height:14px; }
435
+ .inspector-ready :is(.inspector-layer-row,.inspector-layer-group-title):hover .inspector-eye,.inspector-ready .inspector-eye:focus-visible,.inspector-ready .inspector-eye[aria-pressed=true] { opacity:1; }
436
+ .inspector-zrange { position:relative; display:block; width:64px; height:14px; }.inspector-zrange::before { content:''; position:absolute; inset:6px 0; height:2px; background:var(--border); }
437
+ .inspector-zrange i { position:absolute; top:5px; height:4px; border-radius:2px; background:var(--accent); opacity:.65; }.inspector-zrange em { position:absolute; top:1px; height:12px; width:1px; background:var(--accent); }
438
+ .inspector-ready .inspector-layer-heading { padding:14px 20px 6px; align-items:flex-start; border:0; }
439
+ .inspector-ready .inspector-layer-title strong { font-size:16px; font-weight:600; }.inspector-ready .inspector-layer-title > code { font-size:10px; color:var(--faint); }
440
+ .inspector-layer-status { display:flex; align-items:center; gap:8px; font-size:12px; color:var(--muted); margin-top:6px; }.inspector-layer-status[data-state=Enabled] { color:var(--ok); }.inspector-layer-status[data-state="Outside zoom"] { color:var(--warn); }
441
+ .inspector-ready .inspector-layer-rules { font-size:12px; }.inspector-ready .inspector-layer-rules th { padding:8px 20px; font-size:11px; text-transform:uppercase; letter-spacing:.05em; }
442
+ .inspector-ready .inspector-layer-rules th:first-child { width:42%; }.inspector-ready .inspector-layer-rules td { padding:8px 20px; vertical-align:middle; min-height:44px; }
443
+ .inspector-ready .inspector-layer-rules code { font:12px/1.5 var(--mono); }.inspector-ready .inspector-layer-rule-name { color:var(--ink); }.inspector-ready .inspector-layer-dependency { color:var(--accent); font:10px var(--mono); }
444
+ .inspector-ready .inspector-layer-rule-editor { gap:9px; flex-wrap:wrap; }.inspector-ready .inspector-layer-rule-editor input:not([type=color]):not([type=checkbox]):not([type=range]),.inspector-ready .inspector-layer-rule-editor select { min-height:26px; border:1px solid var(--border); font:12px var(--mono); border-radius:6px; }
445
+ .inspector-ready .inspector-layer-rule-editor input[type=color] { width:22px; height:22px; }
446
+ .inspector-ready .inspector-layer-rule-editor input[type=range] { width:110px; min-width:70px; padding:0; flex:0 1 110px; }
447
+ .inspector-rule-toggle { display:flex; align-items:center; gap:10px; }.inspector-rule-toggle input { width:14px; height:14px; min-height:0; margin:0; accent-color:var(--accent); }
448
+ .inspector-rule-choices { display:inline-flex; flex-wrap:wrap; border:1px solid var(--border); border-radius:6px; overflow:hidden; }.inspector-ready .inspector-rule-choices button { min-height:26px; border:0; border-radius:0; font:11px var(--mono); padding:3px 8px; }
449
+ .inspector-rule-spark { display:flex; align-items:center; gap:10px; color:var(--accent); font:11px var(--mono); margin-bottom:5px; }.inspector-rule-spark svg { width:130px; height:26px; overflow:visible; }
450
+ .inspector-filter-chips { display:flex; flex-wrap:wrap; gap:6px; margin-block:10px; }.inspector-filter-chip { padding:4px 9px; background:var(--surface); border:1px solid var(--border); border-radius:13px; font:11px var(--mono); }
451
+ .inspector-ready .inspector-layer-section,.inspector-ready .inspector-layer-origin { padding:12px 20px; }.inspector-ready .inspector-layer-section h4 { font-size:11px; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); }
452
+ .inspector-ready .inspector-layer-evidence { padding:0; }.inspector-ready .inspector-layer-evidence-row { padding:12px 20px; font-size:12px; grid-template-columns:20px 130px 1fr; }
453
+ .inspector-ready .inspector-layer-evidence-row[data-state=pass] .inspector-layer-indicator { color:var(--ok); }.inspector-ready .inspector-layer-evidence-row[data-state=warn] .inspector-layer-indicator { color:var(--warn); }
454
+ .inspector-ready .inspector-layer-json { font:11px/1.6 var(--mono); color:var(--ink); }
455
+ .inspector-ready .inspector-table { font:12px var(--mono); }.inspector-ready .inspector-table th { padding:8px 12px; font:11px Inter,system-ui,sans-serif; text-transform:uppercase; border-right:0; }.inspector-ready .inspector-table td { height:32px; padding:6px 12px; }.inspector-ready .inspector-table tbody tr:nth-child(even) { background:transparent; }
456
+ .inspector-ready .inspector-network-body { display:grid; grid-template-columns:minmax(0,min(38%,360px)) minmax(0,1fr); }.inspector-tiles-sidebar { display:flex; flex-direction:column; min-width:0; min-height:0; overflow:hidden; border-right:1px solid var(--border); }.inspector-ready .inspector-network-detail { border:0; min-width:0; }
457
+ .inspector-ready .inspector-tile-heading { padding:14px 20px 8px; }.inspector-tile-heading strong { font:15px var(--mono); }.inspector-tile-status { padding:0 20px 10px; color:var(--muted); font-size:12px; }
458
+ .inspector-tile-grid { display:grid; gap:3px; padding:12px; }.inspector-ready .inspector-tile-cell { aspect-ratio:1; padding:0; font:10px var(--mono); min-width:0; min-height:20px; border-radius:4px; background:color-mix(in srgb,var(--accent) var(--weight,10%),var(--paper)); }
459
+ .inspector-ready .inspector-tile-cell[aria-pressed=true] { border-color:var(--accent); box-shadow:0 0 0 2px var(--accent-soft); }.inspector-tile-grid-title { margin:10px 12px 0; color:var(--muted); font-size:11px; }
460
+ .inspector-tile-chip-list { display:flex; gap:6px; flex-wrap:wrap; padding:10px 20px; border-bottom:1px solid var(--border); }.inspector-ready .inspector-tile-chip { font:11px var(--mono); border-radius:13px; }
461
+ .inspector-asset-layout { display:grid; grid-template-columns:minmax(0,1fr) 300px; min-height:0; flex:1; }.inspector-asset-main { min-width:0; overflow:auto; padding:12px; }.inspector-asset-aside { overflow:auto; border-left:1px solid var(--border); }.inspector-asset-aside h3 { font:500 15px var(--mono); margin:16px 20px; }.inspector-asset-variants { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:1px; background:var(--border); }.inspector-asset-variant { padding:20px 10px; display:flex; flex-direction:column; align-items:center; gap:10px; background:var(--surface); font:10px var(--mono); overflow-wrap:anywhere; }.inspector-asset-variant[data-night=true] { background:#262626; color:#f5f5f5; }
462
+ .inspector-ready .inspector-sprite-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(104px,1fr)); gap:4px; }.inspector-ready .inspector-sprite-card { border:1px solid transparent; border-radius:8px; background:transparent; padding:16px 6px 10px; cursor:pointer; }.inspector-sprite-card[aria-pressed=true] { background:var(--inspector-selected)!important; border-color:var(--accent)!important; }
463
+ .inspector-debug-overlay { display:grid; grid-template-columns:84px 1fr auto; align-items:center; gap:16px; padding:10px 20px; border-bottom:1px solid var(--border); cursor:pointer; }.inspector-debug-overlay:hover { background:var(--hover); }.inspector-debug-overlay small { display:block; color:var(--muted); font-size:12px; }.inspector-debug-preview { width:84px; height:58px; border-radius:6px; overflow:hidden; }.inspector-debug-preview svg { width:100%; height:100%; }
464
+ .inspector-section-heading { background:var(--surface); padding:8px 20px; font-size:11px; font-weight:600; letter-spacing:.05em; text-transform:uppercase; border-bottom:1px solid var(--border); }
465
+ .inspector-ready .inspector-overdraw-help { margin:0; border:0; border-bottom:1px solid var(--border); border-radius:0; padding:12px 20px 12px 120px; background:var(--surface); }.inspector-debug-scale { display:flex; align-items:center; gap:12px; padding:16px 20px; flex-wrap:wrap; }.inspector-debug-scale input[type=number] { width:64px; }
466
+ .inspector-ready .map-controls { padding:0!important; background:var(--paper); }.inspector-ready .controls-heading { display:none; }.inspector-ready .controls-grid { display:block; }.inspector-ready .controls-selection,.inspector-ready .controls-runtime { border:0; padding:0; }
467
+ .inspector-ready .controls-section-title { margin:0; padding:8px 20px; background:var(--surface); border-bottom:1px solid var(--border); font-size:11px; letter-spacing:.05em; }.inspector-ready .control-field { display:grid; grid-template-columns:200px 1fr; align-items:center; margin:0; padding:10px 20px; border-bottom:1px solid var(--border); gap:16px; font-size:13px; }.inspector-ready .control-field select { max-width:100%; width:max-content; min-width:132px; }
468
+ .inspector-ready #controls-modules { display:block; }.inspector-ready .control-module { border:0; border-bottom:1px solid var(--border); border-radius:0; margin:0; background:var(--paper); }.inspector-ready .control-module-header { padding:10px 20px; }.inspector-ready .control-module-header label { justify-content:flex-start; font-size:13px; font-weight:500; }.inspector-ready .control-module-header input { order:-1; margin-right:12px; }.inspector-ready .control-module fieldset { padding:0; background:var(--surface); }.inspector-ready .control-module .control-field { padding-left:58px; border:0; }.inspector-ready .inspector-scene-content { max-width:1000px; width:100%; }.inspector-palettes { display:flex; gap:8px; flex-wrap:wrap; }.inspector-ready .inspector-palette { display:flex; align-items:center; gap:8px; padding:5px 10px 5px 5px; }.inspector-palette-swatches { display:grid; grid-template-columns:repeat(2,11px); grid-template-rows:repeat(2,11px); overflow:hidden; border-radius:4px; }
469
+ @container(max-width:760px) {
470
+ .inspector-ready .inspector-layer-title { flex-basis:180px; }.inspector-ready .inspector-camera-field input[type=range] { width:80px; }
471
+ .inspector-ready .inspector-layer-rules th:first-child { width:45%; }.inspector-ready .inspector-layer-rules td { padding:8px 12px; }
472
+ .inspector-asset-layout { grid-template-columns:1fr; }.inspector-asset-aside { border:0; border-top:1px solid var(--border); }
473
+ .inspector-ready .control-field { grid-template-columns:160px 1fr; }.inspector-ready .control-module .control-field { padding-left:30px; }
474
+ }
475
+ @media(prefers-reduced-motion:reduce) { .inspector-ready * { scroll-behavior:auto!important; transition:none!important; animation:none!important; } }
476
+
477
+ .inspector-palettes{display:flex;gap:8px;flex-wrap:wrap}.inspector-ready .inspector-palettes button{display:flex;flex-direction:column;gap:7px;padding:10px 14px;min-width:110px;background:var(--paper);text-align:left;text-transform:capitalize}.inspector-ready .inspector-palettes button[aria-checked=true]{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}.inspector-palette-swatches{display:flex;height:26px;width:100%;overflow:hidden;border-radius:4px}.inspector-palette-swatches i{flex:1;min-width:20px}.inspector-ready .inspector-palette-field{align-items:flex-start;flex-direction:column}.inspector-model-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:10px}.inspector-ready .inspector-model-card{display:flex;flex-direction:column;align-items:center;padding:0;overflow:hidden;border-radius:10px}.inspector-model-card canvas{background:linear-gradient(var(--surface),var(--paper));max-width:100%;height:auto!important}.inspector-model-card strong{font:500 12px var(--mono);padding:9px 12px 2px;overflow-wrap:anywhere}.inspector-model-card small{font-size:11px;color:var(--muted);padding:0 12px 9px}.inspector-model-card[aria-pressed=true]{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}.inspector-asset-aside>canvas{display:block;margin:auto;max-width:100%;touch-action:none;cursor:grab}.inspector-font-ranges{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.inspector-font-ranges small{color:var(--muted);font-size:11px}
478
+
479
+ .inspector-debug-specimen{display:flex;align-items:center;gap:18px;min-height:70px;padding:12px 20px;border:1px solid var(--border);border-radius:8px;background:var(--surface)}.inspector-rule-font-preview{overflow:hidden;max-width:100%}.inspector-rule-font-preview canvas{max-width:100%;height:auto!important}
480
+ /* Keep the new shell independent of the legacy in-header camera and toolbar breakpoints. */
481
+ .inspector-ready .inspector-shell>.inspector-camera{order:0;flex:0 0 auto;margin:0;border-top:0;background:var(--paper);padding:8px 12px}
482
+ .inspector-ready .inspector-shell>.inspector-head{flex-wrap:nowrap}.inspector-ready .inspector-shell .inspector-head-main{display:flex;gap:2px}
483
+ .inspector-ready .inspector-shell .inspector-layer-tools input[type=search]{order:0;flex:0 0 auto;width:100%;height:30px}
484
+ .inspector-ready .inspector-shell .inspector-layer-tools{flex-wrap:nowrap}.inspector-ready .inspector-shell .inspector-layer-tools-row{flex:none;min-height:28px}
485
+ .inspector-ready .inspector-shell .inspector-layer-row{display:grid;grid-template-columns:20px 18px minmax(0,1fr) 64px;height:30px;min-height:30px;padding:0 12px 0 6px;gap:8px}
486
+ .inspector-ready .inspector-shell .inspector-layer-label{display:block;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
487
+ .inspector-ready .inspector-shell .inspector-layer-heading{flex:none}.inspector-ready .inspector-shell .inspector-layer-title{flex:1 1 180px}
488
+ .inspector-ready .inspector-shell .inspector-layer-sidebar .inspector-layer-list{flex:1;min-height:0}.inspector-ready .inspector-shell .map-controls .control-field{display:grid;grid-template-columns:160px minmax(0,1fr);padding:12px 20px;margin:0}
489
+ .inspector-ready .inspector-shell .control-module-header label{display:flex;gap:12px}.inspector-ready .inspector-shell .map-controls input[role=switch]{width:26px!important;height:15px!important;min-height:0;border:0;background:var(--faint)}.inspector-ready .inspector-shell .map-controls input[role=switch]:checked{background:var(--accent)}.inspector-ready .inspector-shell .map-controls input[role=switch]::before{width:11px;height:11px;top:2px;left:2px;background:white}.inspector-ready .inspector-shell .map-controls input[role=switch]:checked::before{transform:translateX(11px)}
490
+ @container(max-width:760px){.inspector-ready .inspector-shell .inspector-layer-tools{padding:8px 12px}.inspector-ready .inspector-shell .inspector-tabs button{padding-inline:7px}.inspector-ready .inspector-shell .inspector-head{padding-inline:4px}.inspector-ready .inspector-shell .inspector-camera-field{flex:0 0 auto}}
491
+
492
+ /* Shared density also wins over panel styles injected after the shell stylesheet. */
493
+ .inspector-ready .inspector-shell { font:12px/1.35 var(--ui); }
494
+ .inspector-ready .inspector-shell :is(button,input,select,summary) { min-height:var(--inspector-control-height); }
495
+ .inspector-ready .inspector-shell button { padding:2px 6px; border-radius:3px; }
496
+ .inspector-ready .inspector-shell :is(input[type=checkbox],input[type=radio],input[type=range],input[type=color]) { min-height:0; }
497
+ .inspector-ready .inspector-shell .inspector-head { height:var(--inspector-header-height); min-height:var(--inspector-header-height); padding:0 4px; }
498
+ .inspector-ready .inspector-shell .inspector-tabs { gap:0; }
499
+ .inspector-ready .inspector-shell .inspector-tabs button { height:34px; min-height:34px; padding:0 8px; border-radius:4px; }
500
+ .inspector-ready .inspector-shell .inspector-head .inspector-icon { width:30px; height:30px; min-height:30px; padding:5px; margin-block:0; }
501
+ .inspector-ready .inspector-shell > .inspector-camera { min-height:48px; padding:8px; gap:6px; }
502
+ .inspector-ready .inspector-shell .inspector-camera-field { height:var(--inspector-camera-control-height); border-radius:5px; }
503
+ .inspector-ready .inspector-shell .inspector-camera-field > span:first-child { padding-left:6px; }
504
+ .inspector-ready .inspector-shell .inspector-camera-field input[type=number] { width:48px; min-height:0; padding:0 3px; font-size:12px; }
505
+ .inspector-ready .inspector-shell .inspector-camera-field input[type=range] { width:88px; margin:0 6px 0 2px; }
506
+ .inspector-ready .inspector-shell .inspector-unit { padding-right:6px; }
507
+ .inspector-ready .inspector-shell .inspector-camera-reset { display:inline-flex; align-items:center; justify-content:center; flex:none; width:26px; height:100%; min-height:0; padding:4px; border:0; border-left:1px solid var(--border); border-radius:0; background:transparent; }
508
+ .inspector-ready .inspector-shell .inspector-camera-reset:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }
509
+ .inspector-ready .inspector-shell .inspector-copy-agent { min-height:var(--inspector-camera-control-height); padding:4px 10px; font-size:12px; white-space:nowrap; }
510
+ .inspector-ready .inspector-shell .inspector-menu { top:var(--inspector-header-height); right:4px; width:216px; padding:6px; border-radius:4px; }
511
+ .inspector-ready .inspector-shell .inspector-dock-options { gap:4px; margin-bottom:6px; }
512
+ .inspector-ready .inspector-shell .inspector-dock-options button { height:26px; padding:2px; }
513
+ .inspector-ready .inspector-shell :is(.inspector-segments,.inspector-layer-modes,.controls-segments) { padding:1px; gap:1px; border-radius:3px; }
514
+ .inspector-ready .inspector-shell :is(.inspector-segments,.inspector-layer-modes,.controls-segments) button { min-height:20px; padding:1px 6px; border-radius:2px; }
515
+ .inspector-ready .inspector-shell .inspector-tools { min-height:28px; padding:3px 8px; gap:4px; }
516
+ .inspector-ready .inspector-shell .inspector-tools input[type=search] { height:var(--inspector-control-height); padding:0 6px; border-radius:3px; }
517
+ .inspector-ready .inspector-shell .inspector-layer-tools-row { min-width:0; min-height:var(--inspector-control-height); gap:6px; flex-wrap:wrap; }
518
+ .inspector-ready .inspector-shell .inspector-layer-modes { max-width:100%; overflow-x:auto; }
519
+ .inspector-ready .inspector-shell .inspector-subtabs { padding:0 8px; gap:12px; }
520
+ .inspector-ready .inspector-shell .inspector-subtabs button { min-height:var(--inspector-nav-height); padding:3px 0; border-radius:0; }
521
+ .inspector-ready .inspector-shell .inspector-footer { min-height:20px; padding:2px 8px; gap:8px; }
522
+ .inspector-ready .inspector-shell .inspector-section-heading { padding:4px 10px; }
523
+ .inspector-ready .inspector-shell .inspector-kv { gap:3px 8px; padding:6px 8px; font:11px/1.4 var(--mono); }
524
+ .inspector-ready .inspector-shell .inspector-layer-group-title { height:var(--inspector-row-height); min-height:var(--inspector-row-height); padding:0 8px 0 6px!important; }
525
+ .inspector-ready .inspector-shell .inspector-layer-row { grid-template-columns:18px 16px minmax(0,1fr) 52px; height:var(--inspector-row-height); min-height:var(--inspector-row-height); padding:0 8px 0 4px; gap:4px; border-radius:0; }
526
+ .inspector-ready .inspector-shell .inspector-layer-glyph { font-size:13px; }
527
+ .inspector-ready .inspector-shell .inspector-eye { min-height:18px; width:18px; height:18px; padding:0; }
528
+ .inspector-ready .inspector-shell .inspector-zrange { width:52px; }
529
+ .inspector-ready .inspector-shell .inspector-layer-heading { padding:8px 10px 4px; }
530
+ .inspector-ready .inspector-shell .inspector-layer-title strong { font-size:13px; }
531
+ .inspector-ready .inspector-shell .inspector-layer-status { margin-top:3px; gap:6px; font-size:11px; }
532
+ .inspector-ready .inspector-shell .inspector-layer-rules th { padding:4px 10px; }
533
+ .inspector-ready .inspector-shell .inspector-layer-rules td { padding:3px 10px; min-height:24px; }
534
+ .inspector-ready .inspector-shell .inspector-layer-rules code { font:11px/1.4 var(--mono); }
535
+ .inspector-ready .inspector-shell .inspector-layer-rule-editor { gap:5px; }
536
+ .inspector-ready .inspector-shell .inspector-layer-rule-editor :is(input:not([type=color]):not([type=checkbox]):not([type=range]),select) { min-height:20px; font:11px/1.4 var(--mono); border-radius:3px; }
537
+ .inspector-ready .inspector-shell .inspector-layer-rule-editor input[type=color] { width:18px; height:20px; }
538
+ .inspector-ready .inspector-shell .inspector-layer-rule-editor input[type=range] { width:88px; min-width:60px; flex-basis:88px; }
539
+ .inspector-ready .inspector-shell .inspector-rule-toggle { gap:6px; }
540
+ .inspector-ready .inspector-shell .inspector-rule-choices { border-radius:3px; }
541
+ .inspector-ready .inspector-shell .inspector-rule-choices button { min-height:20px; padding:1px 5px; border-radius:0; }
542
+ .inspector-ready .inspector-shell .inspector-rule-spark { gap:6px; margin-bottom:3px; }
543
+ .inspector-ready .inspector-shell .inspector-rule-spark svg { width:100px; height:22px; }
544
+ .inspector-ready .inspector-shell .inspector-filter-chips { gap:4px; margin-block:6px; }
545
+ .inspector-ready .inspector-shell .inspector-filter-chip { padding:2px 6px; }
546
+ .inspector-ready .inspector-shell :is(.inspector-layer-section,.inspector-layer-origin) { padding:6px 10px; }
547
+ .inspector-ready .inspector-shell .inspector-layer-evidence-row { padding:6px 10px; grid-template-columns:16px 110px minmax(0,1fr); }
548
+ .inspector-ready .inspector-shell .inspector-table { font:11px/1.4 var(--mono); }
549
+ .inspector-ready .inspector-shell .inspector-table th { padding:4px 8px; font:11px var(--ui); }
550
+ .inspector-ready .inspector-shell .inspector-table td { height:var(--inspector-row-height); padding:2px 8px; }
551
+ .inspector-ready .inspector-shell .inspector-tile-heading { padding:8px 10px 4px; }
552
+ .inspector-ready .inspector-shell .inspector-tile-heading strong { font-size:13px; }
553
+ .inspector-ready .inspector-shell .inspector-tile-status { padding:0 10px 6px; }
554
+ .inspector-ready .inspector-shell .inspector-tile-grid { padding:8px; gap:2px; }
555
+ .inspector-ready .inspector-shell .inspector-tile-cell { padding:0; min-height:20px; }
556
+ .inspector-ready .inspector-shell .inspector-tile-grid-title { margin:6px 8px 0; }
557
+ .inspector-ready .inspector-shell .inspector-tile-chip-list { padding:5px 10px; gap:4px; }
558
+ .inspector-ready .inspector-shell .inspector-asset-main { padding:8px; }
559
+ .inspector-ready .inspector-shell .inspector-asset-aside h3 { font-size:13px; margin:8px 10px; }
560
+ .inspector-ready .inspector-shell .inspector-asset-variant { padding:10px 6px; gap:6px; }
561
+ .inspector-ready .inspector-shell .inspector-sprite-grid { grid-template-columns:repeat(auto-fill,minmax(88px,1fr)); }
562
+ .inspector-ready .inspector-shell .inspector-sprite-card { padding:6px 4px; border-radius:4px; }
563
+ .inspector-ready .inspector-shell .inspector-sprite-preview { height:44px; }
564
+ .inspector-ready .inspector-shell .inspector-sprite-card figcaption { margin-top:4px; }
565
+ .inspector-ready .inspector-shell .inspector-font-card { padding:8px; }
566
+ .inspector-ready .inspector-shell .inspector-font-card figcaption { margin-bottom:5px; font:11px/1.4 var(--mono); }
567
+ .inspector-ready .inspector-shell .inspector-font-loading { padding:8px; }
568
+ .inspector-ready .inspector-shell .inspector-model-grid { grid-template-columns:repeat(auto-fill,minmax(140px,1fr)); gap:6px; }
569
+ .inspector-ready .inspector-shell .inspector-model-card { padding:0; border-radius:4px; }
570
+ .inspector-ready .inspector-shell .inspector-model-card strong { padding:5px 8px 2px; }
571
+ .inspector-ready .inspector-shell .inspector-model-card small { padding:0 8px 5px; }
572
+ .inspector-ready .inspector-shell .inspector-debug-overlay { grid-template-columns:60px 1fr auto; gap:8px; padding:6px 10px; }
573
+ .inspector-ready .inspector-shell .inspector-debug-preview { width:60px; height:40px; border-radius:3px; }
574
+ .inspector-ready .inspector-shell .inspector-overdraw-help { padding:6px 10px 6px 78px; }
575
+ .inspector-ready .inspector-shell .inspector-debug-scale { gap:8px; padding:8px 10px; }
576
+ .inspector-ready .inspector-shell .inspector-debug-specimen { gap:10px; min-height:52px; padding:6px 10px; border-radius:4px; }
577
+ .inspector-ready .inspector-shell .controls-section-title { padding:4px 10px; }
578
+ .inspector-ready .inspector-shell .map-controls .control-field { grid-template-columns:140px minmax(0,1fr); padding:6px 10px; gap:8px; font-size:12px; }
579
+ .inspector-ready .inspector-shell .control-module-header { padding:6px 10px; }
580
+ .inspector-ready .inspector-shell .control-module-header label { font-size:12px; gap:6px; }
581
+ .inspector-ready .inspector-shell .control-module-header input { margin-right:6px; }
582
+ .inspector-ready .inspector-shell .control-module .control-field { padding-left:38px; }
583
+ .inspector-ready .inspector-shell .map-controls .control-field.inspector-palette-field { grid-template-columns:70px minmax(0,1fr); align-items:center; }
584
+ .inspector-ready .inspector-shell .inspector-palettes { gap:4px; align-items:center; }
585
+ .inspector-ready .inspector-shell .inspector-palettes button { flex-direction:row; align-items:center; gap:5px; padding:2px 6px; min-width:0; height:var(--inspector-control-height); font-size:11px; white-space:nowrap; }
586
+ .inspector-ready .inspector-shell .inspector-palette-swatches { width:24px; height:10px; flex:none; border-radius:2px; }
587
+ .inspector-ready .inspector-shell .inspector-palette-swatches i { min-width:0; }
588
+ @container(max-width:480px) {
589
+ .inspector-ready .inspector-shell .map-controls .control-field { grid-template-columns:110px minmax(0,1fr); }
590
+ .inspector-ready .inspector-shell .control-module .control-field { padding-left:24px; }
591
+ .inspector-ready .inspector-shell .inspector-camera-field input[type=range] { width:48px; }
592
+ }
593
+ `;
594
+
595
+ // src/inspector/styles.ts
596
+ var legacyStyles = `
597
+ body.inspector-ready { color-scheme:light dark; --paper:light-dark(#fff,#202124); --surface:light-dark(#f1f3f4,#292a2d); --ink:light-dark(#202124,#e8eaed); --muted:light-dark(#62676e,#9aa0a6); --border:light-dark(#dadce0,#3c4043); --accent:light-dark(#1a73e8,#8ab4f8); --inspector-selected:light-dark(#e8f0fe,#394457); --inspector-string:light-dark(#a31515,#e6b98b); --inspector-number:light-dark(#1a1aa6,#a8c7fa); font:12px/1.45 Arial,system-ui,sans-serif; background:var(--paper); }
598
+ .inspector-ready button,.inspector-ready input,.inspector-ready select,.inspector-ready summary { min-height:24px; border-radius:2px; font:inherit; }
599
+ .inspector-ready button { padding:3px 8px; background:var(--paper); color:var(--ink); }
600
+ .inspector-ready button:hover { background:var(--surface); border-color:var(--border); }
601
+ .inspector-ready button:focus-visible,.inspector-ready input:focus-visible,.inspector-ready select:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }
602
+ .inspector-ready button[aria-pressed=true] { color:var(--accent); background:var(--inspector-selected); }
603
+ .inspector-ready .tool-icon { width:14px; height:14px; }
604
+ .inspector-ready .workspace { display:grid; grid-template-columns:minmax(0,1fr); grid-template-rows:minmax(180px,1fr) var(--inspector-height,340px); flex:1; min-height:0; }
605
+ .inspector-ready .workspace[data-dock=right] { grid-template-columns:minmax(200px,1fr) max(640px,var(--inspector-width,760px)); grid-template-rows:minmax(0,1fr); }
606
+ .inspector-ready .workspace[data-dock=left] { grid-template-columns:max(640px,var(--inspector-width,760px)) minmax(200px,1fr); grid-template-rows:minmax(0,1fr); }
607
+ .inspector-ready .workspace[data-dock=left] > .stage { grid-column:2; grid-row:1; }
608
+ .inspector-ready .workspace[data-dock=left] > .inspector-shell { grid-column:1; grid-row:1; border-right:1px solid var(--border); }
609
+ .inspector-ready .workspace[data-collapsed=true] { grid-template-columns:minmax(0,1fr); grid-template-rows:minmax(0,1fr); }
610
+ .inspector-ready .workspace[data-collapsed=true] > .inspector-shell { display:none; }
611
+ .inspector-ready .workspace > .inspector { display:none !important; }
612
+ .inspector-ready .inspect-hint { top:8px; font-size:11px; padding:4px 7px; background:var(--paper); color:var(--ink); border:1px solid var(--accent); }
613
+ #inspector-reopen { display:none; position:absolute; top:8px; left:8px; z-index:10; box-shadow:0 1px 4px #0002; }
614
+ .workspace[data-collapsed=true] > #inspector-reopen { display:block; }
615
+ .inspector-ready .workspace[data-collapsed=true] > .stage { grid-column:1; grid-row:1; }
616
+ .inspector-shell { container-type:inline-size; display:flex; flex-direction:column; min-width:0; min-height:0; overflow:visible; position:relative; background:var(--paper); border-top:1px solid var(--border); color:var(--ink); }
617
+ .inspector-head { min-height:40px; display:flex; align-items:center; border-bottom:1px solid var(--border); background:var(--surface); padding:0 3px; position:relative; flex:none; }
618
+ .inspector-head-main { display:flex; align-items:center; gap:12px; flex:1; min-width:0; align-self:stretch; }
619
+ .inspector-tabs { display:flex; min-width:0; overflow-x:auto; scrollbar-width:none; flex:0 1 auto; align-self:stretch; }
620
+ .inspector-tabs button { border:0; border-bottom:2px solid transparent; background:transparent; border-radius:0; white-space:nowrap; padding:8px 10px; min-height:38px; }
621
+ .inspector-tabs button[aria-selected=true] { color:var(--accent); border-bottom-color:var(--accent); background:var(--paper); }
622
+ .inspector-head .inspector-icon,.inspector-tools .inspector-icon { display:inline-flex; align-items:center; justify-content:center; border:0; background:transparent; padding:3px; width:26px; height:26px; flex:none; }
623
+ .inspector-head .inspector-icon:disabled { opacity:.35; cursor:default; }
624
+ .inspector-icon svg { width:16px; height:16px; pointer-events:none; }
625
+ .inspector-divider { height:16px; border-left:1px solid var(--border); margin:0 4px; }
626
+ .inspector-camera { display:flex; align-items:center; flex-wrap:nowrap; gap:8px; min-width:0; flex:none; border:0; border-left:1px solid var(--border); margin:5px 0; padding:0 0 0 12px; overflow-x:auto; scrollbar-width:thin; }
627
+ .inspector-camera-field { display:flex; align-items:center; gap:4px; min-width:0; flex:none; }
628
+ .inspector-camera-field > label { color:var(--muted); font-size:11px; line-height:1; }
629
+ .inspector-stepper { display:flex; align-items:stretch; height:26px; border:1px solid var(--border); border-radius:4px; overflow:hidden; background:var(--paper); }
630
+ .inspector-stepper input[type=number] { appearance:textfield; -moz-appearance:textfield; width:54px; min-width:0; min-height:0; padding:2px 0; border:0; border-radius:0; background:transparent; color:var(--ink); text-align:center; font-variant-numeric:tabular-nums; }
631
+ .inspector-stepper input::-webkit-inner-spin-button,.inspector-stepper input::-webkit-outer-spin-button { -webkit-appearance:none; margin:0; }
632
+ .inspector-stepper button { width:24px; min-height:0; padding:0; border:0; border-radius:0; background:transparent; font-size:15px; line-height:1; }
633
+ .inspector-stepper button:disabled { color:var(--muted); opacity:.4; cursor:default; }
634
+ .inspector-camera:disabled { opacity:.5; }
635
+ .inspector-camera-reset { flex:none; height:26px; }
636
+ .inspector-menu { position:absolute; z-index:40; right:5px; top:40px; padding:8px; background:var(--paper); color:var(--ink); border:1px solid var(--border); box-shadow:0 4px 16px #0003; min-width:190px; border-radius:4px; }
637
+ :is([data-dock=left],[data-dock=right]) .inspector-head { flex-wrap:wrap; }
638
+ :is([data-dock=left],[data-dock=right]) .inspector-head > .inspector-icon { margin-block:6px; }
639
+ :is([data-dock=left],[data-dock=right]) .inspector-head-main { display:contents; }
640
+ :is([data-dock=left],[data-dock=right]) .inspector-tabs { flex:1; }
641
+ :is([data-dock=left],[data-dock=right]) .inspector-camera-field { flex:1 0 96px; }
642
+ :is([data-dock=left],[data-dock=right]) .inspector-stepper { flex:1; min-width:0; }
643
+ :is([data-dock=left],[data-dock=right]) .inspector-stepper input[type=number] { flex:1; width:0; }
644
+ :is([data-dock=left],[data-dock=right]) .inspector-stepper button { flex:none; width:22px; }
645
+ :is([data-dock=left],[data-dock=right]) .inspector-menu { top:38px; }
646
+ @container(max-width:1050px) {
647
+ .inspector-head { flex-wrap:wrap; }
648
+ .inspector-head > .inspector-icon { margin-block:6px; }
649
+ .inspector-head-main { display:contents; }
650
+ .inspector-tabs { flex:1; }
651
+ .inspector-camera-field { flex:1 0 96px; }
652
+ .inspector-stepper { flex:1; min-width:0; }
653
+ .inspector-stepper input[type=number] { flex:1; width:0; }
654
+ .inspector-stepper button { flex:none; width:22px; }
655
+ .inspector-menu { top:38px; }
656
+ }
657
+ .inspector-menu-label { color:var(--muted); margin:2px 0 6px; }
658
+ .inspector-dock-options { display:flex; gap:4px; }
659
+ .inspector-dock-options button { padding:6px; display:flex; }
660
+ .inspector-dock-options svg { width:24px; height:20px; }
661
+ .inspector-resize { position:absolute; z-index:5; height:6px; left:0; right:0; top:-4px; cursor:ns-resize; touch-action:none; }
662
+ [data-dock=right] > .inspector-shell > .inspector-resize { top:0; bottom:0; left:-4px; width:6px; height:auto; right:auto; cursor:ew-resize; }
663
+ [data-dock=left] > .inspector-shell > .inspector-resize { top:0; bottom:0; right:-4px; width:6px; height:auto; left:auto; cursor:ew-resize; }
664
+ .inspector-resize-shield { position:absolute; inset:0; z-index:30; background:transparent; }
665
+ .inspector-body { flex:1; display:flex; flex-direction:column; min-height:0; overflow:hidden; }
666
+ .inspector-panel { flex:1; min-height:0; min-width:0; display:flex; flex-direction:column; overflow:hidden; }
667
+ .inspector-tools { min-height:29px; display:flex; flex-wrap:wrap; align-items:center; gap:5px; border-bottom:1px solid var(--border); padding:3px 6px; flex:none; }
668
+ .inspector-tools input[type=search] { width:160px; flex:1; min-width:75px; border:1px solid var(--border); background:var(--paper); color:var(--ink); padding:2px 6px; }
669
+ .inspector-tools label { display:flex; align-items:center; gap:4px; white-space:nowrap; color:var(--muted); }
670
+ .inspector-tools select { max-width:160px; padding:2px; }
671
+ .inspector-tools input[type=checkbox] { min-height:0; margin:0; accent-color:var(--accent); }
672
+ .inspector-tools button { border-color:transparent; background:transparent; }
673
+ .inspector-footer { min-height:23px; padding:3px 8px; border-top:1px solid var(--border); color:var(--muted); font-size:11px; flex:none; display:flex; gap:12px; }
674
+ .inspector-scroll { flex:1; overflow:auto; min-height:0; min-width:0; }
675
+ .inspector-empty { color:var(--muted); padding:22px; text-align:center; margin:auto; max-width:420px; }
676
+ .inspector-empty strong { color:var(--ink); display:block; margin-bottom:6px; font-weight:400; }
677
+ .inspector-inspect-grid { display:grid; grid-template-columns:minmax(220px,1fr) minmax(300px,42%); flex:1; min-height:0; }
678
+ .inspector-tree-column { display:flex; flex-direction:column; min-height:0; border-right:1px solid var(--border); }
679
+ .inspector-tree { flex:1; min-height:0; overflow:auto; font:12px/1.65 ui-monospace,SFMono-Regular,Consolas,monospace; padding:5px 0; }
680
+ .inspector-tree-row { display:flex; width:100%; gap:6px; align-items:center; min-height:23px; padding:2px 8px; white-space:nowrap; border:0 !important; border-radius:0 !important; text-align:left; background:transparent !important; font:inherit !important; }
681
+ .inspector-tree-row[aria-selected=true] { background:var(--inspector-selected) !important; }
682
+ .inspector-tree-row:hover { background:var(--surface) !important; }
683
+ .inspector-tree-row .inspector-token { color:var(--inspector-number); }
684
+ .inspector-tree-row .inspector-attr { color:var(--inspector-string); }
685
+ .inspector-tree-row .inspector-dim { color:var(--muted); }
686
+ .inspector-tree-row .inspector-truncate { overflow:hidden; text-overflow:ellipsis; }
687
+ .inspector-details { min-width:0; min-height:0; display:flex; flex-direction:column; }
688
+ .inspector-subtabs { display:flex; gap:2px; border-bottom:1px solid var(--border); flex:none; overflow-x:auto; }
689
+ .inspector-subtabs button { border:0; border-bottom:2px solid transparent; border-radius:0; padding:4px 8px; white-space:nowrap; }
690
+ .inspector-subtabs button[aria-selected=true] { border-bottom-color:var(--accent); color:var(--accent); }
691
+ .inspector-rule { border-bottom:1px solid var(--border); padding:7px 9px; font:12px/1.7 ui-monospace,SFMono-Regular,Consolas,monospace; }
692
+ .inspector-rule-title { display:flex; justify-content:space-between; gap:8px; color:var(--ink); overflow-wrap:anywhere; margin-bottom:4px; }
693
+ .inspector-rule-origin { color:var(--muted); font:11px/1.6 Arial,sans-serif; text-align:right; }
694
+ .inspector-property { display:flex; align-items:center; gap:4px; padding:1px 0 1px 14px; min-height:24px; }
695
+ .inspector-property-name { color:light-dark(#881391,#c58af9); flex:none; }
696
+ .inspector-property input[type=text],.inspector-property input[type=number],.inspector-property select { border:1px solid transparent; background:transparent; min-width:30px; flex:1; width:100%; padding:0 2px; color:var(--inspector-string); border-radius:0; font:inherit; min-height:22px; }
697
+ .inspector-property input:focus,.inspector-property input:hover { border-color:var(--border); }
698
+ .inspector-property input[type=color] { width:14px; height:14px; padding:0; min-height:0; border:0; flex:none; background:transparent; cursor:pointer; }
699
+ .inspector-property input[type=color]::-webkit-color-swatch-wrapper { padding:0; }
700
+ .inspector-property input[type=color]::-webkit-color-swatch { border:1px solid var(--border); }
701
+ .inspector-property .inspector-revert { border:0; padding:0 3px; min-height:18px; color:var(--muted); background:transparent; }
702
+ .inspector-property[data-modified=true] { background:color-mix(in srgb,var(--accent) 8%,transparent); }
703
+ .inspector-expression { display:block; padding-left:14px; color:var(--muted); white-space:pre-wrap; overflow-wrap:anywhere; font-size:11px; }
704
+ .inspector-table { border-collapse:collapse; width:100%; font-size:11px; text-align:left; }
705
+ .inspector-table th { position:sticky; top:0; z-index:1; background:var(--surface); font-weight:400; border-bottom:1px solid var(--border); border-right:1px solid var(--border); padding:3px 7px; white-space:nowrap; }
706
+ .inspector-table th button { font:inherit; border:0; padding:0; min-height:18px; background:transparent; }
707
+ .inspector-table td { padding:3px 7px; border-bottom:1px solid color-mix(in srgb,var(--border) 45%,transparent); max-width:320px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; height:24px; }
708
+ .inspector-table tbody tr:nth-child(even) { background:color-mix(in srgb,var(--surface) 55%,transparent); }
709
+ .inspector-table tbody tr[aria-selected=true] { background:var(--inspector-selected); }
710
+ .inspector-table tbody tr:hover { background:var(--surface); }
711
+ .inspector-table .inspector-cell-button { text-align:left; border:0; background:transparent; min-height:18px; padding:0; color:inherit; max-width:100%; overflow:hidden; text-overflow:ellipsis; font:inherit; }
712
+ .inspector-table .inspector-number { font-variant-numeric:tabular-nums; text-align:right; }
713
+ .inspector-table .inspector-error { color:light-dark(#b3261e,#f2b8b5); }
714
+ .inspector-network-body { display:flex; flex:1; min-height:0; min-width:0; }
715
+ .inspector-network-body > .inspector-scroll { flex:1; min-width:0; }
716
+ .inspector-network-body[data-selected=true] > .inspector-scroll { flex:1 1 55%; }
717
+ .inspector-network-detail { display:flex; flex-direction:column; flex:1 1 45%; min-width:290px; min-height:0; border-left:1px solid var(--border); background:var(--paper); }
718
+ .inspector-network-detail[hidden] { display:none; }
719
+ .inspector-tile-row { cursor:pointer; }
720
+ .inspector-tile-heading { flex:none; justify-content:space-between; }
721
+ .inspector-tile-heading strong { font-size:12px; font-weight:500; }
722
+ .inspector-tile-heading button { border:0; font-size:18px; }
723
+ .inspector-tile-content { min-height:0; }
724
+ .inspector-tile-content select { width:100%; min-width:0; background:var(--paper); color:var(--ink); border:1px solid var(--border); padding:4px; }
725
+ .inspector-tile-paging { justify-content:space-between; }
726
+ .inspector-tile-feature { border-bottom:1px solid var(--border); padding:5px 8px; }
727
+ .inspector-tile-feature summary { cursor:pointer; overflow-wrap:anywhere; font-size:11px; }
728
+ .inspector-tile-feature details { padding-left:12px; }
729
+ .inspector-tile-feature pre { white-space:pre-wrap; overflow-wrap:anywhere; font:11px/1.6 ui-monospace,monospace; color:var(--inspector-string); margin:6px 0; }
730
+ .inspector-kv { display:grid; grid-template-columns:minmax(100px,auto) minmax(0,1fr); gap:5px 12px; font:12px/1.5 ui-monospace,monospace; padding:8px; margin:0; }
731
+ .inspector-kv dt { color:var(--muted); overflow-wrap:anywhere; }.inspector-kv dd { margin:0; overflow-wrap:anywhere; }
732
+ .inspector-note { color:var(--muted); font-size:11px; padding:6px 8px; margin:0; }
733
+ .inspector-status-message { color:var(--accent); padding:4px 8px; border-top:1px solid var(--border); font-size:11px; }
734
+ .inspector-shell .notice { position:static; padding:5px 9px; border:0; font:11px/1.5 ui-monospace,monospace; box-shadow:none; color:light-dark(#b3261e,#f2b8b5); }
735
+ .inspector-scene-content { display:flex; flex-direction:column; flex:1; min-height:0; min-width:0; width:100%; max-width:1000px; align-self:flex-start; }
736
+ .inspector-shell .map-controls { flex:1; width:100%; max-width:none; padding:0 16px 16px; border:0; overflow:auto; background:var(--paper); }
737
+ .inspector-shell .controls-grid,.inspector-shell #controls-modules { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:0 24px; align-items:start; }
738
+ .inspector-shell .controls-grid { border-bottom:1px solid var(--border); padding-bottom:8px; }
739
+ .inspector-shell .controls-selection,.inspector-shell .controls-runtime { border:0; padding-bottom:0; }
740
+ .inspector-shell .map-controls .control-field,.inspector-shell .control-module-header label { display:grid; grid-template-columns:min(36%,160px) minmax(0,1fr); align-items:center; gap:12px; min-height:28px; }
741
+ .inspector-shell .map-controls .control-field > select,.inspector-shell .map-controls .control-field > input[type=number] { justify-self:start; width:min(200px,100%); max-width:100%; }
742
+ .inspector-shell .map-controls input[type=checkbox] { appearance:none; -webkit-appearance:none; position:relative; justify-self:start; flex:none; width:34px; height:20px; min-height:20px; margin:0; padding:0; border:1px solid light-dark(#9aa0a6,#7c838d); border-radius:999px; background:light-dark(#e3e7ed,#444950); cursor:pointer; transition:background-color 120ms ease,border-color 120ms ease; }
743
+ .inspector-shell .map-controls input[type=checkbox]::before { content:''; position:absolute; top:2px; left:2px; width:14px; height:14px; border-radius:50%; background:light-dark(#fff,#c4c8cf); box-shadow:0 1px 2px #0003; transition:transform 120ms ease,background-color 120ms ease; }
744
+ .inspector-shell .map-controls input[type=checkbox]:checked { background:var(--accent); border-color:var(--accent); }
745
+ .inspector-shell .map-controls input[type=checkbox]:checked::before { transform:translateX(14px); background:light-dark(#fff,#202124); }
746
+ .inspector-shell .map-controls input[type=checkbox]:hover:not(:disabled) { box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 12%,transparent); }
747
+ .inspector-shell .map-controls input[type=checkbox]:focus-visible { outline:2px solid var(--accent); outline-offset:3px; }
748
+ .inspector-shell .map-controls input[type=checkbox]:disabled { cursor:not-allowed; }
749
+ .inspector-shell .map-controls .control-field:has(input[type=checkbox]:not(:disabled)) { cursor:pointer; }
750
+ .inspector-shell .controls-heading button { min-width:60px; }
751
+ .inspector-shell .controls-segments button { padding:4px 10px; border-radius:0; }
752
+ @media (prefers-reduced-motion:reduce) { .inspector-shell .map-controls input[type=checkbox],.inspector-shell .map-controls input[type=checkbox]::before { transition:none; } }
753
+ .inspector-shell .control-module { border:0; border-bottom:1px solid var(--border); border-radius:0; background:var(--paper); margin:0; }
754
+ .inspector-shell .control-module-header { padding:5px 0; }.inspector-shell .control-module-header label { font-size:12px; font-weight:400; }
755
+ .inspector-shell .control-module fieldset { min-width:0; padding:0 0 8px 16px; }
756
+ .inspector-shell .controls-section-title { margin:10px 0 4px; font-size:11px; letter-spacing:0; text-transform:none; }
757
+ .inspector-shell .control-field { margin:5px 0; }
758
+ @container(max-width:680px) { .inspector-shell .controls-grid,.inspector-shell #controls-modules { grid-template-columns:minmax(0,1fr); }.inspector-shell .map-controls .control-field,.inspector-shell .control-module-header label { grid-template-columns:min(36%,160px) minmax(0,1fr); }.inspector-shell .controls-segments button { padding-inline:8px; } }
759
+ .inspector-shell .controls-code { background:var(--surface); }.inspector-shell .controls-selection .control-field select { max-width:100%; }
760
+ .inspector-shell .panel-heading h2 { font:12px Arial,sans-serif; }.inspector-shell .review-tabs { margin:0; padding:0; }
761
+ .inspector-assets-content { scrollbar-gutter:stable; }
762
+ .inspector-font-list { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,350px),1fr)); max-width:1100px; }
763
+ .inspector-font-card { min-width:0; margin:0; padding:14px 12px; border-bottom:1px solid var(--border); }
764
+ .inspector-font-card figcaption { margin-bottom:8px; font:12px/1.5 ui-monospace,monospace; }
765
+ .inspector-font-sample { min-height:94px; overflow-x:auto; border:1px solid var(--border); border-radius:3px; background:#fff; color:#303b4b; }
766
+ .inspector-font-preview { display:block; }
767
+ .inspector-font-loading { display:block; padding:15px 10px; font-size:11px; color:#62676e; }
768
+ .inspector-sprite-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(112px,1fr)); gap:1px; padding:1px; background:var(--border); }
769
+ .inspector-sprite-card { margin:0; min-width:0; padding:10px 8px 8px; background:var(--paper); text-align:center; }
770
+ .inspector-sprite-card:hover { background:var(--inspector-selected); }
771
+ .inspector-sprite-preview { height:60px; display:flex; align-items:center; justify-content:center; border:1px solid #dadce0; border-radius:3px; background:repeating-conic-gradient(#e8eaed 0% 25%,#fff 0% 50%) 0 0 / 12px 12px; color:#62676e; }
772
+ .inspector-sprite-card figcaption { margin-top:7px; font:11px/1.4 ui-monospace,monospace; overflow-wrap:anywhere; }
773
+ .inspector-sprite-size { display:block; min-height:15px; margin-top:3px; color:var(--muted); font-size:10px; }
774
+ .inspector-geometry-json { width:100%; min-height:100px; resize:vertical; background:var(--paper); color:var(--ink); border:1px solid var(--border); font:11px/1.5 ui-monospace,monospace; padding:6px; }
775
+ .inspector-vertex { position:absolute; width:10px; height:10px; border:2px solid #1a73e8; background:#fff; transform:translate(-50%,-50%); pointer-events:auto; padding:0; min-height:0; border-radius:1px; cursor:move; touch-action:none; }
776
+ .inspector-geometry-handles { position:absolute; inset:0; pointer-events:none; z-index:5; overflow:hidden; }
777
+ .inspector-performance { padding:10px; }.inspector-performance svg { width:100%; height:90px; display:block; }.inspector-performance dl { max-width:600px; }
778
+ .inspector-overdraw-help { max-width:640px; margin:10px 10px 0; padding:10px 12px; border:1px solid var(--border); border-left:3px solid var(--accent); border-radius:4px; background:var(--surface); }
779
+ .inspector-overdraw-help[hidden] { display:none; }
780
+ .inspector-overdraw-help strong { font-size:12px; }.inspector-overdraw-help p { margin:5px 0 0; line-height:1.5; }
781
+ .inspector-overdraw-legend { display:flex; flex-wrap:wrap; gap:6px 20px; margin:10px 0 8px; }
782
+ .inspector-overdraw-key { display:inline-flex; align-items:center; gap:6px; font:11px/1.4 ui-monospace,monospace; }
783
+ .inspector-overdraw-swatch { width:24px; height:10px; border:1px solid #0002; border-radius:2px; }
784
+ .inspector-overdraw-caption { color:var(--muted); font-size:11px; }
785
+ .inspector-badge { background:var(--inspector-selected); color:var(--accent); border-radius:7px; padding:0 5px; margin-left:3px; font:10px/1.5 Arial,sans-serif; }
786
+ [data-dock=left] .inspector-inspect-grid,[data-dock=right] .inspector-inspect-grid { display:flex; flex-direction:column; }
787
+ [data-dock=left] .inspector-tree-column,[data-dock=right] .inspector-tree-column { flex:0 0 32%; border-right:0; border-bottom:1px solid var(--border); }
788
+ [data-dock=left] .inspector-details,[data-dock=right] .inspector-details { flex:1; }
789
+ .inspector-zoom { padding:8px; }.inspector-zoom > select { width:100%; margin-bottom:8px; }
790
+ @container(max-width:680px) { .inspector-inspect-grid { display:flex;flex-direction:column; }.inspector-tree-column { flex:0 0 32%;border-right:0;border-bottom:1px solid var(--border); }.inspector-details { flex:1; } }
791
+ `;
792
+ var inspectorStyles2 = legacyStyles + inspectorStyles + cardStyles;
793
+
794
+ // src/inspector/theme-labels.ts
795
+ var themeLabels = {
796
+ light: "Light",
797
+ default: "Default",
798
+ faded: "Faded",
799
+ monochrome: "Monochrome"
800
+ };
801
+ var defaultPaletteMapIds = ["streets", "map-1777"];
802
+
803
+ // src/workbench-client.ts
804
+ var workbenchClient = String.raw`
805
+ let wbCatalog;
806
+ let wbLastContext = "";
807
+ let wbInterval;
808
+ let wbRefreshing = false;
809
+ let wbError = "";
810
+ let wbLastPublished = 0;
811
+ let wbPickerKey = "";
812
+ let wbTimeOfDay = "";
813
+ const wbPaletteColors = new Map();
814
+ const wbThemeLabels = ${JSON.stringify(themeLabels)};
815
+ const wbDefaultPaletteMaps = ${JSON.stringify(defaultPaletteMapIds)};
816
+ const wbThemeLabel = (theme, metadata) => theme === "light" && wbDefaultPaletteMaps.some(id => metadata?.["tileflow:map"] === id || (Array.isArray(metadata?.["tileflow:extends"]) && metadata["tileflow:extends"].includes(id))) ? "Default" : Object.hasOwn(wbThemeLabels,theme) ? wbThemeLabels[theme] : theme.replace(/[-_]/g," ");
817
+ const wbClient = "view-" + Math.random().toString(36).slice(2);
818
+ const wb = (id) => document.getElementById("wb-" + id);
819
+ if (options.workbenchUrl) initializeWorkbench();
820
+
821
+ function initializeWorkbench() {
822
+ wb("theme").addEventListener("change", () => selectWorkbenchAppearance());
823
+ for (const [moment, label] of Object.entries({day:"Day", dawn:"Dawn", dusk:"Dusk", night:"Night"})) {
824
+ const segment = document.createElement("button");
825
+ segment.type = "button"; segment.value = moment; segment.textContent = label;
826
+ segment.setAttribute("role", "radio"); segment.setAttribute("aria-checked", "false");
827
+ segment.tabIndex = -1; segment.disabled = true;
828
+ segment.addEventListener("click", () => selectWorkbenchAppearance(moment));
829
+ segment.addEventListener("keydown", (event) => {
830
+ const direction = {ArrowRight:1, ArrowDown:1, ArrowLeft:-1, ArrowUp:-1}[event.key];
831
+ if (!direction && event.key !== "Home" && event.key !== "End") return;
832
+ event.preventDefault();
833
+ const enabled = [...wb("time-of-day").querySelectorAll("button")].filter((item) => !item.disabled);
834
+ if (!enabled.length) return;
835
+ const index = event.key === "Home" ? 0 : event.key === "End" ? enabled.length - 1 : (enabled.indexOf(segment) + direction + enabled.length) % enabled.length;
836
+ const next = enabled[index];
837
+ selectWorkbenchAppearance(next.value); next.focus();
838
+ });
839
+ wb("time-of-day").append(segment);
840
+ }
841
+ void refreshWorkbench();
842
+ wbInterval = setInterval(refreshWorkbench, 2500);
843
+ addEventListener("beforeunload", () => clearInterval(wbInterval));
844
+ addEventListener("keydown", (event) => {
845
+ if (event.target.closest?.("input,select,textarea,[contenteditable]")) return;
846
+ if (event.key === "i" && !event.metaKey && !event.ctrlKey) setInspectionEnabled(!inspectionEnabled, true);
847
+ });
848
+ }
849
+
850
+ async function workbenchRequest(path, body) {
851
+ const response = await fetch(options.workbenchUrl + path, body === undefined
852
+ ? {cache:"no-store"}
853
+ : {method:"POST", headers:{"Content-Type":"application/json", "X-Tileflow-Workbench":"1"}, body:JSON.stringify(body)});
854
+ const result = await response.json();
855
+ if (!response.ok) throw new Error(result.message || "Inspector request failed.");
856
+ return result;
857
+ }
858
+
859
+ async function refreshWorkbench() {
860
+ if (wbRefreshing) return;
861
+ wbRefreshing = true;
862
+ try {
863
+ wbCatalog = await workbenchRequest("");
864
+ const side = states[activeSideId];
865
+ const owner = wbCatalog.sides[side.serviceId || side.id];
866
+ resolveWorkbenchAppearance(side, owner);
867
+ if (side.bridgeConnected && side.workbenchMap && side.workbenchTheme && !side.workbenchRestored) {
868
+ side.workbenchRestored = true;
869
+ if (side.id === "left") {
870
+ const query = new URL(location.href).searchParams;
871
+ const requested = owner?.selections.find((entry) => !entry.scene && entry.map === side.workbenchMap && entry.id === query.get("view"));
872
+ if (requested) {
873
+ side.requestedTimeOfDay = query.get("time-of-day");
874
+ applyWorkbenchAppearance(side, requested);
875
+ }
876
+ }
877
+ }
878
+ syncWorkbenchPickers();
879
+ const failure = side.runtimeFailed ? side.runtimeErrors.at(-1) : owner?.status === "invalid" ? owner.diagnostics.map((item) => item.message).join(" ") : wbError;
880
+ wb("notice").hidden = !failure;
881
+ wb("notice").textContent = failure ? (owner?.status === "invalid" ? "Showing the last available preview. " : "") + failure : "";
882
+ await publishWorkbenchContext();
883
+ } catch (error) { showWorkbenchError(error); }
884
+ finally { wbRefreshing = false; }
885
+ }
886
+
887
+ function resolveWorkbenchAppearance(side, owner) {
888
+ const source = new URL(side.definition.previewUrl, location.href);
889
+ const startup = owner?.selections.find((entry) => entry.scene && entry.scene === source.searchParams.get("scene"));
890
+ const metadata = side.bridgeConnected ? side.map?.getStyle?.()?.metadata : undefined;
891
+ // The command chooses the map. Never infer it from catalog order or a saved URL's view.
892
+ side.workbenchMap ||= source.searchParams.get("map") || startup?.map || metadata?.["tileflow:map"];
893
+ if (!side.pendingAppearance && metadata?.["tileflow:map"] === side.workbenchMap) side.workbenchTheme = metadata?.["tileflow:theme"] || side.workbenchTheme;
894
+ side.workbenchTheme ||= source.searchParams.get("theme") || startup?.theme;
895
+ }
896
+
897
+ function applyWorkbenchAppearance(side, selected) {
898
+ if (selected.map !== side.workbenchMap || selected.scene) return;
899
+ const changed = selected.theme !== side.workbenchTheme;
900
+ side.workbenchTheme = selected.theme;
901
+ if (changed || side.pendingAppearance) {
902
+ side.pendingAppearance = selected;
903
+ side.definition.label = selected.label;
904
+ scheduleMapControls();
905
+ void loadMapControls();
906
+ }
907
+ syncWorkbenchPickers();
908
+ writeUrlState();
909
+ if (side.id === "left") {
910
+ const url = new URL(location.href);
911
+ url.searchParams.set("view", selected.id);
912
+ const moment = wbTimeOfDay;
913
+ if (moment) url.searchParams.set("time-of-day", moment); else url.searchParams.delete("time-of-day");
914
+ history.replaceState(null, "", url);
915
+ }
916
+ }
917
+
918
+ function syncWorkbenchPickers() {
919
+ const side = states[activeSideId];
920
+ const owner = wbCatalog?.sides[side.serviceId || side.id];
921
+ const selections = owner?.selections.filter((entry) => entry.map === side.workbenchMap) || [];
922
+ const selectedTheme = side.workbenchTheme;
923
+ if (!side.camera || !selections.length || !selectedTheme) {
924
+ wbPickerKey = "";
925
+ wb("theme").replaceChildren(new Option("Waiting for map…", ""));
926
+ wb("theme").disabled = true;
927
+ wb("time-of-day-field").hidden = true;
928
+ wbTimeOfDay = "";
929
+ return;
930
+ }
931
+ const variants = owner?.timeOfDay?.[side.workbenchMap] || {};
932
+ const themeMetadata = side.map?.getStyle?.()?.metadata;
933
+ const key = JSON.stringify([activeSideId, selectedTheme, selections, variants, side.requestedTimeOfDay, themeMetadata?.["tileflow:map"], themeMetadata?.["tileflow:extends"]]);
934
+ if (key === wbPickerKey) return;
935
+ wbPickerKey = key;
936
+ const allThemes = [...new Set(selections.map((entry) => entry.theme))];
937
+ const family = Object.keys(variants).find((name) => Object.values(variants[name]).includes(selectedTheme));
938
+ const members = new Set(Object.values(variants).flatMap((moments) => Object.values(moments)));
939
+ const themes = allThemes.filter((name) => !members.has(name) || Object.hasOwn(variants, name));
940
+ if (themeMetadata?.["tileflow:map"] === "streets" || (Array.isArray(themeMetadata?.["tileflow:extends"]) && themeMetadata["tileflow:extends"].includes("streets"))) {
941
+ const streetsOrder = new Map([["light", 0], ["faded", 1], ["monochrome", 2]]);
942
+ themes.sort((left, right) => (streetsOrder.get(left) ?? 3) - (streetsOrder.get(right) ?? 3));
943
+ }
944
+ const moments = family ? variants[family] : {};
945
+ wbTimeOfDay = moments[side.requestedTimeOfDay] === selectedTheme ? side.requestedTimeOfDay : Object.keys(moments).find((moment) => moments[moment] === selectedTheme) || "";
946
+ for (const segment of wb("time-of-day").querySelectorAll("button")) {
947
+ const selected = segment.value === wbTimeOfDay;
948
+ segment.setAttribute("aria-checked", String(selected));
949
+ segment.tabIndex = selected ? 0 : -1;
950
+ segment.disabled = !moments[segment.value];
951
+ }
952
+ wb("time-of-day-field").hidden = !family;
953
+ wb("theme").replaceChildren(...themes.map((theme) => new Option(wbThemeLabel(theme,themeMetadata), theme)));
954
+ wb("theme").value = family || selectedTheme;
955
+ wb("theme").disabled = themes.length < 2;
956
+ wb("palettes").replaceChildren(...themes.map(theme => {
957
+ const button=document.createElement("button");button.type="button";button.setAttribute("role","radio");button.setAttribute("aria-checked",String(theme===(family||selectedTheme)));
958
+ const swatches=document.createElement("span");swatches.className="inspector-palette-swatches";
959
+ const label=document.createElement("span");label.textContent=wbThemeLabel(theme,themeMetadata);button.append(swatches,label);
960
+ button.onclick=()=>{wb("theme").value=theme;selectWorkbenchAppearance();};
961
+ const concrete=variants[theme]?.[wbTimeOfDay] || variants[theme]?.day || theme;
962
+ const fill=colors=>{if(!button.isConnected)return;swatches.replaceChildren(...colors.map(color=>{const swatch=document.createElement("i");swatch.style.background=color;return swatch;}));};
963
+ if(wbPaletteColors.has(concrete))queueMicrotask(()=>fill(wbPaletteColors.get(concrete)));
964
+ else {
965
+ const url=new URL(side.definition.basePath+"/styles/"+side.workbenchMap+"/"+concrete+".json",location.href);
966
+ fetch(url).then(response=>{if(!response.ok)throw new Error("Theme unavailable");return response.json();}).then(style=>{
967
+ const choices=[/background/,/water/,/land.*(?:park|vegetation)|vegetation/,/building/];
968
+ const colors=choices.map(pattern=>style.layers.flatMap(layer=>pattern.test(layer.id)||pattern.test(layer.type)?Object.entries(layer.paint||{}).filter(([key,value])=>key.endsWith("-color")&&typeof value==="string").map(([,value])=>value):[])[0]).filter(Boolean);
969
+ wbPaletteColors.set(concrete,colors);fill(colors);
970
+ }).catch(()=>{swatches.textContent="—";});
971
+ }
972
+ return button;
973
+ }));
974
+ }
975
+
976
+ function selectWorkbenchAppearance(requested = wbTimeOfDay) {
977
+ const side = states[activeSideId];
978
+ const owner = wbCatalog?.sides[side.serviceId || side.id];
979
+ const moments = owner?.timeOfDay?.[side.workbenchMap]?.[wb("theme").value];
980
+ const moment = moments?.[requested] ? requested : moments ? Object.keys(moments)[0] : undefined;
981
+ const theme = moments?.[moment] || wb("theme").value;
982
+ const selected = owner?.selections.find((entry) => !entry.scene && entry.map === side.workbenchMap && entry.theme === theme);
983
+ if (!selected) { wbPickerKey = ""; syncWorkbenchPickers(); return; }
984
+ side.requestedTimeOfDay = moment;
985
+ applyWorkbenchAppearance(side, selected);
986
+ }
987
+
988
+ function currentWorkbenchDefinition() {
989
+ const side = states[activeSideId];
990
+ const camera = side.camera || canonicalCamera;
991
+ const metadata = side.map?.getStyle?.().metadata;
992
+ if (!camera || !validPortableName(metadata?.["tileflow:map"]) || !validConcreteThemeName(metadata?.["tileflow:theme"]))
993
+ throw new Error("Wait for the current map to finish loading.");
994
+ const container = side.map?.getContainer?.();
995
+ const width = boundedInteger(container?.clientWidth, 64, 4096, 1280);
996
+ const height = boundedInteger(container?.clientHeight, 64, 4096, 800);
997
+ return {map:metadata["tileflow:map"], theme:metadata["tileflow:theme"],
998
+ camera:{type:"center", center:[roundNumber(camera.center[0]),roundNumber(camera.center[1])], zoom:roundNumber(camera.zoom), bearing:roundNumber(camera.bearing), pitch:roundNumber(camera.pitch)},
999
+ viewport:{width,height,dpr:1}, target:{kind:"map"}};
1000
+ }
1001
+
1002
+ function workbenchContext() {
1003
+ const side = states[activeSideId];
1004
+ const layerObservation = globalThis.__tileflowInspectorUI?.layerContext?.();
1005
+ const selected = !layerObservation && selectedInspection?.side === side ? selectedInspection : undefined;
1006
+ const feature = selected?.features[selected.featureIndex];
1007
+ const style = side.map?.getStyle?.();
1008
+ const metadata = style?.metadata;
1009
+ const layer = feature && style?.layers?.find((entry) => entry.id === feature.layerId);
1010
+ const provenance = feature && findSidecarLayer(side.sidecar, feature.layerId);
1011
+ let definition;
1012
+ try { definition = currentWorkbenchDefinition(); } catch { /* Loading previews have no camera yet. */ }
1013
+ const themeVariants = wbCatalog?.sides[side.serviceId || side.id]?.timeOfDay?.[definition?.map] || {};
1014
+ const themeFamily = definition && Object.keys(themeVariants).find(name => Object.values(themeVariants[name]).includes(definition.theme));
1015
+ const appearance = definition ? {theme:wbThemeLabel(themeFamily || definition.theme,metadata), themeId:themeFamily || definition.theme,
1016
+ timeOfDay:themeFamily ? (themeVariants[themeFamily][wbTimeOfDay] === definition.theme ? wbTimeOfDay : Object.keys(themeVariants[themeFamily]).find(moment => themeVariants[themeFamily][moment] === definition.theme)) : undefined,
1017
+ concreteThemeId:definition.theme} : undefined;
1018
+ const configuration = side.definition.captureConfig ? " --config " + quoteCliArgument(side.definition.captureConfig) : "";
1019
+ const semanticOwner = provenance && sidecarLayerWithSemanticAttribution(provenance).semanticAttribution[0]?.semanticOwner;
1020
+ const captureCommand = definition ? "tileflow capture" + configuration + " --map " + quoteCliArgument(definition.map) + " --theme " + quoteCliArgument(definition.theme) +
1021
+ " --center=" + definition.camera.center.join(",") + " --zoom=" + definition.camera.zoom + " --bearing=" + definition.camera.bearing + " --pitch=" + definition.camera.pitch +
1022
+ " --width=" + definition.viewport.width + " --height=" + definition.viewport.height + " --dpr=" + definition.viewport.dpr + " --json" : undefined;
1023
+ return {schemaVersion:1, source:"browser-observation", config:side.definition.captureConfig || "tileflow.config.ts",
1024
+ status:side.artifactStatus, generation:side.artifactGeneration, inspectionEnabled,
1025
+ displayedGeneration:Number(side.frame.contentDocument?.documentElement.dataset.tileflowArtifactGeneration), definition, appearance,
1026
+ captureSemantics:"Compiled map at the observed camera and viewport; temporary preview edits are excluded.",
1027
+ review:{referenceId:null, before:null, after:null, liveMode:stage.dataset.mode, source:"map"},
1028
+ commands:{inspect:"tileflow inspect" + configuration + (semanticOwner ? " --domain " + quoteCliArgument(semanticOwner) : " --limit 80") + " --json", capture:captureCommand,
1029
+ session:"tileflow inspect session --url " + quoteCliArgument(location.origin) + " --json"},
1030
+ selection:layerObservation || (feature ? {coordinate:selected.coordinate,
1031
+ provenance:provenance ? sidecarLayerWithSemanticAttribution(provenance).semanticAttribution : [],
1032
+ provenanceScope:"Semantic ownership only; exact file, line and token are not inferred.",
1033
+ feature, layer:layer ? {id:layer.id,type:layer.type,source:layer.source,sourceLayer:layer["source-layer"],minzoom:layer.minzoom,maxzoom:layer.maxzoom,filter:layer.filter,paint:layer.paint,layout:layer.layout} : undefined} : null)};
1034
+ }
1035
+
1036
+ function boundedWorkbenchContext() {
1037
+ const observed = workbenchContext();
1038
+ const nodeLimit = observed.selection?.kind === "layer" ? 2400 : 200;
1039
+ let entries = 0;
1040
+ const omitted = [];
1041
+ function visit(value, path, depth) {
1042
+ if (++entries > nodeLimit || depth > 16) { omitted.push(path); return undefined; }
1043
+ if (typeof value === "string") { if (value.length > 512) omitted.push(path); return value.slice(0,512); }
1044
+ if (!value || typeof value !== "object") return value;
1045
+ if (Array.isArray(value)) {
1046
+ if (value.length > 32) omitted.push(path);
1047
+ return value.slice(0,32).map((entry,index) => visit(entry,path + "/" + index,depth + 1));
1048
+ }
1049
+ return Object.fromEntries(Object.entries(value).flatMap(([key,entry]) => {
1050
+ const result = visit(entry,path + "/" + key.replaceAll("~","~0").replaceAll("/","~1"),depth + 1);
1051
+ return result === undefined ? [] : [[key,result]];
1052
+ }));
1053
+ }
1054
+ const context = visit(observed,"",0);
1055
+ context.projection = {truncated:omitted.length > 0, omitted:omitted.slice(0,32)};
1056
+ return context;
1057
+ }
1058
+
1059
+ async function publishWorkbenchContext() {
1060
+ const side = states[activeSideId];
1061
+ if (!side.bridgeReady) return;
1062
+ const context = boundedWorkbenchContext();
1063
+ const key = JSON.stringify(context);
1064
+ if (key === wbLastContext && Date.now() - wbLastPublished < 60_000) return;
1065
+ await workbenchRequest("/context", {client:wbClient, side:side.serviceId || side.id, generation:side.artifactGeneration, context});
1066
+ wbLastContext = key;
1067
+ wbLastPublished = Date.now();
1068
+ }
1069
+
1070
+ function featureDisplayName(feature) {
1071
+ return String(feature.properties?.name || feature.properties?.class || feature.layerId.replace(/^tileflow-/, "").replace(/-render-/, " · ") || "Map element")
1072
+ .replace(/([a-z])([A-Z])/g, "$1 $2").replaceAll("_", " ").replaceAll("-", " ").replace(/^./, (letter) => letter.toUpperCase());
1073
+ }
1074
+
1075
+ function renderFeatureSummary(side, feature, layer, picker, raw) {
1076
+ const section = document.createElement("section"); section.className = "feature-summary";
1077
+ const heading = document.createElement("h3");
1078
+ heading.textContent = featureDisplayName(feature);
1079
+ const kind = document.createElement("div"); kind.className = "compact-note";
1080
+ const kinds = {fill:"Area", line:"Line", symbol:"Label or icon", "fill-extrusion":"3D building", circle:"Point", raster:"Imagery", hillshade:"Terrain shading", background:"Background"};
1081
+ kind.textContent = (kinds[layer?.type] || layer?.type || feature.geometryType) + " · Zoom " + (side.camera?.zoom ?? 0).toFixed(2);
1082
+ const headingStyle = document.createElement("h4"); headingStyle.textContent = "Appearance here";
1083
+ section.append(heading, kind, headingStyle);
1084
+ const values = document.createElement("dl"); values.className = "property-list";
1085
+ const labels = {"fill-color":"Fill color", "fill-opacity":"Opacity", "fill-outline-color":"Outline", "line-color":"Line color", "line-width":"Width", "line-opacity":"Opacity", "line-gap-width":"Gap", "text-color":"Text color", "text-halo-color":"Text outline", "text-halo-width":"Outline width", "text-opacity":"Text opacity", "text-size":"Text size", "icon-color":"Icon color", "icon-opacity":"Icon opacity", "fill-extrusion-color":"Building color", "fill-extrusion-height":"Height", "fill-extrusion-opacity":"Opacity"};
1086
+ for (const [name, rawValue] of Object.entries({...layer?.paint, ...(layer?.layout?.["text-size"] !== undefined ? {"text-size":layer.layout["text-size"]} : {})}).slice(0,8)) {
1087
+ const value = evaluateBasicZoomValue(rawValue, side.camera?.zoom ?? 0);
1088
+ const label = document.createElement("dt"); label.textContent = labels[name] || name.replaceAll("-", " "); label.title = name;
1089
+ const detail = document.createElement("dd"); detail.textContent = value === unsupportedValue ? "Varies by feature" : formatCurveValue(value);
1090
+ if (typeof value === "string" && /^(#[0-9a-f]{3,8}|rgba?\(|hsla?\()/i.test(value)) {
1091
+ const swatch = document.createElement("span"); swatch.className = "color-swatch"; swatch.style.backgroundColor = value; swatch.setAttribute("aria-hidden", "true"); detail.append(swatch);
1092
+ }
1093
+ values.append(label,detail);
1094
+ }
1095
+ section.append(values);
1096
+ const provenance = findSidecarLayer(side.sidecar, feature.layerId);
1097
+ const owners = provenance ? sidecarLayerWithSemanticAttribution(provenance).semanticAttribution : [];
1098
+ const label = document.createElement("h4"); label.textContent = "Where to edit";
1099
+ section.append(label);
1100
+ const paths = [...new Set(owners.flatMap((owner) => owner.authoringPaths))];
1101
+ for (const path of paths.slice(0,8)) { const code = document.createElement("code"); code.className = "authoring-path"; code.textContent = path; section.append(code); }
1102
+ const note = document.createElement("p"); note.className = "compact-note";
1103
+ note.textContent = paths.length ? "Edit these configuration sections in your editor or ask your coding agent. The map updates here." : "No semantic configuration path is available for this layer.";
1104
+ section.append(note);
1105
+ const details = document.createElement("details"); const summary = document.createElement("summary"); summary.textContent = "Feature data";
1106
+ const provenanceNote = document.createElement("p"); provenanceNote.className = "compact-note"; provenanceNote.textContent = "Configuration paths describe semantic ownership. Exact source lines and tokens are not reported.";
1107
+ details.append(summary,provenanceNote,raw);
1108
+ inspectionOutput.append(section,picker,details);
1109
+ }
1110
+
1111
+ function showWorkbenchError(error) {
1112
+ const message = safeErrorMessage(error);
1113
+ wbError = message;
1114
+ setFeedback(message);
1115
+ wb("notice").textContent = message; wb("notice").hidden = false;
1116
+ }
1117
+ `;
1118
+
1119
+ // src/workbench-controls.ts
1120
+ var controlsStyles = `
1121
+ .map-controls { flex:0 0 360px; width:360px; max-width:45vw; min-height:0; overflow:auto; border-left:1px solid var(--border); background:var(--paper); padding:0 18px 18px; }
1122
+ .map-controls[hidden] { display:none; }
1123
+ .controls-heading { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:8px 0; border-bottom:1px solid var(--border); }
1124
+ .controls-heading h2 { margin:0; font:inherit; }
1125
+ .controls-segments { display:flex; min-width:0; width:max-content; max-width:100%; border:1px solid var(--border); border-radius:4px; overflow:hidden; }
1126
+ .controls-segments button { flex:1; min-width:0; padding:4px 10px; border:0; border-radius:0; background:var(--paper); color:var(--muted); cursor:pointer; }
1127
+ .controls-segments button + button { border-left:1px solid var(--border); }
1128
+ .controls-segments button[aria-checked=true] { color:var(--accent); background:var(--inspector-selected,var(--surface)); box-shadow:inset 0 -2px var(--accent); }
1129
+ .controls-segments button:disabled { opacity:.4; cursor:not-allowed; }
1130
+ .controls-selection { border-bottom:1px solid var(--border); padding-bottom:8px; }
1131
+ .controls-selection .control-field select { width:190px; max-width:70%; }
1132
+ .controls-selection [hidden] { display:none; }
1133
+ .controls-section-title { font-size:10px; letter-spacing:.1em; text-transform:uppercase; color:var(--muted); margin:20px 0 9px; }
1134
+ .control-module { border:1px solid var(--border); border-radius:8px; margin:7px 0; background:#fffdf7; overflow:hidden; }
1135
+ .control-module-header { display:flex; align-items:center; justify-content:space-between; padding:10px 12px; gap:8px; }
1136
+ .control-module-header label { display:flex; align-items:center; justify-content:space-between; flex:1; flex-direction:row; gap:10px; font-size:13px; font-weight:600; cursor:pointer; }
1137
+ .control-module-header input { appearance:none; width:30px; height:18px; min-height:0; padding:0; border:0; border-radius:12px; background:#d5cec1; cursor:pointer; position:relative; flex:none; }
1138
+ .control-module-header input::before { content:''; position:absolute; width:12px; height:12px; top:3px; left:3px; border-radius:50%; background:#fff; }
1139
+ .control-module-header input:checked { background:var(--accent); }
1140
+ .control-module-header input:checked::before { transform:translateX(12px); }
1141
+ .control-module-header input:focus-visible { outline:2px solid var(--accent); outline-offset:3px; }
1142
+ .control-module fieldset { border:0; padding:0 12px 12px; margin:0; }
1143
+ .control-module fieldset[hidden] { display:none; }
1144
+ .control-field { display:flex; align-items:center; justify-content:space-between; flex-direction:row; gap:12px; font-size:12px; margin:9px 0; }
1145
+ .control-field select,.control-field input[type=number] { width:132px; min-width:0; padding:5px 7px; font:inherit; }
1146
+ .control-field input[type=checkbox] { accent-color:var(--accent); }
1147
+ .controls-runtime { border:0; border-bottom:1px solid var(--border); padding:0 0 12px; margin:0; min-width:0; }
1148
+ #controls-modules { border:0; padding:0; margin:0; min-width:0; }
1149
+ .controls-runtime:disabled,#controls-modules:disabled { opacity:.6; }
1150
+ #controls-status { font-size:12px; line-height:1.5; color:var(--muted); margin:8px 0; overflow-wrap:anywhere; }
1151
+ #controls-status[data-error=true] { color:#a32e36; }
1152
+ @media(max-width:760px) { .workspace:has(.map-controls:not([hidden])) { display:flex; flex-direction:column; } .map-controls { flex:0 1 55vh; width:auto; max-width:none; border-left:0; border-top:1px solid var(--border); } .workspace:has(.map-controls:not([hidden])) .stage { flex:1 0 35vh; } }
1153
+ `;
1154
+ var controlsPanel = `
1155
+ <aside class="map-controls" id="map-controls" aria-label="Scene settings" tabindex="-1" hidden>
1156
+ <div class="controls-heading"><h2>Styles</h2><button id="controls-reset">Reset</button></div>
1157
+ <div class="controls-grid">
1158
+ <div class="controls-selection workbench-only inspector-card" id="controls-selection" hidden>
1159
+ <div class="controls-section-title">Appearance</div>
1160
+ <div class="control-field" id="wb-time-of-day-field" hidden><span>Time of day</span><div class="controls-segments" id="wb-time-of-day" role="radiogroup" aria-label="Time of day"></div></div>
1161
+ <div class="control-field inspector-palette-field" id="wb-theme-field"><span>Theme</span><select id="wb-theme" aria-label="Theme" disabled hidden><option value="">Waiting for map\u2026</option></select><div id="wb-palettes" class="inspector-palettes" role="radiogroup" aria-label="Theme"></div></div>
1162
+ </div>
1163
+ <fieldset class="controls-runtime inspector-card" id="controls-runtime">
1164
+ <div class="controls-section-title">Preview rendering</div>
1165
+ <label class="control-field">3D view<input id="controls-3d" type="checkbox" role="switch"></label>
1166
+ <label class="control-field">Building animation<select id="controls-animation" hidden><option value="0">Off</option><option value="800">Grow</option><option value="fade">Fade</option></select><span class="controls-segments" id="controls-animation-segments" role="radiogroup" aria-label="Building animation"></span></label>
1167
+ </fieldset>
1168
+ </div>
1169
+ <section class="inspector-card controls-module-card">
1170
+ <div class="controls-section-title">Map modules</div>
1171
+ <fieldset id="controls-modules"></fieldset>
1172
+ </section>
1173
+ <p id="controls-status" role="status" hidden></p>
1174
+ </aside>
1175
+ `;
1176
+ var controlsClient = String.raw`
1177
+ const ctl = (id) => document.getElementById("controls-" + id);
1178
+ const controlsPanel = document.getElementById("map-controls");
1179
+ const controlsButton = document.getElementById("controls-toggle");
1180
+ let controlsKey = "", controlsSequence = 0, controlsModel, controlsDraft = {}, controlsBusy = false;
1181
+ let controlsApplyTimer, controlsRevision = 0, controlsPending = false, controlsResetPending = false;
1182
+ let controlsLoadingKey = "", controlsBindings = [], controlsLastError = "";
1183
+ let controlsHistoryCurrent, controlsInitial, controlsHistoryQuiet = 0;
1184
+ const controlsUndo = [], controlsRedo = [];
1185
+ ctl("selection").hidden = !options.workbenchUrl;
1186
+ controlsButton.addEventListener("click", () => toggleMapControls(controlsPanel.hidden, true));
1187
+ ctl("reset").addEventListener("click", () => {
1188
+ if (!controlsModel) return;
1189
+ controlsDraft = {};
1190
+ ctl("3d").checked = false; ctl("animation").value = "0";
1191
+ renderMapControls();
1192
+ scheduleMapControls(true);
1193
+ });
1194
+ for (const name of ["3d", "animation"]) ctl(name).addEventListener("change", () => { syncAnimationSegments(); scheduleMapControls(); });
1195
+ for (const [value,label] of [["0","Off"],["800","Grow"],["fade","Fade"]]) {
1196
+ const button=document.createElement("button"); button.type="button";button.value=value;button.textContent=label;button.setAttribute("role","radio");
1197
+ button.addEventListener("click",()=>{ctl("animation").value=value;ctl("animation").dispatchEvent(new Event("change"));});ctl("animation-segments").append(button);
1198
+ }
1199
+ function syncAnimationSegments(){for(const button of ctl("animation-segments").children)button.setAttribute("aria-checked",String(button.value===ctl("animation").value));}
1200
+ syncAnimationSegments();
1201
+ addEventListener("keydown", (event) => { if (event.key === "Escape" && !controlsPanel.hidden) toggleMapControls(false, true); });
1202
+ const controlsTimer = setInterval(() => { if (!controlsPanel.hidden) void loadMapControls(); }, 1000);
1203
+ addEventListener("beforeunload", () => { clearInterval(controlsTimer); cancelMapControlsApply(); });
1204
+
1205
+ function cancelMapControlsApply() {
1206
+ clearTimeout(controlsApplyTimer); controlsApplyTimer = undefined;
1207
+ controlsPending = false; controlsResetPending = false; controlsRevision++;
1208
+ }
1209
+ function setMapControlsDisabled(disabled) {
1210
+ for (const name of ["modules", "runtime", "reset"]) ctl(name).disabled = disabled;
1211
+ }
1212
+ function scheduleMapControls(reset = false) {
1213
+ clearTimeout(controlsApplyTimer);
1214
+ controlsRevision++; controlsPending = true; controlsResetPending = reset;
1215
+ controlsNotice(reset ? "Restoring the original map…" : "Updating preview…");
1216
+ controlsApplyTimer = setTimeout(() => {
1217
+ controlsApplyTimer = undefined;
1218
+ void applyMapControls();
1219
+ }, reset ? 0 : 300);
1220
+ }
1221
+
1222
+ function toggleMapControls(open, focus = false) {
1223
+ if (globalThis.__tileflowInspectorUI) { globalThis.__tileflowInspectorUI.controls(open); return; }
1224
+ if (open) {
1225
+ setInspectionEnabled(false);
1226
+ }
1227
+ controlsPanel.hidden = !open;
1228
+ controlsButton.setAttribute("aria-expanded", String(open));
1229
+ for (const side of Object.values(states)) side.map?.resize?.();
1230
+ if (open) void loadMapControls();
1231
+ if (focus) (open ? controlsPanel : controlsButton).focus();
1232
+ }
1233
+ function controlsNotice(text, error = false) {
1234
+ ctl("status").hidden = !error;
1235
+ ctl("status").textContent = error ? text : "";
1236
+ ctl("status").dataset.error = String(error);
1237
+ }
1238
+ function controlsIdentity(side) {
1239
+ const meta = side.map?.getStyle?.()?.metadata;
1240
+ return meta ? {map:meta["tileflow:map"], theme:meta["tileflow:theme"]} : undefined;
1241
+ }
1242
+ function livePreviewUrl(side) {
1243
+ return new URL(side.frame.contentWindow.location.href, location.href);
1244
+ }
1245
+ async function loadMapControls() {
1246
+ const side = states[activeSideId], identity = controlsIdentity(side);
1247
+ if (!side.bridgeConnected || !identity?.map || !identity.theme) {
1248
+ setMapControlsDisabled(!side.bridgeConnected || !controlsModel);
1249
+ controlsNotice("Waiting for the map to finish loading…");
1250
+ return;
1251
+ }
1252
+ identity.theme = side.workbenchTheme || identity.theme;
1253
+ const key = JSON.stringify([side.id, side.definition.basePath, identity.map, side.loadEpoch]);
1254
+ const epoch = side.loadEpoch;
1255
+ const loadingKey = key + identity.theme;
1256
+ if (key === controlsKey && controlsModel?.theme === identity.theme) {
1257
+ setMapControlsDisabled(false);
1258
+ return;
1259
+ }
1260
+ if (loadingKey === controlsLoadingKey) return;
1261
+ const preserve = key === controlsKey && controlsModel;
1262
+ if (!preserve) {
1263
+ const pendingAppearance = side.pendingAppearance;
1264
+ cancelMapControlsApply();
1265
+ controlsPending = Boolean(pendingAppearance);
1266
+ controlsKey = key;
1267
+ controlsModel = undefined;
1268
+ ctl("modules").replaceChildren(); setMapControlsDisabled(true);
1269
+ }
1270
+ controlsLoadingKey = loadingKey;
1271
+ const sequence = ++controlsSequence;
1272
+ controlsNotice("Reading this map’s modules…");
1273
+ try {
1274
+ const url = new URL(side.definition.basePath + "/__controls", location.href);
1275
+ url.search = new URLSearchParams(identity).toString();
1276
+ const response = await fetch(url, {cache:"no-store"});
1277
+ const model = await response.json();
1278
+ if (!response.ok) throw new Error(model.error || "Controls are unavailable for this preview.");
1279
+ if (sequence !== controlsSequence || activeSideId !== side.id || key !== controlsKey || epoch !== side.loadEpoch || identity.theme !== (side.workbenchTheme || controlsIdentity(side)?.theme)) return;
1280
+ if (preserve) {
1281
+ // Keep the actual inputs and their handlers mounted, including focus/selection.
1282
+ for (const module of controlsModel.modules) {
1283
+ const updated = model.modules.find((item) => item.id === module.id);
1284
+ if (!updated) continue;
1285
+ module.enabled = updated.enabled;
1286
+ for (const field of module.fields) Object.assign(field, updated.fields.find((item) => item.path === field.path));
1287
+ }
1288
+ controlsModel.theme = model.theme;
1289
+ for (const sync of controlsBindings) sync();
1290
+ } else {
1291
+ controlsModel = {...model, side, key, epoch};
1292
+ const query = livePreviewUrl(side).searchParams;
1293
+ controlsDraft = JSON.parse(query.get("controls") || "{}");
1294
+ side.appliedSceneStyle ||= JSON.stringify([identity.map, controlsIdentity(side).theme, controlsDraft]);
1295
+ ctl("3d").checked = query.get("buildings3d") !== "off";
1296
+ ctl("animation").value = query.get("animation") === "fade" ? "fade" : Number(query.get("animation")) > 0 ? "800" : "0"; syncAnimationSegments();
1297
+ renderMapControls();
1298
+ controlsUndo.length = controlsRedo.length = 0;
1299
+ controlsHistoryCurrent = globalThis.__tileflowScene.snapshot();
1300
+ controlsInitial = structuredClone(controlsHistoryCurrent);
1301
+ }
1302
+ setMapControlsDisabled(false);
1303
+ controlsNotice(Object.keys(controlsDraft).length ? "Preview overrides applied." : "Using the map’s original settings.");
1304
+ } catch (error) {
1305
+ if (sequence === controlsSequence) { if (!preserve) controlsKey = ""; controlsNotice(safeErrorMessage(error), true); }
1306
+ } finally {
1307
+ if (sequence === controlsSequence) {
1308
+ controlsLoadingKey = "";
1309
+ if (controlsPending && controlsApplyTimer === undefined) void applyMapControls();
1310
+ }
1311
+ }
1312
+ }
1313
+ function controlValue(object, path) { return path.split(".").reduce((value, key) => value?.[key], object); }
1314
+ function changeControl(module, path, value, original) {
1315
+ const patch = controlsDraft[module] ||= {};
1316
+ const parts = path.split("."); const leaf = parts.pop();
1317
+ let parent = patch;
1318
+ for (const part of parts) parent = parent[part] ||= {};
1319
+ if (value === original) delete parent[leaf]; else parent[leaf] = value;
1320
+ for (const [key, item] of Object.entries(patch)) if (item && typeof item === "object" && !Object.keys(item).length) delete patch[key];
1321
+ if (!Object.keys(patch).length) delete controlsDraft[module];
1322
+ scheduleMapControls();
1323
+ }
1324
+ function renderMapControls() {
1325
+ ctl("modules").replaceChildren();
1326
+ controlsBindings = [];
1327
+ const names = {poi:"Points of interest", land:"Land", water:"Water", nautical:"Nautical", roads:"Roads", buildings:"Buildings", boundaries:"Boundaries", labels:"Labels", aeroways:"Airports", transit:"Transit", vegetation:"Vegetation", addresses:"Addresses", landforms:"Landforms"};
1328
+ const modules = [...controlsModel.modules].sort((a,b) => {
1329
+ const priority = {vegetation:0, buildings:1};
1330
+ return (priority[a.id] ?? 2) - (priority[b.id] ?? 2);
1331
+ });
1332
+ for (const module of modules) {
1333
+ const card = document.createElement("section"); card.className = "control-module";
1334
+ const row = document.createElement("div"); row.className = "control-module-header";
1335
+ const label = document.createElement("label"); label.textContent = names[module.id] || module.id;
1336
+ const toggle = document.createElement("input"); toggle.type = "checkbox"; toggle.checked = controlsDraft[module.id]?.enabled ?? module.enabled;
1337
+ toggle.setAttribute("role", "switch");
1338
+ toggle.setAttribute("aria-label", "Enable " + (names[module.id] || module.id));
1339
+ label.append(toggle); row.append(label); card.append(row);
1340
+ const fieldset = document.createElement("fieldset");
1341
+ const syncModuleEnabled = () => {
1342
+ fieldset.disabled = !toggle.checked;
1343
+ fieldset.hidden = !toggle.checked;
1344
+ };
1345
+ syncModuleEnabled();
1346
+ controlsBindings.push(() => {
1347
+ if (document.activeElement !== toggle) toggle.checked = controlsDraft[module.id]?.enabled ?? module.enabled;
1348
+ syncModuleEnabled();
1349
+ });
1350
+ toggle.addEventListener("change", () => { changeControl(module.id, "enabled", toggle.checked, module.enabled); syncModuleEnabled(); });
1351
+ if (module.fields.length) {
1352
+ card.append(fieldset);
1353
+ for (const field of module.fields) {
1354
+ const line = document.createElement("label"); line.className = "control-field";
1355
+ const title = document.createElement("span"); title.textContent = field.label;
1356
+ const input = document.createElement(field.choices ? "select" : "input");
1357
+ const override = controlValue(controlsDraft[module.id], field.path);
1358
+ const value = field.inherit && override === undefined ? "inherit" : override ?? field.value;
1359
+ const labels = {"3d":"Always 3D",auto:"Follow 2D / 3D view",flat:"Flat (2D)",none:"Hidden",style:"Map light",sun:"Scene sun",faceted:"Faceted",smooth:"Smooth",toon:"Toon"};
1360
+ if (field.choices) {
1361
+ const choices = field.choices.map((choice) => new Option(labels[choice] || choice, choice));
1362
+ if (field.inherit) choices.unshift(new Option("Auto · " + (labels[field.value] || field.value), "inherit"));
1363
+ input.replaceChildren(...choices); input.value = String(value);
1364
+ } else if (typeof field.value === "boolean") { input.type = "checkbox"; input.setAttribute("role", "switch"); input.checked = value; }
1365
+ else { input.type = "number"; input.required = true; input.min = String(field.min); input.max = String(field.max); input.step = "any"; input.value = String(value); }
1366
+ controlsBindings.push(() => {
1367
+ if (document.activeElement === input || !input.checkValidity()) return;
1368
+ const override = controlValue(controlsDraft[module.id], field.path);
1369
+ const value = field.inherit && override === undefined ? "inherit" : override ?? field.value;
1370
+ if (input.type === "checkbox") input.checked = value;
1371
+ else input.value = String(value);
1372
+ if (field.inherit) input.options[0].textContent = "Auto · " + (labels[field.value] || field.value);
1373
+ });
1374
+ input.addEventListener(input.type === "number" ? "input" : "change", () => {
1375
+ if (!input.checkValidity()) { cancelMapControlsApply(); controlsNotice("Enter a valid value to update the preview.", true); return; }
1376
+ const value = field.inherit && input.value === "inherit" ? undefined : input.type === "checkbox" ? input.checked : input.type === "number" ? Number(input.value) : input.value;
1377
+ changeControl(module.id, field.path, value, field.inherit ? undefined : field.value);
1378
+ });
1379
+ line.append(title, input); fieldset.append(line);
1380
+ }
1381
+ }
1382
+ ctl("modules").append(card);
1383
+ }
1384
+ }
1385
+ async function applyMapControls() {
1386
+ if (!controlsModel || controlsBusy || !controlsPending) return;
1387
+ const revision = controlsRevision, reset = controlsResetPending;
1388
+ const {side, map, key} = controlsModel;
1389
+ const theme = side.workbenchTheme || controlsModel.theme;
1390
+ const epoch = side.loadEpoch, source = side.frame.src;
1391
+ if (!side.bridgeConnected || side.id !== activeSideId || side.loadEpoch !== controlsModel.epoch) return;
1392
+ if (!reset && [...ctl("modules").querySelectorAll("input")].some((input) => !input.checkValidity())) {
1393
+ controlsPending = false;
1394
+ controlsNotice("Enter valid field values to update the preview.", true); return;
1395
+ }
1396
+ const draft = reset ? {} : structuredClone(controlsDraft);
1397
+ const next = {map, theme, controls:draft, buildings3d:reset || ctl("3d").checked, animation:reset ? 0 : ctl("animation").value === "fade" ? "fade" : Number(ctl("animation").value)};
1398
+ const styleKey = JSON.stringify([map, theme, draft]);
1399
+ const isCurrent = () => revision === controlsRevision && controlsKey === key && side.id === activeSideId && side.bridgeConnected && epoch === side.loadEpoch && source === side.frame.src;
1400
+ const before = controlsHistoryCurrent && {...controlsHistoryCurrent, style:side.map.getStyle()};
1401
+ controlsBusy = true; controlsPending = false; controlsLastError = "";
1402
+ controlsNotice("Compiling preview…");
1403
+ try {
1404
+ if (styleKey !== side.appliedSceneStyle) {
1405
+ const style = new URL(side.definition.basePath + "/styles/" + map + "/" + theme + ".json", location.href);
1406
+ if (Object.keys(draft).length) style.searchParams.set("controls", JSON.stringify(draft));
1407
+ const response = await fetch(style, {cache:"no-store"});
1408
+ const document = await response.json();
1409
+ if (!response.ok) throw new Error(document.error || "Unable to compile these settings.");
1410
+ next.style = document;
1411
+ }
1412
+ if (!isCurrent()) return;
1413
+ const apply = side.frame.contentWindow.__tileflowApplyScene;
1414
+ if (!apply) throw new Error("Reload Inspector once to enable live scene updates.");
1415
+ if (!await apply(JSON.stringify(next), isCurrent)) return;
1416
+ side.appliedSceneStyle = styleKey;
1417
+ const after = globalThis.__tileflowScene.snapshot();
1418
+ if (!controlsHistoryQuiet && before && JSON.stringify([before.theme,before.controls,before.buildings3d,before.animation]) !== JSON.stringify([after.theme,after.controls,after.buildings3d,after.animation])) {
1419
+ controlsUndo.push({before,after,time:performance.now()}); if(controlsUndo.length>100)controlsUndo.shift(); controlsRedo.length=0;
1420
+ }
1421
+ controlsHistoryCurrent=after;
1422
+ side.pendingAppearance = undefined;
1423
+ side.definition.previewUrl = livePreviewUrl(side).href;
1424
+ side.sidecarAbort?.abort(); side.sidecar = undefined; side.sidecarPromise = undefined;
1425
+ if (selectedInspection?.side === side) void loadSidecar(side);
1426
+ if (isCurrent()) controlsNotice("");
1427
+ } catch (error) {
1428
+ if (revision === controlsRevision && controlsKey === key) { controlsLastError = safeErrorMessage(error); controlsNotice(controlsLastError + " The previous map is still shown.", true); }
1429
+ } finally {
1430
+ controlsBusy = false;
1431
+ if (controlsPending && controlsApplyTimer === undefined) void applyMapControls();
1432
+ }
1433
+ }
1434
+ globalThis.__tileflowScene = {
1435
+ get history() {return {undo:controlsUndo.at(-1)?.time || 0,redo:controlsRedo.at(-1)?.redoTime || 0,changed:controlsUndo.length>0||controlsRedo.length>0};},
1436
+ async undo() {const entry=controlsUndo.at(-1);if(!entry)return;await this.restore(entry.before);controlsUndo.pop();entry.redoTime=performance.now();controlsRedo.push(entry);},
1437
+ async redo() {const entry=controlsRedo.at(-1);if(!entry)return;await this.restore(entry.after);controlsRedo.pop();entry.time=performance.now();controlsUndo.push(entry);},
1438
+ async reset() {if(controlsInitial)await this.restore(controlsInitial);controlsUndo.length=controlsRedo.length=0;},
1439
+ audit(active) {controlsHistoryQuiet=Math.max(0,controlsHistoryQuiet+(active?1:-1));},
1440
+ variants() {
1441
+ const side = states[activeSideId], owner = wbCatalog?.sides[side.serviceId || side.id];
1442
+ const variants = owner?.timeOfDay?.[side.workbenchMap] || {};
1443
+ return (owner?.selections || []).filter(entry => entry.map === side.workbenchMap && !entry.scene).map(entry => {
1444
+ const family = Object.keys(variants).find(name => Object.values(variants[name]).includes(entry.theme));
1445
+ return {theme:entry.theme, palette:family || entry.theme, timeOfDay:family ? Object.keys(variants[family]).find(moment => variants[family][moment] === entry.theme) : undefined};
1446
+ });
1447
+ },
1448
+ snapshot() {
1449
+ const side = states[activeSideId];
1450
+ return {side:side.id, theme:side.workbenchTheme || controlsIdentity(side)?.theme, controls:structuredClone(controlsDraft), buildings3d:ctl("3d").checked, animation:ctl("animation").value, style:side.map?.getStyle()};
1451
+ },
1452
+ async style(theme, signal) {
1453
+ const side = states[activeSideId], identity = controlsIdentity(side);
1454
+ const url = new URL(side.definition.basePath + "/styles/" + identity.map + "/" + theme + ".json", location.href);
1455
+ if (Object.keys(controlsDraft).length) url.searchParams.set("controls", JSON.stringify(controlsDraft));
1456
+ const response = await fetch(url, {signal});
1457
+ if (!response.ok) throw new Error("Could not read theme " + theme);
1458
+ return response.json();
1459
+ },
1460
+ async apply(theme, signal) {
1461
+ if (signal?.aborted) throw new Error("Audit cancelled.");
1462
+ const side = states[activeSideId], owner = wbCatalog?.sides[side.serviceId || side.id];
1463
+ const entry = owner?.selections.find(item => item.map === side.workbenchMap && item.theme === theme && !item.scene);
1464
+ if (!entry) throw new Error("Theme is not available: " + theme);
1465
+ applyWorkbenchAppearance(side, entry);
1466
+ const started = performance.now();
1467
+ do {
1468
+ if (signal?.aborted) throw new Error("Audit cancelled.");
1469
+ if (states[activeSideId] !== side) throw new Error("The active map changed.");
1470
+ if (performance.now() - started > 30000) throw new Error("Theme update timed out.");
1471
+ await new Promise(resolve => setTimeout(resolve,40));
1472
+ } while (controlsBusy || controlsPending || controlsLoadingKey || controlsApplyTimer !== undefined);
1473
+ if (controlsLastError) throw new Error(controlsLastError);
1474
+ if (controlsIdentity(side)?.theme !== theme) throw new Error("Theme could not be applied.");
1475
+ },
1476
+ async restore(saved) {
1477
+ if (states[activeSideId].id !== saved.side) return;
1478
+ controlsHistoryQuiet++;
1479
+ try {
1480
+ controlsDraft = structuredClone(saved.controls);
1481
+ ctl("3d").checked = saved.buildings3d; ctl("animation").value = saved.animation;
1482
+ scheduleMapControls();
1483
+ await this.apply(saved.theme);
1484
+ const side = states[activeSideId];
1485
+ if (saved.style) await side.frame.contentWindow.__tileflowApplyScene(JSON.stringify({map:controlsIdentity(side).map,theme:saved.theme,controls:saved.controls,buildings3d:saved.buildings3d,animation:saved.animation === "fade" ? "fade" : Number(saved.animation),style:saved.style}), () => states[activeSideId] === side);
1486
+ renderMapControls();syncAnimationSegments();controlsHistoryCurrent=this.snapshot();
1487
+ } finally {controlsHistoryQuiet--;}
1488
+ }
1489
+ };
1490
+ `;
1491
+
1492
+ // src/workbench-ui.ts
1493
+ function workbenchIcon(name) {
1494
+ const paths = {
1495
+ inspect: '<path d="M9 3H4a1 1 0 0 0-1 1v5m12-6h5a1 1 0 0 1 1 1v5M3 15v5a1 1 0 0 0 1 1h5"/><path d="m11 11 10 4-5 1-1 5z"/>',
1496
+ settings: '<path d="M4 7h16M4 17h16"/><circle cx="9" cy="7" r="3"/><circle cx="15" cy="17" r="3"/>',
1497
+ close: '<path d="m6 6 12 12M6 18 18 6"/>'
1498
+ };
1499
+ return `<svg class="tool-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">${paths[name]}</svg>`;
1500
+ }
1501
+ var workbenchStyles = `
1502
+ body { display:flex; flex-direction:column; }
1503
+ .workspace { flex:1; min-height:0; }
1504
+ input[type=range] { accent-color:var(--accent); }
1505
+ button:disabled { opacity:.5; cursor:not-allowed; }
1506
+ select,input[type=text],input[type=number] { background:var(--paper); color:var(--ink); border:1px solid var(--border); padding:5px 8px; }
1507
+ summary { cursor:pointer; }
1508
+ .tool-icon { width:17px; height:17px; flex:none; vertical-align:middle; }
1509
+ button:has(.tool-icon) { display:inline-flex; align-items:center; justify-content:center; gap:7px; }
1510
+ .inspect-hint { position:absolute; z-index:4; top:8px; left:8px; pointer-events:none; }
1511
+ .compact-note { font-size:12px; color:var(--muted); line-height:1.5; }
1512
+ .property-list { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:8px; }
1513
+ .property-list dd { margin:0; }
1514
+ .authoring-path { display:block; overflow-wrap:anywhere; }
1515
+ .color-swatch { width:16px; height:16px; border:1px solid var(--border); border-radius:2px; display:inline-block; }
1516
+ `;
1517
+ var workbenchPanels = `<div id="wb-notice" role="status" hidden></div>`;
1518
+
1519
+ // src/comparison-html.ts
1520
+ function renderTileflowComparisonHtml(options) {
1521
+ const documentOptions = serializeInlineJson(options);
1522
+ const fontBasePath = options.basePath.split("/").map((segment) => encodeURIComponent(decodeURIComponent(segment)).replaceAll("'", "%27")).join("/");
1523
+ const nonceAttribute = options.cspNonce ? ` nonce="${escapeHtml(options.cspNonce)}"` : "";
1524
+ return `<!doctype html>
1525
+ <html lang="en" data-tileflow-state="loading">
1526
+ <head>
1527
+ <meta charset="utf-8" />
1528
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1529
+ <title>${escapeHtml(options.title)}</title>
1530
+ <style${nonceAttribute}>
1531
+ @font-face { font-family:'Inter'; font-weight:100 900; font-display:swap; src:url('${fontBasePath}/__workbench-fonts/inter.woff2') format('woff2'); }
1532
+ @font-face { font-family:'JetBrains Mono'; font-weight:400; font-display:swap; src:url('${fontBasePath}/__workbench-fonts/jetbrains-mono.woff2') format('woff2'); }
1533
+ @font-face { font-family:'Mona Sans'; font-weight:200 900; font-display:swap; src:url('${fontBasePath}/__workbench-fonts/mona-sans.woff2') format('woff2'); }
1534
+ @font-face { font-family:'Hubot Sans'; font-weight:200 900; font-display:swap; src:url('${fontBasePath}/__workbench-fonts/hubot-sans.woff2') format('woff2'); }
1535
+ :root {
1536
+ color-scheme: light;
1537
+ --accent: #3157d5;
1538
+ --border: #d8dbe2;
1539
+ --ink: #202124;
1540
+ --muted: #656b76;
1541
+ --paper: #f6f7f9;
1542
+ --split: 50%;
1543
+ --overlay-alpha: .5;
1544
+ }
1545
+ * { box-sizing: border-box; }
1546
+ [hidden] { display: none !important; }
1547
+ .stage[data-mode="single"] .pane-right { display: none; }
1548
+ html, body { height: 100%; margin: 0; }
1549
+ body {
1550
+ display: grid;
1551
+ grid-template-rows: auto minmax(0, 1fr);
1552
+ overflow: hidden;
1553
+ background: var(--paper);
1554
+ color: var(--ink);
1555
+ font: 13px/1.4 ui-sans-serif, system-ui, sans-serif;
1556
+ }
1557
+ button, input, select { font: inherit; }
1558
+ button {
1559
+ border: 1px solid var(--border);
1560
+ border-radius: 6px;
1561
+ background: white;
1562
+ color: var(--ink);
1563
+ cursor: pointer;
1564
+ padding: 5px 9px;
1565
+ }
1566
+ button:hover { border-color: #a8afbd; }
1567
+ button[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }
1568
+ button:focus-visible, input:focus-visible, select:focus-visible, .inspector:focus-visible {
1569
+ outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent);
1570
+ outline-offset: 2px;
1571
+ }
1572
+ .workspace { position: relative; min-height: 0; display: grid; grid-template-columns: minmax(0, 1fr) auto; }
1573
+ .stage { position: relative; min-width: 0; min-height: 0; overflow: hidden; background: #d8dadd; }
1574
+ .pane { position: absolute; inset: 0; min-width: 0; min-height: 0; overflow: hidden; background: #d8dadd; }
1575
+ .pane iframe { width: 100%; height: 100%; display: block; border: 0; background: #d8dadd; }
1576
+ .stage[data-mode="side-by-side"] { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 1px; }
1577
+ .stage[data-mode="side-by-side"] .pane { position: relative; inset: auto; }
1578
+ .stage[data-mode="side-by-side"] .pane-right { grid-column: 2; }
1579
+ .stage:not([data-mode="side-by-side"]) .pane-right { z-index: 2; }
1580
+ .stage[data-mode="split"] .pane-right { clip-path: inset(0 0 0 var(--split)); }
1581
+ .stage[data-mode="overlay"] .pane-right { opacity: var(--overlay-alpha); }
1582
+ .stage[data-mode="blink"] .pane-right { opacity: 0; }
1583
+ .stage[data-mode="blink"][data-blink-side="right"] .pane-right { opacity: 1; }
1584
+ .stage[data-mode="blink"][data-blink-side="right"] .pane-left { opacity: 0; }
1585
+ .stage:not([data-mode="side-by-side"]) .pane-right { pointer-events: none; }
1586
+ .stage[data-driver="right"]:not([data-mode="side-by-side"]) .pane-right { pointer-events: auto; }
1587
+ .stage[data-driver="right"]:not([data-mode="side-by-side"]) .pane-left { pointer-events: none; }
1588
+ .stage[data-driver="left"]:not([data-mode="side-by-side"]) .pane-left { pointer-events: auto; }
1589
+ .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; }
1590
+ .stage[data-mode="split"] .split-line { display: block; }
1591
+ .inspector {
1592
+ width: min(390px, 42vw);
1593
+ border-left: 1px solid var(--border);
1594
+ background: white;
1595
+ overflow: auto;
1596
+ padding: 12px;
1597
+ }
1598
+ .inspector[hidden] { display: none; }
1599
+ .inspector h2 { margin: 0 0 10px; font-size: 15px; }
1600
+ .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; }
1601
+ .inspector-heading h2 { margin: 0; }
1602
+ .inspector h3 { margin: 16px 0 6px; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
1603
+ .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; }
1604
+ .inspector select { max-width: 100%; padding: 5px 7px; border: 1px solid var(--border); border-radius: 6px; }
1605
+ .feature-picker { display: grid; gap: 4px; margin: 8px 0; color: var(--muted); }
1606
+ .feature-picker select { width: 100%; color: var(--ink); }
1607
+ .curve-meta { margin: 5px 0; color: var(--muted); font-size: 11px; }
1608
+ .empty { color: var(--muted); }
1609
+ .curve { width: 100%; height: 116px; display: block; border: 1px solid var(--border); border-radius: 6px; background: #fbfbfc; }
1610
+ .color-curve { display: grid; grid-template-columns: repeat(97, 1fr); height: 42px; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
1611
+ .sprite-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(92px, 1fr)); gap: 7px; margin-top: 8px; }
1612
+ .sprite-card { min-width: 0; border: 1px solid var(--border); border-radius: 7px; padding: 7px; background: var(--paper); text-align: center; }
1613
+ .sprite-card canvas { display: block; margin: auto; max-width: 64px; max-height: 64px; image-rendering: auto; }
1614
+ .sprite-name { display: block; margin-top: 4px; overflow: hidden; text-overflow: ellipsis; font-size: 10px; }
1615
+ .visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
1616
+ @media (max-width: 760px) {
1617
+ .workspace { grid-template-columns: minmax(0, 1fr); }
1618
+ .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); }
1619
+ }
1620
+ @media (prefers-reduced-motion: reduce) {
1621
+ *, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
1622
+ }
1623
+ ${workbenchStyles}
1624
+ ${controlsStyles}
1625
+ ${inspectorStyles2}
1626
+ </style>
1627
+ </head>
1628
+ <body>
1629
+ <div class="workspace">
1630
+ <main class="stage" id="stage" data-driver="left" data-tileflow-capture-id="tileflow-comparison" data-tileflow-state="loading">
1631
+ <section class="pane pane-left" data-side="left">
1632
+ <iframe id="left-frame" title="${options.right ? "Left Tileflow preview" : "Tileflow map preview"}" referrerpolicy="no-referrer"></iframe>
1633
+ </section>
1634
+ <section class="pane pane-right" data-side="right">
1635
+ <iframe id="right-frame" title="Right Tileflow preview" referrerpolicy="no-referrer"></iframe>
1636
+ </section>
1637
+ <div class="split-line" aria-hidden="true"></div>
1638
+ <div class="inspect-hint" id="wb-inspect-hint" hidden>Click an element to inspect \xB7 Esc to exit</div>
1639
+ </main>
1640
+ <aside class="inspector" id="inspector" tabindex="-1" aria-labelledby="inspector-title" hidden>
1641
+ <button type="button" id="inspect-toggle" aria-controls="inspector" aria-expanded="false" aria-pressed="false" aria-keyshortcuts="i" title="Select a map element (I). Escape to stop.">${workbenchIcon("inspect")}Inspect</button>
1642
+ <button type="button" id="controls-toggle" aria-controls="map-controls" aria-expanded="false">Scene</button>
1643
+ <div class="inspector-heading"><h2 id="inspector-title">${workbenchIcon("inspect")} Inspect the map</h2><button type="button" id="inspector-close" aria-label="Close inspector">${workbenchIcon("close")}</button></div>
1644
+ <div id="inspection" class="empty">Click a feature to understand how it is drawn and find its configuration.</div>
1645
+ <details class="zoom-details"><summary>Appearance at different zoom levels</summary>
1646
+ <select id="curve-property" disabled><option>Select a rendered layer first</option></select>
1647
+ <div id="curve-output" class="empty">Select an element to explore its appearance.</div></details>
1648
+ <details><summary>Compiler details</summary><h3>Compiler sidecar</h3><pre id="sidecar-output">No layer selected.</pre></details>
1649
+ <details><summary>Sprite atlas</summary>
1650
+ <button type="button" id="load-sprites">Load active side sprites</button>
1651
+ <div id="sprite-output" class="sprite-grid"></div></details>
1652
+ </aside>
1653
+ ${workbenchPanels}
1654
+ ${controlsPanel}
1655
+ </div>
1656
+ <script type="module"${nonceAttribute}>
1657
+ const options = ${documentOptions};
1658
+ const bridgeSchemaVersion = 1;
1659
+ const bridgeSetCameraType = "tileflow:comparison-set-camera";
1660
+ const bridgeSetInspectionType = "tileflow:comparison-set-inspection";
1661
+ const bridgeCameraType = "tileflow:comparison-camera";
1662
+ const bridgeInspectionType = "tileflow:comparison-inspection";
1663
+ const bridgeReadyType = "tileflow:comparison-ready";
1664
+ const bridgeRuntimeErrorType = "tileflow:comparison-runtime-error";
1665
+ const stage = document.getElementById("stage");
1666
+ const inspector = document.getElementById("inspector");
1667
+ const inspectionOutput = document.getElementById("inspection");
1668
+ const curveProperty = document.getElementById("curve-property");
1669
+ const curveOutput = document.getElementById("curve-output");
1670
+ const sidecarOutput = document.getElementById("sidecar-output");
1671
+ const spriteOutput = document.getElementById("sprite-output");
1672
+ const inspectButton = document.getElementById("inspect-toggle");
1673
+ const inspectorCloseButton = document.getElementById("inspector-close");
1674
+ const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
1675
+ const sideIds = options.right ? ["left", "right"] : ["left"];
1676
+ const states = Object.fromEntries(sideIds.map((id) => {
1677
+ const definition = options[id];
1678
+ return [id, {
1679
+ id,
1680
+ definition,
1681
+ frame: document.getElementById(id + "-frame"),
1682
+ map: undefined,
1683
+ camera: undefined,
1684
+ bridgeReady: false,
1685
+ artifactStatus: "building",
1686
+ artifactGeneration: -1,
1687
+ artifactLastGoodGeneration: undefined,
1688
+ sidecar: undefined,
1689
+ sidecarPromise: undefined,
1690
+ sidecarAbort: undefined,
1691
+ spriteAbort: undefined,
1692
+ loadEpoch: 0,
1693
+ lastCameraRequestId: undefined,
1694
+ cleanupBridge: undefined,
1695
+ runtimeErrors: [],
1696
+ runtimeFailed: false
1697
+ }];
1698
+ }));
1699
+ let canonicalCamera = cameraFromUrl();
1700
+ let activeSideId = "left";
1701
+ let inspectionEnabled = false;
1702
+ let selectedInspection;
1703
+ let requestSequence = 0;
1704
+ let blinkTimer;
1705
+ let blinkPaused = reducedMotion.matches;
1706
+ let blinkSide = "left";
1707
+ let spriteLoadSequence = 0;
1708
+ let spriteOwnerId;
1709
+
1710
+ const initialUi = readUiFromUrl();
1711
+
1712
+ applySliders();
1713
+ setMode(initialUi.mode || options.initialMode);
1714
+ for (const id of sideIds) {
1715
+ const side = states[id];
1716
+ side.frame.src = side.definition.previewUrl;
1717
+ side.frame.addEventListener("load", () => connectSide(side));
1718
+ side.frame.addEventListener("focus", () => selectSide(side.id));
1719
+ startStatus(side);
1720
+ }
1721
+
1722
+ inspectButton.addEventListener("click", () => setInspectionEnabled(!inspectionEnabled, true));
1723
+ inspectorCloseButton.addEventListener("click", () => setInspectionEnabled(false, true));
1724
+ document.getElementById("load-sprites").addEventListener("click", () => loadSprites(states[activeSideId]));
1725
+ curveProperty.addEventListener("change", renderCurve);
1726
+ reducedMotion.addEventListener?.("change", () => {
1727
+ blinkPaused = reducedMotion.matches;
1728
+ restartBlink();
1729
+ });
1730
+ addEventListener("keydown", (event) => {
1731
+ if (event.key === "Escape" && inspectionEnabled) setInspectionEnabled(false, true);
1732
+ });
1733
+ addEventListener("message", receiveBridgeMessage);
1734
+ addEventListener("beforeunload", () => {
1735
+ clearInterval(blinkTimer);
1736
+ for (const side of Object.values(states)) {
1737
+ side.eventSource?.close();
1738
+ side.sidecarAbort?.abort();
1739
+ side.spriteAbort?.abort();
1740
+ cleanupSideBridge(side);
1741
+ }
1742
+ });
1743
+
1744
+ function selectSide(id, writeState = true) {
1745
+ if (!states[id]) return;
1746
+ const changed = activeSideId !== id || stage.dataset.driver !== id;
1747
+ const previousSpriteOwner = spriteOwnerId;
1748
+ activeSideId = id;
1749
+ stage.dataset.driver = id;
1750
+ if (changed && previousSpriteOwner && previousSpriteOwner !== id) {
1751
+ states[previousSpriteOwner].spriteAbort?.abort();
1752
+ spriteLoadSequence += 1;
1753
+ spriteOwnerId = undefined;
1754
+ spriteOutput.replaceChildren();
1755
+ }
1756
+ if (writeState) writeUrlState();
1757
+ if (!canonicalCamera) maybeEstablishInitialCamera();
1758
+ }
1759
+
1760
+ function setInspectionEnabled(enabled, manageFocus = false) {
1761
+ inspectionEnabled = enabled;
1762
+ if (globalThis.__tileflowInspectorUI) {
1763
+ inspectButton.setAttribute("aria-pressed", String(enabled));
1764
+ document.getElementById("wb-inspect-hint").hidden = !enabled;
1765
+ for (const side of Object.values(states)) sendInspectionState(side);
1766
+ globalThis.__tileflowInspectorUI.inspectionChanged(enabled);
1767
+ return;
1768
+ }
1769
+ if (enabled) toggleMapControls(false);
1770
+ inspectButton.setAttribute("aria-pressed", String(enabled));
1771
+ inspectButton.setAttribute("aria-expanded", String(enabled));
1772
+ inspector.hidden = !enabled;
1773
+ document.getElementById("wb-inspect-hint").hidden = !enabled;
1774
+ for (const side of Object.values(states)) { sendInspectionState(side); side.map?.resize?.(); }
1775
+ if (manageFocus) {
1776
+ if (enabled) inspector.focus();
1777
+ else inspectButton.focus();
1778
+ }
1779
+ }
1780
+
1781
+
1782
+ function setMode(mode) {
1783
+ if (!options.right) mode = "single";
1784
+ if (!["single", "side-by-side", "split", "overlay", "blink"].includes(mode)) return;
1785
+ if (mode === "single" && activeSideId !== "left") selectSide("left", false);
1786
+ stage.dataset.mode = mode;
1787
+ restartBlink();
1788
+ writeUrlState();
1789
+ setTimeout(() => {
1790
+ for (const side of Object.values(states)) side.map?.resize?.();
1791
+ }, 0);
1792
+ }
1793
+
1794
+ function applySliders() {
1795
+ const split = boundedNumber(initialUi.split, 5, 95, 50);
1796
+ const alpha = boundedNumber(initialUi.alpha, 0, 100, 50);
1797
+ stage.style.setProperty("--split", split + "%");
1798
+ stage.style.setProperty("--overlay-alpha", String(alpha / 100));
1799
+ }
1800
+
1801
+ function restartBlink() {
1802
+ clearInterval(blinkTimer);
1803
+ blinkTimer = undefined;
1804
+ blinkSide = "left";
1805
+ stage.dataset.blinkSide = blinkSide;
1806
+ if (stage.dataset.mode !== "blink" || blinkPaused) return;
1807
+ blinkTimer = setInterval(() => {
1808
+ blinkSide = blinkSide === "left" ? "right" : "left";
1809
+ stage.dataset.blinkSide = blinkSide;
1810
+ }, 700);
1811
+ }
1812
+
1813
+ function cleanupSideBridge(side) {
1814
+ try { side.cleanupBridge?.(); }
1815
+ catch (error) {
1816
+ // A stopped dev server can replace the iframe with a cross-origin browser error page.
1817
+ if (error?.name !== "SecurityError") throw error;
1818
+ } finally { side.cleanupBridge = undefined; }
1819
+ }
1820
+
1821
+ function connectSide(side) {
1822
+ side.loadEpoch += 1;
1823
+ side.appliedSceneStyle = undefined;
1824
+ side.pendingAppearance = undefined;
1825
+ const epoch = side.loadEpoch;
1826
+ cleanupSideBridge(side);
1827
+ side.cleanupBridge = undefined;
1828
+ side.sidecarAbort?.abort();
1829
+ side.spriteAbort?.abort();
1830
+ side.sidecarAbort = undefined;
1831
+ side.spriteAbort = undefined;
1832
+ side.map = undefined;
1833
+ side.bridgeConnected = false;
1834
+ side.bridgeReady = false;
1835
+ side.camera = undefined;
1836
+ side.sidecar = undefined;
1837
+ side.sidecarPromise = undefined;
1838
+ side.runtimeErrors = [];
1839
+ side.runtimeFailed = false;
1840
+ side.lastCameraRequestId = undefined;
1841
+ if (spriteOwnerId === side.id) {
1842
+ spriteLoadSequence += 1;
1843
+ spriteOwnerId = undefined;
1844
+ spriteOutput.replaceChildren();
1845
+ spriteOutput.textContent = "The selected map reloaded; load its sprite atlas again.";
1846
+ }
1847
+ if (selectedInspection?.side === side) {
1848
+ selectedInspection = undefined;
1849
+ inspectionOutput.className = "empty";
1850
+ inspectionOutput.textContent = "The selected map reloaded; click a rendered feature again.";
1851
+ curveProperty.disabled = true;
1852
+ curveProperty.replaceChildren(new Option("Select a rendered layer first"));
1853
+ curveOutput.className = "empty";
1854
+ curveOutput.textContent = "Paint and layout values appear here.";
1855
+ sidecarOutput.textContent = "No layer selected.";
1856
+ }
1857
+ updateDocumentReadiness();
1858
+ const started = performance.now();
1859
+ const findMap = () => {
1860
+ if (side.loadEpoch !== epoch) return;
1861
+ let childWindow;
1862
+ try {
1863
+ childWindow = side.frame.contentWindow;
1864
+ const map = childWindow?.__tileflowPreviewMap;
1865
+ if (map) {
1866
+ side.map = map;
1867
+ // Inspector can edit a connected map while its tiles are still loading.
1868
+ // bridgeReady remains the separate renderer-readiness signal.
1869
+ side.bridgeConnected = true;
1870
+ side.camera = readMapCamera(map);
1871
+ side.cleanupBridge = installChildBridge(side, childWindow, map, epoch);
1872
+ if (options.workbenchUrl) void refreshWorkbench();
1873
+ if (!controlsPanel.hidden) void loadMapControls();
1874
+ return;
1875
+ }
1876
+ } catch (error) {
1877
+ markSideRuntimeError(side, error);
1878
+ return;
1879
+ }
1880
+ if (performance.now() - started < 10_000) {
1881
+ setTimeout(findMap, 50);
1882
+ } else {
1883
+ markSideRuntimeError(side, "Preview map did not initialize.");
1884
+ maybeEstablishInitialCamera();
1885
+ }
1886
+ };
1887
+ findMap();
1888
+ }
1889
+
1890
+ function installChildBridge(side, childWindow, map, epoch) {
1891
+ let applyingRequestId;
1892
+ let inspectionActive = inspectionEnabled;
1893
+ let movementFrame;
1894
+ let queuedUserMovement = false;
1895
+ let releaseFrame;
1896
+ let releaseFrameAfterPaint;
1897
+ let readyTimer;
1898
+ let readyAnnounced = false;
1899
+ let inspectedFeatures = [];
1900
+ let highlightedIndex = -1;
1901
+ let hoveredFeature, hoveredCoordinate, hoverFrame, hoverHint;
1902
+ let inspectedCoordinate;
1903
+ let highlight;
1904
+ const drawHighlight = () => {
1905
+ const geometry = hoveredFeature?.geometry || inspectedFeatures[highlightedIndex]?.geometry;
1906
+ if ((!inspectionActive && !globalThis.__tileflowInspectorUI) || !geometry || !map.getContainer || !map.project) { if (highlight) highlight.style.display = "none"; return; }
1907
+ if (!highlight) {
1908
+ highlight = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1909
+ highlight.setAttribute("aria-hidden", "true");
1910
+ highlight.style.cssText = "position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:3;overflow:hidden";
1911
+ map.getContainer().append(highlight);
1912
+ }
1913
+ highlight.style.display = "";
1914
+ highlight.replaceChildren();
1915
+ let remaining = 3000;
1916
+ const project = (coordinate) => {
1917
+ if (--remaining < 0 || !Array.isArray(coordinate) || !coordinate.slice(0,2).every(Number.isFinite)) return;
1918
+ const point = map.project(coordinate);
1919
+ return Number.isFinite(point.x) && Number.isFinite(point.y) ? point : undefined;
1920
+ };
1921
+ const draw = (coordinates, closed = false) => {
1922
+ if (!Array.isArray(coordinates)) return;
1923
+ const points = coordinates.slice(0, Math.max(0,remaining)).map(project).filter(Boolean);
1924
+ if (!points.length) return;
1925
+ const shape = document.createElementNS("http://www.w3.org/2000/svg", "path");
1926
+ shape.setAttribute("d", points.map((point,index) => (index ? "L" : "M") + point.x + " " + point.y).join(" ") + (closed ? "Z" : ""));
1927
+ shape.setAttribute("fill", closed ? "#1a73e822" : "none");
1928
+ shape.setAttribute("stroke", "#1a73e8"); shape.setAttribute("stroke-width", "3");
1929
+ shape.setAttribute("stroke-linejoin", "round"); highlight.append(shape);
1930
+ };
1931
+ const coordinates = geometry.coordinates;
1932
+ const containsPoint = (ring) => {
1933
+ const coordinate = hoveredCoordinate || inspectedCoordinate;
1934
+ if (!coordinate) return false;
1935
+ const [x,y] = coordinate;
1936
+ let inside = false;
1937
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
1938
+ const [xi,yi] = ring[i], [xj,yj] = ring[j];
1939
+ if ((yi > y) !== (yj > y) && x < (xj-xi)*(y-yi)/(yj-yi)+xi) inside = !inside;
1940
+ }
1941
+ return inside;
1942
+ };
1943
+ if (geometry.type === "Polygon") coordinates.forEach((ring) => draw(ring,true));
1944
+ else if (geometry.type === "MultiPolygon") coordinates.filter((polygon) => containsPoint(polygon[0]) && !polygon.slice(1).some(containsPoint)).forEach((polygon) => polygon.forEach((ring) => draw(ring,true)));
1945
+ else if (geometry.type === "LineString") draw(coordinates);
1946
+ else if (geometry.type === "MultiLineString") coordinates.forEach((line) => draw(line));
1947
+ else for (const coordinate of geometry.type === "MultiPoint" ? coordinates : [coordinates]) {
1948
+ const point = project(coordinate); if (!point) continue;
1949
+ const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
1950
+ circle.setAttribute("cx", point.x); circle.setAttribute("cy", point.y); circle.setAttribute("r", "10");
1951
+ circle.setAttribute("fill", "#1a73e822"); circle.setAttribute("stroke", "#1a73e8"); circle.setAttribute("stroke-width", "3"); highlight.append(circle);
1952
+ }
1953
+ };
1954
+ side.highlightFeature = (index) => { highlightedIndex = index; drawHighlight(); };
1955
+ side.getInspectedFeature = () => inspectedFeatures[highlightedIndex];
1956
+ const handleKey = (event) => {
1957
+ if (event.target.closest?.("input,select,textarea,[contenteditable]")) return;
1958
+ if (event.key === "Escape" && inspectionEnabled) { event.preventDefault(); setInspectionEnabled(false, true); }
1959
+ if (event.key.toLowerCase() === "i" && !event.metaKey && !event.ctrlKey && !event.altKey) { event.preventDefault(); setInspectionEnabled(!inspectionEnabled, true); }
1960
+ };
1961
+ const targetOrigin = childWindow.location.origin;
1962
+ // The bridge is installed by the same-origin parent, so callbacks created here
1963
+ // belong to the parent realm. Dispatch directly with an explicit child source;
1964
+ // commands still cross the iframe boundary through validated postMessage events.
1965
+ const postToParent = (message) => receiveBridgeMessage({
1966
+ data: message,
1967
+ origin: targetOrigin,
1968
+ source: childWindow
1969
+ });
1970
+ let userMovementActive = false;
1971
+ const postCamera = (phase, requestId, userInitiated = false) => {
1972
+ postToParent({
1973
+ type: bridgeCameraType,
1974
+ schemaVersion: bridgeSchemaVersion,
1975
+ side: side.id,
1976
+ phase,
1977
+ ...(requestId === undefined ? {} : {requestId}),
1978
+ ...(userInitiated ? {userInitiated: true} : {}),
1979
+ camera: readMapCamera(map)
1980
+ });
1981
+ };
1982
+ const handleMoveStart = (event) => {
1983
+ if (applyingRequestId === undefined && (event?.originalEvent || event?.tileflowInspector === true)) userMovementActive = true;
1984
+ };
1985
+ const handleMove = (event) => {
1986
+ if (applyingRequestId !== undefined) return;
1987
+ if (event?.originalEvent || event?.tileflowInspector === true) userMovementActive = true;
1988
+ queuedUserMovement = queuedUserMovement || userMovementActive;
1989
+ if (movementFrame !== undefined) return;
1990
+ movementFrame = childWindow.requestAnimationFrame(() => {
1991
+ movementFrame = undefined;
1992
+ const userInitiated = queuedUserMovement;
1993
+ queuedUserMovement = false;
1994
+ postCamera("move", undefined, userInitiated);
1995
+ });
1996
+ };
1997
+ const handleMoveEnd = (event) => {
1998
+ if (applyingRequestId !== undefined) {
1999
+ postCamera("end", applyingRequestId);
2000
+ scheduleApplyingRelease(applyingRequestId);
2001
+ return;
2002
+ }
2003
+ if (event?.originalEvent || event?.tileflowInspector === true) userMovementActive = true;
2004
+ if (movementFrame !== undefined) {
2005
+ childWindow.cancelAnimationFrame(movementFrame);
2006
+ movementFrame = undefined;
2007
+ }
2008
+ queuedUserMovement = false;
2009
+ postCamera("end", undefined, userMovementActive);
2010
+ userMovementActive = false;
2011
+ };
2012
+ const clearHover = () => {
2013
+ if (hoverFrame !== undefined) childWindow.cancelAnimationFrame(hoverFrame);
2014
+ hoverFrame = undefined; hoveredFeature = undefined; hoveredCoordinate = undefined;
2015
+ if (hoverHint) hoverHint.hidden = true;
2016
+ drawHighlight();
2017
+ };
2018
+ side.hoverFeature = (index) => {
2019
+ clearHover();
2020
+ if (!inspectedFeatures[index]) return;
2021
+ hoveredFeature = inspectedFeatures[index];
2022
+ hoveredCoordinate = inspectedCoordinate;
2023
+ drawHighlight();
2024
+ };
2025
+ const handleHover = (event) => {
2026
+ if (!inspectionActive || !globalThis.__tileflowInspectorUI || hoverFrame !== undefined) return;
2027
+ hoverFrame = childWindow.requestAnimationFrame(() => {
2028
+ hoverFrame = undefined;
2029
+ hoveredFeature = map.queryRenderedFeatures(event.point).find((feature) => !feature.layer.id.startsWith("__tileflow-inspector-"));
2030
+ hoveredCoordinate = [event.lngLat.lng, event.lngLat.lat];
2031
+ if (!hoverHint) {
2032
+ hoverHint = document.createElement("div");
2033
+ hoverHint.style.cssText = "position:absolute;z-index:6;pointer-events:none;padding:4px 7px;background:#fff;color:#202124;border:1px solid #dadce0;border-radius:3px;box-shadow:0 2px 5px #0003;font:11px/1.5 monospace;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap";
2034
+ map.getContainer().append(hoverHint);
2035
+ }
2036
+ hoverHint.hidden = !hoveredFeature;
2037
+ if (hoveredFeature) {
2038
+ hoverHint.textContent = hoveredFeature.layer.id + " \xB7 " + hoveredFeature.geometry.type + (hoveredFeature.id === undefined ? "" : " #" + hoveredFeature.id);
2039
+ hoverHint.style.left = Math.max(4,Math.min(event.point.x + 14,map.getContainer().clientWidth - 310)) + "px";
2040
+ hoverHint.style.top = Math.max(4,Math.min(event.point.y + 14,map.getContainer().clientHeight - 32)) + "px";
2041
+ }
2042
+ drawHighlight();
2043
+ });
2044
+ };
2045
+ const handleClick = (event) => {
2046
+ if (!inspectionActive) return;
2047
+ clearHover();
2048
+ inspectedCoordinate = [event.lngLat.lng, event.lngLat.lat];
2049
+ inspectedFeatures = map.queryRenderedFeatures(event.point).filter((feature) => !feature.layer.id.startsWith("__tileflow-inspector-")).slice(0, 16);
2050
+ const features = inspectedFeatures.map(projectFeature);
2051
+ postToParent({
2052
+ type: bridgeInspectionType,
2053
+ schemaVersion: bridgeSchemaVersion,
2054
+ side: side.id,
2055
+ coordinate: [event.lngLat.lng, event.lngLat.lat],
2056
+ features
2057
+ });
2058
+ };
2059
+ side.inspectRenderedFeature = (feature, coordinate) => {
2060
+ clearHover();
2061
+ inspectedCoordinate = coordinate;
2062
+ inspectedFeatures = [feature];
2063
+ postToParent({type: bridgeInspectionType, schemaVersion: bridgeSchemaVersion,
2064
+ side: side.id, coordinate, features: [projectFeature(feature)]});
2065
+ };
2066
+ const handleError = (event) => {
2067
+ postToParent({
2068
+ type: bridgeRuntimeErrorType,
2069
+ schemaVersion: bridgeSchemaVersion,
2070
+ side: side.id,
2071
+ message: sanitizeRuntimeMessage(event?.error?.message || event?.message || "Map runtime error")
2072
+ });
2073
+ };
2074
+ const handleCommand = (event) => {
2075
+ if (event.source !== childWindow.parent || event.origin !== targetOrigin) return;
2076
+ const message = event.data;
2077
+ if (!message || message.schemaVersion !== bridgeSchemaVersion) return;
2078
+ if (message.type === bridgeSetInspectionType && typeof message.enabled === "boolean") {
2079
+ inspectionActive = message.enabled;
2080
+ if (!inspectionActive) clearHover();
2081
+ map.getCanvas().style.cursor = inspectionActive ? "crosshair" : "";
2082
+ drawHighlight();
2083
+ return;
2084
+ }
2085
+ if (message.type !== bridgeSetCameraType || !validCamera(message.camera)) return;
2086
+ if (movementFrame !== undefined) {
2087
+ childWindow.cancelAnimationFrame(movementFrame);
2088
+ movementFrame = undefined;
2089
+ }
2090
+ queuedUserMovement = false;
2091
+ userMovementActive = false;
2092
+ cancelApplyingRelease();
2093
+ applyingRequestId = typeof message.requestId === "number" ? message.requestId : 0;
2094
+ try {
2095
+ map.jumpTo(message.camera);
2096
+ postCamera("end", applyingRequestId);
2097
+ scheduleApplyingRelease(applyingRequestId);
2098
+ } catch (error) {
2099
+ applyingRequestId = undefined;
2100
+ handleError({error});
2101
+ }
2102
+ };
2103
+ const announceReady = () => {
2104
+ if (readyAnnounced || side.loadEpoch !== epoch) return;
2105
+ readyAnnounced = true;
2106
+ clearTimeout(readyTimer);
2107
+ postToParent({
2108
+ type: bridgeReadyType,
2109
+ schemaVersion: bridgeSchemaVersion,
2110
+ side: side.id,
2111
+ camera: readMapCamera(map)
2112
+ });
2113
+ };
2114
+ const handleReadyTimeout = () => {
2115
+ if (readyAnnounced || side.loadEpoch !== epoch) return;
2116
+ markSideRuntimeError(side, "Preview map did not become ready within 10 seconds.");
2117
+ maybeEstablishInitialCamera();
2118
+ };
2119
+ const cancelApplyingRelease = () => {
2120
+ if (releaseFrame !== undefined) childWindow.cancelAnimationFrame(releaseFrame);
2121
+ if (releaseFrameAfterPaint !== undefined) childWindow.cancelAnimationFrame(releaseFrameAfterPaint);
2122
+ releaseFrame = undefined;
2123
+ releaseFrameAfterPaint = undefined;
2124
+ };
2125
+ const scheduleApplyingRelease = (requestId) => {
2126
+ cancelApplyingRelease();
2127
+ releaseFrame = childWindow.requestAnimationFrame(() => {
2128
+ releaseFrame = undefined;
2129
+ releaseFrameAfterPaint = childWindow.requestAnimationFrame(() => {
2130
+ releaseFrameAfterPaint = undefined;
2131
+ if (applyingRequestId === requestId) applyingRequestId = undefined;
2132
+ });
2133
+ });
2134
+ };
2135
+ map.on("move", drawHighlight);
2136
+ map.on("resize", drawHighlight);
2137
+ childWindow.addEventListener("keydown", handleKey);
2138
+ map.on("movestart", handleMoveStart);
2139
+ map.on("move", handleMove);
2140
+ map.on("moveend", handleMoveEnd);
2141
+ map.on("click", handleClick);
2142
+ map.on("mousemove", handleHover);
2143
+ map.on("mouseout", clearHover);
2144
+ map.on("error", handleError);
2145
+ childWindow.addEventListener("message", handleCommand);
2146
+ if (map.loaded?.()) announceReady();
2147
+ else {
2148
+ map.once("load", announceReady);
2149
+ map.once("idle", announceReady);
2150
+ readyTimer = setTimeout(handleReadyTimeout, 10_000);
2151
+ }
2152
+ return () => {
2153
+ if (movementFrame !== undefined) childWindow.cancelAnimationFrame(movementFrame);
2154
+ cancelApplyingRelease();
2155
+ clearTimeout(readyTimer);
2156
+ clearHover(); hoverHint?.remove();
2157
+ highlight?.remove(); side.highlightFeature = undefined; side.hoverFeature = undefined;
2158
+ side.getInspectedFeature = undefined; side.inspectRenderedFeature = undefined;
2159
+ map.off("move", drawHighlight); map.off("resize", drawHighlight);
2160
+ childWindow.removeEventListener("keydown", handleKey);
2161
+ map.off("movestart", handleMoveStart);
2162
+ map.off("move", handleMove);
2163
+ map.off("moveend", handleMoveEnd);
2164
+ map.off("click", handleClick);
2165
+ map.off("mousemove", handleHover);
2166
+ map.off("mouseout", clearHover);
2167
+ map.off("error", handleError);
2168
+ map.off("load", announceReady);
2169
+ map.off("idle", announceReady);
2170
+ childWindow.removeEventListener("message", handleCommand);
2171
+ };
2172
+ }
2173
+
2174
+ function receiveBridgeMessage(event) {
2175
+ if (event.origin !== location.origin || !event.data || event.data.schemaVersion !== bridgeSchemaVersion) return;
2176
+ const side = Object.values(states).find((candidate) => candidate.frame.contentWindow === event.source);
2177
+ if (!side || event.data.side !== side.id) return;
2178
+ const message = event.data;
2179
+ if (message.type === bridgeReadyType && validCamera(message.camera)) {
2180
+ side.bridgeReady = true;
2181
+ side.camera = message.camera;
2182
+ maybeEstablishInitialCamera();
2183
+ sendInspectionState(side);
2184
+ void loadSidecar(side);
2185
+ if (options.workbenchUrl) void refreshWorkbench();
2186
+ if (!controlsPanel.hidden) void loadMapControls();
2187
+ updateDocumentReadiness();
2188
+ return;
2189
+ }
2190
+ if (message.type === bridgeCameraType && validCamera(message.camera)) {
2191
+ if (message.requestId !== undefined) {
2192
+ if (message.requestId !== side.lastCameraRequestId) return;
2193
+ side.camera = message.camera;
2194
+ if (
2195
+ side.id === stage.dataset.driver &&
2196
+ canonicalCamera &&
2197
+ !camerasEqual(message.camera, canonicalCamera)
2198
+ ) {
2199
+ canonicalCamera = message.camera;
2200
+ sendCamera(states[side.id === "left" ? "right" : "left"], canonicalCamera);
2201
+ writeUrlState();
2202
+ }
2203
+ if (message.phase === "end" && selectedInspection?.side === side) renderCurve();
2204
+ return;
2205
+ }
2206
+ side.camera = message.camera;
2207
+ if (message.userInitiated !== true) {
2208
+ if (canonicalCamera && !camerasEqual(message.camera, canonicalCamera)) {
2209
+ sendCamera(side, canonicalCamera);
2210
+ }
2211
+ return;
2212
+ }
2213
+ selectSide(side.id, false);
2214
+ canonicalCamera = message.camera;
2215
+ const other = states[side.id === "left" ? "right" : "left"];
2216
+ sendCamera(other, canonicalCamera);
2217
+ if (message.phase === "end") {
2218
+ writeUrlState();
2219
+ if (selectedInspection?.side === side) renderCurve();
2220
+ }
2221
+ return;
2222
+ }
2223
+ if (message.type === bridgeInspectionType && Array.isArray(message.features)) {
2224
+ selectSide(side.id);
2225
+ selectedInspection = {side, coordinate: message.coordinate, features: message.features.slice(0, 16), featureIndex: 0};
2226
+ renderInspection(true);
2227
+ return;
2228
+ }
2229
+ if (message.type === bridgeRuntimeErrorType) {
2230
+ markSideRuntimeError(side, message.message);
2231
+ }
2232
+ }
2233
+
2234
+ function sendCamera(side, camera) {
2235
+ if (!side?.bridgeReady || !camera || !side.frame.contentWindow) return;
2236
+ if (side.camera && camerasEqual(side.camera, camera)) return;
2237
+ const requestId = ++requestSequence;
2238
+ side.lastCameraRequestId = requestId;
2239
+ side.frame.contentWindow.postMessage({
2240
+ type: bridgeSetCameraType,
2241
+ schemaVersion: bridgeSchemaVersion,
2242
+ requestId,
2243
+ camera
2244
+ }, location.origin);
2245
+ }
2246
+
2247
+ function maybeEstablishInitialCamera() {
2248
+ if (canonicalCamera) {
2249
+ for (const side of Object.values(states)) sendCamera(side, canonicalCamera);
2250
+ writeUrlState();
2251
+ return;
2252
+ }
2253
+ const driver = states[stage.dataset.driver];
2254
+ let source = driver.bridgeReady && driver.camera ? driver : undefined;
2255
+ const driverUnavailable = driver.runtimeFailed || (
2256
+ driver.artifactStatus === "invalid" && !Number.isInteger(driver.artifactLastGoodGeneration)
2257
+ );
2258
+ if (!source && driverUnavailable && states.left.bridgeReady && states.left.camera) {
2259
+ source = states.left;
2260
+ }
2261
+ if (!source) return;
2262
+ canonicalCamera = source.camera;
2263
+ for (const side of Object.values(states)) {
2264
+ if (side !== source) sendCamera(side, canonicalCamera);
2265
+ }
2266
+ writeUrlState();
2267
+ }
2268
+
2269
+ function sendInspectionState(side) {
2270
+ if (!side.bridgeReady || !side.frame.contentWindow) return;
2271
+ side.frame.contentWindow.postMessage({
2272
+ type: bridgeSetInspectionType,
2273
+ schemaVersion: bridgeSchemaVersion,
2274
+ enabled: inspectionEnabled
2275
+ }, location.origin);
2276
+ }
2277
+
2278
+ function startStatus(side) {
2279
+ fetch(side.definition.statusUrl, {cache: "no-store"})
2280
+ .then((response) => response.ok ? response.json() : Promise.reject(new Error("Status unavailable")))
2281
+ .then((status) => applyArtifactStatus(side, status))
2282
+ .catch(() => undefined);
2283
+ const events = new EventSource(side.definition.eventsUrl);
2284
+ side.eventSource = events;
2285
+ let connected = false, disconnected = false;
2286
+ events.addEventListener("open", () => {
2287
+ if (connected && disconnected) {
2288
+ side.artifactGeneration = -1;
2289
+ side.latestArtifactStatus = undefined;
2290
+ side.frame.contentWindow?.location.reload();
2291
+ }
2292
+ connected = true; disconnected = false;
2293
+ });
2294
+ events.addEventListener("error", () => { if (connected) disconnected = true; });
2295
+ for (const name of ["building", "ready", "invalid"]) {
2296
+ events.addEventListener(name, (event) => {
2297
+ try { applyArtifactStatus(side, JSON.parse(event.data)); } catch {}
2298
+ });
2299
+ }
2300
+ }
2301
+
2302
+ function applyArtifactStatus(side, status) {
2303
+ if (!status || !["building", "ready", "invalid"].includes(status.status)) return;
2304
+ const generation = Number.isInteger(status.generation) && status.generation >= 0 ? status.generation : 0;
2305
+ if (generation < side.artifactGeneration) return;
2306
+ if (
2307
+ generation === side.artifactGeneration &&
2308
+ status.status === "building" &&
2309
+ side.artifactStatus !== "building"
2310
+ ) return;
2311
+ side.artifactGeneration = generation;
2312
+ side.artifactStatus = status.status;
2313
+ side.artifactLastGoodGeneration = Number.isInteger(status.lastGoodGeneration)
2314
+ ? status.lastGoodGeneration
2315
+ : undefined;
2316
+ side.artifactDiagnostic = status.diagnostics?.[0]?.message;
2317
+ side.latestArtifactStatus = status;
2318
+ side.previewStatusReceiver?.(status);
2319
+ maybeEstablishInitialCamera();
2320
+ updateDocumentReadiness();
2321
+ }
2322
+
2323
+ function updateDocumentReadiness() {
2324
+ const sides = Object.values(states);
2325
+ const failed = sides.some((side) => side.runtimeFailed || (
2326
+ side.artifactStatus === "invalid" && !Number.isInteger(side.artifactLastGoodGeneration)
2327
+ ));
2328
+ const ready = sides.every((side) => side.bridgeReady && (
2329
+ side.artifactStatus === "ready" ||
2330
+ (side.artifactStatus === "invalid" && Number.isInteger(side.artifactLastGoodGeneration))
2331
+ ));
2332
+ const value = failed ? "error" : ready ? "idle" : "loading";
2333
+ document.documentElement.dataset.tileflowState = value;
2334
+ stage.dataset.tileflowState = value;
2335
+ }
2336
+
2337
+
2338
+ async function loadSidecar(side) {
2339
+ if (!side.definition.sidecarUrl) return undefined;
2340
+ const key = currentSidecarUrl(side);
2341
+ if (side.sidecarKey !== key) {
2342
+ side.sidecarAbort?.abort();
2343
+ side.sidecar = undefined;
2344
+ side.sidecarPromise = undefined;
2345
+ side.sidecarKey = key;
2346
+ }
2347
+ if (side.sidecar !== undefined) return side.sidecar;
2348
+ if (side.sidecarPromise) return side.sidecarPromise;
2349
+ const epoch = side.loadEpoch;
2350
+ const controller = new AbortController();
2351
+ side.sidecarAbort?.abort();
2352
+ side.sidecarAbort = controller;
2353
+ const promise = fetch(key, {cache: "no-store", signal: controller.signal})
2354
+ .then((response) => response.ok ? response.json() : Promise.reject(new Error("Compiler sidecar unavailable")))
2355
+ .then((document) => {
2356
+ if (side.loadEpoch !== epoch || side.sidecarKey !== key || controller.signal.aborted) return undefined;
2357
+ side.sidecar = document;
2358
+ if (selectedInspection?.side === side) renderInspection();
2359
+ return document;
2360
+ })
2361
+ .catch((error) => {
2362
+ if (error?.name !== "AbortError" && selectedInspection?.side === side) {
2363
+ sidecarOutput.textContent = "Unable to load compiler sidecar; reload or select the feature to retry.";
2364
+ }
2365
+ return undefined;
2366
+ })
2367
+ .finally(() => {
2368
+ if (side.sidecarPromise === promise) side.sidecarPromise = undefined;
2369
+ if (side.sidecarAbort === controller) side.sidecarAbort = undefined;
2370
+ });
2371
+ side.sidecarPromise = promise;
2372
+ return promise;
2373
+ }
2374
+
2375
+ function currentSidecarUrl(side) {
2376
+ const configured = new URL(side.definition.sidecarUrl, location.href);
2377
+ const marker = "/__inspection/";
2378
+ const markerIndex = configured.pathname.lastIndexOf(marker);
2379
+ if (markerIndex < 0) return configured.href;
2380
+ const suffixParts = configured.pathname.slice(markerIndex + marker.length).split("/");
2381
+ const configuredMap = suffixParts[0];
2382
+ const configuredThemeFile = suffixParts[1];
2383
+ const configuredTheme = configuredThemeFile?.endsWith(".json") ? configuredThemeFile.slice(0, -5) : undefined;
2384
+ if (suffixParts.length !== 2 || !validPortableName(configuredMap) || !validPortableName(configuredTheme)) {
2385
+ return configured.href;
2386
+ }
2387
+ const metadata = side.map?.getStyle?.().metadata;
2388
+ const map = metadata?.["tileflow:map"];
2389
+ const theme = metadata?.["tileflow:theme"];
2390
+ if (!validPortableName(map) || !validPortableName(theme)) {
2391
+ return configured.href;
2392
+ }
2393
+ configured.pathname = configured.pathname.slice(0, markerIndex) + marker + map + "/" + theme + ".json";
2394
+ const controls = livePreviewUrl(side).searchParams.get("controls");
2395
+ if (controls) configured.searchParams.set("controls", controls);
2396
+ else configured.searchParams.delete("controls");
2397
+ return configured.href;
2398
+ }
2399
+
2400
+ // Metadata refreshes update the inspection without navigating away from the active tab.
2401
+ function renderInspection(reveal = false) {
2402
+ if (!selectedInspection) return;
2403
+ const side = selectedInspection.side;
2404
+ const feature = selectedInspection.features[selectedInspection.featureIndex];
2405
+ inspectionOutput.replaceChildren();
2406
+ for (const entry of Object.values(states)) entry.highlightFeature?.(entry === side ? selectedInspection.featureIndex : -1);
2407
+ if (!feature) {
2408
+ inspectionOutput.className = "empty";
2409
+ inspectionOutput.textContent = side.definition.label + ": no rendered feature at this point.";
2410
+ curveProperty.disabled = true;
2411
+ curveOutput.className = "empty";
2412
+ curveOutput.textContent = "Paint and layout values appear here.";
2413
+ sidecarOutput.textContent = "No layer selected.";
2414
+ if (reveal) globalThis.__tileflowInspectorUI?.selectionChanged();
2415
+ return;
2416
+ }
2417
+ inspectionOutput.className = "";
2418
+ const layer = side.map?.getStyle?.().layers?.find((candidate) => candidate.id === feature.layerId);
2419
+ const title = document.createElement("strong");
2420
+ title.textContent = side.definition.label + " \xB7 " + feature.layerId;
2421
+ const pickerLabel = document.createElement("label");
2422
+ pickerLabel.className = "feature-picker";
2423
+ pickerLabel.append(document.createTextNode("Elements at this point (" + selectedInspection.features.length + ")"));
2424
+ const picker = document.createElement("select");
2425
+ picker.setAttribute("aria-label", "Rendered feature at inspected point");
2426
+ selectedInspection.features.forEach((candidate, index) => {
2427
+ const option = document.createElement("option");
2428
+ option.value = String(index);
2429
+ option.textContent = options.workbenchUrl ? featureDisplayName(candidate) + " \xB7 " + candidate.geometryType : String(index + 1) + ". " + candidate.layerId + " \xB7 " + candidate.geometryType + (candidate.id === null ? "" : " \xB7 " + String(candidate.id));
2430
+ option.title = candidate.layerId;
2431
+ picker.append(option);
2432
+ });
2433
+ picker.value = String(selectedInspection.featureIndex);
2434
+ picker.addEventListener("change", () => {
2435
+ selectedInspection.featureIndex = boundedInteger(Number(picker.value), 0, selectedInspection.features.length - 1, 0);
2436
+ renderInspection(true);
2437
+ });
2438
+ pickerLabel.append(picker);
2439
+ const summary = document.createElement("pre");
2440
+ summary.textContent = boundedJson({
2441
+ coordinate: selectedInspection.coordinate,
2442
+ feature: {id: feature.id, geometryType: feature.geometryType, properties: feature.properties},
2443
+ layer: layer ? {
2444
+ id: layer.id,
2445
+ type: layer.type,
2446
+ source: layer.source,
2447
+ sourceLayer: layer["source-layer"],
2448
+ minzoom: layer.minzoom,
2449
+ maxzoom: layer.maxzoom,
2450
+ filter: layer.filter
2451
+ } : undefined
2452
+ });
2453
+ if (options.workbenchUrl) renderFeatureSummary(side, feature, layer, pickerLabel, summary);
2454
+ else inspectionOutput.append(title, pickerLabel, summary);
2455
+ populateCurveProperties(layer);
2456
+ const sidecarLayer = findSidecarLayer(side.sidecar, feature.layerId);
2457
+ sidecarOutput.textContent = sidecarLayer
2458
+ ? boundedJson(sidecarLayerWithSemanticAttribution(sidecarLayer))
2459
+ : "No compiler sidecar entry for this layer.";
2460
+ if (side.sidecar === undefined && !side.sidecarPromise) void loadSidecar(side);
2461
+ if (reveal) globalThis.__tileflowInspectorUI?.selectionChanged();
2462
+ }
2463
+
2464
+ function populateCurveProperties(layer) {
2465
+ const previousValue = curveProperty.value;
2466
+ curveProperty.replaceChildren();
2467
+ if (!layer) {
2468
+ curveProperty.disabled = true;
2469
+ return;
2470
+ }
2471
+ const entries = [];
2472
+ for (const section of ["paint", "layout"]) {
2473
+ for (const name of Object.keys(layer[section] || {}).sort()) entries.push({section, name});
2474
+ }
2475
+ if (entries.length === 0) {
2476
+ curveProperty.disabled = true;
2477
+ curveOutput.textContent = "This layer has no paint or layout values.";
2478
+ return;
2479
+ }
2480
+ for (const entry of entries) {
2481
+ const option = document.createElement("option");
2482
+ option.value = entry.section + ":" + entry.name;
2483
+ option.textContent = entry.section + "." + entry.name;
2484
+ curveProperty.append(option);
2485
+ }
2486
+ if (Array.from(curveProperty.options).some((option) => option.value === previousValue)) {
2487
+ curveProperty.value = previousValue;
2488
+ }
2489
+ curveProperty.disabled = false;
2490
+ renderCurve();
2491
+ }
2492
+
2493
+ function renderCurve() {
2494
+ if (!selectedInspection) return;
2495
+ const side = selectedInspection.side;
2496
+ const feature = selectedInspection.features[selectedInspection.featureIndex];
2497
+ if (!feature) return;
2498
+ const layer = side.map?.getStyle?.().layers?.find((candidate) => candidate.id === feature.layerId);
2499
+ const [section, name] = curveProperty.value.split(":");
2500
+ const raw = layer?.[section]?.[name];
2501
+ curveOutput.replaceChildren();
2502
+ if (raw === undefined) return;
2503
+ const maximumZoom = 24;
2504
+ const samples = Array.from({length: maximumZoom * 4 + 1}, (_, index) => {
2505
+ const zoom = index / 4;
2506
+ return {zoom, value: evaluateBasicZoomValue(raw, zoom)};
2507
+ });
2508
+ if (samples.some((sample) => sample.value === unsupportedValue)) {
2509
+ const pre = document.createElement("pre");
2510
+ pre.textContent = "Unsupported or data-driven expression; no approximate curve is shown.\\n" + boundedJson(raw);
2511
+ curveOutput.append(pre);
2512
+ return;
2513
+ }
2514
+ const currentZoom = boundedNumber(side.camera?.zoom ?? canonicalCamera?.zoom, 0, maximumZoom, 0);
2515
+ const currentValue = evaluateBasicZoomValue(raw, currentZoom);
2516
+ const meta = document.createElement("div");
2517
+ meta.className = "curve-meta";
2518
+ meta.textContent = "Current z" + formatNumber(currentZoom) + ": " + formatCurveValue(currentValue) + " \xB7 sampled every 0.25 zoom";
2519
+ curveOutput.append(meta);
2520
+ if (samples.every((sample) => typeof sample.value === "number" && Number.isFinite(sample.value))) {
2521
+ curveOutput.append(renderNumericCurve(samples, currentZoom));
2522
+ return;
2523
+ }
2524
+ if (samples.every((sample) => parseColor(sample.value))) {
2525
+ const strip = document.createElement("div");
2526
+ strip.className = "color-curve";
2527
+ for (const sample of samples) {
2528
+ const swatch = document.createElement("span");
2529
+ swatch.style.background = colorToCss(parseColor(sample.value));
2530
+ swatch.title = "z" + formatNumber(sample.zoom) + ": " + String(sample.value);
2531
+ strip.append(swatch);
2532
+ }
2533
+ curveOutput.append(strip);
2534
+ return;
2535
+ }
2536
+ const pre = document.createElement("pre");
2537
+ pre.textContent = samples.map((sample) => "z" + sample.zoom + " " + String(sample.value)).join("\\n");
2538
+ curveOutput.append(pre);
2539
+ }
2540
+
2541
+ const unsupportedValue = Symbol("unsupported");
2542
+ function evaluateBasicZoomValue(value, zoom) {
2543
+ if (!Array.isArray(value)) {
2544
+ return value && typeof value === "object" && ("stops" in value || "property" in value)
2545
+ ? unsupportedValue
2546
+ : value;
2547
+ }
2548
+ if (value[0] === "literal") return value[1];
2549
+ if (value[0] === "step" && isZoomInput(value[1])) {
2550
+ let output = value[2];
2551
+ for (let index = 3; index + 1 < value.length; index += 2) {
2552
+ if (zoom < value[index]) break;
2553
+ output = value[index + 1];
2554
+ }
2555
+ return evaluateBasicZoomValue(output, zoom);
2556
+ }
2557
+ if (value[0] === "interpolate" && isZoomInput(value[2])) {
2558
+ const stops = [];
2559
+ for (let index = 3; index + 1 < value.length; index += 2) stops.push([value[index], value[index + 1]]);
2560
+ if (!stops.length) return unsupportedValue;
2561
+ if (zoom <= stops[0][0]) return evaluateBasicZoomValue(stops[0][1], zoom);
2562
+ if (zoom >= stops.at(-1)[0]) return evaluateBasicZoomValue(stops.at(-1)[1], zoom);
2563
+ const upperIndex = stops.findIndex((stop) => zoom < stop[0]);
2564
+ const lower = stops[upperIndex - 1];
2565
+ const upper = stops[upperIndex];
2566
+ const interpolation = value[1];
2567
+ let base;
2568
+ if (Array.isArray(interpolation) && interpolation.length === 1 && interpolation[0] === "linear") {
2569
+ base = 1;
2570
+ } else if (
2571
+ Array.isArray(interpolation) && interpolation.length === 2 &&
2572
+ interpolation[0] === "exponential" && Number.isFinite(Number(interpolation[1])) &&
2573
+ Number(interpolation[1]) > 0
2574
+ ) {
2575
+ base = Number(interpolation[1]);
2576
+ } else {
2577
+ return unsupportedValue;
2578
+ }
2579
+ const progress = interpolationProgress(zoom, lower[0], upper[0], base);
2580
+ const from = evaluateBasicZoomValue(lower[1], zoom);
2581
+ const to = evaluateBasicZoomValue(upper[1], zoom);
2582
+ if (typeof from === "number" && typeof to === "number") return from + (to - from) * progress;
2583
+ const fromColor = parseColor(from);
2584
+ const toColor = parseColor(to);
2585
+ return fromColor && toColor ? colorToCss(fromColor.map((part, index) => part + (toColor[index] - part) * progress)) : unsupportedValue;
2586
+ }
2587
+ return unsupportedValue;
2588
+ }
2589
+
2590
+ function isZoomInput(value) { return Array.isArray(value) && value.length === 1 && value[0] === "zoom"; }
2591
+ function interpolationProgress(value, lower, upper, base) {
2592
+ const distance = upper - lower;
2593
+ if (!distance) return 0;
2594
+ if (!Number.isFinite(base) || base === 1) return (value - lower) / distance;
2595
+ return (Math.pow(base, value - lower) - 1) / (Math.pow(base, distance) - 1);
2596
+ }
2597
+
2598
+ function renderNumericCurve(samples, currentZoom) {
2599
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
2600
+ svg.setAttribute("class", "curve");
2601
+ svg.setAttribute("viewBox", "0 0 320 116");
2602
+ const values = samples.map((sample) => sample.value);
2603
+ const minimum = Math.min(...values);
2604
+ const maximum = Math.max(...values);
2605
+ const span = maximum - minimum || 1;
2606
+ const points = samples.map((sample) => {
2607
+ const x = 10 + sample.zoom / 24 * 300;
2608
+ const y = 104 - (sample.value - minimum) / span * 92;
2609
+ return x.toFixed(2) + "," + y.toFixed(2);
2610
+ }).join(" ");
2611
+ const polyline = document.createElementNS(svg.namespaceURI, "polyline");
2612
+ polyline.setAttribute("points", points);
2613
+ polyline.setAttribute("fill", "none");
2614
+ polyline.setAttribute("stroke", "#3157d5");
2615
+ polyline.setAttribute("stroke-width", "2");
2616
+ svg.append(polyline);
2617
+ const marker = document.createElementNS(svg.namespaceURI, "line");
2618
+ const markerX = 10 + currentZoom / 24 * 300;
2619
+ marker.setAttribute("x1", String(markerX));
2620
+ marker.setAttribute("x2", String(markerX));
2621
+ marker.setAttribute("y1", "8");
2622
+ marker.setAttribute("y2", "108");
2623
+ marker.setAttribute("stroke", "#ba3838");
2624
+ marker.setAttribute("stroke-width", "1.5");
2625
+ marker.setAttribute("stroke-dasharray", "3 3");
2626
+ svg.append(marker);
2627
+ const title = document.createElementNS(svg.namespaceURI, "title");
2628
+ title.textContent = "minimum " + minimum + ", maximum " + maximum + ", current zoom " + formatNumber(currentZoom);
2629
+ svg.append(title);
2630
+ svg.setAttribute("role", "img");
2631
+ svg.setAttribute("aria-label", title.textContent);
2632
+ return svg;
2633
+ }
2634
+
2635
+ async function loadSprites(side) {
2636
+ const epoch = side.loadEpoch;
2637
+ const sequence = ++spriteLoadSequence;
2638
+ spriteOwnerId = side.id;
2639
+ side.spriteAbort?.abort();
2640
+ const controller = new AbortController();
2641
+ side.spriteAbort = controller;
2642
+ spriteOutput.replaceChildren();
2643
+ const style = side.map?.getStyle?.();
2644
+ if (typeof style?.sprite !== "string") {
2645
+ spriteOutput.textContent = "The active style has no single served sprite atlas.";
2646
+ if (side.spriteAbort === controller) side.spriteAbort = undefined;
2647
+ return;
2648
+ }
2649
+ let base;
2650
+ try {
2651
+ base = new URL(style.sprite, side.frame.contentWindow.location.href);
2652
+ } catch {
2653
+ spriteOutput.textContent = "Invalid sprite URL.";
2654
+ if (side.spriteAbort === controller) side.spriteAbort = undefined;
2655
+ return;
2656
+ }
2657
+ if (!urlBelongsToSide(base, side)) {
2658
+ spriteOutput.textContent = "Only sprite atlases served by the active comparison side can be opened.";
2659
+ if (side.spriteAbort === controller) side.spriteAbort = undefined;
2660
+ return;
2661
+ }
2662
+ try {
2663
+ const atlasUrl = spriteAssetUrl(base, ".json");
2664
+ const imageUrl = spriteAssetUrl(base, ".png");
2665
+ const [atlasResponse, image] = await Promise.all([
2666
+ fetch(atlasUrl, {cache: "no-store", signal: controller.signal}),
2667
+ loadImage(imageUrl, controller.signal)
2668
+ ]);
2669
+ if (!atlasResponse.ok) throw new Error("Sprite metadata request failed");
2670
+ const atlas = await atlasResponse.json();
2671
+ if (
2672
+ controller.signal.aborted ||
2673
+ side.loadEpoch !== epoch ||
2674
+ sequence !== spriteLoadSequence ||
2675
+ activeSideId !== side.id
2676
+ ) return;
2677
+ const entries = Object.entries(atlas).sort(([left], [right]) => left.localeCompare(right)).slice(0, 256);
2678
+ for (const [name, item] of entries) {
2679
+ if (!validSpriteEntry(item, image)) continue;
2680
+ const card = document.createElement("div");
2681
+ card.className = "sprite-card";
2682
+ const canvas = document.createElement("canvas");
2683
+ const ratio = item.pixelRatio || 1;
2684
+ canvas.width = Math.max(1, Math.ceil(item.width / ratio));
2685
+ canvas.height = Math.max(1, Math.ceil(item.height / ratio));
2686
+ canvas.getContext("2d").drawImage(image, item.x, item.y, item.width, item.height, 0, 0, canvas.width, canvas.height);
2687
+ const label = document.createElement("span");
2688
+ label.className = "sprite-name";
2689
+ label.textContent = name;
2690
+ label.title = name;
2691
+ card.append(canvas, label);
2692
+ spriteOutput.append(card);
2693
+ }
2694
+ if (!spriteOutput.childElementCount) spriteOutput.textContent = "The atlas is empty.";
2695
+ } catch (error) {
2696
+ if (error?.name !== "AbortError" && sequence === spriteLoadSequence) {
2697
+ spriteOutput.textContent = "Unable to load the served sprite atlas: " + safeErrorMessage(error);
2698
+ }
2699
+ } finally {
2700
+ if (side.spriteAbort === controller) side.spriteAbort = undefined;
2701
+ }
2702
+ }
2703
+
2704
+ function loadImage(url, signal) {
2705
+ return new Promise((resolve, reject) => {
2706
+ const image = new Image();
2707
+ const cleanup = () => signal.removeEventListener("abort", handleAbort);
2708
+ const handleAbort = () => {
2709
+ image.src = "";
2710
+ cleanup();
2711
+ reject(new DOMException("Sprite image request aborted", "AbortError"));
2712
+ };
2713
+ image.onload = () => { cleanup(); resolve(image); };
2714
+ image.onerror = () => { cleanup(); reject(new Error("Sprite image request failed")); };
2715
+ if (signal.aborted) return handleAbort();
2716
+ signal.addEventListener("abort", handleAbort, {once: true});
2717
+ image.src = url;
2718
+ });
2719
+ }
2720
+
2721
+ function urlBelongsToSide(url, side) {
2722
+ const basePath = side.definition.basePath;
2723
+ return url.origin === location.origin &&
2724
+ (url.pathname === basePath || url.pathname.startsWith(basePath + "/"));
2725
+ }
2726
+
2727
+ function spriteAssetUrl(base, suffix) {
2728
+ const asset = new URL(base.href);
2729
+ asset.pathname += suffix;
2730
+ return asset.href;
2731
+ }
2732
+
2733
+ function validSpriteEntry(value, image) {
2734
+ return value && [value.x, value.y, value.width, value.height].every(Number.isFinite) &&
2735
+ value.x >= 0 && value.y >= 0 && value.width > 0 && value.height > 0 &&
2736
+ value.x + value.width <= image.naturalWidth && value.y + value.height <= image.naturalHeight;
2737
+ }
2738
+
2739
+
2740
+
2741
+
2742
+ function writeUrlState() {
2743
+ const url = new URL(location.href);
2744
+ url.searchParams.set("mode", stage.dataset.mode);
2745
+ url.searchParams.set("split", String(Math.round(initialUi.split)));
2746
+ url.searchParams.set("alpha", String(Math.round(initialUi.alpha)));
2747
+ url.searchParams.set("driver", stage.dataset.driver);
2748
+ if (canonicalCamera) {
2749
+ const values = {
2750
+ lng: canonicalCamera.center[0], lat: canonicalCamera.center[1], zoom: canonicalCamera.zoom,
2751
+ bearing: canonicalCamera.bearing, pitch: canonicalCamera.pitch
2752
+ };
2753
+ for (const [name, value] of Object.entries(values)) url.searchParams.set(name, formatNumber(value));
2754
+ }
2755
+ history.replaceState(history.state, "", url.href);
2756
+ }
2757
+
2758
+ function readUiFromUrl() {
2759
+ const query = new URL(location.href).searchParams;
2760
+ const modeValue = singleQueryValue(query, "mode");
2761
+ const mode = ["single", "side-by-side", "split", "overlay", "blink"].includes(modeValue) ? modeValue : undefined;
2762
+ const driver = singleQueryValue(query, "driver");
2763
+ selectSide(driver === "right" && options.right ? "right" : "left", false);
2764
+ const split = singleQueryValue(query, "split");
2765
+ const alpha = singleQueryValue(query, "alpha");
2766
+ return {
2767
+ mode,
2768
+ split: split === undefined ? 50 : boundedNumber(Number(split), 5, 95, 50),
2769
+ alpha: alpha === undefined ? 50 : boundedNumber(Number(alpha), 0, 100, 50)
2770
+ };
2771
+ }
2772
+
2773
+ function cameraFromUrl() {
2774
+ const query = new URL(location.href).searchParams;
2775
+ if (!["lng", "lat", "zoom", "bearing", "pitch"].every((name) => query.getAll(name).length === 1)) return undefined;
2776
+ const camera = {
2777
+ center: [Number(query.get("lng")), Number(query.get("lat"))],
2778
+ zoom: Number(query.get("zoom")), bearing: Number(query.get("bearing")), pitch: Number(query.get("pitch"))
2779
+ };
2780
+ return validCamera(camera) ? camera : undefined;
2781
+ }
2782
+
2783
+ function readMapCamera(map) {
2784
+ const center = map.getCenter();
2785
+ return {
2786
+ center: [wrapLongitude(center.lng), center.lat], zoom: map.getZoom(),
2787
+ bearing: map.getBearing(), pitch: map.getPitch()
2788
+ };
2789
+ }
2790
+
2791
+ function validCamera(camera) {
2792
+ return camera && Array.isArray(camera.center) && camera.center.length === 2 &&
2793
+ camera.center.every(Number.isFinite) && camera.center[0] >= -180 && camera.center[0] <= 180 &&
2794
+ camera.center[1] >= -90 && camera.center[1] <= 90 && Number.isFinite(camera.zoom) &&
2795
+ camera.zoom >= 0 && camera.zoom <= 24 && Number.isFinite(camera.bearing) &&
2796
+ camera.bearing >= -180 && camera.bearing <= 180 && Number.isFinite(camera.pitch) &&
2797
+ camera.pitch >= 0 && camera.pitch <= 85;
2798
+ }
2799
+
2800
+ function projectFeature(feature) {
2801
+ return {
2802
+ layerId: String(feature.layer?.id || "").slice(0, 256),
2803
+ id: ["number", "string"].includes(typeof feature.id) ? feature.id : null,
2804
+ geometryType: String(feature.geometry?.type || "Unknown").slice(0, 32),
2805
+ properties: sanitizeProperties(feature.properties)
2806
+ };
2807
+ }
2808
+
2809
+ function sanitizeProperties(input) {
2810
+ const result = {};
2811
+ for (const key of Object.keys(input || {}).sort().slice(0, 64)) {
2812
+ if (key.length > 128) continue;
2813
+ const value = input[key];
2814
+ if (typeof value === "boolean" || (typeof value === "number" && Number.isFinite(value))) result[key] = value;
2815
+ else if (typeof value === "string") result[key] = value.slice(0, 512);
2816
+ }
2817
+ return result;
2818
+ }
2819
+
2820
+ function findSidecarLayer(document, layerId) {
2821
+ const layers = document?.layers;
2822
+ if (Array.isArray(layers)) return layers.find((layer) => layer?.id === layerId || layer?.layerId === layerId);
2823
+ if (layers && typeof layers === "object") return layers[layerId];
2824
+ return undefined;
2825
+ }
2826
+
2827
+ function sidecarLayerWithSemanticAttribution(layer) {
2828
+ // Sidecar v1 proves semantic provenance, not token provenance. Keep explicit
2829
+ // tokens when present, but never infer them by matching compiled values.
2830
+ const contributions = Array.isArray(layer?.contributions) ? layer.contributions : [];
2831
+ const semanticAttribution = contributions.flatMap((contribution) => {
2832
+ const semanticOwner = typeof contribution?.owner === "string" ? contribution.owner : undefined;
2833
+ const semanticTarget = typeof contribution?.target === "string" ? contribution.target : undefined;
2834
+ if (!semanticOwner || !semanticTarget) return [];
2835
+ const operations = Array.isArray(contribution.operations) ? contribution.operations : [];
2836
+ const authoringPaths = [...new Set([
2837
+ "modules." + semanticOwner,
2838
+ ...causalStringArray(contribution.authoringPaths),
2839
+ ...(operations.length > 0 ? ["modules." + semanticOwner + ".renderStack"] : [])
2840
+ ])];
2841
+ const themeTokens = causalArray(contribution.themeTokens);
2842
+ return [{
2843
+ semanticOwner,
2844
+ semanticTarget,
2845
+ authoringPaths,
2846
+ ...(themeTokens.length > 0 ? {themeTokens} : {})
2847
+ }];
2848
+ });
2849
+ return {...layer, semanticAttribution};
2850
+ }
2851
+
2852
+ function causalStringArray(value) {
2853
+ return causalArray(value).filter((entry) => typeof entry === "string");
2854
+ }
2855
+
2856
+ function causalArray(value) {
2857
+ return Array.isArray(value) ? value.slice(0, 64) : [];
2858
+ }
2859
+
2860
+ function markSideRuntimeError(side, input) {
2861
+ const message = sanitizeRuntimeMessage(safeErrorMessage(input));
2862
+ if (!side.runtimeErrors.includes(message)) side.runtimeErrors.push(message);
2863
+ side.runtimeErrors = side.runtimeErrors.slice(-16);
2864
+ side.runtimeFailed = true;
2865
+ updateDocumentReadiness();
2866
+ }
2867
+
2868
+ function sanitizeRuntimeMessage(value) {
2869
+ return String(value).replace(/https?:\\/\\/[^\\s)]+/gu, (url) => {
2870
+ try { return new URL(url).origin; } catch { return "[url]"; }
2871
+ }).slice(0, 512);
2872
+ }
2873
+
2874
+ function parseColor(value) {
2875
+ if (typeof value !== "string") return undefined;
2876
+ const hex = /^#([a-f\\d]{3}|[a-f\\d]{6}|[a-f\\d]{8})$/iu.exec(value);
2877
+ if (hex) {
2878
+ let digits = hex[1];
2879
+ if (digits.length === 3) digits = digits.split("").map((part) => part + part).join("");
2880
+ const alpha = digits.length === 8 ? parseInt(digits.slice(6, 8), 16) / 255 : 1;
2881
+ return [parseInt(digits.slice(0, 2), 16), parseInt(digits.slice(2, 4), 16), parseInt(digits.slice(4, 6), 16), alpha];
2882
+ }
2883
+ const rgb = /^rgba?\\(\\s*([\\d.]+)\\s*,\\s*([\\d.]+)\\s*,\\s*([\\d.]+)(?:\\s*,\\s*([\\d.]+))?\\s*\\)$/iu.exec(value);
2884
+ return rgb ? [Number(rgb[1]), Number(rgb[2]), Number(rgb[3]), rgb[4] === undefined ? 1 : Number(rgb[4])] : undefined;
2885
+ }
2886
+
2887
+ function colorToCss(color) {
2888
+ return "rgba(" + color.slice(0, 3).map((part) => Math.round(part)).join(",") + "," + Math.max(0, Math.min(1, color[3])) + ")";
2889
+ }
2890
+
2891
+ function boundedJson(value) {
2892
+ let result;
2893
+ try { result = JSON.stringify(value, null, 2); } catch { result = "[unserializable]"; }
2894
+ if (typeof result !== "string") result = String(value);
2895
+ return result.length > 12_000 ? result.slice(0, 12_000) + "\\n\u2026" : result;
2896
+ }
2897
+ function camerasEqual(left, right) {
2898
+ return Boolean(left && right) &&
2899
+ Math.abs(left.center[0] - right.center[0]) < 1e-7 &&
2900
+ Math.abs(left.center[1] - right.center[1]) < 1e-7 &&
2901
+ Math.abs(left.zoom - right.zoom) < 1e-7 &&
2902
+ Math.abs(left.bearing - right.bearing) < 1e-7 &&
2903
+ Math.abs(left.pitch - right.pitch) < 1e-7;
2904
+ }
2905
+ function formatCurveValue(value) {
2906
+ if (typeof value === "number" && Number.isFinite(value)) return formatNumber(value);
2907
+ return typeof value === "string" ? value : boundedJson(value);
2908
+ }
2909
+ function singleQueryValue(query, name) {
2910
+ const values = query.getAll(name);
2911
+ return values.length === 1 ? values[0] : undefined;
2912
+ }
2913
+ function validPortableName(value) {
2914
+ return typeof value === "string" &&
2915
+ /^[a-z][a-z0-9-]{0,63}$/u.test(value) &&
2916
+ value !== "constructor" && value !== "prototype" &&
2917
+ !/^(?:aux|con|nul|prn|com[1-9]|lpt[1-9])$/u.test(value);
2918
+ }
2919
+ function validConcreteThemeName(value) { return validPortableName(value) && value !== "system"; }
2920
+ function quoteCliArgument(value) {
2921
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/u.test(value)) return value;
2922
+ const quote = String.fromCharCode(39);
2923
+ const escapedQuote = quote + '"' + quote + '"' + quote;
2924
+ return quote + value.replaceAll(quote, escapedQuote) + quote;
2925
+ }
2926
+ function safeErrorMessage(error) { return error instanceof Error ? error.message : String(error || "Unknown error"); }
2927
+ function boundedNumber(value, minimum, maximum, fallback) { return Number.isFinite(value) ? Math.max(minimum, Math.min(maximum, value)) : fallback; }
2928
+ function boundedInteger(value, minimum, maximum, fallback) { return Number.isInteger(value) && value >= minimum && value <= maximum ? value : fallback; }
2929
+ function roundNumber(value) { return Number(value.toFixed(6)); }
2930
+ function formatNumber(value) { return String(roundNumber(value)); }
2931
+ function wrapLongitude(value) { return ((value + 180) % 360 + 360) % 360 - 180; }
2932
+ function setFeedback(value) { globalThis.__tileflowInspectorUI?.feedback(value); }
2933
+ ${controlsClient}
2934
+ ${workbenchClient}
2935
+ globalThis.__tileflowInspectorHost = {
2936
+ schemaVersion: 1,
2937
+ subscribePreviewStatus: (child, receive) => {
2938
+ const side = Object.values(states).find((entry) => entry.frame.contentWindow === child);
2939
+ if (!side) return undefined;
2940
+ side.previewStatusReceiver = receive;
2941
+ if (side.latestArtifactStatus) receive(side.latestArtifactStatus);
2942
+ return () => { if (side.previewStatusReceiver === receive) side.previewStatusReceiver = undefined; };
2943
+ },
2944
+ sides: () => Object.values(states), active: () => states[activeSideId],
2945
+ selection: () => selectedInspection,
2946
+ clearFeatureSelection: () => {
2947
+ selectedInspection = undefined;
2948
+ for (const side of Object.values(states)) { side.highlightFeature?.(-1); side.hoverFeature?.(-1); }
2949
+ },
2950
+ loadSidecar: (side) => loadSidecar(side),
2951
+ selectFeature: (index) => { if (selectedInspection && selectedInspection.features[index]) { selectedInspection.featureIndex = index; renderInspection(true); } },
2952
+ inspect: (enabled) => setInspectionEnabled(enabled), inspecting: () => inspectionEnabled,
2953
+ controls: () => loadMapControls(),
2954
+ sprites: () => loadSprites(states[activeSideId])
2955
+ };
2956
+ </script>
2957
+ <script type="module"${nonceAttribute} src="${fontBasePath}/__workbench-runtime/inspector.js"></script>
2958
+ </body>
2959
+ </html>`;
2960
+ }
2961
+ function serializeInlineJson(value) {
2962
+ return JSON.stringify(value).replace(
2963
+ /[<>&\u2028\u2029]/gu,
2964
+ (character) => `\\u${character.codePointAt(0).toString(16).padStart(4, "0")}`
2965
+ );
2966
+ }
2967
+ function escapeHtml(value) {
2968
+ return value.replace(/[&<>"']/gu, (character) => {
2969
+ switch (character) {
2970
+ case "&":
2971
+ return "&amp;";
2972
+ case "<":
2973
+ return "&lt;";
2974
+ case ">":
2975
+ return "&gt;";
2976
+ case '"':
2977
+ return "&quot;";
2978
+ default:
2979
+ return "&#39;";
2980
+ }
2981
+ });
2982
+ }
2983
+
2984
+ // src/comparison.ts
2985
+ var tileflowComparisonSchemaVersion = 1;
2986
+ function createTileflowComparisonRequestHandler(options) {
2987
+ const comparisonBasePath = normalizeTileflowBasePath(options.basePath);
2988
+ const left = normalizeSide(options.left, "left", comparisonBasePath);
2989
+ const right = options.right ? normalizeSide(options.right, "right", comparisonBasePath) : void 0;
2990
+ if (right) assertDisjointSideRoutes(left.basePath, right.basePath);
2991
+ const workbenchUrl = options.workbench ? normalizeTileflowBasePath(options.workbench.url) : void 0;
2992
+ if (workbenchUrl !== void 0) {
2993
+ if (!workbenchUrl || comparisonBasePath && !matchesBasePath(workbenchUrl, comparisonBasePath))
2994
+ throw new TypeError(
2995
+ "Inspector API must own a non-root route inside the comparison base path."
2996
+ );
2997
+ assertDisjointSideRoutes(workbenchUrl, left.basePath);
2998
+ if (right) assertDisjointSideRoutes(workbenchUrl, right.basePath);
2999
+ assertDisjointSideRoutes(workbenchUrl, `${comparisonBasePath}/__workbench-fonts`);
3000
+ assertDisjointSideRoutes(workbenchUrl, `${comparisonBasePath}/__workbench-runtime`);
3001
+ }
3002
+ const title = normalizeLabel(options.title ?? "Tileflow Inspector", "title");
3003
+ const initialMode = right ? options.initialMode ?? "side-by-side" : "single";
3004
+ return async function handleTileflowComparisonRequest(request) {
3005
+ const url = new URL(request.url);
3006
+ const pathname = url.pathname;
3007
+ if (pathname === `${comparisonBasePath}/__workbench-runtime/inspector.js`) {
3008
+ if (request.method !== "GET" && request.method !== "HEAD")
3009
+ return jsonResponse({ error: "Method not allowed" }, 405);
3010
+ const source = await readFile(
3011
+ new URL("../dist/inspector-browser.js", import.meta.url),
3012
+ "utf8"
3013
+ );
3014
+ return new Response(request.method === "HEAD" ? null : source, {
3015
+ headers: {
3016
+ "Content-Type": "text/javascript; charset=utf-8",
3017
+ "Cache-Control": "no-store",
3018
+ "X-Content-Type-Options": "nosniff"
3019
+ }
3020
+ });
3021
+ }
3022
+ const font = pathname.slice(`${comparisonBasePath}/__workbench-fonts/`.length);
3023
+ if (pathname.startsWith(`${comparisonBasePath}/__workbench-fonts/`) && ["mona-sans.woff2", "hubot-sans.woff2", "inter.woff2", "jetbrains-mono.woff2"].includes(font)) {
3024
+ if (request.method !== "GET" && request.method !== "HEAD")
3025
+ return jsonResponse({ error: "Method not allowed" }, 405);
3026
+ const bytes = await readFile(new URL(`../ui-fonts/${font}`, import.meta.url));
3027
+ return new Response(request.method === "HEAD" ? null : bytes, {
3028
+ headers: {
3029
+ "Content-Type": "font/woff2",
3030
+ "Cache-Control": "public, max-age=3600",
3031
+ "X-Content-Type-Options": "nosniff"
3032
+ }
3033
+ });
3034
+ }
3035
+ if (options.workbench && workbenchUrl && matchesBasePath(pathname, workbenchUrl))
3036
+ return await options.workbench.handle(request);
3037
+ if (matchesBasePath(pathname, left.basePath)) return await left.handler(request);
3038
+ if (right && matchesBasePath(pathname, right.basePath)) return await right.handler(request);
3039
+ if (isComparisonRoot(pathname, comparisonBasePath)) {
3040
+ if (request.method !== "GET" && request.method !== "HEAD") {
3041
+ return jsonResponse({ error: "Method not allowed" }, 405);
3042
+ }
3043
+ const cspNonce = randomBytes(18).toString("base64");
3044
+ const html = renderTileflowComparisonHtml({
3045
+ basePath: comparisonBasePath,
3046
+ cspNonce,
3047
+ initialMode,
3048
+ left: publicSide(left),
3049
+ ...right ? { right: publicSide(right) } : {},
3050
+ schemaVersion: tileflowComparisonSchemaVersion,
3051
+ title,
3052
+ ...workbenchUrl ? { workbenchUrl } : {}
3053
+ });
3054
+ return new Response(request.method === "HEAD" ? null : html, {
3055
+ headers: {
3056
+ "Cache-Control": "no-store",
3057
+ "Content-Security-Policy": comparisonContentSecurityPolicy(cspNonce),
3058
+ "Content-Type": "text/html; charset=utf-8",
3059
+ "Referrer-Policy": "no-referrer",
3060
+ "X-Content-Type-Options": "nosniff"
3061
+ }
3062
+ });
3063
+ }
3064
+ return jsonResponse({ error: "Not found" }, 404);
3065
+ };
3066
+ }
3067
+ function comparisonContentSecurityPolicy(nonce) {
3068
+ return [
3069
+ "default-src 'none'",
3070
+ "base-uri 'none'",
3071
+ "connect-src 'self'",
3072
+ "font-src 'self'",
3073
+ "form-action 'none'",
3074
+ "frame-ancestors 'none'",
3075
+ "frame-src 'self'",
3076
+ "img-src 'self' blob: data:",
3077
+ "object-src 'none'",
3078
+ `script-src 'nonce-${nonce}'`,
3079
+ `style-src-elem 'nonce-${nonce}'`,
3080
+ "style-src-attr 'unsafe-inline'"
3081
+ ].join("; ");
3082
+ }
3083
+ function normalizeSide(side, name, comparisonBasePath) {
3084
+ const basePath = normalizeTileflowBasePath(side.basePath);
3085
+ if (!basePath) {
3086
+ throw new TypeError(`Tileflow comparison ${name} basePath must not own the server root.`);
3087
+ }
3088
+ assertDisjointSideRoutes(basePath, `${comparisonBasePath}/__workbench-runtime`);
3089
+ if (comparisonBasePath && !matchesBasePath(basePath, comparisonBasePath)) {
3090
+ throw new TypeError(
3091
+ `Tileflow comparison ${name} basePath must be inside ${comparisonBasePath}.`
3092
+ );
3093
+ }
3094
+ if (typeof side.handler !== "function") {
3095
+ throw new TypeError(`Tileflow comparison ${name} handler must be a function.`);
3096
+ }
3097
+ return {
3098
+ ...side,
3099
+ basePath,
3100
+ ...side.captureConfig === void 0 ? {} : { captureConfig: normalizeCaptureConfig(side.captureConfig, `${name} captureConfig`) },
3101
+ eventsUrl: `${basePath}/__events`,
3102
+ label: normalizeLabel(side.label, `${name} label`),
3103
+ previewUrl: normalizeOwnedUrl(side.previewUrl, basePath, `${name} previewUrl`),
3104
+ ...side.sidecarUrl ? { sidecarUrl: normalizeOwnedUrl(side.sidecarUrl, basePath, `${name} sidecarUrl`) } : {},
3105
+ statusUrl: `${basePath}/__status`
3106
+ };
3107
+ }
3108
+ function publicSide(side) {
3109
+ return {
3110
+ basePath: side.basePath,
3111
+ ...side.captureConfig ? { captureConfig: side.captureConfig } : {},
3112
+ eventsUrl: side.eventsUrl,
3113
+ label: side.label,
3114
+ previewUrl: side.previewUrl,
3115
+ ...side.sidecarUrl ? { sidecarUrl: side.sidecarUrl } : {},
3116
+ statusUrl: side.statusUrl
3117
+ };
3118
+ }
3119
+ function normalizeCaptureConfig(value, label) {
3120
+ 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)) {
3121
+ throw new TypeError(`Invalid Tileflow comparison ${label}.`);
3122
+ }
3123
+ const segments = value.split("/");
3124
+ if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
3125
+ throw new TypeError(
3126
+ `Tileflow comparison ${label} must be a normalized path inside the current working directory.`
3127
+ );
3128
+ }
3129
+ return value;
3130
+ }
3131
+ function normalizeOwnedUrl(value, basePath, label) {
3132
+ if (typeof value !== "string" || value.length === 0 || value.length > 4096 || value !== value.trim() || !value.startsWith("/") || value.includes("\\") || /[\p{Cc}]/u.test(value)) {
3133
+ throw new TypeError(`Invalid Tileflow comparison ${label}.`);
3134
+ }
3135
+ let parsed;
3136
+ try {
3137
+ parsed = new URL(value, "http://tileflow.local");
3138
+ } catch {
3139
+ throw new TypeError(`Invalid Tileflow comparison ${label}.`);
3140
+ }
3141
+ if (parsed.origin !== "http://tileflow.local" || parsed.hash || !matchesBasePath(parsed.pathname, basePath)) {
3142
+ throw new TypeError(`Tileflow comparison ${label} must remain below ${basePath}.`);
3143
+ }
3144
+ return `${parsed.pathname}${parsed.search}`;
3145
+ }
3146
+ function normalizeLabel(value, label) {
3147
+ if (typeof value !== "string" || value.length === 0 || value.length > 160 || value !== value.trim() || /[\p{Cc}]/u.test(value)) {
3148
+ throw new TypeError(`Invalid Tileflow comparison ${label}.`);
3149
+ }
3150
+ return value.split(" \xB7 ").map((part, index) => index > 0 && /[\\/]/u.test(part) ? part.split(/[\\/]/u).at(-1) : part).join(" \xB7 ");
3151
+ }
3152
+ function assertDisjointSideRoutes(left, right) {
3153
+ if (matchesBasePath(left, right) || matchesBasePath(right, left)) {
3154
+ throw new TypeError("Tileflow comparison side base paths must be disjoint.");
3155
+ }
3156
+ }
3157
+ function isComparisonRoot(pathname, basePath) {
3158
+ return basePath ? pathname === basePath || pathname === `${basePath}/` : pathname === "/";
3159
+ }
3160
+ function matchesBasePath(pathname, basePath) {
3161
+ return pathname === basePath || pathname.startsWith(`${basePath}/`);
3162
+ }
3163
+ function jsonResponse(body, status) {
3164
+ return new Response(`${JSON.stringify(body, null, 2)}
3165
+ `, {
3166
+ status,
3167
+ headers: {
3168
+ "Cache-Control": "no-store",
3169
+ "Content-Type": "application/json; charset=utf-8",
3170
+ "X-Content-Type-Options": "nosniff"
3171
+ }
3172
+ });
3173
+ }
3174
+
3175
+ // src/server.ts
3176
+ function getTileflowStyleSelection(path) {
3177
+ const match = /^\/styles\/([a-z][a-z0-9-]{0,63})\/([a-z][a-z0-9-]{0,63})\.json$/u.exec(path);
3178
+ return match?.[1] && match[2] && tileflowMapIdSchema.safeParse(match[1]).success && tileflowThemeNameSchema.safeParse(match[2]).success ? { mapName: match[1], themeName: match[2] } : void 0;
3179
+ }
3180
+ function getTileflowStyleInspectionSelection(path) {
3181
+ const match = /^\/__inspection\/([a-z][a-z0-9-]{0,63})\/([a-z][a-z0-9-]{0,63})\.json$/u.exec(
3182
+ path
3183
+ );
3184
+ return match?.[1] && match[2] && tileflowMapIdSchema.safeParse(match[1]).success && tileflowThemeNameSchema.safeParse(match[2]).success ? { mapName: match[1], themeName: match[2] } : void 0;
3185
+ }
3186
+ function isTileflowRequestUrl(url, basePath) {
3187
+ return getTileflowRequestPath(url, basePath) !== null;
3188
+ }
3189
+ function getTileflowRequestPath(url, basePath) {
3190
+ const pathname = getRequestPathname(url);
3191
+ if (pathname === null) return null;
3192
+ const path = stripTileflowBasePath(pathname, normalizeTileflowBasePath(basePath));
3193
+ return path !== null && isOwnedTileflowRequestPath(path) ? path : null;
3194
+ }
3195
+ function createTileflowNodeRequest(request) {
3196
+ const protocol = getNodeHeader(request, "x-forwarded-proto") ?? "http";
3197
+ const host = getNodeHeader(request, "host") ?? "localhost";
3198
+ return new Request(`${protocol}://${host}${request.url ?? "/"}`, {
3199
+ headers: Object.fromEntries(
3200
+ Object.entries(request.headers).flatMap(
3201
+ ([name, value]) => value === void 0 ? [] : [[name, Array.isArray(value) ? value.join(", ") : value]]
3202
+ )
3203
+ ),
3204
+ method: request.method
3205
+ });
3206
+ }
3207
+ async function writeTileflowNodeResponse(response, tileflowResponse) {
3208
+ response.statusCode = tileflowResponse.status;
3209
+ tileflowResponse.headers.forEach((value, key) => {
3210
+ response.setHeader(key, value);
3211
+ });
3212
+ if (!tileflowResponse.body) {
3213
+ response.end();
3214
+ return;
3215
+ }
3216
+ const reader = tileflowResponse.body.getReader();
3217
+ try {
3218
+ while (true) {
3219
+ const { done, value } = await reader.read();
3220
+ if (done) break;
3221
+ if (response.destroyed) {
3222
+ await reader.cancel();
3223
+ return;
3224
+ }
3225
+ response.write(Buffer.from(value));
3226
+ }
3227
+ response.end();
3228
+ } finally {
3229
+ reader.releaseLock();
3230
+ }
3231
+ }
3232
+ function createTileflowDevRequestHandler(options = {}) {
3233
+ const basePath = normalizeTileflowBasePath(options.basePath);
3234
+ const configPath = options.config ?? defaultTileflowConfigPath;
3235
+ const cwd = options.cwd ?? process.cwd();
3236
+ const apiBaseUrl = options.apiBaseUrl ?? defaultTileflowApiUrl;
3237
+ let ownedSessionPromise;
3238
+ const lineBackgroundGlyphs = createTileflowLineBackgroundGlyphResponder({ cwd });
3239
+ let closed = false;
3240
+ let lastOrigin;
3241
+ let controlsGeneration;
3242
+ const controlsCache = /* @__PURE__ */ new Map();
3243
+ const loadOwnedSession = (origin) => {
3244
+ lastOrigin = origin;
3245
+ ownedSessionPromise ??= createTileflowArtifactSession({
3246
+ assetBaseUrl: `${origin}${basePath}`,
3247
+ config: configPath,
3248
+ cwd,
3249
+ inspection: options.inspection,
3250
+ styleBaseUrl: options.styleBaseUrl ?? `${origin}${basePath}`,
3251
+ apiBaseUrl
3252
+ }).catch((error) => {
3253
+ ownedSessionPromise = void 0;
3254
+ throw error;
3255
+ });
3256
+ return ownedSessionPromise;
3257
+ };
3258
+ const handleTileflowDevRequest = async (request) => {
3259
+ if (closed) return jsonResponse2({ error: "Tileflow request handler is closed." }, 503);
3260
+ if (request.method !== "GET" && request.method !== "HEAD") {
3261
+ return jsonResponse2({ error: "Method not allowed" }, 405);
3262
+ }
3263
+ const url = new URL(request.url);
3264
+ const path = getTileflowRequestPath(url.pathname, basePath);
3265
+ if (path === null) {
3266
+ return jsonResponse2({ error: "Not found" }, 404);
3267
+ }
3268
+ let acquisition;
3269
+ try {
3270
+ if (path === "/__events") {
3271
+ return options.session ? eventStreamResponse(options.session) : jsonResponse2({ error: "Live events require a development session." }, 404);
3272
+ }
3273
+ if (path.startsWith("/__runtime/")) {
3274
+ const runtimeResponse = (await import("./preview-assets-ATLLYY72.js")).getTileflowPreviewRuntimeResponse(path);
3275
+ if (runtimeResponse) return runtimeResponse;
3276
+ }
3277
+ const session = options.session ?? await loadOwnedSession(url.origin);
3278
+ acquisition = session.acquireArtifacts();
3279
+ const artifacts = acquisition?.artifacts;
3280
+ const state = session.getState();
3281
+ const controls = url.searchParams.get("controls");
3282
+ if (controls !== null) parsePreviewControls(controls);
3283
+ const controlled = (map, theme) => {
3284
+ if (!artifacts || controls === null) return void 0;
3285
+ if (controlsGeneration !== acquisition?.generation) {
3286
+ controlsCache.clear();
3287
+ controlsGeneration = acquisition?.generation;
3288
+ }
3289
+ const key = JSON.stringify([map, theme, controls]);
3290
+ let value = controlsCache.get(key);
3291
+ if (!value) {
3292
+ value = compilePreviewControls(artifacts, map, theme, controls, apiBaseUrl);
3293
+ if (controlsCache.size >= 8) controlsCache.delete(controlsCache.keys().next().value);
3294
+ controlsCache.set(key, value);
3295
+ }
3296
+ return value;
3297
+ };
3298
+ if (path === "/__status") {
3299
+ return jsonResponse2(createTileflowArtifactStatus(state));
3300
+ }
3301
+ if (path === "/" || path === "") {
3302
+ const { renderTileflowPreviewHtml, resolveTileflowPreview } = await import("./preview.js");
3303
+ const requestedMaps = url.searchParams.getAll("map");
3304
+ const requestedThemes = url.searchParams.getAll("theme");
3305
+ const requestedScenes = url.searchParams.getAll("scene");
3306
+ if (requestedScenes.length > 1) {
3307
+ return jsonResponse2({ error: "Tileflow scene query must appear at most once." }, 400);
3308
+ }
3309
+ if (requestedMaps.length > 1) {
3310
+ return jsonResponse2({ error: "Tileflow map query must appear at most once." }, 400);
3311
+ }
3312
+ if (requestedThemes.length > 1) {
3313
+ return jsonResponse2({ error: "Tileflow theme query must appear at most once." }, 400);
3314
+ }
3315
+ const requestedMap = requestedMaps[0] ?? (requestedScenes.length ? void 0 : options.map);
3316
+ const requestedTheme = requestedThemes[0] ?? (requestedScenes.length ? void 0 : options.theme);
3317
+ const preview = artifacts ? resolveTileflowPreview(artifacts.project, {
3318
+ map: requestedMap,
3319
+ scene: requestedScenes[0] ?? (requestedMaps.length ? void 0 : options.scene),
3320
+ theme: requestedTheme
3321
+ }) : void 0;
3322
+ const previewStyle = preview ? controlled(preview.mapName, preview.themeName)?.style ?? artifacts?.styles[preview.mapName]?.[preview.themeName] : void 0;
3323
+ const isSemanticPreview = preview !== void 0 && previewStyle?.metadata?.["tileflow:compiler"] === "tileflow-semantic";
3324
+ const fontFaces = previewStyle ? getTileflowStyleFontFaces(previewStyle) : [];
3325
+ return htmlResponse(
3326
+ renderTileflowPreviewHtml(
3327
+ preview,
3328
+ basePath,
3329
+ createTileflowArtifactStatus(state),
3330
+ isSemanticPreview,
3331
+ fontFaces,
3332
+ options.fitViewport,
3333
+ controls ?? void 0
3334
+ )
3335
+ );
3336
+ }
3337
+ if (!artifacts) return unavailableArtifactsResponse(state);
3338
+ if (path === "/__controls") {
3339
+ return compactJsonResponse({
3340
+ ...describePreviewControls(
3341
+ artifacts,
3342
+ url.searchParams.get("map") ?? "",
3343
+ url.searchParams.get("theme") ?? ""
3344
+ ),
3345
+ generation: acquisition?.generation
3346
+ });
3347
+ }
3348
+ if (path.startsWith(`/${tileflowLineBackgroundGlyphRoute}/`)) {
3349
+ const glyphRequest = parseTileflowLineBackgroundGlyphPath(path);
3350
+ const upstream = glyphRequest ? artifacts.lineBackgroundGlyphs?.[glyphRequest.mapName] : void 0;
3351
+ if (!glyphRequest || !upstream) return jsonResponse2({ error: "Unknown glyph range." }, 404);
3352
+ return await lineBackgroundGlyphs(upstream, glyphRequest.fontStack, glyphRequest.range);
3353
+ }
3354
+ if (path.startsWith("/tilesets/")) {
3355
+ const fileName = path.replace(/^\/+/, "");
3356
+ const localTileset = artifacts.localTilesets?.find(
3357
+ (candidate) => candidate.fileName === fileName
3358
+ );
3359
+ if (!localTileset) return jsonResponse2({ error: "Unknown local tileset." }, 404);
3360
+ return localTilesetResponse(request, localTileset);
3361
+ }
3362
+ if (path.startsWith("/generations/") && "files" in artifacts) {
3363
+ const fileName = path.replace(/^\/+/, "");
3364
+ const asset = artifacts.files?.find((candidate) => candidate.fileName === fileName);
3365
+ if (!asset) return jsonResponse2({ error: `Unknown generation asset: ${fileName}` }, 404);
3366
+ return assetResponse(asset);
3367
+ }
3368
+ if (path === "/manifest.json") {
3369
+ return jsonResponse2(artifacts.manifest);
3370
+ }
3371
+ if (path === "/build-manifest.json") {
3372
+ return jsonResponse2(artifacts.buildManifest);
3373
+ }
3374
+ if (path.startsWith("/__inspection/")) {
3375
+ const selection = getTileflowStyleInspectionSelection(path);
3376
+ const inspection = selection ? controlled(selection.mapName, selection.themeName)?.inspection ?? artifacts.styleInspections?.[selection.mapName]?.[selection.themeName] : void 0;
3377
+ if (!selection || !inspection) {
3378
+ return jsonResponse2({ error: "Compiler inspection is unavailable." }, 404);
3379
+ }
3380
+ return compactJsonResponse(inspection);
3381
+ }
3382
+ if (path.startsWith("/styles/")) {
3383
+ const selection = getTileflowStyleSelection(path);
3384
+ const style = selection ? controlled(selection.mapName, selection.themeName)?.style ?? artifacts.styles[selection.mapName]?.[selection.themeName] : void 0;
3385
+ if (!selection || !style) {
3386
+ return jsonResponse2({ error: "Unknown map theme style." }, 404);
3387
+ }
3388
+ return compactJsonResponse(style);
3389
+ }
3390
+ if (path.startsWith("/icons/")) {
3391
+ const assetName = path.replace(/^\/+/, "");
3392
+ const asset = artifacts.assets.find((candidate) => candidate.fileName === assetName);
3393
+ if (!asset) {
3394
+ return jsonResponse2({ error: `Unknown icon asset: ${assetName}` }, 404);
3395
+ }
3396
+ return assetResponse(asset);
3397
+ }
3398
+ if (path.startsWith("/fonts/")) {
3399
+ const assetName = path.replace(/^\/+/, "");
3400
+ const asset = artifacts.assets.find((candidate) => candidate.fileName === assetName);
3401
+ if (!asset) {
3402
+ return jsonResponse2({ error: `Unknown font asset: ${assetName}` }, 404);
3403
+ }
3404
+ return assetResponse(asset);
3405
+ }
3406
+ if (path.startsWith("/models/")) {
3407
+ const assetName = path.replace(/^\/+/, "");
3408
+ const asset = artifacts.assets.find((candidate) => candidate.fileName === assetName);
3409
+ if (!asset) {
3410
+ return jsonResponse2({ error: `Unknown model asset: ${assetName}` }, 404);
3411
+ }
3412
+ return assetResponse(asset);
3413
+ }
3414
+ return jsonResponse2({ error: "Not found" }, 404);
3415
+ } catch (error) {
3416
+ options.onError?.(error);
3417
+ if (url.searchParams.has("controls") || path === "/__controls") {
3418
+ return jsonResponse2(
3419
+ { error: error instanceof Error ? error.message : "Invalid map controls." },
3420
+ 400
3421
+ );
3422
+ }
3423
+ return tileflowErrorResponse(error);
3424
+ } finally {
3425
+ await acquisition?.release();
3426
+ }
3427
+ };
3428
+ return Object.assign(handleTileflowDevRequest, {
3429
+ async close() {
3430
+ if (closed) return;
3431
+ closed = true;
3432
+ const ownedSession = await ownedSessionPromise?.catch(() => void 0);
3433
+ await ownedSession?.close();
3434
+ },
3435
+ async refresh() {
3436
+ if (closed) return;
3437
+ if (options.session) {
3438
+ await options.session.refresh("request-handler");
3439
+ return;
3440
+ }
3441
+ if (!lastOrigin) return;
3442
+ await (await loadOwnedSession(lastOrigin)).refresh("request-handler");
3443
+ }
3444
+ });
3445
+ }
3446
+ async function localTilesetResponse(request, file) {
3447
+ const headers = new Headers({
3448
+ "Access-Control-Allow-Origin": "*",
3449
+ "Access-Control-Expose-Headers": "ETag",
3450
+ "Accept-Ranges": "bytes",
3451
+ "Cache-Control": "no-store",
3452
+ "Content-Type": "application/vnd.pmtiles",
3453
+ ETag: file.etag
3454
+ });
3455
+ if (request.method === "HEAD") {
3456
+ headers.set("Content-Length", String(file.byteLength));
3457
+ return new Response(null, { headers, status: 200 });
3458
+ }
3459
+ const range = parseLocalTilesetRange(request.headers.get("range"), file.byteLength);
3460
+ if (!range) {
3461
+ headers.set("Content-Range", `bytes */${file.byteLength}`);
3462
+ return new Response(null, { headers, status: 416 });
3463
+ }
3464
+ const length = range.end - range.start + 1;
3465
+ if (length > 16 * 1024 * 1024) {
3466
+ headers.set("Content-Range", `bytes */${file.byteLength}`);
3467
+ return new Response(null, { headers, status: 416 });
3468
+ }
3469
+ const handle = await open(file.sourcePath, "r");
3470
+ try {
3471
+ const bytes = new Uint8Array(length);
3472
+ let read = 0;
3473
+ while (read < length) {
3474
+ const result = await handle.read(bytes, read, length - read, range.start + read);
3475
+ if (result.bytesRead === 0) throw new Error("Local PMTiles snapshot ended during read.");
3476
+ read += result.bytesRead;
3477
+ }
3478
+ headers.set("Content-Length", String(length));
3479
+ headers.set("Content-Range", `bytes ${range.start}-${range.end}/${file.byteLength}`);
3480
+ return new Response(bytes, { headers, status: 206 });
3481
+ } finally {
3482
+ await handle.close();
3483
+ }
3484
+ }
3485
+ function parseLocalTilesetRange(value, size) {
3486
+ const match = value?.match(/^bytes=(\d+)-(\d*)$/u);
3487
+ if (!match) return null;
3488
+ const start = Number(match[1]);
3489
+ const requestedEnd = match[2] ? Number(match[2]) : size - 1;
3490
+ const end = Math.min(requestedEnd, size - 1);
3491
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(requestedEnd) || start < 0 || start >= size || requestedEnd < start) {
3492
+ return null;
3493
+ }
3494
+ return { end, start };
3495
+ }
3496
+ function createTileflowArtifactStatus(state) {
3497
+ return {
3498
+ schemaVersion: tileflowArtifactSessionSchemaVersion,
3499
+ generation: state.generation,
3500
+ status: state.status,
3501
+ ..."lastGoodGeneration" in state && state.lastGoodGeneration !== void 0 ? { lastGoodGeneration: state.lastGoodGeneration } : {},
3502
+ ...state.status === "invalid" ? { diagnostics: state.diagnostics } : {}
3503
+ };
3504
+ }
3505
+ function unavailableArtifactsResponse(state) {
3506
+ return jsonResponse2(
3507
+ {
3508
+ error: "No valid Tileflow artifact generation is available.",
3509
+ ...createTileflowArtifactStatus(state)
3510
+ },
3511
+ 409
3512
+ );
3513
+ }
3514
+ function eventStreamResponse(session) {
3515
+ const encoder = new TextEncoder();
3516
+ let unsubscribe = () => {
3517
+ };
3518
+ const stream = new ReadableStream({
3519
+ start(controller) {
3520
+ const enqueue = (state) => {
3521
+ const status = createTileflowArtifactStatus(state);
3522
+ controller.enqueue(
3523
+ encoder.encode(`event: ${status.status}
3524
+ data: ${JSON.stringify(status)}
3525
+
3526
+ `)
3527
+ );
3528
+ };
3529
+ enqueue(session.getState());
3530
+ unsubscribe = session.subscribe(enqueue);
3531
+ },
3532
+ cancel() {
3533
+ unsubscribe();
3534
+ }
3535
+ });
3536
+ return new Response(stream, {
3537
+ headers: {
3538
+ "Access-Control-Allow-Origin": "*",
3539
+ "Cache-Control": "no-cache, no-transform",
3540
+ Connection: "keep-alive",
3541
+ "Content-Type": "text/event-stream; charset=utf-8",
3542
+ "X-Accel-Buffering": "no"
3543
+ }
3544
+ });
3545
+ }
3546
+ function getNodeHeader(request, name) {
3547
+ const value = request.headers[name];
3548
+ return Array.isArray(value) ? value[0] : value;
3549
+ }
3550
+ function stripTileflowBasePath(pathname, basePath) {
3551
+ if (!basePath) {
3552
+ return pathname;
3553
+ }
3554
+ if (pathname === basePath) {
3555
+ return "/";
3556
+ }
3557
+ if (pathname.startsWith(`${basePath}/`)) {
3558
+ return pathname.slice(basePath.length);
3559
+ }
3560
+ return null;
3561
+ }
3562
+ function getRequestPathname(url) {
3563
+ try {
3564
+ return new URL(url ?? "/", "http://tileflow.local").pathname;
3565
+ } catch {
3566
+ return null;
3567
+ }
3568
+ }
3569
+ function isOwnedTileflowRequestPath(path) {
3570
+ return path === "/" || path === "/build-manifest.json" || path === "/manifest.json" || path === "/__events" || path === "/__status" || path === "/__controls" || getTileflowStyleInspectionSelection(path) !== void 0 || path.startsWith("/generations/") || path.startsWith("/icons/") || path.startsWith("/tilesets/") || getTileflowStyleSelection(path) !== void 0 || path.startsWith("/fonts/") || path.startsWith("/models/") || path.startsWith(`/${tileflowLineBackgroundGlyphRoute}/`) || path.startsWith("/__runtime/");
3571
+ }
3572
+ function tileflowErrorResponse(error) {
3573
+ if (error instanceof Error && "code" in error && error.code === "PREVIEW_SELECTION_INVALID") {
3574
+ return jsonResponse2({ error: error.message }, 400);
3575
+ }
3576
+ if (error instanceof TileflowStyleValidationError) {
3577
+ return jsonResponse2(
3578
+ {
3579
+ error: error.message,
3580
+ issues: error.issues
3581
+ },
3582
+ 400
3583
+ );
3584
+ }
3585
+ if (error instanceof TileflowValidationError) {
3586
+ return jsonResponse2(
3587
+ {
3588
+ error: error.message,
3589
+ messages: error.messages
3590
+ },
3591
+ 400
3592
+ );
3593
+ }
3594
+ const message = error instanceof Error ? error.message : "Unknown error";
3595
+ return jsonResponse2({ error: message }, 500);
3596
+ }
3597
+ function jsonResponse2(body, status = 200) {
3598
+ return new Response(`${JSON.stringify(body, null, 2)}
3599
+ `, {
3600
+ headers: {
3601
+ "Access-Control-Allow-Origin": "*",
3602
+ "Cache-Control": "no-store",
3603
+ "Content-Type": "application/json; charset=utf-8"
3604
+ },
3605
+ status
3606
+ });
3607
+ }
3608
+ function compactJsonResponse(body, status = 200) {
3609
+ return new Response(`${JSON.stringify(body)}
3610
+ `, {
3611
+ headers: {
3612
+ "Access-Control-Allow-Origin": "*",
3613
+ "Cache-Control": "no-store",
3614
+ "Content-Type": "application/json; charset=utf-8"
3615
+ },
3616
+ status
3617
+ });
3618
+ }
3619
+ function htmlResponse(body, status = 200) {
3620
+ return new Response(body, {
3621
+ headers: {
3622
+ "Access-Control-Allow-Origin": "*",
3623
+ "Cache-Control": "no-store",
3624
+ "Content-Type": "text/html; charset=utf-8"
3625
+ },
3626
+ status
3627
+ });
3628
+ }
3629
+ function assetResponse(asset) {
3630
+ const body = typeof asset.source === "string" ? asset.source : Buffer.from(asset.source);
3631
+ return new Response(body, {
3632
+ headers: {
3633
+ "Access-Control-Allow-Origin": "*",
3634
+ "Cache-Control": "no-store",
3635
+ "Content-Type": asset.contentType
3636
+ }
3637
+ });
3638
+ }
3639
+ export {
3640
+ createTileflowArtifactDiagnostics,
3641
+ createTileflowArtifactStatus,
3642
+ createTileflowComparisonRequestHandler,
3643
+ createTileflowDevRequestHandler,
3644
+ createTileflowNodeRequest,
3645
+ getTileflowAssetBasePath,
3646
+ getTileflowAssetFileName,
3647
+ getTileflowRequestPath,
3648
+ getTileflowStyleInspectionSelection,
3649
+ getTileflowStyleSelection,
3650
+ isTileflowRequestUrl,
3651
+ joinTileflowPublicUrl,
3652
+ normalizeTileflowBasePath,
3653
+ resolveTileflowArtifactPublicUrls,
3654
+ tileflowArtifactSessionSchemaVersion,
3655
+ tileflowComparisonSchemaVersion,
3656
+ writeTileflowNodeResponse
3657
+ };