Files
feuerwerk/src/ui/Theme.ts
T
root b87fcdfbad Mark overrides in gold instead of bold, and up the hierarchy
An inspector label whose value deviates from its base was drawn bold, which is
easy to miss next to the regular labels and says nothing in the hierarchy, where
a folded subtree could hide a change entirely.

The colour now lives as a theme token (`override`, #f2c84b) used by both the
inspector labels and the hierarchy rows. Whether a row deviates is decided by
`src/core/properties/deviation.ts`: `objectDeviates` compares an object against
what it follows -- the container template for content, the asset otherwise --
and `deviationMarks` carries the mark up to every ancestor, so a folded subtree
still shows that something inside was changed.

`deviationMarks` takes the stored holder, not the resolved one: in the resolved
view container content looks like an ordinary object and `instanceTargetOf`
would compare against the asset, marking every instance.

Covered by 3 unit tests and 5 e2e checks, including one that strips an object's
own overrides to prove the row stays gold for a deviating child and restores
them byte for byte. Full suite remote on the RTX 4070: 275 checks, all passed.
2026-09-09 10:54:25 +02:00

126 lines
3.6 KiB
TypeScript

/** Design tokens for the canvas UI. Sizes only one widget uses may stay local to it. */
import { TextStyle } from 'pixi.js';
export const Theme = {
font: 'Inter, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
monoFont: '"JetBrains Mono", Consolas, "Courier New", monospace',
fontSize: 13,
smallFontSize: 11,
viewportLabelSize: 10,
rowHeight: 26,
controlHeight: 24,
padding: 8,
gap: 6,
radius: 4,
fieldPadding: 6,
checkboxSize: 16,
swatchSize: 12,
scrollbarWidth: 6,
/** Horizontal indent per nesting level in tree lists. */
treeIndent: 14,
colors: {
background: 0x1b1d22,
panel: 0x24272e,
panelHeader: 0x2c3038,
border: 0x383c46,
control: 0x16181d,
controlHover: 0x1e2127,
controlActive: 0x2a2e37,
accent: 0x4c8dff,
accentText: 0xffffff,
text: 0xe6e8ee,
textMuted: 0x8a90a0,
selection: 0x2b4a80,
danger: 0xe0553f,
viewport: 0x121316,
gizmo: 0x4c8dff,
/** Behavior symbols and owner links. */
behavior: 0xb066ff,
/** Collider outlines: solid (blocking) and trigger (events only). */
collider: 0x4fd18b,
trigger: 0xf2c84b,
container: 0x3fb8c9,
marquee: 0x4c8dff,
/** A value deviating from what it follows: an override on a placed object, a non-default on an asset. */
override: 0xf2c84b,
/** Node graph: canvas, node chrome, per-category title colors, port colors, live highlight. */
graphBackground: 0x15171b,
node: 0x262a32,
nodeBorder: 0x3b404b,
nodeSelected: 0x4c8dff,
categoryEvent: 0xc0392b,
categoryFlow: 0x7f8c8d,
categoryData: 0x2e8b57,
categoryVariable: 0xb8860b,
categoryAction: 0x2f6fd6,
portExec: 0xffffff,
portNumber: 0x6fcf97,
portBoolean: 0xe57373,
portString: 0xe6a8ff,
portObject: 0x56ccf2,
portAny: 0xbdbdbd,
hot: 0xffc857,
},
} as const;
const styleCache = new Map<string, TextStyle>();
export type TextStyleToken =
| 'body'
| 'bodyBold'
| 'muted'
| 'override'
| 'small'
| 'title'
| 'mono'
| 'accent'
| 'viewportLabel'
| 'viewportLabelMuted';
/** Shared TextStyle instances so Pixi can cache glyph measurements per token. */
export function textStyle(token: TextStyleToken = 'body'): TextStyle {
let style = styleCache.get(token);
if (style) return style;
const base = { fontFamily: Theme.font, fontSize: Theme.fontSize, fill: Theme.colors.text };
switch (token) {
case 'body':
style = new TextStyle(base);
break;
case 'bodyBold':
style = new TextStyle({ ...base, fontWeight: '600' });
break;
case 'muted':
style = new TextStyle({ ...base, fill: Theme.colors.textMuted });
break;
case 'override':
style = new TextStyle({ ...base, fill: Theme.colors.override });
break;
case 'small':
style = new TextStyle({ ...base, fontSize: Theme.smallFontSize, fill: Theme.colors.textMuted });
break;
case 'title':
style = new TextStyle({
...base,
fontSize: Theme.smallFontSize,
fontWeight: '700',
letterSpacing: 0.5,
fill: Theme.colors.textMuted,
});
break;
case 'mono':
style = new TextStyle({ ...base, fontFamily: Theme.monoFont });
break;
case 'accent':
style = new TextStyle({ ...base, fill: Theme.colors.accentText, fontWeight: '600' });
break;
case 'viewportLabel':
style = new TextStyle({ ...base, fontSize: Theme.viewportLabelSize });
break;
case 'viewportLabelMuted':
style = new TextStyle({ ...base, fontSize: Theme.viewportLabelSize, fill: Theme.colors.textMuted });
break;
}
styleCache.set(token, style);
return style;
}