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.
126 lines
3.6 KiB
TypeScript
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;
|
|
}
|