@bitmagic/cli 0.1.83-dev.2 → 0.1.83-dev.3
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/assets/vxl-summary.js +1 -1
- package/dist/assets/vxl-summary.js.map +1 -1
- package/dist/commands/steam-upload.js +6 -4
- package/dist/commands/steam-upload.js.map +1 -1
- package/dist/commands/steam.js +4 -2
- package/dist/commands/steam.js.map +1 -1
- package/dist/forge/browser-host.js +1 -1
- package/dist/forge/browser-host.js.map +1 -1
- package/dist/project/published-three.js +4 -6
- package/dist/project/published-three.js.map +1 -1
- package/dist/scaffold/project-files.js +2 -2
- package/dist/scaffold/project-files.js.map +1 -1
- package/dist/steam/art-generate.js +2 -1
- package/dist/steam/art-generate.js.map +1 -1
- package/dist/steam/art-render.js.map +1 -1
- package/dist/steam/art.js +1 -1
- package/dist/steam/art.js.map +1 -1
- package/dist/steam/check.js.map +1 -1
- package/dist/steam/electron-cache.d.ts +1 -1
- package/dist/steam/electron-cache.js +2 -2
- package/dist/steam/electron-cache.js.map +1 -1
- package/dist/steam/fuses.js +1 -1
- package/dist/steam/fuses.js.map +1 -1
- package/dist/steam/media.js +6 -2
- package/dist/steam/media.js.map +1 -1
- package/dist/steam/package-windows.d.ts +1 -2
- package/dist/steam/package-windows.js +1 -2
- package/dist/steam/package-windows.js.map +1 -1
- package/dist/steam/paths.d.ts +0 -2
- package/dist/steam/paths.js +0 -1
- package/dist/steam/paths.js.map +1 -1
- package/dist/steam/portal.js +1 -1
- package/dist/steam/portal.js.map +1 -1
- package/dist/steam/raster.d.ts +0 -1
- package/dist/steam/raster.js +1 -1
- package/dist/steam/raster.js.map +1 -1
- package/dist/steam/store-copy.d.ts +0 -1
- package/dist/steam/store-copy.js +3 -10
- package/dist/steam/store-copy.js.map +1 -1
- package/dist/steam/tags.js +6 -3
- package/dist/steam/tags.js.map +1 -1
- package/dist/theme/kernel.d.ts +14 -0
- package/dist/theme/kernel.js +15 -0
- package/dist/theme/kernel.js.map +1 -0
- package/dist/theme/theme-io.js +11 -16
- package/dist/theme/theme-io.js.map +1 -1
- package/dist/vendor/hud-theme/catalog.js +71 -0
- package/dist/vendor/hud-theme/engine/hud/ThemeTokens.js +115 -0
- package/dist/{theme/theme-color-math.js → vendor/hud-theme/engine/hud/colorMath.js} +13 -14
- package/dist/vendor/hud-theme/engine/hud/decorations.js +179 -0
- package/dist/vendor/hud-theme/engine/hud/fonts.js +147 -0
- package/dist/vendor/hud-theme/engine/hud/presets.js +80 -0
- package/dist/vendor/hud-theme/engine/hud/resolveWorldTheme.js +66 -0
- package/dist/vendor/hud-theme/engine/hud/themes/bitmagic.js +29 -0
- package/dist/vendor/hud-theme/engine/hud/themes/nexus.js +69 -0
- package/dist/vendor/hud-theme/engine/hud/themes/racing.js +57 -0
- package/dist/vendor/hud-theme/engine/hud/themes/rift-raider.js +63 -0
- package/dist/vendor/hud-theme/engine/hud/themes/simulator.js +55 -0
- package/dist/vendor/hud-theme/engine/hud/themes/village-keep.js +108 -0
- package/dist/vendor/hud-theme/engine/hud/validateTheme.js +535 -0
- package/dist/vendor/hud-theme/index.js +26 -0
- package/dist/{theme/theme-merge.js → vendor/hud-theme/merge.js} +0 -1
- package/dist/{theme/theme-report.js → vendor/hud-theme/report.js} +8 -9
- package/dist/{theme/theme-stored.js → vendor/hud-theme/stored.js} +6 -9
- package/dist/{theme/theme-validate.js → vendor/hud-theme/validate-strict.js} +102 -164
- package/dist/verify/browser.js +27 -3
- package/dist/verify/browser.js.map +1 -1
- package/dist/verify/categories.d.ts +1 -1
- package/dist/verify/categories.js +1 -0
- package/dist/verify/categories.js.map +1 -1
- package/dist/verify/classify.d.ts +3 -0
- package/dist/verify/classify.js +7 -0
- package/dist/verify/classify.js.map +1 -1
- package/dist/verify/hud-theme.d.ts +19 -14
- package/dist/verify/hud-theme.js +23 -17
- package/dist/verify/hud-theme.js.map +1 -1
- package/dist/verify/start.d.ts +3 -2
- package/dist/verify/start.js +1 -1
- package/dist/verify/start.js.map +1 -1
- package/package.json +7 -6
- package/dist/theme/hud-theme-catalog.d.ts +0 -235
- package/dist/theme/hud-theme-catalog.js +0 -389
- package/dist/theme/hud-theme-catalog.js.map +0 -1
- package/dist/theme/theme-color-math.d.ts +0 -21
- package/dist/theme/theme-color-math.js.map +0 -1
- package/dist/theme/theme-merge.d.ts +0 -36
- package/dist/theme/theme-merge.js.map +0 -1
- package/dist/theme/theme-report.d.ts +0 -28
- package/dist/theme/theme-report.js.map +0 -1
- package/dist/theme/theme-stored.d.ts +0 -21
- package/dist/theme/theme-stored.js.map +0 -1
- package/dist/theme/theme-validate.d.ts +0 -41
- package/dist/theme/theme-validate.js.map +0 -1
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import { BACKGROUND_POSITIONS, BACKGROUND_REPEATS, BACKGROUND_SIZE_KEYWORDS, BACKGROUND_TILE_SIZE, BORDER_IMAGE_REPEATS, DECORATION_SLOT_KINDS, DEFAULT_THEME_FONT, DEFAULT_THEME_SHAPE, FONT_BODY_WEIGHTS, FONT_HEADING_WEIGHTS, GLOW_KEYS, HUD_ELEMENT_CLASSES, IMAGE_RENDERINGS, LABEL_CASES, MAX_INLINE_SVG_LENGTH, REQUIRED_THEME_COLOR_KEYS, THEME_COLOR_KEYS, THEME_ICON_KEYS, THEME_NUMBER_RANGES, isHexColor, } from './ThemeTokens.js';
|
|
2
|
+
import { HUD_FONTS } from './fonts.js';
|
|
3
|
+
import { HUD_DECORATIONS } from './decorations.js';
|
|
4
|
+
const FONT_KEYS = Object.keys(HUD_FONTS);
|
|
5
|
+
const DECORATION_KEYS = Object.keys(HUD_DECORATIONS);
|
|
6
|
+
const ELEMENT_CLASSES = HUD_ELEMENT_CLASSES;
|
|
7
|
+
// Not engine/utils/guards.ts' isRecord: the HUD kernel ships to the CLI and the agent
|
|
8
|
+
// through @bitmagic/hud-theme, whose build refuses any engine file outside engine/hud/.
|
|
9
|
+
function isObject(v) {
|
|
10
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
11
|
+
}
|
|
12
|
+
// Levenshtein, not character containment. The old scorer counted how many of the
|
|
13
|
+
// input's characters appear ANYWHERE in a candidate, so long candidates won on
|
|
14
|
+
// volume: "balloo2" suggested "rubik-mono-one"-class noise instead of "baloo-2",
|
|
15
|
+
// and the suggestion an agent is meant to copy was sometimes worse than no
|
|
16
|
+
// suggestion. Edit distance measures what a typo actually is. Only suggest when
|
|
17
|
+
// the distance is small relative to the input — a wild guess should get the
|
|
18
|
+
// full candidate list from the error message, not false confidence in one name.
|
|
19
|
+
function editDistance(a, b) {
|
|
20
|
+
// Single-row DP; inputs are short catalog keys, so O(len a * len b) is fine.
|
|
21
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
22
|
+
for (let i = 1; i <= a.length; i++) {
|
|
23
|
+
const cur = [i];
|
|
24
|
+
for (let j = 1; j <= b.length; j++) {
|
|
25
|
+
const deletion = (prev[j] ?? 0) + 1;
|
|
26
|
+
const insertion = (cur[j - 1] ?? 0) + 1;
|
|
27
|
+
const substitution = (prev[j - 1] ?? 0) + (a[i - 1] === b[j - 1] ? 0 : 1);
|
|
28
|
+
cur[j] = Math.min(deletion, insertion, substitution);
|
|
29
|
+
}
|
|
30
|
+
prev = cur;
|
|
31
|
+
}
|
|
32
|
+
return prev[b.length] ?? 0;
|
|
33
|
+
}
|
|
34
|
+
export function suggestClosest(value, candidates) {
|
|
35
|
+
const v = value.toLowerCase();
|
|
36
|
+
let best;
|
|
37
|
+
let bestScore = Infinity;
|
|
38
|
+
for (const c of candidates) {
|
|
39
|
+
const score = editDistance(v, c.toLowerCase());
|
|
40
|
+
if (score < bestScore) {
|
|
41
|
+
bestScore = score;
|
|
42
|
+
best = c;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const maxDistance = Math.max(2, Math.ceil(v.length / 3));
|
|
46
|
+
return bestScore <= maxDistance ? best : undefined;
|
|
47
|
+
}
|
|
48
|
+
function checkHex(ctx, path, value) {
|
|
49
|
+
if (typeof value !== 'string') {
|
|
50
|
+
ctx.errors.push({ path, message: `expected hex color string, got ${typeof value}` });
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
if (!isHexColor(value)) {
|
|
54
|
+
ctx.errors.push({
|
|
55
|
+
path,
|
|
56
|
+
message: `${value} is not a valid 6-digit hex color`,
|
|
57
|
+
suggestion: 'Use e.g. "#A0DAB9" — six hex digits with a leading #.',
|
|
58
|
+
});
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* `background-size`: one of the three keywords, or an explicit tile size.
|
|
65
|
+
*
|
|
66
|
+
* A repeating decoration NEEDS the explicit form. Every decoration SVG carries
|
|
67
|
+
* only a `viewBox`, which gives it no intrinsic size, so `auto` does not mean
|
|
68
|
+
* "one tile at the viewBox dimensions" — the browser scales a single copy to
|
|
69
|
+
* fill the element, and a pattern meant to tile renders as one enormous motif.
|
|
70
|
+
* With only the three keywords available, a hand-authored tiling texture could
|
|
71
|
+
* not be expressed at all. The tile pattern itself (`BACKGROUND_TILE_SIZE`) is
|
|
72
|
+
* deliberately strict; see its note in ThemeTokens.ts.
|
|
73
|
+
*/
|
|
74
|
+
function checkBackgroundSize(ctx, path, value) {
|
|
75
|
+
if (typeof value === 'string' && BACKGROUND_SIZE_KEYWORDS.includes(value)) {
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
if (typeof value === 'string' && BACKGROUND_TILE_SIZE.test(value))
|
|
79
|
+
return value;
|
|
80
|
+
ctx.errors.push({
|
|
81
|
+
path,
|
|
82
|
+
message: `${JSON.stringify(value)} is not "cover", "contain", "auto", or an explicit tile size`,
|
|
83
|
+
suggestion: 'Use "32px 32px" for a repeating pattern (a viewBox-only SVG has no intrinsic '
|
|
84
|
+
+ 'size, so "auto" stretches one copy over the whole element), or one of the keywords.',
|
|
85
|
+
});
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
function checkEnum(ctx, path, value, allowed) {
|
|
89
|
+
if (typeof value !== 'string' || !allowed.includes(value)) {
|
|
90
|
+
const suggestion = typeof value === 'string' ? suggestClosest(value, allowed) : undefined;
|
|
91
|
+
ctx.errors.push({
|
|
92
|
+
path,
|
|
93
|
+
message: `${JSON.stringify(value)} is not one of: ${allowed.join(', ')}`,
|
|
94
|
+
suggestion: suggestion ? `Closest match: "${suggestion}"` : undefined,
|
|
95
|
+
});
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
function checkNumber(ctx, path, value, token, fallback) {
|
|
101
|
+
const [min, max] = THEME_NUMBER_RANGES[token];
|
|
102
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
103
|
+
ctx.errors.push({ path, message: `expected number in [${min}, ${max}]` });
|
|
104
|
+
return fallback;
|
|
105
|
+
}
|
|
106
|
+
if (value < min || value > max) {
|
|
107
|
+
ctx.errors.push({ path, message: `${value} outside allowed range [${min}, ${max}]` });
|
|
108
|
+
return fallback;
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
function checkFont(ctx, path, raw) {
|
|
113
|
+
if (!isObject(raw)) {
|
|
114
|
+
ctx.errors.push({ path, message: 'missing font object' });
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const key = checkEnum(ctx, `${path}.key`, raw.key, FONT_KEYS);
|
|
118
|
+
if (!key)
|
|
119
|
+
return null;
|
|
120
|
+
const labelCase = raw.case === undefined
|
|
121
|
+
? DEFAULT_THEME_FONT.case
|
|
122
|
+
: checkEnum(ctx, `${path}.case`, raw.case, LABEL_CASES) ?? DEFAULT_THEME_FONT.case;
|
|
123
|
+
const weightBody = raw.weightBody === undefined
|
|
124
|
+
? DEFAULT_THEME_FONT.weightBody
|
|
125
|
+
: (checkEnum(ctx, `${path}.weightBody`, String(raw.weightBody), FONT_BODY_WEIGHTS.map(String))
|
|
126
|
+
? raw.weightBody
|
|
127
|
+
: DEFAULT_THEME_FONT.weightBody);
|
|
128
|
+
const weightHeading = raw.weightHeading === undefined
|
|
129
|
+
? DEFAULT_THEME_FONT.weightHeading
|
|
130
|
+
: (checkEnum(ctx, `${path}.weightHeading`, String(raw.weightHeading), FONT_HEADING_WEIGHTS.map(String))
|
|
131
|
+
? raw.weightHeading
|
|
132
|
+
: DEFAULT_THEME_FONT.weightHeading);
|
|
133
|
+
const trackingLabel = raw.trackingLabel === undefined
|
|
134
|
+
? DEFAULT_THEME_FONT.trackingLabel
|
|
135
|
+
: checkNumber(ctx, `${path}.trackingLabel`, raw.trackingLabel, 'trackingLabel', DEFAULT_THEME_FONT.trackingLabel);
|
|
136
|
+
const outlineWidth = raw.outlineWidth === undefined
|
|
137
|
+
? DEFAULT_THEME_FONT.outlineWidth
|
|
138
|
+
: checkNumber(ctx, `${path}.outlineWidth`, raw.outlineWidth, 'outlineWidth', DEFAULT_THEME_FONT.outlineWidth);
|
|
139
|
+
// Absent stays absent — the resolved field is optional on purpose.
|
|
140
|
+
const sizeScale = raw.sizeScale === undefined
|
|
141
|
+
? undefined
|
|
142
|
+
: checkNumber(ctx, `${path}.sizeScale`, raw.sizeScale, 'sizeScale', 1);
|
|
143
|
+
// Auto-correct case if the font doesn't support the requested case (e.g.
|
|
144
|
+
// Pacifico + 'upper' looks awkward — snap to the font's supported case
|
|
145
|
+
// and emit a warning explaining the swap). The resolved theme stored in
|
|
146
|
+
// the registry uses the corrected value, so the HUD renders cleanly even
|
|
147
|
+
// when the input was wrong.
|
|
148
|
+
const fontEntry = HUD_FONTS[key];
|
|
149
|
+
const effectiveCase = fontEntry.suitsCase.includes(labelCase)
|
|
150
|
+
? labelCase
|
|
151
|
+
: (fontEntry.suitsCase[0] ?? labelCase);
|
|
152
|
+
if (effectiveCase !== labelCase) {
|
|
153
|
+
ctx.warnings.push({
|
|
154
|
+
path: `${path}.case`,
|
|
155
|
+
message: `font "${key}" is designed for case "${effectiveCase}" — auto-corrected from "${labelCase}". Pick a different font (e.g. "bebas-neue", "rubik-mono-one") if you want case "${labelCase}".`,
|
|
156
|
+
suggestion: `Either keep the auto-correction (case: "${effectiveCase}") or change font.key.`,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
key, case: effectiveCase, weightBody, weightHeading, trackingLabel, outlineWidth,
|
|
161
|
+
// Spread so an absent scale leaves the KEY absent, not set to undefined.
|
|
162
|
+
...(sizeScale === undefined ? {} : { sizeScale }),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function checkColors(ctx, path, raw) {
|
|
166
|
+
if (!isObject(raw)) {
|
|
167
|
+
ctx.errors.push({ path, message: 'missing colors object' });
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const required = REQUIRED_THEME_COLOR_KEYS;
|
|
171
|
+
const result = {};
|
|
172
|
+
let allRequiredOk = true;
|
|
173
|
+
for (const field of required) {
|
|
174
|
+
const hex = checkHex(ctx, `${path}.${field}`, raw[field]);
|
|
175
|
+
if (hex)
|
|
176
|
+
result[field] = hex;
|
|
177
|
+
else
|
|
178
|
+
allRequiredOk = false;
|
|
179
|
+
}
|
|
180
|
+
for (const field of ['warning', 'success', 'textMuted']) {
|
|
181
|
+
if (raw[field] !== undefined) {
|
|
182
|
+
const hex = checkHex(ctx, `${path}.${field}`, raw[field]);
|
|
183
|
+
if (hex)
|
|
184
|
+
result[field] = hex;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
// Fallbacks chosen to remain on-brand without forcing the agent to fill them.
|
|
188
|
+
if (field === 'warning')
|
|
189
|
+
result.warning = result.primary ?? '#FF8200';
|
|
190
|
+
if (field === 'success')
|
|
191
|
+
result.success = result.primary ?? '#F9E547';
|
|
192
|
+
if (field === 'textMuted')
|
|
193
|
+
result.textMuted = '#b3b3b3';
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// outline stays ABSENT when omitted — no fallback fill. Absence means "no outline
|
|
197
|
+
// concept" and lets base styles keep their per-element default borders.
|
|
198
|
+
if (raw.outline !== undefined) {
|
|
199
|
+
const hex = checkHex(ctx, `${path}.outline`, raw.outline);
|
|
200
|
+
if (hex)
|
|
201
|
+
result.outline = hex;
|
|
202
|
+
}
|
|
203
|
+
if (!allRequiredOk)
|
|
204
|
+
return null;
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
function checkShape(ctx, path, raw) {
|
|
208
|
+
if (raw === undefined)
|
|
209
|
+
return { ...DEFAULT_THEME_SHAPE };
|
|
210
|
+
if (!isObject(raw)) {
|
|
211
|
+
ctx.errors.push({ path, message: 'expected shape object' });
|
|
212
|
+
return { ...DEFAULT_THEME_SHAPE };
|
|
213
|
+
}
|
|
214
|
+
const radiusPill = raw.radiusPill === undefined
|
|
215
|
+
? DEFAULT_THEME_SHAPE.radiusPill
|
|
216
|
+
: checkNumber(ctx, `${path}.radiusPill`, raw.radiusPill, 'radiusPill', DEFAULT_THEME_SHAPE.radiusPill);
|
|
217
|
+
const radiusCard = raw.radiusCard === undefined
|
|
218
|
+
? DEFAULT_THEME_SHAPE.radiusCard
|
|
219
|
+
: checkNumber(ctx, `${path}.radiusCard`, raw.radiusCard, 'radiusCard', DEFAULT_THEME_SHAPE.radiusCard);
|
|
220
|
+
const glow = raw.glow === undefined
|
|
221
|
+
? DEFAULT_THEME_SHAPE.glow
|
|
222
|
+
: checkEnum(ctx, `${path}.glow`, raw.glow, GLOW_KEYS) ?? DEFAULT_THEME_SHAPE.glow;
|
|
223
|
+
const imageRendering = raw.imageRendering === undefined
|
|
224
|
+
? DEFAULT_THEME_SHAPE.imageRendering
|
|
225
|
+
: checkEnum(ctx, `${path}.imageRendering`, raw.imageRendering, IMAGE_RENDERINGS) ??
|
|
226
|
+
DEFAULT_THEME_SHAPE.imageRendering;
|
|
227
|
+
const borderWidth = raw.borderWidth === undefined
|
|
228
|
+
? DEFAULT_THEME_SHAPE.borderWidth
|
|
229
|
+
: checkNumber(ctx, `${path}.borderWidth`, raw.borderWidth, 'borderWidth', DEFAULT_THEME_SHAPE.borderWidth);
|
|
230
|
+
const bevel = raw.bevel === undefined
|
|
231
|
+
? DEFAULT_THEME_SHAPE.bevel
|
|
232
|
+
: checkNumber(ctx, `${path}.bevel`, raw.bevel, 'bevel', DEFAULT_THEME_SHAPE.bevel);
|
|
233
|
+
const gloss = raw.gloss === undefined
|
|
234
|
+
? DEFAULT_THEME_SHAPE.gloss
|
|
235
|
+
: checkNumber(ctx, `${path}.gloss`, raw.gloss, 'gloss', DEFAULT_THEME_SHAPE.gloss);
|
|
236
|
+
// Absent stays absent — like colors.outline, absence is meaningful (keyed glow).
|
|
237
|
+
const glowColor = raw.glowColor === undefined
|
|
238
|
+
? undefined
|
|
239
|
+
: checkHex(ctx, `${path}.glowColor`, raw.glowColor) ?? undefined;
|
|
240
|
+
// No warning for glow:'none' + glowColor: the explicit hex simply wins,
|
|
241
|
+
// including over 'none'. An agent patching glowColor onto a zero-glow theme
|
|
242
|
+
// is asking for a glow in one edit — making 'none' veto it would turn the
|
|
243
|
+
// most direct ask ("purple neon") into a two-token puzzle. Turning a glow
|
|
244
|
+
// OFF again is deleting glowColor.
|
|
245
|
+
return {
|
|
246
|
+
radiusPill, radiusCard, glow, imageRendering, borderWidth, bevel, gloss,
|
|
247
|
+
...(glowColor === undefined ? {} : { glowColor }),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function checkBorderImageValue(ctx, path, raw) {
|
|
251
|
+
if (typeof raw === 'string') {
|
|
252
|
+
return checkEnum(ctx, path, raw, DECORATION_KEYS);
|
|
253
|
+
}
|
|
254
|
+
if (isObject(raw) && typeof raw.svg === 'string') {
|
|
255
|
+
if (raw.svg.length > MAX_INLINE_SVG_LENGTH) {
|
|
256
|
+
ctx.errors.push({ path: `${path}.svg`, message: `inline SVG exceeds 16kb limit` });
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
const borderSlice = checkNumber(ctx, `${path}.borderSlice`, raw.borderSlice ?? 30, 'borderSlice', 30);
|
|
260
|
+
const repeat = checkEnum(ctx, `${path}.repeat`, raw.repeat ?? 'stretch', BORDER_IMAGE_REPEATS) ?? 'stretch';
|
|
261
|
+
return { svg: raw.svg, borderSlice, repeat };
|
|
262
|
+
}
|
|
263
|
+
ctx.errors.push({ path, message: 'expected curated decoration key string or inline SVG object' });
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
function checkBackgroundValue(ctx, path, raw) {
|
|
267
|
+
if (typeof raw === 'string') {
|
|
268
|
+
return checkEnum(ctx, path, raw, DECORATION_KEYS);
|
|
269
|
+
}
|
|
270
|
+
if (isObject(raw) && typeof raw.svg === 'string') {
|
|
271
|
+
if (raw.svg.length > MAX_INLINE_SVG_LENGTH) {
|
|
272
|
+
ctx.errors.push({ path: `${path}.svg`, message: `inline SVG exceeds 16kb limit` });
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
const size = checkBackgroundSize(ctx, `${path}.size`, raw.size ?? 'auto') ?? 'auto';
|
|
276
|
+
const position = checkEnum(ctx, `${path}.position`, raw.position ?? 'center', BACKGROUND_POSITIONS) ?? 'center';
|
|
277
|
+
const repeat = checkEnum(ctx, `${path}.repeat`, raw.repeat ?? 'no-repeat', BACKGROUND_REPEATS) ?? 'no-repeat';
|
|
278
|
+
return { svg: raw.svg, size, position, repeat };
|
|
279
|
+
}
|
|
280
|
+
ctx.errors.push({ path, message: 'expected curated decoration key string or inline SVG object' });
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
function checkDecorationSlots(ctx, path, raw) {
|
|
284
|
+
if (!isObject(raw))
|
|
285
|
+
return {};
|
|
286
|
+
const slots = {};
|
|
287
|
+
for (const kind of DECORATION_SLOT_KINDS) {
|
|
288
|
+
if (raw[kind] === undefined)
|
|
289
|
+
continue;
|
|
290
|
+
const value = kind === 'borderImage'
|
|
291
|
+
? checkBorderImageValue(ctx, `${path}.${kind}`, raw[kind])
|
|
292
|
+
: checkBackgroundValue(ctx, `${path}.${kind}`, raw[kind]);
|
|
293
|
+
if (value !== null) {
|
|
294
|
+
if (typeof value === 'string') {
|
|
295
|
+
const entry = HUD_DECORATIONS[value];
|
|
296
|
+
if (!entry.suitsSlots.includes(kind)) {
|
|
297
|
+
ctx.warnings.push({
|
|
298
|
+
path: `${path}.${kind}`,
|
|
299
|
+
message: `decoration "${value}" isn't designed for slot "${kind}" (suits: ${entry.suitsSlots.join(', ')})`,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Workaround for noUncheckedIndexedAccess narrowing on the union literal.
|
|
304
|
+
slots[kind] = value;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return slots;
|
|
308
|
+
}
|
|
309
|
+
function checkDecorations(ctx, path, raw) {
|
|
310
|
+
if (raw === undefined)
|
|
311
|
+
return {};
|
|
312
|
+
if (!isObject(raw)) {
|
|
313
|
+
ctx.errors.push({ path, message: 'expected decorations object' });
|
|
314
|
+
return {};
|
|
315
|
+
}
|
|
316
|
+
const out = {};
|
|
317
|
+
for (const key of Object.keys(raw)) {
|
|
318
|
+
if (!ELEMENT_CLASSES.includes(key)) {
|
|
319
|
+
ctx.errors.push({
|
|
320
|
+
path: `${path}.${key}`,
|
|
321
|
+
message: `unknown HUD element class "${key}"`,
|
|
322
|
+
suggestion: `Known: ${ELEMENT_CLASSES.join(', ')}.`,
|
|
323
|
+
});
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
out[key] = checkDecorationSlots(ctx, `${path}.${key}`, raw[key]);
|
|
327
|
+
}
|
|
328
|
+
return out;
|
|
329
|
+
}
|
|
330
|
+
// Per-element override leaves are validated as WARN-AND-DROP, not as errors.
|
|
331
|
+
// These blocks used to be raw casts, so `elements.healthBar.colors.background:
|
|
332
|
+
// "bright-green"` sailed through validation and produced broken CSS with no
|
|
333
|
+
// message anywhere — the quietest possible failure. Errors would be the wrong
|
|
334
|
+
// fix: an error rejects the WHOLE theme, and already-published games carry
|
|
335
|
+
// frozen inline themes this validator must keep accepting. Dropping the one bad
|
|
336
|
+
// leaf keeps the theme rendering exactly as it did before, and the warning
|
|
337
|
+
// finally names the field. (The agent-side validator is deliberately stricter —
|
|
338
|
+
// at WRITE time a bad leaf is an error; at LOAD time it cannot be.)
|
|
339
|
+
function dropLeaf(ctx, path, message, suggestion) {
|
|
340
|
+
ctx.warnings.push({ path, message: `${message} — ignoring this override`, ...(suggestion ? { suggestion } : {}) });
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
function checkPartialColors(ctx, path, raw) {
|
|
344
|
+
if (!isObject(raw)) {
|
|
345
|
+
dropLeaf(ctx, path, 'expected a colors object');
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
const KNOWN = THEME_COLOR_KEYS;
|
|
349
|
+
const out = {};
|
|
350
|
+
for (const key of Object.keys(raw)) {
|
|
351
|
+
if (!KNOWN.includes(key)) {
|
|
352
|
+
dropLeaf(ctx, `${path}.${key}`, `unknown color token "${key}"`, `Known: ${KNOWN.join(', ')}.`);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
const value = raw[key];
|
|
356
|
+
if (!isHexColor(value)) {
|
|
357
|
+
dropLeaf(ctx, `${path}.${key}`, `${JSON.stringify(value)} is not a valid 6-digit hex color`);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
out[key] = value;
|
|
361
|
+
}
|
|
362
|
+
return out;
|
|
363
|
+
}
|
|
364
|
+
function partialNumber(ctx, path, value, token) {
|
|
365
|
+
const [min, max] = THEME_NUMBER_RANGES[token];
|
|
366
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < min || value > max) {
|
|
367
|
+
return dropLeaf(ctx, path, `${JSON.stringify(value)} is not a number in [${min}, ${max}]`);
|
|
368
|
+
}
|
|
369
|
+
return value;
|
|
370
|
+
}
|
|
371
|
+
function checkPartialShape(ctx, path, raw) {
|
|
372
|
+
if (!isObject(raw)) {
|
|
373
|
+
dropLeaf(ctx, path, 'expected a shape object');
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
const out = {};
|
|
377
|
+
if (raw.radiusPill !== undefined)
|
|
378
|
+
out.radiusPill = partialNumber(ctx, `${path}.radiusPill`, raw.radiusPill, 'radiusPill');
|
|
379
|
+
if (raw.radiusCard !== undefined)
|
|
380
|
+
out.radiusCard = partialNumber(ctx, `${path}.radiusCard`, raw.radiusCard, 'radiusCard');
|
|
381
|
+
if (raw.borderWidth !== undefined)
|
|
382
|
+
out.borderWidth = partialNumber(ctx, `${path}.borderWidth`, raw.borderWidth, 'borderWidth');
|
|
383
|
+
if (raw.bevel !== undefined)
|
|
384
|
+
out.bevel = partialNumber(ctx, `${path}.bevel`, raw.bevel, 'bevel');
|
|
385
|
+
if (raw.glow !== undefined) {
|
|
386
|
+
if (typeof raw.glow === 'string' && GLOW_KEYS.includes(raw.glow))
|
|
387
|
+
out.glow = raw.glow;
|
|
388
|
+
else
|
|
389
|
+
dropLeaf(ctx, `${path}.glow`, `${JSON.stringify(raw.glow)} is not one of: ${GLOW_KEYS.join(', ')}`);
|
|
390
|
+
}
|
|
391
|
+
if (raw.imageRendering !== undefined) {
|
|
392
|
+
if (IMAGE_RENDERINGS.includes(raw.imageRendering))
|
|
393
|
+
out.imageRendering = raw.imageRendering;
|
|
394
|
+
else
|
|
395
|
+
dropLeaf(ctx, `${path}.imageRendering`, `${JSON.stringify(raw.imageRendering)} is not "auto" or "pixelated"`);
|
|
396
|
+
}
|
|
397
|
+
if (raw.gloss !== undefined)
|
|
398
|
+
out.gloss = partialNumber(ctx, `${path}.gloss`, raw.gloss, 'gloss');
|
|
399
|
+
if (raw.glowColor !== undefined) {
|
|
400
|
+
if (isHexColor(raw.glowColor))
|
|
401
|
+
out.glowColor = raw.glowColor;
|
|
402
|
+
else
|
|
403
|
+
dropLeaf(ctx, `${path}.glowColor`, `${JSON.stringify(raw.glowColor)} is not a valid 6-digit hex color`);
|
|
404
|
+
}
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
function checkPartialFont(ctx, path, raw) {
|
|
408
|
+
if (!isObject(raw)) {
|
|
409
|
+
dropLeaf(ctx, path, 'expected a font object');
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
const out = {};
|
|
413
|
+
if (raw.key !== undefined) {
|
|
414
|
+
if (typeof raw.key === 'string' && FONT_KEYS.includes(raw.key))
|
|
415
|
+
out.key = raw.key;
|
|
416
|
+
else {
|
|
417
|
+
const close = typeof raw.key === 'string' ? suggestClosest(raw.key, FONT_KEYS) : undefined;
|
|
418
|
+
dropLeaf(ctx, `${path}.key`, `${JSON.stringify(raw.key)} is not a curated font key`, close ? `Closest match: "${close}"` : undefined);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (raw.case !== undefined) {
|
|
422
|
+
if (LABEL_CASES.includes(raw.case))
|
|
423
|
+
out.case = raw.case;
|
|
424
|
+
else
|
|
425
|
+
dropLeaf(ctx, `${path}.case`, `${JSON.stringify(raw.case)} is not "upper" or "normal"`);
|
|
426
|
+
}
|
|
427
|
+
if (raw.trackingLabel !== undefined)
|
|
428
|
+
out.trackingLabel = partialNumber(ctx, `${path}.trackingLabel`, raw.trackingLabel, 'trackingLabel');
|
|
429
|
+
if (raw.outlineWidth !== undefined)
|
|
430
|
+
out.outlineWidth = partialNumber(ctx, `${path}.outlineWidth`, raw.outlineWidth, 'outlineWidth');
|
|
431
|
+
if (raw.sizeScale !== undefined)
|
|
432
|
+
out.sizeScale = partialNumber(ctx, `${path}.sizeScale`, raw.sizeScale, 'sizeScale');
|
|
433
|
+
if (raw.weightBody !== undefined) {
|
|
434
|
+
if (FONT_BODY_WEIGHTS.includes(raw.weightBody))
|
|
435
|
+
out.weightBody = raw.weightBody;
|
|
436
|
+
else
|
|
437
|
+
dropLeaf(ctx, `${path}.weightBody`, `${JSON.stringify(raw.weightBody)} is not one of ${FONT_BODY_WEIGHTS.join(', ')}`);
|
|
438
|
+
}
|
|
439
|
+
if (raw.weightHeading !== undefined) {
|
|
440
|
+
if (FONT_HEADING_WEIGHTS.includes(raw.weightHeading))
|
|
441
|
+
out.weightHeading = raw.weightHeading;
|
|
442
|
+
else
|
|
443
|
+
dropLeaf(ctx, `${path}.weightHeading`, `${JSON.stringify(raw.weightHeading)} is not one of ${FONT_HEADING_WEIGHTS.join(', ')}`);
|
|
444
|
+
}
|
|
445
|
+
return out;
|
|
446
|
+
}
|
|
447
|
+
function checkPartialTheme(ctx, path, raw) {
|
|
448
|
+
if (!isObject(raw)) {
|
|
449
|
+
ctx.errors.push({ path, message: 'expected partial theme object' });
|
|
450
|
+
return { font: null, colors: null, shape: null, decorations: null };
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
font: raw.font === undefined ? null : checkPartialFont(ctx, `${path}.font`, raw.font),
|
|
454
|
+
colors: raw.colors === undefined ? null : checkPartialColors(ctx, `${path}.colors`, raw.colors),
|
|
455
|
+
shape: raw.shape === undefined ? null : checkPartialShape(ctx, `${path}.shape`, raw.shape),
|
|
456
|
+
decorations: raw.decorations === undefined
|
|
457
|
+
? null
|
|
458
|
+
: checkDecorationSlots(ctx, `${path}.decorations`, raw.decorations),
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
function checkElements(ctx, path, raw) {
|
|
462
|
+
if (raw === undefined)
|
|
463
|
+
return {};
|
|
464
|
+
if (!isObject(raw)) {
|
|
465
|
+
ctx.errors.push({ path, message: 'expected elements object' });
|
|
466
|
+
return {};
|
|
467
|
+
}
|
|
468
|
+
const out = {};
|
|
469
|
+
for (const key of Object.keys(raw)) {
|
|
470
|
+
if (!ELEMENT_CLASSES.includes(key)) {
|
|
471
|
+
ctx.errors.push({ path: `${path}.${key}`, message: `unknown HUD element class "${key}"` });
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
out[key] = checkPartialTheme(ctx, `${path}.${key}`, raw[key]);
|
|
475
|
+
}
|
|
476
|
+
return out;
|
|
477
|
+
}
|
|
478
|
+
function checkIcons(ctx, path, raw) {
|
|
479
|
+
if (raw === undefined || raw === null)
|
|
480
|
+
return {};
|
|
481
|
+
if (!isObject(raw)) {
|
|
482
|
+
ctx.errors.push({ path, message: 'icons must be an object with optional heart/mouse/arrow SVG strings' });
|
|
483
|
+
return {};
|
|
484
|
+
}
|
|
485
|
+
const ICON_SLOTS = THEME_ICON_KEYS;
|
|
486
|
+
const out = {};
|
|
487
|
+
for (const key of Object.keys(raw)) {
|
|
488
|
+
if (!ICON_SLOTS.includes(key)) {
|
|
489
|
+
ctx.errors.push({
|
|
490
|
+
path: `${path}.${key}`,
|
|
491
|
+
message: `unknown icon slot "${key}"`,
|
|
492
|
+
suggestion: `Known: ${ICON_SLOTS.join(', ')}`,
|
|
493
|
+
});
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
const value = raw[key];
|
|
497
|
+
if (value === undefined || value === null)
|
|
498
|
+
continue;
|
|
499
|
+
if (typeof value !== 'string') {
|
|
500
|
+
ctx.errors.push({ path: `${path}.${key}`, message: 'icon SVG must be a string' });
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (value.length > MAX_INLINE_SVG_LENGTH) {
|
|
504
|
+
ctx.errors.push({ path: `${path}.${key}`, message: `icon SVG exceeds 16kb limit` });
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
out[key] = value;
|
|
508
|
+
}
|
|
509
|
+
return out;
|
|
510
|
+
}
|
|
511
|
+
export function validateAndResolveTheme(input) {
|
|
512
|
+
const ctx = { errors: [], warnings: [] };
|
|
513
|
+
if (!isObject(input)) {
|
|
514
|
+
ctx.errors.push({ path: '', message: 'theme must be an object' });
|
|
515
|
+
return { ok: false, errors: ctx.errors, warnings: ctx.warnings };
|
|
516
|
+
}
|
|
517
|
+
const name = typeof input.name === 'string' && input.name.length > 0 ? input.name : null;
|
|
518
|
+
if (!name) {
|
|
519
|
+
ctx.errors.push({ path: 'name', message: 'name is required and must be non-empty' });
|
|
520
|
+
}
|
|
521
|
+
const font = checkFont(ctx, 'font', input.font);
|
|
522
|
+
const colors = checkColors(ctx, 'colors', input.colors);
|
|
523
|
+
const shape = checkShape(ctx, 'shape', input.shape);
|
|
524
|
+
const decorations = checkDecorations(ctx, 'decorations', input.decorations);
|
|
525
|
+
const elements = checkElements(ctx, 'elements', input.elements);
|
|
526
|
+
const icons = checkIcons(ctx, 'icons', input.icons);
|
|
527
|
+
if (ctx.errors.length > 0 || !name || !font || !colors) {
|
|
528
|
+
return { ok: false, errors: ctx.errors, warnings: ctx.warnings };
|
|
529
|
+
}
|
|
530
|
+
return {
|
|
531
|
+
ok: true,
|
|
532
|
+
theme: { name, font, colors, shape, decorations, elements, icons },
|
|
533
|
+
warnings: ctx.warnings,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @bitmagic/hud-theme — the HUD theme kernel for tools (the hosted agent and the `bitmagic` CLI).
|
|
3
|
+
*
|
|
4
|
+
* Two halves, one copy each:
|
|
5
|
+
*
|
|
6
|
+
* - The engine half is the engine's own DOM-free theme files in game/src/engine/hud/ (tokens and
|
|
7
|
+
* their vocabulary lists, colour maths, the font and decoration catalogues, the lenient
|
|
8
|
+
* load-time validator, the presets, resolveWorldTheme). This package compiles them; it does not
|
|
9
|
+
* copy them. The engine re-exports the same modules from `engine/hud/index.js`, and the engine
|
|
10
|
+
* tarball ships them under `engine/hud/` as it always has, so a vendored project needs nothing
|
|
11
|
+
* from this package.
|
|
12
|
+
* - The tool half lives in src/: the catalogue view, the strict write-time validator, the RFC 7386
|
|
13
|
+
* merge patch, the theme report and the stored-theme resolver.
|
|
14
|
+
*
|
|
15
|
+
* The CLI bundles this package into its own dist/ (cli/scripts/bundle-hud-theme.mjs), because a
|
|
16
|
+
* published CLI runs against vendored engines of other versions and cannot import theirs.
|
|
17
|
+
*/
|
|
18
|
+
export { DEFAULT_THEME_FONT, DEFAULT_THEME_SHAPE, GLOW_KEYS, HEX_COLOR, isHexColor, MAX_INLINE_SVG_LENGTH, THEME_NUMBER_RANGES, THEME_COLOR_KEYS, REQUIRED_THEME_COLOR_KEYS, THEME_SHAPE_KEYS, DECORATION_SLOT_KINDS, THEME_ICON_KEYS, } from './engine/hud/ThemeTokens.js';
|
|
19
|
+
export { relLuminance, contrastRatio, mixHex, readableTextColor, inkOnSurface, mutedInkOnSurface, accentInkOn, ON_SURFACE_MIN_CONTRAST, ACCENT_INK_MIN_CONTRAST, } from './engine/hud/colorMath.js';
|
|
20
|
+
export { validateAndResolveTheme, } from './engine/hud/validateTheme.js';
|
|
21
|
+
export { resolveWorldTheme, HUD_THEME_LOG_MARKER, HUD_UNKNOWN_PRESET_PHRASE, HUD_INVALID_THEME_PHRASE, } from './engine/hud/resolveWorldTheme.js';
|
|
22
|
+
export { HUD_FONTS, HUD_DECORATIONS, HUD_ELEMENT_CLASSES, HUD_PRESET_NAMES, isHudPresetName, HUD_PRESET_THEMES, presetTheme, HUD_PRESET_INFO, DEFAULT_THEME_TOKENS, HUD_DECORATION_SVGS, HUD_GLOW_KEYS, HUD_SHAPE_TOKENS, HUD_COLOR_TOKENS, HUD_REQUIRED_COLORS, HUD_DECORATION_SLOTS, HUD_ICON_SLOTS, HUD_ICON_KEYS, } from './catalog.js';
|
|
23
|
+
export { validateThemeStrict, isEngineFatalIssue, suggestClosest, } from './validate-strict.js';
|
|
24
|
+
export { applyMergePatch, diffThemes, deleteAtPath } from './merge.js';
|
|
25
|
+
export { buildThemeReport } from './report.js';
|
|
26
|
+
export { resolveStoredTheme } from './stored.js';
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
* changed: leaf-level diff of what the write actually did — the confirmation
|
|
9
9
|
* of the intended edit AND of anything a full replace changed
|
|
10
10
|
* without meaning to.
|
|
11
|
-
* contrast: the derived-ink pairings computed with
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* contrast: the derived-ink pairings computed with the engine's own
|
|
12
|
+
* colorMath module (compiled into this package, not copied), so
|
|
13
|
+
* "SUBSTITUTED -> renders #X" is a promise about the pixels, not an
|
|
14
|
+
* estimate.
|
|
15
15
|
*
|
|
16
16
|
* Deliberately report-only: the engine self-heals every pairing marked
|
|
17
17
|
* SUBSTITUTED, so nothing here blocks a write. The report exists so the agent
|
|
@@ -19,12 +19,12 @@
|
|
|
19
19
|
* renders as forest green might be exactly right, or reason to pick a colour
|
|
20
20
|
* the ground can carry.
|
|
21
21
|
*/
|
|
22
|
-
import { accentInkOn, contrastRatio, inkOnSurface, mutedInkOnSurface, readableTextColor, } from './
|
|
23
|
-
import {
|
|
24
|
-
|
|
22
|
+
import { accentInkOn, contrastRatio, inkOnSurface, mutedInkOnSurface, readableTextColor, } from './engine/hud/colorMath.js';
|
|
23
|
+
import { isHexColor } from './engine/hud/ThemeTokens.js';
|
|
24
|
+
import { diffThemes } from './merge.js';
|
|
25
25
|
const MAX_DIFF_LINES = 12;
|
|
26
26
|
function hex(value) {
|
|
27
|
-
return
|
|
27
|
+
return isHexColor(value) ? value : null;
|
|
28
28
|
}
|
|
29
29
|
function ratio(a, b) {
|
|
30
30
|
return `${(Math.round(contrastRatio(a, b) * 10) / 10).toFixed(1)}:1`;
|
|
@@ -139,4 +139,3 @@ export function buildThemeReport(after, options) {
|
|
|
139
139
|
}
|
|
140
140
|
return lines.join('\n');
|
|
141
141
|
}
|
|
142
|
-
//# sourceMappingURL=theme-report.js.map
|
|
@@ -3,14 +3,12 @@
|
|
|
3
3
|
* logic both lanes need before a patch (the base), a report (the `before`
|
|
4
4
|
* side), or a verdict (`show`/`check`) can exist.
|
|
5
5
|
*
|
|
6
|
-
* It
|
|
7
|
-
* readStoredTheme
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* form (or a label fix) had to land twice with nothing watching. This file is
|
|
11
|
-
* part of the byte-identical kit mirror, so the gate now watches.
|
|
6
|
+
* It used to live twice (the write tool's currentEffectiveTheme and the CLI's
|
|
7
|
+
* readStoredTheme), and the copies had diverged: one named an unknown preset
|
|
8
|
+
* truthfully, the other folded it into "the engine default". Both lanes now
|
|
9
|
+
* call this one.
|
|
12
10
|
*/
|
|
13
|
-
import { DEFAULT_THEME_TOKENS,
|
|
11
|
+
import { DEFAULT_THEME_TOKENS, isHudPresetName, presetTheme } from './catalog.js';
|
|
14
12
|
/** Resolve a raw `worldProfileData.hud.theme` value to what the engine renders. */
|
|
15
13
|
export function resolveStoredTheme(stored) {
|
|
16
14
|
if (stored === undefined || stored === null) {
|
|
@@ -21,7 +19,7 @@ export function resolveStoredTheme(stored) {
|
|
|
21
19
|
};
|
|
22
20
|
}
|
|
23
21
|
if (typeof stored === 'string') {
|
|
24
|
-
if (
|
|
22
|
+
if (isHudPresetName(stored)) {
|
|
25
23
|
return {
|
|
26
24
|
kind: 'preset',
|
|
27
25
|
tokens: presetTheme(stored),
|
|
@@ -47,4 +45,3 @@ export function resolveStoredTheme(stored) {
|
|
|
47
45
|
label: `the engine default (hud.theme has unexpected type ${typeof stored})`,
|
|
48
46
|
};
|
|
49
47
|
}
|
|
50
|
-
//# sourceMappingURL=theme-stored.js.map
|