@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.
Files changed (93) hide show
  1. package/dist/assets/vxl-summary.js +1 -1
  2. package/dist/assets/vxl-summary.js.map +1 -1
  3. package/dist/commands/steam-upload.js +6 -4
  4. package/dist/commands/steam-upload.js.map +1 -1
  5. package/dist/commands/steam.js +4 -2
  6. package/dist/commands/steam.js.map +1 -1
  7. package/dist/forge/browser-host.js +1 -1
  8. package/dist/forge/browser-host.js.map +1 -1
  9. package/dist/project/published-three.js +4 -6
  10. package/dist/project/published-three.js.map +1 -1
  11. package/dist/scaffold/project-files.js +2 -2
  12. package/dist/scaffold/project-files.js.map +1 -1
  13. package/dist/steam/art-generate.js +2 -1
  14. package/dist/steam/art-generate.js.map +1 -1
  15. package/dist/steam/art-render.js.map +1 -1
  16. package/dist/steam/art.js +1 -1
  17. package/dist/steam/art.js.map +1 -1
  18. package/dist/steam/check.js.map +1 -1
  19. package/dist/steam/electron-cache.d.ts +1 -1
  20. package/dist/steam/electron-cache.js +2 -2
  21. package/dist/steam/electron-cache.js.map +1 -1
  22. package/dist/steam/fuses.js +1 -1
  23. package/dist/steam/fuses.js.map +1 -1
  24. package/dist/steam/media.js +6 -2
  25. package/dist/steam/media.js.map +1 -1
  26. package/dist/steam/package-windows.d.ts +1 -2
  27. package/dist/steam/package-windows.js +1 -2
  28. package/dist/steam/package-windows.js.map +1 -1
  29. package/dist/steam/paths.d.ts +0 -2
  30. package/dist/steam/paths.js +0 -1
  31. package/dist/steam/paths.js.map +1 -1
  32. package/dist/steam/portal.js +1 -1
  33. package/dist/steam/portal.js.map +1 -1
  34. package/dist/steam/raster.d.ts +0 -1
  35. package/dist/steam/raster.js +1 -1
  36. package/dist/steam/raster.js.map +1 -1
  37. package/dist/steam/store-copy.d.ts +0 -1
  38. package/dist/steam/store-copy.js +3 -10
  39. package/dist/steam/store-copy.js.map +1 -1
  40. package/dist/steam/tags.js +6 -3
  41. package/dist/steam/tags.js.map +1 -1
  42. package/dist/theme/kernel.d.ts +14 -0
  43. package/dist/theme/kernel.js +15 -0
  44. package/dist/theme/kernel.js.map +1 -0
  45. package/dist/theme/theme-io.js +11 -16
  46. package/dist/theme/theme-io.js.map +1 -1
  47. package/dist/vendor/hud-theme/catalog.js +71 -0
  48. package/dist/vendor/hud-theme/engine/hud/ThemeTokens.js +115 -0
  49. package/dist/{theme/theme-color-math.js → vendor/hud-theme/engine/hud/colorMath.js} +13 -14
  50. package/dist/vendor/hud-theme/engine/hud/decorations.js +179 -0
  51. package/dist/vendor/hud-theme/engine/hud/fonts.js +147 -0
  52. package/dist/vendor/hud-theme/engine/hud/presets.js +80 -0
  53. package/dist/vendor/hud-theme/engine/hud/resolveWorldTheme.js +66 -0
  54. package/dist/vendor/hud-theme/engine/hud/themes/bitmagic.js +29 -0
  55. package/dist/vendor/hud-theme/engine/hud/themes/nexus.js +69 -0
  56. package/dist/vendor/hud-theme/engine/hud/themes/racing.js +57 -0
  57. package/dist/vendor/hud-theme/engine/hud/themes/rift-raider.js +63 -0
  58. package/dist/vendor/hud-theme/engine/hud/themes/simulator.js +55 -0
  59. package/dist/vendor/hud-theme/engine/hud/themes/village-keep.js +108 -0
  60. package/dist/vendor/hud-theme/engine/hud/validateTheme.js +535 -0
  61. package/dist/vendor/hud-theme/index.js +26 -0
  62. package/dist/{theme/theme-merge.js → vendor/hud-theme/merge.js} +0 -1
  63. package/dist/{theme/theme-report.js → vendor/hud-theme/report.js} +8 -9
  64. package/dist/{theme/theme-stored.js → vendor/hud-theme/stored.js} +6 -9
  65. package/dist/{theme/theme-validate.js → vendor/hud-theme/validate-strict.js} +102 -164
  66. package/dist/verify/browser.js +27 -3
  67. package/dist/verify/browser.js.map +1 -1
  68. package/dist/verify/categories.d.ts +1 -1
  69. package/dist/verify/categories.js +1 -0
  70. package/dist/verify/categories.js.map +1 -1
  71. package/dist/verify/classify.d.ts +3 -0
  72. package/dist/verify/classify.js +7 -0
  73. package/dist/verify/classify.js.map +1 -1
  74. package/dist/verify/hud-theme.d.ts +19 -14
  75. package/dist/verify/hud-theme.js +23 -17
  76. package/dist/verify/hud-theme.js.map +1 -1
  77. package/dist/verify/start.d.ts +3 -2
  78. package/dist/verify/start.js +1 -1
  79. package/dist/verify/start.js.map +1 -1
  80. package/package.json +7 -6
  81. package/dist/theme/hud-theme-catalog.d.ts +0 -235
  82. package/dist/theme/hud-theme-catalog.js +0 -389
  83. package/dist/theme/hud-theme-catalog.js.map +0 -1
  84. package/dist/theme/theme-color-math.d.ts +0 -21
  85. package/dist/theme/theme-color-math.js.map +0 -1
  86. package/dist/theme/theme-merge.d.ts +0 -36
  87. package/dist/theme/theme-merge.js.map +0 -1
  88. package/dist/theme/theme-report.d.ts +0 -28
  89. package/dist/theme/theme-report.js.map +0 -1
  90. package/dist/theme/theme-stored.d.ts +0 -21
  91. package/dist/theme/theme-stored.js.map +0 -1
  92. package/dist/theme/theme-validate.d.ts +0 -41
  93. 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';
@@ -78,4 +78,3 @@ export function deleteAtPath(target, dottedPath) {
78
78
  delete node[leaf];
79
79
  return true;
80
80
  }
81
- //# sourceMappingURL=theme-merge.js.map
@@ -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 EXACTLY the engine's
12
- * arithmetic (theme-color-math mirrors engine colorMath, gate-
13
- * checked), so "SUBSTITUTED -> renders #X" is a promise about the
14
- * pixels, not an estimate.
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 './theme-color-math.js';
23
- import { diffThemes } from './theme-merge.js';
24
- const HEX = /^#[0-9a-fA-F]{6}$/;
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 typeof value === 'string' && HEX.test(value) ? value : null;
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 lived twice: the write tool's currentEffectiveTheme and the CLI's
7
- * readStoredTheme each hand-rolled the stored-value→tokens mapping, and they
8
- * had already diverged — the CLI copy named an unknown preset truthfully while
9
- * the agent copy silently folded it into "the engine default". A new stored
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, HUD_PRESETS, presetTheme } from './hud-theme-catalog.js';
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 (HUD_PRESETS.includes(stored)) {
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