@tileflow/core 0.1.0-alpha.44 → 0.1.0-alpha.46

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/runtime.js CHANGED
@@ -86,13 +86,2398 @@ var init_time_of_day = __esm({
86
86
  }
87
87
  });
88
88
 
89
+ // src/cartography/expression-operators.ts
90
+ function isMapLibreExpressionOperator(value) {
91
+ return typeof value === "string" && mapLibreExpressionOperators.has(value);
92
+ }
93
+ var mapLibreExpressionOperators;
94
+ var init_expression_operators = __esm({
95
+ "src/cartography/expression-operators.ts"() {
96
+ "use strict";
97
+ mapLibreExpressionOperators = /* @__PURE__ */ new Set([
98
+ "!",
99
+ "!=",
100
+ "%",
101
+ "*",
102
+ "+",
103
+ "-",
104
+ "/",
105
+ "<",
106
+ "<=",
107
+ "==",
108
+ ">",
109
+ ">=",
110
+ "^",
111
+ "abs",
112
+ "accumulated",
113
+ "acos",
114
+ "all",
115
+ "any",
116
+ "array",
117
+ "asin",
118
+ "at",
119
+ "atan",
120
+ "boolean",
121
+ "case",
122
+ "ceil",
123
+ "coalesce",
124
+ "collator",
125
+ "concat",
126
+ "cos",
127
+ "distance",
128
+ "downcase",
129
+ "e",
130
+ "elevation",
131
+ "error",
132
+ "feature-state",
133
+ "filter-<",
134
+ "filter-<=",
135
+ "filter-==",
136
+ "filter->",
137
+ "filter->=",
138
+ "filter-has",
139
+ "filter-has-id",
140
+ "filter-id-<",
141
+ "filter-id-<=",
142
+ "filter-id-==",
143
+ "filter-id->",
144
+ "filter-id->=",
145
+ "filter-id-in",
146
+ "filter-in-large",
147
+ "filter-in-small",
148
+ "filter-type-==",
149
+ "filter-type-in",
150
+ "floor",
151
+ "format",
152
+ "geometry-type",
153
+ "get",
154
+ "global-state",
155
+ "has",
156
+ "heatmap-density",
157
+ "id",
158
+ "image",
159
+ "in",
160
+ "index-of",
161
+ "interpolate",
162
+ "interpolate-hcl",
163
+ "interpolate-lab",
164
+ "is-supported-script",
165
+ "join",
166
+ "length",
167
+ "let",
168
+ "line-progress",
169
+ "literal",
170
+ "ln",
171
+ "ln2",
172
+ "log10",
173
+ "log2",
174
+ "match",
175
+ "max",
176
+ "min",
177
+ "number",
178
+ "number-format",
179
+ "object",
180
+ "pi",
181
+ "properties",
182
+ "resolved-locale",
183
+ "rgb",
184
+ "rgba",
185
+ "round",
186
+ "sin",
187
+ "slice",
188
+ "split",
189
+ "sqrt",
190
+ "step",
191
+ "string",
192
+ "tan",
193
+ "to-boolean",
194
+ "to-color",
195
+ "to-number",
196
+ "to-rgba",
197
+ "to-string",
198
+ "typeof",
199
+ "upcase",
200
+ "var",
201
+ "within",
202
+ "zoom"
203
+ ]);
204
+ }
205
+ });
206
+
207
+ // src/cartography/values.ts
208
+ function isTileflowThemeTokenReference(value) {
209
+ return isRecord(value) && hasExactOwnKeys(value, ["category", "kind", "token"]) && value.kind === "theme-token" && typeof value.token === "string" && /^[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z][A-Za-z0-9_-]*)*$/.test(value.token) && tileflowThemeTokenCategories.includes(value.category);
210
+ }
211
+ function isTileflowFixedValue(value) {
212
+ return isRecord(value) && hasExactOwnKeys(value, ["kind", "reason", "value"]) && value.kind === "theme-fixed" && typeof value.reason === "string" && Boolean(value.reason.trim()) && Object.hasOwn(value, "value");
213
+ }
214
+ function isTileflowThemeColorOperation(value) {
215
+ if (!isRecord(value) || value.kind !== "theme-color") return false;
216
+ if (value.operation === "alpha") {
217
+ return hasExactOwnKeys(value, ["color", "kind", "opacity", "operation"]) && Object.hasOwn(value, "color") && Object.hasOwn(value, "opacity");
218
+ }
219
+ return value.operation === "mix" && hasExactOwnKeys(value, ["amount", "from", "kind", "operation", "space", "to"]) && value.space === "oklch" && Object.hasOwn(value, "amount") && Object.hasOwn(value, "from") && Object.hasOwn(value, "to");
220
+ }
221
+ function isRecord(value) {
222
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
223
+ }
224
+ function hasExactOwnKeys(value, expectedKeys) {
225
+ const ownKeys = Reflect.ownKeys(value);
226
+ return ownKeys.length === expectedKeys.length && ownKeys.every((key) => typeof key === "string" && expectedKeys.includes(key));
227
+ }
228
+ var tileflowThemeTokenCategories;
229
+ var init_values = __esm({
230
+ "src/cartography/values.ts"() {
231
+ "use strict";
232
+ tileflowThemeTokenCategories = Object.freeze([
233
+ "color",
234
+ "font",
235
+ "image",
236
+ "number"
237
+ ]);
238
+ }
239
+ });
240
+
241
+ // src/maps/operations.ts
242
+ function isTileflowReset(value) {
243
+ if (!isPlainRecord(value)) return false;
244
+ return value.$tileflow === "reset" && Object.keys(value).length === 1;
245
+ }
246
+ function deepFreeze(value) {
247
+ if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
248
+ Object.freeze(value);
249
+ for (const child of Object.values(value)) deepFreeze(child);
250
+ return value;
251
+ }
252
+ function isPlainRecord(value) {
253
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
254
+ const prototype = Object.getPrototypeOf(value);
255
+ return prototype === Object.prototype || prototype === null;
256
+ }
257
+ var resetValue, disableValue;
258
+ var init_operations = __esm({
259
+ "src/maps/operations.ts"() {
260
+ "use strict";
261
+ resetValue = deepFreeze({ $tileflow: "reset" });
262
+ disableValue = deepFreeze({ op: "disable" });
263
+ }
264
+ });
265
+
266
+ // src/theme-blend.ts
267
+ function planTileflowThemeBlend(styles, options = {}) {
268
+ if (styles.length < 2)
269
+ throw new TileflowThemeBlendError("A theme blend needs two or more themes.");
270
+ const [base] = styles;
271
+ assertSameStructure(styles, base);
272
+ const mixedImages = new Set(imageProperties);
273
+ if (options.iconImages === "switch") mixedImages.delete("icon-image");
274
+ const paints = [];
275
+ const switches = [];
276
+ const imageGroups = collectImageNames(styles, base, mixedImages);
277
+ const symbolLayers = /* @__PURE__ */ new Set();
278
+ const images = /* @__PURE__ */ new Map();
279
+ let splitLayers = 0;
280
+ let copiedLayers = 0;
281
+ let switchedLayers = 0;
282
+ const layers = [];
283
+ for (const [index, layer] of base.layers.entries()) {
284
+ const variants = styles.map((style) => style.layers[index]);
285
+ layers.push(...planLayer(variants, String(layer.type)));
286
+ }
287
+ const switchesByLayer = /* @__PURE__ */ new Map();
288
+ for (const entry of switches) {
289
+ const list = switchesByLayer.get(entry.layer) ?? [];
290
+ list.push(entry);
291
+ switchesByLayer.set(entry.layer, list);
292
+ }
293
+ const sky = differingRoot(styles, "sky");
294
+ const light = differingRoot(styles, "light");
295
+ const metadata = alignInteractionManifest(base.metadata, layers);
296
+ return {
297
+ themes: styles.length,
298
+ paints,
299
+ images: [...images.values()],
300
+ switches,
301
+ ...sky === void 0 ? {} : { sky },
302
+ ...light === void 0 ? {} : { light },
303
+ summary: { splitLayers, copiedLayers, switchedLayers, switchedValues: switches.length },
304
+ styleAt(position) {
305
+ const at = clampPosition(position, styles.length);
306
+ const dominant = Math.round(at);
307
+ const style = {
308
+ ...base,
309
+ ...metadata === void 0 ? {} : { metadata },
310
+ // Symbols show the nearest theme, as the label cross-fade leaves them between changes.
311
+ layers: layers.map(
312
+ (layer) => fillLayer(
313
+ layer,
314
+ symbolLayers.has(String(layer.id)) ? dominant : at,
315
+ dominant,
316
+ switchesByLayer.get(String(layer.id))
317
+ )
318
+ )
319
+ };
320
+ if (sky !== void 0) style.sky = fillTemplate(sky, at);
321
+ if (light !== void 0) style.light = fillTemplate(light, at);
322
+ return style;
323
+ }
324
+ };
325
+ function planLayer(variants, type) {
326
+ const [layer] = variants;
327
+ const id = String(layer.id);
328
+ const paint = { ...layer.paint };
329
+ const layout = { ...layer.layout };
330
+ const follows = type === "symbol" ? "dominant" : "position";
331
+ const ownPaints = [];
332
+ const ownSwitches = [];
333
+ const driven = [];
334
+ const paintOf = (variant) => variant.paint ?? {};
335
+ for (const group of ["layout", "paint"]) {
336
+ const target = group === "paint" ? paint : layout;
337
+ for (const property of propertyNames(variants, group)) {
338
+ const values = variants.map(
339
+ (variant) => variant[group]?.[property]
340
+ );
341
+ if (values.every((value) => same2(value, values[0]))) continue;
342
+ if (mixedImages.has(property)) {
343
+ const tuples = imageTuples(values);
344
+ if (tuples && tuples.every((names) => imageGroups.get(names[0])?.size === 1)) {
345
+ for (const names of tuples) {
346
+ if (names.every((name) => name === names[0])) continue;
347
+ images.set(JSON.stringify(names), {
348
+ names,
349
+ follows: property === "icon-image" ? "dominant" : "position"
350
+ });
351
+ }
352
+ continue;
353
+ }
354
+ }
355
+ if (group === "layout" || values.includes(void 0) || imageProperties.has(property)) {
356
+ ownSwitches.push({ group, property, values });
357
+ continue;
358
+ }
359
+ if (values.some(isDataDriven)) {
360
+ driven.push(property);
361
+ continue;
362
+ }
363
+ const template = tryMixTemplate(values);
364
+ if (template === void 0) {
365
+ ownSwitches.push({ group, property, values });
366
+ continue;
367
+ }
368
+ ownPaints.push({ property, template });
369
+ target[property] = template;
370
+ }
371
+ }
372
+ const planned = { ...layer, paint, layout };
373
+ let results = [{ layer: planned, extra: [] }];
374
+ if (driven.length > 0) {
375
+ const split = type === "symbol" ? void 0 : splitLayer();
376
+ const copies = split ?? (type === "symbol" ? void 0 : copyLayer());
377
+ if (copies) results = copies;
378
+ else
379
+ for (const property of driven)
380
+ ownSwitches.push({
381
+ group: "paint",
382
+ property,
383
+ values: variants.map((variant) => paintOf(variant)[property])
384
+ });
385
+ }
386
+ if (options.featureSwitches === "layers" && results.length === 1 && ownSwitches.some((entry) => entry.values.some(isDataDriven)))
387
+ results = switchLayer();
388
+ for (const result of results) {
389
+ const resultId = String(result.layer.id);
390
+ if (type === "symbol") symbolLayers.add(resultId);
391
+ const overridden = new Set(result.extra.map((entry) => entry.property));
392
+ for (const entry of [
393
+ ...ownPaints.filter((own) => !overridden.has(own.property)),
394
+ ...result.extra
395
+ ])
396
+ paints.push({ layer: resultId, property: entry.property, template: entry.template, follows });
397
+ if (result.visibility)
398
+ switches.push({
399
+ layer: resultId,
400
+ group: "layout",
401
+ property: "visibility",
402
+ values: result.visibility
403
+ });
404
+ else for (const entry of ownSwitches) switches.push({ layer: resultId, ...entry });
405
+ }
406
+ return results.map((result) => result.layer);
407
+ function switchLayer() {
408
+ switchedLayers += variants.length;
409
+ return variants.map((_, theme) => {
410
+ const copyPaint = { ...paint };
411
+ const copyLayout = { ...layout };
412
+ for (const entry of ownSwitches) {
413
+ const target = entry.group === "paint" ? copyPaint : copyLayout;
414
+ const value = entry.values[theme];
415
+ if (value === void 0) delete target[entry.property];
416
+ else target[entry.property] = value;
417
+ }
418
+ const shown = copyLayout.visibility === "none" ? "none" : "visible";
419
+ return {
420
+ layer: { ...planned, id: `${id}::theme-${theme}`, paint: copyPaint, layout: copyLayout },
421
+ extra: [],
422
+ visibility: variants.map((__, nearest) => nearest === theme ? shown : "none")
423
+ };
424
+ });
425
+ }
426
+ function splitLayer() {
427
+ if (isLegacyFilter(layer.filter)) return void 0;
428
+ const values = driven.map(
429
+ (property) => [
430
+ property,
431
+ variants.map((variant) => inlineBranchBindings(paintOf(variant)[property]))
432
+ ]
433
+ );
434
+ if (values.some(([, entries]) => entries.some((entry) => !entry))) return void 0;
435
+ const branches = splitBranches(
436
+ Object.fromEntries(
437
+ values.map(([property, entries]) => [property, entries.map((entry) => entry.value)])
438
+ )
439
+ );
440
+ if (!branches) return void 0;
441
+ const groups = /* @__PURE__ */ new Map();
442
+ for (const branch of branches) {
443
+ const key = JSON.stringify(branch.valuesByProperty);
444
+ let group = groups.get(key);
445
+ if (!group) {
446
+ const templates = {};
447
+ for (const [property, values2] of Object.entries(branch.valuesByProperty)) {
448
+ const template = tryMixTemplate(values2);
449
+ if (template === void 0) return void 0;
450
+ templates[property] = template;
451
+ }
452
+ group = { templates, conditions: [] };
453
+ groups.set(key, group);
454
+ }
455
+ group.conditions.push(
456
+ branch.conditions.length === 1 ? branch.conditions[0] : ["all", ...branch.conditions]
457
+ );
458
+ }
459
+ splitLayers += groups.size;
460
+ return [...groups.values()].map((group, number) => {
461
+ const chosen = group.conditions.length === 1 ? group.conditions[0] : ["any", ...group.conditions];
462
+ return {
463
+ layer: {
464
+ ...planned,
465
+ id: `${id}::branch-${number}`,
466
+ filter: layer.filter === void 0 ? chosen : ["all", layer.filter, chosen],
467
+ paint: { ...paint, ...group.templates }
468
+ },
469
+ extra: Object.entries(group.templates).filter(([, template]) => containsLeaf(template)).map(([property, template]) => ({ property, template }))
470
+ };
471
+ });
472
+ }
473
+ function copyLayer() {
474
+ const opacity = opacityProperties[type];
475
+ if (!opacity || variants.some((variant) => isDataDriven(paintOf(variant)[opacity])))
476
+ return void 0;
477
+ copiedLayers += variants.length;
478
+ return variants.map((variant, theme) => {
479
+ const weight = new BlendLeaf(variants.map((_, other) => other === theme ? 1 : 0));
480
+ const template = scaleByWeight(paint[opacity] ?? 1, weight);
481
+ const copyPaint = { ...paint, [opacity]: template };
482
+ for (const property of driven) copyPaint[property] = paintOf(variant)[property];
483
+ return {
484
+ layer: { ...planned, id: `${id}::theme-${theme}`, paint: copyPaint },
485
+ extra: [{ property: opacity, template }]
486
+ };
487
+ });
488
+ }
489
+ }
490
+ }
491
+ function fillLayer(layer, position, dominant, layerSwitches) {
492
+ const filled = { ...layer };
493
+ for (const group of ["layout", "paint"]) {
494
+ const values = layer[group];
495
+ if (!values) continue;
496
+ const out = {};
497
+ for (const [key, value] of Object.entries(values)) out[key] = fillTemplate(value, position);
498
+ for (const entry of layerSwitches ?? []) {
499
+ if (entry.group !== group) continue;
500
+ const value = entry.values[dominant];
501
+ if (value === void 0) delete out[entry.property];
502
+ else out[entry.property] = value;
503
+ }
504
+ if (Object.keys(out).length > 0) filled[group] = out;
505
+ else delete filled[group];
506
+ }
507
+ return filled;
508
+ }
509
+ function fillTemplate(node, position) {
510
+ if (node instanceof BlendLeaf) return mixLeaf(node.values, position);
511
+ if (Array.isArray(node)) return node.map((item) => fillTemplate(item, position));
512
+ if (node && typeof node === "object" && containsLeaf(node)) {
513
+ return Object.fromEntries(
514
+ Object.entries(node).map(([key, value]) => [key, fillTemplate(value, position)])
515
+ );
516
+ }
517
+ return node;
518
+ }
519
+ function containsLeaf(node) {
520
+ if (node instanceof BlendLeaf) return true;
521
+ if (Array.isArray(node)) return node.some(containsLeaf);
522
+ if (node && typeof node === "object") return Object.values(node).some(containsLeaf);
523
+ return false;
524
+ }
525
+ function mixLeaf(values, position) {
526
+ const at = clampPosition(position, values.length);
527
+ const index = Math.min(values.length - 2, Math.floor(at));
528
+ const t = at - index;
529
+ const from = values[index];
530
+ const to = values[index + 1];
531
+ if (t <= 0 || from === to) return from;
532
+ if (t >= 1) return to;
533
+ if (typeof from === "number" && typeof to === "number") return from + (to - from) * t;
534
+ const a = oklab(String(from));
535
+ const b = oklab(String(to));
536
+ if (!a || !b) return t < 0.5 ? from : to;
537
+ return colourFromOklab(a.map((value, channel) => value + (b[channel] - value) * t));
538
+ }
539
+ function clampPosition(position, themes) {
540
+ if (!Number.isFinite(position)) return 0;
541
+ return Math.min(themes - 1, Math.max(0, position));
542
+ }
543
+ function assertSameStructure(styles, base) {
544
+ const ids = (style) => style.layers.map((layer) => layer.id);
545
+ for (const style of styles) {
546
+ if (!same2(ids(style), ids(base)))
547
+ throw new TileflowThemeBlendError(
548
+ "Blended themes must have the same layers in the same order."
549
+ );
550
+ if (!same2(style.sources, base.sources))
551
+ throw new TileflowThemeBlendError("Blended themes must use the same sources.");
552
+ if (!same2(style.sprite, base.sprite) || !same2(style.glyphs, base.glyphs))
553
+ throw new TileflowThemeBlendError("Blended themes must use the same sprite and glyphs.");
554
+ for (const key of ["projection", "terrain"])
555
+ if (!same2(style[key], base[key]))
556
+ throw new TileflowThemeBlendError(`Blended themes must use the same ${key}.`);
557
+ for (const [index, layer] of style.layers.entries()) {
558
+ const other = base.layers[index];
559
+ for (const key of ["type", "source", "source-layer", "filter", "minzoom", "maxzoom"])
560
+ if (!same2(layer[key], other[key]))
561
+ throw new TileflowThemeBlendError(
562
+ `Blended themes differ in ${key} of layer ${JSON.stringify(layer.id)}.`
563
+ );
564
+ }
565
+ }
566
+ }
567
+ function propertyNames(variants, group) {
568
+ const names = /* @__PURE__ */ new Set();
569
+ for (const variant of variants)
570
+ for (const name of Object.keys(variant[group] ?? {})) names.add(name);
571
+ return [...names];
572
+ }
573
+ function differingRoot(styles, key) {
574
+ const values = styles.map((style) => style[key]);
575
+ if (values.every((value) => same2(value, values[0]))) return void 0;
576
+ return tryMixTemplate(values);
577
+ }
578
+ function collectImageNames(styles, base, properties) {
579
+ const uses = /* @__PURE__ */ new Map();
580
+ for (const [index] of base.layers.entries()) {
581
+ for (const group of ["layout", "paint"]) {
582
+ for (const property of properties) {
583
+ const values = styles.map(
584
+ (style) => style.layers[index]?.[group]?.[property]
585
+ );
586
+ if (values[0] === void 0) continue;
587
+ for (const names of imageTuples(values) ?? []) {
588
+ const set = uses.get(names[0]) ?? /* @__PURE__ */ new Set();
589
+ set.add(JSON.stringify(names));
590
+ uses.set(names[0], set);
591
+ }
592
+ }
593
+ }
594
+ }
595
+ return uses;
596
+ }
597
+ function alignInteractionManifest(metadata, layers) {
598
+ if (!isRecord2(metadata)) return void 0;
599
+ const planned = /* @__PURE__ */ new Map();
600
+ for (const [index, layer] of layers.entries()) {
601
+ const id = String(layer.id);
602
+ const original = /^(.+)::(?:branch|theme)-\d+$/u.exec(id)?.[1] ?? id;
603
+ planned.set(original, [...planned.get(original) ?? [], { id, index }]);
604
+ }
605
+ let aligned;
606
+ const interactions = metadata["tileflow:interaction-manifest"];
607
+ const domains = isRecord2(interactions) ? interactions.domains : void 0;
608
+ if (isRecord2(interactions) && isRecord2(domains) && isRecord2(domains.poi)) {
609
+ const poi = domains.poi;
610
+ if (Array.isArray(poi.layers)) {
611
+ const poiLayers = poi.layers.flatMap((entry) => {
612
+ if (!isRecord2(entry) || typeof entry.layerId !== "string") return [entry];
613
+ const copies = planned.get(entry.layerId);
614
+ if (!copies) return [entry];
615
+ return copies.map(({ id, index }) => ({ ...entry, layerId: id, priority: index }));
616
+ });
617
+ aligned = {
618
+ ...metadata,
619
+ "tileflow:interaction-manifest": {
620
+ ...interactions,
621
+ domains: { ...domains, poi: { ...poi, layers: poiLayers } }
622
+ }
623
+ };
624
+ }
625
+ }
626
+ const overlays = metadata["tileflow:overlay-placement-manifest"];
627
+ if (isRecord2(overlays) && isRecord2(overlays.anchors)) {
628
+ const anchors = Object.fromEntries(
629
+ Object.entries(overlays.anchors).map(([placement, anchor]) => [
630
+ placement,
631
+ typeof anchor === "string" ? planned.get(anchor)?.[0]?.id ?? anchor : anchor
632
+ ])
633
+ );
634
+ aligned = {
635
+ ...aligned ?? metadata,
636
+ "tileflow:overlay-placement-manifest": { ...overlays, anchors }
637
+ };
638
+ }
639
+ return aligned;
640
+ }
641
+ function imageTuples(values, out = []) {
642
+ const [first] = values;
643
+ if (values.every((value) => typeof value === "string")) {
644
+ out.push(values);
645
+ return out;
646
+ }
647
+ if (!Array.isArray(first) || typeof first[0] !== "string") {
648
+ return values.every((value) => same2(value, first)) ? out : void 0;
649
+ }
650
+ const operator = first[0];
651
+ if (!values.every(
652
+ (value) => Array.isArray(value) && value[0] === operator && value.length === first.length
653
+ )) {
654
+ return void 0;
655
+ }
656
+ const at = (index) => values.map((value) => value[index]);
657
+ const sameAt = (index) => at(index).every((value) => same2(value, first[index]));
658
+ const outputs = [];
659
+ if (operator === "literal" || operator === "image") outputs.push(1);
660
+ else if (operator === "coalesce")
661
+ for (let index = 1; index < first.length; index += 1) outputs.push(index);
662
+ else if (operator === "match") {
663
+ if (!sameAt(1)) return void 0;
664
+ for (let index = 2; index < first.length - 1; index += 2) {
665
+ if (!sameAt(index)) return void 0;
666
+ outputs.push(index + 1);
667
+ }
668
+ outputs.push(first.length - 1);
669
+ } else if (operator === "case") {
670
+ for (let index = 1; index < first.length - 1; index += 2) {
671
+ if (!sameAt(index)) return void 0;
672
+ outputs.push(index + 1);
673
+ }
674
+ outputs.push(first.length - 1);
675
+ } else if (operator === "step") {
676
+ if (!sameAt(1)) return void 0;
677
+ outputs.push(2);
678
+ for (let index = 3; index < first.length; index += 2) {
679
+ if (!sameAt(index)) return void 0;
680
+ outputs.push(index + 1);
681
+ }
682
+ } else {
683
+ return values.every((value) => same2(value, first)) ? out : void 0;
684
+ }
685
+ for (const index of outputs) if (!imageTuples(at(index), out)) return void 0;
686
+ return out;
687
+ }
688
+ function tryMixTemplate(values) {
689
+ const [first] = values;
690
+ if (values.every((value) => same2(value, first))) return first;
691
+ if (Array.isArray(first) && first[0] !== "literal" && values.every((value) => Array.isArray(value) && value.length === first.length)) {
692
+ const items = [];
693
+ for (let index = 0; index < first.length; index += 1) {
694
+ const item = tryMixTemplate(values.map((value) => value[index]));
695
+ if (item === void 0 && first[index] !== void 0) return void 0;
696
+ items.push(item);
697
+ }
698
+ return items;
699
+ }
700
+ if (first && typeof first === "object" && !Array.isArray(first)) {
701
+ const keys = Object.keys(first);
702
+ if (!values.every((value) => value && typeof value === "object" && same2(Object.keys(value), keys)))
703
+ return void 0;
704
+ const out = {};
705
+ for (const key of keys) {
706
+ const item = tryMixTemplate(values.map((value) => value[key]));
707
+ if (item === void 0) return void 0;
708
+ out[key] = item;
709
+ }
710
+ return out;
711
+ }
712
+ if (values.every((value) => typeof value === "number")) return new BlendLeaf(values);
713
+ if (values.every((value) => typeof value === "string" && oklab(value)))
714
+ return new BlendLeaf(values);
715
+ return void 0;
716
+ }
717
+ function isDataDriven(value) {
718
+ if (!Array.isArray(value)) return false;
719
+ if (typeof value[0] === "string" && featureOperators.has(value[0])) return true;
720
+ return value.some(isDataDriven);
721
+ }
722
+ function decisionOf(node) {
723
+ if (!Array.isArray(node) || typeof node[0] !== "string") return void 0;
724
+ const [operator] = node;
725
+ if (operator === "match" && isDataDriven(node[1])) {
726
+ const labels = [];
727
+ const outputs = [];
728
+ for (let index = 2; index < node.length - 1; index += 2) {
729
+ labels.push(node[index]);
730
+ outputs.push(node[index + 1]);
731
+ }
732
+ outputs.push(node.at(-1));
733
+ return {
734
+ index: ["match", node[1], ...labels.flatMap((label, index) => [label, index]), labels.length],
735
+ outputs
736
+ };
737
+ }
738
+ if (operator === "case") {
739
+ const conditions = [];
740
+ const outputs = [];
741
+ for (let index = 1; index < node.length - 1; index += 2) {
742
+ conditions.push(node[index]);
743
+ outputs.push(node[index + 1]);
744
+ }
745
+ outputs.push(node.at(-1));
746
+ if (!conditions.some(isDataDriven)) return void 0;
747
+ return {
748
+ index: [
749
+ "case",
750
+ ...conditions.flatMap((condition, index) => [condition, index]),
751
+ conditions.length
752
+ ],
753
+ outputs
754
+ };
755
+ }
756
+ if (operator === "step" && isDataDriven(node[1])) {
757
+ const outputs = [node[2]];
758
+ const stops = [];
759
+ for (let index = 3; index < node.length; index += 2) {
760
+ stops.push(node[index]);
761
+ outputs.push(node[index + 1]);
762
+ }
763
+ return {
764
+ index: ["step", node[1], 0, ...stops.flatMap((stop, index) => [stop, index + 1])],
765
+ outputs
766
+ };
767
+ }
768
+ return void 0;
769
+ }
770
+ function firstDecision(value) {
771
+ const decision = decisionOf(value);
772
+ if (decision) return decision;
773
+ if (!Array.isArray(value) || value[0] === "literal") return void 0;
774
+ for (const item of value) {
775
+ const found = firstDecision(item);
776
+ if (found) return found;
777
+ }
778
+ return void 0;
779
+ }
780
+ function choose(value, key, branch) {
781
+ const decision = decisionOf(value);
782
+ if (decision && JSON.stringify(decision.index) === key)
783
+ return choose(decision.outputs[branch], key, branch);
784
+ if (!Array.isArray(value) || value[0] === "literal") return value;
785
+ return value.map((item) => choose(item, key, branch));
786
+ }
787
+ function splitBranches(valuesByProperty, conditions = []) {
788
+ let decision;
789
+ for (const values of Object.values(valuesByProperty)) {
790
+ decision = firstDecision(values[0]);
791
+ if (decision) break;
792
+ }
793
+ if (!decision) {
794
+ const residual = Object.values(valuesByProperty).some((values) => values.some(isDataDriven));
795
+ return residual ? void 0 : [{ conditions, valuesByProperty }];
796
+ }
797
+ if (reads(decision.index, "zoom") || reads(decision.index, "feature-state")) return void 0;
798
+ const key = JSON.stringify(decision.index);
799
+ const branches = [];
800
+ for (let branch = 0; branch < decision.outputs.length; branch += 1) {
801
+ const chosen = Object.fromEntries(
802
+ Object.entries(valuesByProperty).map(([property, values]) => [
803
+ property,
804
+ values.map((value) => choose(value, key, branch))
805
+ ])
806
+ );
807
+ const nested = splitBranches(chosen, [...conditions, ["==", decision.index, branch]]);
808
+ if (!nested) return void 0;
809
+ branches.push(...nested);
810
+ if (branches.length > maximumBranches) return void 0;
811
+ }
812
+ return branches;
813
+ }
814
+ function inlineBranchBindings(input) {
815
+ const limit = 2e4;
816
+ const visit = (node, scope, depth) => {
817
+ if (depth > 96) throw new RangeError("Expression nesting limit");
818
+ if (!Array.isArray(node) || node[0] === "literal" || isColorProgramMarker(node))
819
+ return { value: node, size: 1 };
820
+ if (node[0] === "var") {
821
+ const bound = scope.get(String(node[1]));
822
+ if (!bound) throw new TypeError("Unbound branch variable");
823
+ return bound;
824
+ }
825
+ if (node[0] === "let") {
826
+ const inner = new Map(scope);
827
+ for (let index = 1; index < node.length - 1; index += 2)
828
+ inner.set(String(node[index]), visit(node[index + 1], scope, depth + 1));
829
+ return visit(node.at(-1), inner, depth + 1);
830
+ }
831
+ let size = 1;
832
+ const value = node.map((child) => {
833
+ const result = visit(child, scope, depth + 1);
834
+ size += result.size;
835
+ if (size > limit) throw new RangeError("Expanded branch limit");
836
+ return result.value;
837
+ });
838
+ return { value, size };
839
+ };
840
+ try {
841
+ return { value: visit(input, /* @__PURE__ */ new Map(), 0).value };
842
+ } catch {
843
+ return void 0;
844
+ }
845
+ }
846
+ function scaleByWeight(opacity, weight) {
847
+ const input = Array.isArray(opacity) ? opacity[0] === "interpolate" ? opacity[2] : opacity[1] : void 0;
848
+ if (Array.isArray(opacity) && (opacity[0] === "interpolate" || opacity[0] === "step") && reads(input, "zoom")) {
849
+ const head = opacity[0] === "interpolate" ? 3 : 2;
850
+ return opacity.map(
851
+ (item, index) => index >= head && (index - head) % 2 === (opacity[0] === "interpolate" ? 1 : 0) ? scaleByWeight(item, weight) : item
852
+ );
853
+ }
854
+ if (typeof opacity === "number")
855
+ return new BlendLeaf(weight.values.map((value) => opacity * Number(value)));
856
+ return ["*", opacity, weight];
857
+ }
858
+ function same2(a, b) {
859
+ return JSON.stringify(a) === JSON.stringify(b);
860
+ }
861
+ function oklab(colour) {
862
+ const cached = oklabCache.get(colour);
863
+ if (cached !== void 0) return cached;
864
+ const rgba = parseColour(colour);
865
+ let result = null;
866
+ if (rgba) {
867
+ const [r, g, b] = rgba.slice(0, 3).map((channel) => {
868
+ const value = channel / 255;
869
+ return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
870
+ });
871
+ const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
872
+ const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
873
+ const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
874
+ result = [
875
+ 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
876
+ 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
877
+ 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
878
+ rgba[3]
879
+ ];
880
+ }
881
+ if (oklabCache.size > 4096) oklabCache.clear();
882
+ oklabCache.set(colour, result);
883
+ return result;
884
+ }
885
+ function colourFromOklab([lightness, a, b, alpha]) {
886
+ const l = (lightness + 0.3963377774 * a + 0.2158037573 * b) ** 3;
887
+ const m = (lightness - 0.1055613458 * a - 0.0638541728 * b) ** 3;
888
+ const s = (lightness - 0.0894841775 * a - 1.291485548 * b) ** 3;
889
+ const [red, green, blue] = [
890
+ 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
891
+ -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
892
+ -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
893
+ ].map((value) => {
894
+ const clamped = Math.min(1, Math.max(0, value));
895
+ const encoded = clamped <= 31308e-7 ? clamped * 12.92 : 1.055 * clamped ** (1 / 2.4) - 0.055;
896
+ return Math.round(encoded * 255);
897
+ });
898
+ return `rgba(${red}, ${green}, ${blue}, ${Number(alpha.toFixed(3))})`;
899
+ }
900
+ function parseColour(colour) {
901
+ const text = colour.trim().toLowerCase();
902
+ const hex = /^#([\da-f]{3,8})$/u.exec(text)?.[1];
903
+ if (hex && [3, 4, 6, 8].includes(hex.length)) {
904
+ const full = hex.length <= 4 ? [...hex].map((digit) => digit + digit).join("") : hex;
905
+ const channels = full.match(/../gu).map((pair) => Number.parseInt(pair, 16));
906
+ return [channels[0], channels[1], channels[2], channels.length > 3 ? channels[3] / 255 : 1];
907
+ }
908
+ const functional = /^(rgba?|hsla?)\(([^)]+)\)$/u.exec(text);
909
+ if (!functional) return null;
910
+ const parts = functional[2].split(/[\s,/]+/u).filter(Boolean);
911
+ if (parts.length < 3 || parts.length > 4) return null;
912
+ const alpha = parts[3] === void 0 ? 1 : parseUnit(parts[3], 1);
913
+ if (functional[1].startsWith("rgb")) {
914
+ const [r, g, b] = parts.slice(0, 3).map((part) => parseUnit(part, 255));
915
+ if ([r, g, b, alpha].some((value) => value === null)) return null;
916
+ return [r, g, b, alpha];
917
+ }
918
+ const hue = Number.parseFloat(parts[0]);
919
+ const saturation = parseUnit(parts[1], 1);
920
+ const light = parseUnit(parts[2], 1);
921
+ if (!Number.isFinite(hue) || saturation === null || light === null || alpha === null) return null;
922
+ const chroma = (1 - Math.abs(2 * light - 1)) * saturation;
923
+ const sector = (hue % 360 + 360) % 360 / 60;
924
+ const x = chroma * (1 - Math.abs(sector % 2 - 1));
925
+ const [r1, g1, b1] = sector < 1 ? [chroma, x, 0] : sector < 2 ? [x, chroma, 0] : sector < 3 ? [0, chroma, x] : sector < 4 ? [0, x, chroma] : sector < 5 ? [x, 0, chroma] : [chroma, 0, x];
926
+ const offset = light - chroma / 2;
927
+ return [(r1 + offset) * 255, (g1 + offset) * 255, (b1 + offset) * 255, alpha];
928
+ }
929
+ function parseUnit(part, scale) {
930
+ const percentage = part.endsWith("%");
931
+ const value = Number.parseFloat(part);
932
+ if (!Number.isFinite(value)) return null;
933
+ return percentage ? value / 100 * scale : value;
934
+ }
935
+ var TileflowThemeBlendError, BlendLeaf, imageProperties, opacityProperties, featureOperators, maximumBranches, isRecord2, reads, isLegacyFilter, oklabCache;
936
+ var init_theme_blend = __esm({
937
+ "src/theme-blend.ts"() {
938
+ "use strict";
939
+ init_color_program();
940
+ TileflowThemeBlendError = class extends Error {
941
+ name = "TileflowThemeBlendError";
942
+ };
943
+ BlendLeaf = class {
944
+ constructor(values) {
945
+ this.values = values;
946
+ }
947
+ values;
948
+ };
949
+ imageProperties = /* @__PURE__ */ new Set([
950
+ "icon-image",
951
+ "fill-pattern",
952
+ "line-pattern",
953
+ "background-pattern",
954
+ "fill-extrusion-pattern"
955
+ ]);
956
+ opacityProperties = {
957
+ background: "background-opacity",
958
+ circle: "circle-opacity",
959
+ fill: "fill-opacity",
960
+ "fill-extrusion": "fill-extrusion-opacity",
961
+ heatmap: "heatmap-opacity",
962
+ line: "line-opacity",
963
+ raster: "raster-opacity"
964
+ };
965
+ featureOperators = /* @__PURE__ */ new Set([
966
+ "feature-state",
967
+ "geometry-type",
968
+ "get",
969
+ "has",
970
+ "id",
971
+ "in",
972
+ "properties"
973
+ ]);
974
+ maximumBranches = 24;
975
+ isRecord2 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
976
+ reads = (value, operator) => Array.isArray(value) && (value[0] === operator || value.some((item) => reads(item, operator)));
977
+ isLegacyFilter = (filter) => Array.isArray(filter) && typeof filter[1] === "string" && !Array.isArray(filter[1]);
978
+ oklabCache = /* @__PURE__ */ new Map();
979
+ }
980
+ });
981
+
982
+ // src/sun.ts
983
+ function normalizeTileflowAzimuth(azimuth) {
984
+ const turned = azimuth % 360;
985
+ return turned < 0 ? turned + 360 : turned === 0 ? 0 : turned;
986
+ }
987
+ function tileflowSunLight(direction, base) {
988
+ const radial = base?.position?.[0] ?? 1.15;
989
+ return {
990
+ ...base ?? {},
991
+ anchor: "map",
992
+ position: [
993
+ radial,
994
+ Number(normalizeTileflowAzimuth(direction.azimuth).toFixed(3)),
995
+ // MapLibre lights the extrusions from above the horizon only.
996
+ Number(Math.min(90, Math.max(0, 90 - direction.altitude)).toFixed(3))
997
+ ]
998
+ };
999
+ }
1000
+ var tileflowDefaultSunPreset, degrees;
1001
+ var init_sun = __esm({
1002
+ "src/sun.ts"() {
1003
+ "use strict";
1004
+ tileflowDefaultSunPreset = Object.freeze({
1005
+ azimuth: 210,
1006
+ altitude: 60,
1007
+ color: "#ffffff",
1008
+ intensity: 0.2,
1009
+ shadowIntensity: 1,
1010
+ ambient: Object.freeze({ color: "#ffffff", intensity: 0.8 })
1011
+ });
1012
+ degrees = Math.PI / 180;
1013
+ }
1014
+ });
1015
+
1016
+ // src/themes/visual-semantics.ts
1017
+ function isTileflowVisualColorCollection(property) {
1018
+ return property === "colors" || property.endsWith("Colors") || property.endsWith("-colors");
1019
+ }
1020
+ function classifyTileflowVisualProperty(property, _value) {
1021
+ if (property === "color" || property.endsWith("Color") || property.endsWith("-color") || isTileflowVisualColorCollection(property)) {
1022
+ return "color";
1023
+ }
1024
+ if (property === "font" || property === "fallbacks" || property.endsWith("-font")) return "font";
1025
+ if (property === "image" || property === "pattern" || property.endsWith("-image") || property.endsWith("-pattern")) {
1026
+ return "image";
1027
+ }
1028
+ return visualNumberProperties.has(property) ? "number" : void 0;
1029
+ }
1030
+ var visualNumberProperties;
1031
+ var init_visual_semantics = __esm({
1032
+ "src/themes/visual-semantics.ts"() {
1033
+ "use strict";
1034
+ visualNumberProperties = /* @__PURE__ */ new Set([
1035
+ "angle",
1036
+ "base",
1037
+ "blur",
1038
+ "brightnessScale",
1039
+ "crownScale",
1040
+ "crownAnchor",
1041
+ "sizeAnchor",
1042
+ "sizeCompression",
1043
+ "contrast",
1044
+ "dash",
1045
+ "exaggeration",
1046
+ "emissiveStrength",
1047
+ "emissiveVariation",
1048
+ "emissiveHeight",
1049
+ "colorMix",
1050
+ "colorScale",
1051
+ "metallic",
1052
+ "roughness",
1053
+ "horizonBlend",
1054
+ "hue",
1055
+ "saturation",
1056
+ "lightness",
1057
+ "fadeDuration",
1058
+ "gapWidth",
1059
+ "groundOcclusion",
1060
+ "groundOcclusionRadius",
1061
+ "haloBlur",
1062
+ "haloWidth",
1063
+ "height",
1064
+ "heightScale",
1065
+ "illuminationDirection",
1066
+ "intensity",
1067
+ "letterSpacing",
1068
+ "lineHeight",
1069
+ "maxAngle",
1070
+ "maxWidth",
1071
+ "miterLimit",
1072
+ "modelOcclusion",
1073
+ "offset",
1074
+ "opacity",
1075
+ "padding",
1076
+ "patternWidths",
1077
+ "radius",
1078
+ "radialOffset",
1079
+ "rotate",
1080
+ "roundLimit",
1081
+ "size",
1082
+ "spacing",
1083
+ "starIntensity",
1084
+ "strokeOpacity",
1085
+ "strokeWidth",
1086
+ "textFitPadding",
1087
+ "translate",
1088
+ "width",
1089
+ "widthScale",
1090
+ "background-emissive-strength",
1091
+ "background-opacity",
1092
+ "circle-blur",
1093
+ "circle-emissive-strength",
1094
+ "circle-opacity",
1095
+ "circle-radius",
1096
+ "circle-stroke-opacity",
1097
+ "circle-stroke-width",
1098
+ "circle-translate",
1099
+ "color-relief-opacity",
1100
+ "fill-emissive-strength",
1101
+ "fill-extrusion-ambient-occlusion-ground-attenuation",
1102
+ "fill-extrusion-ambient-occlusion-ground-radius",
1103
+ "fill-extrusion-ambient-occlusion-intensity",
1104
+ "fill-extrusion-ambient-occlusion-radius",
1105
+ "fill-extrusion-base",
1106
+ "fill-extrusion-emissive-strength",
1107
+ "fill-extrusion-flood-light-ground-attenuation",
1108
+ "fill-extrusion-flood-light-ground-radius",
1109
+ "fill-extrusion-flood-light-intensity",
1110
+ "fill-extrusion-flood-light-wall-radius",
1111
+ "fill-extrusion-height",
1112
+ "fill-extrusion-opacity",
1113
+ "fill-extrusion-translate",
1114
+ "fill-extrusion-vertical-scale",
1115
+ "fill-opacity",
1116
+ "fill-translate",
1117
+ "heatmap-intensity",
1118
+ "heatmap-opacity",
1119
+ "heatmap-radius",
1120
+ "heatmap-weight",
1121
+ "hillshade-exaggeration",
1122
+ "hillshade-emissive-strength",
1123
+ "hillshade-illumination-direction",
1124
+ "icon-emissive-strength",
1125
+ "icon-halo-blur",
1126
+ "icon-halo-width",
1127
+ "icon-opacity",
1128
+ "icon-occlusion-opacity",
1129
+ "icon-offset",
1130
+ "icon-padding",
1131
+ "icon-size",
1132
+ "icon-text-fit-padding",
1133
+ "icon-translate",
1134
+ "line-blur",
1135
+ "line-dasharray",
1136
+ "line-emissive-strength",
1137
+ "line-gap-width",
1138
+ "line-miter-limit",
1139
+ "line-opacity",
1140
+ "line-offset",
1141
+ "line-round-limit",
1142
+ "line-translate",
1143
+ "line-width",
1144
+ "raster-brightness-max",
1145
+ "raster-brightness-min",
1146
+ "raster-contrast",
1147
+ "raster-emissive-strength",
1148
+ "raster-fade-duration",
1149
+ "raster-hue-rotate",
1150
+ "raster-opacity",
1151
+ "raster-saturation",
1152
+ "text-emissive-strength",
1153
+ "text-halo-blur",
1154
+ "text-halo-width",
1155
+ "text-letter-spacing",
1156
+ "text-line-height",
1157
+ "text-max-angle",
1158
+ "text-max-width",
1159
+ "text-offset",
1160
+ "text-occlusion-opacity",
1161
+ "text-opacity",
1162
+ "text-padding",
1163
+ "text-radial-offset",
1164
+ "text-rotate",
1165
+ "text-size",
1166
+ "text-translate",
1167
+ "symbol-spacing"
1168
+ ]);
1169
+ }
1170
+ });
1171
+
1172
+ // src/themes/model.ts
1173
+ function resolveTileflowTheme(theme) {
1174
+ assertThemeDocument(theme);
1175
+ const resolver = new ThemeValueResolver(theme);
1176
+ const tokens = {
1177
+ color: resolver.resolveCategory("color"),
1178
+ font: resolver.resolveCategory("font"),
1179
+ image: resolver.resolveCategory("image"),
1180
+ number: resolver.resolveCategory("number")
1181
+ };
1182
+ const typography = resolver.resolveTypography(theme.typography);
1183
+ const { light: lighting, sun } = resolver.resolveLighting(theme.lighting);
1184
+ return {
1185
+ colorScheme: theme.colorScheme,
1186
+ id: theme.id,
1187
+ lighting,
1188
+ ...sun ? { sun } : {},
1189
+ tokens,
1190
+ typography,
1191
+ version: theme.version
1192
+ };
1193
+ }
1194
+ function resolveThemeValues(value, theme, path = "value") {
1195
+ const resolver = new ThemeValueResolver(theme);
1196
+ resolver.resolveAllTokens();
1197
+ const resolved = resolver.resolveTree(value, path);
1198
+ const errors = auditTileflowThemeValues(value, path).filter(({ severity }) => severity === "error");
1199
+ if (errors.length > 0) throw new TileflowThemeAuditError(errors);
1200
+ return resolved;
1201
+ }
1202
+ function auditTileflowThemeValues(value, path = "value", context = inferAuditContext(path)) {
1203
+ const diagnostics = [];
1204
+ visitAuditableValue(value, path, void 0, {
1205
+ context,
1206
+ diagnostics,
1207
+ resolvingVariables: /* @__PURE__ */ new Set(),
1208
+ variables: /* @__PURE__ */ new Map()
1209
+ });
1210
+ return diagnostics.sort(compareAuditDiagnostics);
1211
+ }
1212
+ function visitThemeIntentNode(value, path, expectedCategory, state) {
1213
+ const tokenReference = isTileflowThemeTokenReference(value) ? value : void 0;
1214
+ const fixedValue = isTileflowFixedValue(value) ? value : void 0;
1215
+ const colorOperation = isTileflowThemeColorOperation(value) ? value : void 0;
1216
+ if (!tokenReference && !fixedValue && !colorOperation) {
1217
+ addThemeNodeDiagnostic(
1218
+ expectedCategory ?? inferThemeNodeCategory(value),
1219
+ path,
1220
+ state,
1221
+ themeNodeDisplayValue(value),
1222
+ `Malformed Tileflow ${String(value.kind)} node cannot establish explicit theme intent.`,
1223
+ "Use token.*, fixed(value, {reason}), or color.* constructors without extra fields."
1224
+ );
1225
+ return;
1226
+ }
1227
+ const actualCategory = tokenReference?.category ?? (colorOperation ? "color" : void 0);
1228
+ if (!expectedCategory) {
1229
+ addThemeNodeDiagnostic(
1230
+ actualCategory ?? inferThemeNodeCategory(value),
1231
+ path,
1232
+ state,
1233
+ themeNodeDisplayValue(value),
1234
+ "Tileflow theme values may only be consumed by a categorized visual property.",
1235
+ "Move the value to a color, font, image, or visual-number field; keep structure literal."
1236
+ );
1237
+ return;
1238
+ }
1239
+ if (actualCategory && actualCategory !== expectedCategory) {
1240
+ addThemeNodeDiagnostic(
1241
+ expectedCategory,
1242
+ path,
1243
+ state,
1244
+ themeNodeDisplayValue(value),
1245
+ `Tileflow theme value category mismatch: expected ${expectedCategory}, received ${actualCategory}.`,
1246
+ `Use token.${expectedCategory}(...) or an explicit fixed ${expectedCategory} value.`
1247
+ );
1248
+ return;
1249
+ }
1250
+ if (fixedValue) {
1251
+ if (!isThemePrimitiveForCategory(fixedValue.value, expectedCategory)) {
1252
+ addThemeNodeDiagnostic(
1253
+ expectedCategory,
1254
+ path,
1255
+ state,
1256
+ themeNodeDisplayValue(value),
1257
+ `Tileflow fixed value category mismatch: expected ${expectedCategory}.`,
1258
+ `Use a primitive ${expectedCategory} value inside fixed(value, {reason}).`
1259
+ );
1260
+ }
1261
+ return;
1262
+ }
1263
+ if (!colorOperation) return;
1264
+ if (colorOperation.operation === "alpha") {
1265
+ visitAuditableValue(colorOperation.color, `${path}.color`, "color", state, true);
1266
+ visitAuditableValue(colorOperation.opacity, `${path}.opacity`, "number", state, true);
1267
+ return;
1268
+ }
1269
+ visitAuditableValue(colorOperation.from, `${path}.from`, "color", state, true);
1270
+ visitAuditableValue(colorOperation.to, `${path}.to`, "color", state, true);
1271
+ visitAuditableValue(colorOperation.amount, `${path}.amount`, "number", state, true);
1272
+ }
1273
+ function addThemeNodeDiagnostic(category, path, state, value, message, suggestion) {
1274
+ state.diagnostics.push({
1275
+ category,
1276
+ code: "THEME_IMPLICIT_FIXED",
1277
+ message,
1278
+ ...state.context.owner ? { owner: state.context.owner } : {},
1279
+ path,
1280
+ phase: "theme-audit",
1281
+ scope: state.context.scope,
1282
+ severity: "error",
1283
+ suggestion,
1284
+ ...state.context.target ? { target: state.context.target } : {},
1285
+ value
1286
+ });
1287
+ }
1288
+ function inferThemeNodeCategory(value) {
1289
+ if (isThemeTokenCategory(value.category)) return value.category;
1290
+ if (value.kind === "theme-color") return "color";
1291
+ return typeof value.value === "number" ? "number" : "color";
1292
+ }
1293
+ function themeNodeDisplayValue(value) {
1294
+ if (typeof value.token === "string") return value.token;
1295
+ if (typeof value.value === "string" || typeof value.value === "number") return value.value;
1296
+ if (typeof value.operation === "string") return value.operation;
1297
+ return String(value.kind);
1298
+ }
1299
+ function isThemePrimitiveForCategory(value, category) {
1300
+ return category === "number" ? typeof value === "number" && Number.isFinite(value) || Array.isArray(value) && value.every((entry) => typeof entry === "number" && Number.isFinite(entry)) : typeof value === "string" && value.length > 0;
1301
+ }
1302
+ function isThemeValueCandidate(value) {
1303
+ return isRecord3(value) && (value.kind === "theme-token" || value.kind === "theme-fixed" || value.kind === "theme-color");
1304
+ }
1305
+ function visitAuditableValue(value, path, category, state, intentRequired = false) {
1306
+ if (isThemeStructuralPath(path)) category = void 0;
1307
+ if (isThemeValueCandidate(value)) {
1308
+ visitThemeIntentNode(value, path, category, state);
1309
+ return;
1310
+ }
1311
+ if (typeof value === "string" || typeof value === "number") {
1312
+ addAuditDiagnostic(value, path, category, state, intentRequired);
1313
+ return;
1314
+ }
1315
+ if (Array.isArray(value)) {
1316
+ visitAuditableArray(value, path, category, state, false, intentRequired);
1317
+ return;
1318
+ }
1319
+ if (!isRecord3(value)) return;
1320
+ if (value.kind === "expression" && Array.isArray(value.value)) {
1321
+ visitAuditableArray(value.value, `${path}.value`, category, state, true, intentRequired);
1322
+ return;
1323
+ }
1324
+ if (value.kind === "zoom" && Array.isArray(value.stops)) {
1325
+ for (const [index, stop] of value.stops.entries()) {
1326
+ if (Array.isArray(stop)) {
1327
+ visitAuditableValue(stop[0], `${path}.stops[${index}][0]`, void 0, state);
1328
+ visitAuditableValue(
1329
+ stop[1],
1330
+ `${path}.stops[${index}][1]`,
1331
+ category,
1332
+ state,
1333
+ category !== void 0 && category !== "number"
1334
+ );
1335
+ }
1336
+ }
1337
+ return;
1338
+ }
1339
+ const collectionCategory = isTileflowVisualColorCollection(lastPathKey(path)) ? category : void 0;
1340
+ for (const key of Object.keys(value).sort(compareCodeUnits)) {
1341
+ visitAuditableValue(
1342
+ value[key],
1343
+ `${path}.${key}`,
1344
+ classifyTileflowVisualProperty(key, value[key]) ?? collectionCategory,
1345
+ state
1346
+ );
1347
+ }
1348
+ }
1349
+ function lastPathKey(path) {
1350
+ return path.endsWith("]") ? "" : path.slice(path.lastIndexOf(".") + 1);
1351
+ }
1352
+ function visitAuditableArray(value, path, category, state, forceExpression = false, intentRequired = false) {
1353
+ const operator = typeof value[0] === "string" ? value[0] : void 0;
1354
+ const isExpression = forceExpression || isMapLibreExpressionOperator(operator);
1355
+ if (!isExpression) {
1356
+ for (const [index, entry] of value.entries()) {
1357
+ visitAuditableValue(entry, `${path}[${index}]`, category, state, intentRequired);
1358
+ }
1359
+ return;
1360
+ }
1361
+ visitExpressionOutputs(value, path, category, state, intentRequired);
1362
+ }
1363
+ function visitExpressionOutputs(value, path, category, state, intentRequired) {
1364
+ const operator = typeof value[0] === "string" ? value[0] : void 0;
1365
+ if (!operator) return;
1366
+ if (operator === "let") {
1367
+ const variables = new Map(state.variables);
1368
+ for (let index = 1; index < value.length - 1; index += 2) {
1369
+ const name = value[index];
1370
+ if (typeof name === "string") {
1371
+ variables.set(name, { path: `${path}[${index + 1}]`, value: value[index + 1] });
1372
+ }
1373
+ }
1374
+ if (value.length > 1) {
1375
+ visitAuditableValue(
1376
+ value.at(-1),
1377
+ `${path}[${value.length - 1}]`,
1378
+ category,
1379
+ { ...state, variables },
1380
+ intentRequired || category !== void 0 && category !== "number"
1381
+ );
1382
+ }
1383
+ return;
1384
+ }
1385
+ if (operator === "var") {
1386
+ const name = value[1];
1387
+ if (typeof name !== "string" || state.resolvingVariables.has(name)) {
1388
+ addDynamicExpressionDiagnostic(
1389
+ operator,
1390
+ path,
1391
+ category,
1392
+ state,
1393
+ intentRequired || category !== void 0 && category !== "number"
1394
+ );
1395
+ return;
1396
+ }
1397
+ const variable = state.variables.get(name);
1398
+ if (!variable) {
1399
+ addDynamicExpressionDiagnostic(
1400
+ operator,
1401
+ path,
1402
+ category,
1403
+ state,
1404
+ intentRequired || category !== void 0 && category !== "number"
1405
+ );
1406
+ return;
1407
+ }
1408
+ visitAuditableValue(
1409
+ variable.value,
1410
+ variable.path,
1411
+ category,
1412
+ { ...state, resolvingVariables: /* @__PURE__ */ new Set([...state.resolvingVariables, name]) },
1413
+ intentRequired
1414
+ );
1415
+ return;
1416
+ }
1417
+ if (operator === "literal") {
1418
+ if (value.length > 1) {
1419
+ visitLiteralOutput(value[1], `${path}[1]`, category, state, intentRequired);
1420
+ }
1421
+ return;
1422
+ }
1423
+ if (expressionDataOperators.has(operator)) {
1424
+ for (let index = 1; index < value.length; index += 1) {
1425
+ visitAuditableValue(value[index], `${path}[${index}]`, void 0, state);
1426
+ }
1427
+ addDynamicExpressionDiagnostic(
1428
+ operator,
1429
+ path,
1430
+ category,
1431
+ state,
1432
+ intentRequired || category !== void 0 && category !== "number"
1433
+ );
1434
+ return;
1435
+ }
1436
+ if (operator === "rgb" || operator === "rgba") {
1437
+ for (let index = 1; index < value.length; index += 1) {
1438
+ visitAuditableValue(value[index], `${path}[${index}]`, void 0, state);
1439
+ }
1440
+ addDynamicExpressionDiagnostic(operator, path, category, state, true);
1441
+ return;
1442
+ }
1443
+ const outputIndices = new Set(expressionOutputIndices(operator, value.length));
1444
+ for (let index = 1; index < value.length; index += 1) {
1445
+ const isOutput = outputIndices.has(index);
1446
+ visitAuditableValue(
1447
+ value[index],
1448
+ `${path}[${index}]`,
1449
+ isOutput ? category : void 0,
1450
+ state,
1451
+ isOutput && (intentRequired || category !== void 0 && category !== "number")
1452
+ );
1453
+ }
1454
+ }
1455
+ function visitLiteralOutput(value, path, category, state, intentRequired) {
1456
+ if (Array.isArray(value)) {
1457
+ for (const [index, entry] of value.entries()) {
1458
+ visitLiteralOutput(entry, `${path}[${index}]`, category, state, intentRequired);
1459
+ }
1460
+ return;
1461
+ }
1462
+ visitAuditableValue(value, path, category, state, intentRequired);
1463
+ }
1464
+ function addAuditDiagnostic(value, path, category, state, blocking = false) {
1465
+ if (!category) return;
1466
+ if (category === "number") {
1467
+ if (typeof value !== "number" || !Number.isFinite(value)) return;
1468
+ } else {
1469
+ if (typeof value !== "string") return;
1470
+ }
1471
+ state.diagnostics.push({
1472
+ category,
1473
+ code: "THEME_IMPLICIT_FIXED",
1474
+ message: `Visual ${category} literal is implicitly fixed; use token.${category}(...) or fixed(value, {reason}).`,
1475
+ ...state.context.owner ? { owner: state.context.owner } : {},
1476
+ path,
1477
+ phase: "theme-audit",
1478
+ scope: state.context.scope,
1479
+ severity: category === "number" && !blocking ? "warning" : "error",
1480
+ suggestion: `Replace the literal with token.${category}(...) or document the invariant with fixed(value, {reason}).`,
1481
+ ...state.context.target ? { target: state.context.target } : {},
1482
+ value
1483
+ });
1484
+ }
1485
+ function addDynamicExpressionDiagnostic(operator, path, category, state, intentRequired) {
1486
+ if (!category || category === "number" || !intentRequired) return;
1487
+ state.diagnostics.push({
1488
+ category,
1489
+ code: "THEME_IMPLICIT_FIXED",
1490
+ message: `Visual ${category} expression has a dynamic output from ${operator}; every possible output must be token.${category}(...), fixed(value, {reason}), or a valid theme color operation.`,
1491
+ ...state.context.owner ? { owner: state.context.owner } : {},
1492
+ path,
1493
+ phase: "theme-audit",
1494
+ scope: state.context.scope,
1495
+ severity: "error",
1496
+ suggestion: `Make every ${category} output branch carry explicit theme intent.`,
1497
+ ...state.context.target ? { target: state.context.target } : {},
1498
+ value: operator
1499
+ });
1500
+ }
1501
+ function expressionOutputIndices(operator, length) {
1502
+ switch (operator) {
1503
+ case "case":
1504
+ return uniqueIndices([...range(2, length - 1, 2), length - 1]);
1505
+ case "match":
1506
+ return uniqueIndices([...range(3, length - 1, 2), length - 1]);
1507
+ case "step":
1508
+ return range(2, length, 2);
1509
+ case "interpolate":
1510
+ case "interpolate-hcl":
1511
+ case "interpolate-lab":
1512
+ return range(4, length, 2);
1513
+ case "format":
1514
+ return range(1, length, 2);
1515
+ case "at":
1516
+ return length > 2 ? [2] : [];
1517
+ case "array":
1518
+ return length > 1 ? [length - 1] : [];
1519
+ case "image":
1520
+ return length > 1 ? [1] : [];
1521
+ case "coalesce":
1522
+ case "concat":
1523
+ case "join":
1524
+ case "boolean":
1525
+ case "number":
1526
+ case "object":
1527
+ case "string":
1528
+ case "to-boolean":
1529
+ case "to-color":
1530
+ case "to-number":
1531
+ case "to-string":
1532
+ return range(1, length, 1);
1533
+ default:
1534
+ return range(1, length, 1);
1535
+ }
1536
+ }
1537
+ function uniqueIndices(indices) {
1538
+ return [...new Set(indices)];
1539
+ }
1540
+ function inferAuditContext(path) {
1541
+ if (path === "terrain" || path.startsWith("terrain.")) return { scope: "terrain" };
1542
+ if (path === "modules" || path.startsWith("modules.")) {
1543
+ const owner = path.match(/^modules\.([^.[]+)/u)?.[1];
1544
+ return { scope: "module", ...owner ? { owner } : {} };
1545
+ }
1546
+ return { scope: "value" };
1547
+ }
1548
+ function compareAuditDiagnostics(left, right) {
1549
+ const leftKey = [
1550
+ left.scope,
1551
+ left.owner ?? "",
1552
+ left.target ?? "",
1553
+ left.path,
1554
+ left.category,
1555
+ typeof left.value,
1556
+ String(left.value)
1557
+ ];
1558
+ const rightKey = [
1559
+ right.scope,
1560
+ right.owner ?? "",
1561
+ right.target ?? "",
1562
+ right.path,
1563
+ right.category,
1564
+ typeof right.value,
1565
+ String(right.value)
1566
+ ];
1567
+ for (let index = 0; index < leftKey.length; index += 1) {
1568
+ const compared = compareCodeUnits(leftKey[index], rightKey[index]);
1569
+ if (compared !== 0) return compared;
1570
+ }
1571
+ return 0;
1572
+ }
1573
+ function range(start, end, step) {
1574
+ const values = [];
1575
+ for (let index = start; index < end; index += step) values.push(index);
1576
+ return values;
1577
+ }
1578
+ function assertThemeIdentity(theme) {
1579
+ if (!isThemeName(theme.id)) {
1580
+ throw new Error(
1581
+ `Invalid Tileflow theme id ${JSON.stringify(theme.id)}; expected a portable identifier.`
1582
+ );
1583
+ }
1584
+ if (!Number.isSafeInteger(theme.version) || theme.version < 1) {
1585
+ throw new Error(`Tileflow theme "${theme.id}" version must be a positive integer.`);
1586
+ }
1587
+ if (theme.colorScheme !== "light" && theme.colorScheme !== "dark") {
1588
+ throw new Error(`Tileflow theme "${theme.id}" colorScheme must be light or dark.`);
1589
+ }
1590
+ }
1591
+ function assertThemeDocument(theme) {
1592
+ const record3 = assertPlainThemeRecord(theme, "theme");
1593
+ assertExactThemeKeys(record3, "theme", themeDocumentKeys);
1594
+ assertRequiredThemeKeys(record3, "theme", themeDocumentKeys);
1595
+ assertThemeIdentity(theme);
1596
+ assertThemeTokens(record3.tokens, "theme.tokens", true);
1597
+ assertThemeTypography(record3.typography, "theme.typography");
1598
+ assertThemeLighting(record3.lighting, "theme.lighting");
1599
+ cloneJson(theme);
1600
+ }
1601
+ function assertThemeTokens(value, path, complete) {
1602
+ const tokens = assertPlainThemeRecord(value, path);
1603
+ assertExactThemeKeys(tokens, path, tileflowThemeTokenCategories);
1604
+ if (complete) assertRequiredThemeKeys(tokens, path, tileflowThemeTokenCategories);
1605
+ for (const category of tileflowThemeTokenCategories) {
1606
+ if (!Object.hasOwn(tokens, category)) continue;
1607
+ const values = assertPlainThemeRecord(tokens[category], `${path}.${category}`);
1608
+ for (const name of Reflect.ownKeys(values)) {
1609
+ if (typeof name !== "string") {
1610
+ throw new Error(
1611
+ `Invalid Tileflow ${category} token key ${String(name)} at ${path}.${category}.`
1612
+ );
1613
+ }
1614
+ assertTokenName(name, category);
1615
+ assertThemeCategoryValue(values[name], category, `${path}.${category}.${name}`);
1616
+ }
1617
+ }
1618
+ }
1619
+ function assertThemeTypography(value, path) {
1620
+ const typography = assertPlainThemeRecord(value, path);
1621
+ assertExactThemeKeys(typography, path, themeTypographyKeys);
1622
+ assertThemeTypographyStyleFields(typography, path);
1623
+ for (const domain of themeTypographyDomains) {
1624
+ if (!Object.hasOwn(typography, domain)) continue;
1625
+ const stylePath = `${path}.${domain}`;
1626
+ const style = assertPlainThemeRecord(typography[domain], stylePath);
1627
+ assertExactThemeKeys(style, stylePath, themeTypographyStyleKeys);
1628
+ assertThemeTypographyStyleFields(style, stylePath);
1629
+ }
1630
+ }
1631
+ function assertThemeTypographyStyleFields(style, path) {
1632
+ if (Object.hasOwn(style, "font")) {
1633
+ assertThemeCategoryValue(style.font, "font", `${path}.font`);
1634
+ }
1635
+ if (Object.hasOwn(style, "fallbacks")) {
1636
+ if (!Array.isArray(style.fallbacks)) {
1637
+ throw themeTypeError(`${path}.fallbacks`, "font array", style.fallbacks);
1638
+ }
1639
+ for (const [index, fallback] of style.fallbacks.entries()) {
1640
+ assertThemeCategoryValue(fallback, "font", `${path}.fallbacks[${index}]`);
1641
+ }
1642
+ }
1643
+ if (Object.hasOwn(style, "letterSpacing")) {
1644
+ assertThemeCategoryValue(style.letterSpacing, "number", `${path}.letterSpacing`);
1645
+ }
1646
+ if (Object.hasOwn(style, "transform") && !["lowercase", "none", "uppercase"].includes(String(style.transform))) {
1647
+ throw themeTypeError(`${path}.transform`, "text transform", style.transform);
1648
+ }
1649
+ }
1650
+ function assertThemeLighting(value, path, allowReset = false) {
1651
+ if (allowReset && isTileflowReset(value)) return;
1652
+ const authored = assertPlainThemeRecord(value, path);
1653
+ assertExactThemeKeys(authored, path, themeLightingKeys);
1654
+ const lighting = allowReset ? Object.fromEntries(Object.entries(authored).filter(([, entry]) => !isTileflowReset(entry))) : authored;
1655
+ if (Object.hasOwn(lighting, "sun")) {
1656
+ const sun = assertPlainThemeRecord(lighting.sun, `${path}.sun`);
1657
+ assertExactThemeKeys(sun, `${path}.sun`, themeSunKeys);
1658
+ for (const key of ["altitude", "azimuth", "intensity", "shadowIntensity"]) {
1659
+ if (Object.hasOwn(sun, key))
1660
+ assertThemeCategoryValue(sun[key], "number", `${path}.sun.${key}`);
1661
+ }
1662
+ if (Object.hasOwn(sun, "color"))
1663
+ assertThemeCategoryValue(sun.color, "color", `${path}.sun.color`);
1664
+ }
1665
+ if (Object.hasOwn(lighting, "ambient")) {
1666
+ const ambient = assertPlainThemeRecord(lighting.ambient, `${path}.ambient`);
1667
+ assertExactThemeKeys(ambient, `${path}.ambient`, themeAmbientKeys);
1668
+ if (Object.hasOwn(ambient, "intensity"))
1669
+ assertThemeCategoryValue(ambient.intensity, "number", `${path}.ambient.intensity`);
1670
+ if (Object.hasOwn(ambient, "color"))
1671
+ assertThemeCategoryValue(ambient.color, "color", `${path}.ambient.color`);
1672
+ }
1673
+ if (Object.hasOwn(lighting, "anchor") && lighting.anchor !== "map" && lighting.anchor !== "viewport") {
1674
+ throw themeTypeError(`${path}.anchor`, "map or viewport", lighting.anchor);
1675
+ }
1676
+ if (Object.hasOwn(lighting, "color")) {
1677
+ assertThemeCategoryValue(lighting.color, "color", `${path}.color`);
1678
+ }
1679
+ if (Object.hasOwn(lighting, "intensity")) {
1680
+ assertThemeCategoryValue(lighting.intensity, "number", `${path}.intensity`);
1681
+ }
1682
+ if (Object.hasOwn(lighting, "position")) {
1683
+ if (!Array.isArray(lighting.position) || lighting.position.length !== 3) {
1684
+ throw themeTypeError(`${path}.position`, "three number values", lighting.position);
1685
+ }
1686
+ for (const [index, position] of lighting.position.entries()) {
1687
+ assertThemeCategoryValue(position, "number", `${path}.position[${index}]`);
1688
+ }
1689
+ }
1690
+ }
1691
+ function assertThemeCategoryValue(value, expectedCategory, path) {
1692
+ if (!isRecord3(value)) {
1693
+ assertTokenPrimitive(expectedCategory, value, path);
1694
+ return;
1695
+ }
1696
+ if (value.kind === "theme-token") {
1697
+ assertThemeTokenReferenceNode(value, expectedCategory, path);
1698
+ return;
1699
+ }
1700
+ if (value.kind === "theme-fixed") {
1701
+ assertThemeFixedNode(value, expectedCategory, path);
1702
+ return;
1703
+ }
1704
+ if (value.kind === "theme-color") {
1705
+ if (expectedCategory !== "color") {
1706
+ throw themeTypeError(path, `${expectedCategory} value`, value);
1707
+ }
1708
+ assertThemeColorOperationNode(value, path);
1709
+ return;
1710
+ }
1711
+ throw themeTypeError(path, `${expectedCategory} value`, value);
1712
+ }
1713
+ function assertThemeTokenReferenceNode(reference, expectedCategory, path) {
1714
+ assertExactThemeKeys(reference, path, ["category", "kind", "token"]);
1715
+ assertRequiredThemeKeys(reference, path, ["category", "kind", "token"]);
1716
+ if (!isThemeTokenCategory(reference.category)) {
1717
+ throw themeTypeError(`${path}.category`, "color, font, image, or number", reference.category);
1718
+ }
1719
+ if (typeof reference.token !== "string") {
1720
+ throw themeTypeError(`${path}.token`, "token name", reference.token);
1721
+ }
1722
+ assertTokenName(reference.token, reference.category);
1723
+ if (reference.category !== expectedCategory) {
1724
+ throw new Error(
1725
+ `Tileflow theme token type mismatch at ${path}; expected ${expectedCategory}, received ${reference.category} token "${reference.token}".`
1726
+ );
1727
+ }
1728
+ }
1729
+ function assertThemeFixedNode(fixedValue, expectedCategory, path) {
1730
+ assertExactThemeKeys(fixedValue, path, ["kind", "reason", "value"]);
1731
+ assertRequiredThemeKeys(fixedValue, path, ["kind", "reason", "value"]);
1732
+ if (typeof fixedValue.reason !== "string" || !fixedValue.reason.trim()) {
1733
+ throw new Error(`Tileflow fixed value at ${path} requires a non-empty reason.`);
1734
+ }
1735
+ assertTokenPrimitive(expectedCategory, fixedValue.value, `${path}.value`);
1736
+ }
1737
+ function assertThemeColorOperationNode(operation, path) {
1738
+ if (operation.operation === "alpha") {
1739
+ assertExactThemeKeys(operation, path, ["color", "kind", "opacity", "operation"]);
1740
+ assertRequiredThemeKeys(operation, path, ["color", "kind", "opacity", "operation"]);
1741
+ assertThemeCategoryValue(operation.color, "color", `${path}.color`);
1742
+ assertThemeCategoryValue(operation.opacity, "number", `${path}.opacity`);
1743
+ return;
1744
+ }
1745
+ if (operation.operation === "mix") {
1746
+ assertExactThemeKeys(operation, path, ["amount", "from", "kind", "operation", "space", "to"]);
1747
+ assertRequiredThemeKeys(operation, path, [
1748
+ "amount",
1749
+ "from",
1750
+ "kind",
1751
+ "operation",
1752
+ "space",
1753
+ "to"
1754
+ ]);
1755
+ if (operation.space !== "oklch") {
1756
+ throw new Error(`Tileflow theme color mix at ${path} must use the oklch color space.`);
1757
+ }
1758
+ assertThemeCategoryValue(operation.from, "color", `${path}.from`);
1759
+ assertThemeCategoryValue(operation.to, "color", `${path}.to`);
1760
+ assertThemeCategoryValue(operation.amount, "number", `${path}.amount`);
1761
+ return;
1762
+ }
1763
+ throw themeTypeError(`${path}.operation`, "alpha or mix", operation.operation);
1764
+ }
1765
+ function assertPlainThemeRecord(value, path) {
1766
+ if (!isPlainRecord2(value)) throw themeTypeError(path, "plain object", value);
1767
+ return value;
1768
+ }
1769
+ function assertExactThemeKeys(value, path, allowedKeys) {
1770
+ const allowed = new Set(allowedKeys);
1771
+ for (const key of Reflect.ownKeys(value)) {
1772
+ if (typeof key !== "string" || !allowed.has(key)) {
1773
+ throw new Error(`Unknown Tileflow theme field ${JSON.stringify(String(key))} at ${path}.`);
1774
+ }
1775
+ }
1776
+ }
1777
+ function assertRequiredThemeKeys(value, path, requiredKeys) {
1778
+ for (const key of requiredKeys) {
1779
+ if (!Object.hasOwn(value, key)) {
1780
+ throw new Error(`Missing Tileflow theme field ${JSON.stringify(key)} at ${path}.`);
1781
+ }
1782
+ }
1783
+ }
1784
+ function isThemeTokenCategory(value) {
1785
+ return tileflowThemeTokenCategories.includes(value);
1786
+ }
1787
+ function assertTokenPrimitive(category, value, path) {
1788
+ if (category === "number") {
1789
+ assertFiniteNumber(value, path);
1790
+ return;
1791
+ }
1792
+ if (typeof value !== "string" || value.length === 0) {
1793
+ throw themeTypeError(path, category, value);
1794
+ }
1795
+ if (category === "color") assertColor(value, path);
1796
+ if (category === "font") assertFontName(value, path);
1797
+ if (category === "image") assertImageName(value, path);
1798
+ }
1799
+ function assertThemeNodeResolutionContext(value, expectedCategory, path) {
1800
+ if (value.kind === "theme-token") {
1801
+ if (!isThemeTokenCategory(value.category)) {
1802
+ assertExactThemeKeys(value, path, ["category", "kind", "token"]);
1803
+ assertRequiredThemeKeys(value, path, ["category", "kind", "token"]);
1804
+ throw themeTypeError(`${path}.category`, "color, font, image, or number", value.category);
1805
+ }
1806
+ assertThemeTokenReferenceNode(value, expectedCategory ?? value.category, path);
1807
+ if (!expectedCategory) throwThemeValueOutsideVisualField(path);
1808
+ return;
1809
+ }
1810
+ if (value.kind === "theme-fixed") {
1811
+ assertExactThemeKeys(value, path, ["kind", "reason", "value"]);
1812
+ assertRequiredThemeKeys(value, path, ["kind", "reason", "value"]);
1813
+ if (typeof value.reason !== "string" || !value.reason.trim()) {
1814
+ throw new Error(`Tileflow fixed value at ${path} requires a non-empty reason.`);
1815
+ }
1816
+ if (!expectedCategory) throwThemeValueOutsideVisualField(path);
1817
+ if (!isThemePrimitiveForCategory(value.value, expectedCategory)) {
1818
+ throw themeTypeError(path, `${expectedCategory} value`, value.value);
1819
+ }
1820
+ return;
1821
+ }
1822
+ if (value.kind === "theme-color") {
1823
+ assertThemeColorOperationNode(value, path);
1824
+ if (!expectedCategory) throwThemeValueOutsideVisualField(path);
1825
+ if (expectedCategory !== "color") {
1826
+ throw themeTypeError(path, `${expectedCategory} value`, value);
1827
+ }
1828
+ }
1829
+ }
1830
+ function throwThemeValueOutsideVisualField(path) {
1831
+ throw new Error(
1832
+ `Tileflow theme value at ${path} is outside a categorized visual property; theme values cannot control filters, zoom bounds, visibility, metadata, or structure.`
1833
+ );
1834
+ }
1835
+ function inferThemeCategoryFromPath(path) {
1836
+ const tokenCategory = path.match(/^theme\.tokens\.(color|font|image|number)(?:\.|$)/u)?.[1];
1837
+ if (isThemeTokenCategory(tokenCategory)) return tokenCategory;
1838
+ if (/^theme\.lighting\.position(?:\[\d+\])?$/u.test(path)) return "number";
1839
+ if (/^theme\.lighting\.sun\.(?:altitude|azimuth|shadowIntensity)$/u.test(path)) return "number";
1840
+ const segments = path.replace(/\[\d+\]/g, "").split(".");
1841
+ return [...segments].reverse().map((segment) => classifyTileflowVisualProperty(segment)).find((category) => category !== void 0);
1842
+ }
1843
+ function isThemeStructuralPath(path) {
1844
+ const segments = path.replace(/\[\d+\]/g, "").split(".");
1845
+ const modulesIndex = segments.indexOf("modules");
1846
+ const dataIndex = segments.indexOf("data");
1847
+ if (dataIndex >= 0 && segments[dataIndex + 1] === "schema" && ["fields", "layers"].includes(segments[dataIndex + 2] ?? "")) {
1848
+ return true;
1849
+ }
1850
+ if (modulesIndex >= 0 && segments[modulesIndex + 1] === "poi" && segments.length === modulesIndex + 3 && segments[modulesIndex + 2] === "color") {
1851
+ return true;
1852
+ }
1853
+ const terrainIndex = segments.indexOf("terrain");
1854
+ const firstPropertyIndex = modulesIndex >= 0 ? modulesIndex + 2 : terrainIndex >= 0 ? terrainIndex + 1 : segments[0] === "map" ? 2 : 0;
1855
+ return segments.slice(firstPropertyIndex).some((segment) => themeStructuralPathSegments.has(segment));
1856
+ }
1857
+ function assertTokenName(name, category) {
1858
+ if (!/^[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z][A-Za-z0-9_-]*)*$/.test(name)) {
1859
+ throw new Error(
1860
+ `Invalid Tileflow ${category} token ${JSON.stringify(name)}; expected a portable dot-separated semantic name.`
1861
+ );
1862
+ }
1863
+ }
1864
+ function isThemeName(value) {
1865
+ return isTileflowThemeName(value);
1866
+ }
1867
+ function mixOklch(from, to, amount) {
1868
+ const a = rgbaToOklch(parseColor(from));
1869
+ const b = rgbaToOklch(parseColor(to));
1870
+ let hueDelta = b.h - a.h;
1871
+ if (hueDelta > 180) hueDelta -= 360;
1872
+ if (hueDelta < -180) hueDelta += 360;
1873
+ const rgba = oklchToRgba({
1874
+ c: interpolate(a.c, b.c, amount),
1875
+ h: (a.h + hueDelta * amount + 360) % 360,
1876
+ l: interpolate(a.l, b.l, amount)
1877
+ });
1878
+ const alpha = interpolate(parseColor(from).a, parseColor(to).a, amount);
1879
+ return formatColor({ ...rgba, a: alpha });
1880
+ }
1881
+ function applyAlpha(value, opacity) {
1882
+ const rgba = parseColor(value);
1883
+ return formatSrgbColor({ ...rgba, a: rgba.a * opacity });
1884
+ }
1885
+ function parseColor(value) {
1886
+ const hex = value.match(/^#([0-9a-f]{3,8})$/i)?.[1];
1887
+ if (hex) {
1888
+ const expanded = hex.length === 3 || hex.length === 4 ? [...hex].map((entry) => entry + entry).join("") : hex;
1889
+ if (expanded.length === 6 || expanded.length === 8) {
1890
+ return {
1891
+ a: expanded.length === 8 ? Number.parseInt(expanded.slice(6), 16) / 255 : 1,
1892
+ b: Number.parseInt(expanded.slice(4, 6), 16) / 255,
1893
+ g: Number.parseInt(expanded.slice(2, 4), 16) / 255,
1894
+ r: Number.parseInt(expanded.slice(0, 2), 16) / 255
1895
+ };
1896
+ }
1897
+ }
1898
+ const rgb = value.match(
1899
+ /^rgba?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)(?:\s*,\s*(\d+(?:\.\d+)?))?\s*\)$/i
1900
+ );
1901
+ if (rgb) {
1902
+ const result = {
1903
+ a: Number(rgb[4] ?? 1),
1904
+ b: Number(rgb[3]) / 255,
1905
+ g: Number(rgb[2]) / 255,
1906
+ r: Number(rgb[1]) / 255
1907
+ };
1908
+ if (Object.values(result).every((entry) => Number.isFinite(entry) && entry >= 0 && entry <= 1)) {
1909
+ return result;
1910
+ }
1911
+ }
1912
+ throw new Error(`Invalid Tileflow theme color ${JSON.stringify(value)}.`);
1913
+ }
1914
+ function rgbaToOklch({ b, g, r }) {
1915
+ const linear = [r, g, b].map(srgbToLinear);
1916
+ const l = 0.4122214708 * linear[0] + 0.5363325363 * linear[1] + 0.0514459929 * linear[2];
1917
+ const m = 0.2119034982 * linear[0] + 0.6806995451 * linear[1] + 0.1073969566 * linear[2];
1918
+ const s = 0.0883024619 * linear[0] + 0.2817188376 * linear[1] + 0.6299787005 * linear[2];
1919
+ const lRoot = Math.cbrt(l);
1920
+ const mRoot = Math.cbrt(m);
1921
+ const sRoot = Math.cbrt(s);
1922
+ const labA = 1.9779984951 * lRoot - 2.428592205 * mRoot + 0.4505937099 * sRoot;
1923
+ const labB = 0.0259040371 * lRoot + 0.7827717662 * mRoot - 0.808675766 * sRoot;
1924
+ return {
1925
+ c: Math.hypot(labA, labB),
1926
+ h: Math.atan2(labB, labA) * 180 / Math.PI + 360,
1927
+ l: 0.2104542553 * lRoot + 0.793617785 * mRoot - 0.0040720468 * sRoot
1928
+ };
1929
+ }
1930
+ function oklchToRgba({ c, h, l }) {
1931
+ const radians = h * Math.PI / 180;
1932
+ const labA = c * Math.cos(radians);
1933
+ const labB = c * Math.sin(radians);
1934
+ const lRoot = l + 0.3963377774 * labA + 0.2158037573 * labB;
1935
+ const mRoot = l - 0.1055613458 * labA - 0.0638541728 * labB;
1936
+ const sRoot = l - 0.0894841775 * labA - 1.291485548 * labB;
1937
+ const lLinear = lRoot ** 3;
1938
+ const mLinear = mRoot ** 3;
1939
+ const sLinear = sRoot ** 3;
1940
+ return {
1941
+ a: 1,
1942
+ b: clamp01(-0.0041960863 * lLinear - 0.7034186147 * mLinear + 1.707614701 * sLinear),
1943
+ g: clamp01(-1.2684380046 * lLinear + 2.6097574011 * mLinear - 0.3413193965 * sLinear),
1944
+ r: clamp01(4.0767416621 * lLinear - 3.3077115913 * mLinear + 0.2309699292 * sLinear)
1945
+ };
1946
+ }
1947
+ function formatColor({ a, b, g, r }) {
1948
+ const channels = [r, g, b].map((entry) => Math.round(linearToSrgb(entry) * 255));
1949
+ if (a >= 0.9995) {
1950
+ return `#${channels.map((entry) => entry.toString(16).padStart(2, "0")).join("")}`;
1951
+ }
1952
+ return `rgba(${channels[0]}, ${channels[1]}, ${channels[2]}, ${round(a, 4)})`;
1953
+ }
1954
+ function formatSrgbColor({ a, b, g, r }) {
1955
+ const channels = [r, g, b].map((entry) => Math.round(clamp01(entry) * 255));
1956
+ if (a >= 0.9995) {
1957
+ return `#${channels.map((entry) => entry.toString(16).padStart(2, "0")).join("")}`;
1958
+ }
1959
+ return `rgba(${channels[0]}, ${channels[1]}, ${channels[2]}, ${round(a, 4)})`;
1960
+ }
1961
+ function srgbToLinear(value) {
1962
+ return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
1963
+ }
1964
+ function linearToSrgb(value) {
1965
+ const clamped = clamp01(value);
1966
+ return clamped <= 31308e-7 ? clamped * 12.92 : 1.055 * clamped ** (1 / 2.4) - 0.055;
1967
+ }
1968
+ function assertColor(value, path) {
1969
+ if (typeof value !== "string") throw themeTypeError(path, "color", value);
1970
+ try {
1971
+ parseColor(value);
1972
+ } catch {
1973
+ throw new Error(`Invalid Tileflow theme color ${JSON.stringify(value)} at ${path}.`);
1974
+ }
1975
+ }
1976
+ function assertFiniteNumber(value, path) {
1977
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1978
+ throw themeTypeError(path, "finite number", value);
1979
+ }
1980
+ }
1981
+ function assertFontName(value, path) {
1982
+ if (value.length > 100 || value !== value.trim() || value !== value.normalize("NFC") || /[\p{Cc}\\]/u.test(value)) {
1983
+ throw new Error(
1984
+ `Invalid Tileflow font ${JSON.stringify(value)} at ${path}; expected an exact NFC face name without whitespace, controls, or backslashes.`
1985
+ );
1986
+ }
1987
+ }
1988
+ function assertImageName(value, path) {
1989
+ if (value.length > 128 || value !== value.trim() || value !== value.normalize("NFC") || /[\p{Cc}\\]/u.test(value)) {
1990
+ throw new Error(`Invalid Tileflow image token value ${JSON.stringify(value)} at ${path}.`);
1991
+ }
1992
+ }
1993
+ function assertUnitInterval(value, path) {
1994
+ assertFiniteNumber(value, path);
1995
+ if (value < 0 || value > 1) {
1996
+ throw new Error(`Tileflow theme value at ${path} must be between 0 and 1.`);
1997
+ }
1998
+ }
1999
+ function themeTypeError(path, expected, value) {
2000
+ return new Error(
2001
+ `Invalid Tileflow theme value at ${path}; expected ${expected}, received ${describe(value)}.`
2002
+ );
2003
+ }
2004
+ function describe(value) {
2005
+ if (value === null) return "null";
2006
+ if (Array.isArray(value)) return "array";
2007
+ return typeof value;
2008
+ }
2009
+ function interpolate(from, to, amount) {
2010
+ return from + (to - from) * amount;
2011
+ }
2012
+ function clamp01(value) {
2013
+ return Math.max(0, Math.min(1, value));
2014
+ }
2015
+ function round(value, digits) {
2016
+ const power = 10 ** digits;
2017
+ return Math.round(value * power) / power;
2018
+ }
2019
+ function cloneJson(value) {
2020
+ const serialized = JSON.stringify(value);
2021
+ if (serialized === void 0) throw new Error("Tileflow themes must be JSON-serializable.");
2022
+ return JSON.parse(serialized);
2023
+ }
2024
+ function isRecord3(value) {
2025
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
2026
+ }
2027
+ function isPlainRecord2(value) {
2028
+ if (!isRecord3(value)) return false;
2029
+ const prototype = Object.getPrototypeOf(value);
2030
+ return prototype === Object.prototype || prototype === null;
2031
+ }
2032
+ function compareCodeUnits(left, right) {
2033
+ return left < right ? -1 : left > right ? 1 : 0;
2034
+ }
2035
+ var tileflowThemeLimits, TileflowThemeAuditError, themeDocumentKeys, themeTypographyStyleKeys, themeTypographyKeys, themeTypographyDomains, themeLightingKeys, themeSunKeys, themeAmbientKeys, expressionDataOperators, ThemeValueResolver, themeStructuralPathSegments;
2036
+ var init_model = __esm({
2037
+ "src/themes/model.ts"() {
2038
+ "use strict";
2039
+ init_expression_operators();
2040
+ init_values();
2041
+ init_operations();
2042
+ init_portable_identity_rules();
2043
+ init_sun();
2044
+ init_visual_semantics();
2045
+ tileflowThemeLimits = Object.freeze({ maxThemes: 64 });
2046
+ TileflowThemeAuditError = class extends Error {
2047
+ diagnostics;
2048
+ constructor(diagnostics) {
2049
+ const errors = diagnostics.filter(({ severity }) => severity === "error").sort(compareAuditDiagnostics).map((diagnostic) => ({ ...diagnostic }));
2050
+ const first = errors[0];
2051
+ super(
2052
+ `Tileflow theme audit failed with ${errors.length} blocking diagnostic${errors.length === 1 ? "" : "s"}${first ? `; first error at ${first.path}` : ""}.`
2053
+ );
2054
+ this.name = "TileflowThemeAuditError";
2055
+ this.diagnostics = errors;
2056
+ }
2057
+ };
2058
+ themeDocumentKeys = [
2059
+ "colorScheme",
2060
+ "id",
2061
+ "lighting",
2062
+ "tokens",
2063
+ "typography",
2064
+ "version"
2065
+ ];
2066
+ themeTypographyStyleKeys = ["fallbacks", "font", "letterSpacing", "transform"];
2067
+ themeTypographyKeys = [
2068
+ ...themeTypographyStyleKeys,
2069
+ "places",
2070
+ "poi",
2071
+ "roads",
2072
+ "water"
2073
+ ];
2074
+ themeTypographyDomains = ["places", "poi", "roads", "water"];
2075
+ themeLightingKeys = ["ambient", "anchor", "color", "intensity", "position", "sun"];
2076
+ themeSunKeys = ["altitude", "azimuth", "color", "intensity", "shadowIntensity"];
2077
+ themeAmbientKeys = ["color", "intensity"];
2078
+ expressionDataOperators = /* @__PURE__ */ new Set([
2079
+ "accumulated",
2080
+ "collator",
2081
+ "distance",
2082
+ "elevation",
2083
+ "error",
2084
+ "feature-state",
2085
+ "geometry-type",
2086
+ "get",
2087
+ "global-state",
2088
+ "has",
2089
+ "heatmap-density",
2090
+ "id",
2091
+ "in",
2092
+ "index-of",
2093
+ "is-supported-script",
2094
+ "length",
2095
+ "line-progress",
2096
+ "properties",
2097
+ "resolved-locale",
2098
+ "typeof",
2099
+ "within",
2100
+ "zoom"
2101
+ ]);
2102
+ ThemeValueResolver = class {
2103
+ #cache = /* @__PURE__ */ new Map();
2104
+ #stack = [];
2105
+ #theme;
2106
+ constructor(theme) {
2107
+ assertThemeDocument(theme);
2108
+ this.#theme = theme;
2109
+ }
2110
+ resolveAllTokens() {
2111
+ for (const category of tileflowThemeTokenCategories) {
2112
+ this.resolveCategory(category);
2113
+ }
2114
+ }
2115
+ resolveCategory(category) {
2116
+ return Object.fromEntries(
2117
+ Object.keys(this.#theme.tokens[category]).sort(compareCodeUnits).map((name) => [
2118
+ name,
2119
+ this.#resolveToken(category, name, `theme.tokens.${category}.${name}`)
2120
+ ])
2121
+ );
2122
+ }
2123
+ resolveTree(value, path, expectedCategory) {
2124
+ const category = isThemeStructuralPath(path) ? void 0 : expectedCategory ?? inferThemeCategoryFromPath(path);
2125
+ if (isThemeValueCandidate(value)) {
2126
+ assertThemeNodeResolutionContext(value, category, path);
2127
+ }
2128
+ if (isTileflowThemeTokenReference(value)) {
2129
+ return this.#resolveReference(value, path);
2130
+ }
2131
+ if (isTileflowFixedValue(value)) {
2132
+ return this.resolveTree(value.value, `${path}.value`, category);
2133
+ }
2134
+ if (isTileflowThemeColorOperation(value)) return this.#resolveColorOperation(value, path);
2135
+ if (Array.isArray(value)) {
2136
+ return value.map((entry, index) => this.resolveTree(entry, `${path}[${index}]`, category));
2137
+ }
2138
+ if (!isRecord3(value)) return value;
2139
+ if (value.kind === "expression" && Array.isArray(value.value)) {
2140
+ return {
2141
+ ...value,
2142
+ value: this.resolveTree(value.value, `${path}.value`, category)
2143
+ };
2144
+ }
2145
+ if (value.kind === "zoom" && Array.isArray(value.stops)) {
2146
+ return {
2147
+ ...value,
2148
+ stops: value.stops.map(
2149
+ (stop, index) => Array.isArray(stop) ? [
2150
+ this.resolveTree(stop[0], `${path}.stops[${index}][0]`),
2151
+ this.resolveTree(stop[1], `${path}.stops[${index}][1]`, category)
2152
+ ] : this.resolveTree(stop, `${path}.stops[${index}]`)
2153
+ )
2154
+ };
2155
+ }
2156
+ const result = {};
2157
+ for (const key of Reflect.ownKeys(value)) {
2158
+ const nextPath = typeof key === "symbol" ? `${path}.[${String(key)}]` : `${path}.${String(key)}`;
2159
+ const nextValue = value[key];
2160
+ const nextCategory = typeof key === "string" ? classifyTileflowVisualProperty(key, nextValue) : void 0;
2161
+ result[key] = this.resolveTree(nextValue, nextPath, nextCategory);
2162
+ }
2163
+ return result;
2164
+ }
2165
+ resolveTypography(typography) {
2166
+ const global = this.#resolveTypographyStyle(typography, "theme.typography", {
2167
+ font: "Noto Sans Regular"
2168
+ });
2169
+ return {
2170
+ ...global,
2171
+ places: this.#resolveTypographyStyle(typography.places, "theme.typography.places", global),
2172
+ poi: this.#resolveTypographyStyle(typography.poi, "theme.typography.poi", global),
2173
+ roads: this.#resolveTypographyStyle(typography.roads, "theme.typography.roads", global),
2174
+ water: this.#resolveTypographyStyle(typography.water, "theme.typography.water", global)
2175
+ };
2176
+ }
2177
+ resolveLighting(lighting) {
2178
+ const { ambient, sun, ...flat } = lighting;
2179
+ const light = this.#resolveFlatLight(flat);
2180
+ if (sun === void 0 && ambient === void 0) return { light };
2181
+ const resolvedSun = this.resolveTree(sun ?? {}, "theme.lighting.sun");
2182
+ const resolvedAmbient = this.resolveTree(ambient ?? {}, "theme.lighting.ambient");
2183
+ const defaults = tileflowDefaultSunPreset;
2184
+ const number = (value, path, fallback, range2) => {
2185
+ if (value === void 0) return fallback;
2186
+ assertFiniteNumber(value, path);
2187
+ if (range2 && (value < range2[0] || value > range2[1])) {
2188
+ throw new Error(`Tileflow ${path} must be between ${range2[0]} and ${range2[1]}.`);
2189
+ }
2190
+ return value;
2191
+ };
2192
+ const colour = (value, path, fallback) => {
2193
+ if (value === void 0) return fallback;
2194
+ assertColor(value, path);
2195
+ return value;
2196
+ };
2197
+ const preset = {
2198
+ azimuth: normalizeTileflowAzimuth(
2199
+ number(resolvedSun.azimuth, "theme.lighting.sun.azimuth", defaults.azimuth)
2200
+ ),
2201
+ altitude: number(
2202
+ resolvedSun.altitude,
2203
+ "theme.lighting.sun.altitude",
2204
+ defaults.altitude,
2205
+ [-90, 90]
2206
+ ),
2207
+ color: colour(resolvedSun.color, "theme.lighting.sun.color", defaults.color),
2208
+ intensity: number(
2209
+ resolvedSun.intensity,
2210
+ "theme.lighting.sun.intensity",
2211
+ defaults.intensity,
2212
+ [0, 1]
2213
+ ),
2214
+ shadowIntensity: number(
2215
+ resolvedSun.shadowIntensity,
2216
+ "theme.lighting.sun.shadowIntensity",
2217
+ defaults.shadowIntensity,
2218
+ [0, 1]
2219
+ ),
2220
+ ambient: {
2221
+ color: colour(
2222
+ resolvedAmbient.color,
2223
+ "theme.lighting.ambient.color",
2224
+ defaults.ambient.color
2225
+ ),
2226
+ intensity: number(
2227
+ resolvedAmbient.intensity,
2228
+ "theme.lighting.ambient.intensity",
2229
+ defaults.ambient.intensity,
2230
+ [0, 1]
2231
+ )
2232
+ }
2233
+ };
2234
+ return { light: sun === void 0 ? light : tileflowSunLight(preset, light), sun: preset };
2235
+ }
2236
+ #resolveFlatLight(lighting) {
2237
+ const result = this.resolveTree(lighting, "theme.lighting");
2238
+ if (result.anchor !== void 0 && result.anchor !== "map" && result.anchor !== "viewport") {
2239
+ throw themeTypeError("theme.lighting.anchor", "map or viewport", result.anchor);
2240
+ }
2241
+ if (result.color !== void 0) assertColor(result.color, "theme.lighting.color");
2242
+ if (result.intensity !== void 0) {
2243
+ assertFiniteNumber(result.intensity, "theme.lighting.intensity");
2244
+ if (result.intensity < 0 || result.intensity > 1) {
2245
+ throw new Error("Tileflow theme.lighting.intensity must be between 0 and 1.");
2246
+ }
2247
+ }
2248
+ if (result.position !== void 0) {
2249
+ if (!Array.isArray(result.position) || result.position.length !== 3 || result.position.some((entry) => typeof entry !== "number" || !Number.isFinite(entry))) {
2250
+ throw themeTypeError("theme.lighting.position", "three finite numbers", result.position);
2251
+ }
2252
+ }
2253
+ return result;
2254
+ }
2255
+ #resolveTypographyStyle(style, path, defaults) {
2256
+ const resolved = this.resolveTree(style ?? {}, path);
2257
+ const font = resolved.font ?? defaults.font;
2258
+ if (typeof font !== "string" || !font) throw themeTypeError(`${path}.font`, "font", font);
2259
+ assertFontName(font, `${path}.font`);
2260
+ const fallbacks = resolved.fallbacks ?? defaults.fallbacks;
2261
+ if (fallbacks !== void 0 && (!Array.isArray(fallbacks) || fallbacks.some((entry) => typeof entry !== "string" || !entry))) {
2262
+ throw themeTypeError(`${path}.fallbacks`, "font array", fallbacks);
2263
+ }
2264
+ if (Array.isArray(fallbacks)) {
2265
+ const seen = /* @__PURE__ */ new Set();
2266
+ for (const [index, fallback] of fallbacks.entries()) {
2267
+ assertFontName(fallback, `${path}.fallbacks[${index}]`);
2268
+ if (seen.has(fallback)) {
2269
+ throw new Error(
2270
+ `Duplicate Tileflow font fallback ${JSON.stringify(fallback)} at ${path}.fallbacks[${index}].`
2271
+ );
2272
+ }
2273
+ seen.add(fallback);
2274
+ }
2275
+ }
2276
+ const letterSpacing = resolved.letterSpacing ?? defaults.letterSpacing;
2277
+ if (letterSpacing !== void 0) assertFiniteNumber(letterSpacing, `${path}.letterSpacing`);
2278
+ const transform = resolved.transform ?? defaults.transform;
2279
+ if (transform !== void 0 && !["lowercase", "none", "uppercase"].includes(String(transform))) {
2280
+ throw themeTypeError(`${path}.transform`, "text transform", transform);
2281
+ }
2282
+ return {
2283
+ font,
2284
+ ...fallbacks === void 0 ? {} : { fallbacks: [...fallbacks] },
2285
+ ...letterSpacing === void 0 ? {} : { letterSpacing },
2286
+ ...transform === void 0 ? {} : { transform }
2287
+ };
2288
+ }
2289
+ #resolveReference(reference, path) {
2290
+ return this.#resolveToken(reference.category, reference.token, path);
2291
+ }
2292
+ #resolveToken(category, name, consumedAt) {
2293
+ const cacheKey = `${category}:${name}`;
2294
+ const cached = this.#cache.get(cacheKey);
2295
+ if (cached !== void 0) return cached;
2296
+ const tokens = this.#theme.tokens[category];
2297
+ if (!Object.hasOwn(tokens, name)) {
2298
+ throw new Error(
2299
+ `Unknown Tileflow ${category} token "${name}" at ${consumedAt} in theme "${this.#theme.id}".`
2300
+ );
2301
+ }
2302
+ const cycleStart = this.#stack.indexOf(cacheKey);
2303
+ if (cycleStart >= 0) {
2304
+ const cycle = [...this.#stack.slice(cycleStart), cacheKey].map((key) => key.replace(":", ".")).join(" -> ");
2305
+ throw new Error(`Circular Tileflow theme token reference: ${cycle}.`);
2306
+ }
2307
+ this.#stack.push(cacheKey);
2308
+ try {
2309
+ const path = `theme.tokens.${category}.${name}`;
2310
+ const value = this.resolveTree(tokens[name], path, category);
2311
+ assertTokenPrimitive(category, value, path);
2312
+ this.#cache.set(cacheKey, value);
2313
+ return value;
2314
+ } finally {
2315
+ this.#stack.pop();
2316
+ }
2317
+ }
2318
+ #resolveColorOperation(operation, path) {
2319
+ if (operation.operation === "alpha") {
2320
+ const source = this.resolveTree(operation.color, `${path}.color`, "color");
2321
+ const opacity = this.resolveTree(operation.opacity, `${path}.opacity`, "number");
2322
+ assertColor(source, `${path}.color`);
2323
+ assertUnitInterval(opacity, `${path}.opacity`);
2324
+ return applyAlpha(source, opacity);
2325
+ }
2326
+ const from = this.resolveTree(operation.from, `${path}.from`, "color");
2327
+ const to = this.resolveTree(operation.to, `${path}.to`, "color");
2328
+ const amount = this.resolveTree(operation.amount, `${path}.amount`, "number");
2329
+ assertColor(from, `${path}.from`);
2330
+ assertColor(to, `${path}.to`);
2331
+ assertUnitInterval(amount, `${path}.amount`);
2332
+ if (operation.space !== "oklch") {
2333
+ throw new Error(`Tileflow theme color mix at ${path} must use the oklch color space.`);
2334
+ }
2335
+ return mixOklch(from, to, amount);
2336
+ }
2337
+ };
2338
+ themeStructuralPathSegments = /* @__PURE__ */ new Set([
2339
+ "after",
2340
+ "before",
2341
+ "filter",
2342
+ "id",
2343
+ "maxZoom",
2344
+ "maxzoom",
2345
+ "metadata",
2346
+ "minZoom",
2347
+ "minzoom",
2348
+ "owner",
2349
+ "placement",
2350
+ "requires",
2351
+ "source",
2352
+ "source-layer",
2353
+ "sourceLayer",
2354
+ "target",
2355
+ "type",
2356
+ "visibility"
2357
+ ]);
2358
+ }
2359
+ });
2360
+
2361
+ // src/color-program.ts
2362
+ function colorProgramMarker(value, program) {
2363
+ return [
2364
+ "let",
2365
+ marker,
2366
+ ["literal", program],
2367
+ "__tileflow_color_value",
2368
+ value,
2369
+ ["var", "__tileflow_color_value"]
2370
+ ];
2371
+ }
2372
+ function isColorProgramMarker(value) {
2373
+ return Array.isArray(value) && value.length === 6 && value[0] === "let" && value[1] === marker && Array.isArray(value[2]) && value[2][0] === "literal";
2374
+ }
2375
+ function colorProgramValue(value) {
2376
+ return isColorProgramMarker(value) ? value[4] : value;
2377
+ }
2378
+ function colorProgramOf(value) {
2379
+ return isColorProgramMarker(value) ? value[2][1] : value;
2380
+ }
2381
+ function extractTileflowColorPrograms(style, tokens) {
2382
+ const programs = [];
2383
+ const ids = /* @__PURE__ */ new Map();
2384
+ const bindings = [];
2385
+ const visit = (node, path) => {
2386
+ if (record2(node) && node.kind === "expression" && isColorProgramMarker(node.value))
2387
+ return visit(node.value, path);
2388
+ if (isColorProgramMarker(node)) {
2389
+ const program = colorProgramOf(node);
2390
+ const key = JSON.stringify(program);
2391
+ let id = ids.get(key);
2392
+ if (id === void 0) {
2393
+ id = programs.length;
2394
+ ids.set(key, id);
2395
+ programs.push(program);
2396
+ }
2397
+ bindings.push([
2398
+ path[0] === "layers" && path[2] === "paint" ? [path[1], ...path.slice(3)] : path,
2399
+ id
2400
+ ]);
2401
+ return colorProgramValue(node);
2402
+ }
2403
+ if (Array.isArray(node)) return node.map((v, i) => visit(v, [...path, i]));
2404
+ if (record2(node))
2405
+ return Object.fromEntries(
2406
+ Object.entries(node).filter(([key]) => key !== tileflowColorMetadataKey).map(([key, v]) => [key, visit(v, [...path, key])])
2407
+ );
2408
+ return node;
2409
+ };
2410
+ const result = visit(style, []);
2411
+ if (bindings.length)
2412
+ result.metadata = {
2413
+ ...result.metadata,
2414
+ // Keep the declared catalog shape across presets. Per-preset pruning makes a shared
2415
+ // object structurally different and repeats the entire catalog in the transport.
2416
+ [tileflowColorMetadataKey]: { version: 1, programs, bindings, tokens }
2417
+ };
2418
+ return result;
2419
+ }
2420
+ function restoreTileflowColorPrograms(style) {
2421
+ const result = structuredClone(style);
2422
+ const metadata = result.metadata?.[tileflowColorMetadataKey];
2423
+ if (!metadata) return result;
2424
+ validateBindings(metadata);
2425
+ for (const [path, id] of metadata.bindings)
2426
+ updatePath(result, path, (value) => colorProgramMarker(value, metadata.programs[id]));
2427
+ return result;
2428
+ }
2429
+ function updatePath(root, inputPath, change) {
2430
+ const path = typeof inputPath[0] === "number" ? ["layers", inputPath[0], "paint", ...inputPath.slice(1)] : inputPath;
2431
+ if (!path.length || path.length > 96 || path.some((key) => ["__proto__", "constructor", "prototype"].includes(String(key))))
2432
+ throw new TypeError("Invalid colour binding path.");
2433
+ let node = root;
2434
+ for (const key of path.slice(0, -1)) {
2435
+ if (!node || typeof node !== "object" || !Object.hasOwn(node, key))
2436
+ throw new TypeError("Missing colour binding path.");
2437
+ node = node[key];
2438
+ }
2439
+ const last = path.at(-1);
2440
+ if (!node || !Object.hasOwn(node, last)) throw new TypeError("Missing colour binding value.");
2441
+ node[last] = change(node[last]);
2442
+ }
2443
+ function validateBindings(value) {
2444
+ if (value.version !== 1 || !Array.isArray(value.programs) || value.programs.length > 4096 || !Array.isArray(value.bindings) || value.bindings.length > 16384 || !record2(value.tokens?.color) || !record2(value.tokens?.number))
2445
+ throw new TypeError("Invalid colour program catalog.");
2446
+ for (const [path, id] of value.bindings) {
2447
+ if (!Array.isArray(path) || !path.length || path.some((key) => typeof key !== "string" && !Number.isSafeInteger(key)) || !Number.isSafeInteger(id) || id < 0 || id >= value.programs.length || !(typeof path[0] === "number" || ["metadata", "light", "sky"].includes(String(path[0]))))
2448
+ throw new TypeError("Invalid colour program binding.");
2449
+ if (typeof path[0] === "number" && (path[0] < 0 || !String(path[1]).endsWith("color")))
2450
+ throw new TypeError("Colour programs can only change paint colours.");
2451
+ if (path[0] === "metadata" && (path[1] === tileflowColorMetadataKey || ![
2452
+ "tileflow:buildings",
2453
+ "tileflow:vegetation",
2454
+ "tileflow:sun",
2455
+ "tileflow:sky",
2456
+ "tileflow:route"
2457
+ ].includes(String(path[1]))))
2458
+ throw new TypeError("Invalid colour metadata binding.");
2459
+ }
2460
+ }
2461
+ function record2(value) {
2462
+ return !!value && typeof value === "object" && !Array.isArray(value);
2463
+ }
2464
+ var tileflowColorMetadataKey, marker;
2465
+ var init_color_program = __esm({
2466
+ "src/color-program.ts"() {
2467
+ "use strict";
2468
+ tileflowColorMetadataKey = "tileflow:colors";
2469
+ marker = "__tileflow_color_program";
2470
+ }
2471
+ });
2472
+
89
2473
  // src/manifest-types.ts
90
- var tileflowRuntimeManifestVersion, tileflowTimeOfDayManifestVersion, tileflowRuntimeManifestLimits;
2474
+ var tileflowRuntimeManifestVersion, tileflowTimeOfDayManifestVersion, tileflowFamilyManifestVersion, tileflowRuntimeManifestLimits;
91
2475
  var init_manifest_types = __esm({
92
2476
  "src/manifest-types.ts"() {
93
2477
  "use strict";
94
2478
  tileflowRuntimeManifestVersion = 1;
95
2479
  tileflowTimeOfDayManifestVersion = 2;
2480
+ tileflowFamilyManifestVersion = 3;
96
2481
  tileflowRuntimeManifestLimits = Object.freeze({ maximumBytes: 1024 * 1024 });
97
2482
  }
98
2483
  });
@@ -154,23 +2539,6 @@ var init_runtime_view_schema = __esm({
154
2539
  }
155
2540
  });
156
2541
 
157
- // src/themes/model.ts
158
- var tileflowThemeLimits, themeTypographyStyleKeys, themeTypographyKeys;
159
- var init_model = __esm({
160
- "src/themes/model.ts"() {
161
- "use strict";
162
- tileflowThemeLimits = Object.freeze({ maxThemes: 64 });
163
- themeTypographyStyleKeys = ["fallbacks", "font", "letterSpacing", "transform"];
164
- themeTypographyKeys = [
165
- ...themeTypographyStyleKeys,
166
- "places",
167
- "poi",
168
- "roads",
169
- "water"
170
- ];
171
- }
172
- });
173
-
174
2542
  // src/time-of-day-schema.ts
175
2543
  function createTileflowTimeOfDaySchema(operations, themeName) {
176
2544
  return operations.record(
@@ -236,6 +2604,7 @@ function createTileflowRuntimeManifestSchema(primitives, operations, identities
236
2604
  apiUrl: operations.optional(apiOriginSchema),
237
2605
  defaultTheme: tileflowThemeNameSchema2,
238
2606
  environment: operations.optional(operations.maxLength(safeTextSchema, 128)),
2607
+ familyUrl: operations.optional(publicResourceUrlSchema),
239
2608
  mapId: operations.optional(operations.maxLength(safeTextSchema, 128)),
240
2609
  systemThemes: operations.optional(
241
2610
  operations.object({
@@ -317,11 +2686,20 @@ function createTileflowRuntimeManifestSchema(primitives, operations, identities
317
2686
  maps: operations.record(tileflowPortableIdSchema2, manifestMapSchema),
318
2687
  version: operations.union(
319
2688
  operations.literal(tileflowRuntimeManifestVersion),
320
- operations.literal(2)
2689
+ operations.union(operations.literal(2), operations.literal(3))
321
2690
  )
322
2691
  }),
323
2692
  (manifest, context) => {
324
2693
  const mapCount = Object.keys(manifest.maps).length;
2694
+ if (manifest.version !== 3)
2695
+ for (const [name, map] of Object.entries(manifest.maps)) {
2696
+ if (map.familyUrl !== void 0)
2697
+ context.addIssue({
2698
+ code: "custom",
2699
+ message: "familyUrl requires manifest version 3",
2700
+ path: ["maps", name, "familyUrl"]
2701
+ });
2702
+ }
325
2703
  if (manifest.version === 1) {
326
2704
  for (const [name, map] of Object.entries(manifest.maps)) {
327
2705
  if (map.timeOfDay !== void 0)
@@ -534,6 +2912,7 @@ var manifest_exports = {};
534
2912
  __export(manifest_exports, {
535
2913
  parseTileflowRuntimeManifest: () => parseTileflowRuntimeManifest,
536
2914
  safeParseTileflowRuntimeManifest: () => safeParseTileflowRuntimeManifest,
2915
+ tileflowFamilyManifestVersion: () => tileflowFamilyManifestVersion,
537
2916
  tileflowRuntimeManifestLimits: () => tileflowRuntimeManifestLimits,
538
2917
  tileflowRuntimeManifestSchema: () => tileflowRuntimeManifestSchema,
539
2918
  tileflowRuntimeManifestVersion: () => tileflowRuntimeManifestVersion,
@@ -567,6 +2946,664 @@ var init_manifest = __esm({
567
2946
  // src/runtime.ts
568
2947
  init_portable_identity_rules();
569
2948
  init_time_of_day();
2949
+
2950
+ // src/style-family.ts
2951
+ init_portable_identity_rules();
2952
+ var tileflowStyleFamilyLimits = Object.freeze({
2953
+ themes: 64,
2954
+ documentBytes: 16 * 1024 * 1024,
2955
+ expandedStyleBytes: 8 * 1024 * 1024,
2956
+ expandedFamilyBytes: 32 * 1024 * 1024,
2957
+ layers: 4096,
2958
+ nodes: 5e5,
2959
+ depth: 96
2960
+ });
2961
+ function createTileflowStyleFamily(styles) {
2962
+ const themes = Object.keys(styles).sort();
2963
+ if (!themes.length || themes.length > tileflowStyleFamilyLimits.themes || themes.some((name) => !isTileflowThemeName(name)))
2964
+ fail("Invalid theme names.");
2965
+ const parameters = [];
2966
+ const columns = /* @__PURE__ */ new Map();
2967
+ const add = (values) => {
2968
+ const key = serializeCanonicalJson(values);
2969
+ let index = columns.get(key);
2970
+ if (index === void 0) {
2971
+ index = parameters.length;
2972
+ columns.set(key, index);
2973
+ parameters.push(values);
2974
+ }
2975
+ return { $variant: index };
2976
+ };
2977
+ const factor = (values) => {
2978
+ const first = values[0];
2979
+ if (values.some((value) => isReference(value) || isConstantReference(value)))
2980
+ return add(values);
2981
+ if (Array.isArray(first) && values.every((v) => Array.isArray(v) && v.length === first.length))
2982
+ return first.map((_, index) => factor(values.map((v) => v[index])));
2983
+ if (record(first) && values.every((v) => record(v) && same(Object.keys(v).sort(), Object.keys(first).sort())))
2984
+ return Object.fromEntries(
2985
+ Object.keys(first).sort().map((key) => [key, factor(values.map((v) => v[key]))])
2986
+ );
2987
+ if (values.every((v) => same(v, first))) return first;
2988
+ return add(values);
2989
+ };
2990
+ const documents = themes.map((name) => JSON.parse(serializeCanonicalJson(styles[name])));
2991
+ for (const document2 of documents) assertStyle(document2);
2992
+ const template = factor(documents);
2993
+ const packed = shareConstants(template);
2994
+ const family = {
2995
+ kind: "tileflow-style-family",
2996
+ version: 1,
2997
+ themes,
2998
+ template: packed.template,
2999
+ constants: packed.constants,
3000
+ parameters,
3001
+ updates: planUpdates(template)
3002
+ };
3003
+ return parseTileflowStyleFamily(family);
3004
+ }
3005
+ function parseTileflowStyleFamily(input) {
3006
+ assertJsonBudget(input);
3007
+ if (!record(input) || !same(Object.keys(input).sort(), [
3008
+ "constants",
3009
+ "kind",
3010
+ "parameters",
3011
+ "template",
3012
+ "themes",
3013
+ "updates",
3014
+ "version"
3015
+ ]) || input.kind !== "tileflow-style-family" || input.version !== 1 || !Array.isArray(input.themes) || !input.themes.length || input.themes.length > tileflowStyleFamilyLimits.themes || input.themes.some((name) => typeof name !== "string" || !isTileflowThemeName(name)) || new Set(input.themes).size !== input.themes.length || !Array.isArray(input.parameters) || !Array.isArray(input.constants))
3016
+ fail("Invalid family document.");
3017
+ const family = input;
3018
+ if (bytes(input) > tileflowStyleFamilyLimits.documentBytes) fail("Family document is too large.");
3019
+ for (const values of family.parameters)
3020
+ if (!Array.isArray(values) || values.length !== family.themes.length)
3021
+ fail("Invalid parameter column.");
3022
+ walk(family.template, (value) => {
3023
+ if (isReference(value) && (!Number.isSafeInteger(value.$variant) || value.$variant < 0 || value.$variant >= family.parameters.length))
3024
+ fail("Invalid parameter reference.");
3025
+ if (isConstantReference(value) && (!Number.isSafeInteger(value.$constant) || value.$constant < 0 || value.$constant >= family.constants.length))
3026
+ fail("Invalid constant reference.");
3027
+ });
3028
+ let total = 0;
3029
+ const sizes = family.parameters.map((column) => column.map(bytes));
3030
+ const constants = family.constants.map(bytes);
3031
+ for (let index = 0; index < family.themes.length; index += 1) {
3032
+ const size = expandedBytes(family.template, sizes, constants, index);
3033
+ total += size;
3034
+ if (size > tileflowStyleFamilyLimits.expandedStyleBytes || total > tileflowStyleFamilyLimits.expandedFamilyBytes)
3035
+ fail("Expanded family is too large.");
3036
+ const style = materialize(family.template, family, index);
3037
+ assertJsonBudget(style);
3038
+ assertStyle(style);
3039
+ }
3040
+ if (!same(family.updates, planUpdates(expandTileflowFamilyTemplate(family))))
3041
+ fail("Invalid update plan.");
3042
+ return family;
3043
+ }
3044
+ function materializeTileflowStyleFamily(family, theme) {
3045
+ const index = family.themes.indexOf(theme);
3046
+ if (index < 0) fail(`Unknown family theme: ${theme}.`);
3047
+ return materialize(family.template, family, index);
3048
+ }
3049
+ function expandTileflowStyleFamily(input) {
3050
+ const family = parseTileflowStyleFamily(input);
3051
+ return Object.fromEntries(
3052
+ family.themes.map((name) => [name, materializeTileflowStyleFamily(family, name)])
3053
+ );
3054
+ }
3055
+ function readTileflowFamilyValue(family, path, theme) {
3056
+ const index = family.themes.indexOf(theme);
3057
+ if (index < 0) fail(`Unknown family theme: ${theme}.`);
3058
+ let node = family.template;
3059
+ for (const key of path) {
3060
+ if (isReference(node)) node = family.parameters[node.$variant][index];
3061
+ else if (isConstantReference(node)) node = family.constants[node.$constant];
3062
+ node = node && typeof node === "object" ? node[key] : void 0;
3063
+ }
3064
+ return node === void 0 ? void 0 : materialize(node, family, index);
3065
+ }
3066
+ function materialize(node, family, index) {
3067
+ if (isReference(node)) return structuredClone(family.parameters[node.$variant][index]);
3068
+ if (isConstantReference(node)) return structuredClone(family.constants[node.$constant]);
3069
+ if (Array.isArray(node)) return node.map((value) => materialize(value, family, index));
3070
+ if (record(node))
3071
+ return Object.fromEntries(
3072
+ Object.entries(node).map(([key, value]) => [key, materialize(value, family, index)])
3073
+ );
3074
+ return node;
3075
+ }
3076
+ function planUpdates(template) {
3077
+ let appearanceOnly = true;
3078
+ const paint = /* @__PURE__ */ new Map();
3079
+ const roots = /* @__PURE__ */ new Set();
3080
+ walk(template, (node, path) => {
3081
+ if (!isReference(node)) return;
3082
+ if (path[0] === "name") return;
3083
+ if (path[0] === "metadata") {
3084
+ const key = path[1];
3085
+ if ([
3086
+ "tileflow:colors",
3087
+ "tileflow:family-image-theme",
3088
+ "tileflow:theme",
3089
+ "tileflow:colorScheme",
3090
+ "tileflow:sun",
3091
+ "tileflow:route",
3092
+ "tileflow:sky",
3093
+ "tileflow:sourceRequirements",
3094
+ "tileflow:dataRequirements"
3095
+ ].includes(String(key)))
3096
+ return;
3097
+ if (key === "tileflow:buildings" && ["color", "modelColors", "modelLighting", "floodLight"].includes(String(path[2])))
3098
+ return;
3099
+ if (key === "tileflow:vegetation" && path[2] === "colors") return;
3100
+ appearanceOnly = false;
3101
+ return;
3102
+ }
3103
+ if (path[0] === "layers" && path[2] === "layout" && path[3] === "icon-image") return;
3104
+ if (path[0] === "sky" || path[0] === "light") {
3105
+ roots.add(path[0]);
3106
+ return;
3107
+ }
3108
+ if (path[0] === "layers" && typeof path[1] === "number" && path[2] === "paint" && typeof path[3] === "string") {
3109
+ paint.set(`${path[1]}/${path[3]}`, [path[1], path[3]]);
3110
+ } else appearanceOnly = false;
3111
+ });
3112
+ if (record(template) && record(template.metadata) && template.metadata["tileflow:colors"] && Array.isArray(template.layers))
3113
+ template.layers.forEach((layer, index) => {
3114
+ if (record(layer) && record(layer.paint)) {
3115
+ for (const property of Object.keys(layer.paint))
3116
+ if (property.endsWith("color")) paint.set(`${index}/${property}`, [index, property]);
3117
+ }
3118
+ });
3119
+ return { appearanceOnly, paint: [...paint.values()], roots: [...roots].sort() };
3120
+ }
3121
+ function expandedBytes(node, sizes, constants, index) {
3122
+ if (isReference(node)) return sizes[node.$variant][index];
3123
+ if (isConstantReference(node)) return constants[node.$constant];
3124
+ if (Array.isArray(node))
3125
+ return 2 + Math.max(0, node.length - 1) + node.reduce((n, v) => n + expandedBytes(v, sizes, constants, index), 0);
3126
+ if (record(node))
3127
+ return 2 + Math.max(0, Object.keys(node).length - 1) + Object.entries(node).reduce(
3128
+ (n, [key, v]) => n + bytes(key) + 1 + expandedBytes(v, sizes, constants, index),
3129
+ 0
3130
+ );
3131
+ return bytes(node);
3132
+ }
3133
+ function assertStyle(style) {
3134
+ if (!record(style) || style.version !== 8 || !record(style.sources) || !Array.isArray(style.layers) || style.layers.length > tileflowStyleFamilyLimits.layers)
3135
+ fail("Invalid expanded MapLibre style.");
3136
+ const ids = /* @__PURE__ */ new Set();
3137
+ for (const layer of style.layers) {
3138
+ if (!record(layer) || typeof layer.id !== "string" || ids.has(layer.id) || typeof layer.type !== "string")
3139
+ fail("Invalid or duplicate layer.");
3140
+ ids.add(layer.id);
3141
+ }
3142
+ }
3143
+ function assertJsonBudget(input) {
3144
+ let nodes = 0;
3145
+ const stack = /* @__PURE__ */ new Set();
3146
+ const visit = (value, depth) => {
3147
+ if (++nodes > tileflowStyleFamilyLimits.nodes || depth > tileflowStyleFamilyLimits.depth)
3148
+ fail("Family complexity limit exceeded.");
3149
+ if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value))
3150
+ return;
3151
+ if (typeof value !== "object" || !value || stack.has(value)) fail("Expected acyclic JSON.");
3152
+ stack.add(value);
3153
+ if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
3154
+ fail("Expected JSON objects.");
3155
+ for (const [key, item] of Object.entries(value)) {
3156
+ if (["__proto__", "constructor", "prototype"].includes(key)) fail("Unsafe object key.");
3157
+ visit(item, depth + 1);
3158
+ }
3159
+ stack.delete(value);
3160
+ };
3161
+ visit(input, 0);
3162
+ }
3163
+ function walk(node, visit, path = []) {
3164
+ visit(node, path);
3165
+ if (isReference(node) || isConstantReference(node)) return;
3166
+ if (Array.isArray(node)) node.forEach((value, i) => walk(value, visit, [...path, i]));
3167
+ else if (record(node))
3168
+ Object.entries(node).forEach(([key, value]) => walk(value, visit, [...path, key]));
3169
+ }
3170
+ function isReference(node) {
3171
+ return record(node) && Object.keys(node).length === 1 && Object.hasOwn(node, "$variant");
3172
+ }
3173
+ function isConstantReference(node) {
3174
+ return record(node) && Object.keys(node).length === 1 && Object.hasOwn(node, "$constant");
3175
+ }
3176
+ function expandTileflowFamilyTemplate(family) {
3177
+ const visit = (node) => {
3178
+ if (isConstantReference(node)) return structuredClone(family.constants[node.$constant]);
3179
+ if (isReference(node)) return node;
3180
+ if (Array.isArray(node)) return node.map(visit);
3181
+ if (record(node))
3182
+ return Object.fromEntries(
3183
+ Object.entries(node).map(([key, value]) => [key, visit(value)])
3184
+ );
3185
+ return node;
3186
+ };
3187
+ return visit(family.template);
3188
+ }
3189
+ function withTileflowFamilyTemplate(family, template) {
3190
+ assertJsonBudget(template);
3191
+ const packed = shareConstants(template);
3192
+ return parseTileflowStyleFamily({
3193
+ ...family,
3194
+ template: packed.template,
3195
+ constants: packed.constants
3196
+ });
3197
+ }
3198
+ function shareConstants(template) {
3199
+ const counts = /* @__PURE__ */ new Map();
3200
+ const keys = /* @__PURE__ */ new WeakMap();
3201
+ const inspect = (node) => {
3202
+ if (isReference(node) || isConstantReference(node)) return false;
3203
+ if (!node || typeof node !== "object") return true;
3204
+ const children = Array.isArray(node) ? node : Object.values(node);
3205
+ const constant = children.map(inspect).every(Boolean);
3206
+ if (constant) {
3207
+ const key = JSON.stringify(node);
3208
+ if (key.length >= 96) {
3209
+ keys.set(node, key);
3210
+ counts.set(key, (counts.get(key) ?? 0) + 1);
3211
+ }
3212
+ }
3213
+ return constant;
3214
+ };
3215
+ inspect(template);
3216
+ const constants = [];
3217
+ const ids = /* @__PURE__ */ new Map();
3218
+ const pack = (node) => {
3219
+ if (!node || typeof node !== "object" || isReference(node)) return node;
3220
+ const key = keys.get(node);
3221
+ if (key && counts.get(key) > 1) {
3222
+ let index = ids.get(key);
3223
+ if (index === void 0) {
3224
+ index = constants.length;
3225
+ ids.set(key, index);
3226
+ constants.push(node);
3227
+ }
3228
+ return { $constant: index };
3229
+ }
3230
+ if (Array.isArray(node)) return node.map(pack);
3231
+ return Object.fromEntries(
3232
+ Object.entries(node).map(([key2, value]) => [key2, pack(value)])
3233
+ );
3234
+ };
3235
+ return { template: pack(template), constants };
3236
+ }
3237
+ function record(node) {
3238
+ return !!node && typeof node === "object" && !Array.isArray(node);
3239
+ }
3240
+ function same(a, b) {
3241
+ return serializeCanonicalJson(a) === serializeCanonicalJson(b);
3242
+ }
3243
+ function serializeCanonicalJson(value) {
3244
+ const sort = (node) => Array.isArray(node) ? node.map(sort) : record(node) ? Object.fromEntries(
3245
+ Object.keys(node).sort().map((key) => [key, sort(node[key])])
3246
+ ) : node;
3247
+ return JSON.stringify(sort(value));
3248
+ }
3249
+ function bytes(value) {
3250
+ return new TextEncoder().encode(JSON.stringify(value)).length;
3251
+ }
3252
+ function fail(message) {
3253
+ throw new TypeError(`Tileflow style family: ${message}`);
3254
+ }
3255
+
3256
+ // src/style-family-loader.ts
3257
+ async function loadTileflowStyleFamily(url, options = {}) {
3258
+ const request = options.fetch ?? globalThis.fetch.bind(globalThis);
3259
+ const response = await request(url, { credentials: "same-origin", signal: options.signal });
3260
+ if (!response.ok) throw new Error(`Tileflow style family request failed: ${response.status}`);
3261
+ const maximum = tileflowStyleFamilyLimits.documentBytes;
3262
+ const length = response.headers.get("content-length");
3263
+ if (length && Number(length) > maximum) {
3264
+ await response.body?.cancel();
3265
+ throw new TypeError("Tileflow style family is too large.");
3266
+ }
3267
+ const reader = response.body?.getReader();
3268
+ if (!reader) throw new TypeError("Tileflow style family response has no body.");
3269
+ const chunks = [];
3270
+ let total = 0;
3271
+ try {
3272
+ for (; ; ) {
3273
+ const { done, value } = await reader.read();
3274
+ if (done) break;
3275
+ total += value.byteLength;
3276
+ if (total > maximum) {
3277
+ await reader.cancel();
3278
+ throw new TypeError("Tileflow style family is too large.");
3279
+ }
3280
+ chunks.push(value);
3281
+ }
3282
+ } finally {
3283
+ reader.releaseLock();
3284
+ }
3285
+ const bytes2 = new Uint8Array(total);
3286
+ let offset = 0;
3287
+ for (const chunk of chunks) {
3288
+ bytes2.set(chunk, offset);
3289
+ offset += chunk.length;
3290
+ }
3291
+ return parseTileflowStyleFamily(
3292
+ JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes2))
3293
+ );
3294
+ }
3295
+
3296
+ // src/color-program-runtime.ts
3297
+ init_color_program();
3298
+
3299
+ // src/themes/index.ts
3300
+ init_color_program();
3301
+ init_model();
3302
+ init_visual_semantics();
3303
+ var baseThemeColorRoles = {
3304
+ background: "surface.background",
3305
+ boundary: "boundaries.default",
3306
+ building: "surface.building",
3307
+ land: "surface.land",
3308
+ park: "surface.park",
3309
+ road: "roads.default",
3310
+ roadCasing: "roads.casing",
3311
+ roadMajor: "roads.major",
3312
+ text: "labels.primary",
3313
+ textHalo: "labels.halo",
3314
+ textMuted: "labels.muted",
3315
+ water: "surface.water"
3316
+ };
3317
+ var tileflowRequiredThemeColorRoles = Object.freeze(Object.values(baseThemeColorRoles));
3318
+ var baseThemeColorRoleSet = new Set(tileflowRequiredThemeColorRoles);
3319
+ function mix(hexA, hexB, amount) {
3320
+ if (isColorProgramMarker(hexA) || isColorProgramMarker(hexB))
3321
+ return colorProgramMarker(
3322
+ mix(colorProgramValue(hexA), colorProgramValue(hexB), amount),
3323
+ ["srgb", colorProgramOf(hexA), colorProgramOf(hexB), amount]
3324
+ );
3325
+ const a = hexToRgb(hexA);
3326
+ const b = hexToRgb(hexB);
3327
+ const r = Math.round(a.r + (b.r - a.r) * amount);
3328
+ const g = Math.round(a.g + (b.g - a.g) * amount);
3329
+ const bValue = Math.round(a.b + (b.b - a.b) * amount);
3330
+ return rgbToHex(r, g, bValue);
3331
+ }
3332
+ function hexToRgb(hex) {
3333
+ const { b, g, r } = hexToRgba(hex);
3334
+ return { b, g, r };
3335
+ }
3336
+ function hexToRgba(hex) {
3337
+ hex = colorProgramValue(hex);
3338
+ const rgbMatch = hex.match(
3339
+ /^rgba?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)(?:\s*,\s*(\d+(?:\.\d+)?))?\s*\)$/
3340
+ );
3341
+ if (rgbMatch) {
3342
+ return {
3343
+ a: roundAlpha(Number(rgbMatch[4] ?? 1)),
3344
+ r: Math.round(Number(rgbMatch[1])),
3345
+ g: Math.round(Number(rgbMatch[2])),
3346
+ b: Math.round(Number(rgbMatch[3]))
3347
+ };
3348
+ }
3349
+ const value = hex.replace("#", "");
3350
+ const normalized = value.length === 3 || value.length === 4 ? value.split("").map((character) => character + character).join("") : value;
3351
+ return {
3352
+ a: normalized.length === 8 ? roundAlpha(Number.parseInt(normalized.slice(6, 8), 16) / 255) : 1,
3353
+ r: Number.parseInt(normalized.slice(0, 2), 16),
3354
+ g: Number.parseInt(normalized.slice(2, 4), 16),
3355
+ b: Number.parseInt(normalized.slice(4, 6), 16)
3356
+ };
3357
+ }
3358
+ function roundAlpha(value) {
3359
+ return Math.round(value * 1e3) / 1e3;
3360
+ }
3361
+ function rgbToHex(r, g, b) {
3362
+ return `#${[r, g, b].map((value) => value.toString(16).padStart(2, "0")).join("")}`;
3363
+ }
3364
+
3365
+ // src/color-program-runtime.ts
3366
+ init_model();
3367
+ function applyTileflowColorOverrides(style, overrides) {
3368
+ const keys = Object.keys(overrides);
3369
+ if (!keys.length) return style;
3370
+ const catalog = style.metadata?.[tileflowColorMetadataKey];
3371
+ if (!catalog)
3372
+ throw new TypeError(
3373
+ "This style has no runtime colour bindings. Rebuild with runtimeColors enabled."
3374
+ );
3375
+ validateBindings(catalog);
3376
+ if (keys.length > 1024 || keys.some(
3377
+ (key) => !Object.hasOwn(catalog.tokens.color, key) || typeof overrides[key] !== "string" || overrides[key].length > 128
3378
+ ))
3379
+ throw new TypeError("Unknown colour token or invalid override.");
3380
+ const theme = {
3381
+ id: "runtime",
3382
+ version: 1,
3383
+ colorScheme: "light",
3384
+ lighting: {},
3385
+ typography: { font: "Noto Sans Regular" },
3386
+ tokens: {
3387
+ ...catalog.tokens,
3388
+ color: { ...catalog.tokens.color, ...overrides },
3389
+ font: {},
3390
+ image: {}
3391
+ }
3392
+ };
3393
+ const resolved = resolveTileflowTheme(theme);
3394
+ const concrete = {
3395
+ ...theme,
3396
+ tokens: { ...theme.tokens, color: resolved.tokens.color, number: resolved.tokens.number }
3397
+ };
3398
+ const evaluate = (program) => {
3399
+ if (typeof program === "string") return program;
3400
+ if (Array.isArray(program) && program[0] === "srgb" && program.length === 4 && typeof program[3] === "number" && program[3] >= 0 && program[3] <= 1)
3401
+ return mix(evaluate(program[1]), evaluate(program[2]), program[3]);
3402
+ if (program && typeof program === "object" && !Array.isArray(program)) {
3403
+ const node = program;
3404
+ if (node.kind === "theme-color" && node.operation === "alpha")
3405
+ program = { ...node, color: evaluate(node.color) };
3406
+ else if (node.kind === "theme-color" && node.operation === "mix")
3407
+ program = { ...node, from: evaluate(node.from), to: evaluate(node.to) };
3408
+ }
3409
+ return resolveThemeValues({ color: program }, concrete).color;
3410
+ };
3411
+ const affected = (node, seen = /* @__PURE__ */ new Set()) => {
3412
+ if (!node || typeof node !== "object") return false;
3413
+ const ref = node;
3414
+ if (ref.kind === "theme-token" && ref.category === "color" && typeof ref.token === "string") {
3415
+ if (Object.hasOwn(overrides, ref.token)) return true;
3416
+ if (seen.has(ref.token)) return false;
3417
+ return affected(catalog.tokens.color[ref.token], /* @__PURE__ */ new Set([...seen, ref.token]));
3418
+ }
3419
+ return Object.values(node).some((value) => affected(value, seen));
3420
+ };
3421
+ const values = catalog.programs.map(
3422
+ (program) => affected(program) ? evaluate(program) : void 0
3423
+ );
3424
+ const result = structuredClone(style);
3425
+ for (const [path, id] of catalog.bindings)
3426
+ if (values[id] !== void 0) updatePath(result, path, () => colorProgramValue(values[id]));
3427
+ return result;
3428
+ }
3429
+
3430
+ // src/style-family-render.ts
3431
+ init_color_program();
3432
+ init_theme_blend();
3433
+ var preparedFamilies = /* @__PURE__ */ new WeakMap();
3434
+ function prepareTileflowStyleFamilyForRendering(family) {
3435
+ const cached = preparedFamilies.get(family);
3436
+ if (cached) return cached;
3437
+ if (readTileflowFamilyValue(family, ["metadata", "tileflow:family-prepared"], family.themes[0]) === 1)
3438
+ return family;
3439
+ if (!family.updates.appearanceOnly || family.themes.length < 2) return family;
3440
+ const styles = expandTileflowStyleFamily(family);
3441
+ const plan = planTileflowThemeBlend(
3442
+ family.themes.map((name) => restoreTileflowColorPrograms(styles[name])),
3443
+ { iconImages: "mix" }
3444
+ );
3445
+ const first = plan.styleAt(0);
3446
+ const descendants = (id) => first.layers.map((layer) => String(layer.id)).filter(
3447
+ (name) => name === id || name.startsWith(`${id}::branch-`) || name.startsWith(`${id}::theme-`)
3448
+ );
3449
+ const prepared = createTileflowStyleFamily(
3450
+ Object.fromEntries(
3451
+ family.themes.map((name, index) => {
3452
+ const style = plan.styleAt(index);
3453
+ const metadata = {
3454
+ ...styles[name].metadata,
3455
+ "tileflow:family-prepared": 1,
3456
+ ...plan.images.length ? { "tileflow:family-images": plan.images, "tileflow:family-image-theme": name } : {},
3457
+ // A split physical layer must retain the planner's interaction mapping.
3458
+ ...style.metadata?.["tileflow:interaction-manifest"] ? { "tileflow:interaction-manifest": style.metadata["tileflow:interaction-manifest"] } : {},
3459
+ ...style.metadata?.["tileflow:overlay-placement-manifest"] ? {
3460
+ "tileflow:overlay-placement-manifest": style.metadata["tileflow:overlay-placement-manifest"]
3461
+ } : {}
3462
+ };
3463
+ const vegetation = metadata["tileflow:vegetation"];
3464
+ if (Array.isArray(vegetation?.flatLayers))
3465
+ metadata["tileflow:vegetation"] = {
3466
+ ...vegetation,
3467
+ flatLayers: vegetation.flatLayers.flatMap(descendants)
3468
+ };
3469
+ const display = metadata["tileflow:vegetation-display"];
3470
+ if (Array.isArray(display?.flatLayers))
3471
+ metadata["tileflow:vegetation-display"] = {
3472
+ ...display,
3473
+ flatLayers: display.flatLayers.flatMap(
3474
+ (layer) => descendants(layer.id).map((id) => ({ ...layer, id }))
3475
+ )
3476
+ };
3477
+ const colors = styles[name].metadata?.[tileflowColorMetadataKey];
3478
+ const preparedStyle = { ...style, transition: { duration: 0, delay: 0 }, metadata };
3479
+ if (colors) {
3480
+ const restoredMetadata = restoreTileflowColorPrograms(styles[name]).metadata;
3481
+ for (const key of Object.keys(restoredMetadata))
3482
+ if (key !== "tileflow:interaction-manifest" && key !== "tileflow:overlay-placement-manifest" && key !== "tileflow:vegetation-display")
3483
+ preparedStyle.metadata[key] = key === "tileflow:vegetation" ? {
3484
+ ...restoredMetadata[key],
3485
+ flatLayers: metadata[key].flatLayers
3486
+ } : restoredMetadata[key];
3487
+ }
3488
+ return [
3489
+ name,
3490
+ colors ? extractTileflowColorPrograms(preparedStyle, colors.tokens) : preparedStyle
3491
+ ];
3492
+ })
3493
+ )
3494
+ );
3495
+ if (!prepared.updates.appearanceOnly)
3496
+ throw new TypeError("Family preparation changed its structural contract.");
3497
+ preparedFamilies.set(family, prepared);
3498
+ preparedFamilies.set(prepared, prepared);
3499
+ return prepared;
3500
+ }
3501
+
3502
+ // src/style-family-runtime.ts
3503
+ function resolveTileflowStyleResourceUrls(style, documentUrl) {
3504
+ const result = structuredClone(style);
3505
+ const absolute = (value) => (
3506
+ // URL encodes path placeholders. Query values may contain a nested URL whose
3507
+ // escaped placeholders must remain opaque to the outer tile substitution.
3508
+ new URL(value, documentUrl).href.replace(
3509
+ /^[^?#]+/u,
3510
+ (path) => path.replaceAll(/%7B([^%]+)%7D/giu, "{$1}")
3511
+ )
3512
+ );
3513
+ const property = (record3, key) => {
3514
+ if (typeof record3[key] === "string") record3[key] = absolute(record3[key]);
3515
+ };
3516
+ property(result, "glyphs");
3517
+ if (typeof result.sprite === "string") result.sprite = absolute(result.sprite);
3518
+ else if (Array.isArray(result.sprite))
3519
+ for (const sprite of result.sprite) property(sprite, "url");
3520
+ for (const source of Object.values(result.sources)) {
3521
+ property(source, "url");
3522
+ if (source.type === "geojson") property(source, "data");
3523
+ if (Array.isArray(source.tiles))
3524
+ source.tiles = source.tiles.map(
3525
+ (value) => typeof value === "string" ? absolute(value) : value
3526
+ );
3527
+ if (Array.isArray(source.urls))
3528
+ source.urls = source.urls.map(
3529
+ (value) => typeof value === "string" ? absolute(value) : value
3530
+ );
3531
+ }
3532
+ const metadata = result.metadata;
3533
+ const fonts = metadata?.["tileflow:fontFaces"];
3534
+ if (Array.isArray(fonts)) {
3535
+ for (const face of fonts) if (face && typeof face === "object") property(face, "source");
3536
+ }
3537
+ for (const key of ["tileflow:buildings", "tileflow:vegetation"]) {
3538
+ const descriptor = metadata?.[key];
3539
+ if (Array.isArray(descriptor?.models))
3540
+ for (const model of descriptor.models) property(model, "url");
3541
+ }
3542
+ return result;
3543
+ }
3544
+ function resolveTileflowFamilyResourceUrls(family, documentUrl) {
3545
+ if (family.updates.appearanceOnly)
3546
+ return withTileflowFamilyTemplate(
3547
+ family,
3548
+ resolveTileflowStyleResourceUrls(
3549
+ expandTileflowFamilyTemplate(family),
3550
+ documentUrl
3551
+ )
3552
+ );
3553
+ return createTileflowStyleFamily(
3554
+ Object.fromEntries(
3555
+ Object.entries(expandTileflowStyleFamily(family)).map(([name, style]) => [
3556
+ name,
3557
+ resolveTileflowStyleResourceUrls(style, documentUrl)
3558
+ ])
3559
+ )
3560
+ );
3561
+ }
3562
+ async function loadTileflowRuntimeAppearance(style, options = {}) {
3563
+ if (!style.familyUrl || !style.theme || style.family) return style;
3564
+ const family = prepareTileflowStyleFamilyForRendering(
3565
+ resolveTileflowFamilyResourceUrls(
3566
+ await loadTileflowStyleFamily(style.familyUrl, options),
3567
+ style.familyUrl
3568
+ )
3569
+ );
3570
+ return { ...style, family, style: materializeTileflowStyleFamily(family, style.theme) };
3571
+ }
3572
+
3573
+ // src/appearance.ts
3574
+ init_time_of_day();
3575
+ function resolveTileflowAppearance(map, selection, context) {
3576
+ const theme = (moment) => {
3577
+ if (moment !== void 0 && !isTileflowTimeOfDay(moment))
3578
+ throw new TypeError("Invalid appearance moment.");
3579
+ return resolveTileflowRuntimeTheme(map, selection.palette, context.colorScheme, moment).name;
3580
+ };
3581
+ if (selection.mode === "fixed") return { themes: [theme(selection.moment)], position: 0 };
3582
+ if (selection.mode === "system")
3583
+ return { themes: [theme(selection[context.colorScheme])], position: 0 };
3584
+ if (selection.mode !== "clock")
3585
+ throw new TypeError("Expected fixed, system, or clock appearance.");
3586
+ const schedule = [...selection.schedule].sort((a, b) => a.hour - b.hour);
3587
+ if (schedule.length < 2 || schedule.length > 24 || schedule.some(
3588
+ (stop, index) => !Number.isFinite(stop.hour) || stop.hour < 0 || stop.hour >= 24 || !isTileflowTimeOfDay(stop.moment) || index > 0 && stop.hour === schedule[index - 1].hour
3589
+ ))
3590
+ throw new TypeError("A clock schedule needs 2\u201324 distinct hours from 0 to less than 24.");
3591
+ if (!Number.isFinite(context.date.getTime())) throw new TypeError("Invalid appearance date.");
3592
+ const hour = context.date.getHours() + context.date.getMinutes() / 60 + context.date.getSeconds() / 3600;
3593
+ let lower = -1;
3594
+ schedule.forEach((stop, index) => {
3595
+ if (stop.hour <= hour) lower = index;
3596
+ });
3597
+ if (lower < 0) lower = schedule.length - 1;
3598
+ const from = schedule[lower];
3599
+ const to = schedule[(lower + 1) % schedule.length];
3600
+ if (!selection.blend) return { themes: [theme(from.moment)], position: 0 };
3601
+ const span = (to.hour - from.hour + 24) % 24;
3602
+ const themes = [theme(from.moment), theme(to.moment)];
3603
+ return themes[0] === themes[1] ? { themes: [themes[0]], position: 0 } : { themes, position: (hour - from.hour + 24) % 24 / span };
3604
+ }
3605
+
3606
+ // src/runtime.ts
570
3607
  init_time_of_day();
571
3608
  var defaultTileflowManifestUrl = "/tileflow/manifest.json";
572
3609
  var defaultTileflowRuntimeView = Object.freeze({
@@ -659,6 +3696,7 @@ function resolveTileflowRuntimeStyle(options) {
659
3696
  styleId: theme.styleId
660
3697
  },
661
3698
  colorScheme: theme.colorScheme,
3699
+ ...options.manifestMap.familyUrl ? { familyUrl: options.manifestMap.familyUrl } : {},
662
3700
  ...theme.fontFaces === void 0 ? {} : { fontFaces: [...theme.fontFaces] },
663
3701
  revision: theme.revision,
664
3702
  style: theme.styleUrl,
@@ -849,7 +3887,8 @@ function createTileflowSessionController(input) {
849
3887
  async function requestSessionGrant(input) {
850
3888
  return runWithinSessionGrantTimeout(input.timeoutMs, async (signal) => {
851
3889
  const apiUrl = getTileflowAnalyticsApiUrl(input.analytics);
852
- const response = await input.fetchGrant(`${apiUrl}/v1/sessions/start`, {
3890
+ const fetchGrant = input.fetchGrant;
3891
+ const response = await fetchGrant(`${apiUrl}/v1/sessions/start`, {
853
3892
  body: JSON.stringify({
854
3893
  mapId: input.analytics.mapId,
855
3894
  sdkVersion: input.analytics.sdkVersion,
@@ -876,7 +3915,7 @@ async function requestSessionGrant(input) {
876
3915
  else throw error;
877
3916
  }
878
3917
  throwIfSessionGrantAborted(signal);
879
- if (response.status === 409 && isRecord(body) && body.code === "COMMERCIAL_SESSION_RESTART_REQUIRED" && body.ok === false && body.retryWithNewSession === true && body.sessionId === input.sessionId) {
3918
+ if (response.status === 409 && isRecord4(body) && body.code === "COMMERCIAL_SESSION_RESTART_REQUIRED" && body.ok === false && body.retryWithNewSession === true && body.sessionId === input.sessionId) {
880
3919
  throw new TileflowSessionRestartError();
881
3920
  }
882
3921
  if (!response.ok) {
@@ -949,7 +3988,7 @@ function sessionGrantAbortReason(signal) {
949
3988
  return error;
950
3989
  }
951
3990
  function parseSessionGrantResponse(value, expectedSessionId) {
952
- if (!isRecord(value) || value.ok !== true || value.sessionId !== expectedSessionId) return null;
3991
+ if (!isRecord4(value) || value.ok !== true || value.sessionId !== expectedSessionId) return null;
953
3992
  if (value.usageMode !== void 0 && value.usageMode !== "session") return null;
954
3993
  if (value.billingEnabled !== void 0 && value.billingEnabled !== false) return null;
955
3994
  if (value.disposition !== void 0 && value.disposition !== "ordinary" && value.disposition !== "unbilled_fail_open") {
@@ -968,11 +4007,11 @@ function parseSessionGrantResponse(value, expectedSessionId) {
968
4007
  }
969
4008
  return value;
970
4009
  }
971
- function isRecord(value) {
4010
+ function isRecord4(value) {
972
4011
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
973
4012
  }
974
4013
  function isPlainRuntimeRecord(value) {
975
- if (!isRecord(value)) return false;
4014
+ if (!isRecord4(value)) return false;
976
4015
  const prototype = Object.getPrototypeOf(value);
977
4016
  return prototype === Object.prototype || prototype === null;
978
4017
  }
@@ -1175,6 +4214,7 @@ function resolveLoadedTileflowManifestUrls(manifest, manifestUrl) {
1175
4214
  mapName,
1176
4215
  {
1177
4216
  ...entry,
4217
+ ...entry.familyUrl ? { familyUrl: resolveTileflowLoadedResourceUrl(entry.familyUrl, manifestUrl) } : {},
1178
4218
  themes: Object.fromEntries(
1179
4219
  Object.entries(entry.themes).map(([themeName, theme]) => {
1180
4220
  const styleUrl = resolveTileflowLoadedResourceUrl(theme.styleUrl, manifestUrl);
@@ -1224,6 +4264,7 @@ function resolveTileflowManifestMap(manifest, mapName) {
1224
4264
  mapId: entry.mapId ?? analytics?.mapId,
1225
4265
  name: mapName,
1226
4266
  defaultTheme: entry.defaultTheme,
4267
+ ...entry.familyUrl ? { familyUrl: entry.familyUrl } : {},
1227
4268
  ...entry.environment ? { environment: entry.environment } : {},
1228
4269
  ...entry.systemThemes ? { systemThemes: { ...entry.systemThemes } } : {},
1229
4270
  ...entry.timeOfDay ? { timeOfDay: entry.timeOfDay } : {},
@@ -1306,14 +4347,14 @@ async function readBoundedUtf8Response(response, maximumBytes, errors) {
1306
4347
  }
1307
4348
  chunks.push(next.value);
1308
4349
  }
1309
- const bytes = new Uint8Array(byteLength);
4350
+ const bytes2 = new Uint8Array(byteLength);
1310
4351
  let offset = 0;
1311
4352
  for (const chunk of chunks) {
1312
- bytes.set(chunk, offset);
4353
+ bytes2.set(chunk, offset);
1313
4354
  offset += chunk.byteLength;
1314
4355
  }
1315
4356
  try {
1316
- return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
4357
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes2);
1317
4358
  } catch {
1318
4359
  throw new Error(errors.invalidUtf8);
1319
4360
  }
@@ -1408,24 +4449,33 @@ function getTileflowLocationBaseUrl() {
1408
4449
  return `${getTileflowLocationOrigin().replace(/\/+$/u, "")}/`;
1409
4450
  }
1410
4451
  export {
4452
+ applyTileflowColorOverrides,
1411
4453
  assertValidTileflowRuntimeSource,
1412
4454
  clearTileflowManifestCache,
1413
4455
  createTileflowSessionController,
1414
4456
  createTileflowSessionId,
1415
4457
  defaultTileflowManifestUrl,
1416
4458
  defaultTileflowRuntimeView,
4459
+ expandTileflowFamilyTemplate,
1417
4460
  getTileflowAnalyticsApiUrl,
1418
4461
  getTileflowStyleFontFaces,
1419
4462
  inferTileflowAnalyticsFromStyleUrl,
1420
4463
  isTileflowTimeOfDay,
1421
4464
  loadTileflowManifest,
4465
+ loadTileflowRuntimeAppearance,
4466
+ loadTileflowStyleFamily,
4467
+ materializeTileflowStyleFamily,
1422
4468
  mergeTileflowAnalytics,
1423
4469
  nearestTileflowBlendTheme,
1424
4470
  normalizeTileflowRuntimeCenter,
1425
4471
  normalizeTileflowStaticImageSize,
1426
4472
  normalizeTileflowSurfaceId,
1427
4473
  normalizeTileflowUrl,
4474
+ parseTileflowStyleFamily,
4475
+ prepareTileflowStyleFamilyForRendering,
1428
4476
  resolveTileflowAnalyticsRequestUrl,
4477
+ resolveTileflowAppearance,
4478
+ resolveTileflowFamilyResourceUrls,
1429
4479
  resolveTileflowManifestMap,
1430
4480
  resolveTileflowMapMode,
1431
4481
  resolveTileflowRuntimeStyle,
@@ -1433,13 +4483,16 @@ export {
1433
4483
  resolveTileflowRuntimeThemeBlend,
1434
4484
  resolveTileflowRuntimeView,
1435
4485
  resolveTileflowStaticImageUrl,
4486
+ resolveTileflowStyleResourceUrls,
1436
4487
  shouldLoadTileflowManifest,
1437
4488
  startTileflowSession,
4489
+ tileflowStyleFamilyLimits,
1438
4490
  tileflowStyleFontFaceLimits,
1439
4491
  tileflowStyleFontFacesMetadataKey,
1440
4492
  tileflowThemeBlendLimits,
1441
4493
  tileflowTimesOfDay,
1442
4494
  validateTileflowRuntimeSource,
1443
4495
  validateTileflowThemeBlendSelection,
1444
- validateTileflowThemeSelection
4496
+ validateTileflowThemeSelection,
4497
+ withTileflowFamilyTemplate
1445
4498
  };