mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-11 03:16:23 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
506c30e590 | ||
|
|
45fd44db01 |
@@ -0,0 +1,79 @@
|
||||
import { expect, test } from "@playwright/test"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { expectAppVisible } from "../utils/waits"
|
||||
|
||||
const draftID = "draft_background_image"
|
||||
const directory = "/tmp/background-image"
|
||||
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
|
||||
const image = Buffer.from(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
|
||||
"base64",
|
||||
)
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await mockOpenCodeServer(page, {
|
||||
directory,
|
||||
project: {
|
||||
id: "proj_background_image",
|
||||
worktree: directory,
|
||||
vcs: "git",
|
||||
name: "background-image",
|
||||
time: { created: 1700000000000, updated: 1700000000000 },
|
||||
sandboxes: [],
|
||||
},
|
||||
provider: { all: [], connected: [], default: {} },
|
||||
sessions: [],
|
||||
pageMessages: () => ({ items: [] }),
|
||||
})
|
||||
await page.addInitScript(
|
||||
({ directory, draftID, server }) => {
|
||||
localStorage.setItem("opencode-theme-id", "oc-2")
|
||||
localStorage.setItem("opencode-color-scheme", "dark")
|
||||
localStorage.setItem(
|
||||
"opencode.global.dat:server",
|
||||
JSON.stringify({
|
||||
projects: { local: [{ worktree: directory, expanded: true }] },
|
||||
lastProject: { local: directory },
|
||||
}),
|
||||
)
|
||||
localStorage.setItem(
|
||||
"opencode.window.browser.dat:tabs",
|
||||
JSON.stringify([{ type: "draft", draftID, server, directory }]),
|
||||
)
|
||||
},
|
||||
{ directory, draftID, server },
|
||||
)
|
||||
await page.goto(`/new-session?draftId=${draftID}`)
|
||||
await expectAppVisible(page.locator('[data-component="composer-editor"]'))
|
||||
})
|
||||
|
||||
test("selects, restores, and removes a background image", async ({ page }) => {
|
||||
const providerTip = page.locator('[data-component="new-session-tip"][data-kind="provider"]')
|
||||
await expect(providerTip).toBeVisible()
|
||||
await page.keyboard.press("Control+,")
|
||||
const settings = page.getByTestId("settings-screen")
|
||||
await expect(settings).toBeFocused()
|
||||
await settings.getByRole("tab", { name: "Appearance", exact: true }).click()
|
||||
const chooser = page.waitForEvent("filechooser")
|
||||
await settings.getByRole("button", { name: "Choose image", exact: true }).click()
|
||||
await (await chooser).setFiles({ name: "background.png", mimeType: "image/png", buffer: image })
|
||||
await expect(settings.getByRole("button", { name: "Remove", exact: true })).toBeVisible()
|
||||
const shell = page.locator('[data-component="app-shell"]')
|
||||
await expect(shell).toHaveAttribute("data-background-image", "")
|
||||
await expect(shell).toHaveCSS("background-image", /blob:/)
|
||||
|
||||
await settings.getByRole("button", { name: "Back to app", exact: true }).click()
|
||||
await expect(settings).toBeHidden()
|
||||
await expect(page.locator('[data-component="new-session"][data-background-surface="canvas"]')).toBeVisible()
|
||||
await expect(providerTip).toBeHidden()
|
||||
await page.reload()
|
||||
await expectAppVisible(page.locator('[data-component="composer-editor"]'))
|
||||
await expect(shell).toHaveAttribute("data-background-image", "")
|
||||
|
||||
await page.keyboard.press("Control+,")
|
||||
await expect(settings).toBeFocused()
|
||||
await settings.getByRole("tab", { name: "Appearance", exact: true }).click()
|
||||
await settings.getByRole("button", { name: "Remove", exact: true }).click()
|
||||
await expect(settings.getByRole("button", { name: "Remove", exact: true })).toBeHidden()
|
||||
await expect(shell).not.toHaveAttribute("data-background-image", "")
|
||||
})
|
||||
@@ -132,6 +132,7 @@ export function ComposerEditor(props: ComposerEditorProps) {
|
||||
</Show>
|
||||
<form
|
||||
data-component="composer"
|
||||
data-background-surface="composer"
|
||||
data-dock-border-underlay={props.borderUnderlay ? "true" : undefined}
|
||||
class="group/composer relative min-h-[96px] w-full overflow-clip rounded-xl bg-v2-background-bg-base"
|
||||
classList={{
|
||||
|
||||
@@ -19,6 +19,7 @@ export function Home() {
|
||||
const scroll = createHomeScrollController(sessions.data.groups)
|
||||
return (
|
||||
<div
|
||||
data-background-surface="panel"
|
||||
class={`
|
||||
mx-2 mb-[var(--shell-bottom-inset,8px)] mt-[var(--shell-top-inset,8px)] flex min-h-0 flex-1 flex-col self-stretch overflow-hidden rounded-[10px]
|
||||
bg-v2-background-bg-base shadow-[var(--v2-elevation-raised)]
|
||||
|
||||
@@ -3,6 +3,38 @@
|
||||
@import "@opencode/ui/styles/tokens";
|
||||
@import "tw-animate-css";
|
||||
|
||||
[data-component="app-shell"][data-background-image] {
|
||||
background-image: linear-gradient(rgb(255 255 255 / 28%), rgb(255 255 255 / 28%)), var(--app-background-image);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
[data-color-scheme="dark"] [data-component="app-shell"][data-background-image] {
|
||||
background-image: linear-gradient(rgb(0 0 0 / 28%), rgb(0 0 0 / 28%)), var(--app-background-image);
|
||||
}
|
||||
|
||||
[data-component="app-shell"][data-background-image] .bg-v2-background-bg-deep {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
[data-component="app-shell"][data-background-image] [data-background-surface="panel"] {
|
||||
background-color: color-mix(in srgb, var(--v2-background-bg-base) 72%, transparent);
|
||||
}
|
||||
|
||||
[data-component="app-shell"][data-background-image] [data-background-surface="canvas"] {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
[data-component="app-shell"][data-background-image] [data-background-surface="composer"] {
|
||||
background-color: color-mix(in srgb, var(--v2-background-bg-base) 55%, transparent);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
[data-component="app-shell"][data-background-image] [data-component="new-session-tip"][data-kind="provider"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "JetBrainsMono Nerd Font Mono";
|
||||
src: url("/assets/JetBrainsMonoNerdFontMono-Regular.woff2") format("woff2");
|
||||
|
||||
@@ -55,6 +55,7 @@ export function NewSessionView(props: {
|
||||
<div class="@container relative flex flex-col min-h-0 h-full flex-1">
|
||||
<div
|
||||
data-component="new-session"
|
||||
data-background-surface="canvas"
|
||||
class="relative flex-1 min-h-0 overflow-hidden rounded-[10px] bg-v2-background-bg-base shadow-[var(--v2-elevation-raised)]"
|
||||
>
|
||||
<ComposerDropzone
|
||||
@@ -194,6 +195,7 @@ function NewSessionTips(props: { workspaceEligible: boolean; onWorkspace: () =>
|
||||
<div
|
||||
ref={setRef}
|
||||
data-component="new-session-tip"
|
||||
data-kind={displayed()}
|
||||
data-visible={tip() !== undefined}
|
||||
class="group/new-session-tip pointer-events-auto relative flex h-6 max-w-full items-center transition-[opacity,transform] duration-[250ms] ease-[cubic-bezier(0.215,0.61,0.355,1)] motion-reduce:transition-none"
|
||||
classList={{ "data-[visible=false]:animate-out fade-out slide-out-to-bottom-4": true }}
|
||||
|
||||
@@ -1028,6 +1028,13 @@ export const dict = {
|
||||
"settings.appearance.row.tabs.vertical": "Vertical",
|
||||
"settings.appearance.row.projectName.title": "Show project names",
|
||||
"settings.appearance.row.projectName.description": "Show project names in vertical tabs and the mobile tab drawer",
|
||||
"settings.appearance.row.backgroundImage.title": "Background image",
|
||||
"settings.appearance.row.backgroundImage.description": "Choose an image for the app background.",
|
||||
"settings.appearance.row.backgroundImage.choose": "Choose image",
|
||||
"settings.appearance.row.backgroundImage.remove": "Remove",
|
||||
"settings.appearance.row.backgroundImage.pickerTitle": "Choose a background image",
|
||||
"settings.appearance.row.backgroundImage.error.unsupported": "Choose a PNG, JPEG, GIF, WebP, AVIF, or BMP image.",
|
||||
"settings.appearance.row.backgroundImage.error.too-large": "Background images must be 20 MB or smaller.",
|
||||
"settings.notifications.description": "Choose when to receive notifications and hear sounds",
|
||||
"settings.shortcuts.description": "Customize shortcuts for common actions",
|
||||
"settings.servers.description": "Manage server connections",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Component } from "solid-js"
|
||||
import { Component, Show } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Button } from "@opencode/ui/button"
|
||||
import { Select } from "@opencode/ui/select"
|
||||
import { TextInput } from "@opencode/ui/text-input"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
@@ -6,6 +8,9 @@ import { ExternalLink } from "@/runtime/platform/external-link"
|
||||
import { SettingsList } from "@/settings/list"
|
||||
import { SettingsRow } from "@/settings/row"
|
||||
import { createAppearanceSettingsController, type AppearanceSettingsController } from "@/settings/general/controllers"
|
||||
import { useSettings } from "@/settings/model"
|
||||
import { BackgroundImageSelectionError } from "@/settings/appearance/background-image"
|
||||
import { showToast } from "@/shell/notifications/toast"
|
||||
import "@/settings/settings.css"
|
||||
|
||||
const schemeOptions: ("system" | "light" | "dark")[] = ["system", "light", "dark"]
|
||||
@@ -64,6 +69,26 @@ const FontSetting: Component<{
|
||||
export const SettingsAppearance: Component = () => {
|
||||
const language = useLanguage()
|
||||
const appearance = createAppearanceSettingsController()
|
||||
const settings = useSettings()
|
||||
const [state, setState] = createStore({ backgroundBusy: false })
|
||||
|
||||
const backgroundAction = async (action: () => Promise<void>) => {
|
||||
if (state.backgroundBusy) return
|
||||
setState("backgroundBusy", true)
|
||||
await action().catch((error: unknown) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("common.requestFailed"),
|
||||
description:
|
||||
error instanceof BackgroundImageSelectionError
|
||||
? language.t(`settings.appearance.row.backgroundImage.error.${error.reason}`)
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: String(error),
|
||||
})
|
||||
})
|
||||
setState("backgroundBusy", false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -121,6 +146,42 @@ export const SettingsAppearance: Component = () => {
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<Show when={settings.appearance.backgroundImage.available}>
|
||||
<SettingsRow
|
||||
title={language.t("settings.appearance.row.backgroundImage.title")}
|
||||
description={language.t("settings.appearance.row.backgroundImage.description")}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
data-action="settings-background-image"
|
||||
size="normal"
|
||||
variant="neutral"
|
||||
disabled={state.backgroundBusy}
|
||||
onClick={() =>
|
||||
void backgroundAction(() =>
|
||||
settings.appearance.backgroundImage.select(
|
||||
language.t("settings.appearance.row.backgroundImage.pickerTitle"),
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
{language.t("settings.appearance.row.backgroundImage.choose")}
|
||||
</Button>
|
||||
<Show when={settings.appearance.backgroundImage.active()}>
|
||||
<Button
|
||||
data-action="settings-background-image-remove"
|
||||
size="normal"
|
||||
variant="ghost"
|
||||
disabled={state.backgroundBusy}
|
||||
onClick={() => void backgroundAction(() => settings.appearance.backgroundImage.clear())}
|
||||
>
|
||||
{language.t("settings.appearance.row.backgroundImage.remove")}
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
</SettingsRow>
|
||||
</Show>
|
||||
|
||||
<FontSetting kind="ui" fonts={appearance.fonts} />
|
||||
<FontSetting kind="code" fonts={appearance.fonts} />
|
||||
<FontSetting kind="terminal" fonts={appearance.fonts} />
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { createDraftStore, type DraftStore } from "@/runtime/persistence/drafts"
|
||||
import type { Platform } from "@/runtime/platform/platform"
|
||||
import { createBackgroundImageSettings } from "./background-image"
|
||||
|
||||
function setup(file?: File, initial?: string) {
|
||||
let value = initial ?? null
|
||||
const written: unknown[] = []
|
||||
const draftStore: DraftStore = {
|
||||
getItem: async () => value,
|
||||
setItem: async (_key, next) => {
|
||||
value = next
|
||||
},
|
||||
removeItem: async () => {
|
||||
value = null
|
||||
},
|
||||
putBlob: async () => ({ id: "blob-id", url: "blob:background" }),
|
||||
setDocument: async (_key, document) => {
|
||||
written.push(document)
|
||||
value = JSON.stringify(document)
|
||||
},
|
||||
}
|
||||
const platform: Platform = {
|
||||
platform: "web",
|
||||
draftStore,
|
||||
openExternal() {},
|
||||
restart: async () => {},
|
||||
notify: async () => {},
|
||||
openAttachmentPickerDialog: async (_options, onFile) => {
|
||||
if (file) await onFile(file)
|
||||
},
|
||||
}
|
||||
const background = createBackgroundImageSettings(platform, false)
|
||||
return { background, written, value: () => value }
|
||||
}
|
||||
|
||||
describe("background image settings", () => {
|
||||
test("reloads an image through the document and blob store", async () => {
|
||||
const documents = new Map<string, string>()
|
||||
const blobs = new Map<string, Blob>()
|
||||
const draftStore = createDraftStore({
|
||||
get: async (key) => documents.get(key) ?? null,
|
||||
set: async (key, value) => {
|
||||
documents.set(key, value)
|
||||
return []
|
||||
},
|
||||
remove: async (key) => {
|
||||
documents.delete(key)
|
||||
},
|
||||
putBlob: async (blob) => {
|
||||
const id = crypto.randomUUID()
|
||||
blobs.set(id, blob)
|
||||
return id
|
||||
},
|
||||
getBlob: async (id) => blobs.get(id) ?? null,
|
||||
})
|
||||
const file = new File([new Uint8Array([1, 2, 3])], "background.png", { type: "image/png" })
|
||||
const value: Platform = {
|
||||
platform: "web",
|
||||
draftStore,
|
||||
openExternal() {},
|
||||
restart: async () => {},
|
||||
notify: async () => {},
|
||||
openAttachmentPickerDialog: async (_options, onFile) => {
|
||||
await onFile(file)
|
||||
},
|
||||
}
|
||||
|
||||
const first = createBackgroundImageSettings(value, false)
|
||||
await first.ready
|
||||
await first.select("Choose a background image")
|
||||
const second = createBackgroundImageSettings(value, false)
|
||||
await second.ready
|
||||
expect(second.active()).toBe(true)
|
||||
expect(second.url()).toStartWith("blob:")
|
||||
})
|
||||
|
||||
test("loads, replaces, and clears a persisted image", async () => {
|
||||
const current = JSON.stringify({ image: { mime: "image/png", blob: { id: "old", url: "blob:old" } } })
|
||||
const { background, written, value } = setup(new File([new Uint8Array([1, 2, 3])], "new.webp"), current)
|
||||
await background.ready
|
||||
expect(background.active()).toBe(true)
|
||||
expect(background.url()).toBe("blob:old")
|
||||
|
||||
await background.select("Choose a background image")
|
||||
expect(background.url()).toBe("blob:background")
|
||||
expect(written).toEqual([{ image: { mime: "image/webp", blob: { id: "blob-id", url: "blob:background" } } }])
|
||||
|
||||
await background.clear()
|
||||
expect(background.active()).toBe(false)
|
||||
expect(value()).toBeNull()
|
||||
})
|
||||
|
||||
test("rejects unsupported files", async () => {
|
||||
const { background } = setup(new File(["<svg />"], "background.svg", { type: "image/svg+xml" }))
|
||||
await background.ready
|
||||
expect(background.select("Choose a background image")).rejects.toMatchObject({
|
||||
reason: "unsupported",
|
||||
})
|
||||
})
|
||||
|
||||
test("rejects files larger than 20 MB", async () => {
|
||||
const { background } = setup(new File([new Uint8Array(20 * 1024 * 1024 + 1)], "background.png"))
|
||||
await background.ready
|
||||
expect(background.select("Choose a background image")).rejects.toMatchObject({
|
||||
reason: "too-large",
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,115 @@
|
||||
import { Option, Schema } from "effect"
|
||||
import { getOwner, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import type { Platform } from "@/runtime/platform/platform"
|
||||
|
||||
const key = "opencode.global.dat:appearance.background-image"
|
||||
const maxBytes = 20 * 1024 * 1024
|
||||
const mime = new Map([
|
||||
["avif", "image/avif"],
|
||||
["bmp", "image/bmp"],
|
||||
["gif", "image/gif"],
|
||||
["jpeg", "image/jpeg"],
|
||||
["jpg", "image/jpeg"],
|
||||
["png", "image/png"],
|
||||
["webp", "image/webp"],
|
||||
])
|
||||
const accepted = new Set(mime.values())
|
||||
const documentSchema = Schema.Struct({
|
||||
image: Schema.optional(
|
||||
Schema.Struct({
|
||||
mime: Schema.String,
|
||||
blob: Schema.Struct({ id: Schema.String, url: Schema.optional(Schema.String) }),
|
||||
}),
|
||||
),
|
||||
})
|
||||
const decode = Schema.decodeUnknownOption(Schema.fromJsonString(documentSchema))
|
||||
|
||||
export class BackgroundImageSelectionError extends Error {
|
||||
constructor(readonly reason: "unsupported" | "too-large") {
|
||||
super(reason)
|
||||
this.name = "BackgroundImageSelectionError"
|
||||
}
|
||||
}
|
||||
|
||||
export function createBackgroundImageSettings(platform: Platform, sync = true) {
|
||||
const [state, setState] = createStore<{
|
||||
image: { mime: string; blob: { id: string; url: string } } | undefined
|
||||
}>({ image: undefined })
|
||||
const channel = sync && typeof BroadcastChannel !== "undefined" ? new BroadcastChannel(key) : undefined
|
||||
let revision = 0
|
||||
|
||||
const load = async () => {
|
||||
const current = ++revision
|
||||
const raw = await platform.draftStore?.getItem(key)
|
||||
if (current !== revision || !raw) {
|
||||
if (current === revision) setState("image", undefined)
|
||||
return
|
||||
}
|
||||
const parsed = decode(raw)
|
||||
const image = Option.isSome(parsed) ? parsed.value.image : undefined
|
||||
if (current !== revision) return
|
||||
setState(
|
||||
"image",
|
||||
image?.blob.url?.startsWith("blob:") && accepted.has(image.mime)
|
||||
? { mime: image.mime, blob: { id: image.blob.id, url: image.blob.url } }
|
||||
: undefined,
|
||||
)
|
||||
}
|
||||
|
||||
channel?.addEventListener("message", () => void load().catch(() => undefined))
|
||||
if (getOwner()) onCleanup(() => channel?.close())
|
||||
const ready = load().catch(() => undefined)
|
||||
|
||||
return {
|
||||
ready,
|
||||
available: !!platform.draftStore,
|
||||
active: () => !!state.image,
|
||||
url: () => state.image?.blob.url,
|
||||
async select(title: string) {
|
||||
const file = await pick(platform, title)
|
||||
if (!file) return
|
||||
const type = file.type.toLowerCase()
|
||||
const extension = file.name.split(".").at(-1)?.toLowerCase()
|
||||
const contentType = accepted.has(type) ? type : extension ? mime.get(extension) : undefined
|
||||
if (!contentType) throw new BackgroundImageSelectionError("unsupported")
|
||||
if (file.size > maxBytes) throw new BackgroundImageSelectionError("too-large")
|
||||
const store = platform.draftStore
|
||||
if (!store) return
|
||||
revision++
|
||||
const blob = await store.putBlob(file)
|
||||
await store.setDocument(key, { image: { mime: contentType, blob } })
|
||||
revision++
|
||||
setState("image", { mime: contentType, blob })
|
||||
channel?.postMessage(null)
|
||||
},
|
||||
async clear() {
|
||||
revision++
|
||||
await platform.draftStore?.removeItem(key)
|
||||
revision++
|
||||
setState("image", undefined)
|
||||
channel?.postMessage(null)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function pick(platform: Platform, title: string) {
|
||||
if (platform.openAttachmentPickerDialog) {
|
||||
let selected: File | undefined
|
||||
await platform.openAttachmentPickerDialog(
|
||||
{ title, extensions: [...mime.keys()], accept: [...accepted] },
|
||||
async (file) => {
|
||||
selected ??= file
|
||||
},
|
||||
)
|
||||
return selected
|
||||
}
|
||||
return new Promise<File | undefined>((resolve) => {
|
||||
const input = document.createElement("input")
|
||||
input.type = "file"
|
||||
input.accept = [...accepted].join(",")
|
||||
input.addEventListener("change", () => resolve(input.files?.[0]), { once: true })
|
||||
input.addEventListener("cancel", () => resolve(undefined), { once: true })
|
||||
input.click()
|
||||
})
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import { timelinePresets, type TimelineCategory, type TimelineDetail } from "@op
|
||||
import { persisted } from "@/runtime/persistence/storage"
|
||||
import { Persistence } from "@/runtime/persistence/schema"
|
||||
import { ScopedKey, type ServerScope } from "@/runtime/server/scope"
|
||||
import { usePlatform } from "@/runtime/platform/platform"
|
||||
import { createBackgroundImageSettings } from "@/settings/appearance/background-image"
|
||||
|
||||
export type Settings = typeof settingsSchema.Type
|
||||
export type WorkspaceDefaultDestination = Settings["workspaces"]["defaultDestination"]
|
||||
@@ -277,7 +279,9 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
||||
name: "Settings",
|
||||
gate: false,
|
||||
init: () => {
|
||||
const platform = usePlatform()
|
||||
const [store, setStore, , ready] = persisted({ key: "settings.v3" }, settingsPersistence, defaultSettings)
|
||||
const backgroundImage = createBackgroundImageSettings(platform)
|
||||
const showFileTree = withFallback(() => store.general?.showFileTree, defaultSettings.general.showFileTree)
|
||||
const showSearch = withFallback(() => store.general?.showSearch, defaultSettings.general.showSearch)
|
||||
const showStatus = withFallback(() => store.general?.showStatus, defaultSettings.general.showStatus)
|
||||
@@ -375,6 +379,7 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
|
||||
customAgents: showCustomAgents,
|
||||
},
|
||||
appearance: {
|
||||
backgroundImage,
|
||||
fontSize: withFallback(() => store.appearance?.fontSize, defaultSettings.appearance.fontSize),
|
||||
setFontSize(value: number) {
|
||||
setStore("appearance", "fontSize", value)
|
||||
|
||||
@@ -40,8 +40,13 @@ export default function Layout(props: ParentProps) {
|
||||
return (
|
||||
<TitlebarRightProvider>
|
||||
<div
|
||||
data-component="app-shell"
|
||||
data-background-image={preferences.appearance.backgroundImage.active() ? "" : undefined}
|
||||
class="relative bg-v2-background-bg-deep flex-1 min-h-0 min-w-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text"
|
||||
style={{
|
||||
"--app-background-image": preferences.appearance.backgroundImage.url()
|
||||
? `url("${preferences.appearance.backgroundImage.url()}")`
|
||||
: undefined,
|
||||
// Native Windows chrome supplies the gap; retain paint clearance for the panels' outer outlines.
|
||||
"--shell-top-inset": bottomTitlebar()
|
||||
? "max(0px, calc(8px - env(safe-area-inset-top, 0px)))"
|
||||
|
||||
Reference in New Issue
Block a user