@tileflow/dev 0.1.0-alpha.31 → 0.1.0-alpha.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  parseTileflowLineBackgroundGlyphPath,
6
6
  tileflowArtifactSessionSchemaVersion,
7
7
  tileflowLineBackgroundGlyphRoute
8
- } from "./chunk-2QXKVQ6O.js";
8
+ } from "./chunk-WJOF6USY.js";
9
9
  import {
10
10
  getTileflowAssetBasePath,
11
11
  getTileflowAssetFileName,
@@ -13,7 +13,7 @@ import {
13
13
  normalizeTileflowBasePath,
14
14
  resolveTileflowArtifactPublicUrls
15
15
  } from "./chunk-UTRO4IBQ.js";
16
- import "./chunk-IR22ZTQW.js";
16
+ import "./chunk-QYU4OW5A.js";
17
17
  import "./chunk-7C4F6GAX.js";
18
18
  import {
19
19
  TileflowStyleValidationError
@@ -34,12 +34,1492 @@ import {
34
34
  tileflowThemeNameSchema
35
35
  } from "@tileflow/core";
36
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
+
37
232
  // src/comparison.ts
38
233
  import { randomBytes } from "crypto";
234
+ import { readFile } from "fs/promises";
235
+
236
+ // src/devtools/card-styles.ts
237
+ var cardStyles = `
238
+ .devtools-ready .dt-shell {
239
+ --dt-page:color-mix(in srgb,var(--surface) 80%,var(--border));
240
+ --dt-card-heading:color-mix(in srgb,var(--paper) 55%,var(--surface));
241
+ --dt-content-gap:12px;
242
+ }
243
+ .devtools-ready .dt-shell :is(.dt-card-page,.dt-layer-content,.dt-tile-content,.dt-diagnostics-list) {
244
+ padding:14px; background:var(--dt-page);
245
+ }
246
+ .devtools-ready .dt-shell :is(.dt-card,.dt-layer-evidence,.dt-layer-neighbors,.dt-layer-section,.dt-layer-origin,.dt-layer-samples,.dt-tile-content > .dt-kv,.dt-tile-feature,.dt-diagnostic,.dt-diagnostics-empty) {
247
+ min-width:0; margin:0 0 var(--dt-content-gap); border:1px solid var(--border); border-radius:10px; background:var(--paper);
248
+ }
249
+ .devtools-ready .dt-shell .dt-card { padding:0; overflow:hidden; }
250
+ .devtools-ready .dt-shell :is(.dt-card-page,.dt-layer-content,.dt-tile-content,.dt-diagnostics-list) > :last-child { margin-bottom:0; }
251
+ .devtools-ready .dt-shell :is(.dt-card > .dt-section-heading,.dt-card > .controls-section-title,.dt-layer-neighbors > strong) {
252
+ display:block; margin:0; padding:10px 14px; border:0; border-bottom:1px solid var(--border); background:var(--dt-card-heading);
253
+ color:var(--ink); font:600 12px/1.4 var(--ui); letter-spacing:0; text-transform:none;
254
+ }
255
+ .devtools-ready .dt-shell .dt-card-table { overflow:auto; }
256
+ .devtools-ready .dt-shell .dt-card-table :is(.dt-table,.dt-layer-rules) { margin:0; }
257
+ .devtools-ready .dt-shell .dt-card-table th { background:var(--dt-card-heading); padding:9px 14px; text-transform:none; letter-spacing:0; }
258
+ .devtools-ready .dt-shell .dt-card-table td { padding:9px 14px; height:36px; border-bottom-color:var(--border); }
259
+ .devtools-ready .dt-shell :is(.dt-card-table,.dt-layer-samples) tr:last-child td { border-bottom:0; }
260
+ .devtools-ready .dt-shell .dt-subtabs { padding-inline:14px; background:var(--paper); }
261
+
262
+ /* Map: each existing section is one card, with the enabled module fields kept inline. */
263
+ .devtools-ready .dt-shell .dt-scene-content { max-width:none; background:var(--dt-page); }
264
+ .devtools-ready .dt-shell .map-controls { padding:14px!important; background:var(--dt-page); }
265
+ .devtools-ready .dt-shell .controls-grid { display:block; border:0; padding:0; }
266
+ .devtools-ready .dt-shell :is(.controls-selection,.controls-runtime) { border:1px solid var(--border); padding:0; }
267
+ .devtools-ready .dt-shell .controls-selection[hidden] { display:none; }
268
+ .devtools-ready .dt-shell .controls-section-title { margin:0; }
269
+ .devtools-ready .dt-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
+ .devtools-ready .dt-shell .map-controls .control-field.dt-palette-field { grid-template-columns:minmax(120px,180px) minmax(0,1fr); }
273
+ .devtools-ready .dt-shell :is(.controls-selection,.controls-runtime) > .control-field:last-child { border-bottom:0; }
274
+ .devtools-ready .dt-shell .control-module { background:transparent; }
275
+ .devtools-ready .dt-shell .control-module:last-child { border-bottom:0; }
276
+ .devtools-ready .dt-shell .control-module-header { min-height:44px; padding:10px 18px; }
277
+ .devtools-ready .dt-shell .control-module-header label { gap:10px; }
278
+ .devtools-ready .dt-shell .control-module-header input { margin-right:0; }
279
+ .devtools-ready .dt-shell .control-module fieldset { padding:0; border-top:1px solid var(--border); background:color-mix(in srgb,var(--paper) 65%,var(--surface)); }
280
+ .devtools-ready .dt-shell .control-module .control-field { padding-left:54px; border-bottom:1px solid var(--border); }
281
+ .devtools-ready .dt-shell .control-module .control-field:last-child { border-bottom:0; }
282
+ .devtools-ready .dt-shell .map-controls :is(select,input[type=number]) { border-radius:5px; }
283
+ .devtools-ready .dt-shell :is(.controls-segments,.dt-palettes button) { border-radius:5px; }
284
+ .devtools-ready .dt-shell .dt-palettes { gap:6px; flex-wrap:wrap; }
285
+ .devtools-ready .dt-shell .dt-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
+ .devtools-ready .dt-shell .dt-layer-content { container-type:inline-size; }
289
+ .devtools-ready .dt-shell :is(.dt-layer-evidence,.dt-layer-neighbors) { padding:0; overflow:hidden; }
290
+ .devtools-ready .dt-shell .dt-layer-evidence-row { padding:10px 14px; grid-template-columns:16px 110px minmax(0,1fr); }
291
+ .devtools-ready .dt-shell .dt-layer-evidence-row:last-child { border-bottom:0; }
292
+ .devtools-ready .dt-shell .dt-layer-neighbors > div { margin:0; padding:9px 14px; border-bottom:1px solid var(--border); }
293
+ .devtools-ready .dt-shell .dt-layer-neighbors > div:last-child { border-bottom:0; }
294
+ .devtools-ready .dt-shell :is(.dt-layer-section,.dt-layer-origin) { padding:12px 14px; }
295
+ .devtools-ready .dt-shell .dt-layer-section h4 { color:var(--ink); font:600 12px/1.4 var(--ui); text-transform:none; letter-spacing:0; }
296
+ .devtools-ready .dt-shell .dt-layer-content > .dt-tools { margin-bottom:12px; padding:0 0 8px; border:0; background:transparent; }
297
+ .devtools-ready .dt-shell .dt-layer-content > .dt-note { padding:0 2px 12px; }
298
+ .devtools-ready .dt-shell .dt-layer-samples { max-height:240px; }
299
+ .devtools-ready .dt-shell .dt-layer-samples td { padding:8px 12px; }
300
+ .devtools-ready .dt-shell .dt-layer-group-title { height:30px; min-height:30px; background:var(--dt-card-heading)!important; }
301
+ .devtools-ready .dt-shell .dt-layer-group-title strong { text-transform:capitalize; letter-spacing:0; }
302
+ .devtools-ready .dt-shell .dt-layer-row { grid-template-columns:18px 26px minmax(0,1fr) 52px; height:36px; min-height:36px; gap:6px; }
303
+ .devtools-ready .dt-shell .dt-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
+ .devtools-ready .dt-shell .dt-layer-evidence-row { grid-template-columns:16px minmax(0,1fr); gap:4px 8px; }
306
+ .devtools-ready .dt-shell .dt-layer-evidence-row > :last-child { grid-column:2; }
307
+ }
308
+
309
+ /* Tiles: keep whole tables together and allow long layer names to wrap. */
310
+ .devtools-ready .dt-shell .dt-tile-content > .dt-note { padding:0 2px 10px; }
311
+ .devtools-ready .dt-shell .dt-tile-content .dt-table td { vertical-align:top; white-space:normal; overflow-wrap:anywhere; }
312
+ .devtools-ready .dt-shell .dt-tile-content .dt-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
+ .devtools-ready .dt-shell .dt-tile-content > .dt-kv { padding:0; gap:0; overflow:hidden; grid-template-columns:minmax(100px,35%) minmax(0,1fr); }
314
+ .devtools-ready .dt-shell .dt-tile-content > .dt-kv > * { padding:10px 14px; border-bottom:1px solid var(--border); }
315
+ .devtools-ready .dt-shell .dt-tile-content > .dt-kv > :nth-last-child(-n+2) { border-bottom:0; }
316
+ .devtools-ready .dt-shell .dt-tile-feature { padding:10px 14px; }
317
+ .devtools-ready .dt-shell .dt-tile-content > :is(.dt-tile-chip-list,.dt-tile-paging) { padding:0 0 10px; margin-bottom:10px; background:transparent; }
318
+
319
+ /* Debug: overlay controls and resolution controls retain their existing actions. */
320
+ .devtools-ready .dt-shell .dt-card > .dt-debug-overlay { grid-template-columns:76px minmax(0,1fr) auto; padding:10px 18px; gap:14px; }
321
+ .devtools-ready .dt-shell .dt-debug-overlay:last-child { border-bottom:0; }
322
+ .devtools-ready .dt-shell .dt-debug-preview { width:76px; height:50px; border-radius:6px; }
323
+ .devtools-ready .dt-shell .dt-debug-overlay strong { font-weight:500; }
324
+ .devtools-ready .dt-shell .dt-debug-overlay small { margin-top:3px; font-size:11px; }
325
+ .devtools-ready .dt-shell .dt-overdraw-help { max-width:none; padding:10px 18px 12px 108px; }
326
+ .devtools-ready .dt-shell .dt-debug-scale { padding:12px 18px; gap:12px; }
327
+ .devtools-ready .dt-shell .dt-debug-scale > span:first-child { flex:0 0 180px; }
328
+ .devtools-ready .dt-shell .dt-debug-scale > input[type=range] { width:108px; min-width:60px; }
329
+ .devtools-ready .dt-shell .dt-debug-scale > .dt-debug-specimen { flex:1; min-width:160px; }
330
+ .devtools-ready .dt-shell .dt-card > .dt-note { padding:0 18px 12px; }
331
+ .devtools-ready .dt-shell .dt-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
+ .devtools-ready .dt-shell .dt-assets-content { background:var(--dt-page); }
335
+ .devtools-ready .dt-shell .dt-asset-main { padding:14px; }
336
+ .devtools-ready .dt-shell :is(.dt-sprite-grid,.dt-model-grid,.dt-font-list) { gap:10px; background:transparent; }
337
+ .devtools-ready .dt-shell :is(.dt-sprite-card,.dt-model-card,.dt-font-card) { border:1px solid var(--border); border-radius:8px; background:var(--paper); overflow:hidden; }
338
+ .devtools-ready .dt-shell .dt-sprite-card { padding:10px 8px; }
339
+ .devtools-ready .dt-shell .dt-font-list { padding:14px; max-width:none; }
340
+ .devtools-ready .dt-shell .dt-font-card { padding:12px; }
341
+ .devtools-ready .dt-shell .dt-asset-aside { margin:14px 14px 14px 0; border:1px solid var(--border); border-radius:10px; background:var(--paper); }
342
+ .devtools-ready .dt-shell .dt-asset-aside h3 { margin:0; padding:12px 14px; background:var(--dt-card-heading); border-bottom:1px solid var(--border); }
343
+ .devtools-ready .dt-shell .dt-diagnostic { overflow:hidden; }
344
+ .devtools-ready .dt-shell .dt-diagnostic > summary { padding:12px 14px; }
345
+ .devtools-ready .dt-shell .dt-diagnostic[open] > summary { background:var(--dt-card-heading); border-bottom:1px solid var(--border); }
346
+ .devtools-ready .dt-shell .dt-diagnostic-detail { padding:12px 14px 14px 39px; }
347
+ .devtools-ready .dt-shell .dt-diagnostics-empty { padding:20px; max-width:none; }
348
+
349
+ @container(max-width:760px) {
350
+ .devtools-ready .dt-shell .dt-asset-aside { margin:0 14px 14px; }
351
+ }
352
+ @container(max-width:480px) {
353
+ .devtools-ready .dt-shell :is(.dt-card-page,.dt-layer-content,.dt-tile-content,.dt-diagnostics-list,.dt-asset-main,.dt-font-list) { padding:10px; }
354
+ .devtools-ready .dt-shell .map-controls { padding:10px!important; }
355
+ .devtools-ready .dt-shell .map-controls :is(.control-field,.control-field.dt-palette-field) { grid-template-columns:110px minmax(0,1fr); gap:10px; padding-inline:12px; }
356
+ .devtools-ready .dt-shell .control-module .control-field { padding-left:24px; }
357
+ .devtools-ready .dt-shell .dt-debug-scale > span:first-child { flex-basis:100%; }
358
+ .devtools-ready .dt-shell .dt-card > .dt-debug-overlay { grid-template-columns:60px minmax(0,1fr) auto; padding:10px 12px; gap:10px; }
359
+ .devtools-ready .dt-shell .dt-debug-preview { width:60px; height:40px; }
360
+ .devtools-ready .dt-shell .dt-overdraw-help { padding-left:12px; }
361
+ }
362
+ `;
363
+
364
+ // src/devtools/inspector-styles.ts
365
+ var inspectorStyles = `
366
+ body.devtools-ready {
367
+ --paper:#F8F6F2; --surface:#F0EDE7; --ink:#30251F; --muted:#635449; --faint:#A0907F;
368
+ --border:#E1DCD3; --border-strong:#C7BFB3; --accent:#38668B; --dt-selected:#E6ECF2;
369
+ --hover:#E9E5DE; --accent-soft:#DFE8F0; --ok:#2F704B; --ok-soft:#E3EBDB; --warn:#815D21; --warn-soft:#F3E6CF;
370
+ --danger:#B13C34; --dt-string:var(--ink); --dt-number:var(--accent); --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
371
+ --ui:-apple-system,BlinkMacSystemFont,'Segoe UI',Arial,sans-serif;
372
+ --dt-control-height:22px; --dt-row-height:22px; --dt-nav-height:27px;
373
+ --dt-header-height:44px; --dt-camera-control-height:30px;
374
+ color-scheme:light; font:12px/1.35 var(--ui); -webkit-font-smoothing:antialiased;
375
+ }
376
+ body.devtools-ready[data-inspector-theme=dark] {
377
+ --paper:#1D1816; --surface:#171311; --ink:#F5EDD9; --muted:#C3B7A9; --faint:#857869; --border:#40352F; --border-strong:#5A4B43;
378
+ --accent:#83ADD1; --dt-selected:#22303B; --hover:#2E2622; --accent-soft:#223039; --ok:#91B887; --ok-soft:#24301F; --warn:#E2B36F; --warn-soft:#3A2F20; --danger:#F1746B; color-scheme:dark;
379
+ }
380
+ @media(prefers-color-scheme:dark) { body.devtools-ready[data-inspector-theme=system] {
381
+ --paper:#1D1816; --surface:#171311; --ink:#F5EDD9; --muted:#C3B7A9; --faint:#857869; --border:#40352F; --border-strong:#5A4B43;
382
+ --accent:#83ADD1; --dt-selected:#22303B; --hover:#2E2622; --accent-soft:#223039; --ok:#91B887; --ok-soft:#24301F; --warn:#E2B36F; --warn-soft:#3A2F20; --danger:#F1746B; color-scheme:dark;
383
+ }}
384
+ .devtools-ready button,.devtools-ready input,.devtools-ready select { font:inherit; }
385
+ .devtools-ready button { border:1px solid var(--border); border-radius:6px; min-height:28px; padding:4px 10px; cursor:pointer; }
386
+ .devtools-ready button:disabled { cursor:default; opacity:.45; }
387
+ .devtools-ready button:hover:not(:disabled) { background:var(--hover); }
388
+ .devtools-ready input[type=range] { accent-color:var(--accent); }
389
+ .devtools-ready code,.devtools-ready pre { font-family:var(--mono); }
390
+ .devtools-ready .dt-head { height:40px; min-height:40px; flex-wrap:nowrap; gap:2px; padding:0 8px; background:var(--paper); }
391
+ .devtools-ready .dt-head-main { display:flex; align-items:center; flex:1; min-width:0; }
392
+ .devtools-ready .dt-tabs { flex:1; align-items:center; gap:2px; }
393
+ .devtools-ready .dt-tabs button { border:0; border-radius:6px; min-height:28px; padding:5px 10px; color:var(--muted); background:transparent; }
394
+ .devtools-ready .dt-tabs button[aria-selected=true] { color:var(--ink); background:var(--surface); }
395
+ .devtools-ready .dt-head > .dt-icon { width:28px; height:28px; margin:0; }
396
+ .devtools-ready .dt-icon[aria-pressed=true] { background:var(--accent-soft); color:var(--accent); }
397
+ .devtools-ready .dt-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
+ .devtools-ready .dt-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
+ .devtools-ready .dt-camera-field > span:first-child { padding:0 4px 0 9px; color:var(--faint); font-size:10px; text-transform:uppercase; letter-spacing:.04em; }
400
+ .devtools-ready .dt-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
+ .devtools-ready .dt-camera-field input::-webkit-inner-spin-button { appearance:none; }
402
+ .devtools-ready .dt-camera-field input[type=range] { width:110px; padding:0; margin:0 10px 0 2px; min-height:0; }
403
+ .devtools-ready .dt-unit { padding-right:9px; color:var(--faint); }
404
+ .devtools-ready .dt-copy-agent { margin-left:auto; background:var(--ink); color:var(--paper); border-color:var(--ink); font-weight:500; }
405
+ .devtools-ready .dt-copy-agent:hover { background:var(--ink); opacity:.88; }
406
+ .devtools-ready .dt-menu { top:38px; right:8px; width:236px; padding:10px; border-radius:10px; box-shadow:0 8px 28px #0002; }
407
+ .devtools-ready .dt-dock-options { display:grid; grid-template-columns:repeat(3,1fr); gap:6px; margin-bottom:8px; }
408
+ .devtools-ready .dt-dock-options button { width:auto; height:34px; border:1px solid var(--border); justify-content:center; }
409
+ .devtools-ready .dt-menu-label { font-size:11px; }
410
+ .devtools-ready .dt-segments,.devtools-ready .dt-layer-modes,.devtools-ready .controls-segments { display:inline-flex; padding:2px; background:var(--surface); border:1px solid var(--border); border-radius:6px; gap:2px; }
411
+ .devtools-ready .dt-segments button,.devtools-ready .dt-layer-modes button,.devtools-ready .controls-segments button { min-height:23px; border:0; border-radius:4px; padding:3px 9px; font-size:12px; background:transparent; color:var(--muted); }
412
+ .devtools-ready .dt-segments button[aria-pressed=true],.devtools-ready .dt-layer-modes button[aria-pressed=true],.devtools-ready .controls-segments button[aria-checked=true] { color:var(--ink); background:var(--paper); box-shadow:0 1px 2px #0001; }
413
+ .devtools-ready .dt-menu .dt-segments { display:flex; }.devtools-ready .dt-menu .dt-segments button { flex:1; }
414
+ .devtools-ready .dt-tools { padding:10px 12px; gap:8px; background:var(--paper); }
415
+ .devtools-ready .dt-tools input[type=search] { height:30px; padding:0 9px; background:var(--surface); border-radius:6px; width:auto; }
416
+ .devtools-ready .dt-subtabs { padding:0 20px; gap:18px; }
417
+ .devtools-ready .dt-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
+ .devtools-ready .dt-subtabs button[aria-selected=true] { color:var(--ink); border-bottom-color:var(--ink); }
419
+ .devtools-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
+ .devtools-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
+ .devtools-ready input[role=switch]:checked { background:var(--accent); }
422
+ .devtools-ready input[role=switch]:checked::before { transform:translateX(11px); }
423
+ .devtools-ready .dt-layer-split { display:grid; grid-template-columns:minmax(0,min(38%,360px)) minmax(0,1fr); }
424
+ .devtools-ready .dt-layer-sidebar { display:flex; flex-direction:column; min-width:0; min-height:0; overflow:hidden; border-right:1px solid var(--border); }
425
+ .devtools-ready .dt-layer-list { flex:1; border:0; }.devtools-ready .dt-layer-tools { flex-direction:column; align-items:stretch; }
426
+ .devtools-ready .dt-layer-tools-row { display:flex; align-items:center; gap:8px; }.devtools-ready .dt-layer-total { margin-left:auto; font:11px var(--mono); }
427
+ .devtools-ready .dt-layer-visible { font-size:12px; }.devtools-ready .dt-layer-group-title { height:32px; font-size:11px; padding:0 12px 0 8px!important; }
428
+ .devtools-ready .dt-layer-group-title strong { text-transform:uppercase; letter-spacing:.05em; }
429
+ .devtools-ready .dt-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
+ .devtools-ready .dt-layer-row[aria-pressed=true] { background:var(--dt-selected); border-left-color:var(--accent); color:var(--ink); }
431
+ .devtools-ready .dt-layer-glyph { color:var(--muted); font-size:15px; }.devtools-ready .dt-layer-label { font-size:12px; }
432
+ .devtools-ready .dt-layer-prefix { color:var(--faint); }.devtools-ready .dt-layer-name { font-weight:500; }
433
+ .devtools-ready .dt-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
+ .devtools-ready .dt-eye svg { width:14px; height:14px; }
435
+ .devtools-ready :is(.dt-layer-row,.dt-layer-group-title):hover .dt-eye,.devtools-ready .dt-eye:focus-visible,.devtools-ready .dt-eye[aria-pressed=true] { opacity:1; }
436
+ .dt-zrange { position:relative; display:block; width:64px; height:14px; }.dt-zrange::before { content:''; position:absolute; inset:6px 0; height:2px; background:var(--border); }
437
+ .dt-zrange i { position:absolute; top:5px; height:4px; border-radius:2px; background:var(--accent); opacity:.65; }.dt-zrange em { position:absolute; top:1px; height:12px; width:1px; background:var(--accent); }
438
+ .devtools-ready .dt-layer-heading { padding:14px 20px 6px; align-items:flex-start; border:0; }
439
+ .devtools-ready .dt-layer-title strong { font-size:16px; font-weight:600; }.devtools-ready .dt-layer-title > code { font-size:10px; color:var(--faint); }
440
+ .dt-layer-status { display:flex; align-items:center; gap:8px; font-size:12px; color:var(--muted); margin-top:6px; }.dt-layer-status[data-state=Enabled] { color:var(--ok); }.dt-layer-status[data-state="Outside zoom"] { color:var(--warn); }
441
+ .devtools-ready .dt-layer-rules { font-size:12px; }.devtools-ready .dt-layer-rules th { padding:8px 20px; font-size:11px; text-transform:uppercase; letter-spacing:.05em; }
442
+ .devtools-ready .dt-layer-rules th:first-child { width:42%; }.devtools-ready .dt-layer-rules td { padding:8px 20px; vertical-align:middle; min-height:44px; }
443
+ .devtools-ready .dt-layer-rules code { font:12px/1.5 var(--mono); }.devtools-ready .dt-layer-rule-name { color:var(--ink); }.devtools-ready .dt-layer-dependency { color:var(--accent); font:10px var(--mono); }
444
+ .devtools-ready .dt-layer-rule-editor { gap:9px; flex-wrap:wrap; }.devtools-ready .dt-layer-rule-editor input:not([type=color]):not([type=checkbox]):not([type=range]),.devtools-ready .dt-layer-rule-editor select { min-height:26px; border:1px solid var(--border); font:12px var(--mono); border-radius:6px; }
445
+ .devtools-ready .dt-layer-rule-editor input[type=color] { width:22px; height:22px; }
446
+ .devtools-ready .dt-layer-rule-editor input[type=range] { width:110px; min-width:70px; padding:0; flex:0 1 110px; }
447
+ .dt-rule-toggle { display:flex; align-items:center; gap:10px; }.dt-rule-toggle input { width:14px; height:14px; min-height:0; margin:0; accent-color:var(--accent); }
448
+ .dt-rule-choices { display:inline-flex; flex-wrap:wrap; border:1px solid var(--border); border-radius:6px; overflow:hidden; }.devtools-ready .dt-rule-choices button { min-height:26px; border:0; border-radius:0; font:11px var(--mono); padding:3px 8px; }
449
+ .dt-rule-spark { display:flex; align-items:center; gap:10px; color:var(--accent); font:11px var(--mono); margin-bottom:5px; }.dt-rule-spark svg { width:130px; height:26px; overflow:visible; }
450
+ .dt-filter-chips { display:flex; flex-wrap:wrap; gap:6px; margin-block:10px; }.dt-filter-chip { padding:4px 9px; background:var(--surface); border:1px solid var(--border); border-radius:13px; font:11px var(--mono); }
451
+ .devtools-ready .dt-layer-section,.devtools-ready .dt-layer-origin { padding:12px 20px; }.devtools-ready .dt-layer-section h4 { font-size:11px; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); }
452
+ .devtools-ready .dt-layer-evidence { padding:0; }.devtools-ready .dt-layer-evidence-row { padding:12px 20px; font-size:12px; grid-template-columns:20px 130px 1fr; }
453
+ .devtools-ready .dt-layer-evidence-row[data-state=pass] .dt-layer-indicator { color:var(--ok); }.devtools-ready .dt-layer-evidence-row[data-state=warn] .dt-layer-indicator { color:var(--warn); }
454
+ .devtools-ready .dt-layer-json { font:11px/1.6 var(--mono); color:var(--ink); }
455
+ .devtools-ready .dt-table { font:12px var(--mono); }.devtools-ready .dt-table th { padding:8px 12px; font:11px Inter,system-ui,sans-serif; text-transform:uppercase; border-right:0; }.devtools-ready .dt-table td { height:32px; padding:6px 12px; }.devtools-ready .dt-table tbody tr:nth-child(even) { background:transparent; }
456
+ .devtools-ready .dt-network-body { display:grid; grid-template-columns:minmax(0,min(38%,360px)) minmax(0,1fr); }.dt-tiles-sidebar { display:flex; flex-direction:column; min-width:0; min-height:0; overflow:hidden; border-right:1px solid var(--border); }.devtools-ready .dt-network-detail { border:0; min-width:0; }
457
+ .devtools-ready .dt-tile-heading { padding:14px 20px 8px; }.dt-tile-heading strong { font:15px var(--mono); }.dt-tile-status { padding:0 20px 10px; color:var(--muted); font-size:12px; }
458
+ .dt-tile-grid { display:grid; gap:3px; padding:12px; }.devtools-ready .dt-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
+ .devtools-ready .dt-tile-cell[aria-pressed=true] { border-color:var(--accent); box-shadow:0 0 0 2px var(--accent-soft); }.dt-tile-grid-title { margin:10px 12px 0; color:var(--muted); font-size:11px; }
460
+ .dt-tile-chip-list { display:flex; gap:6px; flex-wrap:wrap; padding:10px 20px; border-bottom:1px solid var(--border); }.devtools-ready .dt-tile-chip { font:11px var(--mono); border-radius:13px; }
461
+ .dt-asset-layout { display:grid; grid-template-columns:minmax(0,1fr) 300px; min-height:0; flex:1; }.dt-asset-main { min-width:0; overflow:auto; padding:12px; }.dt-asset-aside { overflow:auto; border-left:1px solid var(--border); }.dt-asset-aside h3 { font:500 15px var(--mono); margin:16px 20px; }.dt-asset-variants { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:1px; background:var(--border); }.dt-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; }.dt-asset-variant[data-night=true] { background:#2e2622; color:#f5edd9; }
462
+ .devtools-ready .dt-sprite-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(104px,1fr)); gap:4px; }.devtools-ready .dt-sprite-card { border:1px solid transparent; border-radius:8px; background:transparent; padding:16px 6px 10px; cursor:pointer; }.dt-sprite-card[aria-pressed=true] { background:var(--dt-selected)!important; border-color:var(--accent)!important; }
463
+ .dt-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; }.dt-debug-overlay:hover { background:var(--hover); }.dt-debug-overlay small { display:block; color:var(--muted); font-size:12px; }.dt-debug-preview { width:84px; height:58px; border-radius:6px; overflow:hidden; }.dt-debug-preview svg { width:100%; height:100%; }
464
+ .dt-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
+ .devtools-ready .dt-overdraw-help { margin:0; border:0; border-bottom:1px solid var(--border); border-radius:0; padding:12px 20px 12px 120px; background:var(--surface); }.dt-debug-scale { display:flex; align-items:center; gap:12px; padding:16px 20px; flex-wrap:wrap; }.dt-debug-scale input[type=number] { width:64px; }
466
+ .devtools-ready .map-controls { padding:0!important; background:var(--paper); }.devtools-ready .controls-heading { display:none; }.devtools-ready .controls-grid { display:block; }.devtools-ready .controls-selection,.devtools-ready .controls-runtime { border:0; padding:0; }
467
+ .devtools-ready .controls-section-title { margin:0; padding:8px 20px; background:var(--surface); border-bottom:1px solid var(--border); font-size:11px; letter-spacing:.05em; }.devtools-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; }.devtools-ready .control-field select { max-width:100%; width:max-content; min-width:132px; }
468
+ .devtools-ready #controls-modules { display:block; }.devtools-ready .control-module { border:0; border-bottom:1px solid var(--border); border-radius:0; margin:0; background:var(--paper); }.devtools-ready .control-module-header { padding:10px 20px; }.devtools-ready .control-module-header label { justify-content:flex-start; font-size:13px; font-weight:500; }.devtools-ready .control-module-header input { order:-1; margin-right:12px; }.devtools-ready .control-module fieldset { padding:0; background:var(--surface); }.devtools-ready .control-module .control-field { padding-left:58px; border:0; }.devtools-ready .dt-scene-content { max-width:1000px; width:100%; }.dt-palettes { display:flex; gap:8px; flex-wrap:wrap; }.devtools-ready .dt-palette { display:flex; align-items:center; gap:8px; padding:5px 10px 5px 5px; }.dt-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
+ .devtools-ready .dt-layer-title { flex-basis:180px; }.devtools-ready .dt-camera-field input[type=range] { width:80px; }
471
+ .devtools-ready .dt-layer-rules th:first-child { width:45%; }.devtools-ready .dt-layer-rules td { padding:8px 12px; }
472
+ .dt-asset-layout { grid-template-columns:1fr; }.dt-asset-aside { border:0; border-top:1px solid var(--border); }
473
+ .devtools-ready .control-field { grid-template-columns:160px 1fr; }.devtools-ready .control-module .control-field { padding-left:30px; }
474
+ }
475
+ @media(prefers-reduced-motion:reduce) { .devtools-ready * { scroll-behavior:auto!important; transition:none!important; animation:none!important; } }
476
+
477
+ .dt-palettes{display:flex;gap:8px;flex-wrap:wrap}.devtools-ready .dt-palettes button{display:flex;flex-direction:column;gap:7px;padding:10px 14px;min-width:110px;background:var(--paper);text-align:left;text-transform:capitalize}.devtools-ready .dt-palettes button[aria-checked=true]{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}.dt-palette-swatches{display:flex;height:26px;width:100%;overflow:hidden;border-radius:4px}.dt-palette-swatches i{flex:1;min-width:20px}.devtools-ready .dt-palette-field{align-items:flex-start;flex-direction:column}.dt-model-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:10px}.devtools-ready .dt-model-card{display:flex;flex-direction:column;align-items:center;padding:0;overflow:hidden;border-radius:10px}.dt-model-card canvas{background:linear-gradient(var(--surface),var(--paper));max-width:100%;height:auto!important}.dt-model-card strong{font:500 12px var(--mono);padding:9px 12px 2px;overflow-wrap:anywhere}.dt-model-card small{font-size:11px;color:var(--muted);padding:0 12px 9px}.dt-model-card[aria-pressed=true]{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}.dt-asset-aside>canvas{display:block;margin:auto;max-width:100%;touch-action:none;cursor:grab}.dt-font-ranges{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.dt-font-ranges small{color:var(--muted);font-size:11px}
478
+
479
+ .dt-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)}.dt-rule-font-preview{overflow:hidden;max-width:100%}.dt-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
+ .devtools-ready .dt-shell>.dt-camera{order:0;flex:0 0 auto;margin:0;border-top:0;background:var(--paper);padding:8px 12px}
482
+ .devtools-ready .dt-shell>.dt-head{flex-wrap:nowrap}.devtools-ready .dt-shell .dt-head-main{display:flex;gap:2px}
483
+ .devtools-ready .dt-shell .dt-layer-tools input[type=search]{order:0;flex:0 0 auto;width:100%;height:30px}
484
+ .devtools-ready .dt-shell .dt-layer-tools{flex-wrap:nowrap}.devtools-ready .dt-shell .dt-layer-tools-row{flex:none;min-height:28px}
485
+ .devtools-ready .dt-shell .dt-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
+ .devtools-ready .dt-shell .dt-layer-label{display:block;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
487
+ .devtools-ready .dt-shell .dt-layer-heading{flex:none}.devtools-ready .dt-shell .dt-layer-title{flex:1 1 180px}
488
+ .devtools-ready .dt-shell .dt-layer-sidebar .dt-layer-list{flex:1;min-height:0}.devtools-ready .dt-shell .map-controls .control-field{display:grid;grid-template-columns:160px minmax(0,1fr);padding:12px 20px;margin:0}
489
+ .devtools-ready .dt-shell .control-module-header label{display:flex;gap:12px}.devtools-ready .dt-shell .map-controls input[role=switch]{width:26px!important;height:15px!important;min-height:0;border:0;background:var(--faint)}.devtools-ready .dt-shell .map-controls input[role=switch]:checked{background:var(--accent)}.devtools-ready .dt-shell .map-controls input[role=switch]::before{width:11px;height:11px;top:2px;left:2px;background:white}.devtools-ready .dt-shell .map-controls input[role=switch]:checked::before{transform:translateX(11px)}
490
+ @container(max-width:760px){.devtools-ready .dt-shell .dt-layer-tools{padding:8px 12px}.devtools-ready .dt-shell .dt-tabs button{padding-inline:7px}.devtools-ready .dt-shell .dt-head{padding-inline:4px}.devtools-ready .dt-shell .dt-camera-field{flex:0 0 auto}}
491
+
492
+ /* Shared density also wins over panel styles injected after the shell stylesheet. */
493
+ .devtools-ready .dt-shell { font:12px/1.35 var(--ui); }
494
+ .devtools-ready .dt-shell :is(button,input,select,summary) { min-height:var(--dt-control-height); }
495
+ .devtools-ready .dt-shell button { padding:2px 6px; border-radius:3px; }
496
+ .devtools-ready .dt-shell :is(input[type=checkbox],input[type=radio],input[type=range],input[type=color]) { min-height:0; }
497
+ .devtools-ready .dt-shell .dt-head { height:var(--dt-header-height); min-height:var(--dt-header-height); padding:0 4px; }
498
+ .devtools-ready .dt-shell .dt-tabs { gap:0; }
499
+ .devtools-ready .dt-shell .dt-tabs button { height:34px; min-height:34px; padding:0 8px; border-radius:4px; }
500
+ .devtools-ready .dt-shell .dt-head .dt-icon { width:30px; height:30px; min-height:30px; padding:5px; margin-block:0; }
501
+ .devtools-ready .dt-shell > .dt-camera { min-height:48px; padding:8px; gap:6px; }
502
+ .devtools-ready .dt-shell .dt-camera-field { height:var(--dt-camera-control-height); border-radius:5px; }
503
+ .devtools-ready .dt-shell .dt-camera-field > span:first-child { padding-left:6px; }
504
+ .devtools-ready .dt-shell .dt-camera-field input[type=number] { width:48px; min-height:0; padding:0 3px; font-size:12px; }
505
+ .devtools-ready .dt-shell .dt-camera-field input[type=range] { width:88px; margin:0 6px 0 2px; }
506
+ .devtools-ready .dt-shell .dt-unit { padding-right:6px; }
507
+ .devtools-ready .dt-shell .dt-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
+ .devtools-ready .dt-shell .dt-camera-reset:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }
509
+ .devtools-ready .dt-shell .dt-copy-agent { min-height:var(--dt-camera-control-height); padding:4px 10px; font-size:12px; white-space:nowrap; }
510
+ .devtools-ready .dt-shell .dt-menu { top:var(--dt-header-height); right:4px; width:216px; padding:6px; border-radius:4px; }
511
+ .devtools-ready .dt-shell .dt-dock-options { gap:4px; margin-bottom:6px; }
512
+ .devtools-ready .dt-shell .dt-dock-options button { height:26px; padding:2px; }
513
+ .devtools-ready .dt-shell :is(.dt-segments,.dt-layer-modes,.controls-segments) { padding:1px; gap:1px; border-radius:3px; }
514
+ .devtools-ready .dt-shell :is(.dt-segments,.dt-layer-modes,.controls-segments) button { min-height:20px; padding:1px 6px; border-radius:2px; }
515
+ .devtools-ready .dt-shell .dt-tools { min-height:28px; padding:3px 8px; gap:4px; }
516
+ .devtools-ready .dt-shell .dt-tools input[type=search] { height:var(--dt-control-height); padding:0 6px; border-radius:3px; }
517
+ .devtools-ready .dt-shell .dt-layer-tools-row { min-width:0; min-height:var(--dt-control-height); gap:6px; flex-wrap:wrap; }
518
+ .devtools-ready .dt-shell .dt-layer-modes { max-width:100%; overflow-x:auto; }
519
+ .devtools-ready .dt-shell .dt-subtabs { padding:0 8px; gap:12px; }
520
+ .devtools-ready .dt-shell .dt-subtabs button { min-height:var(--dt-nav-height); padding:3px 0; border-radius:0; }
521
+ .devtools-ready .dt-shell .dt-footer { min-height:20px; padding:2px 8px; gap:8px; }
522
+ .devtools-ready .dt-shell .dt-section-heading { padding:4px 10px; }
523
+ .devtools-ready .dt-shell .dt-kv { gap:3px 8px; padding:6px 8px; font:11px/1.4 var(--mono); }
524
+ .devtools-ready .dt-shell .dt-layer-group-title { height:var(--dt-row-height); min-height:var(--dt-row-height); padding:0 8px 0 6px!important; }
525
+ .devtools-ready .dt-shell .dt-layer-row { grid-template-columns:18px 16px minmax(0,1fr) 52px; height:var(--dt-row-height); min-height:var(--dt-row-height); padding:0 8px 0 4px; gap:4px; border-radius:0; }
526
+ .devtools-ready .dt-shell .dt-layer-glyph { font-size:13px; }
527
+ .devtools-ready .dt-shell .dt-eye { min-height:18px; width:18px; height:18px; padding:0; }
528
+ .devtools-ready .dt-shell .dt-zrange { width:52px; }
529
+ .devtools-ready .dt-shell .dt-layer-heading { padding:8px 10px 4px; }
530
+ .devtools-ready .dt-shell .dt-layer-title strong { font-size:13px; }
531
+ .devtools-ready .dt-shell .dt-layer-status { margin-top:3px; gap:6px; font-size:11px; }
532
+ .devtools-ready .dt-shell .dt-layer-rules th { padding:4px 10px; }
533
+ .devtools-ready .dt-shell .dt-layer-rules td { padding:3px 10px; min-height:24px; }
534
+ .devtools-ready .dt-shell .dt-layer-rules code { font:11px/1.4 var(--mono); }
535
+ .devtools-ready .dt-shell .dt-layer-rule-editor { gap:5px; }
536
+ .devtools-ready .dt-shell .dt-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
+ .devtools-ready .dt-shell .dt-layer-rule-editor input[type=color] { width:18px; height:20px; }
538
+ .devtools-ready .dt-shell .dt-layer-rule-editor input[type=range] { width:88px; min-width:60px; flex-basis:88px; }
539
+ .devtools-ready .dt-shell .dt-rule-toggle { gap:6px; }
540
+ .devtools-ready .dt-shell .dt-rule-choices { border-radius:3px; }
541
+ .devtools-ready .dt-shell .dt-rule-choices button { min-height:20px; padding:1px 5px; border-radius:0; }
542
+ .devtools-ready .dt-shell .dt-rule-spark { gap:6px; margin-bottom:3px; }
543
+ .devtools-ready .dt-shell .dt-rule-spark svg { width:100px; height:22px; }
544
+ .devtools-ready .dt-shell .dt-filter-chips { gap:4px; margin-block:6px; }
545
+ .devtools-ready .dt-shell .dt-filter-chip { padding:2px 6px; }
546
+ .devtools-ready .dt-shell :is(.dt-layer-section,.dt-layer-origin) { padding:6px 10px; }
547
+ .devtools-ready .dt-shell .dt-layer-evidence-row { padding:6px 10px; grid-template-columns:16px 110px minmax(0,1fr); }
548
+ .devtools-ready .dt-shell .dt-table { font:11px/1.4 var(--mono); }
549
+ .devtools-ready .dt-shell .dt-table th { padding:4px 8px; font:11px var(--ui); }
550
+ .devtools-ready .dt-shell .dt-table td { height:var(--dt-row-height); padding:2px 8px; }
551
+ .devtools-ready .dt-shell .dt-tile-heading { padding:8px 10px 4px; }
552
+ .devtools-ready .dt-shell .dt-tile-heading strong { font-size:13px; }
553
+ .devtools-ready .dt-shell .dt-tile-status { padding:0 10px 6px; }
554
+ .devtools-ready .dt-shell .dt-tile-grid { padding:8px; gap:2px; }
555
+ .devtools-ready .dt-shell .dt-tile-cell { padding:0; min-height:20px; }
556
+ .devtools-ready .dt-shell .dt-tile-grid-title { margin:6px 8px 0; }
557
+ .devtools-ready .dt-shell .dt-tile-chip-list { padding:5px 10px; gap:4px; }
558
+ .devtools-ready .dt-shell .dt-asset-main { padding:8px; }
559
+ .devtools-ready .dt-shell .dt-asset-aside h3 { font-size:13px; margin:8px 10px; }
560
+ .devtools-ready .dt-shell .dt-asset-variant { padding:10px 6px; gap:6px; }
561
+ .devtools-ready .dt-shell .dt-sprite-grid { grid-template-columns:repeat(auto-fill,minmax(88px,1fr)); }
562
+ .devtools-ready .dt-shell .dt-sprite-card { padding:6px 4px; border-radius:4px; }
563
+ .devtools-ready .dt-shell .dt-sprite-preview { height:44px; }
564
+ .devtools-ready .dt-shell .dt-sprite-card figcaption { margin-top:4px; }
565
+ .devtools-ready .dt-shell .dt-font-card { padding:8px; }
566
+ .devtools-ready .dt-shell .dt-font-card figcaption { margin-bottom:5px; font:11px/1.4 var(--mono); }
567
+ .devtools-ready .dt-shell .dt-font-loading { padding:8px; }
568
+ .devtools-ready .dt-shell .dt-model-grid { grid-template-columns:repeat(auto-fill,minmax(140px,1fr)); gap:6px; }
569
+ .devtools-ready .dt-shell .dt-model-card { padding:0; border-radius:4px; }
570
+ .devtools-ready .dt-shell .dt-model-card strong { padding:5px 8px 2px; }
571
+ .devtools-ready .dt-shell .dt-model-card small { padding:0 8px 5px; }
572
+ .devtools-ready .dt-shell .dt-debug-overlay { grid-template-columns:60px 1fr auto; gap:8px; padding:6px 10px; }
573
+ .devtools-ready .dt-shell .dt-debug-preview { width:60px; height:40px; border-radius:3px; }
574
+ .devtools-ready .dt-shell .dt-overdraw-help { padding:6px 10px 6px 78px; }
575
+ .devtools-ready .dt-shell .dt-debug-scale { gap:8px; padding:8px 10px; }
576
+ .devtools-ready .dt-shell .dt-debug-specimen { gap:10px; min-height:52px; padding:6px 10px; border-radius:4px; }
577
+ .devtools-ready .dt-shell .controls-section-title { padding:4px 10px; }
578
+ .devtools-ready .dt-shell .map-controls .control-field { grid-template-columns:140px minmax(0,1fr); padding:6px 10px; gap:8px; font-size:12px; }
579
+ .devtools-ready .dt-shell .control-module-header { padding:6px 10px; }
580
+ .devtools-ready .dt-shell .control-module-header label { font-size:12px; gap:6px; }
581
+ .devtools-ready .dt-shell .control-module-header input { margin-right:6px; }
582
+ .devtools-ready .dt-shell .control-module .control-field { padding-left:38px; }
583
+ .devtools-ready .dt-shell .map-controls .control-field.dt-palette-field { grid-template-columns:70px minmax(0,1fr); align-items:center; }
584
+ .devtools-ready .dt-shell .dt-palettes { gap:4px; align-items:center; }
585
+ .devtools-ready .dt-shell .dt-palettes button { flex-direction:row; align-items:center; gap:5px; padding:2px 6px; min-width:0; height:var(--dt-control-height); font-size:11px; white-space:nowrap; }
586
+ .devtools-ready .dt-shell .dt-palette-swatches { width:24px; height:10px; flex:none; border-radius:2px; }
587
+ .devtools-ready .dt-shell .dt-palette-swatches i { min-width:0; }
588
+ @container(max-width:480px) {
589
+ .devtools-ready .dt-shell .map-controls .control-field { grid-template-columns:110px minmax(0,1fr); }
590
+ .devtools-ready .dt-shell .control-module .control-field { padding-left:24px; }
591
+ .devtools-ready .dt-shell .dt-camera-field input[type=range] { width:48px; }
592
+ }
593
+ `;
594
+
595
+ // src/devtools/styles.ts
596
+ var legacyStyles = `
597
+ body.devtools-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); --dt-selected:light-dark(#e8f0fe,#394457); --dt-string:light-dark(#a31515,#e6b98b); --dt-number:light-dark(#1a1aa6,#a8c7fa); font:12px/1.45 Arial,system-ui,sans-serif; background:var(--paper); }
598
+ .devtools-ready button,.devtools-ready input,.devtools-ready select,.devtools-ready summary { min-height:24px; border-radius:2px; font:inherit; }
599
+ .devtools-ready button { padding:3px 8px; background:var(--paper); color:var(--ink); }
600
+ .devtools-ready button:hover { background:var(--surface); border-color:var(--border); }
601
+ .devtools-ready button:focus-visible,.devtools-ready input:focus-visible,.devtools-ready select:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }
602
+ .devtools-ready button[aria-pressed=true] { color:var(--accent); background:var(--dt-selected); }
603
+ .devtools-ready .tool-icon { width:14px; height:14px; }
604
+ .devtools-ready .workspace { display:grid; grid-template-columns:minmax(0,1fr); grid-template-rows:minmax(180px,1fr) var(--dt-height,340px); flex:1; min-height:0; }
605
+ .devtools-ready .workspace[data-dock=right] { grid-template-columns:minmax(200px,1fr) max(640px,var(--dt-width,760px)); grid-template-rows:minmax(0,1fr); }
606
+ .devtools-ready .workspace[data-dock=left] { grid-template-columns:max(640px,var(--dt-width,760px)) minmax(200px,1fr); grid-template-rows:minmax(0,1fr); }
607
+ .devtools-ready .workspace[data-dock=left] > .stage { grid-column:2; grid-row:1; }
608
+ .devtools-ready .workspace[data-dock=left] > .dt-shell { grid-column:1; grid-row:1; border-right:1px solid var(--border); }
609
+ .devtools-ready .workspace[data-collapsed=true] { grid-template-columns:minmax(0,1fr); grid-template-rows:minmax(0,1fr); }
610
+ .devtools-ready .workspace[data-collapsed=true] > .dt-shell { display:none; }
611
+ .devtools-ready .workspace > .inspector { display:none !important; }
612
+ .devtools-ready .inspect-hint { top:8px; font-size:11px; padding:4px 7px; background:var(--paper); color:var(--ink); border:1px solid var(--accent); }
613
+ #dt-reopen { display:none; position:absolute; top:8px; left:8px; z-index:10; box-shadow:0 1px 4px #0002; }
614
+ .workspace[data-collapsed=true] > #dt-reopen { display:block; }
615
+ .devtools-ready .workspace[data-collapsed=true] > .stage { grid-column:1; grid-row:1; }
616
+ .dt-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
+ .dt-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
+ .dt-head-main { display:flex; align-items:center; gap:12px; flex:1; min-width:0; align-self:stretch; }
619
+ .dt-tabs { display:flex; min-width:0; overflow-x:auto; scrollbar-width:none; flex:0 1 auto; align-self:stretch; }
620
+ .dt-tabs button { border:0; border-bottom:2px solid transparent; background:transparent; border-radius:0; white-space:nowrap; padding:8px 10px; min-height:38px; }
621
+ .dt-tabs button[aria-selected=true] { color:var(--accent); border-bottom-color:var(--accent); background:var(--paper); }
622
+ .dt-head .dt-icon,.dt-tools .dt-icon { display:inline-flex; align-items:center; justify-content:center; border:0; background:transparent; padding:3px; width:26px; height:26px; flex:none; }
623
+ .dt-head .dt-icon:disabled { opacity:.35; cursor:default; }
624
+ .dt-icon svg { width:16px; height:16px; pointer-events:none; }
625
+ .dt-divider { height:16px; border-left:1px solid var(--border); margin:0 4px; }
626
+ .dt-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
+ .dt-camera-field { display:flex; align-items:center; gap:4px; min-width:0; flex:none; }
628
+ .dt-camera-field > label { color:var(--muted); font-size:11px; line-height:1; }
629
+ .dt-stepper { display:flex; align-items:stretch; height:26px; border:1px solid var(--border); border-radius:4px; overflow:hidden; background:var(--paper); }
630
+ .dt-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
+ .dt-stepper input::-webkit-inner-spin-button,.dt-stepper input::-webkit-outer-spin-button { -webkit-appearance:none; margin:0; }
632
+ .dt-stepper button { width:24px; min-height:0; padding:0; border:0; border-radius:0; background:transparent; font-size:15px; line-height:1; }
633
+ .dt-stepper button:disabled { color:var(--muted); opacity:.4; cursor:default; }
634
+ .dt-camera:disabled { opacity:.5; }
635
+ .dt-camera-reset { flex:none; height:26px; }
636
+ .dt-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]) .dt-head { flex-wrap:wrap; }
638
+ :is([data-dock=left],[data-dock=right]) .dt-head > .dt-icon { margin-block:6px; }
639
+ :is([data-dock=left],[data-dock=right]) .dt-head-main { display:contents; }
640
+ :is([data-dock=left],[data-dock=right]) .dt-tabs { flex:1; }
641
+ :is([data-dock=left],[data-dock=right]) .dt-camera-field { flex:1 0 96px; }
642
+ :is([data-dock=left],[data-dock=right]) .dt-stepper { flex:1; min-width:0; }
643
+ :is([data-dock=left],[data-dock=right]) .dt-stepper input[type=number] { flex:1; width:0; }
644
+ :is([data-dock=left],[data-dock=right]) .dt-stepper button { flex:none; width:22px; }
645
+ :is([data-dock=left],[data-dock=right]) .dt-menu { top:38px; }
646
+ @container(max-width:1050px) {
647
+ .dt-head { flex-wrap:wrap; }
648
+ .dt-head > .dt-icon { margin-block:6px; }
649
+ .dt-head-main { display:contents; }
650
+ .dt-tabs { flex:1; }
651
+ .dt-camera-field { flex:1 0 96px; }
652
+ .dt-stepper { flex:1; min-width:0; }
653
+ .dt-stepper input[type=number] { flex:1; width:0; }
654
+ .dt-stepper button { flex:none; width:22px; }
655
+ .dt-menu { top:38px; }
656
+ }
657
+ .dt-menu-label { color:var(--muted); margin:2px 0 6px; }
658
+ .dt-dock-options { display:flex; gap:4px; }
659
+ .dt-dock-options button { padding:6px; display:flex; }
660
+ .dt-dock-options svg { width:24px; height:20px; }
661
+ .dt-resize { position:absolute; z-index:5; height:6px; left:0; right:0; top:-4px; cursor:ns-resize; touch-action:none; }
662
+ [data-dock=right] > .dt-shell > .dt-resize { top:0; bottom:0; left:-4px; width:6px; height:auto; right:auto; cursor:ew-resize; }
663
+ [data-dock=left] > .dt-shell > .dt-resize { top:0; bottom:0; right:-4px; width:6px; height:auto; left:auto; cursor:ew-resize; }
664
+ .dt-resize-shield { position:absolute; inset:0; z-index:30; background:transparent; }
665
+ .dt-body { flex:1; display:flex; flex-direction:column; min-height:0; overflow:hidden; }
666
+ .dt-panel { flex:1; min-height:0; min-width:0; display:flex; flex-direction:column; overflow:hidden; }
667
+ .dt-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
+ .dt-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
+ .dt-tools label { display:flex; align-items:center; gap:4px; white-space:nowrap; color:var(--muted); }
670
+ .dt-tools select { max-width:160px; padding:2px; }
671
+ .dt-tools input[type=checkbox] { min-height:0; margin:0; accent-color:var(--accent); }
672
+ .dt-tools button { border-color:transparent; background:transparent; }
673
+ .dt-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
+ .dt-scroll { flex:1; overflow:auto; min-height:0; min-width:0; }
675
+ .dt-empty { color:var(--muted); padding:22px; text-align:center; margin:auto; max-width:420px; }
676
+ .dt-empty strong { color:var(--ink); display:block; margin-bottom:6px; font-weight:400; }
677
+ .dt-inspect-grid { display:grid; grid-template-columns:minmax(220px,1fr) minmax(300px,42%); flex:1; min-height:0; }
678
+ .dt-tree-column { display:flex; flex-direction:column; min-height:0; border-right:1px solid var(--border); }
679
+ .dt-tree { flex:1; min-height:0; overflow:auto; font:12px/1.65 ui-monospace,SFMono-Regular,Consolas,monospace; padding:5px 0; }
680
+ .dt-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
+ .dt-tree-row[aria-selected=true] { background:var(--dt-selected) !important; }
682
+ .dt-tree-row:hover { background:var(--surface) !important; }
683
+ .dt-tree-row .dt-token { color:var(--dt-number); }
684
+ .dt-tree-row .dt-attr { color:var(--dt-string); }
685
+ .dt-tree-row .dt-dim { color:var(--muted); }
686
+ .dt-tree-row .dt-truncate { overflow:hidden; text-overflow:ellipsis; }
687
+ .dt-details { min-width:0; min-height:0; display:flex; flex-direction:column; }
688
+ .dt-subtabs { display:flex; gap:2px; border-bottom:1px solid var(--border); flex:none; overflow-x:auto; }
689
+ .dt-subtabs button { border:0; border-bottom:2px solid transparent; border-radius:0; padding:4px 8px; white-space:nowrap; }
690
+ .dt-subtabs button[aria-selected=true] { border-bottom-color:var(--accent); color:var(--accent); }
691
+ .dt-rule { border-bottom:1px solid var(--border); padding:7px 9px; font:12px/1.7 ui-monospace,SFMono-Regular,Consolas,monospace; }
692
+ .dt-rule-title { display:flex; justify-content:space-between; gap:8px; color:var(--ink); overflow-wrap:anywhere; margin-bottom:4px; }
693
+ .dt-rule-origin { color:var(--muted); font:11px/1.6 Arial,sans-serif; text-align:right; }
694
+ .dt-property { display:flex; align-items:center; gap:4px; padding:1px 0 1px 14px; min-height:24px; }
695
+ .dt-property-name { color:light-dark(#881391,#c58af9); flex:none; }
696
+ .dt-property input[type=text],.dt-property input[type=number],.dt-property select { border:1px solid transparent; background:transparent; min-width:30px; flex:1; width:100%; padding:0 2px; color:var(--dt-string); border-radius:0; font:inherit; min-height:22px; }
697
+ .dt-property input:focus,.dt-property input:hover { border-color:var(--border); }
698
+ .dt-property input[type=color] { width:14px; height:14px; padding:0; min-height:0; border:0; flex:none; background:transparent; cursor:pointer; }
699
+ .dt-property input[type=color]::-webkit-color-swatch-wrapper { padding:0; }
700
+ .dt-property input[type=color]::-webkit-color-swatch { border:1px solid var(--border); }
701
+ .dt-property .dt-revert { border:0; padding:0 3px; min-height:18px; color:var(--muted); background:transparent; }
702
+ .dt-property[data-modified=true] { background:color-mix(in srgb,var(--accent) 8%,transparent); }
703
+ .dt-expression { display:block; padding-left:14px; color:var(--muted); white-space:pre-wrap; overflow-wrap:anywhere; font-size:11px; }
704
+ .dt-table { border-collapse:collapse; width:100%; font-size:11px; text-align:left; }
705
+ .dt-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
+ .dt-table th button { font:inherit; border:0; padding:0; min-height:18px; background:transparent; }
707
+ .dt-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
+ .dt-table tbody tr:nth-child(even) { background:color-mix(in srgb,var(--surface) 55%,transparent); }
709
+ .dt-table tbody tr[aria-selected=true] { background:var(--dt-selected); }
710
+ .dt-table tbody tr:hover { background:var(--surface); }
711
+ .dt-table .dt-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
+ .dt-table .dt-number { font-variant-numeric:tabular-nums; text-align:right; }
713
+ .dt-table .dt-error { color:light-dark(#b3261e,#f2b8b5); }
714
+ .dt-network-body { display:flex; flex:1; min-height:0; min-width:0; }
715
+ .dt-network-body > .dt-scroll { flex:1; min-width:0; }
716
+ .dt-network-body[data-selected=true] > .dt-scroll { flex:1 1 55%; }
717
+ .dt-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
+ .dt-network-detail[hidden] { display:none; }
719
+ .dt-tile-row { cursor:pointer; }
720
+ .dt-tile-heading { flex:none; justify-content:space-between; }
721
+ .dt-tile-heading strong { font-size:12px; font-weight:500; }
722
+ .dt-tile-heading button { border:0; font-size:18px; }
723
+ .dt-tile-content { min-height:0; }
724
+ .dt-tile-content select { width:100%; min-width:0; background:var(--paper); color:var(--ink); border:1px solid var(--border); padding:4px; }
725
+ .dt-tile-paging { justify-content:space-between; }
726
+ .dt-tile-feature { border-bottom:1px solid var(--border); padding:5px 8px; }
727
+ .dt-tile-feature summary { cursor:pointer; overflow-wrap:anywhere; font-size:11px; }
728
+ .dt-tile-feature details { padding-left:12px; }
729
+ .dt-tile-feature pre { white-space:pre-wrap; overflow-wrap:anywhere; font:11px/1.6 ui-monospace,monospace; color:var(--dt-string); margin:6px 0; }
730
+ .dt-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
+ .dt-kv dt { color:var(--muted); overflow-wrap:anywhere; }.dt-kv dd { margin:0; overflow-wrap:anywhere; }
732
+ .dt-note { color:var(--muted); font-size:11px; padding:6px 8px; margin:0; }
733
+ .dt-status-message { color:var(--accent); padding:4px 8px; border-top:1px solid var(--border); font-size:11px; }
734
+ .dt-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
+ .dt-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
+ .dt-shell .map-controls { flex:1; width:100%; max-width:none; padding:0 16px 16px; border:0; overflow:auto; background:var(--paper); }
737
+ .dt-shell .controls-grid,.dt-shell #controls-modules { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:0 24px; align-items:start; }
738
+ .dt-shell .controls-grid { border-bottom:1px solid var(--border); padding-bottom:8px; }
739
+ .dt-shell .controls-selection,.dt-shell .controls-runtime { border:0; padding-bottom:0; }
740
+ .dt-shell .map-controls .control-field,.dt-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
+ .dt-shell .map-controls .control-field > select,.dt-shell .map-controls .control-field > input[type=number] { justify-self:start; width:min(200px,100%); max-width:100%; }
742
+ .dt-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
+ .dt-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
+ .dt-shell .map-controls input[type=checkbox]:checked { background:var(--accent); border-color:var(--accent); }
745
+ .dt-shell .map-controls input[type=checkbox]:checked::before { transform:translateX(14px); background:light-dark(#fff,#202124); }
746
+ .dt-shell .map-controls input[type=checkbox]:hover:not(:disabled) { box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 12%,transparent); }
747
+ .dt-shell .map-controls input[type=checkbox]:focus-visible { outline:2px solid var(--accent); outline-offset:3px; }
748
+ .dt-shell .map-controls input[type=checkbox]:disabled { cursor:not-allowed; }
749
+ .dt-shell .map-controls .control-field:has(input[type=checkbox]:not(:disabled)) { cursor:pointer; }
750
+ .dt-shell .controls-heading button { min-width:60px; }
751
+ .dt-shell .controls-segments button { padding:4px 10px; border-radius:0; }
752
+ @media (prefers-reduced-motion:reduce) { .dt-shell .map-controls input[type=checkbox],.dt-shell .map-controls input[type=checkbox]::before { transition:none; } }
753
+ .dt-shell .control-module { border:0; border-bottom:1px solid var(--border); border-radius:0; background:var(--paper); margin:0; }
754
+ .dt-shell .control-module-header { padding:5px 0; }.dt-shell .control-module-header label { font-size:12px; font-weight:400; }
755
+ .dt-shell .control-module fieldset { min-width:0; padding:0 0 8px 16px; }
756
+ .dt-shell .controls-section-title { margin:10px 0 4px; font-size:11px; letter-spacing:0; text-transform:none; }
757
+ .dt-shell .control-field { margin:5px 0; }
758
+ @container(max-width:680px) { .dt-shell .controls-grid,.dt-shell #controls-modules { grid-template-columns:minmax(0,1fr); }.dt-shell .map-controls .control-field,.dt-shell .control-module-header label { grid-template-columns:min(36%,160px) minmax(0,1fr); }.dt-shell .controls-segments button { padding-inline:8px; } }
759
+ .dt-shell .controls-code { background:var(--surface); }.dt-shell .controls-selection .control-field select { max-width:100%; }
760
+ .dt-shell .panel-heading h2 { font:12px Arial,sans-serif; }.dt-shell .review-tabs { margin:0; padding:0; }
761
+ .dt-assets-content { scrollbar-gutter:stable; }
762
+ .dt-font-list { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,350px),1fr)); max-width:1100px; }
763
+ .dt-font-card { min-width:0; margin:0; padding:14px 12px; border-bottom:1px solid var(--border); }
764
+ .dt-font-card figcaption { margin-bottom:8px; font:12px/1.5 ui-monospace,monospace; }
765
+ .dt-font-sample { min-height:94px; overflow-x:auto; border:1px solid var(--border); border-radius:3px; background:#fff; color:#303b4b; }
766
+ .dt-font-preview { display:block; }
767
+ .dt-font-loading { display:block; padding:15px 10px; font-size:11px; color:#62676e; }
768
+ .dt-sprite-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(112px,1fr)); gap:1px; padding:1px; background:var(--border); }
769
+ .dt-sprite-card { margin:0; min-width:0; padding:10px 8px 8px; background:var(--paper); text-align:center; }
770
+ .dt-sprite-card:hover { background:var(--dt-selected); }
771
+ .dt-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
+ .dt-sprite-card figcaption { margin-top:7px; font:11px/1.4 ui-monospace,monospace; overflow-wrap:anywhere; }
773
+ .dt-sprite-size { display:block; min-height:15px; margin-top:3px; color:var(--muted); font-size:10px; }
774
+ .dt-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
+ .dt-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
+ .dt-geometry-handles { position:absolute; inset:0; pointer-events:none; z-index:5; overflow:hidden; }
777
+ .dt-performance { padding:10px; }.dt-performance svg { width:100%; height:90px; display:block; }.dt-performance dl { max-width:600px; }
778
+ .dt-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
+ .dt-overdraw-help[hidden] { display:none; }
780
+ .dt-overdraw-help strong { font-size:12px; }.dt-overdraw-help p { margin:5px 0 0; line-height:1.5; }
781
+ .dt-overdraw-legend { display:flex; flex-wrap:wrap; gap:6px 20px; margin:10px 0 8px; }
782
+ .dt-overdraw-key { display:inline-flex; align-items:center; gap:6px; font:11px/1.4 ui-monospace,monospace; }
783
+ .dt-overdraw-swatch { width:24px; height:10px; border:1px solid #0002; border-radius:2px; }
784
+ .dt-overdraw-caption { color:var(--muted); font-size:11px; }
785
+ .dt-badge { background:var(--dt-selected); color:var(--accent); border-radius:7px; padding:0 5px; margin-left:3px; font:10px/1.5 Arial,sans-serif; }
786
+ [data-dock=left] .dt-inspect-grid,[data-dock=right] .dt-inspect-grid { display:flex; flex-direction:column; }
787
+ [data-dock=left] .dt-tree-column,[data-dock=right] .dt-tree-column { flex:0 0 32%; border-right:0; border-bottom:1px solid var(--border); }
788
+ [data-dock=left] .dt-details,[data-dock=right] .dt-details { flex:1; }
789
+ .dt-zoom { padding:8px; }.dt-zoom > select { width:100%; margin-bottom:8px; }
790
+ @container(max-width:680px) { .dt-inspect-grid { display:flex;flex-direction:column; }.dt-tree-column { flex:0 0 32%;border-right:0;border-bottom:1px solid var(--border); }.dt-details { flex:1; } }
791
+ `;
792
+ var devtoolsStyles = legacyStyles + inspectorStyles + cardStyles;
793
+
794
+ // src/devtools/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 || "Map DevTools 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="dt-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.__tileflowDevtoolsUI?.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(--dt-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 dt-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 dt-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="dt-palettes" role="radiogroup" aria-label="Theme"></div></div>
1162
+ </div>
1163
+ <fieldset class="controls-runtime dt-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="dt-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.__tileflowDevtoolsUI) { globalThis.__tileflowDevtoolsUI.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 DevTools 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>`;
39
1518
 
40
1519
  // src/comparison-html.ts
41
1520
  function renderTileflowComparisonHtml(options) {
42
1521
  const documentOptions = serializeInlineJson(options);
1522
+ const fontBasePath = options.basePath.split("/").map((segment) => encodeURIComponent(decodeURIComponent(segment)).replaceAll("'", "%27")).join("/");
43
1523
  const nonceAttribute = options.cspNonce ? ` nonce="${escapeHtml(options.cspNonce)}"` : "";
44
1524
  return `<!doctype html>
45
1525
  <html lang="en" data-tileflow-state="loading">
@@ -48,6 +1528,10 @@ function renderTileflowComparisonHtml(options) {
48
1528
  <meta name="viewport" content="width=device-width, initial-scale=1" />
49
1529
  <title>${escapeHtml(options.title)}</title>
50
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'); }
51
1535
  :root {
52
1536
  color-scheme: light;
53
1537
  --accent: #3157d5;
@@ -59,6 +1543,8 @@ function renderTileflowComparisonHtml(options) {
59
1543
  --overlay-alpha: .5;
60
1544
  }
61
1545
  * { box-sizing: border-box; }
1546
+ [hidden] { display: none !important; }
1547
+ .stage[data-mode="single"] .pane-right { display: none; }
62
1548
  html, body { height: 100%; margin: 0; }
63
1549
  body {
64
1550
  display: grid;
@@ -83,49 +1569,10 @@ function renderTileflowComparisonHtml(options) {
83
1569
  outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent);
84
1570
  outline-offset: 2px;
85
1571
  }
86
- .toolbar {
87
- z-index: 10;
88
- display: flex;
89
- min-height: 48px;
90
- align-items: center;
91
- gap: 8px;
92
- padding: 7px 10px;
93
- border-bottom: 1px solid var(--border);
94
- background: rgba(255, 255, 255, .97);
95
- flex-wrap: wrap;
96
- }
97
- .toolbar-title { font-weight: 750; margin-right: 4px; }
98
- .toolbar-group { display: inline-flex; min-width: 0; align-items: center; gap: 8px; flex-wrap: wrap; }
99
- .toolbar-separator { width: 1px; height: 25px; flex: 0 0 auto; background: var(--border); }
100
- .toolbar label { display: inline-flex; align-items: center; gap: 5px; color: var(--muted); }
101
- .toolbar input[type="range"] { width: 96px; }
102
- .toolbar input[type="text"] { width: 150px; padding: 5px 7px; border: 1px solid var(--border); border-radius: 6px; }
103
1572
  .workspace { position: relative; min-height: 0; display: grid; grid-template-columns: minmax(0, 1fr) auto; }
104
1573
  .stage { position: relative; min-width: 0; min-height: 0; overflow: hidden; background: #d8dadd; }
105
1574
  .pane { position: absolute; inset: 0; min-width: 0; min-height: 0; overflow: hidden; background: #d8dadd; }
106
1575
  .pane iframe { width: 100%; height: 100%; display: block; border: 0; background: #d8dadd; }
107
- .pane-label {
108
- position: absolute;
109
- z-index: 3;
110
- top: 10px;
111
- left: 10px;
112
- display: flex;
113
- max-width: calc(100% - 20px);
114
- align-items: center;
115
- gap: 7px;
116
- border: 1px solid rgba(0, 0, 0, .12);
117
- border-radius: 999px;
118
- background: rgba(255, 255, 255, .93);
119
- box-shadow: 0 3px 14px rgba(0, 0, 0, .12);
120
- padding: 5px 9px;
121
- pointer-events: none;
122
- }
123
- .pane-label-text { overflow: hidden; text-overflow: ellipsis; }
124
- .side-state-text { flex: 0 0 auto; color: var(--muted); font-size: 10px; text-transform: uppercase; letter-spacing: .03em; }
125
- .side-state { width: 8px; height: 8px; flex: 0 0 auto; border-radius: 50%; background: #8d929b; }
126
- .side-state[data-status="ready"] { background: #248a4a; }
127
- .side-state[data-status="building"] { background: #cf8114; }
128
- .side-state[data-status="invalid"], .side-state[data-status="error"] { background: #ba3838; }
129
1576
  .stage[data-mode="side-by-side"] { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 1px; }
130
1577
  .stage[data-mode="side-by-side"] .pane { position: relative; inset: auto; }
131
1578
  .stage[data-mode="side-by-side"] .pane-right { grid-column: 2; }
@@ -165,74 +1612,46 @@ function renderTileflowComparisonHtml(options) {
165
1612
  .sprite-card { min-width: 0; border: 1px solid var(--border); border-radius: 7px; padding: 7px; background: var(--paper); text-align: center; }
166
1613
  .sprite-card canvas { display: block; margin: auto; max-width: 64px; max-height: 64px; image-rendering: auto; }
167
1614
  .sprite-name { display: block; margin-top: 4px; overflow: hidden; text-overflow: ellipsis; font-size: 10px; }
168
- .feedback { color: var(--muted); min-width: 64px; }
169
1615
  .visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
170
1616
  @media (max-width: 760px) {
171
- .toolbar { align-content: center; gap: 6px; padding: 6px; }
172
- .toolbar-title { flex: 1 0 calc(100% - 12px); }
173
- .toolbar-separator { display: none; }
174
- .toolbar-group { flex: 1 1 100%; gap: 5px; }
175
- .toolbar-group button { padding-inline: 7px; }
176
- .toolbar input[type="range"] { width: min(24vw, 86px); }
177
- .toolbar input[type="text"] { width: min(34vw, 130px); }
178
1617
  .workspace { grid-template-columns: minmax(0, 1fr); }
179
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); }
180
1619
  }
181
1620
  @media (prefers-reduced-motion: reduce) {
182
1621
  *, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
183
1622
  }
1623
+ ${workbenchStyles}
1624
+ ${controlsStyles}
1625
+ ${devtoolsStyles}
184
1626
  </style>
185
1627
  </head>
186
1628
  <body>
187
- <header class="toolbar" aria-label="Comparison controls">
188
- <span class="toolbar-title" id="title"></span>
189
- <div class="toolbar-group" role="group" aria-label="Comparison mode">
190
- <button type="button" data-mode="side-by-side">Side by side</button>
191
- <button type="button" data-mode="split">Split</button>
192
- <button type="button" data-mode="overlay">Overlay</button>
193
- <button type="button" data-mode="blink">Blink</button>
194
- </div>
195
- <span class="toolbar-separator"></span>
196
- <div class="toolbar-group">
197
- <label id="split-control">Split <input id="split" type="range" min="5" max="95" value="50" aria-label="Split position" /></label>
198
- <label id="alpha-control">Opacity <input id="alpha" type="range" min="0" max="100" value="50" aria-label="Overlay opacity" /></label>
199
- <button type="button" id="driver">Interact: left</button>
200
- <button type="button" id="blink-pause">Pause blink</button>
201
- <button type="button" id="inspect-toggle" aria-controls="inspector" aria-expanded="false" aria-pressed="false">Inspect</button>
202
- </div>
203
- <span class="toolbar-separator"></span>
204
- <div class="toolbar-group">
205
- <input id="scene-name" type="text" value="comparison-view" aria-label="Scene name" />
206
- <label>DPR <select id="scene-dpr"><option value="1">1</option><option value="2">2</option></select></label>
207
- <button type="button" id="copy-scene">Copy scene</button>
208
- <button type="button" id="copy-command">Copy command</button>
209
- </div>
210
- <span class="feedback" id="feedback" role="status"></span>
211
- </header>
212
1629
  <div class="workspace">
213
1630
  <main class="stage" id="stage" data-driver="left" data-tileflow-capture-id="tileflow-comparison" data-tileflow-state="loading">
214
1631
  <section class="pane pane-left" data-side="left">
215
- <div class="pane-label"><span class="side-state" id="left-state" role="status" aria-live="polite"></span><span class="pane-label-text" id="left-label"></span><span class="side-state-text" id="left-status-text">Building</span></div>
216
- <iframe id="left-frame" title="Left Tileflow preview" referrerpolicy="no-referrer"></iframe>
1632
+ <iframe id="left-frame" title="${options.right ? "Left Tileflow preview" : "Tileflow map preview"}" referrerpolicy="no-referrer"></iframe>
217
1633
  </section>
218
1634
  <section class="pane pane-right" data-side="right">
219
- <div class="pane-label"><span class="side-state" id="right-state" role="status" aria-live="polite"></span><span class="pane-label-text" id="right-label"></span><span class="side-state-text" id="right-status-text">Building</span></div>
220
1635
  <iframe id="right-frame" title="Right Tileflow preview" referrerpolicy="no-referrer"></iframe>
221
1636
  </section>
222
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>
223
1639
  </main>
224
1640
  <aside class="inspector" id="inspector" tabindex="-1" aria-labelledby="inspector-title" hidden>
225
- <div class="inspector-heading"><h2 id="inspector-title">Cartographic inspector</h2><button type="button" id="inspector-close" aria-label="Close inspector">Close</button></div>
226
- <div id="inspection" class="empty">Enable Inspect, then click either map.</div>
227
- <h3>Zoom curve</h3>
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>
228
1646
  <select id="curve-property" disabled><option>Select a rendered layer first</option></select>
229
- <div id="curve-output" class="empty">Paint and layout values appear here.</div>
230
- <h3>Compiler sidecar</h3>
231
- <pre id="sidecar-output">No layer selected.</pre>
232
- <h3>Sprite atlas</h3>
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>
233
1650
  <button type="button" id="load-sprites">Load active side sprites</button>
234
- <div id="sprite-output" class="sprite-grid"></div>
1651
+ <div id="sprite-output" class="sprite-grid"></div></details>
235
1652
  </aside>
1653
+ ${workbenchPanels}
1654
+ ${controlsPanel}
236
1655
  </div>
237
1656
  <script type="module"${nonceAttribute}>
238
1657
  const options = ${documentOptions};
@@ -250,24 +1669,16 @@ function renderTileflowComparisonHtml(options) {
250
1669
  const curveOutput = document.getElementById("curve-output");
251
1670
  const sidecarOutput = document.getElementById("sidecar-output");
252
1671
  const spriteOutput = document.getElementById("sprite-output");
253
- const feedback = document.getElementById("feedback");
254
- const splitInput = document.getElementById("split");
255
- const alphaInput = document.getElementById("alpha");
256
- const driverButton = document.getElementById("driver");
257
- const blinkPauseButton = document.getElementById("blink-pause");
258
1672
  const inspectButton = document.getElementById("inspect-toggle");
259
1673
  const inspectorCloseButton = document.getElementById("inspector-close");
260
- const sceneNameInput = document.getElementById("scene-name");
261
- const sceneDprInput = document.getElementById("scene-dpr");
262
1674
  const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
263
- const states = Object.fromEntries(["left", "right"].map((id) => {
1675
+ const sideIds = options.right ? ["left", "right"] : ["left"];
1676
+ const states = Object.fromEntries(sideIds.map((id) => {
264
1677
  const definition = options[id];
265
1678
  return [id, {
266
1679
  id,
267
1680
  definition,
268
1681
  frame: document.getElementById(id + "-frame"),
269
- stateElement: document.getElementById(id + "-state"),
270
- statusTextElement: document.getElementById(id + "-status-text"),
271
1682
  map: undefined,
272
1683
  camera: undefined,
273
1684
  bridgeReady: false,
@@ -296,45 +1707,25 @@ function renderTileflowComparisonHtml(options) {
296
1707
  let spriteLoadSequence = 0;
297
1708
  let spriteOwnerId;
298
1709
 
299
- document.getElementById("title").textContent = options.title;
300
1710
  const initialUi = readUiFromUrl();
301
- splitInput.value = String(initialUi.split);
302
- alphaInput.value = String(initialUi.alpha);
1711
+
303
1712
  applySliders();
304
1713
  setMode(initialUi.mode || options.initialMode);
305
- updateBlinkMotionPreference();
306
- for (const id of ["left", "right"]) {
1714
+ for (const id of sideIds) {
307
1715
  const side = states[id];
308
- document.getElementById(id + "-label").textContent = side.definition.label;
309
1716
  side.frame.src = side.definition.previewUrl;
310
1717
  side.frame.addEventListener("load", () => connectSide(side));
311
1718
  side.frame.addEventListener("focus", () => selectSide(side.id));
312
1719
  startStatus(side);
313
1720
  }
314
1721
 
315
- for (const button of document.querySelectorAll("[data-mode]")) {
316
- button.addEventListener("click", () => setMode(button.dataset.mode));
317
- }
318
- splitInput.addEventListener("input", () => { applySliders(); writeUrlState(); });
319
- alphaInput.addEventListener("input", () => { applySliders(); writeUrlState(); });
320
- driverButton.addEventListener("click", () => {
321
- selectSide(stage.dataset.driver === "left" ? "right" : "left");
322
- });
323
- blinkPauseButton.addEventListener("click", () => {
324
- blinkPaused = !blinkPaused;
325
- updateBlinkMotionPreference();
326
- restartBlink();
327
- });
328
1722
  inspectButton.addEventListener("click", () => setInspectionEnabled(!inspectionEnabled, true));
329
1723
  inspectorCloseButton.addEventListener("click", () => setInspectionEnabled(false, true));
330
- document.getElementById("copy-scene").addEventListener("click", () => copyScene(false));
331
- document.getElementById("copy-command").addEventListener("click", () => copyScene(true));
332
1724
  document.getElementById("load-sprites").addEventListener("click", () => loadSprites(states[activeSideId]));
333
1725
  curveProperty.addEventListener("change", renderCurve);
334
1726
  reducedMotion.addEventListener?.("change", () => {
335
1727
  blinkPaused = reducedMotion.matches;
336
- updateBlinkMotionPreference();
337
- restartBlink();
1728
+ restartBlink();
338
1729
  });
339
1730
  addEventListener("keydown", (event) => {
340
1731
  if (event.key === "Escape" && inspectionEnabled) setInspectionEnabled(false, true);
@@ -346,18 +1737,16 @@ function renderTileflowComparisonHtml(options) {
346
1737
  side.eventSource?.close();
347
1738
  side.sidecarAbort?.abort();
348
1739
  side.spriteAbort?.abort();
349
- side.cleanupBridge?.();
1740
+ cleanupSideBridge(side);
350
1741
  }
351
1742
  });
352
1743
 
353
1744
  function selectSide(id, writeState = true) {
354
- if (id !== "left" && id !== "right") return;
1745
+ if (!states[id]) return;
355
1746
  const changed = activeSideId !== id || stage.dataset.driver !== id;
356
1747
  const previousSpriteOwner = spriteOwnerId;
357
1748
  activeSideId = id;
358
1749
  stage.dataset.driver = id;
359
- driverButton.textContent = "Interact: " + id;
360
- driverButton.setAttribute("aria-label", "Interact with " + states[id].definition.label);
361
1750
  if (changed && previousSpriteOwner && previousSpriteOwner !== id) {
362
1751
  states[previousSpriteOwner].spriteAbort?.abort();
363
1752
  spriteLoadSequence += 1;
@@ -370,33 +1759,31 @@ function renderTileflowComparisonHtml(options) {
370
1759
 
371
1760
  function setInspectionEnabled(enabled, manageFocus = false) {
372
1761
  inspectionEnabled = enabled;
1762
+ if (globalThis.__tileflowDevtoolsUI) {
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.__tileflowDevtoolsUI.inspectionChanged(enabled);
1767
+ return;
1768
+ }
1769
+ if (enabled) toggleMapControls(false);
373
1770
  inspectButton.setAttribute("aria-pressed", String(enabled));
374
1771
  inspectButton.setAttribute("aria-expanded", String(enabled));
375
1772
  inspector.hidden = !enabled;
376
- for (const side of Object.values(states)) sendInspectionState(side);
1773
+ document.getElementById("wb-inspect-hint").hidden = !enabled;
1774
+ for (const side of Object.values(states)) { sendInspectionState(side); side.map?.resize?.(); }
377
1775
  if (manageFocus) {
378
1776
  if (enabled) inspector.focus();
379
1777
  else inspectButton.focus();
380
1778
  }
381
1779
  }
382
1780
 
383
- function updateBlinkMotionPreference() {
384
- const motionBlocked = reducedMotion.matches;
385
- blinkPauseButton.disabled = motionBlocked;
386
- blinkPauseButton.textContent = motionBlocked
387
- ? "Blink paused (reduced motion)"
388
- : blinkPaused ? "Resume blink" : "Pause blink";
389
- }
390
1781
 
391
1782
  function setMode(mode) {
392
- if (!["side-by-side", "split", "overlay", "blink"].includes(mode)) return;
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);
393
1786
  stage.dataset.mode = mode;
394
- for (const button of document.querySelectorAll("[data-mode]")) {
395
- button.setAttribute("aria-pressed", String(button.dataset.mode === mode));
396
- }
397
- document.getElementById("split-control").hidden = mode !== "split";
398
- document.getElementById("alpha-control").hidden = mode !== "overlay";
399
- blinkPauseButton.hidden = mode !== "blink";
400
1787
  restartBlink();
401
1788
  writeUrlState();
402
1789
  setTimeout(() => {
@@ -405,8 +1792,8 @@ function renderTileflowComparisonHtml(options) {
405
1792
  }
406
1793
 
407
1794
  function applySliders() {
408
- const split = boundedNumber(Number(splitInput.value), 5, 95, 50);
409
- const alpha = boundedNumber(Number(alphaInput.value), 0, 100, 50);
1795
+ const split = boundedNumber(initialUi.split, 5, 95, 50);
1796
+ const alpha = boundedNumber(initialUi.alpha, 0, 100, 50);
410
1797
  stage.style.setProperty("--split", split + "%");
411
1798
  stage.style.setProperty("--overlay-alpha", String(alpha / 100));
412
1799
  }
@@ -423,16 +1810,27 @@ function renderTileflowComparisonHtml(options) {
423
1810
  }, 700);
424
1811
  }
425
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
+
426
1821
  function connectSide(side) {
427
1822
  side.loadEpoch += 1;
1823
+ side.appliedSceneStyle = undefined;
1824
+ side.pendingAppearance = undefined;
428
1825
  const epoch = side.loadEpoch;
429
- side.cleanupBridge?.();
1826
+ cleanupSideBridge(side);
430
1827
  side.cleanupBridge = undefined;
431
1828
  side.sidecarAbort?.abort();
432
1829
  side.spriteAbort?.abort();
433
1830
  side.sidecarAbort = undefined;
434
1831
  side.spriteAbort = undefined;
435
1832
  side.map = undefined;
1833
+ side.bridgeConnected = false;
436
1834
  side.bridgeReady = false;
437
1835
  side.camera = undefined;
438
1836
  side.sidecar = undefined;
@@ -456,7 +1854,6 @@ function renderTileflowComparisonHtml(options) {
456
1854
  curveOutput.textContent = "Paint and layout values appear here.";
457
1855
  sidecarOutput.textContent = "No layer selected.";
458
1856
  }
459
- renderSideStatus(side);
460
1857
  updateDocumentReadiness();
461
1858
  const started = performance.now();
462
1859
  const findMap = () => {
@@ -467,7 +1864,13 @@ function renderTileflowComparisonHtml(options) {
467
1864
  const map = childWindow?.__tileflowPreviewMap;
468
1865
  if (map) {
469
1866
  side.map = map;
1867
+ // DevTools 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);
470
1871
  side.cleanupBridge = installChildBridge(side, childWindow, map, epoch);
1872
+ if (options.workbenchUrl) void refreshWorkbench();
1873
+ if (!controlsPanel.hidden) void loadMapControls();
471
1874
  return;
472
1875
  }
473
1876
  } catch (error) {
@@ -493,6 +1896,68 @@ function renderTileflowComparisonHtml(options) {
493
1896
  let releaseFrameAfterPaint;
494
1897
  let readyTimer;
495
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.__tileflowDevtoolsUI) || !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
+ };
496
1961
  const targetOrigin = childWindow.location.origin;
497
1962
  // The bridge is installed by the same-origin parent, so callbacks created here
498
1963
  // belong to the parent realm. Dispatch directly with an explicit child source;
@@ -515,11 +1980,11 @@ function renderTileflowComparisonHtml(options) {
515
1980
  });
516
1981
  };
517
1982
  const handleMoveStart = (event) => {
518
- if (applyingRequestId === undefined && event?.originalEvent) userMovementActive = true;
1983
+ if (applyingRequestId === undefined && (event?.originalEvent || event?.tileflowDevtools === true)) userMovementActive = true;
519
1984
  };
520
1985
  const handleMove = (event) => {
521
1986
  if (applyingRequestId !== undefined) return;
522
- if (event?.originalEvent) userMovementActive = true;
1987
+ if (event?.originalEvent || event?.tileflowDevtools === true) userMovementActive = true;
523
1988
  queuedUserMovement = queuedUserMovement || userMovementActive;
524
1989
  if (movementFrame !== undefined) return;
525
1990
  movementFrame = childWindow.requestAnimationFrame(() => {
@@ -535,7 +2000,7 @@ function renderTileflowComparisonHtml(options) {
535
2000
  scheduleApplyingRelease(applyingRequestId);
536
2001
  return;
537
2002
  }
538
- if (event?.originalEvent) userMovementActive = true;
2003
+ if (event?.originalEvent || event?.tileflowDevtools === true) userMovementActive = true;
539
2004
  if (movementFrame !== undefined) {
540
2005
  childWindow.cancelAnimationFrame(movementFrame);
541
2006
  movementFrame = undefined;
@@ -544,9 +2009,45 @@ function renderTileflowComparisonHtml(options) {
544
2009
  postCamera("end", undefined, userMovementActive);
545
2010
  userMovementActive = false;
546
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.__tileflowDevtoolsUI || hoverFrame !== undefined) return;
2027
+ hoverFrame = childWindow.requestAnimationFrame(() => {
2028
+ hoverFrame = undefined;
2029
+ hoveredFeature = map.queryRenderedFeatures(event.point).find((feature) => !feature.layer.id.startsWith("__tileflow-devtools-"));
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
+ };
547
2045
  const handleClick = (event) => {
548
2046
  if (!inspectionActive) return;
549
- const features = map.queryRenderedFeatures(event.point).slice(0, 16).map(projectFeature);
2047
+ clearHover();
2048
+ inspectedCoordinate = [event.lngLat.lng, event.lngLat.lat];
2049
+ inspectedFeatures = map.queryRenderedFeatures(event.point).filter((feature) => !feature.layer.id.startsWith("__tileflow-devtools-")).slice(0, 16);
2050
+ const features = inspectedFeatures.map(projectFeature);
550
2051
  postToParent({
551
2052
  type: bridgeInspectionType,
552
2053
  schemaVersion: bridgeSchemaVersion,
@@ -555,6 +2056,13 @@ function renderTileflowComparisonHtml(options) {
555
2056
  features
556
2057
  });
557
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
+ };
558
2066
  const handleError = (event) => {
559
2067
  postToParent({
560
2068
  type: bridgeRuntimeErrorType,
@@ -569,7 +2077,9 @@ function renderTileflowComparisonHtml(options) {
569
2077
  if (!message || message.schemaVersion !== bridgeSchemaVersion) return;
570
2078
  if (message.type === bridgeSetInspectionType && typeof message.enabled === "boolean") {
571
2079
  inspectionActive = message.enabled;
2080
+ if (!inspectionActive) clearHover();
572
2081
  map.getCanvas().style.cursor = inspectionActive ? "crosshair" : "";
2082
+ drawHighlight();
573
2083
  return;
574
2084
  }
575
2085
  if (message.type !== bridgeSetCameraType || !validCamera(message.camera)) return;
@@ -622,10 +2132,15 @@ function renderTileflowComparisonHtml(options) {
622
2132
  });
623
2133
  });
624
2134
  };
2135
+ map.on("move", drawHighlight);
2136
+ map.on("resize", drawHighlight);
2137
+ childWindow.addEventListener("keydown", handleKey);
625
2138
  map.on("movestart", handleMoveStart);
626
2139
  map.on("move", handleMove);
627
2140
  map.on("moveend", handleMoveEnd);
628
2141
  map.on("click", handleClick);
2142
+ map.on("mousemove", handleHover);
2143
+ map.on("mouseout", clearHover);
629
2144
  map.on("error", handleError);
630
2145
  childWindow.addEventListener("message", handleCommand);
631
2146
  if (map.loaded?.()) announceReady();
@@ -638,10 +2153,17 @@ function renderTileflowComparisonHtml(options) {
638
2153
  if (movementFrame !== undefined) childWindow.cancelAnimationFrame(movementFrame);
639
2154
  cancelApplyingRelease();
640
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);
641
2161
  map.off("movestart", handleMoveStart);
642
2162
  map.off("move", handleMove);
643
2163
  map.off("moveend", handleMoveEnd);
644
2164
  map.off("click", handleClick);
2165
+ map.off("mousemove", handleHover);
2166
+ map.off("mouseout", clearHover);
645
2167
  map.off("error", handleError);
646
2168
  map.off("load", announceReady);
647
2169
  map.off("idle", announceReady);
@@ -657,10 +2179,11 @@ function renderTileflowComparisonHtml(options) {
657
2179
  if (message.type === bridgeReadyType && validCamera(message.camera)) {
658
2180
  side.bridgeReady = true;
659
2181
  side.camera = message.camera;
660
- renderSideStatus(side);
661
2182
  maybeEstablishInitialCamera();
662
2183
  sendInspectionState(side);
663
2184
  void loadSidecar(side);
2185
+ if (options.workbenchUrl) void refreshWorkbench();
2186
+ if (!controlsPanel.hidden) void loadMapControls();
664
2187
  updateDocumentReadiness();
665
2188
  return;
666
2189
  }
@@ -700,7 +2223,7 @@ function renderTileflowComparisonHtml(options) {
700
2223
  if (message.type === bridgeInspectionType && Array.isArray(message.features)) {
701
2224
  selectSide(side.id);
702
2225
  selectedInspection = {side, coordinate: message.coordinate, features: message.features.slice(0, 16), featureIndex: 0};
703
- renderInspection();
2226
+ renderInspection(true);
704
2227
  return;
705
2228
  }
706
2229
  if (message.type === bridgeRuntimeErrorType) {
@@ -709,7 +2232,7 @@ function renderTileflowComparisonHtml(options) {
709
2232
  }
710
2233
 
711
2234
  function sendCamera(side, camera) {
712
- if (!side.bridgeReady || !camera || !side.frame.contentWindow) return;
2235
+ if (!side?.bridgeReady || !camera || !side.frame.contentWindow) return;
713
2236
  if (side.camera && camerasEqual(side.camera, camera)) return;
714
2237
  const requestId = ++requestSequence;
715
2238
  side.lastCameraRequestId = requestId;
@@ -759,6 +2282,16 @@ function renderTileflowComparisonHtml(options) {
759
2282
  .catch(() => undefined);
760
2283
  const events = new EventSource(side.definition.eventsUrl);
761
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; });
762
2295
  for (const name of ["building", "ready", "invalid"]) {
763
2296
  events.addEventListener(name, (event) => {
764
2297
  try { applyArtifactStatus(side, JSON.parse(event.data)); } catch {}
@@ -781,7 +2314,8 @@ function renderTileflowComparisonHtml(options) {
781
2314
  ? status.lastGoodGeneration
782
2315
  : undefined;
783
2316
  side.artifactDiagnostic = status.diagnostics?.[0]?.message;
784
- renderSideStatus(side);
2317
+ side.latestArtifactStatus = status;
2318
+ side.previewStatusReceiver?.(status);
785
2319
  maybeEstablishInitialCamera();
786
2320
  updateDocumentReadiness();
787
2321
  }
@@ -800,31 +2334,26 @@ function renderTileflowComparisonHtml(options) {
800
2334
  stage.dataset.tileflowState = value;
801
2335
  }
802
2336
 
803
- function renderSideStatus(side) {
804
- const diagnostic = side.artifactDiagnostic;
805
- const message = side.runtimeFailed
806
- ? "Runtime error: " + (side.runtimeErrors.at(-1) || "Preview runtime failed")
807
- : side.artifactStatus === "invalid"
808
- ? "Invalid generation; showing last valid preview" + (diagnostic ? ": " + diagnostic : "")
809
- : "Generation " + String(Math.max(0, side.artifactGeneration)) + " \xB7 " + side.artifactStatus;
810
- side.stateElement.dataset.status = side.runtimeFailed ? "error" : side.artifactStatus;
811
- side.stateElement.title = message;
812
- side.stateElement.setAttribute("aria-label", side.definition.label + ": " + message);
813
- side.statusTextElement.textContent = side.runtimeFailed ? "Runtime error" : side.artifactStatus;
814
- }
815
2337
 
816
2338
  async function loadSidecar(side) {
817
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
+ }
818
2347
  if (side.sidecar !== undefined) return side.sidecar;
819
2348
  if (side.sidecarPromise) return side.sidecarPromise;
820
2349
  const epoch = side.loadEpoch;
821
2350
  const controller = new AbortController();
822
2351
  side.sidecarAbort?.abort();
823
2352
  side.sidecarAbort = controller;
824
- const promise = fetch(currentSidecarUrl(side), {cache: "no-store", signal: controller.signal})
2353
+ const promise = fetch(key, {cache: "no-store", signal: controller.signal})
825
2354
  .then((response) => response.ok ? response.json() : Promise.reject(new Error("Compiler sidecar unavailable")))
826
2355
  .then((document) => {
827
- if (side.loadEpoch !== epoch || controller.signal.aborted) return undefined;
2356
+ if (side.loadEpoch !== epoch || side.sidecarKey !== key || controller.signal.aborted) return undefined;
828
2357
  side.sidecar = document;
829
2358
  if (selectedInspection?.side === side) renderInspection();
830
2359
  return document;
@@ -862,14 +2391,19 @@ function renderTileflowComparisonHtml(options) {
862
2391
  return configured.href;
863
2392
  }
864
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");
865
2397
  return configured.href;
866
2398
  }
867
2399
 
868
- function renderInspection() {
2400
+ // Metadata refreshes update the inspection without navigating away from the active tab.
2401
+ function renderInspection(reveal = false) {
869
2402
  if (!selectedInspection) return;
870
2403
  const side = selectedInspection.side;
871
2404
  const feature = selectedInspection.features[selectedInspection.featureIndex];
872
2405
  inspectionOutput.replaceChildren();
2406
+ for (const entry of Object.values(states)) entry.highlightFeature?.(entry === side ? selectedInspection.featureIndex : -1);
873
2407
  if (!feature) {
874
2408
  inspectionOutput.className = "empty";
875
2409
  inspectionOutput.textContent = side.definition.label + ": no rendered feature at this point.";
@@ -877,6 +2411,7 @@ function renderTileflowComparisonHtml(options) {
877
2411
  curveOutput.className = "empty";
878
2412
  curveOutput.textContent = "Paint and layout values appear here.";
879
2413
  sidecarOutput.textContent = "No layer selected.";
2414
+ if (reveal) globalThis.__tileflowDevtoolsUI?.selectionChanged();
880
2415
  return;
881
2416
  }
882
2417
  inspectionOutput.className = "";
@@ -885,20 +2420,20 @@ function renderTileflowComparisonHtml(options) {
885
2420
  title.textContent = side.definition.label + " \xB7 " + feature.layerId;
886
2421
  const pickerLabel = document.createElement("label");
887
2422
  pickerLabel.className = "feature-picker";
888
- pickerLabel.append(document.createTextNode("Rendered feature (" + selectedInspection.features.length + ")"));
2423
+ pickerLabel.append(document.createTextNode("Elements at this point (" + selectedInspection.features.length + ")"));
889
2424
  const picker = document.createElement("select");
890
2425
  picker.setAttribute("aria-label", "Rendered feature at inspected point");
891
2426
  selectedInspection.features.forEach((candidate, index) => {
892
2427
  const option = document.createElement("option");
893
2428
  option.value = String(index);
894
- option.textContent = String(index + 1) + ". " + candidate.layerId + " \xB7 " + candidate.geometryType +
895
- (candidate.id === null ? "" : " \xB7 " + String(candidate.id));
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;
896
2431
  picker.append(option);
897
2432
  });
898
2433
  picker.value = String(selectedInspection.featureIndex);
899
2434
  picker.addEventListener("change", () => {
900
2435
  selectedInspection.featureIndex = boundedInteger(Number(picker.value), 0, selectedInspection.features.length - 1, 0);
901
- renderInspection();
2436
+ renderInspection(true);
902
2437
  });
903
2438
  pickerLabel.append(picker);
904
2439
  const summary = document.createElement("pre");
@@ -915,13 +2450,15 @@ function renderTileflowComparisonHtml(options) {
915
2450
  filter: layer.filter
916
2451
  } : undefined
917
2452
  });
918
- inspectionOutput.append(title, pickerLabel, summary);
2453
+ if (options.workbenchUrl) renderFeatureSummary(side, feature, layer, pickerLabel, summary);
2454
+ else inspectionOutput.append(title, pickerLabel, summary);
919
2455
  populateCurveProperties(layer);
920
2456
  const sidecarLayer = findSidecarLayer(side.sidecar, feature.layerId);
921
2457
  sidecarOutput.textContent = sidecarLayer
922
2458
  ? boundedJson(sidecarLayerWithSemanticAttribution(sidecarLayer))
923
2459
  : "No compiler sidecar entry for this layer.";
924
2460
  if (side.sidecar === undefined && !side.sidecarPromise) void loadSidecar(side);
2461
+ if (reveal) globalThis.__tileflowDevtoolsUI?.selectionChanged();
925
2462
  }
926
2463
 
927
2464
  function populateCurveProperties(layer) {
@@ -1199,103 +2736,14 @@ function renderTileflowComparisonHtml(options) {
1199
2736
  value.x + value.width <= image.naturalWidth && value.y + value.height <= image.naturalHeight;
1200
2737
  }
1201
2738
 
1202
- async function copyScene(commandOnly) {
1203
- const name = sceneNameInput.value;
1204
- if (!validPortableName(name)) {
1205
- setFeedback("Use a portable kebab-case scene name.");
1206
- return;
1207
- }
1208
- const side = states[activeSideId];
1209
- const camera = side.camera || canonicalCamera;
1210
- if (!camera) {
1211
- setFeedback("Wait for the maps to become ready.");
1212
- return;
1213
- }
1214
- const style = side.map?.getStyle?.();
1215
- const map = style?.metadata?.["tileflow:map"];
1216
- const theme = style?.metadata?.["tileflow:theme"];
1217
- if (!validPortableName(map) || !validConcreteThemeName(theme)) {
1218
- setFeedback("The active preview does not expose a concrete map and theme.");
1219
- return;
1220
- }
1221
- const dpr = Number(sceneDprInput.value) === 2 ? 2 : 1;
1222
- const viewport = readMapViewport(side, dpr);
1223
- const {width, height} = viewport;
1224
- if (commandOnly) {
1225
- const command = [
1226
- "tileflow capture",
1227
- ...(side.definition.captureConfig
1228
- ? ["--config " + quoteCliArgument(side.definition.captureConfig)]
1229
- : []),
1230
- "--map " + quoteCliArgument(map),
1231
- "--theme " + quoteCliArgument(theme),
1232
- "--center=" + formatNumber(camera.center[0]) + "," + formatNumber(camera.center[1]),
1233
- "--zoom=" + formatNumber(camera.zoom),
1234
- "--bearing=" + formatNumber(camera.bearing),
1235
- "--pitch=" + formatNumber(camera.pitch),
1236
- "--width=" + width,
1237
- "--height=" + height,
1238
- "--dpr=" + dpr,
1239
- "--out=" + quoteCliArgument(name + ".png")
1240
- ].join(" ");
1241
- if (!await copyText(command)) {
1242
- setFeedback("Clipboard access is unavailable.");
1243
- return;
1244
- }
1245
- setFeedback("Exploratory capture command copied.");
1246
- return;
1247
- }
1248
- const definition = {
1249
- theme,
1250
- camera: {
1251
- type: "center",
1252
- center: [roundNumber(camera.center[0]), roundNumber(camera.center[1])],
1253
- zoom: roundNumber(camera.zoom),
1254
- bearing: roundNumber(camera.bearing),
1255
- pitch: roundNumber(camera.pitch)
1256
- },
1257
- viewport: {width, height, dpr},
1258
- target: {kind: "map"}
1259
- };
1260
- const serialized = JSON.stringify(definition, null, 2).replace(/^/gm, " ");
1261
- if (!await copyText("'" + name + "': " + serialized.trimStart() + ",")) {
1262
- setFeedback("Clipboard access is unavailable.");
1263
- return;
1264
- }
1265
- setFeedback("Scene entry copied.");
1266
- }
1267
2739
 
1268
- function readMapViewport(side, dpr) {
1269
- const container = side.map?.getContainer?.();
1270
- const canvas = side.map?.getCanvas?.();
1271
- const maximum = dpr === 2 ? 2048 : 4096;
1272
- const width = boundedInteger(container?.clientWidth || canvas?.clientWidth, 64, maximum, 1280);
1273
- const height = boundedInteger(container?.clientHeight || canvas?.clientHeight, 64, maximum, 800);
1274
- return {width, height};
1275
- }
1276
2740
 
1277
- async function copyText(value) {
1278
- try {
1279
- await navigator.clipboard.writeText(value);
1280
- return true;
1281
- } catch {
1282
- const textarea = document.createElement("textarea");
1283
- textarea.value = value;
1284
- textarea.style.position = "fixed";
1285
- textarea.style.opacity = "0";
1286
- document.body.append(textarea);
1287
- textarea.select();
1288
- const copied = document.execCommand("copy");
1289
- textarea.remove();
1290
- return copied;
1291
- }
1292
- }
1293
2741
 
1294
2742
  function writeUrlState() {
1295
2743
  const url = new URL(location.href);
1296
2744
  url.searchParams.set("mode", stage.dataset.mode);
1297
- url.searchParams.set("split", String(Math.round(Number(splitInput.value))));
1298
- url.searchParams.set("alpha", String(Math.round(Number(alphaInput.value))));
2745
+ url.searchParams.set("split", String(Math.round(initialUi.split)));
2746
+ url.searchParams.set("alpha", String(Math.round(initialUi.alpha)));
1299
2747
  url.searchParams.set("driver", stage.dataset.driver);
1300
2748
  if (canonicalCamera) {
1301
2749
  const values = {
@@ -1310,9 +2758,9 @@ function renderTileflowComparisonHtml(options) {
1310
2758
  function readUiFromUrl() {
1311
2759
  const query = new URL(location.href).searchParams;
1312
2760
  const modeValue = singleQueryValue(query, "mode");
1313
- const mode = ["side-by-side", "split", "overlay", "blink"].includes(modeValue) ? modeValue : undefined;
2761
+ const mode = ["single", "side-by-side", "split", "overlay", "blink"].includes(modeValue) ? modeValue : undefined;
1314
2762
  const driver = singleQueryValue(query, "driver");
1315
- selectSide(driver === "right" ? "right" : "left", false);
2763
+ selectSide(driver === "right" && options.right ? "right" : "left", false);
1316
2764
  const split = singleQueryValue(query, "split");
1317
2765
  const alpha = singleQueryValue(query, "alpha");
1318
2766
  return {
@@ -1414,7 +2862,6 @@ function renderTileflowComparisonHtml(options) {
1414
2862
  if (!side.runtimeErrors.includes(message)) side.runtimeErrors.push(message);
1415
2863
  side.runtimeErrors = side.runtimeErrors.slice(-16);
1416
2864
  side.runtimeFailed = true;
1417
- renderSideStatus(side);
1418
2865
  updateDocumentReadiness();
1419
2866
  }
1420
2867
 
@@ -1482,8 +2929,32 @@ function renderTileflowComparisonHtml(options) {
1482
2929
  function roundNumber(value) { return Number(value.toFixed(6)); }
1483
2930
  function formatNumber(value) { return String(roundNumber(value)); }
1484
2931
  function wrapLongitude(value) { return ((value + 180) % 360 + 360) % 360 - 180; }
1485
- function setFeedback(value) { feedback.textContent = value; setTimeout(() => { if (feedback.textContent === value) feedback.textContent = ""; }, 2500); }
2932
+ function setFeedback(value) { globalThis.__tileflowDevtoolsUI?.feedback(value); }
2933
+ ${controlsClient}
2934
+ ${workbenchClient}
2935
+ globalThis.__tileflowDevtoolsHost = {
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
+ };
1486
2956
  </script>
2957
+ <script type="module"${nonceAttribute} src="${fontBasePath}/__workbench-runtime/devtools.js"></script>
1487
2958
  </body>
1488
2959
  </html>`;
1489
2960
  }
@@ -1515,15 +2986,56 @@ var tileflowComparisonSchemaVersion = 1;
1515
2986
  function createTileflowComparisonRequestHandler(options) {
1516
2987
  const comparisonBasePath = normalizeTileflowBasePath(options.basePath);
1517
2988
  const left = normalizeSide(options.left, "left", comparisonBasePath);
1518
- const right = normalizeSide(options.right, "right", comparisonBasePath);
1519
- assertDisjointSideRoutes(left.basePath, right.basePath);
1520
- const title = normalizeLabel(options.title ?? "Tileflow comparison", "title");
1521
- const initialMode = options.initialMode ?? "side-by-side";
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
+ "Map DevTools 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 Map DevTools", "title");
3003
+ const initialMode = right ? options.initialMode ?? "side-by-side" : "single";
1522
3004
  return async function handleTileflowComparisonRequest(request) {
1523
3005
  const url = new URL(request.url);
1524
3006
  const pathname = url.pathname;
3007
+ if (pathname === `${comparisonBasePath}/__workbench-runtime/devtools.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/devtools-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);
1525
3037
  if (matchesBasePath(pathname, left.basePath)) return await left.handler(request);
1526
- if (matchesBasePath(pathname, right.basePath)) return await right.handler(request);
3038
+ if (right && matchesBasePath(pathname, right.basePath)) return await right.handler(request);
1527
3039
  if (isComparisonRoot(pathname, comparisonBasePath)) {
1528
3040
  if (request.method !== "GET" && request.method !== "HEAD") {
1529
3041
  return jsonResponse({ error: "Method not allowed" }, 405);
@@ -1534,9 +3046,10 @@ function createTileflowComparisonRequestHandler(options) {
1534
3046
  cspNonce,
1535
3047
  initialMode,
1536
3048
  left: publicSide(left),
1537
- right: publicSide(right),
3049
+ ...right ? { right: publicSide(right) } : {},
1538
3050
  schemaVersion: tileflowComparisonSchemaVersion,
1539
- title
3051
+ title,
3052
+ ...workbenchUrl ? { workbenchUrl } : {}
1540
3053
  });
1541
3054
  return new Response(request.method === "HEAD" ? null : html, {
1542
3055
  headers: {
@@ -1572,6 +3085,7 @@ function normalizeSide(side, name, comparisonBasePath) {
1572
3085
  if (!basePath) {
1573
3086
  throw new TypeError(`Tileflow comparison ${name} basePath must not own the server root.`);
1574
3087
  }
3088
+ assertDisjointSideRoutes(basePath, `${comparisonBasePath}/__workbench-runtime`);
1575
3089
  if (comparisonBasePath && !matchesBasePath(basePath, comparisonBasePath)) {
1576
3090
  throw new TypeError(
1577
3091
  `Tileflow comparison ${name} basePath must be inside ${comparisonBasePath}.`
@@ -1724,6 +3238,8 @@ function createTileflowDevRequestHandler(options = {}) {
1724
3238
  const lineBackgroundGlyphs = createTileflowLineBackgroundGlyphResponder({ cwd });
1725
3239
  let closed = false;
1726
3240
  let lastOrigin;
3241
+ let controlsGeneration;
3242
+ const controlsCache = /* @__PURE__ */ new Map();
1727
3243
  const loadOwnedSession = (origin) => {
1728
3244
  lastOrigin = origin;
1729
3245
  ownedSessionPromise ??= createTileflowArtifactSession({
@@ -1755,13 +3271,30 @@ function createTileflowDevRequestHandler(options = {}) {
1755
3271
  return options.session ? eventStreamResponse(options.session) : jsonResponse2({ error: "Live events require a development session." }, 404);
1756
3272
  }
1757
3273
  if (path.startsWith("/__runtime/")) {
1758
- const runtimeResponse = (await import("./preview-assets-MVUJJ5XR.js")).getTileflowPreviewRuntimeResponse(path);
3274
+ const runtimeResponse = (await import("./preview-assets-KGUICQHG.js")).getTileflowPreviewRuntimeResponse(path);
1759
3275
  if (runtimeResponse) return runtimeResponse;
1760
3276
  }
1761
3277
  const session = options.session ?? await loadOwnedSession(url.origin);
1762
3278
  acquisition = session.acquireArtifacts();
1763
3279
  const artifacts = acquisition?.artifacts;
1764
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
+ };
1765
3298
  if (path === "/__status") {
1766
3299
  return jsonResponse2(createTileflowArtifactStatus(state));
1767
3300
  }
@@ -1769,20 +3302,24 @@ function createTileflowDevRequestHandler(options = {}) {
1769
3302
  const { renderTileflowPreviewHtml, resolveTileflowPreview } = await import("./preview.js");
1770
3303
  const requestedMaps = url.searchParams.getAll("map");
1771
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
+ }
1772
3309
  if (requestedMaps.length > 1) {
1773
3310
  return jsonResponse2({ error: "Tileflow map query must appear at most once." }, 400);
1774
3311
  }
1775
3312
  if (requestedThemes.length > 1) {
1776
3313
  return jsonResponse2({ error: "Tileflow theme query must appear at most once." }, 400);
1777
3314
  }
1778
- const requestedMap = requestedMaps.length === 1 ? requestedMaps[0] : options.map;
1779
- const requestedTheme = requestedThemes.length === 1 ? requestedThemes[0] : options.theme;
3315
+ const requestedMap = requestedMaps[0] ?? (requestedScenes.length ? void 0 : options.map);
3316
+ const requestedTheme = requestedThemes[0] ?? (requestedScenes.length ? void 0 : options.theme);
1780
3317
  const preview = artifacts ? resolveTileflowPreview(artifacts.project, {
1781
3318
  map: requestedMap,
1782
- scene: options.scene,
3319
+ scene: requestedScenes[0] ?? (requestedMaps.length ? void 0 : options.scene),
1783
3320
  theme: requestedTheme
1784
3321
  }) : void 0;
1785
- const previewStyle = preview ? artifacts?.styles[preview.mapName]?.[preview.themeName] : void 0;
3322
+ const previewStyle = preview ? controlled(preview.mapName, preview.themeName)?.style ?? artifacts?.styles[preview.mapName]?.[preview.themeName] : void 0;
1786
3323
  const isSemanticPreview = preview !== void 0 && previewStyle?.metadata?.["tileflow:compiler"] === "tileflow-semantic";
1787
3324
  const fontFaces = previewStyle ? getTileflowStyleFontFaces(previewStyle) : [];
1788
3325
  return htmlResponse(
@@ -1791,11 +3328,23 @@ function createTileflowDevRequestHandler(options = {}) {
1791
3328
  basePath,
1792
3329
  createTileflowArtifactStatus(state),
1793
3330
  isSemanticPreview,
1794
- fontFaces
3331
+ fontFaces,
3332
+ options.fitViewport,
3333
+ controls ?? void 0
1795
3334
  )
1796
3335
  );
1797
3336
  }
1798
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
+ }
1799
3348
  if (path.startsWith(`/${tileflowLineBackgroundGlyphRoute}/`)) {
1800
3349
  const glyphRequest = parseTileflowLineBackgroundGlyphPath(path);
1801
3350
  const upstream = glyphRequest ? artifacts.lineBackgroundGlyphs?.[glyphRequest.mapName] : void 0;
@@ -1824,7 +3373,7 @@ function createTileflowDevRequestHandler(options = {}) {
1824
3373
  }
1825
3374
  if (path.startsWith("/__inspection/")) {
1826
3375
  const selection = getTileflowStyleInspectionSelection(path);
1827
- const inspection = selection ? artifacts.styleInspections?.[selection.mapName]?.[selection.themeName] : void 0;
3376
+ const inspection = selection ? controlled(selection.mapName, selection.themeName)?.inspection ?? artifacts.styleInspections?.[selection.mapName]?.[selection.themeName] : void 0;
1828
3377
  if (!selection || !inspection) {
1829
3378
  return jsonResponse2({ error: "Compiler inspection is unavailable." }, 404);
1830
3379
  }
@@ -1832,7 +3381,7 @@ function createTileflowDevRequestHandler(options = {}) {
1832
3381
  }
1833
3382
  if (path.startsWith("/styles/")) {
1834
3383
  const selection = getTileflowStyleSelection(path);
1835
- const style = selection ? artifacts.styles[selection.mapName]?.[selection.themeName] : void 0;
3384
+ const style = selection ? controlled(selection.mapName, selection.themeName)?.style ?? artifacts.styles[selection.mapName]?.[selection.themeName] : void 0;
1836
3385
  if (!selection || !style) {
1837
3386
  return jsonResponse2({ error: "Unknown map theme style." }, 404);
1838
3387
  }
@@ -1854,9 +3403,23 @@ function createTileflowDevRequestHandler(options = {}) {
1854
3403
  }
1855
3404
  return assetResponse(asset);
1856
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
+ }
1857
3414
  return jsonResponse2({ error: "Not found" }, 404);
1858
3415
  } catch (error) {
1859
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
+ }
1860
3423
  return tileflowErrorResponse(error);
1861
3424
  } finally {
1862
3425
  await acquisition?.release();
@@ -2004,7 +3567,7 @@ function getRequestPathname(url) {
2004
3567
  }
2005
3568
  }
2006
3569
  function isOwnedTileflowRequestPath(path) {
2007
- return path === "/" || path === "/build-manifest.json" || path === "/manifest.json" || path === "/__events" || path === "/__status" || getTileflowStyleInspectionSelection(path) !== void 0 || path.startsWith("/generations/") || path.startsWith("/icons/") || path.startsWith("/tilesets/") || getTileflowStyleSelection(path) !== void 0 || path.startsWith("/fonts/") || path.startsWith(`/${tileflowLineBackgroundGlyphRoute}/`) || path.startsWith("/__runtime/");
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/");
2008
3571
  }
2009
3572
  function tileflowErrorResponse(error) {
2010
3573
  if (error instanceof Error && "code" in error && error.code === "PREVIEW_SELECTION_INVALID") {