@bitmagic/cli 0.1.83-dev.1 → 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 (206) hide show
  1. package/README.md +69 -0
  2. package/assets/skills/publishing-to-steam/SKILL.md +218 -0
  3. package/dist/assets/vxl-summary.d.ts +6 -20
  4. package/dist/assets/vxl-summary.js +9 -100
  5. package/dist/assets/vxl-summary.js.map +1 -1
  6. package/dist/cli.d.ts +1 -0
  7. package/dist/cli.js +2 -0
  8. package/dist/cli.js.map +1 -1
  9. package/dist/commands/check.js +5 -0
  10. package/dist/commands/check.js.map +1 -1
  11. package/dist/commands/forge.js +1 -1
  12. package/dist/commands/forge.js.map +1 -1
  13. package/dist/commands/ios.js +88 -136
  14. package/dist/commands/ios.js.map +1 -1
  15. package/dist/commands/steam-build.d.ts +67 -0
  16. package/dist/commands/steam-build.js +286 -0
  17. package/dist/commands/steam-build.js.map +1 -0
  18. package/dist/commands/steam-portal.d.ts +1 -0
  19. package/dist/commands/steam-portal.js +319 -0
  20. package/dist/commands/steam-portal.js.map +1 -0
  21. package/dist/commands/steam-upload.d.ts +37 -0
  22. package/dist/commands/steam-upload.js +174 -0
  23. package/dist/commands/steam-upload.js.map +1 -0
  24. package/dist/commands/steam.d.ts +1 -0
  25. package/dist/commands/steam.js +504 -0
  26. package/dist/commands/steam.js.map +1 -0
  27. package/dist/commands/verify.js +6 -0
  28. package/dist/commands/verify.js.map +1 -1
  29. package/dist/editor/asset-viewer.js +5 -4
  30. package/dist/editor/asset-viewer.js.map +1 -1
  31. package/dist/editor/server.js +1 -1
  32. package/dist/editor/server.js.map +1 -1
  33. package/dist/editor/shell-page.js +3 -3
  34. package/dist/forge/browser-host.d.ts +1 -1
  35. package/dist/forge/browser-host.js +1 -1
  36. package/dist/forge/browser-host.js.map +1 -1
  37. package/dist/ios/native-overrides.d.ts +27 -0
  38. package/dist/ios/native-overrides.js +87 -0
  39. package/dist/ios/native-overrides.js.map +1 -0
  40. package/dist/ios/record-validate.d.ts +1 -33
  41. package/dist/ios/record-validate.js +2 -100
  42. package/dist/ios/record-validate.js.map +1 -1
  43. package/dist/ios/store-bridge.d.ts +26 -0
  44. package/dist/ios/store-bridge.js +224 -0
  45. package/dist/ios/store-bridge.js.map +1 -0
  46. package/dist/ios/storekit-config.d.ts +15 -0
  47. package/dist/ios/storekit-config.js +106 -0
  48. package/dist/ios/storekit-config.js.map +1 -0
  49. package/dist/ios/xcode-template.d.ts +12 -3
  50. package/dist/ios/xcode-template.js +158 -27
  51. package/dist/ios/xcode-template.js.map +1 -1
  52. package/dist/package/collect-urls.d.ts +1 -1
  53. package/dist/package/collect-urls.js +10 -2
  54. package/dist/package/collect-urls.js.map +1 -1
  55. package/dist/package/offline-payload.d.ts +35 -0
  56. package/dist/package/offline-payload.js +142 -0
  57. package/dist/package/offline-payload.js.map +1 -0
  58. package/dist/package/packaged-check.d.ts +39 -0
  59. package/dist/package/packaged-check.js +104 -0
  60. package/dist/package/packaged-check.js.map +1 -0
  61. package/dist/package/transform.d.ts +4 -4
  62. package/dist/package/transform.js +3 -3
  63. package/dist/project/published-three.d.ts +14 -0
  64. package/dist/project/published-three.js +91 -0
  65. package/dist/project/published-three.js.map +1 -0
  66. package/dist/publish/bundle.js +6 -0
  67. package/dist/publish/bundle.js.map +1 -1
  68. package/dist/scaffold/project-files.js +7 -4
  69. package/dist/scaffold/project-files.js.map +1 -1
  70. package/dist/scaffold/project.js +2 -0
  71. package/dist/scaffold/project.js.map +1 -1
  72. package/dist/scaffold/skill-files.js +3 -1
  73. package/dist/scaffold/skill-files.js.map +1 -1
  74. package/dist/scaffold/steam-skill-files.d.ts +2 -0
  75. package/dist/scaffold/steam-skill-files.js +7 -0
  76. package/dist/scaffold/steam-skill-files.js.map +1 -0
  77. package/dist/steam/art-generate.d.ts +55 -0
  78. package/dist/steam/art-generate.js +174 -0
  79. package/dist/steam/art-generate.js.map +1 -0
  80. package/dist/steam/art-render.d.ts +25 -0
  81. package/dist/steam/art-render.js +157 -0
  82. package/dist/steam/art-render.js.map +1 -0
  83. package/dist/steam/art-use.d.ts +20 -0
  84. package/dist/steam/art-use.js +84 -0
  85. package/dist/steam/art-use.js.map +1 -0
  86. package/dist/steam/art.d.ts +98 -0
  87. package/dist/steam/art.js +243 -0
  88. package/dist/steam/art.js.map +1 -0
  89. package/dist/steam/check.d.ts +21 -0
  90. package/dist/steam/check.js +113 -0
  91. package/dist/steam/check.js.map +1 -0
  92. package/dist/steam/config.d.ts +57 -0
  93. package/dist/steam/config.js +145 -0
  94. package/dist/steam/config.js.map +1 -0
  95. package/dist/steam/electron-cache.d.ts +11 -0
  96. package/dist/steam/electron-cache.js +105 -0
  97. package/dist/steam/electron-cache.js.map +1 -0
  98. package/dist/steam/fuses.d.ts +16 -0
  99. package/dist/steam/fuses.js +75 -0
  100. package/dist/steam/fuses.js.map +1 -0
  101. package/dist/steam/icon-files.d.ts +16 -0
  102. package/dist/steam/icon-files.js +70 -0
  103. package/dist/steam/icon-files.js.map +1 -0
  104. package/dist/steam/image-info.d.ts +16 -0
  105. package/dist/steam/image-info.js +89 -0
  106. package/dist/steam/image-info.js.map +1 -0
  107. package/dist/steam/launch-check.d.ts +12 -0
  108. package/dist/steam/launch-check.js +101 -0
  109. package/dist/steam/launch-check.js.map +1 -0
  110. package/dist/steam/media.d.ts +48 -0
  111. package/dist/steam/media.js +155 -0
  112. package/dist/steam/media.js.map +1 -0
  113. package/dist/steam/package-macos.d.ts +52 -0
  114. package/dist/steam/package-macos.js +268 -0
  115. package/dist/steam/package-macos.js.map +1 -0
  116. package/dist/steam/package-windows.d.ts +23 -0
  117. package/dist/steam/package-windows.js +83 -0
  118. package/dist/steam/package-windows.js.map +1 -0
  119. package/dist/steam/paths.d.ts +35 -0
  120. package/dist/steam/paths.js +44 -0
  121. package/dist/steam/paths.js.map +1 -0
  122. package/dist/steam/playtest-shots.d.ts +18 -0
  123. package/dist/steam/playtest-shots.js +83 -0
  124. package/dist/steam/playtest-shots.js.map +1 -0
  125. package/dist/steam/portal-recipes.d.ts +25 -0
  126. package/dist/steam/portal-recipes.js +61 -0
  127. package/dist/steam/portal-recipes.js.map +1 -0
  128. package/dist/steam/portal.d.ts +58 -0
  129. package/dist/steam/portal.js +340 -0
  130. package/dist/steam/portal.js.map +1 -0
  131. package/dist/steam/raster.d.ts +58 -0
  132. package/dist/steam/raster.js +285 -0
  133. package/dist/steam/raster.js.map +1 -0
  134. package/dist/steam/shell-files.d.ts +17 -0
  135. package/dist/steam/shell-files.js +268 -0
  136. package/dist/steam/shell-files.js.map +1 -0
  137. package/dist/steam/specs.d.ts +61 -0
  138. package/dist/steam/specs.js +103 -0
  139. package/dist/steam/specs.js.map +1 -0
  140. package/dist/steam/steampipe.d.ts +46 -0
  141. package/dist/steam/steampipe.js +170 -0
  142. package/dist/steam/steampipe.js.map +1 -0
  143. package/dist/steam/store-copy.d.ts +88 -0
  144. package/dist/steam/store-copy.js +214 -0
  145. package/dist/steam/store-copy.js.map +1 -0
  146. package/dist/steam/tags.d.ts +9 -0
  147. package/dist/steam/tags.js +52 -0
  148. package/dist/steam/tags.js.map +1 -0
  149. package/dist/steam/trailer.d.ts +34 -0
  150. package/dist/steam/trailer.js +141 -0
  151. package/dist/steam/trailer.js.map +1 -0
  152. package/dist/theme/kernel.d.ts +14 -0
  153. package/dist/theme/kernel.js +15 -0
  154. package/dist/theme/kernel.js.map +1 -0
  155. package/dist/theme/theme-io.js +11 -16
  156. package/dist/theme/theme-io.js.map +1 -1
  157. package/dist/vendor/hud-theme/catalog.js +71 -0
  158. package/dist/vendor/hud-theme/engine/hud/ThemeTokens.js +115 -0
  159. package/dist/{theme/theme-color-math.js → vendor/hud-theme/engine/hud/colorMath.js} +13 -14
  160. package/dist/vendor/hud-theme/engine/hud/decorations.js +179 -0
  161. package/dist/vendor/hud-theme/engine/hud/fonts.js +147 -0
  162. package/dist/vendor/hud-theme/engine/hud/presets.js +80 -0
  163. package/dist/vendor/hud-theme/engine/hud/resolveWorldTheme.js +66 -0
  164. package/dist/vendor/hud-theme/engine/hud/themes/bitmagic.js +29 -0
  165. package/dist/vendor/hud-theme/engine/hud/themes/nexus.js +69 -0
  166. package/dist/vendor/hud-theme/engine/hud/themes/racing.js +57 -0
  167. package/dist/vendor/hud-theme/engine/hud/themes/rift-raider.js +63 -0
  168. package/dist/vendor/hud-theme/engine/hud/themes/simulator.js +55 -0
  169. package/dist/vendor/hud-theme/engine/hud/themes/village-keep.js +108 -0
  170. package/dist/vendor/hud-theme/engine/hud/validateTheme.js +535 -0
  171. package/dist/vendor/hud-theme/index.js +26 -0
  172. package/dist/{theme/theme-merge.js → vendor/hud-theme/merge.js} +0 -1
  173. package/dist/{theme/theme-report.js → vendor/hud-theme/report.js} +8 -9
  174. package/dist/{theme/theme-stored.js → vendor/hud-theme/stored.js} +6 -9
  175. package/dist/{theme/theme-validate.js → vendor/hud-theme/validate-strict.js} +102 -164
  176. package/dist/verify/browser.d.ts +11 -0
  177. package/dist/verify/browser.js +222 -155
  178. package/dist/verify/browser.js.map +1 -1
  179. package/dist/verify/categories.d.ts +1 -1
  180. package/dist/verify/categories.js +1 -0
  181. package/dist/verify/categories.js.map +1 -1
  182. package/dist/verify/classify.d.ts +14 -2
  183. package/dist/verify/classify.js +23 -4
  184. package/dist/verify/classify.js.map +1 -1
  185. package/dist/verify/hud-theme.d.ts +19 -14
  186. package/dist/verify/hud-theme.js +23 -17
  187. package/dist/verify/hud-theme.js.map +1 -1
  188. package/dist/verify/mobile-parity.d.ts +1 -1
  189. package/dist/verify/mobile-parity.js +1 -1
  190. package/dist/verify/start.d.ts +3 -2
  191. package/dist/verify/start.js +1 -1
  192. package/dist/verify/start.js.map +1 -1
  193. package/package.json +8 -6
  194. package/dist/theme/hud-theme-catalog.d.ts +0 -235
  195. package/dist/theme/hud-theme-catalog.js +0 -389
  196. package/dist/theme/hud-theme-catalog.js.map +0 -1
  197. package/dist/theme/theme-color-math.d.ts +0 -21
  198. package/dist/theme/theme-color-math.js.map +0 -1
  199. package/dist/theme/theme-merge.d.ts +0 -36
  200. package/dist/theme/theme-merge.js.map +0 -1
  201. package/dist/theme/theme-report.d.ts +0 -28
  202. package/dist/theme/theme-report.js.map +0 -1
  203. package/dist/theme/theme-stored.d.ts +0 -21
  204. package/dist/theme/theme-stored.js.map +0 -1
  205. package/dist/theme/theme-validate.d.ts +0 -41
  206. package/dist/theme/theme-validate.js.map +0 -1
@@ -1,15 +1,15 @@
1
1
  /**
2
- * Hand-mirror of game/src/engine/hud/colorMath.ts — the WCAG arithmetic behind
3
- * the derived HUD inks. The theme report computes its contrast pairings with
4
- * EXACTLY the maths the engine will apply, so a SUBSTITUTED line in the report
5
- * is a promise about what will render, not an estimate.
2
+ * The WCAG colour arithmetic behind the derived HUD inks — relative luminance,
3
+ * contrast ratios, and the substitution rules that make `--hud-color-on-surface`,
4
+ * `--hud-color-accent-ink` and friends safe to derive.
6
5
  *
7
- * Mirrored by hand because agent tools cannot import engine source (separate
8
- * tsconfig include sets). Drift-checked BEHAVIOURALLY by scripts/
9
- * check-hud-catalog.ts: the gate imports both copies under tsx and compares
10
- * their outputs over every preset palette plus adversarial pairs, so a changed
11
- * threshold or mix factor on either side goes red in CI.
6
+ * A separate module for one load-bearing reason: it is PURE. No DOM, and nothing
7
+ * outside `engine/hud/` — so `@bitmagic/hud-theme` compiles this exact file and
8
+ * the theme report the agent and the CLI print computes its contrast pairings
9
+ * with the arithmetic the engine applies. A SUBSTITUTED line in that report is a
10
+ * promise about what will render, not an estimate.
12
11
  */
12
+ import { isHexColor } from './ThemeTokens.js';
13
13
  export function relLuminance(hex) {
14
14
  const lin = (v) => (v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4));
15
15
  return 0.2126 * lin(parseInt(hex.slice(1, 3), 16) / 255)
@@ -30,7 +30,7 @@ export function mixHex(from, to, amount) {
30
30
  return `#${channel(1)}${channel(3)}${channel(5)}`;
31
31
  }
32
32
  export function readableTextColor(hex) {
33
- if (!/^#[0-9a-fA-F]{6}$/.test(hex))
33
+ if (!isHexColor(hex))
34
34
  return '#ffffff';
35
35
  return relLuminance(hex) > 0.5 ? '#000000' : '#ffffff';
36
36
  }
@@ -54,7 +54,7 @@ export function readableTextColor(hex) {
54
54
  // to 9.3 for `textMuted`, so this cannot fire on a theme that was already coherent.
55
55
  export const ON_SURFACE_MIN_CONTRAST = 3;
56
56
  export function inkOnSurface(surface, preferred) {
57
- if (!/^#[0-9a-fA-F]{6}$/.test(surface) || !/^#[0-9a-fA-F]{6}$/.test(preferred))
57
+ if (!isHexColor(surface) || !isHexColor(preferred))
58
58
  return preferred;
59
59
  if (contrastRatio(preferred, surface) >= ON_SURFACE_MIN_CONTRAST)
60
60
  return preferred;
@@ -66,7 +66,7 @@ export function inkOnSurface(surface, preferred) {
66
66
  // the bar cannot make the token fire on a theme that was already fine.
67
67
  export const ACCENT_INK_MIN_CONTRAST = 4.5;
68
68
  export function accentInkOn(ground, accent) {
69
- if (!/^#[0-9a-fA-F]{6}$/.test(ground) || !/^#[0-9a-fA-F]{6}$/.test(accent))
69
+ if (!isHexColor(ground) || !isHexColor(accent))
70
70
  return null;
71
71
  if (contrastRatio(accent, ground) >= ACCENT_INK_MIN_CONTRAST)
72
72
  return null;
@@ -84,7 +84,7 @@ export function accentInkOn(ground, accent) {
84
84
  return readableTextColor(ground);
85
85
  }
86
86
  export function mutedInkOnSurface(surface, preferred) {
87
- if (!/^#[0-9a-fA-F]{6}$/.test(surface) || !/^#[0-9a-fA-F]{6}$/.test(preferred))
87
+ if (!isHexColor(surface) || !isHexColor(preferred))
88
88
  return preferred;
89
89
  if (contrastRatio(preferred, surface) >= ON_SURFACE_MIN_CONTRAST)
90
90
  return preferred;
@@ -96,4 +96,3 @@ export function mutedInkOnSurface(surface, preferred) {
96
96
  // the full ink beside it.
97
97
  return mixHex(readableTextColor(surface), surface, 0.78);
98
98
  }
99
- //# sourceMappingURL=theme-color-math.js.map
@@ -0,0 +1,179 @@
1
+ // Compact SVGs (each <500 bytes). Color tokens get substituted at apply time.
2
+ const DRIP_BORDER_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" preserveAspectRatio="none"><rect x="0" y="0" width="60" height="2" fill="__primary__"/><rect x="0" y="0" width="2" height="60" fill="__primary__"/><rect x="58" y="0" width="2" height="60" fill="__primary__"/><path d="M0,58 L60,58 L60,50 Q54,60 48,52 Q42,62 36,53 Q30,64 24,54 Q18,60 12,52 Q6,62 0,52 Z" fill="__primary__"/></svg>`;
3
+ const PIXEL_CORNERS_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" shape-rendering="crispEdges"><path d="M0,0 H4 V2 H2 V4 H0 Z M8,0 H12 V4 H10 V2 H8 Z M0,8 H2 V10 H4 V12 H0 Z M10,8 H12 V12 H8 V10 H10 Z" fill="__primary__"/></svg>`;
4
+ const SCANLINE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 4" preserveAspectRatio="none"><rect width="4" height="1" fill="__primary__" opacity="0.15"/></svg>`;
5
+ const VINE_CORNER_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path d="M0,40 Q0,20 8,12 Q16,4 24,8 Q20,16 12,16 Q4,20 4,40 Z" fill="__success__" opacity="0.7"/><circle cx="10" cy="14" r="2" fill="__success__"/><circle cx="16" cy="10" r="1.5" fill="__success__"/></svg>`;
6
+ const GLOW_AURA_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><radialGradient id="g"><stop offset="0%" stop-color="__primary__" stop-opacity="0.5"/><stop offset="60%" stop-color="__primary__" stop-opacity="0.1"/><stop offset="100%" stop-color="__primary__" stop-opacity="0"/></radialGradient></defs><rect width="100" height="100" fill="url(#g)"/></svg>`;
7
+ const BRACKET_FRAME_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0,0 H8 V1 H1 V8 H0 Z M16,0 H24 V8 H23 V1 H16 Z M0,16 H1 V23 H8 V24 H0 Z M16,24 V23 H23 V16 H24 V24 Z" fill="__primary__"/></svg>`;
8
+ const RIBBON_EDGE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 8" preserveAspectRatio="none"><path d="M0,0 L30,0 L30,4 L26,8 L22,4 L18,8 L14,4 L10,8 L6,4 L2,8 L0,4 Z" fill="__primary__"/></svg>`;
9
+ const CIRCUIT_TRACE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M0,16 H10 M22,16 H32 M16,0 V10 M16,22 V32" stroke="__primary__" stroke-width="1" fill="none"/><circle cx="16" cy="16" r="2" fill="none" stroke="__primary__" stroke-width="1"/></svg>`;
10
+ // A single soft-edged highlight bowing across the top. The flat-opacity ellipse
11
+ // this replaced had a hard rim, so at any size above a small chip it stopped
12
+ // reading as light and started reading as a grey shape sitting on the element.
13
+ const CANDY_GLOSS_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 40" preserveAspectRatio="none"><radialGradient id="cgh" cx=".5" cy=".28" r=".72"><stop offset="0" stop-color="#fff" stop-opacity=".38"/><stop offset=".55" stop-color="#fff" stop-opacity=".15"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></radialGradient><ellipse cx="50" cy="8" rx="48" ry="13" fill="url(#cgh)"/></svg>`;
14
+ const ORNATE_CORNER_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><g id="f"><path d="M2 14 Q2 2 14 2" fill="none" stroke="__primary__" stroke-width="2"/><path d="M8 8 L11 5 L14 8 L11 11 Z" fill="__primary__"/></g></defs><rect x="5" y="5" width="38" height="38" fill="none" stroke="__primary__" stroke-width="1.5"/><use href="#f"/><use href="#f" transform="rotate(90 24 24)"/><use href="#f" transform="rotate(180 24 24)"/><use href="#f" transform="rotate(270 24 24)"/></svg>`;
15
+ const ROYAL_FRAME_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><rect x="3" y="3" width="42" height="42" rx="6" fill="none" stroke="__primary__" stroke-width="2"/><rect x="8" y="8" width="32" height="32" rx="3" fill="none" stroke="__primary__" stroke-width="1" opacity="0.6"/><circle cx="6" cy="6" r="2.4" fill="__primary__"/><circle cx="42" cy="6" r="2.4" fill="__primary__"/><circle cx="6" cy="42" r="2.4" fill="__primary__"/><circle cx="42" cy="42" r="2.4" fill="__primary__"/></svg>`;
16
+ const HAZARD_STRIPES_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M-4 4 L4 -4 M0 16 L16 0 M12 20 L20 12" stroke="__warning__" stroke-width="4" opacity="0.12" fill="none"/></svg>`;
17
+ const PIXEL_FRAME_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" shape-rendering="crispEdges"><path fill-rule="evenodd" d="M0 0 H16 V16 H0 Z M1 1 V15 H15 V1 Z" fill="__text__"/><path fill-rule="evenodd" d="M3 3 H13 V13 H3 Z M4 4 V12 H12 V4 Z" fill="__primary__"/></svg>`;
18
+ const GRAIN_TEXTURE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><g fill="__text__" opacity="0.07"><rect x="3" y="5" width="1" height="1"/><rect x="11" y="2" width="1" height="1"/><rect x="19" y="9" width="1" height="1"/><rect x="27" y="4" width="1" height="1"/><rect x="35" y="12" width="1" height="1"/><rect x="6" y="17" width="1" height="1"/><rect x="15" y="22" width="1" height="1"/><rect x="24" y="18" width="1" height="1"/><rect x="33" y="26" width="1" height="1"/><rect x="2" y="29" width="1" height="1"/><rect x="12" y="34" width="1" height="1"/><rect x="22" y="31" width="1" height="1"/><rect x="30" y="37" width="1" height="1"/><rect x="38" y="33" width="1" height="1"/></g></svg>`;
19
+ export const HUD_DECORATIONS = {
20
+ 'drip-border': {
21
+ key: 'drip-border',
22
+ svg: DRIP_BORDER_SVG,
23
+ suitsSlots: ['borderImage', 'decorationAfter'],
24
+ borderImage: { borderSlice: 20, repeat: 'repeat' },
25
+ // Explicit tile width for the same reason as `circuit-trace` below, with
26
+ // one extra twist: this SVG also carries `preserveAspectRatio="none"`,
27
+ // so it has no intrinsic RATIO either. Under `background-size: auto`
28
+ // that resolves to 100% 100% — a single drip motif stretched across the
29
+ // whole strip, which is the opposite of drips repeating along an edge.
30
+ // `100%` for the height is deliberate: the strip is 12px and the drips
31
+ // should fill it.
32
+ background: { size: '60px 100%', position: 'bottom', repeat: 'repeat-x' },
33
+ evokes: 'Horror drips on edges',
34
+ },
35
+ 'pixel-corners': {
36
+ key: 'pixel-corners',
37
+ svg: PIXEL_CORNERS_SVG,
38
+ suitsSlots: ['borderImage'],
39
+ borderImage: { borderSlice: 4, repeat: 'stretch' },
40
+ evokes: '8-bit pixel corner brackets',
41
+ },
42
+ scanline: {
43
+ key: 'scanline',
44
+ svg: SCANLINE_SVG,
45
+ suitsSlots: ['backdrop'],
46
+ // Explicit tile size: a viewBox-only SVG has no intrinsic size, so
47
+ // `auto` scales ONE copy to the element instead of tiling. See the
48
+ // note on `circuit-trace`.
49
+ background: { size: '4px 4px', position: 'center', repeat: 'repeat' },
50
+ evokes: 'CRT scanlines tiled across the surface',
51
+ },
52
+ 'vine-corner': {
53
+ key: 'vine-corner',
54
+ svg: VINE_CORNER_SVG,
55
+ suitsSlots: ['borderImage', 'decorationBefore'],
56
+ borderImage: { borderSlice: 20, repeat: 'stretch' },
57
+ background: { size: 'auto', position: 'bottom-left', repeat: 'no-repeat' },
58
+ evokes: 'Tropical vines / leaves in corners',
59
+ },
60
+ 'glow-aura': {
61
+ key: 'glow-aura',
62
+ svg: GLOW_AURA_SVG,
63
+ suitsSlots: ['backdrop', 'decorationBefore', 'decorationAfter'],
64
+ background: { size: 'cover', position: 'center', repeat: 'no-repeat' },
65
+ evokes: 'Soft radial glow halo behind the element',
66
+ },
67
+ 'bracket-frame': {
68
+ key: 'bracket-frame',
69
+ svg: BRACKET_FRAME_SVG,
70
+ suitsSlots: ['borderImage'],
71
+ borderImage: { borderSlice: 8, repeat: 'stretch' },
72
+ evokes: 'Sci-fi targeting brackets on each corner',
73
+ },
74
+ 'ribbon-edge': {
75
+ key: 'ribbon-edge',
76
+ svg: RIBBON_EDGE_SVG,
77
+ suitsSlots: ['decorationAfter', 'decorationBefore'],
78
+ background: { size: '30px 100%', position: 'bottom', repeat: 'repeat-x' },
79
+ evokes: 'Bunting / banner saw-tooth edge',
80
+ },
81
+ 'circuit-trace': {
82
+ key: 'circuit-trace',
83
+ svg: CIRCUIT_TRACE_SVG,
84
+ suitsSlots: ['backdrop'],
85
+ // The tile size is EXPLICIT, and has to be. An inline SVG carrying only
86
+ // a `viewBox` has no intrinsic size, so `background-size: auto` does not
87
+ // mean "32x32" — it scales ONE copy to fill the element. This pattern
88
+ // shipped that way and drew a handful of giant crosshairs across the
89
+ // controls overlay instead of a fine circuit texture.
90
+ //
91
+ // Any tiling decoration added here needs the same treatment: state the
92
+ // tile size, or give the SVG width/height attributes.
93
+ background: { size: '32px 32px', position: 'center', repeat: 'repeat' },
94
+ evokes: 'PCB / circuit board trace pattern',
95
+ },
96
+ 'candy-gloss': {
97
+ key: 'candy-gloss',
98
+ svg: CANDY_GLOSS_SVG,
99
+ suitsSlots: ['backdrop', 'decorationBefore'],
100
+ // A FIXED-HEIGHT band, not `cover` and not `100% 100%`.
101
+ //
102
+ // `cover` was the shipped value and was simply wrong: this SVG carries
103
+ // preserveAspectRatio="none", i.e. it is built to be stretched, while
104
+ // `cover` honours the viewBox's 100:40 intrinsic ratio. On a tall element
105
+ // (a CONTROLS panel is ~220px high and ~180 wide) that scaled the image to
106
+ // 550x220 and centred it, so what you saw was the middle of a hugely
107
+ // magnified ellipse — a grey blob across the top third of the panel.
108
+ //
109
+ // `100% 100%` fixes the blob and is still wrong, because it makes the
110
+ // highlight a PROPORTION of the element. On that same panel the gloss then
111
+ // covers the top ~100px and the first rows of key text sit inside it:
112
+ // measured 2.22:1 and 2.92:1 against the muted label colour. A highlight
113
+ // is cast by a light source, so its size is set by the light and not by
114
+ // the object — a fixed band is both the physical answer and the one that
115
+ // cannot swallow a tall element. 56px reads as a full candy gloss on a
116
+ // 36-46px chip and as a sheen along the top edge of a panel.
117
+ background: { size: '100% 56px', position: 'top', repeat: 'no-repeat' },
118
+ evokes: 'Glossy sugar highlight across the top — candy button shine',
119
+ },
120
+ 'ornate-corner': {
121
+ key: 'ornate-corner',
122
+ svg: ORNATE_CORNER_SVG,
123
+ suitsSlots: ['borderImage'],
124
+ borderImage: { borderSlice: 14, repeat: 'stretch' },
125
+ evokes: 'Gothic filigree corner frame — dark fantasy, dungeon',
126
+ },
127
+ 'royal-frame': {
128
+ key: 'royal-frame',
129
+ svg: ROYAL_FRAME_SVG,
130
+ suitsSlots: ['borderImage'],
131
+ borderImage: { borderSlice: 12, repeat: 'stretch' },
132
+ evokes: 'Double gold-trim picture frame with corner studs — epic fantasy',
133
+ },
134
+ 'hazard-stripes': {
135
+ key: 'hazard-stripes',
136
+ svg: HAZARD_STRIPES_SVG,
137
+ suitsSlots: ['backdrop'],
138
+ // Explicit tile size: a viewBox-only SVG has no intrinsic size, so
139
+ // `auto` scales ONE copy to the element instead of tiling. See the
140
+ // note on `circuit-trace`.
141
+ background: { size: '16px 16px', position: 'center', repeat: 'repeat' },
142
+ evokes: 'Faint diagonal caution stripes — military / sci-fi warning panel',
143
+ },
144
+ 'pixel-frame': {
145
+ key: 'pixel-frame',
146
+ svg: PIXEL_FRAME_SVG,
147
+ suitsSlots: ['borderImage'],
148
+ borderImage: { borderSlice: 5, repeat: 'round' },
149
+ evokes: 'NES dialog double-ring pixel border',
150
+ },
151
+ 'grain-texture': {
152
+ key: 'grain-texture',
153
+ svg: GRAIN_TEXTURE_SVG,
154
+ suitsSlots: ['backdrop'],
155
+ // Explicit tile size: a viewBox-only SVG has no intrinsic size, so
156
+ // `auto` scales ONE copy to the element instead of tiling. See the
157
+ // note on `circuit-trace`.
158
+ background: { size: '40px 40px', position: 'center', repeat: 'repeat' },
159
+ evokes: 'Subtle parchment / leather grain tooth',
160
+ },
161
+ };
162
+ // Substitutes `__primary__` / `__danger__` etc. with hex codes from the active theme.
163
+ // Unknown tokens are left as-is so non-token usages of double-underscored strings
164
+ // (none in our SVGs today, but defensive) don't get mangled.
165
+ export function substituteThemeColors(svg, colors) {
166
+ return svg.replace(/__([a-zA-Z][a-zA-Z0-9]*)__/g, (match, name) => {
167
+ const key = name;
168
+ // `?? match` also covers optional colors (outline): a token for a color the
169
+ // theme doesn't define stays as-is instead of substituting "undefined".
170
+ const value = Object.prototype.hasOwnProperty.call(colors, key) ? colors[key] : undefined;
171
+ return value ?? match;
172
+ });
173
+ }
174
+ // Wraps SVG markup as a CSS url(...) data URI. The encoder uses encodeURIComponent
175
+ // (not base64) because SVG-as-data-URI is smaller that way and renders identically.
176
+ export function svgToDataUri(svg) {
177
+ const cleaned = svg.replace(/[\r\n]+/g, '').replace(/\s{2,}/g, ' ');
178
+ return `url("data:image/svg+xml;utf8,${encodeURIComponent(cleaned)}")`;
179
+ }
@@ -0,0 +1,147 @@
1
+ export const HUD_FONTS = {
2
+ 'red-hat-display': {
3
+ key: 'red-hat-display',
4
+ family: 'Red Hat Display',
5
+ weights: [300, 400, 500, 700],
6
+ suitsCase: ['upper', 'normal'],
7
+ evokes: 'Default Bitmagic — warm, technical, modern sans',
8
+ },
9
+ 'chakra-petch': {
10
+ key: 'chakra-petch',
11
+ family: 'Chakra Petch',
12
+ weights: [300, 400, 500, 600, 700],
13
+ suitsCase: ['upper', 'normal'],
14
+ evokes: 'Chamfered techno sans — holographic HUD, sci-fi console, cut corners',
15
+ },
16
+ 'ibm-plex-sans': {
17
+ key: 'ibm-plex-sans',
18
+ family: 'IBM Plex Sans',
19
+ weights: [300, 400, 500, 600, 700],
20
+ suitsCase: ['upper', 'normal'],
21
+ evokes: 'Engineered humanist sans — instrument panel, operator console, tabular data',
22
+ },
23
+ 'archivo-narrow': {
24
+ key: 'archivo-narrow',
25
+ family: 'Archivo Narrow',
26
+ weights: [400, 500, 600, 700],
27
+ suitsCase: ['upper', 'normal'],
28
+ evokes: 'Narrow gothic that keeps its ink — motorsport telemetry, broadcast overlay',
29
+ },
30
+ 'bebas-neue': {
31
+ key: 'bebas-neue',
32
+ family: 'Bebas Neue',
33
+ weights: [400],
34
+ suitsCase: ['upper', 'normal'],
35
+ evokes: 'Tall, bold display — sports, action, news ticker',
36
+ },
37
+ 'press-start-2p': {
38
+ key: 'press-start-2p',
39
+ family: 'Press Start 2P',
40
+ weights: [400],
41
+ suitsCase: ['upper', 'normal'],
42
+ evokes: 'Retro arcade pixel — 8-bit, NES, chiptune',
43
+ },
44
+ creepster: {
45
+ key: 'creepster',
46
+ family: 'Creepster',
47
+ weights: [400],
48
+ suitsCase: ['normal'],
49
+ evokes: 'Horror, halloween, dripping — already display-styled',
50
+ },
51
+ pacifico: {
52
+ key: 'pacifico',
53
+ family: 'Pacifico',
54
+ weights: [400],
55
+ suitsCase: ['normal'],
56
+ evokes: 'Handwritten cursive — tropical, beachy, casual',
57
+ },
58
+ 'space-grotesk': {
59
+ key: 'space-grotesk',
60
+ family: 'Space Grotesk',
61
+ weights: [400, 500, 700],
62
+ suitsCase: ['upper', 'normal'],
63
+ evokes: 'Geometric sans — sci-fi, technical, modern',
64
+ },
65
+ 'rubik-mono-one': {
66
+ key: 'rubik-mono-one',
67
+ family: 'Rubik Mono One',
68
+ weights: [400],
69
+ suitsCase: ['upper', 'normal'],
70
+ evokes: 'Heavy, slabby display — bold, blocky, attention-grabbing',
71
+ },
72
+ vt323: {
73
+ key: 'vt323',
74
+ family: 'VT323',
75
+ weights: [400],
76
+ suitsCase: ['upper', 'normal'],
77
+ evokes: 'CRT terminal monospace — retro computer, hacker, glitch',
78
+ },
79
+ orbitron: {
80
+ key: 'orbitron',
81
+ family: 'Orbitron',
82
+ weights: [400, 500, 700, 900],
83
+ suitsCase: ['upper', 'normal'],
84
+ evokes: 'Futuristic geometric — sci-fi, cyber, racing HUD',
85
+ },
86
+ shrikhand: {
87
+ key: 'shrikhand',
88
+ family: 'Shrikhand',
89
+ weights: [400],
90
+ suitsCase: ['normal'],
91
+ evokes: 'Italic display — celebratory, party, energetic',
92
+ },
93
+ 'baloo-2': {
94
+ key: 'baloo-2',
95
+ family: 'Baloo 2',
96
+ weights: [400, 500, 600, 700, 800],
97
+ suitsCase: ['upper', 'normal'],
98
+ evokes: 'Rounded chunky sans with a real weight range — mobile action-RPG chrome',
99
+ },
100
+ 'lilita-one': {
101
+ key: 'lilita-one',
102
+ family: 'Lilita One',
103
+ weights: [400],
104
+ suitsCase: ['upper', 'normal'],
105
+ evokes: 'Chunky rounded comic display — cartoon battler, toon town',
106
+ },
107
+ fredoka: {
108
+ key: 'fredoka',
109
+ family: 'Fredoka',
110
+ weights: [300, 400, 500, 700],
111
+ suitsCase: ['upper', 'normal'],
112
+ evokes: 'Soft rounded bubble sans — candy, casual mobile, sweet',
113
+ },
114
+ 'grenze-gotisch': {
115
+ key: 'grenze-gotisch',
116
+ family: 'Grenze Gotisch',
117
+ weights: [400, 500, 700, 900],
118
+ suitsCase: ['normal'],
119
+ evokes: 'Readable blackletter — dark fantasy, gothic dungeon',
120
+ },
121
+ cinzel: {
122
+ key: 'cinzel',
123
+ family: 'Cinzel',
124
+ weights: [400, 500, 700, 900],
125
+ suitsCase: ['upper', 'normal'],
126
+ evokes: 'Carved Roman caps — epic fantasy, ancient temple',
127
+ },
128
+ };
129
+ export const DEFAULT_FONT_STACK_OPTIONS = {
130
+ fallback: '"Helvetica Neue", Helvetica, Arial, system-ui, sans-serif',
131
+ };
132
+ export function getFontCssStack(key, opts = DEFAULT_FONT_STACK_OPTIONS) {
133
+ const entry = HUD_FONTS[key];
134
+ return `"${entry.family}", ${opts.fallback}`;
135
+ }
136
+ // Builds the Google Fonts CSS URL for a single font with the weights the theme needs.
137
+ // Published games only ever load the active theme's font, so this URL is the sole
138
+ // font request on the page.
139
+ export function buildGoogleFontsUrl(key, weights) {
140
+ const entry = HUD_FONTS[key];
141
+ const allowed = new Set(entry.weights);
142
+ const filtered = weights.filter(w => allowed.has(w));
143
+ const useWeights = filtered.length > 0 ? filtered : entry.weights;
144
+ const family = entry.family.replace(/ /g, '+');
145
+ const wtParam = useWeights.length > 0 ? `:wght@${[...useWeights].sort((a, b) => a - b).join(';')}` : '';
146
+ return `https://fonts.googleapis.com/css2?family=${family}${wtParam}&display=swap`;
147
+ }
@@ -0,0 +1,80 @@
1
+ import { BITMAGIC_THEME_DATA } from './themes/bitmagic.js';
2
+ import { RIFT_RAIDER_THEME_DATA } from './themes/rift-raider.js';
3
+ import { VILLAGE_KEEP_THEME_DATA } from './themes/village-keep.js';
4
+ import { NEXUS_THEME_DATA } from './themes/nexus.js';
5
+ import { SIMULATOR_THEME_DATA } from './themes/simulator.js';
6
+ import { RACING_THEME_DATA } from './themes/racing.js';
7
+ import { validateAndResolveTheme } from './validateTheme.js';
8
+ // Built-in theme presets — THE FIVE SELECTABLE STYLES. Each preset's raw data
9
+ // lives in its own TS module (themes/*.ts) and goes through
10
+ // validateAndResolveTheme — so the same path that user themes go through
11
+ // catches preset bugs at engine boot.
12
+ //
13
+ // `bitmagic` is deliberately NOT in this set. It is the engine's DEFAULT/
14
+ // FALLBACK look (what a game with no `hud.theme` — or an invalid one — renders
15
+ // with), exported as DEFAULT_HUD_THEME. Listing it as a preset made every "cycle
16
+ // all styles" feature show six looks and every agent catalogue advertise a
17
+ // style that is really the absence of one.
18
+ //
19
+ // Built-in presets are TS modules, not .json files, because the engine runtime
20
+ // loads modules from /dist/ where tsc doesn't copy non-TS files. User-authored
21
+ // themes ship as theme.json on disk (next to world.json) and are fetched +
22
+ // validated at game load time.
23
+ /** The five selectable presets, in catalogue order. `PresetName` is derived from this list. */
24
+ export const PRESET_NAMES = ['rift-raider', 'village-keep', 'nexus', 'simulator', 'racing'];
25
+ /**
26
+ * Each preset's RAW data — the inline-theme shape, plus its `evokes` label — before
27
+ * validation. Tools that patch a preset start from this ("preset X but with orange
28
+ * accents"): it can be spread, edited and written back as an inline theme.
29
+ */
30
+ export const PRESET_THEME_DATA = {
31
+ 'rift-raider': RIFT_RAIDER_THEME_DATA,
32
+ 'village-keep': VILLAGE_KEEP_THEME_DATA,
33
+ nexus: NEXUS_THEME_DATA,
34
+ simulator: SIMULATOR_THEME_DATA,
35
+ racing: RACING_THEME_DATA,
36
+ };
37
+ /** The default look's raw data (`bitmagic`) — the patch base when a game sets no theme. */
38
+ export const DEFAULT_THEME_DATA = BITMAGIC_THEME_DATA;
39
+ /**
40
+ * The one-line mood label each preset carries beside its tokens.
41
+ *
42
+ * Exported separately because `evokes` is deliberately NOT part of `ThemeTokens`
43
+ * — it is authoring metadata for whoever (or whatever) is choosing a preset, not
44
+ * a rendering token, and `validateAndResolveTheme` drops it on the way through.
45
+ * The agent's catalogue reads it from here, so a style and its description are
46
+ * one edit.
47
+ */
48
+ export const PRESET_EVOKES = {
49
+ 'rift-raider': PRESET_THEME_DATA['rift-raider'].evokes,
50
+ 'village-keep': PRESET_THEME_DATA['village-keep'].evokes,
51
+ nexus: PRESET_THEME_DATA.nexus.evokes,
52
+ simulator: PRESET_THEME_DATA.simulator.evokes,
53
+ racing: PRESET_THEME_DATA.racing.evokes,
54
+ };
55
+ function resolvePreset(raw, name) {
56
+ const result = validateAndResolveTheme(raw);
57
+ if (!result.ok) {
58
+ throw new Error(`Built-in preset "${name}" failed validation: ` +
59
+ result.errors.map(e => `${e.path}: ${e.message}`).join('; '));
60
+ }
61
+ return result.theme;
62
+ }
63
+ /**
64
+ * The engine's default look — applied when a game sets no `hud.theme`, and the
65
+ * fallback when a theme is unknown or fails validation. Not a selectable preset.
66
+ */
67
+ export const BITMAGIC_THEME = resolvePreset(BITMAGIC_THEME_DATA, 'bitmagic');
68
+ export const DEFAULT_HUD_THEME = BITMAGIC_THEME;
69
+ export const RIFT_RAIDER_THEME = resolvePreset(RIFT_RAIDER_THEME_DATA, 'rift-raider');
70
+ export const VILLAGE_KEEP_THEME = resolvePreset(VILLAGE_KEEP_THEME_DATA, 'village-keep');
71
+ export const NEXUS_THEME = resolvePreset(NEXUS_THEME_DATA, 'nexus');
72
+ export const SIMULATOR_THEME = resolvePreset(SIMULATOR_THEME_DATA, 'simulator');
73
+ export const RACING_THEME = resolvePreset(RACING_THEME_DATA, 'racing');
74
+ export const HUD_PRESETS = {
75
+ 'rift-raider': RIFT_RAIDER_THEME,
76
+ 'village-keep': VILLAGE_KEEP_THEME,
77
+ nexus: NEXUS_THEME,
78
+ simulator: SIMULATOR_THEME,
79
+ racing: RACING_THEME,
80
+ };
@@ -0,0 +1,66 @@
1
+ import { DEFAULT_HUD_THEME, HUD_PRESETS } from './presets.js';
2
+ import { suggestClosest, validateAndResolveTheme } from './validateTheme.js';
3
+ // Resolve a `hud.theme` value from world.json into a ThemeTokens.
4
+ //
5
+ // - undefined / null / unknown shape → DEFAULT_HUD_THEME (the engine's default look)
6
+ // - string matching a preset name → the preset
7
+ // - string not matching any preset → DEFAULT_HUD_THEME + console warning
8
+ // (this includes legacy names such as
9
+ // 'bitmagic' and 'horror' — the first
10
+ // IS the default, so it is visually a
11
+ // no-op; the second no longer ships)
12
+ // - object → validateAndResolveTheme(obj). On success,
13
+ // the resolved theme. On failure, DEFAULT_HUD_THEME
14
+ // + console warning that includes field paths.
15
+ //
16
+ // This is the single integration point: don't bypass it. The engine calls it
17
+ // once per loadGame() after world data is parsed. Failures never block the game
18
+ // from loading — a broken theme is recoverable; failing the load is not.
19
+ /**
20
+ * The marker on every theme-resolution console warning, and the two phrases that say which
21
+ * failure it was. `bitmagic verify` parses them out of the console (it imports these constants
22
+ * through `@bitmagic/hud-theme`), and it reads warnings from engines of every vendored version, so
23
+ * the wording is frozen: never reword these, only append detail after them.
24
+ */
25
+ export const HUD_THEME_LOG_MARKER = '[HUD]';
26
+ /** The phrase after the marker when `hud.theme` is a string that names no preset. */
27
+ export const HUD_UNKNOWN_PRESET_PHRASE = 'Unknown theme preset';
28
+ /** The phrase after the marker when `hud.theme` is an object the validator rejected. */
29
+ export const HUD_INVALID_THEME_PHRASE = 'Custom theme failed validation';
30
+ function isPresetName(value) {
31
+ return value in HUD_PRESETS;
32
+ }
33
+ export function resolveWorldTheme(value) {
34
+ if (value === undefined || value === null)
35
+ return DEFAULT_HUD_THEME;
36
+ if (typeof value === 'string') {
37
+ if (isPresetName(value))
38
+ return HUD_PRESETS[value];
39
+ // The closest-match hint catches the two real spelling traps: the
40
+ // unhyphenated internal style keys (`riftraider`) and plain typos.
41
+ const close = suggestClosest(value, Object.keys(HUD_PRESETS));
42
+ console.warn(`${HUD_THEME_LOG_MARKER} ${HUD_UNKNOWN_PRESET_PHRASE} "${value}". Known presets: ${Object.keys(HUD_PRESETS).join(', ')}.`
43
+ + `${close ? ` Closest match: "${close}".` : ''} Falling back to the default theme.`);
44
+ return DEFAULT_HUD_THEME;
45
+ }
46
+ const result = validateAndResolveTheme(value);
47
+ // Suggestions ride in parentheses INSIDE each issue. The validator's
48
+ // `suggestion` field carries the actionable half of every message — the
49
+ // closest-match name, the font-case remedy — and this warn used to format
50
+ // only `path: message`, so the hint was computed and then thrown away at
51
+ // the last hop. This console line is the ONLY channel the CLI lane has
52
+ // (`bitmagic verify` parses it; there is no write-tool there to return
53
+ // suggestions), so dropping it here dropped it everywhere.
54
+ // The marker and the two leading phrases are `bitmagic verify`'s parse
55
+ // contract (see the constants above) — the suggestion text extends the
56
+ // detail and must never reword them.
57
+ const formatIssue = (issue) => `${issue.path}: ${issue.message}${issue.suggestion ? ` (${issue.suggestion})` : ''}`;
58
+ if (!result.ok) {
59
+ console.warn(`${HUD_THEME_LOG_MARKER} ${HUD_INVALID_THEME_PHRASE}; falling back to the default theme. Errors:`, result.errors.map(formatIssue).join('; '));
60
+ return DEFAULT_HUD_THEME;
61
+ }
62
+ if (result.warnings.length > 0) {
63
+ console.warn(`${HUD_THEME_LOG_MARKER} Custom theme has warnings:`, result.warnings.map(formatIssue).join('; '));
64
+ }
65
+ return result.theme;
66
+ }
@@ -0,0 +1,29 @@
1
+ // Bitmagic theme — the default. Mirrors creator/DESIGN.md (Aquamarine primary,
2
+ // Shocking Pink danger, etc.). Goes through validateAndResolveTheme in presets.ts
3
+ // so any drift between this object and the schema fails at engine boot.
4
+ export const BITMAGIC_THEME_DATA = {
5
+ name: 'Bitmagic',
6
+ font: {
7
+ key: 'red-hat-display',
8
+ case: 'upper',
9
+ weightBody: 400,
10
+ weightHeading: 700,
11
+ trackingLabel: 1.6,
12
+ },
13
+ colors: {
14
+ primary: '#A0DAB9',
15
+ danger: '#E0218A',
16
+ warning: '#FF8200',
17
+ success: '#F9E547',
18
+ background: '#0b0b0b',
19
+ surface: '#181818',
20
+ text: '#ffffff',
21
+ textMuted: '#b3b3b3',
22
+ },
23
+ shape: {
24
+ radiusPill: 500,
25
+ radiusCard: 8,
26
+ glow: 'aqua',
27
+ imageRendering: 'auto',
28
+ },
29
+ };
@@ -0,0 +1,69 @@
1
+ // Nexus theme — holographic sci-fi conventions (the "Nexus" kit style):
2
+ // hairline cyan on near-black, chamfer-small radii, wide-tracked tech caps,
3
+ // corner brackets and a circuitry backdrop. Depth is carried by glow alone —
4
+ // hairline PANELS; buttons carry a bevel and a 2px rim. Threat states run orange.
5
+ export const NEXUS_THEME_DATA = {
6
+ name: 'Nexus',
7
+ // One-line mood label, read by the agent catalogue so an AI picks the right
8
+ // preset without trying each one. It lives HERE, beside the tokens it
9
+ // describes, so that changing the style and changing its description are the
10
+ // same edit — it drifted badly when it lived only in the agent mirror. The
11
+ // engine validator ignores unknown top-level keys, so this never reaches
12
+ // ThemeTokens; `PRESET_EVOKES` in presets.ts is what exposes it.
13
+ evokes: 'Holographic sci-fi — hairline cyan on near-black, corner brackets, lit teal plate buttons with a graded cyan bezel, depth by glow (Mass Effect-style)',
14
+ font: {
15
+ key: 'chakra-petch',
16
+ case: 'upper',
17
+ weightBody: 500,
18
+ weightHeading: 700,
19
+ // 1.4, not 2.5: orbitron is the widest face in the catalogue and at
20
+ // 2.5px per glyph a label like "RIFT PROGRESS" nearly fills the corner
21
+ // anchor budget on a phone, breaking mid-word. Wide tracking also costs
22
+ // word cohesion at HUD sizes — the opposite defect from the lab's Nexus.
23
+ trackingLabel: 1.4,
24
+ outlineWidth: 0,
25
+ },
26
+ colors: {
27
+ primary: '#4FD8E8',
28
+ danger: '#FF6B4A',
29
+ warning: '#F0A22E',
30
+ success: '#6FD9A4',
31
+ background: '#04070C',
32
+ surface: '#0A121A',
33
+ text: '#E6F6FA',
34
+ // Lifted from '#7EA3B2'. Checked against the panel's LIT header band
35
+ // rather than its nominal face — a secondary label that clears on the
36
+ // flat colour can still miss where the face gradient is brightest.
37
+ textMuted: '#9DBBC8',
38
+ outline: '#2E9FB5',
39
+ },
40
+ shape: {
41
+ // Buttons are OBJECTS even when the panels are flat: `bevel` paints the
42
+ // white-top/dark-bottom gradient over buttons, counters and bars, and a
43
+ // 2px border in colors.outline follows the radius. The plates keep their
44
+ // hairline language — only the controls gain relief.
45
+ radiusPill: 6,
46
+ radiusCard: 4,
47
+ glow: 'cyan',
48
+ imageRendering: 'auto',
49
+ borderWidth: 2,
50
+ bevel: 0.5,
51
+ },
52
+ // NO `controls` backdrop. `circuit-trace` used to sit here and rendered as
53
+ // GIANT CROSSHAIRS across the controls overlay rather than the fine PCB
54
+ // texture it is meant to be — see the sizing note on the decoration itself.
55
+ // The pattern is fixed now, but this style does not need it: the corner
56
+ // brackets already carry the scanner language, and a texture behind the
57
+ // control hints competed with the type sitting on top of it.
58
+ decorations: {
59
+ progressBar: { borderImage: 'bracket-frame' },
60
+ counter: { borderImage: 'bracket-frame' },
61
+ // `backdrop`, not `decorationBefore`. The before/after slots are 12px
62
+ // strips hung OUTSIDE the element, so what this asked for was a glow
63
+ // floating above the crosshair rather than around it — and glow-aura's
64
+ // whole intent is "soft radial glow halo BEHIND the element". It never
65
+ // showed either way, because the slot itself could not render (see
66
+ // buildSlotCss), so the mistake was invisible until the slot was fixed.
67
+ reticle: { backdrop: 'glow-aura' },
68
+ },
69
+ };