feat: refine workbench visuals and map controls
Unify the Agent history extension with the header acrylic surface, preserve the full conversation body, and consolidate shared control and status styling. Restore map flow and SCADA controller behavior, remove obsolete rendering paths, and extend regression coverage. Button press coverage now releases outside the target so state assertions cannot accidentally toggle the control.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const statusTones = ["neutral", "info", "success", "warning", "danger"] as const;
|
||||
|
||||
describe("semantic status language", () => {
|
||||
it("keeps the production status foregrounds above 4.5:1", () => {
|
||||
const styles = readFileSync(join(process.cwd(), "src/styles.css"), "utf8");
|
||||
|
||||
statusTones.forEach((tone) => {
|
||||
const soft = readOklchToken(styles, `--status-${tone}-soft`);
|
||||
const foreground = readOklchToken(styles, `--status-${tone}-foreground`);
|
||||
expect(getOklchContrastRatio(foreground, soft), tone).toBeGreaterThanOrEqual(4.5);
|
||||
});
|
||||
});
|
||||
|
||||
it("defines the complete softened OKLCH token set", () => {
|
||||
const styles = readFileSync(join(process.cwd(), "src/styles.css"), "utf8");
|
||||
|
||||
statusTones.forEach((tone) => {
|
||||
expect(styles).toContain(`--status-${tone}-soft:`);
|
||||
expect(styles).toContain(`--status-${tone}-border:`);
|
||||
expect(styles).toContain(`--status-${tone}-foreground:`);
|
||||
expect(styles).toContain(`--status-${tone}-mark:`);
|
||||
expect(styles).toContain(`.status-tone-${tone}`);
|
||||
});
|
||||
});
|
||||
|
||||
it("stops shared status motion when reduced motion is requested", () => {
|
||||
const styles = readFileSync(join(process.cwd(), "src/styles.css"), "utf8");
|
||||
|
||||
expect(styles).toMatch(
|
||||
/@media \(prefers-reduced-motion: reduce\)[\s\S]*?\.status-dot\[data-activity="live"\][\s\S]*?animation:\s*none/
|
||||
);
|
||||
});
|
||||
|
||||
it("does not derive Agent connection styling from localized copy", () => {
|
||||
const agentHook = readFileSync(
|
||||
join(process.cwd(), "src/features/workbench/hooks/use-workbench-agent.ts"),
|
||||
"utf8"
|
||||
);
|
||||
const agentPanel = readFileSync(
|
||||
join(process.cwd(), "src/features/agent/components/agent-command-panel.tsx"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
expect(agentPanel).not.toMatch(/statusLabel\.(?:includes|match|startsWith)/);
|
||||
expect(agentPanel).not.toContain("getAgentConnection");
|
||||
expect(agentHook).not.toContain("runtimeAvailability");
|
||||
expect(agentPanel).toContain("AgentConnectionStatus");
|
||||
});
|
||||
});
|
||||
|
||||
type OklchColor = {
|
||||
lightness: number;
|
||||
chroma: number;
|
||||
hue: number;
|
||||
};
|
||||
|
||||
function readOklchToken(styles: string, token: string): OklchColor {
|
||||
const match = styles.match(
|
||||
new RegExp(`${token}:\\s*oklch\\((\\d+(?:\\.\\d+)?)%\\s+(\\d+(?:\\.\\d+)?)\\s+(\\d+(?:\\.\\d+)?)\\)`)
|
||||
);
|
||||
if (!match) {
|
||||
throw new Error(`Missing OKLCH token: ${token}`);
|
||||
}
|
||||
return {
|
||||
lightness: Number(match[1]) / 100,
|
||||
chroma: Number(match[2]),
|
||||
hue: Number(match[3])
|
||||
};
|
||||
}
|
||||
|
||||
function getOklchContrastRatio(foreground: OklchColor, background: OklchColor) {
|
||||
const foregroundLuminance = getOklchRelativeLuminance(foreground);
|
||||
const backgroundLuminance = getOklchRelativeLuminance(background);
|
||||
const lighter = Math.max(foregroundLuminance, backgroundLuminance);
|
||||
const darker = Math.min(foregroundLuminance, backgroundLuminance);
|
||||
return (lighter + 0.05) / (darker + 0.05);
|
||||
}
|
||||
|
||||
function getOklchRelativeLuminance({ lightness, chroma, hue }: OklchColor) {
|
||||
const hueRadians = (hue * Math.PI) / 180;
|
||||
const a = chroma * Math.cos(hueRadians);
|
||||
const b = chroma * Math.sin(hueRadians);
|
||||
const lPrime = lightness + 0.3963377774 * a + 0.2158037573 * b;
|
||||
const mPrime = lightness - 0.1055613458 * a - 0.0638541728 * b;
|
||||
const sPrime = lightness - 0.0894841775 * a - 1.291485548 * b;
|
||||
const l = lPrime ** 3;
|
||||
const m = mPrime ** 3;
|
||||
const s = sPrime ** 3;
|
||||
const red = clampLinearRgb(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s);
|
||||
const green = clampLinearRgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s);
|
||||
const blue = clampLinearRgb(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s);
|
||||
return 0.2126 * red + 0.7152 * green + 0.0722 * blue;
|
||||
}
|
||||
|
||||
function clampLinearRgb(value: number) {
|
||||
return Math.min(1, Math.max(0, value));
|
||||
}
|
||||
Reference in New Issue
Block a user