feat(appearance): background image for the project window (#3194)

Settings › Appearance › Background Image picks an image shown through the project window, with opacity (default 15%), fill (scale, original size, tile) and anchor. Drawn over the workbench under dialogs and popups without taking clicks; the settings row previews the image and shows why a path cannot be loaded.
This commit is contained in:
l0ng-ai
2026-11-13 23:19:17 +08:00
committed by GitHub
parent f3d0417f4c
commit 258ba189c6
16 changed files with 424 additions and 5 deletions
+23
View File
@@ -673,6 +673,28 @@ pub struct AppearanceSettings {
pub status_bar_widgets: BTreeMap<String, bool>,
/// Customize Main Toolbar; `None` = the default toolbar.
pub main_toolbar: Option<MainToolbarSettings>,
/// Appearance › Background Image: an image shown through the IDE window.
pub background_image: BackgroundImageSettings,
}
/// Appearance › Background Image. An empty `path` shows no image.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Type)]
#[serde(default, rename_all = "camelCase")]
pub struct BackgroundImageSettings {
pub path: String,
/// Percent, 0–100.
pub opacity: u8,
/// `scale` (cover the window) | `plain` (original size) | `tile`.
pub fill: String,
/// Where the image is anchored: `center`, `top-left`, `top`, `top-right`, `left`, `right`,
/// `bottom-left`, `bottom`, `bottom-right`.
pub anchor: String,
}
impl Default for BackgroundImageSettings {
fn default() -> Self {
Self { path: String::new(), opacity: 15, fill: "scale".into(), anchor: "center".into() }
}
}
/// Main toolbar entries per side, in order: title widget ids (`project`,
@@ -743,6 +765,7 @@ impl Default for AppearanceSettings {
navigation_bar: NavigationBarPosition::StatusBar,
status_bar_widgets: BTreeMap::new(),
main_toolbar: None,
background_image: BackgroundImageSettings::default(),
}
}
}
+1
View File
@@ -24,6 +24,7 @@ The same page also has:
- **Accent color**: the UI's accent color.
- **Different tool window background**: set tool windows apart from the editor.
- **Background Image**: an image shown through the project window, with its **Opacity**, **Fill** (scale, original size or tile) and **Anchor**.
- **Accessibility**: **Use contrast scrollbars** and **Adjust colors for red-green vision deficiency** (for protanopia and deuteranopia).
- **Compact mode** under **UI Options**.
+1
View File
@@ -109,6 +109,7 @@ Settings › Appearance & Behavior › Appearance, unless noted.
| `uiFontFallback` | `""` | **UI fallback font**. |
| `contrastScrollbars` | `false` | **Use contrast scrollbars**. |
| `redGreenVision` | `false` | **Adjust colors for red-green vision deficiency**. |
| `backgroundImage` | `opacity: 15` | **Background Image**: `path` (empty: none), `opacity` (0–100), `fill` (`scale` / `plain` / `tile`), `anchor` (`center`, `top-left`, …). |
| `compactMode` | `false` | **Compact mode**. |
| `showMainToolbar` / `showStatusBar` | `true` / `true` | **Show main toolbar**, **Show status bar**. |
| `navigationBar` | `"statusBar"` | **Navigation bar**: `top`, `statusBar` or `hidden`. |
+1
View File
@@ -24,6 +24,7 @@ description: "选择界面主题和编辑器配色方案,导入 IntelliJ 主
- **强调色**:界面的强调色。
- **不同的工具窗口背景**:让工具窗口与编辑器区分开。
- **背景图像**:透过项目窗口显示的图像,可设置 **不透明度**、**填充**(缩放、原始大小或平铺)和 **位置**。
- **辅助功能**:**使用对比度滚动条** 和 **针对红绿色觉缺陷调整颜色**(适用于红色盲和绿色盲)。
- **UI 选项** 下的 **紧凑模式**。
+1
View File
@@ -109,6 +109,7 @@ exclude = ["**/.git", "**/node_modules", "**/target", "**/dist"]
| `uiFontFallback` | `""` | **UI 备用字体**。 |
| `contrastScrollbars` | `false` | **使用对比度滚动条**。 |
| `redGreenVision` | `false` | **针对红绿色觉缺陷调整颜色**。 |
| `backgroundImage` | `opacity: 15` | **背景图像**:`path`(空为不显示)、`opacity`(0–100)、`fill`(`scale` / `plain` / `tile`)、`anchor`(`center`、`top-left` 等)。 |
| `compactMode` | `false` | **紧凑模式**。 |
| `showMainToolbar` / `showStatusBar` | `true` / `true` | **显示主工具栏**、**显示状态栏**。 |
| `navigationBar` | `"statusBar"` | **导航栏**:`top`、`statusBar` 或 `hidden`。 |
+18
View File
@@ -2392,6 +2392,8 @@ export type AppearanceSettings_Deserialize = {
statusBarWidgets?: { [key in string]: boolean },
/** Customize Main Toolbar; `None` = the default toolbar. */
mainToolbar?: MainToolbarSettings | null,
/** Appearance › Background Image: an image shown through the IDE window. */
backgroundImage?: BackgroundImageSettings,
};
export type AppearanceSettings_Serialize = {
@@ -2454,6 +2456,8 @@ export type AppearanceSettings_Serialize = {
statusBarWidgets: { [key in string]: boolean },
/** Customize Main Toolbar; `None` = the default toolbar. */
mainToolbar: MainToolbarSettings | null,
/** Appearance › Background Image: an image shown through the IDE window. */
backgroundImage: BackgroundImageSettings,
};
export type ApplyOutcome = {
@@ -2533,6 +2537,20 @@ export type AutoUpload = "never" |
/** Every save, including autosave and external changes. */
"always";
/** Appearance › Background Image. An empty `path` shows no image. */
export type BackgroundImageSettings = {
path?: string,
/** Percent, 0–100. */
opacity?: number,
/** `scale` (cover the window) | `plain` (original size) | `tile`. */
fill?: string,
/**
* Where the image is anchored: `center`, `top-left`, `top`, `top-right`, `left`, `right`,
* `bottom-left`, `bottom`, `bottom-right`.
*/
anchor?: string,
};
/** A step run before a configuration launches; a failing step aborts the launch. */
export type BeforeLaunchTask = {
/**
@@ -0,0 +1,108 @@
/** Appearance › Background Image: stored value validation and the variables the workbench draws from. */
import { afterEach, describe, expect, it, vi } from "vitest";
const ipc = vi.hoisted(() => ({
pending: new Map<string, (url: string) => void>(),
read: vi.fn(),
}));
vi.mock("@/platform/ipc", async (importOriginal) => ({
...(await importOriginal<object>()),
call: (p: Promise<string>) => p,
commands: {
fsReadImage: (path: string) => {
ipc.read(path);
return new Promise<string>((resolve) => ipc.pending.set(path, resolve));
},
},
}));
import { applyBackgroundImage } from "../appearanceRuntime";
import { validateAppearanceExtras, type BackgroundImageValid } from "../model/behaviorSchema";
const image = (patch: Partial<BackgroundImageValid> = {}): BackgroundImageValid => ({
...validateAppearanceExtras({}).backgroundImage,
path: "/pics/a.png",
...patch,
});
const flush = () => new Promise((r) => setTimeout(r, 0));
const vars = (root: HTMLElement) =>
Object.fromEntries(
["image", "opacity", "size", "repeat", "position"].map((k) => [
k,
root.style.getPropertyValue(k === "image" ? "--background-image" : `--background-image-${k}`),
]),
);
afterEach(() => {
applyBackgroundImage(image({ path: "" }), document.createElement("div"));
ipc.pending.clear();
ipc.read.mockClear();
});
describe("background image settings", () => {
it("default to no image at 15% opacity, scaled and centred", () => {
expect(validateAppearanceExtras({}).backgroundImage).toEqual({
path: "",
opacity: 15,
fill: "scale",
anchor: "center",
});
});
it("clamp opacity and drop unknown fill and anchor values", () => {
const bg = validateAppearanceExtras({
backgroundImage: { path: " /a.png ", opacity: 140, fill: "stretch", anchor: "middle" },
}).backgroundImage;
expect(bg).toEqual({ path: "/a.png", opacity: 100, fill: "scale", anchor: "center" });
expect(validateAppearanceExtras({ backgroundImage: "x" }).backgroundImage.path).toBe("");
});
});
describe("applyBackgroundImage", () => {
it("sets the layout at once and the image when it has loaded", async () => {
const root = document.createElement("div");
applyBackgroundImage(image({ opacity: 30, fill: "tile", anchor: "bottom-right" }), root);
expect(vars(root)).toEqual({
image: "",
opacity: "0.3",
size: "auto",
repeat: "repeat",
position: "bottom right",
});
ipc.pending.get("/pics/a.png")!("data:image/png;base64,AAA");
await flush();
expect(vars(root).image).toBe('url("data:image/png;base64,AAA")');
});
it("reads the file once per path; layout changes reuse the loaded image", async () => {
const root = document.createElement("div");
applyBackgroundImage(image(), root);
ipc.pending.get("/pics/a.png")!("data:a");
await flush();
applyBackgroundImage(image({ opacity: 50, fill: "plain" }), root);
expect(ipc.read).toHaveBeenCalledTimes(1);
expect(vars(root)).toMatchObject({ image: 'url("data:a")', opacity: "0.5", size: "auto", repeat: "no-repeat" });
});
it("ignores an image that finishes loading after another was picked", async () => {
const root = document.createElement("div");
applyBackgroundImage(image(), root);
applyBackgroundImage(image({ path: "/pics/b.png" }), root);
ipc.pending.get("/pics/b.png")!("data:b");
ipc.pending.get("/pics/a.png")!("data:a");
await flush();
expect(vars(root).image).toBe('url("data:b")');
});
it("clears everything without a path or at 0% opacity", async () => {
const root = document.createElement("div");
applyBackgroundImage(image(), root);
ipc.pending.get("/pics/a.png")!("data:a");
await flush();
applyBackgroundImage(image({ opacity: 0 }), root);
expect(Object.values(vars(root)).every((v) => v === "")).toBe(true);
// Shown again: the file is read again.
applyBackgroundImage(image(), root);
expect(ipc.read).toHaveBeenCalledTimes(2);
});
});
+44 -1
View File
@@ -3,7 +3,7 @@
* other feature owns: root attributes read by tokens / global CSS (compact
* mode, contrast scrollbars, red-green palette, antialiasing, menu icons…),
* the UI font, IDE zoom (+ ⌃⌥= / ⌃⌥- / ⌃⌥0 actions), "Drag-and-drop with Alt
* pressed only" and "Save files if the IDE is idle".
* pressed only", "Save files if the IDE is idle" and the background image.
*/
import { executeAction, type Action } from "@/platform/actions";
import { editorPrefs } from "@/platform/editorSettings";
@@ -13,6 +13,7 @@ import { useProject } from "@/platform/project";
import { setDragRequiresAlt } from "@/ui";
import { ZOOM_LIMITS } from "@/platform/zoom";
import { DEFAULT_SETTINGS, validateSettings, type ValidSettings } from "./model/schema";
import type { BackgroundImageValid } from "./model/behaviorSchema";
const FONT_VARS = [
["--fs-ui", 0],
@@ -52,6 +53,48 @@ export function applyAppearanceOptions(s: ValidSettings, root: HTMLElement = doc
root.dataset.aaIde = a.ideAntialiasing;
root.dataset.aaEditor = a.editorAntialiasing;
applyZoom(a.zoom, root);
applyBackgroundImage(a.backgroundImage, root);
}
// ---------------------------------------------------------------------------
// Background image (IDEA: Appearance › Background Image): drawn over the workbench
// by `Workbench.module.css` from these variables, under dialogs and popups.
const FILLS = {
scale: { size: "cover", repeat: "no-repeat" },
plain: { size: "auto", repeat: "no-repeat" },
tile: { size: "auto", repeat: "repeat" },
} as const;
const BG_VARS = ["--background-image", "--background-image-opacity", "--background-image-size"] as const;
const BG_LAYOUT_VARS = ["--background-image-repeat", "--background-image-position"] as const;
let bgPath = "";
let bgLoad = 0;
export function applyBackgroundImage(bg: BackgroundImageValid, root: HTMLElement = document.documentElement): void {
if (!bg.path || bg.opacity === 0) {
bgPath = "";
bgLoad++;
for (const name of [...BG_VARS, ...BG_LAYOUT_VARS]) root.style.removeProperty(name);
return;
}
const fill = FILLS[bg.fill];
root.style.setProperty("--background-image-opacity", String(bg.opacity / 100));
root.style.setProperty("--background-image-size", fill.size);
root.style.setProperty("--background-image-repeat", fill.repeat);
root.style.setProperty("--background-image-position", bg.anchor.replace("-", " "));
if (bg.path === bgPath) return;
bgPath = bg.path;
const load = ++bgLoad;
root.style.removeProperty("--background-image");
call(commands.fsReadImage(bg.path))
.then((url) => {
if (load === bgLoad) root.style.setProperty("--background-image", `url("${url}")`);
})
.catch((e) => {
if (load === bgLoad) console.warn("cannot load the background image", e);
});
}
/** Editor › Font › "Enable ligatures" also covers mono text outside the editor (Find results, Usages…). */
+14 -1
View File
@@ -128,6 +128,7 @@ export interface NumberInputProps {
/** Integer-only input. */
integer?: boolean;
unit?: string;
disabled?: boolean;
"aria-label"?: string;
}
@@ -143,7 +144,18 @@ export function parseNumberInput(
}
/** Number field: edits commit while valid; invalid text is flagged and reverted on blur. */
export function NumberInput({ id, value, onChange, min, max, step = 1, integer, unit, ...aria }: NumberInputProps) {
export function NumberInput({
id,
value,
onChange,
min,
max,
step = 1,
integer,
unit,
disabled,
...aria
}: NumberInputProps) {
const [draft, setDraft] = useState<string | null>(null);
const text = draft ?? String(value);
const invalid = draft !== null && parseNumberInput(draft, { min, max, integer }) === null;
@@ -155,6 +167,7 @@ export function NumberInput({ id, value, onChange, min, max, step = 1, integer,
inputMode={integer ? "numeric" : "decimal"}
value={text}
invalid={invalid}
disabled={disabled}
aria-label={aria["aria-label"]}
aria-valuemin={min}
aria-valuemax={max}
@@ -13,6 +13,28 @@ export type Antialiasing = "subpixel" | "greyscale" | "none";
export type OpenProjectIn = "ask" | "new" | "same";
export type NotificationDisplay = "balloon" | "sticky" | "none";
export type NavigationBarPosition = "top" | "statusBar" | "hidden";
export type BackgroundImageFill = "scale" | "plain" | "tile";
export const BACKGROUND_IMAGE_ANCHORS = [
"top-left",
"top",
"top-right",
"left",
"center",
"right",
"bottom-left",
"bottom",
"bottom-right",
] as const;
export type BackgroundImageAnchor = (typeof BACKGROUND_IMAGE_ANCHORS)[number];
/** Appearance › Background Image; an empty `path` shows no image. */
export interface BackgroundImageValid {
path: string;
/** Percent, 0–100. */
opacity: number;
fill: BackgroundImageFill;
anchor: BackgroundImageAnchor;
}
/** Themes selectable as "preferred" when syncing with the OS. */
export type ConcreteTheme = ThemeChoiceId;
@@ -52,6 +74,7 @@ export interface AppearanceExtras {
/** View › Appearance › Navigation Bar: the file path breadcrumbs, shown above
* the editor, in the status bar, or not at all. */
navigationBar: NavigationBarPosition;
backgroundImage: BackgroundImageValid;
}
export interface DateFormatValid {
@@ -120,6 +143,7 @@ export const DEFAULT_APPEARANCE_EXTRAS: AppearanceExtras = {
showMainToolbar: true,
showStatusBar: true,
navigationBar: "statusBar",
backgroundImage: { path: "", opacity: 15, fill: "scale", anchor: "center" },
};
const NAV_BAR_POSITIONS: readonly NavigationBarPosition[] = ["top", "statusBar", "hidden"];
@@ -192,6 +216,18 @@ export function validateAppearanceExtras(raw: unknown): AppearanceExtras {
if (!("navigationBar" in a) && typeof legacy === "boolean") return legacy ? "statusBar" : "hidden";
return oneOf(a.navigationBar, NAV_BAR_POSITIONS, d.navigationBar);
})(),
backgroundImage: validateBackgroundImage(a.backgroundImage),
};
}
function validateBackgroundImage(raw: unknown): BackgroundImageValid {
const b = obj(raw);
const d = DEFAULT_APPEARANCE_EXTRAS.backgroundImage;
return {
path: typeof b.path === "string" ? b.path.trim() : d.path,
opacity: int(b.opacity, d.opacity, { min: 0, max: 100 }),
fill: oneOf(b.fill, ["scale", "plain", "tile"], d.fill),
anchor: oneOf(b.anchor, BACKGROUND_IMAGE_ANCHORS, d.anchor),
};
}
@@ -73,6 +73,13 @@
.pathInput {
width: 320px;
}
.backgroundPreview {
height: var(--h-control);
max-width: 64px;
object-fit: cover;
border: 1px solid var(--border);
border-radius: var(--radius-small);
}
.patternInput {
width: 160px;
}
+122 -1
View File
@@ -23,6 +23,9 @@ import { useLabelMatch, useSetting } from "../components/context";
import {
IDLE_LIMITS,
type Antialiasing,
type BackgroundImageAnchor,
type BackgroundImageFill,
type BackgroundImageValid,
type ConcreteTheme,
type DateFormatValid,
type NotificationDisplay,
@@ -34,7 +37,7 @@ import { ACCENT_PRESETS, LIMITS, type ThemeId } from "../model/schema";
import type { PageSpec } from "./spec";
import { HTTP_PROXY_PAGE } from "./httpProxy";
import styles from "./Behavior.module.css";
import { LOCALES, SUPPORTED_LOCALES, tx, useT } from "@/platform/i18n";
import { LOCALES, SUPPORTED_LOCALES, tx, useT, useTx } from "@/platform/i18n";
import { BUILTIN_THEMES, useContributedIconThemes } from "@/platform/themes";
import { currentThemeChoice, originDetail, previewTheme, themeSelectOptions, useThemeChoices } from "@/workbench/theme";
import { themePreview } from "@/workbench/quickSwitch";
@@ -379,6 +382,113 @@ const NAV_BAR_OPTIONS: SelectOption<NavigationBarPosition>[] = [
{ value: "hidden", label: "Don't show" },
];
const FILL_OPTIONS: SelectOption<BackgroundImageFill>[] = [
{ value: "scale", label: "Scale" },
{ value: "plain", label: "Original size" },
{ value: "tile", label: "Tile" },
];
const ANCHOR_OPTIONS: SelectOption<BackgroundImageAnchor>[] = [
{ value: "center", label: "Center" },
{ value: "top-left", label: "Top left" },
{ value: "top", label: "Top" },
{ value: "top-right", label: "Top right" },
{ value: "left", label: "Left" },
{ value: "right", label: "Right" },
{ value: "bottom-left", label: "Bottom left" },
{ value: "bottom", label: "Bottom" },
{ value: "bottom-right", label: "Bottom right" },
];
const IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "avif"];
/** Appearance › Background Image: an image shown through the project window. */
function BackgroundImageRows() {
const [bg, setBg] = useSetting<BackgroundImageValid>("appearance.backgroundImage");
const set = (patch: Partial<BackgroundImageValid>) => setBg({ ...bg, ...patch });
const id = useId();
const tr = useTx();
const [preview, setPreview] = useState<{ path: string; url?: string; error?: string } | null>(null);
useEffect(() => {
if (!bg.path || !isTauri()) return;
let live = true;
call(commands.fsReadImage(bg.path)).then(
(url) => live && setPreview({ path: bg.path, url }),
(e: unknown) => live && setPreview({ path: bg.path, error: e instanceof Error ? e.message : String(e) }),
);
return () => {
live = false;
};
}, [bg.path]);
const shown = preview?.path === bg.path ? preview : null;
const browse = async () => {
const { open } = await import("@/platform/dialog");
const picked = await open({
multiple: false,
title: tr("Background Image"),
defaultPath: bg.path || undefined,
filters: [{ name: tr("Image files"), extensions: IMAGE_EXTENSIONS }],
});
const path = Array.isArray(picked) ? picked[0] : picked;
if (path) set({ path });
};
const off = !bg.path;
return (
<>
<Row label="Image" htmlFor={id} hint={shown?.error ? tr`Cannot load the image: ${shown.error}` : undefined}>
<Input
id={id}
className={styles.pathInput}
value={bg.path}
placeholder={tr("None")}
onChange={(e) => set({ path: e.target.value })}
/>
<IconButton
icon="folder-open"
size="small"
title="Browse…"
aria-label="Browse"
disabled={!isTauri()}
onClick={() => void browse()}
/>
{shown?.url && <img className={styles.backgroundPreview} src={shown.url} alt="" />}
</Row>
<Indented>
<Row label="Opacity">
<NumberInput
value={bg.opacity}
min={0}
max={100}
integer
unit="%"
disabled={off}
aria-label="Opacity"
onChange={(opacity) => set({ opacity })}
/>
</Row>
<Row label="Fill">
<Select
value={bg.fill}
options={FILL_OPTIONS}
disabled={off}
onChange={(fill) => set({ fill })}
aria-label="Fill"
/>
</Row>
<Row label="Anchor">
<Select
value={bg.anchor}
options={ANCHOR_OPTIONS}
disabled={off}
onChange={(anchor) => set({ anchor })}
aria-label="Anchor"
/>
</Row>
</Indented>
</>
);
}
function ToolWindowsGrid() {
return (
<Grid
@@ -749,6 +859,17 @@ export const BEHAVIOR_PAGES: PageSpec[] = [
{ kind: "custom", label: "Different tool window background", component: ToolWindowBackgroundRow },
],
},
{
title: "Background Image",
options: [
{
kind: "custom",
label: "Image",
keywords: ["Background image", "wallpaper", "Opacity", "Fill", "Anchor", "Scale", "Tile"],
component: BackgroundImageRows,
},
],
},
{
title: "Accessibility",
options: [
+16 -1
View File
@@ -1554,5 +1554,20 @@
"No matching tasks": "No matching tasks",
"Move to Editor": "Move to Editor",
"Moves a terminal tab, with its running session, into the editor area (arg: tab id).": "Moves a terminal tab, with its running session, into the editor area (arg: tab id).",
"Starts a login shell where the session was.": "Starts a login shell where the session was."
"Starts a login shell where the session was.": "Starts a login shell where the session was.",
"Background Image": "Background Image",
"Background image": "Background image",
"Cannot load the image: {error}": "Cannot load the image: {error}",
"Opacity": "Opacity",
"Fill": "Fill",
"Anchor": "Anchor",
"Scale": "Scale",
"Original size": "Original size",
"Tile": "Tile",
"Center": "Center",
"Top left": "Top left",
"Top right": "Top right",
"Bottom left": "Bottom left",
"Bottom right": "Bottom right",
"Image files": "Image files"
}
+16 -1
View File
@@ -7945,5 +7945,20 @@
"No matching tasks": "没有匹配的任务",
"Move to Editor": "移至编辑器",
"Moves a terminal tab, with its running session, into the editor area (arg: tab id).": "把终端标签连同正在运行的会话移到编辑器区域(参数:标签 id)。",
"Starts a login shell where the session was.": "在会话原来的目录启动登录 shell。"
"Starts a login shell where the session was.": "在会话原来的目录启动登录 shell。",
"Background Image": "背景图像",
"Background image": "背景图像",
"Cannot load the image: {error}": "无法加载图像:{error}",
"Opacity": "不透明度",
"Fill": "填充",
"Anchor": "位置",
"Scale": "缩放",
"Original size": "原始大小",
"Tile": "平铺",
"Center": "居中",
"Top left": "左上",
"Top right": "右上",
"Bottom left": "左下",
"Bottom right": "右下",
"Image files": "图像文件"
}
+2
View File
@@ -360,6 +360,8 @@ html {
--h-control-small: 20px;
--w-project-panel: 240px;
--z-panel: 10;
/* Appearance › Background Image: over panels, under dialogs and popups. */
--z-background-image: 20;
--z-dialog: 900;
--z-popup: 1000;
--z-toast: 1100;
+14
View File
@@ -115,6 +115,20 @@
var(--bg-frame);
}
/* Settings › Appearance › Background Image (variables from the settings runtime). */
.framed::after {
content: "";
position: absolute;
inset: 0;
z-index: var(--z-background-image);
pointer-events: none;
background-image: var(--background-image, none);
background-size: var(--background-image-size, cover);
background-repeat: var(--background-image-repeat, no-repeat);
background-position: var(--background-image-position, center);
opacity: var(--background-image-opacity, 0);
}
/* LightEdit: the editor as the only island, inset like the project frame's. */
.lightEditFrame {
background: var(--bg-frame);