game-maker / suite (push) Successful in 6m13s
The graph could hold any number of nodes and said nothing about which of them belonged together. Groups and comments are two separate things, as decided in the issue: a group is a titled box that carries the nodes it encloses, a comment is free text that carries nothing. Both live as optional arrays on Graph, not as registry nodes. A comment that were a real node would run through the interpreter, show up in the drop palettes and be compiled into a shader — the wrong place for a piece of text. As arrays on Graph they exist in all four graph kinds at once, and a document without them means exactly "no groups", so no format bump. Membership is computed when the box is grabbed, never stored: the center of a node inside the box travels with it, and a node dragged out is simply out. A box inside a box travels with the outer one, its own nodes included. A group is grabbed by its title bar only — its body stays transparent to the pointer, or a rubber band could never be drawn across the nodes it holds. Deleting a box deletes the box; the nodes stay where they are, because a box that silently took ten nodes with it would be the most expensive surprise in the feature. The color is a free picker, and the title color follows from it: relative luminance per WCAG, threshold at 0.179 where the two contrast ratios meet — not 0.5, which would pick white on mid grey where black reads better. Resizing snaps the far edge to the grid, not the width, so the corner that is not being dragged stays put. The minimum size rises in whole steps for that reason: 80 is no multiple of 24, so a box squeezed to the stop would have sat off-grid in exactly the mode meant to keep it on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
159 lines
4.8 KiB
TypeScript
159 lines
4.8 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,
|
|
hoverCursor: 'default',
|
|
colors: {
|
|
background: 0x1b1d22,
|
|
panel: 0x24272e,
|
|
panelHeader: 0x2c3038,
|
|
border: 0x383c46,
|
|
control: 0x16181d,
|
|
controlHover: 0x1e2127,
|
|
controlActive: 0x16181d,
|
|
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,
|
|
/** Particle source icon and owner links; the same orange the graph gives the category. */
|
|
particles: 0xc26a1f,
|
|
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,
|
|
categoryState: 0x8e5ad6,
|
|
categoryShader: 0x1f8f8f,
|
|
categoryParticles: 0xc26a1f,
|
|
portExec: 0xffffff,
|
|
portNumber: 0x6fcf97,
|
|
portBoolean: 0xe57373,
|
|
portString: 0xe6a8ff,
|
|
portObject: 0x56ccf2,
|
|
portList: 0xf2a75c,
|
|
portStruct: 0xd4c25a,
|
|
portVec3: 0xff9ad5,
|
|
portAny: 0xbdbdbd,
|
|
/** Ring around the port a running edge drag would connect to. */
|
|
portTarget: 0xffffff,
|
|
/** Groups and comments that were never given a colour of their own. */
|
|
annotation: 0x3c4352,
|
|
hot: 0xffc857,
|
|
},
|
|
} as const;
|
|
|
|
const styleCache = new Map<string, TextStyle>();
|
|
|
|
export type TextStyleToken =
|
|
| 'body'
|
|
| 'bodyBold'
|
|
| 'muted'
|
|
| 'override'
|
|
| 'small'
|
|
| 'warning'
|
|
| 'live'
|
|
| 'title'
|
|
| 'mono'
|
|
| 'monoMuted'
|
|
| 'monoWarn'
|
|
| 'monoError'
|
|
| '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 'warning':
|
|
style = new TextStyle({ ...base, fontSize: Theme.smallFontSize, fill: Theme.colors.danger });
|
|
break;
|
|
case 'live':
|
|
style = new TextStyle({ ...base, fontSize: Theme.smallFontSize, fill: Theme.colors.hot });
|
|
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 'monoMuted':
|
|
style = new TextStyle({ ...base, fontFamily: Theme.monoFont, fill: Theme.colors.textMuted });
|
|
break;
|
|
case 'monoWarn':
|
|
style = new TextStyle({ ...base, fontFamily: Theme.monoFont, fill: Theme.colors.override });
|
|
break;
|
|
case 'monoError':
|
|
style = new TextStyle({ ...base, fontFamily: Theme.monoFont, fill: Theme.colors.danger });
|
|
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;
|
|
}
|