Skip to content

Commit 989dd2e

Browse files
Merge pull request #143 from codeflash-ai/codex/desktop-theme-refresh
Refresh desktop app theme
2 parents 7df7375 + e76f36e commit 989dd2e

6 files changed

Lines changed: 1473 additions & 469 deletions

File tree

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
import { readFileSync } from "node:fs";
2+
3+
import { describe, expect, it } from "vitest";
4+
5+
const styles = readFileSync(new URL("./styles.css", import.meta.url), "utf8");
6+
7+
function escapeRegExp(value) {
8+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9+
}
10+
11+
function cssBlock(selector) {
12+
const matches = [...styles.matchAll(new RegExp(`(^|\\n)${escapeRegExp(selector)}\\s*\\{`, "g"))];
13+
const match = matches.at(-1);
14+
const start = match?.index ?? -1;
15+
expect(start, `missing ${selector} block`).toBeGreaterThanOrEqual(0);
16+
17+
const open = styles.indexOf("{", start);
18+
let depth = 0;
19+
for (let index = open; index < styles.length; index += 1) {
20+
const char = styles[index];
21+
if (char === "{") {
22+
depth += 1;
23+
}
24+
if (char === "}") {
25+
depth -= 1;
26+
if (depth === 0) {
27+
return styles.slice(open + 1, index);
28+
}
29+
}
30+
}
31+
32+
throw new Error(`unterminated ${selector} block`);
33+
}
34+
35+
function expectToken(block, property, token) {
36+
expect(block).toContain(`${property}: var(${token});`);
37+
}
38+
39+
function expectNoRawColorValues(block) {
40+
expect(block).not.toMatch(/(?:background|border(?:-color)?|color):\s*(?:#|rgba?\(|linear-gradient)/);
41+
}
42+
43+
function expectNoRawShadowValues(block) {
44+
for (const match of block.matchAll(/box-shadow:\s*([^;]+);/g)) {
45+
expect(match[1], "box-shadow should include a semantic token").toContain("var(--");
46+
expect(match[1], "box-shadow should not use raw colors").not.toMatch(/#|rgba?\(/);
47+
}
48+
}
49+
50+
describe("desktop component theme contract", () => {
51+
it("uses semantic tokens for primary, secondary, Live Mode, and icon controls", () => {
52+
const primaryButton = cssBlock(".primary-button");
53+
const primaryButtonHover = cssBlock(".primary-button:hover:not(:disabled)");
54+
const secondaryButton = cssBlock(".secondary-button");
55+
const secondaryButtonHover = cssBlock(".secondary-button:hover:not(:disabled)");
56+
const liveModeControl = cssBlock(".live-mode-control");
57+
const liveModeButton = cssBlock(".live-mode-button");
58+
const liveModeButtonHover = cssBlock(".live-mode-button:hover:not(:disabled)");
59+
const liveModeButtonActive = cssBlock(".live-mode-button.active");
60+
const iconButton = cssBlock(".icon-button");
61+
62+
expectToken(primaryButton, "background", "--primary-button-bg");
63+
expectToken(primaryButton, "color", "--primary-button-text");
64+
expectToken(primaryButton, "box-shadow", "--surface-shadow");
65+
expectToken(primaryButtonHover, "background", "--primary-button-hover-bg");
66+
expectToken(primaryButtonHover, "box-shadow", "--surface-shadow");
67+
expectToken(secondaryButton, "background", "--control-bg");
68+
expectToken(secondaryButton, "color", "--control-text");
69+
expectToken(secondaryButtonHover, "background", "--control-bg-hover");
70+
expectToken(secondaryButtonHover, "box-shadow", "--surface-shadow");
71+
expectToken(liveModeControl, "background", "--live-control-bg");
72+
expectToken(liveModeControl, "color", "--accent");
73+
expectToken(liveModeButton, "background", "--live-control-bg");
74+
expectToken(liveModeButton, "color", "--accent");
75+
expectToken(liveModeButtonHover, "background", "--live-control-hover-bg");
76+
expectToken(liveModeButtonHover, "box-shadow", "--surface-shadow");
77+
expectToken(liveModeButtonActive, "background", "--primary-button-bg");
78+
expectToken(liveModeButtonActive, "color", "--primary-button-text");
79+
expectToken(liveModeButtonActive, "box-shadow", "--surface-shadow");
80+
expectToken(iconButton, "background", "--control-bg");
81+
expectToken(iconButton, "color", "--control-text");
82+
83+
for (const block of [
84+
primaryButton,
85+
primaryButtonHover,
86+
secondaryButton,
87+
secondaryButtonHover,
88+
liveModeControl,
89+
liveModeButton,
90+
liveModeButtonHover,
91+
liveModeButtonActive,
92+
iconButton,
93+
]) {
94+
expectNoRawColorValues(block);
95+
}
96+
97+
for (const block of [
98+
primaryButton,
99+
primaryButtonHover,
100+
secondaryButtonHover,
101+
liveModeButtonHover,
102+
liveModeButtonActive,
103+
]) {
104+
expectNoRawShadowValues(block);
105+
}
106+
});
107+
108+
it("uses shared surface tokens for cards, panels, metrics, and rows", () => {
109+
const sharedSurface = cssBlock(
110+
".workspace-card,\n.panel,\n.attention-panel,\n.empty-action-panel,\n.mount-hero,\n.safety-strip,\n.file-list,\n.activity-group,\n.summary-grid,\n.advanced-panel,\n.located-path",
111+
);
112+
const homeStat = cssBlock(".home-stat");
113+
const homeStatHover = cssBlock("button.home-stat:hover");
114+
const metric = cssBlock(".metric");
115+
const activityItem = cssBlock(".activity-item");
116+
117+
expectToken(sharedSurface, "background", "--surface");
118+
expectToken(sharedSurface, "box-shadow", "--surface-shadow");
119+
expectToken(homeStat, "background", "--surface");
120+
expectToken(homeStat, "box-shadow", "--surface-shadow");
121+
expectToken(homeStatHover, "background", "--surface-hover");
122+
expectToken(metric, "background", "--surface-muted");
123+
expectToken(activityItem, "background", "--surface-muted");
124+
});
125+
126+
it("uses status role tokens instead of dark-only status colors", () => {
127+
expect(styles).toMatch(/\.status-pill\.ready\s*\{[\s\S]*?background:\s*var\(--status-ready-bg\);[\s\S]*?color:\s*var\(--status-ready-text\);/s);
128+
expect(styles).toMatch(/\.status-pill\.warn\s*\{[\s\S]*?background:\s*var\(--status-warn-bg\);[\s\S]*?color:\s*var\(--status-warn-text\);/s);
129+
expect(styles).toMatch(/\.status-pill\.danger\s*\{[\s\S]*?background:\s*var\(--status-danger-bg\);[\s\S]*?color:\s*var\(--status-danger-text\);/s);
130+
expect(styles).not.toMatch(/:root\[data-theme="dark"\][^{]*\.status-pill\.(ready|warn|danger)\s*\{/);
131+
});
132+
133+
it("keeps review, file, settings, and editor surfaces tokenized", () => {
134+
expect(styles).toMatch(/\.review-filter-bar\s*\{[\s\S]*?background:\s*var\(--field-bg\);/s);
135+
expect(styles).toMatch(/\.file-filter-bar button\s*\{[\s\S]*?background:\s*var\(--control-bg\);/s);
136+
expect(styles).toMatch(/\.file-detail-panel\s*\{[\s\S]*?background:\s*var\(--surface-muted\);/s);
137+
expect(styles).toMatch(/\.file-detail-panel pre\s*\{[\s\S]*?background:\s*var\(--code-bg\);/s);
138+
expect(styles).toMatch(/\.markdown-editor\s*\{[\s\S]*?background:\s*var\(--code-bg\);/s);
139+
expect(styles).toMatch(/\.settings-nav\s*\{[\s\S]*?background:\s*var\(--surface\);/s);
140+
expect(styles).toMatch(/\.theme-segmented\s*\{[\s\S]*?background:\s*var\(--field-bg\);/s);
141+
});
142+
143+
it("keeps task-scoped status, review, and settings overrides tokenized", () => {
144+
const safetyStrip = cssBlock(".safety-strip");
145+
const darkMetric = cssBlock(':root[data-theme="dark"] .review-counts .metric,\n:root[data-theme="dark"] .metric');
146+
const darkMetricText = cssBlock(':root[data-theme="dark"] .metric strong');
147+
const darkReviewFilter = cssBlock(
148+
':root[data-theme="dark"] .review-filter-button.active,\n:root[data-theme="dark"] .review-filter-button:hover',
149+
);
150+
const darkReviewFilterCount = cssBlock(':root[data-theme="dark"] .review-filter-button span');
151+
const darkReviewOverview = cssBlock(':root[data-theme="dark"] .review-overview-panel');
152+
const fileFilterState = cssBlock(".file-filter-bar button.active,\n.file-filter-bar button:hover");
153+
const settingsActivity = cssBlock(".settings-activity-row");
154+
const settingsActivityHover = cssBlock(".settings-activity-row:hover");
155+
const settingsNavState = cssBlock(".settings-nav button:hover,\n.settings-nav button.active");
156+
const darkSettingsNavSmall = cssBlock(
157+
':root[data-theme="dark"] .settings-nav button:hover small,\n:root[data-theme="dark"] .settings-nav button.active small',
158+
);
159+
const darkSharedActive = cssBlock(
160+
':root[data-theme="dark"] .source-view-toggle button.active,\n:root[data-theme="dark"] .file-filter-bar button.active,\n:root[data-theme="dark"] .file-filter-bar button:hover,\n:root[data-theme="dark"] .activity-tabs button.active,\n:root[data-theme="dark"] .settings-nav button:hover,\n:root[data-theme="dark"] .settings-nav button.active,\n:root[data-theme="dark"] .theme-segmented button.active,\n:root[data-theme="dark"] .option-row:hover',
161+
);
162+
const darkSourceCards = cssBlock(
163+
':root[data-theme="dark"] .source-ready-card,\n:root[data-theme="dark"] .connector-choice-card.active,\n:root[data-theme="dark"] .mount-card.active',
164+
);
165+
const primaryButton = cssBlock(".primary-button");
166+
const primaryButtonHover = cssBlock(".primary-button:hover:not(:disabled)");
167+
const secondaryButtonHover = cssBlock(".secondary-button:hover:not(:disabled)");
168+
const liveModeButtonHover = cssBlock(".live-mode-button:hover:not(:disabled)");
169+
const liveModeButtonActive = cssBlock(".live-mode-button.active");
170+
const liveModeHover = cssBlock(".live-mode-control:hover:not(:disabled)");
171+
const liveModeActive = cssBlock(".live-mode-control.active");
172+
const themeSegmentedActive = cssBlock(".theme-segmented button.active");
173+
174+
expectToken(safetyStrip, "border-color", "--control-border-hover");
175+
expectToken(safetyStrip, "background", "--control-selected-bg");
176+
expectToken(darkMetric, "border-color", "--line");
177+
expectToken(darkMetric, "background", "--surface-muted");
178+
expectToken(darkMetricText, "color", "--ink");
179+
expectToken(darkReviewFilter, "background", "--control-selected-bg");
180+
expectToken(darkReviewFilter, "color", "--control-selected-text");
181+
expectToken(darkReviewFilterCount, "background", "--surface-raised");
182+
expectToken(darkReviewOverview, "background", "--surface");
183+
expectToken(fileFilterState, "border-color", "--control-border-hover");
184+
expectToken(fileFilterState, "background", "--control-selected-bg");
185+
expectToken(fileFilterState, "color", "--control-selected-text");
186+
expect(settingsActivity).toContain("border: 1px solid var(--line);");
187+
expectToken(settingsActivity, "background", "--surface-raised");
188+
expectToken(settingsActivity, "color", "--ink");
189+
expectToken(settingsActivityHover, "border-color", "--control-border-hover");
190+
expectToken(settingsActivityHover, "background", "--surface-hover");
191+
expectToken(settingsNavState, "border-color", "--control-border-hover");
192+
expectToken(settingsNavState, "background", "--control-selected-bg");
193+
expectToken(settingsNavState, "color", "--control-selected-text");
194+
expectToken(darkSettingsNavSmall, "color", "--control-muted");
195+
expectToken(darkSharedActive, "box-shadow", "--surface-shadow");
196+
expectToken(darkSourceCards, "border-color", "--line");
197+
expectToken(darkSourceCards, "background", "--surface-muted");
198+
expectToken(darkSourceCards, "color", "--ink");
199+
expectToken(primaryButton, "box-shadow", "--surface-shadow");
200+
expectToken(primaryButtonHover, "box-shadow", "--surface-shadow");
201+
expectToken(secondaryButtonHover, "box-shadow", "--surface-shadow");
202+
expectToken(liveModeButtonHover, "box-shadow", "--surface-shadow");
203+
expectToken(liveModeButtonActive, "box-shadow", "--surface-shadow");
204+
expectToken(liveModeHover, "box-shadow", "--surface-shadow");
205+
expectToken(liveModeActive, "box-shadow", "--surface-shadow");
206+
expectToken(themeSegmentedActive, "background", "--control-selected-bg");
207+
expectToken(themeSegmentedActive, "color", "--control-selected-text");
208+
expect(themeSegmentedActive).toContain("box-shadow: inset 0 0 0 1px var(--control-border-hover);");
209+
210+
for (const block of [
211+
safetyStrip,
212+
darkMetric,
213+
darkMetricText,
214+
darkReviewFilter,
215+
darkReviewFilterCount,
216+
darkReviewOverview,
217+
fileFilterState,
218+
settingsActivity,
219+
settingsActivityHover,
220+
settingsNavState,
221+
darkSettingsNavSmall,
222+
darkSourceCards,
223+
themeSegmentedActive,
224+
]) {
225+
expectNoRawColorValues(block);
226+
}
227+
228+
for (const block of [
229+
primaryButton,
230+
primaryButtonHover,
231+
secondaryButtonHover,
232+
liveModeButtonHover,
233+
liveModeButtonActive,
234+
liveModeHover,
235+
liveModeActive,
236+
themeSegmentedActive,
237+
]) {
238+
expectNoRawColorValues(block);
239+
expectNoRawShadowValues(block);
240+
}
241+
242+
expectNoRawShadowValues(darkSharedActive);
243+
});
244+
245+
it("centers compact source-card controls without broad span overrides", () => {
246+
const sourceReadyCopy = cssBlock(".source-ready-card strong,\n.source-ready-card > div > span");
247+
const sourceReadyControls = cssBlock(
248+
".source-ready-card .status-pill,\n.source-ready-card .secondary-button.compact,\n.connector-choice-card .primary-button.compact,\n.connector-choice-card .secondary-button.compact,\n.mount-details-button",
249+
);
250+
const sourceReadyControlIcons = cssBlock(
251+
".source-ready-card .secondary-button.compact svg,\n.connector-choice-card .primary-button.compact svg,\n.connector-choice-card .secondary-button.compact svg,\n.mount-details-button svg",
252+
);
253+
const sourceReadyButtonText = cssBlock(
254+
".source-ready-card .secondary-button.compact span,\n.connector-choice-card .primary-button.compact span,\n.connector-choice-card .secondary-button.compact span",
255+
);
256+
257+
expect(styles).not.toMatch(/\.source-ready-card strong,\s*\.source-ready-card span\s*\{/);
258+
expect(sourceReadyCopy).toContain("display: block;");
259+
expect(sourceReadyControls).toContain("display: inline-flex;");
260+
expect(sourceReadyControls).toContain("align-items: center;");
261+
expect(sourceReadyControls).toContain("justify-content: center;");
262+
expect(sourceReadyControls).toContain("line-height: 1;");
263+
expect(sourceReadyControlIcons).toContain("flex: 0 0 auto;");
264+
expect(sourceReadyControlIcons).toContain("width: 13px;");
265+
expect(sourceReadyControlIcons).toContain("height: 13px;");
266+
expect(sourceReadyButtonText).toContain("display: inline-flex;");
267+
expect(sourceReadyButtonText).toContain("align-items: center;");
268+
expect(sourceReadyButtonText).toContain("line-height: 1;");
269+
});
270+
271+
it("keeps collapsed sidebar navigation spaced below the mark", () => {
272+
const collapsedNav = cssBlock(".sidebar-collapsed .sidebar nav");
273+
274+
expect(collapsedNav).toContain("margin-top: 10px;");
275+
});
276+
});

0 commit comments

Comments
 (0)