Skip to content
Open
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
Binary file added apps/web/public/backgrounds/alpine.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/backgrounds/aurora.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/backgrounds/coastline.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/backgrounds/dune.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/backgrounds/ember.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/backgrounds/fjord.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/backgrounds/forest-lake.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/backgrounds/grove.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/backgrounds/highlands.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/backgrounds/iris.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/backgrounds/meadow.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/backgrounds/nightfall.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/backgrounds/ocean.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/backgrounds/t3-chat.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/public/backgrounds/terraces.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
98 changes: 98 additions & 0 deletions apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
DEFAULT_UNIFIED_SETTINGS,
type DiffLayout,
type EnvironmentIdentificationMode,
type ThemeBackgroundChoice,
MAX_APPEARANCE_CONTRAST,
MAX_CODE_FONT_SIZE,
MAX_GLASS_OPACITY,
Expand All @@ -32,6 +33,7 @@ import {
MAX_PROMPT_FONT_SIZE,
MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS,
MAX_TERMINAL_FONT_SIZE,
MAX_THEME_BACKGROUND_TRANSPARENCY,
MIN_CODE_FONT_SIZE,
MIN_APPEARANCE_CONTRAST,
MIN_GLASS_OPACITY,
Expand Down Expand Up @@ -63,6 +65,7 @@ import {
useEnvironmentStageLabel,
} from "../SidebarStageBackdrop";
import { isElectron } from "../../env";
import { THEME_BACKGROUND_CHOICES, THEME_BACKGROUND_LABELS } from "../../themeBackground";
import { buildHostedChannelSelectionUrl, type HostedAppChannel } from "../../hostedPairing";
import { useCustomThemes } from "../../hooks/useCustomThemes";
import {
Expand Down Expand Up @@ -1292,6 +1295,101 @@ export function AppearanceSettingsPanel() {
}
/>

<SettingsRow
{...searchableSetting("setting-theme-background")}
description="Show a dimmed scenic backdrop behind the interface. Theme scene follows the active theme; a picked scene stays across theme changes."
resetAction={
settings.themeBackground !== DEFAULT_UNIFIED_SETTINGS.themeBackground ? (
<SettingResetButton
label="background scene"
onClick={() =>
updateSettings({
themeBackground: DEFAULT_UNIFIED_SETTINGS.themeBackground,
})
}
/>
) : null
}
control={
<Select
value={settings.themeBackground}
onValueChange={(value) => {
if (THEME_BACKGROUND_CHOICES.includes(value as ThemeBackgroundChoice)) {
updateSettings({ themeBackground: value as ThemeBackgroundChoice });
}
}}
>
<SelectTrigger className="w-full sm:w-40" aria-label="Background scene">
<SelectValue>{THEME_BACKGROUND_LABELS[settings.themeBackground]}</SelectValue>
</SelectTrigger>
<SelectPopup align="end" alignItemWithTrigger={false}>
{THEME_BACKGROUND_CHOICES.map((choice) => (
<SelectItem hideIndicator key={choice} value={choice}>
{THEME_BACKGROUND_LABELS[choice]}
</SelectItem>
))}
</SelectPopup>
</Select>
}
/>

<SettingsRow
{...searchableSetting("setting-theme-background-transparency")}
description="How much of the background scene shows through the interface. Higher values are more transparent."
resetAction={
settings.themeBackgroundTransparency !==
DEFAULT_UNIFIED_SETTINGS.themeBackgroundTransparency ? (
<SettingResetButton
label="background transparency"
onClick={() =>
updateSettings({
themeBackgroundTransparency:
DEFAULT_UNIFIED_SETTINGS.themeBackgroundTransparency,
})
}
/>
) : null
}
control={
<div className="flex w-full items-center gap-3 sm:w-52">
<output
className="min-w-12 rounded-md bg-muted px-2 py-1 text-center font-mono text-xs font-medium tabular-nums text-foreground"
htmlFor="theme-background-transparency"
>
{settings.themeBackgroundTransparency}%
</output>
<input
aria-label="Background transparency"
className="settings-slider min-w-0 flex-1"
id="theme-background-transparency"
max={MAX_THEME_BACKGROUND_TRANSPARENCY}
min={0}
onChange={(event) => {
const transparency = Number(event.currentTarget.value);
if (
Number.isInteger(transparency) &&
transparency >= 0 &&
transparency <= MAX_THEME_BACKGROUND_TRANSPARENCY
) {
updateSettings({ themeBackgroundTransparency: transparency });
}
}}
step={5}
style={
{
"--settings-slider-progress": `${settings.themeBackgroundTransparency}%`,
"--settings-slider-fill-offset": `${
0.5 - settings.themeBackgroundTransparency / 100
}rem`,
} as CSSProperties
}
type="range"
value={settings.themeBackgroundTransparency}
/>
</div>
}
/>

{showEnvironmentIdentification ? (
<SettingsRow
{...searchableSetting("environment-identification")}
Expand Down
12 changes: 12 additions & 0 deletions apps/web/src/components/settings/settingsSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,18 @@ export const SETTINGS_SEARCH_ITEMS = [
to: "/settings/appearance",
searchTerms: ["transparent transparency solid menus dialogs composer"],
},
{
id: "setting-theme-background",
title: "Background scene",
to: "/settings/appearance",
searchTerms: ["wallpaper backdrop scenery image dimmed tinted theme"],
},
{
id: "setting-theme-background-transparency",
title: "Background transparency",
to: "/settings/appearance",
searchTerms: ["scene opacity see-through wallpaper backdrop"],
},
{
id: "diff-color-scheme",
title: "Diff colors",
Expand Down
14 changes: 12 additions & 2 deletions apps/web/src/hooks/useTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,18 +294,28 @@ function resolveBrowserChromeSurface(): HTMLElement {
export function syncBrowserChromeTheme() {
if (typeof document === "undefined" || typeof getComputedStyle === "undefined") return;
const rootStyles = getComputedStyle(document.documentElement);
// With a background scene active, the scene's solid canvas tint is the
// stable chrome color: the live surface colors are translucent glass and
// would hand the OS window frame a semi-transparent fill.
const backdropTint =
document.documentElement.dataset.appBackdrop === "on"
? normalizeThemeColor(rootStyles.getPropertyValue("--app-backdrop-tint"))
: null;
const themeChromeColor = document.documentElement.dataset.themeId
? normalizeThemeColor(rootStyles.getPropertyValue("--app-chrome-background"))
: null;
const surfaceColor = normalizeThemeColor(
getComputedStyle(resolveBrowserChromeSurface()).backgroundColor,
);
const fallbackColor = normalizeThemeColor(getComputedStyle(document.body).backgroundColor);
const backgroundColor = themeChromeColor ?? surfaceColor ?? fallbackColor;
const backgroundColor = backdropTint ?? themeChromeColor ?? surfaceColor ?? fallbackColor;
if (!backgroundColor) return;

document.documentElement.style.backgroundColor = backgroundColor;
document.body.style.backgroundColor = backgroundColor;
// With a background scene active the body must stay clear: the fixed scene
// layer paints beneath it, and an opaque body fill would hide the scene.
document.body.style.backgroundColor =
document.documentElement.dataset.appBackdrop === "on" ? "transparent" : backgroundColor;
// Update every theme-color meta so any element another layer added (for
// example a media-scoped one) carries the resolved color too.
const themeColorMetas = document.querySelectorAll<HTMLMetaElement>(
Expand Down
98 changes: 98 additions & 0 deletions apps/web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -2222,3 +2222,101 @@ code {
.composer-tiptap li[data-checked] > div > p {
margin: 0;
}

/* ── Theme background scenes ──
With a scene active, a fixed dimmed image paints under every surface and
the main canvas fills re-derive from the theme's solid colors at partial
alpha, so the interface reads as tinted glass over the scene. The solid
colors arrive as --app-backdrop-tint* inline vars, captured while the
backdrop was off. Large surfaces get no backdrop-filter: one blurred
layer that big repaints on every streaming frame. Terminal and code keep
their own fills. These rules sit unlayered so they outrank the theme
blocks in the components layer while active. */
html[data-app-backdrop="on"][data-app-backdrop="on"] {
--backdrop-surface-opacity: calc(100% - var(--backdrop-transparency, 20%));
--backdrop-dim: 20%;
--app-chrome-background: var(--background);
--toolbar-background: color-mix(
in srgb,
var(--app-backdrop-tint-toolbar, var(--app-backdrop-tint)) var(--backdrop-surface-opacity),
transparent
);
--terminal-background: var(--app-backdrop-tint);

@variant dark {
/* Dark canvases leave less contrast headroom, so the scene stays subtle:
a stronger veil rather than a different surface alpha. */
--backdrop-dim: 60%;
}
}

/* Form controls keep a solid fill: a see-through switch thumb or input on
top of the glass would lose its shape. */
html[data-app-backdrop="on"][data-app-backdrop="on"]
:is(
[data-slot="switch-thumb"],
[data-slot="checkbox"],
[data-slot="radio"],
[data-slot="input-control"],
[data-slot="textarea-control"],
[data-slot="select-trigger"],
[data-slot="number-field-group"],
[data-slot="input-group"],
[data-slot="toggle"]
) {
--background: var(--app-backdrop-tint);
}

/* The workspace nests several containers that all paint bg-background; with
translucent fills they would stack into near-opaque layers. Only the
outermost one keeps the glass fill. */
html[data-app-backdrop="on"][data-app-backdrop="on"]
.bg-background
.bg-background:not(
[data-slot="switch-thumb"],
[data-slot="checkbox"],
[data-slot="radio"],
[data-slot="input-control"],
[data-slot="textarea-control"],
[data-slot="select-trigger"],
[data-slot="number-field-group"],
[data-slot="input-group"],
[data-slot="toggle"]
) {
background-color: transparent;
}

html[data-app-backdrop="on"][data-app-backdrop="on"],
html[data-app-backdrop="on"][data-app-backdrop="on"] [data-app-sidebar] {
--background: color-mix(
in srgb,
var(--app-backdrop-tint) var(--backdrop-surface-opacity),
transparent
);
--sidebar: color-mix(
in srgb,
var(--app-backdrop-tint-sidebar, var(--app-backdrop-tint)) var(--backdrop-surface-opacity),
transparent
);
--sidebar-stage-fade: var(--sidebar);
}

/* The scene must sit under every surface but above the html fallback fill,
so the body itself stays transparent (see syncBrowserChromeTheme). */
html[data-app-backdrop="on"][data-app-backdrop="on"] body {
background: transparent;
}

html[data-app-backdrop="on"][data-app-backdrop="on"] body::before {
content: "";
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
background:
linear-gradient(
color-mix(in srgb, var(--app-backdrop-tint) var(--backdrop-dim), transparent),
color-mix(in srgb, var(--app-backdrop-tint) var(--backdrop-dim), transparent)
),
var(--app-backdrop-image) center / cover no-repeat;
}
40 changes: 39 additions & 1 deletion apps/web/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ import {
} from "../components/ui/toast";
import { resolveAndPersistPreferredEditor } from "../editorPreferences";
import { applyAppearanceFontVariables } from "~/appearanceFonts";
import { getThemeDefinition, resolveThemeHalf } from "../themePalette";
import { applyThemeBackground, resolveThemeBackgroundUrl } from "../themeBackground";
import { applyAppearanceContrast } from "~/appearanceContrast";
import { useClientSettings } from "../hooks/useSettings";
import { PlanAgentSelectionHeal } from "../planAgentSelectionHeal";
Expand All @@ -53,7 +55,7 @@ import {
selectProjectGroupingSettings,
} from "../logicalProject";
import { useUiStateStore } from "../uiStateStore";
import { syncBrowserChromeTheme } from "../hooks/useTheme";
import { syncBrowserChromeTheme, useTheme } from "../hooks/useTheme";
import { configureClientTracing } from "../observability/clientTracing";
import { resolveInitialServerAuthGateState } from "../environments/primary";
import { hasHostedPairingRequest, isHostedStaticApp } from "../hostedPairing";
Expand Down Expand Up @@ -172,6 +174,7 @@ function RootRouteView() {
<ContrastAppearanceSync />
<EnvironmentThemeSync />
<GlassAppearanceSync />
<ThemeBackgroundSync />
<FontAppearanceSync />
<CustomSnoozeDialogHost />
<CommandPalette>
Expand Down Expand Up @@ -212,6 +215,7 @@ function RootRouteView() {
<ContrastAppearanceSync />
<EnvironmentThemeSync />
<GlassAppearanceSync />
<ThemeBackgroundSync />
<FontAppearanceSync />
<FirstRunGate
enabled={primaryEnvironmentAuthenticated}
Expand Down Expand Up @@ -284,6 +288,40 @@ function GlassAppearanceSync() {
return null;
}

/**
* Keep the scene layer in step with the backdrop setting and the active
* theme. Runs after the useTheme effect inside this component has applied
* the palette, so the solid tints captured below reflect the current theme.
*/
function ThemeBackgroundSync() {
const themeBackground = useClientSettings((settings) => settings.themeBackground);
const themeBackgroundTransparency = useClientSettings(
(settings) => settings.themeBackgroundTransparency,
);
const { theme, resolvedTheme, themeHalves } = useTheme();

useEffect(() => {
const definition = getThemeDefinition(resolveThemeHalf(theme, themeHalves, resolvedTheme));
applyThemeBackground(
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
resolveThemeBackgroundUrl(themeBackground, definition?.id ?? null),
definition,
resolvedTheme,
);
// The OS window frame and theme-color meta follow the scene tint while a
// scene is on and the theme surface once it is off.
syncBrowserChromeTheme();
}, [themeBackground, theme, resolvedTheme, themeHalves]);

useEffect(() => {
document.documentElement.style.setProperty(
"--backdrop-transparency",
`${themeBackgroundTransparency}%`,
);
}, [themeBackgroundTransparency]);

return null;
}

function FontAppearanceSync() {
const fontFamilySans = useClientSettings((settings) => settings.fontFamilySans);
const fontFamilyCode = useClientSettings((settings) => settings.fontFamilyCode);
Expand Down
Loading
Loading