From 70a2f61dc26b8076f90a54f292050a9f5684220e Mon Sep 17 00:00:00 2001 From: Manoel Date: Sun, 1 Nov 2026 21:12:25 -0300 Subject: [PATCH 1/6] feat(web): add chat wallpaper setting Let users pick a device-local image for the conversation background, with opacity and blur controls. The empty timeline placeholder no longer paints an opaque background, so the wallpaper also shows on new-thread drafts. Co-Authored-By: Claude Opus 5.5 --- .../settings/DesktopClientSettings.test.ts | 3 + apps/web/src/components/ChatView.tsx | 4 +- .../chat/ChatTimelineBackground.tsx | 23 ++++ .../src/components/chat/MessagesTimeline.tsx | 2 +- .../settings/AppearanceSettingsPanel.tsx | 3 + .../components/settings/SettingsPanels.tsx | 8 ++ .../settings/TimelineBackgroundSettings.tsx | 119 ++++++++++++++++++ .../src/components/settings/settingsSearch.ts | 6 + docs/user/appearance.md | 4 + packages/contracts/src/settings.test.ts | 28 +++++ packages/contracts/src/settings.ts | 19 +++ 11 files changed, 217 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/components/chat/ChatTimelineBackground.tsx create mode 100644 apps/web/src/components/settings/TimelineBackgroundSettings.tsx diff --git a/apps/desktop/src/settings/DesktopClientSettings.test.ts b/apps/desktop/src/settings/DesktopClientSettings.test.ts index ec5062f9105bf..88b0efd7099cb 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.test.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.test.ts @@ -21,6 +21,9 @@ const clientSettings: ClientSettings = { notificationMode: "notifications-and-sound", inAppNotificationsEnabled: true, appearanceContrast: 100, + timelineBackgroundImage: "", + timelineBackgroundOpacity: 20, + timelineBackgroundBlur: 0, browserDefaultViewport: { _tag: "preset", width: 1024, height: 600, presetId: "nest-hub" }, browserDefaultZoomFactor: 1.25, browserDefaultAppearance: "dark", diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 6cd1c7775d96b..32d0d90b82df8 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -1,4 +1,5 @@ import { ChildThreadApprovals } from "./chat/ChildThreadApprovals"; +import { ChatTimelineBackground } from "./chat/ChatTimelineBackground"; import { workspaceAreaProps } from "../lib/workspaceFocus"; import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; import { resolveNewThreadModelSelection } from "@t3tools/shared/usageLimits"; @@ -3383,8 +3384,9 @@ export default function ChatView(props: ChatViewProps) { />
+ settings.timelineBackgroundImage); + const opacity = useClientSettings((settings) => settings.timelineBackgroundOpacity); + const blur = useClientSettings((settings) => settings.timelineBackgroundBlur); + if (!image || opacity === 0) return null; + return ( + + ); +} diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 4c28163766058..88c7ec2614a1c 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -1283,7 +1283,7 @@ const ConversationTimeline = memo(function ConversationTimeline({ return ( <> {activityAnnouncer} -
+
); } diff --git a/apps/web/src/components/settings/AppearanceSettingsPanel.tsx b/apps/web/src/components/settings/AppearanceSettingsPanel.tsx index 00783b085d609..797524c4465ea 100644 --- a/apps/web/src/components/settings/AppearanceSettingsPanel.tsx +++ b/apps/web/src/components/settings/AppearanceSettingsPanel.tsx @@ -1,3 +1,4 @@ +import { TimelineBackgroundSettings } from "./TimelineBackgroundSettings"; import type { CSSProperties } from "react"; import { useState } from "react"; import { @@ -162,6 +163,8 @@ export function AppearanceSettingsPanel() {
+ + void) { ...(settings.appearanceContrast !== DEFAULT_UNIFIED_SETTINGS.appearanceContrast ? ["Contrast"] : []), + ...(settings.timelineBackgroundImage !== DEFAULT_UNIFIED_SETTINGS.timelineBackgroundImage || + settings.timelineBackgroundOpacity !== DEFAULT_UNIFIED_SETTINGS.timelineBackgroundOpacity || + settings.timelineBackgroundBlur !== DEFAULT_UNIFIED_SETTINGS.timelineBackgroundBlur + ? ["Wallpaper"] + : []), ...(settings.glassOpacity !== DEFAULT_UNIFIED_SETTINGS.glassOpacity ? ["Glass opacity"] : []), ...(settings.diffColorScheme !== DEFAULT_UNIFIED_SETTINGS.diffColorScheme ? ["Diff colors"] @@ -625,6 +630,9 @@ export function useSettingsRestore(onRestored?: () => void) { } updateSettings({ appearanceContrast: DEFAULT_UNIFIED_SETTINGS.appearanceContrast, + timelineBackgroundImage: DEFAULT_UNIFIED_SETTINGS.timelineBackgroundImage, + timelineBackgroundOpacity: DEFAULT_UNIFIED_SETTINGS.timelineBackgroundOpacity, + timelineBackgroundBlur: DEFAULT_UNIFIED_SETTINGS.timelineBackgroundBlur, diffColorScheme: DEFAULT_UNIFIED_SETTINGS.diffColorScheme, chatWidth: DEFAULT_UNIFIED_SETTINGS.chatWidth, effortDisplay: DEFAULT_UNIFIED_SETTINGS.effortDisplay, diff --git a/apps/web/src/components/settings/TimelineBackgroundSettings.tsx b/apps/web/src/components/settings/TimelineBackgroundSettings.tsx new file mode 100644 index 0000000000000..3d123cabb4553 --- /dev/null +++ b/apps/web/src/components/settings/TimelineBackgroundSettings.tsx @@ -0,0 +1,119 @@ +import { useRef, useState } from "react"; +import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings"; +import { + persistClientSettingsUpdate, + useClientSettings, + useUpdatePrimarySettings, +} from "../../hooks/useSettings"; +import { compressImageForStash } from "../../lib/imageCompression"; +import { Button } from "../ui/button"; +import { SettingsRow, SettingsSection } from "./settingsLayout"; + +export function TimelineBackgroundSettings() { + const image = useClientSettings((settings) => settings.timelineBackgroundImage); + const opacity = useClientSettings((settings) => settings.timelineBackgroundOpacity); + const blur = useClientSettings((settings) => settings.timelineBackgroundBlur); + const updateSettings = useUpdatePrimarySettings(); + const fileInput = useRef(null); + const [error, setError] = useState(""); + const [busy, setBusy] = useState(false); + async function upload(file: File) { + setBusy(true); + setError(""); + try { + const result = await compressImageForStash(file); + if (!result.ok) throw new Error("Choose a readable image no larger than 50 MB."); + await persistClientSettingsUpdate((settings) => ({ + ...settings, + timelineBackgroundImage: result.image.dataUrl, + })); + } catch (cause) { + setError(cause instanceof Error ? cause.message : "Could not save wallpaper."); + } finally { + setBusy(false); + } + } + return ( + + + + {image ? ( + + ) : null} +
+ } + /> + { + const file = event.currentTarget.files?.[0]; + event.currentTarget.value = ""; + if (file) void upload(file); + }} + /> + {error ?

{error}

: null} + {image ? ( + <> + Current wallpaper + + updateSettings({ timelineBackgroundOpacity: Number(event.currentTarget.value) }) + } + /> + } + /> + + updateSettings({ timelineBackgroundBlur: Number(event.currentTarget.value) }) + } + /> + } + /> + + ) : null} + + ); +} diff --git a/apps/web/src/components/settings/settingsSearch.ts b/apps/web/src/components/settings/settingsSearch.ts index d28e467b0cf70..fa8cdb6a09ed3 100644 --- a/apps/web/src/components/settings/settingsSearch.ts +++ b/apps/web/src/components/settings/settingsSearch.ts @@ -309,6 +309,12 @@ export const SETTINGS_SEARCH_ITEMS = [ searchTerms: ["appearance colors palette custom import"], targetId: "appearance", }, + { + id: "appearance-wallpaper", + title: "Chat wallpaper", + to: "/settings/appearance", + searchTerms: ["background", "image", "opacity", "blur"], + }, { // Prefixed because the slider control already owns the `appearance-contrast` id. id: "setting-appearance-contrast", diff --git a/docs/user/appearance.md b/docs/user/appearance.md index 49d8e02d79a82..ad4d2d5f835e1 100644 --- a/docs/user/appearance.md +++ b/docs/user/appearance.md @@ -48,6 +48,10 @@ The main sidebar, right panel, and terminal drawer open and close immediately by in your operating system. Moving between threads always snaps to the selected thread's panel state without replaying its transitions. +In **Settings → Appearance → Wallpaper**, upload an image for the chat background. +Adjust opacity and blur, or choose **Remove** to restore the plain background. +Wallpaper is saved on this device and is independent of the selected theme. + ## Custom themes On web and desktop, choose **Create theme** to adjust a palette, or import a T3 Code or VS Code diff --git a/packages/contracts/src/settings.test.ts b/packages/contracts/src/settings.test.ts index 859644a54c72f..b75c09caef3c2 100644 --- a/packages/contracts/src/settings.test.ts +++ b/packages/contracts/src/settings.test.ts @@ -484,3 +484,31 @@ describe("server model preferences", () => { expect(decodeServerSettingsPatch(input)).toEqual(input); }); }); + +describe("chat wallpaper settings", () => { + it("defaults older clients to no wallpaper and round-trips image controls", () => { + expect(decodeClientSettings({})).toMatchObject({ + timelineBackgroundImage: "", + timelineBackgroundOpacity: 20, + timelineBackgroundBlur: 0, + }); + const patch = { + timelineBackgroundImage: "data:image/png;base64,aGVsbG8=", + timelineBackgroundOpacity: 35, + timelineBackgroundBlur: 2, + }; + expect(decodeClientSettingsPatch(patch)).toEqual(patch); + expect(decodeClientSettings(encodeClientSettings(decodeClientSettings(patch)))).toMatchObject( + patch, + ); + for (const invalid of [ + { timelineBackgroundOpacity: -1 }, + { timelineBackgroundOpacity: 101 }, + { timelineBackgroundOpacity: 0.5 }, + { timelineBackgroundBlur: 31 }, + { timelineBackgroundImage: "x".repeat(1_500_001) }, + ]) { + expect(() => decodeClientSettingsPatch(invalid)).toThrow(); + } + }); +}); diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index e90b977fae32d..d43e74f008f83 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -140,6 +140,13 @@ export const AppearanceContrast = Schema.Int.check( ); export type AppearanceContrast = typeof AppearanceContrast.Type; const DEFAULT_APPEARANCE_CONTRAST: AppearanceContrast = 100; +export const TimelineBackgroundImage = Schema.String.check(Schema.isMaxLength(1_500_000)); +export const TimelineBackgroundOpacity = Schema.Int.check( + Schema.isBetween({ minimum: 0, maximum: 100 }), +); +export const TimelineBackgroundBlur = Schema.Int.check( + Schema.isBetween({ minimum: 0, maximum: 30 }), +); export const MIN_PANEL_ANIMATION_DURATION_MS = 0; export const MAX_PANEL_ANIMATION_DURATION_MS = 400; export const PanelAnimationDurationMs = Schema.Int.check( @@ -363,6 +370,15 @@ export const ClientSettingsSchema = Schema.Struct({ appearanceContrast: AppearanceContrast.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_APPEARANCE_CONTRAST)), ), + timelineBackgroundImage: TimelineBackgroundImage.pipe( + Schema.withDecodingDefault(Effect.succeed("")), + ), + timelineBackgroundOpacity: TimelineBackgroundOpacity.pipe( + Schema.withDecodingDefault(Effect.succeed(20)), + ), + timelineBackgroundBlur: TimelineBackgroundBlur.pipe( + Schema.withDecodingDefault(Effect.succeed(0)), + ), // Zero avoids per-frame layout work from width/height transitions. panelAnimationDurationMs: PanelAnimationDurationMs.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_PANEL_ANIMATION_DURATION_MS)), @@ -1747,6 +1763,9 @@ export const ClientSettingsPatch = Schema.Struct({ loadBalancingEnabled: Schema.optionalKey(Schema.Boolean), loadBalancingWeights: Schema.optionalKey(LoadBalancingWeights), appearanceContrast: Schema.optionalKey(AppearanceContrast), + timelineBackgroundImage: Schema.optionalKey(TimelineBackgroundImage), + timelineBackgroundOpacity: Schema.optionalKey(TimelineBackgroundOpacity), + timelineBackgroundBlur: Schema.optionalKey(TimelineBackgroundBlur), panelAnimationDurationMs: Schema.optionalKey(PanelAnimationDurationMs), browserDefaultViewport: Schema.optionalKey(PreviewViewportSetting), browserDefaultZoomFactor: Schema.optionalKey(PreviewZoomFactor), From 3ffd653128c3a5092c51a716c1efad1634dd43ea Mon Sep 17 00:00:00 2001 From: Manoel Date: Sun, 1 Nov 2026 22:27:18 -0300 Subject: [PATCH 2/6] fix(web): validate wallpaper uploads and settle slider writes Decode each image and fit it to the attachment edge before saving, so a corrupt or oversized file cannot replace a working wallpaper. Sliders now preview locally and persist once the drag settles. Co-Authored-By: Claude Opus 5.5 --- .../settings/TimelineBackgroundSettings.tsx | 64 +++++++++++++++---- 1 file changed, 52 insertions(+), 12 deletions(-) diff --git a/apps/web/src/components/settings/TimelineBackgroundSettings.tsx b/apps/web/src/components/settings/TimelineBackgroundSettings.tsx index 3d123cabb4553..66e371c851b0e 100644 --- a/apps/web/src/components/settings/TimelineBackgroundSettings.tsx +++ b/apps/web/src/components/settings/TimelineBackgroundSettings.tsx @@ -5,10 +5,30 @@ import { useClientSettings, useUpdatePrimarySettings, } from "../../hooks/useSettings"; -import { compressImageForStash } from "../../lib/imageCompression"; +import { + compressImageToByteLimit, + MAX_STASH_IMAGE_DATA_URL_CHARS, + readFileAsDataUrl, +} from "../../lib/imageCompression"; import { Button } from "../ui/button"; import { SettingsRow, SettingsSection } from "./settingsLayout"; +// Base64 stores 3 bytes in every 4 characters. +const MAX_WALLPAPER_BYTES = Math.floor(MAX_STASH_IMAGE_DATA_URL_CHARS / 4) * 3; + +/** Fits the image to the attachment edge and budget, and rejects data that cannot render. */ +async function prepareWallpaper(file: File): Promise { + const result = await compressImageToByteLimit(file, MAX_WALLPAPER_BYTES); + if (!result.ok) throw new Error("Choose a readable image no larger than 50 MB."); + const dataUrl = await readFileAsDataUrl(result.file); + const decoded = new Image(); + decoded.src = dataUrl; + await decoded.decode().catch(() => { + throw new Error("Choose a readable image no larger than 50 MB."); + }); + return dataUrl; +} + export function TimelineBackgroundSettings() { const image = useClientSettings((settings) => settings.timelineBackgroundImage); const opacity = useClientSettings((settings) => settings.timelineBackgroundOpacity); @@ -17,15 +37,29 @@ export function TimelineBackgroundSettings() { const fileInput = useRef(null); const [error, setError] = useState(""); const [busy, setBusy] = useState(false); + // Settings writes serialize the whole image, so a drag only persists once it settles. + const [opacityDraft, setOpacityDraft] = useState(null); + const [blurDraft, setBlurDraft] = useState(null); + const shownOpacity = opacityDraft ?? opacity; + const shownBlur = blurDraft ?? blur; + function settleOpacity() { + if (opacityDraft === null) return; + updateSettings({ timelineBackgroundOpacity: opacityDraft }); + setOpacityDraft(null); + } + function settleBlur() { + if (blurDraft === null) return; + updateSettings({ timelineBackgroundBlur: blurDraft }); + setBlurDraft(null); + } async function upload(file: File) { setBusy(true); setError(""); try { - const result = await compressImageForStash(file); - if (!result.ok) throw new Error("Choose a readable image no larger than 50 MB."); + const timelineBackgroundImage = await prepareWallpaper(file); await persistClientSettingsUpdate((settings) => ({ ...settings, - timelineBackgroundImage: result.image.dataUrl, + timelineBackgroundImage, })); } catch (cause) { setError(cause instanceof Error ? cause.message : "Could not save wallpaper."); @@ -79,6 +113,10 @@ export function TimelineBackgroundSettings() { src={image} alt="Current wallpaper" className="max-h-48 w-full rounded-lg object-cover" + style={{ + opacity: shownOpacity / 100, + filter: shownBlur > 0 ? `blur(${shownBlur}px)` : undefined, + }} /> - updateSettings({ timelineBackgroundOpacity: Number(event.currentTarget.value) }) - } + value={shownOpacity} + onChange={(event) => setOpacityDraft(Number(event.currentTarget.value))} + onPointerUp={settleOpacity} + onKeyUp={settleOpacity} + onBlur={settleOpacity} /> } /> @@ -105,10 +144,11 @@ export function TimelineBackgroundSettings() { type="range" min={0} max={30} - value={blur} - onChange={(event) => - updateSettings({ timelineBackgroundBlur: Number(event.currentTarget.value) }) - } + value={shownBlur} + onChange={(event) => setBlurDraft(Number(event.currentTarget.value))} + onPointerUp={settleBlur} + onKeyUp={settleBlur} + onBlur={settleBlur} /> } /> From 708ccde7ffea444fd156a7dd6b3d76903fd550dc Mon Sep 17 00:00:00 2001 From: Manoel Date: Mon, 2 Nov 2026 07:46:54 -0300 Subject: [PATCH 3/6] fix(web): keep wallpaper settings compilable by React Compiler React Compiler cannot lower try/finally, so the upload error handling moves to a module-level helper. Co-Authored-By: Claude Opus 5.5 --- .../settings/TimelineBackgroundSettings.tsx | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/apps/web/src/components/settings/TimelineBackgroundSettings.tsx b/apps/web/src/components/settings/TimelineBackgroundSettings.tsx index 66e371c851b0e..bbd0c0ac27241 100644 --- a/apps/web/src/components/settings/TimelineBackgroundSettings.tsx +++ b/apps/web/src/components/settings/TimelineBackgroundSettings.tsx @@ -29,6 +29,20 @@ async function prepareWallpaper(file: File): Promise { return dataUrl; } +/** Saves the image as the wallpaper; returns an error message, or "" on success. */ +async function saveWallpaper(file: File): Promise { + try { + const timelineBackgroundImage = await prepareWallpaper(file); + await persistClientSettingsUpdate((settings) => ({ + ...settings, + timelineBackgroundImage, + })); + return ""; + } catch (cause) { + return cause instanceof Error ? cause.message : "Could not save wallpaper."; + } +} + export function TimelineBackgroundSettings() { const image = useClientSettings((settings) => settings.timelineBackgroundImage); const opacity = useClientSettings((settings) => settings.timelineBackgroundOpacity); @@ -55,17 +69,8 @@ export function TimelineBackgroundSettings() { async function upload(file: File) { setBusy(true); setError(""); - try { - const timelineBackgroundImage = await prepareWallpaper(file); - await persistClientSettingsUpdate((settings) => ({ - ...settings, - timelineBackgroundImage, - })); - } catch (cause) { - setError(cause instanceof Error ? cause.message : "Could not save wallpaper."); - } finally { - setBusy(false); - } + setError(await saveWallpaper(file)); + setBusy(false); } return ( From f96e3be0475ca1e3b65d90a7a34fb2cac1573857 Mon Sep 17 00:00:00 2001 From: Manoel Date: Mon, 2 Nov 2026 08:14:48 -0300 Subject: [PATCH 4/6] fix(web): reserve the data URL prefix in the wallpaper budget Co-Authored-By: Claude Opus 5.5 --- .../src/components/settings/TimelineBackgroundSettings.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/settings/TimelineBackgroundSettings.tsx b/apps/web/src/components/settings/TimelineBackgroundSettings.tsx index bbd0c0ac27241..957c40baa3c02 100644 --- a/apps/web/src/components/settings/TimelineBackgroundSettings.tsx +++ b/apps/web/src/components/settings/TimelineBackgroundSettings.tsx @@ -13,8 +13,10 @@ import { import { Button } from "../ui/button"; import { SettingsRow, SettingsSection } from "./settingsLayout"; -// Base64 stores 3 bytes in every 4 characters. -const MAX_WALLPAPER_BYTES = Math.floor(MAX_STASH_IMAGE_DATA_URL_CHARS / 4) * 3; +// Reserve the longest prefix the accepted types produce; base64 stores 3 bytes in every 4 characters. +const WALLPAPER_DATA_URL_PREFIX_CHARS = "data:image/jpeg;base64,".length; +const MAX_WALLPAPER_BYTES = + Math.floor((MAX_STASH_IMAGE_DATA_URL_CHARS - WALLPAPER_DATA_URL_PREFIX_CHARS) / 4) * 3; /** Fits the image to the attachment edge and budget, and rejects data that cannot render. */ async function prepareWallpaper(file: File): Promise { From 75533b298b88f398daa2c9bae0f1722e3903372f Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Tue, 3 Nov 2026 04:13:56 +0000 Subject: [PATCH 5/6] fix(web): reject oversized wallpaper outputs --- .../src/components/settings/TimelineBackgroundSettings.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/web/src/components/settings/TimelineBackgroundSettings.tsx b/apps/web/src/components/settings/TimelineBackgroundSettings.tsx index 957c40baa3c02..841ec254bfe0f 100644 --- a/apps/web/src/components/settings/TimelineBackgroundSettings.tsx +++ b/apps/web/src/components/settings/TimelineBackgroundSettings.tsx @@ -1,4 +1,5 @@ import { useRef, useState } from "react"; +import { IMAGE_ATTACHMENT_MAX_EDGE } from "@t3tools/client-runtime/image-attachment-size"; import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings"; import { persistClientSettingsUpdate, @@ -28,6 +29,11 @@ async function prepareWallpaper(file: File): Promise { await decoded.decode().catch(() => { throw new Error("Choose a readable image no larger than 50 MB."); }); + if (Math.max(decoded.naturalWidth, decoded.naturalHeight) > IMAGE_ATTACHMENT_MAX_EDGE) { + throw new Error( + `Choose an image no larger than ${IMAGE_ATTACHMENT_MAX_EDGE} pixels on each side.`, + ); + } return dataUrl; } From a51cf48f7d3ec1637ca55dbfc3d01dd9e883a070 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Tue, 3 Nov 2026 04:26:20 +0000 Subject: [PATCH 6/6] fix(web): fill wallpaper slider tracks --- .../settings/TimelineBackgroundSettings.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/settings/TimelineBackgroundSettings.tsx b/apps/web/src/components/settings/TimelineBackgroundSettings.tsx index 841ec254bfe0f..970ecddb4ceca 100644 --- a/apps/web/src/components/settings/TimelineBackgroundSettings.tsx +++ b/apps/web/src/components/settings/TimelineBackgroundSettings.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from "react"; +import { type CSSProperties, useRef, useState } from "react"; import { IMAGE_ATTACHMENT_MAX_EDGE } from "@t3tools/client-runtime/image-attachment-size"; import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings"; import { @@ -141,6 +141,12 @@ export function TimelineBackgroundSettings() { min={0} max={100} value={shownOpacity} + style={ + { + "--settings-slider-progress": `${shownOpacity}%`, + "--settings-slider-fill-offset": `${0.5 - shownOpacity / 100}rem`, + } as CSSProperties + } onChange={(event) => setOpacityDraft(Number(event.currentTarget.value))} onPointerUp={settleOpacity} onKeyUp={settleOpacity} @@ -158,6 +164,12 @@ export function TimelineBackgroundSettings() { min={0} max={30} value={shownBlur} + style={ + { + "--settings-slider-progress": `${(shownBlur / 30) * 100}%`, + "--settings-slider-fill-offset": `${0.5 - shownBlur / 30}rem`, + } as CSSProperties + } onChange={(event) => setBlurDraft(Number(event.currentTarget.value))} onPointerUp={settleBlur} onKeyUp={settleBlur}