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
42 changes: 21 additions & 21 deletions docs/architecture/theme-token-optimization.md

Large diffs are not rendered by default.

18 changes: 9 additions & 9 deletions scripts/theme-color-governance-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,10 @@
"max": 0
},
"colorScopes.token.occurrences": {
"max": 291
"max": 284
},
"colorScopes.token.uniqueColors": {
"max": 181
"max": 177
},
"colorScopes.exception.uniqueColors": {
"max": 162
Expand Down Expand Up @@ -144,16 +144,16 @@
"max": 0
},
"cssVarDefinitions.staticContractDefinedUnique": {
"max": 250
"max": 242
},
"cssVarDefinitions.staticContractExternalUsageUnique": {
"max": 250
"max": 242
},
"cssVarDefinitions.staticContractInternalOnlyUnique": {
"max": 0
},
"cssVarDefinitions.staticContractLowExternalUsageUnique": {
"max": 51
"max": 44
},
"tokenAliasLiterals.occurrences": {
"max": 0
Expand Down Expand Up @@ -264,19 +264,19 @@
"max": 0
},
"colorDomainScopes.themePreset.occurrences": {
"max": 168
"max": 167
},
"colorDomainScopes.themePreset.uniqueColors": {
"max": 115
"max": 114
},
"colorDomainScopes.themeRuntime.occurrences": {
"max": 27
},
"colorDomainScopes.tokenContract.occurrences": {
"max": 90
"max": 84
},
"colorDomainScopes.tokenContract.uniqueColors": {
"max": 80
"max": 76
},
"colorDomainScopes.generatedWidget.occurrences": {
"max": 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,10 @@

.snapshot-card__action-btn--accept {
color: var(--color-success);
border: 1px solid var(--git-color-staged-border);
border: 1px solid color-mix(in srgb, var(--git-color-staged) 30%, transparent);

&:hover:not(:disabled) {
background: var(--git-color-staged-bg-hover);
background: color-mix(in srgb, var(--git-color-staged) 15%, transparent);
border-color: color-mix(in srgb, var(--git-color-staged) 50%, transparent);
}
}
Expand All @@ -214,7 +214,7 @@
border: 1px solid var(--color-error-border);

&:hover:not(:disabled) {
background: var(--git-color-deleted-bg-hover);
background: color-mix(in srgb, var(--git-color-deleted) 15%, transparent);
border-color: color-mix(in srgb, var(--git-color-deleted) 50%, transparent);
}
}
Expand Down
17 changes: 0 additions & 17 deletions src/web-ui/src/component-library/styles/tokens.scss
Original file line number Diff line number Diff line change
Expand Up @@ -117,21 +117,12 @@ $git-color-branch-bg: $overlay-white-08;
$git-color-branch-bg-hover: $overlay-white-12;

$git-color-changes: $color-warning;
$git-color-changes-bg: rgba(245, 158, 11, 0.1);
$git-color-changes-bg-hover: rgba(245, 158, 11, 0.15);

$git-color-staged: rgb(34, 197, 94);
$git-color-staged-bg: rgba(34, 197, 94, 0.1);
$git-color-staged-bg-hover: rgba(34, 197, 94, 0.15);
$git-color-staged-border: rgba(34, 197, 94, 0.3);

$git-color-added: $git-color-staged;
$git-color-added-bg: $git-color-staged-bg;
$git-color-added-bg-hover: $git-color-staged-bg-hover;

$git-color-deleted: $color-error;
$git-color-deleted-bg: $color-error-bg;
$git-color-deleted-bg-hover: rgba(239, 68, 68, 0.15);

// ==================== Border system ====================
$border-subtle: $overlay-white-12;
Expand Down Expand Up @@ -428,17 +419,9 @@ $badge-info-text: $color-info;
--git-color-branch-bg: #{$git-color-branch-bg};
--git-color-branch-bg-hover: #{$git-color-branch-bg-hover};
--git-color-changes: #{$git-color-changes};
--git-color-changes-bg: #{$git-color-changes-bg};
--git-color-staged: #{$git-color-staged};
--git-color-staged-bg: #{$git-color-staged-bg};
--git-color-staged-bg-hover: #{$git-color-staged-bg-hover};
--git-color-staged-border: #{$git-color-staged-border};
--git-color-added: #{$git-color-added};
--git-color-added-bg: #{$git-color-added-bg};
--git-color-added-bg-hover: #{$git-color-added-bg-hover};
--git-color-deleted: #{$git-color-deleted};
--git-color-deleted-bg: #{$git-color-deleted-bg};
--git-color-deleted-bg-hover: #{$git-color-deleted-bg-hover};

--border-subtle: #{$border-subtle};
--border-base: #{$border-base};
Expand Down
18 changes: 9 additions & 9 deletions src/web-ui/src/flow_chat/components/InlineDiffPreview.scss
Original file line number Diff line number Diff line change
Expand Up @@ -81,23 +81,23 @@
}

&--added {
background: var(--git-color-staged-bg);
background: color-mix(in srgb, var(--git-color-added) 10%, transparent);

.diff-line__gutter {
background: var(--git-color-staged-bg-hover);
background: color-mix(in srgb, var(--git-color-added) 15%, transparent);
}

.diff-line__prefix {
color: var(--git-color-staged);
color: var(--git-color-added);
font-weight: 500;
}
}

&--removed {
background: var(--git-color-deleted-bg);
background: color-mix(in srgb, var(--git-color-deleted) 10%, transparent);

.diff-line__gutter {
background: var(--git-color-deleted-bg-hover);
background: color-mix(in srgb, var(--git-color-deleted) 15%, transparent);
}

.diff-line__prefix {
Expand Down Expand Up @@ -225,19 +225,19 @@
.light .inline-diff-preview {
.diff-line {
&--added {
background: var(--git-color-staged-bg);
background: color-mix(in srgb, var(--git-color-added) 10%, transparent);

.diff-line__gutter {
background: color-mix(in srgb, var(--git-color-staged) 18%, transparent);
background: color-mix(in srgb, var(--git-color-added) 18%, transparent);
}

.diff-line__prefix {
color: var(--git-color-staged);
color: var(--git-color-added);
}
}

&--removed {
background: var(--git-color-deleted-bg);
background: color-mix(in srgb, var(--git-color-deleted) 10%, transparent);

.diff-line__gutter {
background: color-mix(in srgb, var(--git-color-deleted) 18%, transparent);
Expand Down
8 changes: 4 additions & 4 deletions src/web-ui/src/flow_chat/tool-cards/GetFileDiffDisplay.scss
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,13 @@
word-break: break-all;

.diff-add {
background: var(--git-color-added-bg-hover);
color: var(--color-success);
background: color-mix(in srgb, var(--git-color-added) 15%, transparent);
color: var(--git-color-added);
}

.diff-del {
background: var(--git-color-deleted-bg-hover);
color: var(--color-error);
background: color-mix(in srgb, var(--git-color-deleted) 15%, transparent);
color: var(--git-color-deleted);
}
}
}
6 changes: 3 additions & 3 deletions src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.scss
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@
color: color-mix(in srgb, var(--color-success) 80%, transparent);

&:hover:not(:disabled) {
background: var(--git-color-staged-bg-hover);
background: color-mix(in srgb, var(--git-color-staged) 15%, transparent);
color: var(--git-color-staged);
}

Expand All @@ -350,8 +350,8 @@
color: color-mix(in srgb, var(--color-error) 80%, transparent);

&:hover:not(:disabled) {
background: var(--git-color-deleted-bg-hover);
color: var(--color-error);
background: color-mix(in srgb, var(--git-color-deleted) 15%, transparent);
color: var(--git-color-deleted);
}

&:active:not(:disabled) {
Expand Down
125 changes: 118 additions & 7 deletions src/web-ui/src/infrastructure/theme/core/ThemeService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,15 @@ describe('ThemeService runtime theme tokens', () => {
expect(rootStyle.getPropertyValue('--git-color-branch')).toBe('#a1a1aa');
expect(rootStyle.getPropertyValue('--git-color-branch-bg')).toBe('rgba(255, 255, 255, 0.06)');
expect(rootStyle.getPropertyValue('--git-color-branch-bg-hover')).toBe('rgba(255, 255, 255, 0.12)');
expect(rootStyle.getPropertyValue('--git-color-changes')).toBe('rgb(245, 158, 11)');
expect(rootStyle.getPropertyValue('--git-color-added')).toBe('rgb(34, 197, 94)');
expect(rootStyle.getPropertyValue('--git-color-added-bg')).toBe('rgba(34, 197, 94, 0.1)');
expect(rootStyle.getPropertyValue('--git-color-added-bg-hover')).toBe('rgba(34, 197, 94, 0.15)');
expect(rootStyle.getPropertyValue('--git-color-changes-bg')).toBe('rgba(245, 158, 11, 0.1)');
expect(rootStyle.getPropertyValue('--git-color-deleted-bg-hover')).toBe('rgba(239, 68, 68, 0.15)');
expect(rootStyle.getPropertyValue('--git-color-staged-bg-hover')).toBe('rgba(34, 197, 94, 0.15)');
expect(rootStyle.getPropertyValue('--git-color-staged-border')).toBe('rgba(34, 197, 94, 0.3)');
expect(rootStyle.getPropertyValue('--git-color-deleted')).toBe('rgb(239, 68, 68)');
expect(rootStyle.getPropertyValue('--git-color-staged')).toBe('rgb(34, 197, 94)');
expect(rootStyle.getPropertyValue('--git-color-changes-bg')).toBe('');
expect(rootStyle.getPropertyValue('--git-color-added-bg')).toBe('');
expect(rootStyle.getPropertyValue('--git-color-deleted-bg')).toBe('');
expect(rootStyle.getPropertyValue('--git-color-staged-bg')).toBe('');
expect(rootStyle.getPropertyValue('--git-color-staged-border')).toBe('');
expect(rootStyle.getPropertyValue('--git-color-pull')).toBe('');
expect(rootStyle.getPropertyValue('--git-color-push')).toBe('');
});
Expand Down Expand Up @@ -388,7 +390,7 @@ describe('ThemeService runtime theme tokens', () => {
expect(configAPI.setConfig).not.toHaveBeenCalledWith('themes.custom', expect.anything());
});

it('does not export non-contract dynamic keys from custom themes', () => {
it('does not inject non-contract dynamic keys from custom themes', () => {
const service = new ThemeService();
const customTheme = {
...bitfunLightTheme,
Expand Down Expand Up @@ -616,6 +618,115 @@ describe('ThemeService runtime theme tokens', () => {
).rejects.toThrow(/reserved for a built-in theme/);
});

it('strips non-contract git color keys from registered custom themes', async () => {
const nonContractGitColorKeys = [
'changesBg',
'addedBg',
'deletedBg',
'stagedBg',
'addedBgHover',
'stagedBorder',
'pull',
] as const;
const expectNoNonContractGitColorKeys = (gitColors: ThemeConfig['colors']['git']) => {
const gitRecord = gitColors as unknown as Record<string, unknown>;
nonContractGitColorKeys.forEach(key => {
expect(gitRecord).not.toHaveProperty(key);
});
};
const service = new ThemeService();
const legacyTheme = {
...bitfunDarkTheme,
id: 'custom-legacy-git-bg',
name: 'Legacy Git Backgrounds',
colors: {
...bitfunDarkTheme.colors,
git: {
...bitfunDarkTheme.colors.git,
changesBg: 'rgba(245, 158, 11, 0.1)',
addedBg: 'rgba(34, 197, 94, 0.1)',
deletedBg: 'rgba(239, 68, 68, 0.1)',
stagedBg: 'rgba(16, 185, 129, 0.1)',
addedBgHover: 'rgba(34, 197, 94, 0.2)',
stagedBorder: 'rgba(16, 185, 129, 0.4)',
pull: '#60a5fa',
},
},
} as unknown as ThemeConfig;

await service.registerTheme(legacyTheme);

const normalized = service.getTheme('custom-legacy-git-bg');
expect(normalized).toBeDefined();
if (!normalized) {
throw new Error('Expected custom legacy git theme to be registered');
}
expect(normalized.colors.git.added).toBe(bitfunDarkTheme.colors.git.added);
expectNoNonContractGitColorKeys(normalized.colors.git);

const persistedThemes = vi.mocked(configAPI.setConfig).mock.calls.find(([key]) => key === 'themes.custom')?.[1] as
| ThemeConfig[]
| undefined;
const persistedTheme = persistedThemes?.find(theme => theme.id === 'custom-legacy-git-bg');
expect(persistedTheme).toBeDefined();
if (!persistedTheme) {
throw new Error('Expected custom legacy git theme to be persisted');
}
expectNoNonContractGitColorKeys(persistedTheme.colors.git);

const exported = service.exportTheme('custom-legacy-git-bg');
expect(exported).not.toBeNull();
if (!exported) {
throw new Error('Expected custom legacy git theme to be exported');
}
expectNoNonContractGitColorKeys(exported.theme.colors.git);
});

it('migrates persisted custom themes with non-contract git color keys on load', async () => {
const legacyTheme = {
...bitfunDarkTheme,
id: 'custom-loaded-legacy-git',
name: 'Loaded Legacy Git',
colors: {
...bitfunDarkTheme.colors,
git: {
...bitfunDarkTheme.colors.git,
changesBg: 'rgba(245, 158, 11, 0.1)',
addedBgHover: 'rgba(34, 197, 94, 0.2)',
stagedBorder: 'rgba(16, 185, 129, 0.4)',
},
},
} as unknown as ThemeConfig;
vi.mocked(configAPI.getConfig).mockResolvedValue({ custom: [legacyTheme] });
const service = new ThemeService();

await service.ensureUserThemesLoaded();

const normalized = service.getTheme('custom-loaded-legacy-git');
expect(normalized).toBeDefined();
if (!normalized) {
throw new Error('Expected legacy custom theme to load');
}
expect(normalized.colors.git.added).toBe(bitfunDarkTheme.colors.git.added);
expect(normalized.colors.git.staged).toBe(bitfunDarkTheme.colors.git.staged);
expect(normalized.colors.git as unknown as Record<string, unknown>).not.toHaveProperty('changesBg');
expect(normalized.colors.git as unknown as Record<string, unknown>).not.toHaveProperty('addedBgHover');
expect(normalized.colors.git as unknown as Record<string, unknown>).not.toHaveProperty('stagedBorder');

const migratedThemes = vi.mocked(configAPI.setConfig).mock.calls.find(([key]) => key === 'themes.custom')?.[1] as
| ThemeConfig[]
| undefined;
expect(migratedThemes).toHaveLength(1);
const migratedGitColors = migratedThemes?.[0]?.colors.git as unknown as Record<string, unknown> | undefined;
expect(migratedGitColors).toBeDefined();
if (!migratedGitColors) {
throw new Error('Expected migrated theme to keep git colors');
}
expect(migratedGitColors).not.toHaveProperty('changesBg');
expect(migratedGitColors).not.toHaveProperty('addedBgHover');
expect(migratedGitColors).not.toHaveProperty('stagedBorder');
});

it('projects normalized custom themes through the compact plugin color boundary', async () => {
const service = new ThemeService();
const partialCustomTheme = {
Expand Down
Loading
Loading