/** Design tokens for the canvas UI. Sizes only one widget uses may stay local to it. */ import { TextStyle } from 'pixi.js'; export const Theme = { // Both ship with the build (`fonts.ts`); the rest only covers glyphs outside their Latin subset. font: '"Source Sans 3", "Segoe UI", Roboto, Helvetica, Arial, sans-serif', monoFont: '"Source Code Pro", Consolas, "Courier New", monospace', fontSize: 13, smallFontSize: 11, viewportLabelSize: 10, captionSize: 16, rowHeight: 24, controlHeight: 24, padding: 8, headerPadding: 4, gap: 4, radius: 2, fieldPadding: 4, checkboxSize: 16, scrollbarWidth: 4, treeIndent: 12, hoverCursor: 'default', colors: { background: 0x1b1d22, panel: 0x24272e, panelHeader: 0x2c3038, border: 0x383c46, control: 0x16181d, controlHover: 0x1e2127, controlActive: 0x16181d, accent: 0x4c8dff, accentText: 0xffffff, /** Tab row: the bar the index cards sit on, the card showing, and one under the pointer. */ tabBar: 0x1f2229, tabActive: 0x3f444e, tabHover: 0x2c3038, text: 0xe6e8ee, textMuted: 0x8a90a0, selection: 0x2b4a80, danger: 0xe0553f, viewport: 0x121316, gizmo: 0x4c8dff, /** Every symbol in the scene view: white, held off any ground by a dark outline. */ symbol: 0xffffff, symbolOutline: 0x0b0c0f, /** Owner links of behaviors. */ 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, /** The eyedropper's candidate in the scene view (#276): the colour of an object port. */ objectPick: 0x56ccf2, /** 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(); export type TextStyleToken = | 'body' | 'bodyBold' | 'muted' | 'override' | 'link' | 'small' | 'warning' | 'danger' | '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: '500' }); break; case 'muted': style = new TextStyle({ ...base, fill: Theme.colors.textMuted }); break; case 'override': style = new TextStyle({ ...base, fill: Theme.colors.override }); break; case 'link': style = new TextStyle({ ...base, fill: Theme.colors.accent }); 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 'danger': style = new TextStyle({ ...base, 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: '500', 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; }