Skip to content

Commit 547b545

Browse files
Add support for custom accent color theming
Enable users to set an accent color in the extension settings for improved theming customization. This change applies the accent color throughout the UI using CSS variables.
1 parent 8e4b145 commit 547b545

File tree

7 files changed

+83
-3
lines changed

7 files changed

+83
-3
lines changed

packages/types/src/global-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ export const globalSettingsSchema = z.object({
184184
hasOpenedModeSelector: z.boolean().optional(),
185185
lastModeExportPath: z.string().optional(),
186186
lastModeImportPath: z.string().optional(),
187+
accentColor: z.string().optional(),
187188
})
188189

189190
export type GlobalSettings = z.infer<typeof globalSettingsSchema>

src/core/webview/webviewMessageHandler.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1692,6 +1692,10 @@ export const webviewMessageHandler = async (
16921692
await updateGlobalState("reasoningBlockCollapsed", message.bool ?? true)
16931693
// No need to call postStateToWebview here as the UI already updated optimistically
16941694
break
1695+
case "setAccentColor":
1696+
await updateGlobalState("accentColor", message.text ?? undefined)
1697+
await provider.postStateToWebview()
1698+
break
16951699
case "toggleApiConfigPin":
16961700
if (message.text) {
16971701
const currentPinned = getGlobalState("pinnedApiConfigs") ?? {}

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ export interface WebviewMessage {
199199
| "profileThresholds"
200200
| "setHistoryPreviewCollapsed"
201201
| "setReasoningBlockCollapsed"
202+
| "setAccentColor"
202203
| "openExternal"
203204
| "filterMarketplaceItems"
204205
| "marketplaceButtonClicked"

webview-ui/src/App.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ const App = () => {
7979
cloudOrganizations,
8080
renderContext,
8181
mdmCompliant,
82+
accentColor,
8283
} = useExtensionState()
8384

8485
// Create a persistent state manager
@@ -233,6 +234,18 @@ const App = () => {
233234
}
234235
}, [renderContext]),
235236
)
237+
238+
// Apply accent color to CSS custom property
239+
useEffect(() => {
240+
if (accentColor) {
241+
document.documentElement.style.setProperty("--accent", accentColor)
242+
document.documentElement.style.setProperty("--color-accent", accentColor)
243+
} else {
244+
// Reset to default when no accent color is set
245+
document.documentElement.style.removeProperty("--accent")
246+
document.documentElement.style.removeProperty("--color-accent")
247+
}
248+
}, [accentColor])
236249
// Track marketplace tab views
237250
useEffect(() => {
238251
if (tab === "marketplace") {

webview-ui/src/components/settings/SettingsView.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
791791
{activeTab === "ui" && (
792792
<UISettings
793793
reasoningBlockCollapsed={reasoningBlockCollapsed ?? true}
794+
accentColor={extensionState.accentColor}
794795
setCachedStateField={setCachedStateField}
795796
/>
796797
)}

webview-ui/src/components/settings/UISettings.tsx

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { HTMLAttributes } from "react"
1+
import { HTMLAttributes, useState, useEffect } from "react"
22
import { useAppTranslation } from "@/i18n/TranslationContext"
3-
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
3+
import { VSCodeCheckbox, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
44
import { Glasses } from "lucide-react"
55
import { telemetryClient } from "@/utils/TelemetryClient"
66

@@ -11,11 +11,18 @@ import { ExtensionStateContextType } from "@/context/ExtensionStateContext"
1111

1212
interface UISettingsProps extends HTMLAttributes<HTMLDivElement> {
1313
reasoningBlockCollapsed: boolean
14+
accentColor?: string
1415
setCachedStateField: SetCachedStateField<keyof ExtensionStateContextType>
1516
}
1617

17-
export const UISettings = ({ reasoningBlockCollapsed, setCachedStateField, ...props }: UISettingsProps) => {
18+
export const UISettings = ({ reasoningBlockCollapsed, accentColor, setCachedStateField, ...props }: UISettingsProps) => {
1819
const { t } = useAppTranslation()
20+
const [colorValue, setColorValue] = useState(accentColor || "")
21+
22+
// Sync local state with prop changes
23+
useEffect(() => {
24+
setColorValue(accentColor || "")
25+
}, [accentColor])
1926

2027
const handleReasoningBlockCollapsedChange = (value: boolean) => {
2128
setCachedStateField("reasoningBlockCollapsed", value)
@@ -26,6 +33,20 @@ export const UISettings = ({ reasoningBlockCollapsed, setCachedStateField, ...pr
2633
})
2734
}
2835

36+
const handleAccentColorChange = (value: string) => {
37+
setColorValue(value)
38+
// Only update if it's a valid color or empty
39+
const trimmedValue = value.trim()
40+
if (trimmedValue === "" || /^#[0-9A-Fa-f]{6}$/.test(trimmedValue)) {
41+
setCachedStateField("accentColor", trimmedValue || undefined)
42+
43+
// Track telemetry event
44+
telemetryClient.capture("ui_settings_accent_color_changed", {
45+
hasColor: trimmedValue !== "",
46+
})
47+
}
48+
}
49+
2950
return (
3051
<div {...props}>
3152
<SectionHeader>
@@ -49,6 +70,33 @@ export const UISettings = ({ reasoningBlockCollapsed, setCachedStateField, ...pr
4970
{t("settings:ui.collapseThinking.description")}
5071
</div>
5172
</div>
73+
74+
{/* Accent Color Setting */}
75+
<div className="flex flex-col gap-2">
76+
<label className="font-medium" htmlFor="accent-color-input">
77+
Accent Color
78+
</label>
79+
<div className="flex items-center gap-2">
80+
<VSCodeTextField
81+
id="accent-color-input"
82+
value={colorValue}
83+
placeholder="#007ACC"
84+
onInput={(e: any) => handleAccentColorChange(e.target.value)}
85+
data-testid="accent-color-input"
86+
style={{ flex: 1 }}
87+
/>
88+
<input
89+
type="color"
90+
value={colorValue && /^#[0-9A-Fa-f]{6}$/.test(colorValue) ? colorValue : "#007ACC"}
91+
onChange={(e) => handleAccentColorChange(e.target.value)}
92+
className="h-8 w-16 cursor-pointer rounded border border-vscode-input-border"
93+
data-testid="accent-color-picker"
94+
/>
95+
</div>
96+
<div className="text-vscode-descriptionForeground text-sm">
97+
Customize the accent color used throughout the extension. Leave empty to use the default theme color. Enter a hex color code (e.g., #007ACC) or use the color picker.
98+
</div>
99+
</div>
52100
</div>
53101
</Section>
54102
</div>

webview-ui/src/context/ExtensionStateContext.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ export interface ExtensionStateContextType extends ExtensionState {
165165
setIncludeCurrentTime: (value: boolean) => void
166166
includeCurrentCost?: boolean
167167
setIncludeCurrentCost: (value: boolean) => void
168+
accentColor?: string
169+
setAccentColor: (value: string | undefined) => void
168170
}
169171

170172
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
@@ -298,6 +300,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
298300
const [prevCloudIsAuthenticated, setPrevCloudIsAuthenticated] = useState(false)
299301
const [includeCurrentTime, setIncludeCurrentTime] = useState(true)
300302
const [includeCurrentCost, setIncludeCurrentCost] = useState(true)
303+
const [accentColor, setAccentColor] = useState<string | undefined>(undefined)
301304

302305
const setListApiConfigMeta = useCallback(
303306
(value: ProviderSettingsEntry[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),
@@ -343,6 +346,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
343346
if ((newState as any).includeCurrentCost !== undefined) {
344347
setIncludeCurrentCost((newState as any).includeCurrentCost)
345348
}
349+
// Update accentColor if present in state message
350+
if ((newState as any).accentColor !== undefined) {
351+
setAccentColor((newState as any).accentColor)
352+
}
346353
// Handle marketplace data if present in state message
347354
if (newState.marketplaceItems !== undefined) {
348355
setMarketplaceItems(newState.marketplaceItems)
@@ -596,6 +603,11 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
596603
setIncludeCurrentTime,
597604
includeCurrentCost,
598605
setIncludeCurrentCost,
606+
accentColor,
607+
setAccentColor: (value: string | undefined) => {
608+
setAccentColor(value)
609+
vscode.postMessage({ type: "setAccentColor", text: value })
610+
},
599611
}
600612

601613
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>

0 commit comments

Comments
 (0)