Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 0 additions & 32 deletions OpenBitFun-Installer/src/theme/installerThemesData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ type ThemeSeed = {
secondary?: string;
};
accent: string;
semantic?: Partial<SemanticColors>;
};

function createBackground(seed: ThemeSeed['background']): BackgroundColors {
Expand Down Expand Up @@ -140,7 +139,6 @@ function createInstallerTheme(seed: ThemeSeed): InstallerTheme {
success: tone.semantic.success,
warning: tone.semantic.warning,
error: tone.semantic.error,
...seed.semantic,
},
border: { ...tone.border },
element: { ...tone.element },
Expand All @@ -157,71 +155,41 @@ export const THEMES: InstallerTheme[] = [
type: 'dark',
background: { primary: MIDNIGHT_CARD_BACKGROUND, secondary: DARK_CARD_SURFACE },
accent: DEFAULT_BLUE,
semantic: {
success: '#6aab73',
warning: '#e0a055',
error: '#cc7f7a',
},
}),
createInstallerTheme({
id: 'openbitfun-china-style',
name: 'Ink Charm',
type: 'light',
background: { primary: '#faf8f0', secondary: '#f5f3e8' },
accent: '#2e5e8a',
semantic: {
success: '#52ad5a',
warning: '#f0a020',
error: '#c8102e',
},
}),
createInstallerTheme({
id: 'openbitfun-china-night',
name: 'Ink Night',
type: 'dark',
background: { primary: '#1a1814', secondary: DARK_CARD_SURFACE },
accent: '#73a5cc',
semantic: {
success: '#6bc072',
warning: '#f5b555',
error: '#e85555',
},
}),
createInstallerTheme({
id: 'openbitfun-cyber',
name: 'Cyber',
type: 'dark',
background: { primary: '#0e0e10', secondary: DARK_CARD_SURFACE },
accent: '#00e6ff',
semantic: {
success: '#00ff9f',
warning: '#ffcc00',
error: '#ff0055',
},
}),
createInstallerTheme({
id: 'openbitfun-tokyo-night',
name: 'Tokyo Night',
type: 'dark',
background: { primary: '#1a1b26', secondary: DARK_CARD_SURFACE },
accent: '#7aa2f7',
semantic: {
success: '#9ece6a',
warning: '#e0af68',
error: '#f7768e',
},
}),
createInstallerTheme({
id: 'openbitfun-slate',
name: 'Slate',
type: 'dark',
background: { primary: DARK_CARD_SURFACE },
accent: '#7ab0ee',
semantic: {
success: '#7eb09b',
warning: '#f59e0b',
error: '#c9878d',
},
}),
];

Expand Down
4 changes: 3 additions & 1 deletion design-system/packages/theme-openbitfun/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,6 @@ The same data is available as `@openbitfun/theme-openbitfun/reference-colors.jso
- `color.surface.tertiary` is an opaque low-emphasis fill for persistent grouped content such as cards and field groups.
- `color.surface.subtle` is a translucent local tint for transient feedback and small inset details. It must not define a persistent application plane.
- `color.selection.surface` owns persistent neutral selection. Hover and pressed colors remain action feedback and are not substitutes for selection.
- `color.codeChange.added` and `color.codeChange.removed` own code-line and compact diff-count accents; success/danger status and Git lifecycle colors remain separate semantics.
- `color.codeChange.added` (`#1aa73e`) and `color.codeChange.removed` (`#ec221f`) also anchor success and danger emphasis. Warning emphasis uses `#ff8c00`; information uses the existing creative-action blue (`#2e7eff`). These clear hues share light tints instead of separate per-component palettes.
- `color.status.*.emphasis` colors icons and short emphasis. `content` derives a readable shade from that anchor for text; `surface` and `border` derive 10% and 30% tints. High-contrast themes may strengthen text contrast without changing the emphasis anchors.
- Status source tokens retain their `color-mix()` references. The theme build resolves these mixes to concrete hex/RGBA values so CSS, plugins, and renderer payloads consume the same palette without relying on renderer-specific CSS color support.
8 changes: 6 additions & 2 deletions design-system/packages/theme-openbitfun/scripts/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
resolveTokens,
tokenNameToCssVariable,
} from "@openbitfun/token-engine";
import { resolveStatusColors } from "./resolve-status-colors.mjs";

const packageDirectory = fileURLToPath(new URL("../", import.meta.url));
const sourceDirectory = path.join(packageDirectory, "src");
Expand Down Expand Up @@ -90,11 +91,11 @@ function createReferenceColorArtifacts(document, tokens) {
const referenceColorArtifacts = createReferenceColorArtifacts(reference, referenceTokens);

function selectSemanticTokens(tokens) {
return Object.fromEntries(
return resolveStatusColors(Object.fromEntries(
Object.entries(tokens).filter(([name]) =>
PUBLIC_THEME_TOKEN_PREFIXES.some((prefix) => name.startsWith(prefix)),
),
);
));
}

const semanticThemes = {
Expand Down Expand Up @@ -140,13 +141,16 @@ const requiredSemanticTokens = [
"color.identity.globalSearch.newProject",
"color.identity.globalSearch.openFiles",
"color.status.info.content",
"color.status.info.emphasis",
"color.status.info.surface",
"color.status.success.content",
"color.status.success.emphasis",
"color.status.success.surface",
"color.status.warning.content",
"color.status.warning.emphasis",
"color.status.warning.surface",
"color.status.danger.content",
"color.status.danger.emphasis",
"color.status.danger.surface",
"effect.blur.base",
"opacity.disabled",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Status authoring stays linked to its semantic anchors. Publish concrete colors
// because renderer payloads (including Mermaid and plugins) also consume them.
export function resolveStatusColors(tokens) {
return Object.fromEntries(Object.entries(tokens).map(([name, token]) => {
if (!name.startsWith("color.status.") || !token.value.startsWith("color-mix(")) {
return [name, token];
}

const match = /^color-mix\(in srgb, (#[0-9a-f]{6}) (\d+)%, (#[0-9a-f]{6}|transparent)\)$/i.exec(token.value);
if (!match || Number(match[2]) > 100) {
throw new Error(`Unsupported status color mix for ${name}: ${token.value}`);
}
const channels = (hex) => [1, 3, 5].map((offset) => Number.parseInt(hex.slice(offset, offset + 2), 16));
const foreground = channels(match[1]);
const weight = Number(match[2]) / 100;
const value = match[3] === "transparent"
? `rgba(${foreground.join(", ")}, ${weight})`
: `#${channels(match[3]).map((channel, index) =>
Math.round(foreground[index] * weight + channel * (1 - weight)).toString(16).padStart(2, "0"),
).join("")}`;

return [name, { ...token, value }];
}));
}
27 changes: 15 additions & 12 deletions design-system/packages/theme-openbitfun/src/dark.tokens.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,29 +195,32 @@
},
"status": {
"info": {
"content": { "$type": "color", "$value": "{ref.color.neutral.400}" },
"surface": { "$type": "color", "$value": "rgba(255, 255, 255, 0.08)" },
"border": { "$type": "color", "$value": "rgba(255, 255, 255, 0.24)" }
"emphasis": { "$description": "Clear blue status emphasis, sharing the existing creative-action blue anchor.", "$type": "color", "$value": "{ref.color.blue.550}" },
"content": { "$type": "color", "$value": "color-mix(in srgb, {color.status.info.emphasis} 62%, {ref.color.neutral.0})" },
"surface": { "$type": "color", "$value": "color-mix(in srgb, {color.status.info.emphasis} 10%, transparent)" },
"border": { "$type": "color", "$value": "color-mix(in srgb, {color.status.info.emphasis} 30%, transparent)" }
},
"success": {
"content": { "$type": "color", "$value": "{ref.color.green.400}" },
"surface": { "$type": "color", "$value": "rgba(52, 211, 153, 0.1)" },
"border": { "$type": "color", "$value": "rgba(52, 211, 153, 0.3)" }
"emphasis": { "$description": "Status icons and short emphasis share the code-change anchor; use content for readable text.", "$type": "color", "$value": "{color.codeChange.added}" },
"content": { "$type": "color", "$value": "color-mix(in srgb, {color.status.success.emphasis} 70%, {ref.color.neutral.0})" },
"surface": { "$type": "color", "$value": "color-mix(in srgb, {color.status.success.emphasis} 10%, transparent)" },
"border": { "$type": "color", "$value": "color-mix(in srgb, {color.status.success.emphasis} 30%, transparent)" }
},
"warning": {
"content": { "$type": "color", "$value": "{ref.color.amber.500}" },
"content": { "$type": "color", "$value": "color-mix(in srgb, {color.status.warning.emphasis} 94%, {ref.color.neutral.0})" },
"emphasis": {
"$description": "Short warning-state emphasis using the product orange anchor; use warning content for longer text.",
"$type": "color",
"$value": "{ref.color.amber.550}"
},
"surface": { "$type": "color", "$value": "rgba(245, 158, 11, 0.1)" },
"border": { "$type": "color", "$value": "rgba(245, 158, 11, 0.3)" }
"surface": { "$type": "color", "$value": "color-mix(in srgb, {color.status.warning.emphasis} 10%, transparent)" },
"border": { "$type": "color", "$value": "color-mix(in srgb, {color.status.warning.emphasis} 30%, transparent)" }
},
"danger": {
"content": { "$type": "color", "$value": "{ref.color.red.500}" },
"surface": { "$type": "color", "$value": "rgba(239, 68, 68, 0.1)" },
"border": { "$type": "color", "$value": "rgba(239, 68, 68, 0.3)" }
"emphasis": { "$description": "Status icons and short emphasis share the code-change anchor; use content for readable text.", "$type": "color", "$value": "{color.codeChange.removed}" },
"content": { "$type": "color", "$value": "color-mix(in srgb, {color.status.danger.emphasis} 58%, {ref.color.neutral.0})" },
"surface": { "$type": "color", "$value": "color-mix(in srgb, {color.status.danger.emphasis} 10%, transparent)" },
"border": { "$type": "color", "$value": "color-mix(in srgb, {color.status.danger.emphasis} 30%, transparent)" }
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,18 @@
"ring": { "$type": "color", "$value": "{ref.color.amber.400}" }
},
"status": {
"info": {
"content": { "$type": "color", "$value": "color-mix(in srgb, {color.status.info.emphasis} 40%, {ref.color.neutral.0})" }
},
"success": {
"content": { "$type": "color", "$value": "color-mix(in srgb, {color.status.success.emphasis} 35%, {ref.color.neutral.0})" }
},
"warning": {
"emphasis": { "$type": "color", "$value": "{ref.color.amber.400}" }
"content": { "$type": "color", "$value": "color-mix(in srgb, {color.status.warning.emphasis} 75%, {ref.color.neutral.0})" },
"surface": { "$type": "color", "$value": "color-mix(in srgb, {color.status.warning.emphasis} 4%, transparent)" }
},
"danger": {
"content": { "$type": "color", "$value": "color-mix(in srgb, {color.status.danger.emphasis} 40%, {ref.color.neutral.0})" }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,18 @@
"ring": { "$type": "color", "$value": "{ref.color.blue.700}" }
},
"status": {
"info": {
"content": { "$type": "color", "$value": "color-mix(in srgb, {color.status.info.emphasis} 60%, {ref.color.neutral.1000})" }
},
"success": {
"content": { "$type": "color", "$value": "color-mix(in srgb, {color.status.success.emphasis} 50%, {ref.color.neutral.1000})" }
},
"warning": {
"emphasis": { "$type": "color", "$value": "{ref.color.amber.800}" },
"surface": { "$type": "color", "$value": "rgba(154, 101, 31, 0.04)" }
"content": { "$type": "color", "$value": "color-mix(in srgb, {color.status.warning.emphasis} 40%, {ref.color.neutral.1000})" },
"surface": { "$type": "color", "$value": "color-mix(in srgb, {color.status.warning.emphasis} 4%, transparent)" }
},
"danger": {
"content": { "$type": "color", "$value": "color-mix(in srgb, {color.status.danger.emphasis} 60%, {ref.color.neutral.1000})" }
}
}
}
Expand Down
27 changes: 15 additions & 12 deletions design-system/packages/theme-openbitfun/src/light.tokens.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,29 +195,32 @@
},
"status": {
"info": {
"content": { "$type": "color", "$value": "{ref.color.neutral.750}" },
"surface": { "$type": "color", "$value": "{ref.color.neutral.100}" },
"border": { "$type": "color", "$value": "rgba(16, 26, 39, 0.15)" }
"emphasis": { "$description": "Clear blue status emphasis, sharing the existing creative-action blue anchor.", "$type": "color", "$value": "{ref.color.blue.550}" },
"content": { "$type": "color", "$value": "color-mix(in srgb, {color.status.info.emphasis} 76%, {ref.color.neutral.1000})" },
"surface": { "$type": "color", "$value": "color-mix(in srgb, {color.status.info.emphasis} 10%, transparent)" },
"border": { "$type": "color", "$value": "color-mix(in srgb, {color.status.info.emphasis} 30%, transparent)" }
},
"success": {
"content": { "$type": "color", "$value": "{ref.color.green.700}" },
"surface": { "$type": "color", "$value": "{ref.color.green.100}" },
"border": { "$type": "color", "$value": "{ref.color.green.700}" }
"emphasis": { "$description": "Status icons and short emphasis share the code-change anchor; use content for readable text.", "$type": "color", "$value": "{color.codeChange.added}" },
"content": { "$type": "color", "$value": "color-mix(in srgb, {color.status.success.emphasis} 68%, {ref.color.neutral.1000})" },
"surface": { "$type": "color", "$value": "color-mix(in srgb, {color.status.success.emphasis} 10%, transparent)" },
"border": { "$type": "color", "$value": "color-mix(in srgb, {color.status.success.emphasis} 30%, transparent)" }
},
"warning": {
"content": { "$type": "color", "$value": "{ref.color.amber.800}" },
"content": { "$type": "color", "$value": "color-mix(in srgb, {color.status.warning.emphasis} 54%, {ref.color.neutral.1000})" },
"emphasis": {
"$description": "Short warning-state emphasis using the product orange anchor; use warning content for longer text.",
"$type": "color",
"$value": "{ref.color.amber.550}"
},
"surface": { "$type": "color", "$value": "rgba(154, 101, 31, 0.08)" },
"border": { "$type": "color", "$value": "rgba(154, 101, 31, 0.25)" }
"surface": { "$type": "color", "$value": "color-mix(in srgb, {color.status.warning.emphasis} 10%, transparent)" },
"border": { "$type": "color", "$value": "color-mix(in srgb, {color.status.warning.emphasis} 30%, transparent)" }
},
"danger": {
"content": { "$type": "color", "$value": "{ref.color.red.700}" },
"surface": { "$type": "color", "$value": "rgba(167, 67, 82, 0.12)" },
"border": { "$type": "color", "$value": "rgba(167, 67, 82, 0.36)" }
"emphasis": { "$description": "Status icons and short emphasis share the code-change anchor; use content for readable text.", "$type": "color", "$value": "{color.codeChange.removed}" },
"content": { "$type": "color", "$value": "color-mix(in srgb, {color.status.danger.emphasis} 80%, {ref.color.neutral.1000})" },
"surface": { "$type": "color", "$value": "color-mix(in srgb, {color.status.danger.emphasis} 10%, transparent)" },
"border": { "$type": "color", "$value": "color-mix(in srgb, {color.status.danger.emphasis} 30%, transparent)" }
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
themeTokenCatalog,
themes,
} from "../dist/index.js";
import { resolveStatusColors } from "../scripts/resolve-status-colors.mjs";

const packageDirectory = fileURLToPath(new URL("../", import.meta.url));

Expand Down Expand Up @@ -115,11 +116,29 @@ test("code-change semantics retain the requested addition and removal accents",
}
});

test("warning emphasis retains the product orange anchor in default themes", () => {
assert.equal(themes.light["color.status.warning.emphasis"], "#ff8c00");
assert.equal(themes.dark["color.status.warning.emphasis"], "#ff8c00");
assert.equal(themes.highContrastLight["color.status.warning.emphasis"], "#75501d");
assert.equal(themes.highContrastDark["color.status.warning.emphasis"], "#ffcc00");
test("status families derive from the code-change and product emphasis anchors", async () => {
for (const mode of themeModes) {
const values = themes[mode];
assert.equal(values["color.status.success.emphasis"], values["color.codeChange.added"]);
assert.equal(values["color.status.danger.emphasis"], values["color.codeChange.removed"]);
assert.equal(values["color.status.info.emphasis"], values["color.identity.harness.creative"]);
}

// Editing the canonical addition anchor must reach every derived status role.
const source = mergeTokenDocuments(await readSource("reference.tokens.json"), await readSource("light.tokens.json"));
source.color.codeChange.added.$value = "#123456";
const updated = resolveStatusColors(resolveTokens(source));
assert.equal(updated["color.status.success.emphasis"].value, "#123456");
assert.equal(updated["color.status.success.content"].value, "#0c233a");
assert.equal(updated["color.status.success.surface"].value, "rgba(18, 52, 86, 0.1)");
assert.equal(updated["color.status.success.border"].value, "rgba(18, 52, 86, 0.3)");
assert.equal(updated["color.status.success.content"].sourceValue, source.color.status.success.content.$value);
});

test("warning emphasis retains the product orange anchor in every theme", () => {
for (const mode of themeModes) {
assert.equal(themes[mode]["color.status.warning.emphasis"], "#ff8c00");
}
});

test("semantic theme documents route solid colors through reference scales", async () => {
Expand Down Expand Up @@ -166,20 +185,10 @@ test("all theme variants expose the same semantic theme contract", async () => {
}
});

test("text, action, and field focus pairs meet their contrast requirements", async () => {
const [reference, light, dark, highContrastLight, highContrastDark] = await Promise.all([
readSource("reference.tokens.json"),
readSource("light.tokens.json"),
readSource("dark.tokens.json"),
readSource("high-contrast-light.tokens.json"),
readSource("high-contrast-dark.tokens.json"),
test("text, action, and field focus pairs meet their contrast requirements", () => {
const variants = Object.entries(themes).map(([mode, values]) => [
mode, Object.fromEntries(Object.entries(values).map(([name, value]) => [name, { value }])),
]);
const variants = [
["light", resolveTokens(mergeTokenDocuments(reference, light))],
["dark", resolveTokens(mergeTokenDocuments(reference, dark))],
["highContrastLight", resolveTokens(mergeTokenDocuments(reference, light, highContrastLight))],
["highContrastDark", resolveTokens(mergeTokenDocuments(reference, dark, highContrastDark))],
];

for (const [mode, variant] of variants) {
const backdrop = parseColor(variant["color.surface.canvas"].value);
Expand Down Expand Up @@ -294,7 +303,7 @@ test("default modes preserve the built-in Appearance anchor values", () => {
assert.equal(themes.light["color.identity.globalSearch.openProject"], "#059cb0");
assert.equal(themes.light["color.identity.globalSearch.newProject"], "#3271d7");
assert.equal(themes.light["color.identity.globalSearch.openFiles"], "#9e54ff");
assert.equal(themes.light["color.status.warning.surface"], "rgba(154, 101, 31, 0.08)");
assert.equal(themes.light["color.status.warning.surface"], "rgba(255, 140, 0, 0.1)");
assert.equal(themes.light["shadow.base"], "0 4px 8px rgba(16, 26, 39, 0.07)");
assert.equal(themes.light["shadow.composer"], "0 2px 6px rgba(0, 0, 0, 0.08)");
assert.equal(themes.light["shadow.menu"], "0 4px 10px rgba(0, 0, 0, 0.12)");
Expand Down
Loading
Loading