diff --git a/apps/codex-plus-launcher/Cargo.toml b/apps/codex-plus-launcher/Cargo.toml index e973a9142..37a581faf 100644 --- a/apps/codex-plus-launcher/Cargo.toml +++ b/apps/codex-plus-launcher/Cargo.toml @@ -9,6 +9,11 @@ repository.workspace = true name = "codex-plus-plus" path = "src/main.rs" +# Keep the pre-1.2.43 launcher name available for older manager builds and shortcuts. +[[bin]] +name = "codex-plus" +path = "src/main.rs" + [dependencies] anyhow.workspace = true async-trait = "0.1" diff --git a/apps/codex-plus-manager/src/App.tsx b/apps/codex-plus-manager/src/App.tsx index b4bc12550..9ad3801d0 100644 --- a/apps/codex-plus-manager/src/App.tsx +++ b/apps/codex-plus-manager/src/App.tsx @@ -2564,8 +2564,8 @@ export function App() { if (!current) return current; if (isWindowsPlatform) { const config = { ...current.config }; - delete config.colors; delete config.palette; + config.colors = defaultDreamSkinColors(); return { ...current, config }; } const defaults = defaultDreamSkinTheme(); @@ -3630,15 +3630,6 @@ function DreamSkinScreen({ const themeAppearance = theme.appearance === "light" || theme.appearance === "dark" ? theme.appearance : "auto"; - const windowsAccent = typeof theme.palette?.accent === "string" ? theme.palette.accent : ""; - const updateWindowsAccent = (value: string) => { - const palette = { ...(theme.palette ?? {}) }; - if (value.trim()) palette.accent = value; - else delete palette.accent; - const next: DreamSkinThemeConfig = { ...theme, palette }; - if (!Object.keys(palette).length) delete next.palette; - updateTheme(next); - }; const companion = theme.companion; const companionDataUrl = typeof companion?.dataUrl === "string" ? companion.dataUrl : ""; const companionEnabled = Boolean(companionDataUrl) && companion?.enabled !== false; @@ -3946,7 +3937,7 @@ function DreamSkinScreen({ {isWindowsPlatform - ? t("Windows 使用亮暗模式、图片取色和可选强调色;完整色板仅在 macOS 生效。") + ? t("Windows 现在会读取主题中的完整 colors 色板;清除色板后将恢复从图片自适应配色。") : t("macOS 会应用主题中的图片、文字和颜色配置。")} @@ -4108,24 +4099,34 @@ function DreamSkinScreen({ ))} -
- +
+ {dreamSkinColorFields().map(([key, label]) => ( + updateThemeColor(key, value)} + /> + ))} +
+ -
+ - {t("亮暗模式直接控制 Codex 外观;强调色留空时自动从主题图片提取。")} + {t("Windows 现在会读取主题中的完整 colors 色板;清除色板后将恢复从图片自适应配色。")} ) : ( diff --git a/apps/codex-plus-manager/src/dream-skin.test.ts b/apps/codex-plus-manager/src/dream-skin.test.ts index 32c119cf7..d10463927 100644 --- a/apps/codex-plus-manager/src/dream-skin.test.ts +++ b/apps/codex-plus-manager/src/dream-skin.test.ts @@ -51,6 +51,8 @@ describe("dream skin theme helpers", () => { assert.match(assets, /__GLASS_VISION_CSS_JSON__/); assert.match(renderer, /__CODEX_PLUS_EXTERNAL_DREAM_SKIN_RUNTIME__/); assert.match(renderer, /__CODEX_PLUS_CLEAR_DREAM_SKIN__/); + assert.match(renderer, /data-codex-plus-dream-skin-main-compat/); + assert.match(renderer, /classList\.add\("main-surface"\)/); }); it("preserves target-only theme fields without rewriting them", () => { @@ -116,17 +118,20 @@ describe("dream skin theme helpers", () => { assert.match(app, /Math\.max\(-160, Math\.min\(160, Number\(event\.currentTarget\.value\) \|\| 0\)\)/); }); - it("keeps the Windows skin active when the sidebar is hidden", async () => { + it("keeps the modern Dream Skin runtime active when the sidebar is hidden", async () => { const renderer = await readFile( new URL("../../../assets/inject/upstream/dream-skin/windows/renderer-inject.js", import.meta.url), "utf8", ); - assert.match(renderer, /const shellMain = document\.querySelector\("main\.main-surface"\)/); + assert.match(renderer, /main\.main-surface/); + assert.match(renderer, /data-dream-skin/); + assert.match(renderer, /data-dream-shell/); + assert.match(renderer, /ensure\(\{ root: true/); assert.doesNotMatch(renderer, /!shellMain\s*\|\|\s*!shellSidebar/); }); - it("extends the Windows wallpaper treatment to right and bottom dock panels", async () => { + it("uses the stable selector contract for the full Codex shell", async () => { const renderer = await readFile( new URL("../../../assets/inject/upstream/dream-skin/windows/renderer-inject.js", import.meta.url), "utf8", @@ -136,15 +141,18 @@ describe("dream skin theme helpers", () => { "utf8", ); - assert.match(renderer, /\[data-app-shell-tabs="true"\]/); - assert.match(renderer, /dream-aux-panel-layer/); - assert.match(renderer, /dream-aux-panel-right/); - assert.match(renderer, /dream-aux-panel-bottom/); - assert.match(renderer, /clearAuxiliaryPanelClasses/); - assert.match(css, /\.dream-aux-panel-layer/); - assert.match(css, /\.dream-aux-panel-right/); - assert.match(css, /\.dream-aux-panel-bottom/); - assert.match(css, /\[data-codex-terminal="true"\]/); + assert.match(renderer, /codex-dream-skin-selectors\/1/); + assert.match(renderer, /app-shell-left-panel/); + assert.match(renderer, /home-route/); + assert.match(renderer, /adoptedStyleSheets/); + assert.match(renderer, /MutationObserver/); + assert.match(renderer, /data-codex-plus-dream-skin-main-compat/); + assert.match(renderer, /ensureShellMainCompatibility/); + assert.match(css, /html\[data-dream-skin="active"\]/); + assert.match(css, /main\.main-surface/); + assert.match(css, /_MainContentTopFade_/); + assert.match(css, /aside\.app-shell-left-panel/); + assert.match(css, /\.composer-surface-chrome/); }); it("exposes companion image controls in the theme editor", async () => { @@ -256,7 +264,7 @@ describe("dream skin theme helpers", () => { const css = await readFile(new URL("./styles.css", import.meta.url), "utf8"); assert.match(app, /dream-skin-theme-library/); - assert.match(app, /Windows 使用亮暗模式、图片取色和可选强调色/); + assert.match(app, /Windows 现在会读取主题中的完整 colors 色板/); assert.match(css, /\.dream-skin-market-grid\s*\{[^}]*grid-template-columns:\s*repeat\(3,/s); assert.match(css, /\.dream-skin-theme-list\s*\{[^}]*grid-template-columns:\s*repeat\(3,/s); assert.match(css, /@media \(max-width:\s*760px\)[\s\S]*\.dream-skin-theme-list\s*\{[^}]*grid-template-columns:\s*1fr/s); @@ -286,7 +294,7 @@ describe("dream skin theme helpers", () => { assert.match(customizer, /t\("恢复 Codex 默认配色"\)/); }); - it("exposes only effective Windows appearance and accent controls", async () => { + it("exposes Windows appearance and complete color controls", async () => { const app = await readFile(new URL("./App.tsx", import.meta.url), "utf8"); assert.match(app, /dream-skin-windows-theme-controls/); @@ -295,7 +303,7 @@ describe("dream skin theme helpers", () => { assert.match(app, /暗色/); assert.match(app, /跟随图片配色/); assert.match(app, /isWindowsPlatform \? \([\s\S]*dream-skin-windows-theme-controls[\s\S]*dream-skin-colors/); - assert.match(app, /if \(isWindowsPlatform\) \{[\s\S]*delete config\.colors;[\s\S]*delete config\.palette;/); + assert.doesNotMatch(app, /if \(isWindowsPlatform\) \{[\s\S]*delete config\.colors;/); }); it("separates the remote marketplace from local theme editing", async () => { diff --git a/apps/codex-plus-manager/src/dream-skin.ts b/apps/codex-plus-manager/src/dream-skin.ts index 104d7f1c6..537650e48 100644 --- a/apps/codex-plus-manager/src/dream-skin.ts +++ b/apps/codex-plus-manager/src/dream-skin.ts @@ -183,6 +183,7 @@ export function defaultDreamSkinTheme(): DreamSkinThemeConfig { projectLabel: "◉ 选择项目", statusText: "DREAM SKIN ONLINE", quote: "MAKE SOMETHING WONDERFUL", + colors: defaultDreamSkinColors(), image: "dream-reference.jpg", appearance: "auto", art: { diff --git a/apps/codex-plus-manager/src/i18n-en.ts b/apps/codex-plus-manager/src/i18n-en.ts index 16ff65ad2..0a12cae3d 100644 --- a/apps/codex-plus-manager/src/i18n-en.ts +++ b/apps/codex-plus-manager/src/i18n-en.ts @@ -57,8 +57,6 @@ export const EN_PLAIN: Record = { "亮色": "Light", "暗色": "Dark", "跟随图片配色": "Use image colors", - "亮暗模式直接控制 Codex 外观;强调色留空时自动从主题图片提取。": - "The appearance mode directly controls Codex. Leave the accent empty to derive it from the theme image.", "主题市场": "Theme marketplace", "主题视图": "Theme view", "社区主题": "Community themes", @@ -115,8 +113,8 @@ export const EN_PLAIN: Record = { "主题操作": "Theme actions", "重命名": "Rename", "正在加载主题库…": "Loading theme library...", - "Windows 使用亮暗模式、图片取色和可选强调色;完整色板仅在 macOS 生效。": - "Windows uses appearance mode, image-derived colors, and an optional accent; the full palette applies only on macOS.", + "Windows 现在会读取主题中的完整 colors 色板;清除色板后将恢复从图片自适应配色。": + "Windows uses the theme image, appearance mode, and complete colors palette.", "macOS 会应用主题中的图片、文字和颜色配置。": "macOS applies the image, text, and color settings from the theme.", "Dream Skin 图片预览": "Dream Skin image preview", diff --git a/apps/codex-plus-manager/src/styles.css b/apps/codex-plus-manager/src/styles.css index 97f6c6975..6d055dd11 100644 --- a/apps/codex-plus-manager/src/styles.css +++ b/apps/codex-plus-manager/src/styles.css @@ -5245,8 +5245,7 @@ select { padding-top: 2px; } -.dream-skin-windows-theme-controls > .field, -.dream-skin-windows-accent .field { +.dream-skin-windows-theme-controls > .field { min-width: 0; margin: 0; } @@ -5255,17 +5254,6 @@ select { grid-template-columns: repeat(3, minmax(0, 1fr)); } -.dream-skin-windows-accent { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 10px; - align-items: end; -} - -.dream-skin-windows-accent > button { - min-height: 36px; -} - .dream-skin-windows-theme-note { color: hsl(var(--muted-foreground)); font-size: 12px; @@ -5456,7 +5444,6 @@ select { .dream-skin-text-grid, .dream-skin-colors, - .dream-skin-windows-accent, .dream-skin-companion-fields, .dream-skin-diagnostics-grid, .dream-skin-verification-meta { diff --git a/assets/inject/renderer-inject.js b/assets/inject/renderer-inject.js index fda3287e9..2cfd24400 100644 --- a/assets/inject/renderer-inject.js +++ b/assets/inject/renderer-inject.js @@ -1770,6 +1770,10 @@ document.querySelectorAll(".dream-home-shell").forEach((node) => node.classList.remove("dream-home-shell")); document.querySelectorAll(".dream-skin-home").forEach((node) => node.classList.remove("dream-skin-home")); document.querySelectorAll(".dream-skin-home-shell").forEach((node) => node.classList.remove("dream-skin-home-shell")); + document.querySelectorAll('main[data-codex-plus-dream-skin-main-compat="true"]').forEach((node) => { + node.classList.remove("main-surface"); + node.removeAttribute("data-codex-plus-dream-skin-main-compat"); + }); document.querySelectorAll("[class]").forEach((node) => { for (const className of [...node.classList]) { if ( @@ -1933,6 +1937,12 @@ clearDreamSkinPresentation(); return; } + // Codex 26.727 moved the shell's stable class behind CSS Modules. Keep + // the DreamSkin selector contract working without changing Codex's DOM. + if (!shellMain.classList.contains("main-surface")) { + shellMain.classList.add("main-surface"); + shellMain.setAttribute("data-codex-plus-dream-skin-main-compat", "true"); + } root.classList.add(descriptor.rootClass); root.setAttribute("data-codex-plus-dream-skin", "true"); diff --git a/assets/inject/upstream/dream-skin/macos/dream-skin.css b/assets/inject/upstream/dream-skin/macos/dream-skin.css index 07976b196..2656280ba 100644 --- a/assets/inject/upstream/dream-skin/macos/dream-skin.css +++ b/assets/inject/upstream/dream-skin/macos/dream-skin.css @@ -1,4 +1,5 @@ -:root.codex-dream-skin { +/* Canonical cross-platform skin. Run tools/sync-runtime-assets.mjs after editing. */ +:root[data-dream-skin="active"] { color-scheme: dark !important; --ds-bg: #111318; --ds-panel: #191c22; @@ -41,6 +42,9 @@ rgb(var(--ds-bg-rgb) / .18) 32%, rgb(var(--ds-bg-rgb) / .76) 68%, rgb(var(--ds-bg-rgb) / 1) 100%); + --ds-task-full-veil: linear-gradient( + rgb(var(--ds-bg-rgb) / .10), + rgb(var(--ds-bg-rgb) / .10)); --ds-immersive-edge: rgb(var(--ds-bg-rgb) / .40); --ds-immersive-mid: rgb(var(--ds-bg-rgb) / .26); --ds-immersive-far: rgb(var(--ds-bg-rgb) / .16); @@ -57,7 +61,7 @@ --ds-task-immersive-far: rgb(var(--ds-bg-rgb) / .60); } -html.codex-dream-skin[data-dream-shell="light"] { +html[data-dream-skin="active"][data-dream-shell="light"] { color-scheme: light !important; --ds-bg: #f3f5f6; --ds-panel: #fafbfb; @@ -88,6 +92,9 @@ html.codex-dream-skin[data-dream-shell="light"] { rgb(var(--ds-bg-rgb) / .22) 34%, rgb(var(--ds-bg-rgb) / .78) 70%, rgb(var(--ds-bg-rgb) / 1) 100%); + --ds-task-full-veil: linear-gradient( + rgb(var(--ds-panel-rgb) / .10), + rgb(var(--ds-panel-rgb) / .10)); --ds-immersive-edge: rgb(var(--ds-panel-rgb) / .44); --ds-immersive-mid: rgb(var(--ds-panel-rgb) / .28); --ds-immersive-far: rgb(var(--ds-panel-rgb) / .14); @@ -102,12 +109,31 @@ html.codex-dream-skin[data-dream-shell="light"] { --ds-task-immersive-far: rgb(var(--ds-panel-rgb) / .66); } -html.codex-dream-skin:is([data-dream-art-safe="left"], [data-dream-art-safe-area="left"]) { +/* Native token surfaces (dropdown/popover) resolve from Codex's own + appearanceTheme, not the skin; when that disagrees with data-dream-shell the + popover keeps the opposite palette (#233). Remap the dropdown token family to + skin variables so those surfaces inherit the theme in both shells. */ +html[data-dream-skin="active"] { + --color-token-dropdown-background: var(--ds-panel); +} + +html[data-dream-skin="active"] [class~="bg-token-dropdown-background"] { + color: var(--ds-text); + --color-token-foreground: var(--ds-text); + --color-token-text-secondary: var(--ds-muted); + --color-token-muted-foreground: var(--ds-muted); + --color-token-description-foreground: var(--ds-muted); + --color-token-list-hover-background: rgb(var(--ds-panel-2-rgb) / .9); + --color-token-border: var(--ds-line); + --color-token-border-default: var(--ds-line); +} + +html[data-dream-skin="active"]:is([data-dream-art-safe="left"], [data-dream-art-safe-area="left"]) { --ds-safe-side: left; --ds-art-position: 100% var(--ds-focus-y); } -html.codex-dream-skin:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"]) { +html[data-dream-skin="active"]:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"]) { --ds-safe-side: right; --ds-art-position: 0% var(--ds-focus-y); --ds-hero-scrim: linear-gradient(270deg, @@ -121,7 +147,7 @@ html.codex-dream-skin:is([data-dream-art-safe="right"], [data-dream-art-safe-are rgb(var(--ds-bg-rgb) / .12) 100%); } -html.codex-dream-skin[data-dream-shell="light"]:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"]) { +html[data-dream-skin="active"][data-dream-shell="light"]:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"]) { --ds-hero-scrim: linear-gradient(270deg, rgb(var(--ds-panel-rgb) / .96) 0%, rgb(var(--ds-panel-rgb) / .82) 50%, @@ -133,7 +159,7 @@ html.codex-dream-skin[data-dream-shell="light"]:is([data-dream-art-safe="right"] rgb(var(--ds-bg-rgb) / .12) 100%); } -html.codex-dream-skin:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"]) { +html[data-dream-skin="active"]:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"]) { --ds-safe-side: center; --ds-hero-scrim: linear-gradient(90deg, transparent 0%, @@ -147,7 +173,7 @@ html.codex-dream-skin:is([data-dream-art-safe="center"], [data-dream-art-safe-ar rgb(var(--ds-bg-rgb) / .10) 100%); } -html.codex-dream-skin[data-dream-shell="light"]:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"]) { +html[data-dream-skin="active"][data-dream-shell="light"]:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"]) { --ds-hero-scrim: linear-gradient(90deg, transparent 0%, rgb(var(--ds-panel-rgb) / .72) 25%, @@ -160,7 +186,7 @@ html.codex-dream-skin[data-dream-shell="light"]:is([data-dream-art-safe="center" rgb(var(--ds-bg-rgb) / .10) 100%); } -html.codex-dream-skin:is([data-dream-art-safe="none"], [data-dream-art-safe-area="none"]) { +html[data-dream-skin="active"]:is([data-dream-art-safe="none"], [data-dream-art-safe-area="none"]) { --ds-safe-side: none; --ds-hero-scrim: linear-gradient(180deg, rgb(var(--ds-bg-rgb) / .34), @@ -170,7 +196,7 @@ html.codex-dream-skin:is([data-dream-art-safe="none"], [data-dream-art-safe-area rgb(var(--ds-bg-rgb) / .14)); } -html.codex-dream-skin[data-dream-shell="light"]:is([data-dream-art-safe="none"], [data-dream-art-safe-area="none"]) { +html[data-dream-skin="active"][data-dream-shell="light"]:is([data-dream-art-safe="none"], [data-dream-art-safe-area="none"]) { --ds-hero-scrim: linear-gradient(180deg, rgb(var(--ds-panel-rgb) / .46), rgb(var(--ds-panel-rgb) / .28)); @@ -179,17 +205,17 @@ html.codex-dream-skin[data-dream-shell="light"]:is([data-dream-art-safe="none"], rgb(var(--ds-bg-rgb) / .16)); } -html.codex-dream-skin body { +html[data-dream-skin="active"] body { background: var(--ds-bg) !important; color: var(--ds-text) !important; font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC", "Microsoft YaHei UI", "Segoe UI", system-ui, sans-serif !important; } -html.codex-dream-skin body::before { +html[data-dream-skin="active"] body::before { content: none; } -html.codex-dream-skin aside.app-shell-left-panel { +html[data-dream-skin="active"] aside.app-shell-left-panel { background: linear-gradient(180deg, rgb(var(--ds-panel-rgb) / .98), @@ -202,41 +228,41 @@ html.codex-dream-skin aside.app-shell-left-panel { backdrop-filter: none !important; } -html.codex-dream-skin aside.app-shell-left-panel nav { background: transparent !important; } +html[data-dream-skin="active"] aside.app-shell-left-panel nav { background: transparent !important; } -html.codex-dream-skin aside.app-shell-left-panel button, -html.codex-dream-skin aside.app-shell-left-panel a { +html[data-dream-skin="active"] aside.app-shell-left-panel button, +html[data-dream-skin="active"] aside.app-shell-left-panel a { color: var(--ds-text) !important; transition: background-color .18s cubic-bezier(.22, 1, .36, 1), border-color .18s cubic-bezier(.22, 1, .36, 1), color .18s cubic-bezier(.22, 1, .36, 1) !important; } -html.codex-dream-skin aside.app-shell-left-panel [class*="text-token-foreground"] { +html[data-dream-skin="active"] aside.app-shell-left-panel [class*="text-token-foreground"] { color: var(--ds-text) !important; } -html.codex-dream-skin aside.app-shell-left-panel [class*="text-token-input-placeholder-foreground"] { +html[data-dream-skin="active"] aside.app-shell-left-panel [class*="text-token-input-placeholder-foreground"] { color: rgb(var(--ds-muted-rgb) / .92) !important; } -html.codex-dream-skin aside.app-shell-left-panel svg { +html[data-dream-skin="active"] aside.app-shell-left-panel svg { color: rgb(var(--ds-muted-rgb) / .96) !important; } -html.codex-dream-skin aside.app-shell-left-panel button:hover, -html.codex-dream-skin aside.app-shell-left-panel a:hover { +html[data-dream-skin="active"] aside.app-shell-left-panel button:hover, +html[data-dream-skin="active"] aside.app-shell-left-panel a:hover { background: rgb(var(--ds-accent-rgb) / .09) !important; color: var(--ds-text) !important; transform: none !important; } -html.codex-dream-skin aside.app-shell-left-panel button:hover svg, -html.codex-dream-skin aside.app-shell-left-panel a:hover svg { +html[data-dream-skin="active"] aside.app-shell-left-panel button:hover svg, +html[data-dream-skin="active"] aside.app-shell-left-panel a:hover svg { color: var(--ds-accent) !important; } -html.codex-dream-skin aside.app-shell-left-panel button[aria-label^="切换模式"] { +html[data-dream-skin="active"] aside.app-shell-left-panel button[aria-label^="切换模式"] { background: transparent !important; border: 0 !important; color: var(--ds-accent) !important; @@ -246,24 +272,24 @@ html.codex-dream-skin aside.app-shell-left-panel button[aria-label^="切换模 text-shadow: none !important; } -html.codex-dream-skin aside.app-shell-left-panel button[aria-label^="切换模式"]::after { +html[data-dream-skin="active"] aside.app-shell-left-panel button[aria-label^="切换模式"]::after { content: " ·"; color: var(--ds-secondary); } -html.codex-dream-skin aside.app-shell-left-panel [class~="bg-token-list-hover-background"], -html.codex-dream-skin aside.app-shell-left-panel [aria-current="page"] { +html[data-dream-skin="active"] aside.app-shell-left-panel [class~="bg-token-list-hover-background"], +html[data-dream-skin="active"] aside.app-shell-left-panel [aria-current="page"] { background: rgb(var(--ds-accent-rgb) / .12) !important; border: 1px solid rgb(var(--ds-accent-rgb) / .22) !important; box-shadow: 0 4px 16px rgb(var(--ds-bg-rgb) / .12) !important; color: var(--ds-text) !important; } -html.codex-dream-skin aside.app-shell-left-panel [aria-current="page"] svg { +html[data-dream-skin="active"] aside.app-shell-left-panel [aria-current="page"] svg { color: var(--ds-accent) !important; } -html.codex-dream-skin main.main-surface { +html[data-dream-skin="active"] main.main-surface { position: relative !important; isolation: isolate; overflow: hidden !important; @@ -275,7 +301,7 @@ html.codex-dream-skin main.main-surface { box-shadow: -8px 0 28px rgb(var(--ds-bg-rgb) / .18) !important; } -html.codex-dream-skin main.main-surface:not(.dream-skin-home-shell)::before { +html[data-dream-skin="active"] main.main-surface:not(:has([role="main"]))::before { content: ""; position: absolute; inset: 0; @@ -288,23 +314,36 @@ html.codex-dream-skin main.main-surface:not(.dream-skin-home-shell)::before { opacity: .78; } -html.codex-dream-skin[data-dream-shell="light"] main.main-surface:not(.dream-skin-home-shell)::before { +html[data-dream-skin="active"][data-dream-shell="light"] main.main-surface:not(:has([role="main"]))::before { opacity: .72; } -html.codex-dream-skin:is([data-dream-art-wide="true"], [data-dream-art-aspect="wide"], [data-dream-art-aspect="ultrawide"]) - main.main-surface:not(.dream-skin-home-shell)::before { +html[data-dream-skin="active"]:is([data-dream-art-wide="true"], [data-dream-art-aspect="wide"], [data-dream-art-aspect="ultrawide"]) + main.main-surface:not(:has([role="main"]))::before { background-position: center, center, var(--ds-art-position); background-size: 100% 100%, 100% 100%, cover; } -html.codex-dream-skin:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"]) - main.main-surface:not(.dream-skin-home-shell)::before { +/* Studio's full mode keeps the task artwork at normal strength with only the + baseline readability veil. Ambient mode deliberately uses the heavier task + fade/shade below, while off removes the artwork entirely. */ +html[data-dream-skin="active"]:is([data-dream-task-mode="full"], [data-dream-art-task-mode="full"]) + main.main-surface:not(:has([role="main"]))::before { inset: 0; height: auto; + background-image: var(--ds-task-full-veil), var(--dream-skin-art); + background-position: center, var(--ds-art-position); + background-size: 100% 100%, cover; + opacity: 1; } -html.codex-dream-skin:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"]:has(main.main-surface:not(.dream-skin-home-shell)) body { +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"]) + main.main-surface:not(:has([role="main"]))::before { + inset: 0; + height: auto; +} + +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"]:has(main.main-surface):not(:has(main.main-surface [role="main"])) body { background-image: var(--dream-skin-art) !important; background-position: var(--ds-art-position) !important; background-size: cover !important; @@ -312,7 +351,47 @@ html.codex-dream-skin:is([data-dream-task-mode="ambient"], [data-dream-art-task- background-attachment: fixed !important; } -html.codex-dream-skin:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"]:has(main.main-surface:not(.dream-skin-home-shell)) +html[data-dream-skin="active"]:is([data-dream-task-mode="full"], [data-dream-art-task-mode="full"])[data-dream-art-wide="true"]:has(main.main-surface):not(:has(main.main-surface [role="main"])) body { + background-image: var(--dream-skin-art) !important; + background-position: var(--ds-art-position) !important; + background-size: cover !important; + background-repeat: no-repeat !important; + background-attachment: fixed !important; +} + +html[data-dream-skin="active"]:is([data-dream-task-mode="full"], [data-dream-art-task-mode="full"])[data-dream-art-wide="true"]:has(main.main-surface):not(:has(main.main-surface [role="main"])) + aside.app-shell-left-panel { + background: linear-gradient(90deg, + var(--ds-immersive-sidebar), + var(--ds-immersive-edge) 100%) !important; + border: 0 !important; + border-radius: 0 !important; + box-shadow: none !important; + backdrop-filter: none !important; +} + +html[data-dream-skin="active"]:is([data-dream-task-mode="full"], [data-dream-art-task-mode="full"])[data-dream-art-wide="true"]:has(main.main-surface):not(:has(main.main-surface [role="main"])) + aside.app-shell-left-panel::after { + background: transparent !important; +} + +html[data-dream-skin="active"]:is([data-dream-task-mode="full"], [data-dream-art-task-mode="full"])[data-dream-art-wide="true"]:has(main.main-surface):not(:has(main.main-surface [role="main"])) + main.main-surface:not(:has([role="main"])) { + background: linear-gradient(90deg, + var(--ds-immersive-edge), + var(--ds-immersive-mid) 64%, + var(--ds-immersive-far)) !important; + border: 0 !important; + border-radius: 0 !important; + box-shadow: none !important; +} + +html[data-dream-skin="active"]:is([data-dream-task-mode="full"], [data-dream-art-task-mode="full"])[data-dream-art-wide="true"] + main.main-surface:not(:has([role="main"]))::before { + content: none; +} + +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"]:has(main.main-surface):not(:has(main.main-surface [role="main"])) aside.app-shell-left-panel { background: linear-gradient(90deg, var(--ds-task-immersive-sidebar), @@ -325,13 +404,13 @@ html.codex-dream-skin:is([data-dream-task-mode="ambient"], [data-dream-art-task- /* Native sidebar resize chrome inherits the sidebar background and extends 20px into the main surface. Keeping it clear preserves one continuous image. */ -html.codex-dream-skin:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"]:has(main.main-surface:not(.dream-skin-home-shell)) +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"]:has(main.main-surface):not(:has(main.main-surface [role="main"])) aside.app-shell-left-panel::after { background: transparent !important; } -html.codex-dream-skin:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"]:has(main.main-surface:not(.dream-skin-home-shell)) - main.main-surface:not(.dream-skin-home-shell) { +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"]:has(main.main-surface):not(:has(main.main-surface [role="main"])) + main.main-surface:not(:has([role="main"])) { background: linear-gradient(90deg, var(--ds-task-immersive-edge), var(--ds-task-immersive-mid) 64%, @@ -341,12 +420,12 @@ html.codex-dream-skin:is([data-dream-task-mode="ambient"], [data-dream-art-task- box-shadow: none !important; } -html.codex-dream-skin:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"] - main.main-surface:not(.dream-skin-home-shell)::before { +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"] + main.main-surface:not(:has([role="main"]))::before { content: none; } -html.codex-dream-skin:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"]:has(main.main-surface:not(.dream-skin-home-shell)) +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"]:has(main.main-surface):not(:has(main.main-surface [role="main"])) .composer-surface-chrome { background: var(--ds-immersive-composer-solid) !important; border: 0 !important; @@ -357,7 +436,7 @@ html.codex-dream-skin:is([data-dream-task-mode="ambient"], [data-dream-art-task- backdrop-filter: none !important; } -html.codex-dream-skin:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"]:has(main.main-surface:not(.dream-skin-home-shell)) +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"]:has(main.main-surface):not(:has(main.main-surface [role="main"])) main.main-surface > header.app-header-tint { background: transparent !important; border-bottom: 0 !important; @@ -365,48 +444,48 @@ html.codex-dream-skin:is([data-dream-task-mode="ambient"], [data-dream-art-task- backdrop-filter: none !important; } -html.codex-dream-skin:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"] - main.main-surface:not(.dream-skin-home-shell) [class*="_markdown"] { +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"], [data-dream-task-mode="full"], [data-dream-art-task-mode="full"])[data-dream-art-wide="true"] + main.main-surface:not(:has([role="main"])) [class*="_markdown"] { color: var(--ds-text) !important; text-shadow: 0 1px 2px rgb(var(--ds-bg-rgb) / .82), 0 0 10px rgb(var(--ds-bg-rgb) / .58); } -html.codex-dream-skin[data-dream-shell="light"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"] - main.main-surface:not(.dream-skin-home-shell) [class*="_markdown"] { +html[data-dream-skin="active"][data-dream-shell="light"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"], [data-dream-task-mode="full"], [data-dream-art-task-mode="full"])[data-dream-art-wide="true"] + main.main-surface:not(:has([role="main"])) [class*="_markdown"] { text-shadow: 0 1px 2px rgb(var(--ds-panel-rgb) / .92), 0 0 10px rgb(var(--ds-panel-rgb) / .72); } -html.codex-dream-skin:is([data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"]) - main.main-surface:not(.dream-skin-home-shell)::before { +html[data-dream-skin="active"]:is([data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"]) + main.main-surface:not(:has([role="main"]))::before { inset: 0 0 auto; height: clamp(280px, 46vh, 520px); background-position: center top, center top, var(--ds-art-position); background-size: 100% 100%, 100% 100%, cover; } -html.codex-dream-skin:is([data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"]):is([data-dream-art-wide="true"], [data-dream-art-aspect="wide"], [data-dream-art-aspect="ultrawide"]) - main.main-surface:not(.dream-skin-home-shell)::before { +html[data-dream-skin="active"]:is([data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"]):is([data-dream-art-wide="true"], [data-dream-art-aspect="wide"], [data-dream-art-aspect="ultrawide"]) + main.main-surface:not(:has([role="main"]))::before { inset: 0; height: auto; background-position: center, center, var(--ds-art-position); background-size: 100% 100%, 100% 100%, cover; } -html.codex-dream-skin:is([data-dream-task-mode="off"], [data-dream-art-task-mode="off"]) - main.main-surface:not(.dream-skin-home-shell)::before { +html[data-dream-skin="active"]:is([data-dream-task-mode="off"], [data-dream-art-task-mode="off"]) + main.main-surface:not(:has([role="main"]))::before { content: none; } -html.codex-dream-skin main.main-surface:not(.dream-skin-home-shell) > :not(header.app-header-tint) { +html[data-dream-skin="active"] main.main-surface:not(:has([role="main"])) > :not(header.app-header-tint) { position: relative; z-index: 1; } -html.codex-dream-skin main.main-surface:not(.dream-skin-home-shell) [role="main"] { +html[data-dream-skin="active"] main.main-surface:not(:has([role="main"])) [role="main"] { background: transparent !important; color: var(--ds-text) !important; text-shadow: @@ -414,149 +493,191 @@ html.codex-dream-skin main.main-surface:not(.dream-skin-home-shell) [role="main" 0 0 10px rgb(var(--ds-bg-rgb) / .46); } -html.codex-dream-skin[data-dream-shell="light"] main.main-surface:not(.dream-skin-home-shell) [role="main"] { +html[data-dream-skin="active"][data-dream-shell="light"] main.main-surface:not(:has([role="main"])) [role="main"] { text-shadow: none; } -html.codex-dream-skin main.main-surface:not(.dream-skin-home-shell) article { +/* Newer Codex builds paint the list/detail panes (pull requests, the chat + review/terminal/browser/files side panel) with token surfaces instead of + [role="main"]. The side panel can coexist with a visible home shell, so this + transparency is deliberately not gated on the home-route selector. The nested + surfaces all carry the same token, so they are cleared everywhere and the + translucent pane tint is painted once, on the bordered outer wrapper. */ +html[data-dream-skin="active"] main.main-surface + :is(div, section, aside)[class~="bg-token-main-surface-primary"] { + background: transparent !important; +} + +html[data-dream-skin="active"] main.main-surface + div[class~="bg-token-main-surface-primary"][class~="border-l"] { + background: rgb(var(--ds-panel-rgb) / .62) !important; + backdrop-filter: blur(10px) saturate(106%) !important; +} + +html[data-dream-skin="active"] main.main-surface:not(:has([role="main"])) article { border: 1px solid rgb(var(--ds-muted-rgb) / .12); background: rgb(var(--ds-panel-rgb) / .44); box-shadow: 0 8px 24px rgb(var(--ds-bg-rgb) / .10); backdrop-filter: blur(7px) saturate(105%); } -html.codex-dream-skin[data-dream-shell="light"] main.main-surface:not(.dream-skin-home-shell) article { +html[data-dream-skin="active"][data-dream-shell="light"] main.main-surface:not(:has([role="main"])) article { background: rgb(var(--ds-panel-rgb) / .72); } -html.codex-dream-skin main.main-surface > header.app-header-tint { +html[data-dream-skin="active"][data-dream-shell="light"] + main.main-surface:has([role="main"])::after, +html[data-dream-skin="active"][data-dream-shell="light"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"]::before { + text-shadow: none; +} + +html[data-dream-skin="active"] main.main-surface > header.app-header-tint { /* Preserve Codex's native fixed header geometry and side-panel controls. */ background: rgb(var(--ds-panel-rgb) / .90) !important; border-bottom: 1px solid var(--ds-line) !important; backdrop-filter: blur(14px) saturate(108%) !important; } -#codex-dream-skin-chrome { - position: fixed; - z-index: 31; - overflow: hidden; - pointer-events: none; - border-radius: 16px 0 0 0; -} - -.dream-skin-brand { +/* Decorative chrome lives on the native shell; no injected positioning host is needed. */ +html[data-dream-skin="active"] + main.main-surface:not(:has([role="main"])) > header.app-header-tint::before { + content: var(--dream-skin-name, "Codex Dream Skin") " · " + var(--dream-skin-brand-subtitle, "CODEX DREAM SKIN"); position: absolute; left: 22px; top: 4px; - height: 40px; - display: none; - align-items: center; - gap: 10px; - color: var(--ds-text); -} - -#codex-dream-skin-chrome.dream-skin-home-shell .dream-skin-brand { display: none !important; } - -.dream-skin-portal-mark { - width: 28px; - height: 28px; - display: grid; - place-items: center; - border: 1px solid rgb(var(--ds-accent-rgb) / .44); - border-radius: 50%; - color: var(--ds-accent); - background: rgb(var(--ds-accent-rgb) / .08); - font-size: 18px; - box-shadow: 0 0 0 4px rgb(var(--ds-accent-rgb) / .05); -} - -.dream-skin-brand b { - display: block; + z-index: 2; + max-width: min(42%, 360px); + overflow: hidden; color: var(--ds-accent); - font-size: 13px; - letter-spacing: 0; -} - -.dream-skin-brand small { - display: block; - margin-top: 2px; - color: var(--ds-muted); - font-size: 9px; - letter-spacing: 0; - text-transform: uppercase; + font: 800 11px/1.3 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + letter-spacing: .03em; + text-overflow: ellipsis; + text-shadow: 0 1px 12px rgb(var(--ds-bg-rgb) / .32); + white-space: nowrap; + pointer-events: none; } -.dream-skin-brand small, -.dream-skin-status { display: none !important; } - -.dream-skin-status { +html[data-dream-skin="active"] + main.main-surface:not(:has([role="main"])) > header.app-header-tint::after { + content: var(--dream-skin-status, "DREAM SKIN ONLINE"); position: absolute; right: 84px; top: 13px; - display: none; - align-items: center; - gap: 7px; + z-index: 2; + max-width: 28%; + overflow: hidden; color: var(--ds-muted); - font: 700 10px/1 ui-monospace, "SFMono-Regular", Consolas, monospace; - letter-spacing: 0; + font: 700 9px/1.2 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + letter-spacing: .08em; + text-overflow: ellipsis; + text-shadow: 0 1px 10px rgb(var(--ds-bg-rgb) / .32); + white-space: nowrap; + pointer-events: none; } -.dream-skin-status i { - width: 7px; - height: 7px; - border-radius: 50%; - background: var(--ds-accent); - box-shadow: 0 0 8px rgb(var(--ds-accent-rgb) / .42); +html[data-dream-skin="active"] + main.main-surface:has([role="main"])::after { + content: var(--dream-skin-quote, "MAKE SOMETHING WONDERFUL"); + position: absolute; + right: 28px; + bottom: 72px; + z-index: 2; + pointer-events: none; + color: rgb(var(--ds-accent-rgb) / .74); + font: italic 14px/1.2 "Segoe Print", "Comic Sans MS", cursive; + letter-spacing: 0; + text-shadow: 0 0 13px rgb(var(--ds-accent-rgb) / .30); + transform: rotate(-3deg); } -.dream-skin-quote, -.dream-skin-orbit { display: none !important; } - -.dream-skin-particles { - display: none; +html[data-dream-skin="active"][data-dream-shell="light"] + main.main-surface:not(:has([role="main"])) > header.app-header-tint::before, +html[data-dream-skin="active"][data-dream-shell="light"] + main.main-surface:not(:has([role="main"])) > header.app-header-tint::after { + text-shadow: none; } -.dream-skin-particles i { - position: absolute; - width: 3px; - height: 3px; - border-radius: 50%; - background: var(--ds-accent); - box-shadow: 0 0 8px 2px rgb(var(--ds-accent-rgb) / .24); +/* Windows keeps this native bar outside main; macOS simply has no match. */ +html[data-dream-skin="active"][data-dream-shell="dark"] + [class~="group/application-menu-top-bar"] { + color: var(--ds-text) !important; + background: rgb(var(--ds-panel-rgb) / .90) !important; + border-bottom: 1px solid var(--ds-line) !important; + box-shadow: 0 1px 12px rgb(var(--ds-bg-rgb) / .72) !important; + backdrop-filter: blur(16px) saturate(90%) !important; } -.dream-skin-particles i:nth-child(1) { left: 6%; top: 13%; } -.dream-skin-particles i:nth-child(2) { left: 29%; top: 7%; background: var(--ds-secondary); } -.dream-skin-particles i:nth-child(3) { left: 51%; top: 18%; } -.dream-skin-particles i:nth-child(4) { left: 75%; top: 9%; background: var(--ds-secondary); } -.dream-skin-particles i:nth-child(5) { left: 91%; top: 28%; } -.dream-skin-particles i:nth-child(6) { left: 64%; top: 63%; background: var(--ds-secondary); } -.dream-skin-particles i:nth-child(7) { left: 22%; top: 72%; } -.dream-skin-particles i:nth-child(8) { left: 86%; top: 78%; } +html[data-dream-skin="active"][data-dream-shell="dark"] + [class~="group/application-menu-top-bar"] :is(button, svg) { + color: var(--ds-text) !important; + text-shadow: 0 1px 2px rgb(var(--ds-bg-rgb) / .82) !important; +} -html.codex-dream-skin [role="main"] { +html[data-dream-skin="active"] main.main-surface [role="main"] { background: transparent !important; scrollbar-color: rgb(var(--ds-accent-rgb) / .38) transparent; } -html.codex-dream-skin .dream-skin-home { +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) { --thread-content-max-width: min(1180px, calc(100cqw - 44px)) !important; overflow-x: hidden !important; } -.dream-skin-home > div:first-child { +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child { min-height: 100% !important; padding-top: 15px !important; } -.dream-skin-home > div:first-child > div:first-child { +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child { flex: 0 0 440px !important; min-height: 440px !important; align-items: flex-start !important; padding-bottom: 0 !important; } -.dream-skin-home > div:first-child > div:first-child > div:first-child { +/* Codex 26.721+: home-route's first child now only wraps the (usually empty) + native .home-banners slot; the real content column moved out to become + this wrapper's sibling instead of its descendant. The two rules above + still assume the old nesting and force this now-mostly-empty wrapper to + 100% height / 440px, which pushes the real sibling content off-screen + (see #244). Override with higher specificity only when that new slot is + present, so pre-26.721 layouts are untouched. */ +html[data-dream-skin="active"] + [role="main"]:has([data-testid="home-icon"]) > div:first-child:has(> .home-banners) { + min-height: 0 !important; + padding-top: 0 !important; +} + +html[data-dream-skin="active"] + [role="main"]:has([data-testid="home-icon"]) > div:first-child:has(> .home-banners) > .home-banners { + flex: 0 1 auto !important; + min-height: 0 !important; +} + +html[data-dream-skin="active"] + [role="main"]:has([data-testid="home-icon"]) > div:first-child:has(> .home-banners) > .home-banners > div:first-child { + height: auto !important; + min-height: 0 !important; + border: 0 !important; + border-radius: 0 !important; + background: transparent !important; + box-shadow: none !important; +} + +html[data-dream-skin="active"] + [role="main"]:has([data-testid="home-icon"]) > div:first-child:has(> .home-banners) > .home-banners > div:first-child::before { + content: none !important; +} + +/* Codex 26.721+: this chain (3-6 first-child levels deep) targets the old + compact hero-art card that used to live inside div:first-child's second + level. That level is now the empty .home-banners slot (see #244), so on + the new DOM these rules currently match nothing and the compact hero card + is dormant rather than misrendered. Left as-is pending a real fixture of + the new nested shape; the critical content-invisible bug is the block + above this one. */ +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child { position: relative !important; isolation: isolate; width: calc(100% - 44px) !important; @@ -576,7 +697,7 @@ html.codex-dream-skin .dream-skin-home { box-shadow: 0 16px 38px rgb(var(--ds-bg-rgb) / .30) !important; } -.dream-skin-home > div:first-child > div:first-child > div:first-child::before { +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child::before { content: ""; position: absolute; inset: 0 auto 0 0; @@ -587,26 +708,26 @@ html.codex-dream-skin .dream-skin-home { background: var(--ds-hero-scrim); } -html.codex-dream-skin:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"]) - .dream-skin-home > div:first-child > div:first-child > div:first-child::before { +html[data-dream-skin="active"]:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"]) + [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child::before { inset: 0 0 0 auto; } -html.codex-dream-skin:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"]) - .dream-skin-home > div:first-child > div:first-child > div:first-child::before { +html[data-dream-skin="active"]:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"]) + [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child::before { inset: 0 14%; width: auto; border-radius: 0; } -html.codex-dream-skin:is([data-dream-art-safe="none"], [data-dream-art-safe-area="none"]) - .dream-skin-home > div:first-child > div:first-child > div:first-child::before { +html[data-dream-skin="active"]:is([data-dream-art-safe="none"], [data-dream-art-safe-area="none"]) + [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child::before { inset: 0; width: auto; border-radius: 21px; } -.dream-skin-home > div:first-child > div:first-child > div:first-child > div:first-child { +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child { position: relative; z-index: 1; height: 100%; @@ -615,38 +736,38 @@ html.codex-dream-skin:is([data-dream-art-safe="none"], [data-dream-art-safe-area padding: 0 38px; } -html.codex-dream-skin:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"]) - .dream-skin-home > div:first-child > div:first-child > div:first-child > div:first-child { +html[data-dream-skin="active"]:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"]) + [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child { justify-content: flex-end !important; } -html.codex-dream-skin:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"]) - .dream-skin-home > div:first-child > div:first-child > div:first-child > div:first-child { +html[data-dream-skin="active"]:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"]) + [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child { justify-content: center !important; } -.dream-skin-home > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { width: min(46%, 520px) !important; align-items: flex-start !important; gap: 0 !important; } -html.codex-dream-skin:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"]) - .dream-skin-home > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { +html[data-dream-skin="active"]:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"]) + [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { align-items: flex-end !important; text-align: right !important; } -html.codex-dream-skin:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"]) - .dream-skin-home > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { +html[data-dream-skin="active"]:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"]) + [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { width: min(58%, 640px) !important; align-items: center !important; text-align: center !important; } -.dream-skin-home [data-testid="home-icon"] { display: none !important; } +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-testid="home-icon"] { display: none !important; } -.dream-skin-home [data-feature="game-source"] { +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"] { display: block !important; max-width: 100% !important; color: var(--ds-text) !important; @@ -659,11 +780,17 @@ html.codex-dream-skin:is([data-dream-art-safe="center"], [data-dream-art-safe-ar pointer-events: auto !important; } -.dream-skin-home [data-feature="game-source"]::before { - content: none; +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"]::before { + content: var(--dream-skin-name, "Codex Dream Skin"); + display: block; + margin-bottom: 9px; + color: var(--ds-accent); + font: 800 12px/1.3 "Microsoft YaHei UI", sans-serif; + letter-spacing: .10em; + text-shadow: 0 0 14px rgb(var(--ds-accent-rgb) / .26); } -.dream-skin-home [data-feature="game-source"]::after { +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"]::after { content: var(--dream-skin-tagline, "把喜欢的画面变成可交互的 Codex 工作台。"); display: block; margin-top: 13px; @@ -674,7 +801,7 @@ html.codex-dream-skin:is([data-dream-art-safe="center"], [data-dream-art-safe-ar letter-spacing: 0; } -.dream-skin-home [data-feature="game-source"] button { +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"] button { margin: 0 5px; padding: 2px 9px 3px; border: 1px solid rgb(var(--ds-accent-rgb) / .36); @@ -685,7 +812,7 @@ html.codex-dream-skin:is([data-dream-art-safe="center"], [data-dream-art-safe-ar text-underline-offset: 5px; } -.dream-skin-home [data-feature="game-source"] button::before { +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"] button::before { content: var(--dream-skin-project-prefix, "选择项目 · "); font-size: .46em; font-weight: 700; @@ -693,16 +820,16 @@ html.codex-dream-skin:is([data-dream-art-safe="center"], [data-dream-art-safe-ar opacity: .86; } -.dream-skin-home > div:first-child > div:first-child > div:first-child > div:nth-child(2) { +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:nth-child(2) { left: 14px !important; right: 14px !important; top: 100% !important; margin-top: 13px !important; } -.dream-skin-home .group\/home-suggestions { overflow: visible !important; } +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions { overflow: visible !important; } -.dream-skin-home .group\/home-suggestions button { +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button { position: relative !important; min-height: 118px !important; padding: 14px 13px 12px !important; @@ -723,21 +850,24 @@ html.codex-dream-skin:is([data-dream-art-safe="center"], [data-dream-art-safe-ar border-color .18s cubic-bezier(.22, 1, .36, 1) !important; } -.dream-skin-home .group\/home-suggestions button [class~="text-token-text-primary"] { +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button [class~="text-token-text-primary"] { color: var(--ds-text) !important; } -.dream-skin-home .group\/home-suggestions button:hover { +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button:hover { transform: translateY(-2px) !important; border-color: rgb(var(--ds-accent-rgb) / .42) !important; box-shadow: 0 12px 28px rgb(var(--ds-bg-rgb) / .24) !important; } -.dream-skin-home .group\/home-suggestions button > span:first-child > span:first-child { +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button > span:first-child > span:first-child { width: 38px; height: 38px; - display: grid !important; - place-items: center; + /* Native spans carry justify-start; grid + place-items cannot override + justify-content, so the glyph parks at the circle's left edge. (#176) */ + display: flex !important; + align-items: center !important; + justify-content: center !important; margin: 0 auto; border: 1px solid rgb(var(--ds-accent-rgb) / .24); border-radius: 50%; @@ -746,16 +876,19 @@ html.codex-dream-skin:is([data-dream-art-safe="center"], [data-dream-art-safe-ar box-shadow: 0 0 0 6px rgb(var(--ds-accent-rgb) / .05); } -.dream-skin-home .group\/home-suggestions button svg { +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button svg { width: 21px !important; height: 21px !important; + display: block !important; + margin: 0 !important; + flex: 0 0 auto !important; color: var(--ds-accent) !important; } -.dream-skin-home .group\/home-suggestions button > span:first-child { justify-content: center !important; } -.dream-skin-home .group\/home-suggestions button > span:last-child { align-items: center !important; text-align: center !important; } +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button > span:first-child { justify-content: center !important; } +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button > span:last-child { align-items: center !important; text-align: center !important; } -html.codex-dream-skin .composer-surface-chrome { +html[data-dream-skin="active"] .composer-surface-chrome { overflow: visible !important; border: 1px solid rgb(var(--ds-muted-rgb) / .18) !important; border-radius: 22px !important; @@ -764,11 +897,11 @@ html.codex-dream-skin .composer-surface-chrome { backdrop-filter: blur(16px) saturate(108%) !important; } -html.codex-dream-skin .composer-surface-chrome::before { content: none !important; } +html[data-dream-skin="active"] .composer-surface-chrome::before { content: none !important; } /* A wide image can carry the home screen too. The native hero remains live, but the artwork is painted once on the window instead of repeated in a card. */ -html.codex-dream-skin[data-dream-art-wide="true"]:has(main.main-surface.dream-skin-home-shell) body { +html[data-dream-skin="active"][data-dream-art-wide="true"]:has(main.main-surface [role="main"]) body { background-image: var(--dream-skin-art) !important; background-position: var(--ds-art-position) !important; background-size: cover !important; @@ -776,7 +909,7 @@ html.codex-dream-skin[data-dream-art-wide="true"]:has(main.main-surface.dream-sk background-attachment: fixed !important; } -html.codex-dream-skin[data-dream-art-wide="true"]:has(main.main-surface.dream-skin-home-shell) +html[data-dream-skin="active"][data-dream-art-wide="true"]:has(main.main-surface [role="main"]) aside.app-shell-left-panel { background: linear-gradient(90deg, var(--ds-immersive-sidebar), @@ -787,12 +920,12 @@ html.codex-dream-skin[data-dream-art-wide="true"]:has(main.main-surface.dream-sk backdrop-filter: none !important; } -html.codex-dream-skin[data-dream-art-wide="true"]:has(main.main-surface.dream-skin-home-shell) +html[data-dream-skin="active"][data-dream-art-wide="true"]:has(main.main-surface [role="main"]) aside.app-shell-left-panel::after { background: transparent !important; } -html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface.dream-skin-home-shell { +html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface:has([role="main"]) { background: linear-gradient(90deg, var(--ds-immersive-edge), var(--ds-immersive-mid) 64%, @@ -802,7 +935,7 @@ html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface.dream-skin-h box-shadow: none !important; } -html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface.dream-skin-home-shell +html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface:has([role="main"]) > header.app-header-tint { background: transparent !important; border-bottom: 0 !important; @@ -810,7 +943,7 @@ html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface.dream-skin-h backdrop-filter: none !important; } -html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface +html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface > header.app-header-tint { color: var(--ds-text) !important; text-shadow: @@ -818,27 +951,27 @@ html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface 0 0 8px rgb(var(--ds-bg-rgb) / .52) !important; } -html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface +html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface > header.app-header-tint svg { color: rgb(var(--ds-muted-rgb) / .96) !important; filter: drop-shadow(0 1px 2px rgb(var(--ds-bg-rgb) / .72)); } -html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface.dream-skin-home-shell - .dream-skin-home > div:first-child > div:first-child > div:first-child { +html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface:has([role="main"]) + [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child { border: 0 !important; border-radius: 0 !important; background: transparent !important; box-shadow: none !important; } -html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface.dream-skin-home-shell - .dream-skin-home > div:first-child > div:first-child > div:first-child::before { +html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface:has([role="main"]) + [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child::before { content: none !important; } -html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface.dream-skin-home-shell - .dream-skin-home .group\/home-suggestions button { +html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface:has([role="main"]) + [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button { background: rgb(var(--ds-panel-rgb) / .56) !important; box-shadow: 0 8px 22px rgb(var(--ds-bg-rgb) / .16), @@ -846,7 +979,7 @@ html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface.dream-skin-h backdrop-filter: blur(8px) saturate(104%) !important; } -html.codex-dream-skin[data-dream-art-wide="true"] .composer-surface-chrome { +html[data-dream-skin="active"][data-dream-art-wide="true"] .composer-surface-chrome { background: var(--ds-immersive-composer-solid) !important; border: 0 !important; box-shadow: @@ -856,7 +989,7 @@ html.codex-dream-skin[data-dream-art-wide="true"] .composer-surface-chrome { backdrop-filter: none !important; } -html.codex-dream-skin[data-dream-shell="light"][data-dream-art-wide="true"] +html[data-dream-skin="active"][data-dream-shell="light"][data-dream-art-wide="true"] .composer-surface-chrome { box-shadow: 0 10px 28px rgb(var(--ds-bg-rgb) / .14), @@ -865,7 +998,7 @@ html.codex-dream-skin[data-dream-shell="light"][data-dream-art-wide="true"] backdrop-filter: blur(8px) saturate(102%) !important; } -html.codex-dream-skin[data-dream-shell="light"] +html[data-dream-skin="active"][data-dream-shell="light"] .composer-surface-chrome p.placeholder::after { color: rgb(var(--ds-muted-rgb) / .78) !important; opacity: 1 !important; @@ -874,7 +1007,7 @@ html.codex-dream-skin[data-dream-shell="light"] /* Current Codex builds render the home project picker as a separate opaque cap above the composer. Join both native controls into one continuous surface while leaving task and utility-route inputs untouched. */ -html.codex-dream-skin .dream-skin-home-utility { +html[data-dream-skin="active"] [class*="_homeUtilityBar_"] { top: 0 !important; width: 100% !important; margin-inline: 0 !important; @@ -888,7 +1021,7 @@ html.codex-dream-skin .dream-skin-home-utility { backdrop-filter: blur(16px) saturate(108%) !important; } -html.codex-dream-skin .dream-skin-home:has(.dream-skin-home-utility) +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]):has([class*="_homeUtilityBar_"]) .composer-surface-chrome { border: 0 !important; border-radius: 0 0 22px 22px !important; @@ -899,7 +1032,7 @@ html.codex-dream-skin .dream-skin-home:has(.dream-skin-home-utility) inset 0 -1px rgb(var(--ds-muted-rgb) / .18) !important; } -html.codex-dream-skin[data-dream-art-wide="true"] .dream-skin-home-utility { +html[data-dream-skin="active"][data-dream-art-wide="true"] [class*="_homeUtilityBar_"] { background: var(--ds-immersive-composer-solid) !important; box-shadow: inset 1px 0 var(--ds-immersive-line), @@ -908,8 +1041,8 @@ html.codex-dream-skin[data-dream-art-wide="true"] .dream-skin-home-utility { backdrop-filter: none !important; } -html.codex-dream-skin[data-dream-art-wide="true"] - .dream-skin-home:has(.dream-skin-home-utility) .composer-surface-chrome { +html[data-dream-skin="active"][data-dream-art-wide="true"] + [role="main"]:has([data-testid="home-icon"]):has([class*="_homeUtilityBar_"]) .composer-surface-chrome { box-shadow: 0 10px 30px rgb(var(--ds-bg-rgb) / .20), inset 1px 0 var(--ds-immersive-line), @@ -917,8 +1050,8 @@ html.codex-dream-skin[data-dream-art-wide="true"] inset 0 -1px var(--ds-immersive-line) !important; } -html.codex-dream-skin[data-dream-shell="light"][data-dream-art-wide="true"] - .dream-skin-home-utility { +html[data-dream-skin="active"][data-dream-shell="light"][data-dream-art-wide="true"] + [class*="_homeUtilityBar_"] { box-shadow: inset 1px 0 var(--ds-immersive-line), inset -1px 0 var(--ds-immersive-line), @@ -926,46 +1059,46 @@ html.codex-dream-skin[data-dream-shell="light"][data-dream-art-wide="true"] backdrop-filter: blur(8px) saturate(102%) !important; } -html.codex-dream-skin .dream-skin-home-utility button, -html.codex-dream-skin .composer-surface-chrome button:not([class~="bg-token-foreground"]) { +html[data-dream-skin="active"] [class*="_homeUtilityBar_"] button, +html[data-dream-skin="active"] .composer-surface-chrome button:not([class~="bg-token-foreground"]) { color: var(--ds-muted) !important; } -html.codex-dream-skin .dream-skin-home-utility button svg, -html.codex-dream-skin .composer-surface-chrome button svg { +html[data-dream-skin="active"] [class*="_homeUtilityBar_"] button svg, +html[data-dream-skin="active"] .composer-surface-chrome button svg { color: currentColor !important; } -html.codex-dream-skin .dream-skin-home-utility button *, -html.codex-dream-skin .composer-surface-chrome button:not([class~="bg-token-foreground"]) * { +html[data-dream-skin="active"] [class*="_homeUtilityBar_"] button *, +html[data-dream-skin="active"] .composer-surface-chrome button:not([class~="bg-token-foreground"]) * { color: currentColor !important; } -html.codex-dream-skin .dream-skin-home-utility button:hover, -html.codex-dream-skin .composer-surface-chrome button:not([class~="bg-token-foreground"]):hover { +html[data-dream-skin="active"] [class*="_homeUtilityBar_"] button:hover, +html[data-dream-skin="active"] .composer-surface-chrome button:not([class~="bg-token-foreground"]):hover { color: var(--ds-text) !important; background: rgb(var(--ds-accent-rgb) / .10) !important; } -html.codex-dream-skin .composer-surface-chrome p.placeholder::after { +html[data-dream-skin="active"] .composer-surface-chrome p.placeholder::after { color: rgb(var(--ds-muted-rgb) / .82) !important; opacity: 1 !important; } /* Search routes ship an opaque sticky band and input surface. Keep the control legible while allowing the selected image to remain continuous. */ -html.codex-dream-skin:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"])[data-dream-art-wide="true"] - main.main-surface:not(.dream-skin-home-shell) div.sticky:has(input[type="text"]) { +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"])[data-dream-art-wide="true"] + main.main-surface:not(:has([role="main"])) div.sticky:has(input[type="text"]) { background: transparent !important; } -html.codex-dream-skin:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"])[data-dream-art-wide="true"] - main.main-surface:not(.dream-skin-home-shell) div.sticky:has(input[type="text"])::after { +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"])[data-dream-art-wide="true"] + main.main-surface:not(:has([role="main"])) div.sticky:has(input[type="text"])::after { background: transparent !important; } -html.codex-dream-skin:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"])[data-dream-art-wide="true"] - main.main-surface:not(.dream-skin-home-shell) div.no-drag:has(> input[type="text"]) { +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"])[data-dream-art-wide="true"] + main.main-surface:not(:has([role="main"])) div.no-drag:has(> input[type="text"]) { background: var(--ds-immersive-composer) !important; border: 0 !important; box-shadow: @@ -976,18 +1109,18 @@ html.codex-dream-skin:is([data-dream-task-mode="ambient"], [data-dream-art-task- /* Pull Requests and similar utility routes wrap their content in a native full-size opaque surface. Only clear full-window wrappers, not cards. */ -html.codex-dream-skin:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"])[data-dream-art-wide="true"] - main.main-surface:not(.dream-skin-home-shell) +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"])[data-dream-art-wide="true"] + main.main-surface:not(:has([role="main"])) [class~="bg-token-main-surface-primary"][class~="h-full"][class~="w-full"] { background: transparent !important; } -html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface +html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface .app-shell-main-content-frame { border-top: 0 !important; } -html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface +html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface .app-shell-main-content-top-fade { display: none !important; background: transparent !important; @@ -996,12 +1129,12 @@ html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface /* Codex paints a second opaque fade behind the sticky composer. The composer already owns its readable surface, so retaining this layer creates a false bottom panel and makes the control look duplicated. */ -html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface +html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface .thread-scroll-container .bg-gradient-to-t.from-token-main-surface-primary { background: transparent !important; } -.dream-skin-home div:has(> .horizontal-scroll-fade-mask .group\/project-selector) { +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) div:has(> .horizontal-scroll-fade-mask .group\/project-selector) { position: relative; padding-top: 28px !important; border: 1px solid rgb(var(--ds-muted-rgb) / .16); @@ -1009,7 +1142,7 @@ html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface background: rgb(var(--ds-panel-rgb) / .92) !important; } -.dream-skin-home div:has(> .horizontal-scroll-fade-mask .group\/project-selector)::before { +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) div:has(> .horizontal-scroll-fade-mask .group\/project-selector)::before { content: var(--dream-skin-project-label, "选择项目"); position: absolute; left: 13px; @@ -1022,58 +1155,57 @@ html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface white-space: nowrap; } -.dream-skin-home .group\/project-selector > button { +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/project-selector > button { border-color: rgb(var(--ds-accent-rgb) / .22) !important; background: rgb(var(--ds-accent-rgb) / .08) !important; color: var(--ds-text) !important; box-shadow: 0 3px 12px rgb(var(--ds-bg-rgb) / .12) !important; } -html.codex-dream-skin .ProseMirror { +html[data-dream-skin="active"] .ProseMirror { color: var(--ds-text) !important; caret-color: var(--ds-accent) !important; } -html.codex-dream-skin button[class~="bg-token-foreground"] { +html[data-dream-skin="active"] button[class~="bg-token-foreground"] { background: var(--ds-accent) !important; color: var(--ds-on-accent) !important; box-shadow: 0 5px 14px rgb(var(--ds-accent-rgb) / .20) !important; } -html.codex-dream-skin article, -html.codex-dream-skin [data-message-author-role] { border-radius: 16px; } +html[data-dream-skin="active"] article, +html[data-dream-skin="active"] [data-message-author-role] { border-radius: 16px; } @media (max-width: 1120px) { - .dream-skin-status { display: none !important; } - .dream-skin-home { --thread-content-max-width: min(940px, calc(100cqw - 30px)) !important; } - .dream-skin-home > div:first-child > div:first-child > div:first-child { width: calc(100% - 28px) !important; } + html[data-dream-skin="active"] + main.main-surface:has([role="main"])::after { content: none; } + html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) { --thread-content-max-width: min(940px, calc(100cqw - 30px)) !important; } + html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child { width: calc(100% - 28px) !important; } } @media (max-width: 900px) { - .dream-skin-brand { left: 15px; } - .dream-skin-brand b { font-size: 12px; } - .dream-skin-home > div:first-child > div:first-child { + html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child { flex-basis: 408px !important; min-height: 408px !important; } - .dream-skin-home > div:first-child > div:first-child > div:first-child { + html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child { height: 232px !important; min-height: 232px !important; } - .dream-skin-home > div:first-child > div:first-child > div:first-child > div:first-child { padding: 0 26px; } - .dream-skin-home > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { + html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child { padding: 0 26px; } + html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { width: min(58%, 460px) !important; } - .dream-skin-home [data-feature="game-source"] { font-size: 18px !important; } - .dream-skin-home [data-feature="game-source"]::before { margin-bottom: 7px; font-size: 10px; } - .dream-skin-home [data-feature="game-source"]::after { margin-top: 9px; font-size: 11px; } - .dream-skin-home .group\/home-suggestions button { min-height: 112px !important; font-size: 12px !important; } + html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"] { font-size: 18px !important; } + html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"]::before { margin-bottom: 7px; font-size: 10px; } + html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"]::after { margin-top: 9px; font-size: 11px; } + html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button { min-height: 112px !important; font-size: 12px !important; } } @media (prefers-reduced-motion: reduce) { - html.codex-dream-skin *, - html.codex-dream-skin *::before, - html.codex-dream-skin *::after { + html[data-dream-skin="active"] *, + html[data-dream-skin="active"] *::before, + html[data-dream-skin="active"] *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; diff --git a/assets/inject/upstream/dream-skin/macos/renderer-inject.js b/assets/inject/upstream/dream-skin/macos/renderer-inject.js index 9988cf181..be7998889 100644 --- a/assets/inject/upstream/dream-skin/macos/renderer-inject.js +++ b/assets/inject/upstream/dream-skin/macos/renderer-inject.js @@ -1,10 +1,14 @@ +// Canonical cross-platform renderer. Run tools/sync-runtime-assets.mjs after editing. ((cssText, artDataUrl, themeConfig) => { + const SELECTOR_CONTRACT = {"schema":"codex-dream-skin-selectors/1","selectors":[{"key":"shell-main","selector":"main.main-surface","tier":"L1","scope":"all","required":true},{"key":"left-panel","selector":"aside.app-shell-left-panel","tier":"L1","scope":"all","required":true},{"key":"header-tint","selector":"header.app-header-tint","tier":"L1","scope":"all","required":true},{"key":"home-icon","selector":"[data-testid=\"home-icon\"]","tier":"L1","scope":"home","required":true},{"key":"home-route","selector":"[role=\"main\"]:has([data-testid=\"home-icon\"])","tier":"L1","scope":"home","required":true},{"key":"home-route-css","selector":"[role=\"main\"]","tier":"L1","scope":"home","required":true},{"key":"home-banners","selector":".home-banners","tier":"L2","scope":"home","required":false},{"key":"composer-chrome","selector":".composer-surface-chrome","tier":"L2","scope":"home+thread","required":false},{"key":"composer-toolbar","selector":".composer-surface-chrome [class*=\"_footer_\"]","tier":"L2","scope":"home+thread","required":false},{"key":"home-utility","selector":"[class*=\"_homeUtilityBar_\"]","tier":"L2","scope":"home","required":false},{"key":"game-source","selector":"[data-feature=\"game-source\"]","tier":"L2","scope":"home","required":false},{"key":"home-suggestions","selector":".group\\/home-suggestions","tier":"L2","scope":"home","required":false},{"key":"project-selector","selector":".group\\/project-selector","tier":"L2","scope":"home config","required":false},{"key":"markdown","selector":"[class*=\"_markdown\"]","tier":"L2","scope":"thread","required":false},{"key":"thread-surface","selector":".thread-scroll-container","tier":"L2","scope":"thread","required":false},{"key":"message","selector":"[data-message-author-role]","tier":"L2","scope":"thread","required":false},{"key":"appearance-radio","selector":"input[name=\"appearance-theme\"]","tier":"L2","scope":"settings","required":false},{"key":"overlay-menu","selector":"[role=\"menu\"]","tier":"L2","scope":"overlay","required":false},{"key":"overlay-dialog","selector":"[role=\"dialog\"]","tier":"L2","scope":"overlay","required":false},{"key":"overlay-popper","selector":"[data-radix-popper-content-wrapper]","tier":"L2","scope":"overlay","required":false}],"stableTestids":["app-shell-header-context-menu-surface","home-icon","theme-preview"]}; const STATE_KEY = "__CODEX_DREAM_SKIN_STATE__"; const DISABLED_KEY = "__CODEX_DREAM_SKIN_DISABLED__"; + const STYLE_REGISTRY_KEY = "__CODEX_DREAM_SKIN_STYLE_SHEETS__"; const STYLE_ID = "codex-dream-skin-style"; - const CHROME_ID = "codex-dream-skin-chrome"; const SHELL_ATTR = "data-dream-shell"; - const ART_ATTRS = [ + const PART_ATTR = "data-ds-part"; + const ROOT_ATTRS = [ + "data-dream-skin", SHELL_ATTR, "data-dream-art-wide", "data-dream-art-safe", "data-dream-task-mode", "data-dream-art-safe-area", "data-dream-art-task-mode", "data-dream-art-aspect", "data-dream-art-ready", @@ -26,8 +30,24 @@ "--dream-art-focus-x", "--dream-art-focus-y", "--dream-art-position", "--dream-skin-focus-x", "--dream-skin-focus-y", "--dream-skin-art-position", "--dream-skin-name", "--dream-skin-tagline", "--dream-skin-project-prefix", - "--dream-skin-project-label", + "--dream-skin-project-label", "--dream-skin-brand-subtitle", "--dream-skin-status", + "--dream-skin-quote", "--dream-skin-art", + "--ds-theme-color-background", "--ds-theme-color-panel", + "--ds-theme-color-panel-alt", "--ds-theme-color-accent", + "--ds-theme-color-accent-alt", "--ds-theme-color-secondary", + "--ds-theme-color-highlight", "--ds-theme-color-text", + "--ds-theme-color-muted", "--ds-theme-color-line", + "--ds-theme-font-family", "--ds-theme-font-scale", + "--ds-theme-surface-radius", "--ds-theme-surface-opacity", + "--ds-theme-surface-blur", "--ds-theme-surface-border-alpha", + "--ds-theme-surface-shadow", "--ds-theme-image-focus-x", + "--ds-theme-image-focus-y", "--ds-theme-image-zoom", + "--ds-theme-image-dim", "--ds-theme-image-task-intensity", + "--ds-theme-density-scale", "--ds-theme-motion-level", ]; + const selectorByKey = new Map(SELECTOR_CONTRACT.selectors.map((entry) => [entry.key, entry])); + const stableTestidSelector = (testid) => SELECTOR_CONTRACT.stableTestids?.includes(testid) + ? `[data-testid="${testid}"]` : null; const installToken = {}; const existingAnalysisCache = window[ANALYSIS_CACHE_KEY]; const analysisCache = existingAnalysisCache && typeof existingAnalysisCache.get === "function" && @@ -35,8 +55,12 @@ window[ANALYSIS_CACHE_KEY] = analysisCache; let artAnalysis = typeof THEME.artKey === "string" ? analysisCache.get(THEME.artKey) ?? null : null; let analysisTimer = null; - let samplingNativeShell = false; let rootObserver = null; + let partObserver = null; + let bodyReadyHandler = null; + let styleMode = null; + let styleNode = null; + let styleSheet = null; const now = () => typeof performance === "object" && typeof performance.now === "function" ? performance.now() : Date.now(); const metrics = { @@ -46,15 +70,24 @@ layoutReads: 0, attributeWrites: 0, styleWrites: 0, - textWrites: 0, + styleRepairs: 0, + partPasses: 0, + partWrites: 0, + navigationEvents: 0, + safetyPasses: 0, analysisRuns: 0, analysisCacheHits: artAnalysis ? 1 : 0, firstEnsureMs: null, analysisMs: null, }; - window[DISABLED_KEY] = false; const previous = window[STATE_KEY]; + if (typeof previous?.cleanup === "function") previous.cleanup(); + window[DISABLED_KEY] = false; + + const existingStyleRegistry = window[STYLE_REGISTRY_KEY]; + const styleRegistry = existingStyleRegistry instanceof Set ? existingStyleRegistry : new Set(); + window[STYLE_REGISTRY_KEY] = styleRegistry; const artUrl = (() => { const comma = artDataUrl.indexOf(","); const mime = /^data:([^;,]+)/.exec(artDataUrl)?.[1] || "image/png"; @@ -64,20 +97,6 @@ return URL.createObjectURL(new Blob([bytes], { type: mime })); })(); - if (previous?.observer) previous.observer.disconnect(); - if (previous?.rootObserver) previous.rootObserver.disconnect(); - if (previous?.resizeObserver) previous.resizeObserver.disconnect(); - if (previous?.timer) clearInterval(previous.timer); - if (previous?.scheduler?.timeout) clearTimeout(previous.scheduler.timeout); - if (previous?.scheduler?.frame != null && typeof cancelAnimationFrame === "function") { - cancelAnimationFrame(previous.scheduler.frame); - } - if (previous?.analysisTimer) clearTimeout(previous.analysisTimer); - if (previous?.resizeHandler) window.removeEventListener("resize", previous.resizeHandler); - if (previous?.mediaHandler && previous?.mediaQuery) { - try { previous.mediaQuery.removeEventListener("change", previous.mediaHandler); } catch {} - } - const cssString = (value) => JSON.stringify(String(value ?? "")); const setStyleProperty = (root, name, value) => { @@ -95,18 +114,14 @@ } }; - const setTextContent = (node, value) => { - if (node && node.textContent !== value) { - node.textContent = value; - metrics.textWrites += 1; - } - }; - const parseRgb = (value) => { if (!value || value === "transparent") return null; - const hex = String(value).trim().match(/^#([0-9a-f]{6})$/i); + const hex = String(value).trim().match(/^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i); if (hex) { - const number = Number.parseInt(hex[1], 16); + const rgbHex = hex[1].length <= 4 + ? hex[1].slice(0, 3).split("").map((digit) => `${digit}${digit}`).join("") + : hex[1].slice(0, 6); + const number = Number.parseInt(rgbHex, 16); return { r: number >> 16, g: (number >> 8) & 255, b: number & 255 }; } const m = String(value).match(/rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/i); @@ -118,7 +133,9 @@ const rgbString = (value) => { const rgb = parseRgb(value); - return rgb ? `${Math.round(rgb.r)} ${Math.round(rgb.g)} ${Math.round(rgb.b)}` : null; + return rgb ? [rgb.r, rgb.g, rgb.b] + .map((channel) => Math.round(clamp(channel, 0, 255))) + .join(" ") : null; }; const rgbToHex = ({ r, g, b }) => `#${[r, g, b] @@ -160,98 +177,11 @@ return { r: channel(1 / 3) * 255, g: channel(0) * 255, b: channel(-1 / 3) * 255 }; }; - const luminance = ({ r, g, b }) => { - const lin = [r, g, b].map((c) => { - const x = c / 255; - return x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4; - }); - return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2]; - }; - - /** Detect Codex app light/dark shell for CSS branching. */ - const detectShellMode = () => { + const detectShellAppearance = () => { const root = document.documentElement; - const body = document.body; - const cls = `${root.className || ""} ${body?.className || ""}`.toLowerCase(); - - if (/\b(dark|theme-dark|appearance-dark)\b/.test(cls)) return "dark"; - if (/\b(light|theme-light|appearance-light)\b/.test(cls)) return "light"; - - const dataTheme = ( - root.getAttribute("data-theme") || - root.getAttribute("data-appearance") || - root.getAttribute("data-color-mode") || - body?.getAttribute("data-theme") || - body?.getAttribute("data-appearance") || - "" - ).toLowerCase(); - if (dataTheme.includes("dark")) return "dark"; - if (dataTheme.includes("light")) return "light"; - - // Radios in profile menu (if present in DOM) - const checked = document.querySelector('input[name="appearance-theme"]:checked'); - if (checked) { - const label = (checked.getAttribute("aria-label") || checked.value || "").toLowerCase(); - if (label.includes("暗") || label.includes("dark")) return "dark"; - if (label.includes("浅") || label.includes("light")) return "light"; - if (label.includes("系统") || label.includes("system")) { - return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; - } - } - - // The skin itself declares color-scheme on :root. Once installed, - // reading getComputedStyle(root) directly would therefore keep `auto` - // themes locked to the previous shell mode. Temporarily remove only our - // own root class/attribute, sample the native computed scheme, then restore - // synchronously. Mutation records created by this probe are drained below - // so the root observer does not schedule a redundant ensure pass. - try { - const hadSkin = root.classList.contains("codex-dream-skin"); - const savedShell = root.getAttribute(SHELL_ATTR); - samplingNativeShell = true; - if (hadSkin) root.classList.remove("codex-dream-skin"); - if (savedShell !== null) root.removeAttribute(SHELL_ATTR); - let colorScheme = ""; - try { - colorScheme = getComputedStyle(root).colorScheme || ""; - } finally { - if (hadSkin) root.classList.add("codex-dream-skin"); - if (savedShell !== null) root.setAttribute(SHELL_ATTR, savedShell); - rootObserver?.takeRecords?.(); - samplingNativeShell = false; - } - if (colorScheme.includes("dark") && !colorScheme.includes("light")) return "dark"; - if (colorScheme.includes("light") && !colorScheme.includes("dark")) return "light"; - } catch { - samplingNativeShell = false; - } - - try { - return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; - } catch {} - - // Only use surface luminance before the skin owns those surfaces. Sampling - // our own translucent layers would create route-dependent light/dark flips. - if (!root.classList.contains("codex-dream-skin")) { - const samples = [ - body, - document.querySelector("main.main-surface"), - document.querySelector("aside.app-shell-left-panel"), - ].filter(Boolean); - let votesLight = 0; - let votesDark = 0; - for (const el of samples) { - try { - const rgb = parseRgb(getComputedStyle(el).backgroundColor); - if (!rgb) continue; - const L = luminance(rgb); - if (L >= 0.55) votesLight += 1; - else if (L <= 0.25) votesDark += 1; - } catch {} - } - if (votesLight > votesDark) return "light"; - if (votesDark > votesLight) return "dark"; - } + if (root?.classList?.contains("electron-dark")) return "dark"; + if (root?.classList?.contains("electron-light")) return "light"; + try { return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; } catch {} return "light"; }; @@ -295,14 +225,24 @@ // Image luminance may tune accents and scrims, but auto appearance follows // Codex/ChatGPT (or the OS fallback) so a bright wallpaper cannot flip a // native dark session back to a light shell after analysis. - return detectShellMode(); + return detectShellAppearance(); }; const applyTheme = (root, shell) => { - const colors = THEME.colors || {}; - const explicit = new Set(Array.isArray(THEME.explicitColorKeys) ? THEME.explicitColorKeys : []); + const declaredColors = THEME.colors && typeof THEME.colors === "object" ? THEME.colors : {}; + const legacyPalette = THEME.palette && typeof THEME.palette === "object" ? THEME.palette : {}; + // macOS themes use the full `colors` contract; older Windows themes used + // `palette.accent`. Accept both while keeping one renderer source. + const colors = Object.keys(declaredColors).length ? declaredColors : legacyPalette; + const hasExplicitKeyList = Array.isArray(THEME.explicitColorKeys); + const explicit = new Set(hasExplicitKeyList ? THEME.explicitColorKeys : []); + if (!hasExplicitKeyList && (THEME.colorMode === "explicit" || !Object.hasOwn(THEME, "colorMode"))) { + for (const key of Object.keys(declaredColors)) explicit.add(key); + } + if (typeof legacyPalette.accent === "string") explicit.add("accent"); const adaptive = makeAdaptivePalette(artAnalysis?.accentRgb, shell); - const legacyLight = !THEME.appearance && shell === "light"; + const legacyLight = (THEME.appearance === undefined || THEME.appearance === "auto") + && THEME.colorMode !== "explicit" && shell === "light"; const structural = new Set(["background", "panel", "panelAlt", "text", "muted"]); const pick = (name) => { const allowExplicit = explicit.has(name) && !(legacyLight && structural.has(name)); @@ -326,6 +266,33 @@ for (const [name, value] of Object.entries(variables)) { if (typeof value === "string" && value) setStyleProperty(root, name, value); } + const publicColors = { + "--ds-theme-color-background": variables["--ds-bg"], + "--ds-theme-color-panel": variables["--ds-panel"], + "--ds-theme-color-panel-alt": variables["--ds-panel-2"], + "--ds-theme-color-accent": variables["--ds-green"], + "--ds-theme-color-accent-alt": variables["--ds-lime"], + "--ds-theme-color-secondary": variables["--ds-cyan"], + "--ds-theme-color-highlight": variables["--ds-purple"], + "--ds-theme-color-text": variables["--ds-text"], + "--ds-theme-color-muted": variables["--ds-muted"], + "--ds-theme-color-line": variables["--ds-line"], + }; + for (const [name, value] of Object.entries(publicColors)) { + if (typeof value === "string" && value) setStyleProperty(root, name, value); + } + setStyleProperty(root, "--ds-theme-surface-radius", "12px"); + setStyleProperty(root, "--ds-theme-surface-opacity", "1"); + setStyleProperty(root, "--ds-theme-surface-blur", "0px"); + setStyleProperty(root, "--ds-theme-font-family", "system"); + setStyleProperty(root, "--ds-theme-font-scale", "1"); + setStyleProperty(root, "--ds-theme-surface-border-alpha", "0.14"); + setStyleProperty(root, "--ds-theme-surface-shadow", "soft"); + setStyleProperty(root, "--ds-theme-image-zoom", "1"); + setStyleProperty(root, "--ds-theme-image-dim", "0"); + setStyleProperty(root, "--ds-theme-image-task-intensity", "0.35"); + setStyleProperty(root, "--ds-theme-density-scale", "standard"); + setStyleProperty(root, "--ds-theme-motion-level", "standard"); const rgbVariables = { "--ds-bg-rgb": variables["--ds-bg"], "--ds-panel-rgb": variables["--ds-panel"], @@ -344,6 +311,11 @@ } setStyleProperty(root, "--dream-skin-name", cssString(THEME.name || "Codex Dream Skin")); setStyleProperty(root, "--dream-skin-tagline", cssString(THEME.tagline || "Make something wonderful.")); + setStyleProperty(root, "--dream-skin-quote", cssString(THEME.quote || "MAKE SOMETHING WONDERFUL")); + setStyleProperty(root, "--dream-skin-brand-subtitle", cssString( + THEME.brandSubtitle || "CODEX DREAM SKIN", + )); + setStyleProperty(root, "--dream-skin-status", cssString(THEME.statusText || "DREAM SKIN ONLINE")); setStyleProperty(root, "--dream-skin-project-prefix", cssString(THEME.projectPrefix || "选择项目 · ")); setStyleProperty(root, "--dream-skin-project-label", cssString(THEME.projectLabel || "◉ 选择项目")); }; @@ -377,6 +349,8 @@ setStyleProperty(root, "--dream-skin-focus-x", focusXValue); setStyleProperty(root, "--dream-skin-focus-y", focusYValue); setStyleProperty(root, "--dream-skin-art-position", `${focusXValue} ${focusYValue}`); + setStyleProperty(root, "--ds-theme-image-focus-x", String(Number(focusX.toFixed(4)))); + setStyleProperty(root, "--ds-theme-image-focus-y", String(Number(focusY.toFixed(4)))); }; const analyzeArt = () => new Promise((resolve) => { @@ -524,247 +498,327 @@ image.src = artUrl; }); - let chromeParts = null; - let observedShellMain = null; - let resizeObserver = null; - - const ensureStyle = (root) => { - let style = document.getElementById(STYLE_ID); - if (!style) { - style = document.createElement("style"); - style.id = STYLE_ID; - style.textContent = cssText; - style.dataset.dreamSkinVersion = VERSION; - (document.head || root).appendChild(style); - } else if (style.dataset.dreamSkinStyleRevision !== STYLE_REVISION) { - style.textContent = cssText; + const installStyle = () => { + try { + if (!("adoptedStyleSheets" in document) || typeof CSSStyleSheet !== "function") { + throw new Error("Constructable stylesheets are unavailable"); + } + const sheet = new CSSStyleSheet(); + if (typeof sheet.replaceSync !== "function") throw new Error("replaceSync is unavailable"); + sheet.replaceSync(cssText); + const retained = [...document.adoptedStyleSheets] + .filter((candidate) => !styleRegistry.has(candidate)); + document.adoptedStyleSheets = [...retained, sheet]; + styleRegistry.clear(); + styleRegistry.add(sheet); + document.getElementById(STYLE_ID)?.remove(); + styleSheet = sheet; + styleMode = "adopted"; + return; + } catch { + styleSheet = null; + } + + styleNode = document.getElementById(STYLE_ID) || document.createElement("style"); + styleNode.id = STYLE_ID; + styleNode.textContent = cssText; + if (!styleNode.parentElement) (document.head || document.documentElement).appendChild(styleNode); + styleMode = "style"; + }; + + const ensureStyle = () => { + if (styleMode === "adopted" && styleSheet) { + const current = [...document.adoptedStyleSheets]; + if (!current.includes(styleSheet)) { + document.adoptedStyleSheets = [...current, styleSheet]; + metrics.styleRepairs += 1; + } + return; + } + if (styleNode && document.getElementById(STYLE_ID) !== styleNode) { + document.getElementById(STYLE_ID)?.remove(); + (document.head || document.documentElement).appendChild(styleNode); + metrics.styleRepairs += 1; } - style.dataset.dreamSkinVersion = VERSION; - style.dataset.dreamSkinStyleRevision = STYLE_REVISION; - return style; }; + installStyle(); + const applyRootState = (root) => { metrics.rootPasses += 1; - ensureStyle(root); + ensureStyle(); const shell = resolvedShell(); + setAttribute(root, "data-dream-skin", "active"); setAttribute(root, SHELL_ATTR, shell); setStyleProperty(root, "--dream-skin-art", `url("${artUrl}")`); applyTheme(root, shell); applyArtMetadata(root); - root.classList.add("codex-dream-skin"); return shell; }; - const syncRouteState = (shell, { layout = false } = {}) => { - metrics.routePasses += 1; - const root = document.documentElement; - if (!root) return; - shell ||= root.getAttribute(SHELL_ATTR) || resolvedShell(); - const shellMain = document.querySelector("main.main-surface") || document.querySelector("main"); - const homeIndicator = document.querySelector('[data-testid="home-icon"]'); - const home = homeIndicator?.closest('[role="main"]') || - [...document.querySelectorAll('[role="main"]')].find((candidate) => - candidate.querySelector('[data-feature="game-source"]') && - candidate.querySelector('.group\\\\/home-suggestions')) || null; - for (const candidate of document.querySelectorAll('[role="main"].dream-skin-home')) { - if (candidate !== home) candidate.classList.remove("dream-skin-home"); - } - if (home) home.classList.add("dream-skin-home"); - const homeUtilityBars = new Set(home - ? home.querySelectorAll('[class*="_homeUtilityBar_"]') - : []); - for (const candidate of document.querySelectorAll(".dream-skin-home-utility")) { - if (!homeUtilityBars.has(candidate)) candidate.classList.remove("dream-skin-home-utility"); - } - for (const candidate of homeUtilityBars) candidate.classList.add("dream-skin-home-utility"); - - if (!shellMain || !document.body) return; - if (observedShellMain !== shellMain) { - resizeObserver?.disconnect(); - resizeObserver?.observe(shellMain); - observedShellMain = shellMain; - layout = true; - } - shellMain.classList.toggle("dream-skin-home-shell", Boolean(home)); - let chrome = document.getElementById(CHROME_ID); - let created = false; - if (!chrome || chrome.parentElement !== document.body) { - chrome?.remove(); - chrome = document.createElement("div"); - chrome.id = CHROME_ID; - chrome.setAttribute("aria-hidden", "true"); - chrome.innerHTML = ` -
- - -
-
-
-
-
`; - document.body.appendChild(chrome); - created = true; - chromeParts = null; - } - if (!chromeParts || chromeParts.chrome !== chrome) { - chromeParts = { - chrome, - name: chrome.querySelector(".dream-skin-brand b"), - subtitle: chrome.querySelector(".dream-skin-brand small"), - status: chrome.querySelector(".dream-skin-status span"), - quote: chrome.querySelector(".dream-skin-quote"), - }; + const selectorHit = (key) => { + const selector = selectorByKey.get(key)?.selector; + if (!selector) return false; + try { return Boolean(document.querySelector(selector)); } catch { return false; } + }; + + const stableTestidHit = (testid) => { + const selector = stableTestidSelector(testid); + if (!selector) return false; + try { return Boolean(document.querySelector(selector)); } catch { return false; } + }; + + const partNodes = new Set(); + const queryAll = (selector) => { + if (!selector) return []; + try { return [...document.querySelectorAll(selector)]; } catch { return []; } + }; + const selectorNodes = (key) => queryAll(selectorByKey.get(key)?.selector); + const addPart = (desired, part, nodes) => { + for (const node of nodes) { + if (node && typeof node.setAttribute === "function" && !desired.has(node)) { + desired.set(node, part); + } } - setTextContent(chromeParts.name, THEME.name || "Codex Dream Skin"); - setTextContent(chromeParts.subtitle, THEME.brandSubtitle || "CODEX DREAM SKIN"); - setTextContent(chromeParts.status, THEME.statusText || "DREAM SKIN ONLINE"); - setTextContent(chromeParts.quote, THEME.quote || "MAKE SOMETHING WONDERFUL"); - if (layout || created) { - metrics.layoutReads += 1; - const shellBox = shellMain.getBoundingClientRect(); - setStyleProperty(chrome, "left", `${Math.round(shellBox.left)}px`); - setStyleProperty(chrome, "top", `${Math.round(shellBox.top)}px`); - setStyleProperty(chrome, "width", `${Math.round(shellBox.width)}px`); - setStyleProperty(chrome, "height", `${Math.round(shellBox.height)}px`); + }; + const refreshParts = () => { + metrics.partPasses += 1; + const desired = new Map(); + addPart(desired, "root", [document.documentElement]); + addPart(desired, "sidebar", selectorNodes("left-panel")); + addPart(desired, "main", selectorNodes("shell-main")); + addPart(desired, "header", selectorNodes("header-tint")); + addPart(desired, "home", selectorNodes("home-route")); + addPart(desired, "project-list", selectorNodes("project-selector")); + addPart(desired, "thread", selectorNodes("thread-surface")); + addPart(desired, "message", selectorNodes("message")); + addPart(desired, "composer", selectorNodes("composer-chrome")); + addPart(desired, "composer-toolbar", selectorNodes("composer-toolbar")); + addPart(desired, "dialog", selectorNodes("overlay-dialog")); + const homeHero = selectorNodes("home-icon")[0]?.parentElement; + addPart(desired, "home-hero", homeHero ? [homeHero] : []); + + for (const node of partNodes) { + if (!desired.has(node)) { + node.removeAttribute?.(PART_ATTR); + metrics.partWrites += 1; + } } - chrome.classList.toggle("dream-skin-home-shell", Boolean(home)); - if (chrome.dataset.dreamShell !== shell) { - chrome.dataset.dreamShell = shell; - metrics.attributeWrites += 1; + partNodes.clear(); + for (const [node, part] of desired) { + if (node.getAttribute?.(PART_ATTR) !== part) { + node.setAttribute(PART_ATTR, part); + metrics.partWrites += 1; + } + partNodes.add(node); } }; - const ensure = ({ root: rootPass = true, route = true, layout = true } = {}) => { + const removeParts = () => { + for (const node of partNodes) node.removeAttribute?.(PART_ATTR); + partNodes.clear(); + for (const node of queryAll(`[${PART_ATTR}]`)) node.removeAttribute?.(PART_ATTR); + }; + + const scopeMatches = (scope, baseState, overlay) => { + const active = new Set([baseState]); + if (baseState !== "settings") active.add("all"); + if (overlay) active.add("overlay"); + const tokens = String(scope || "all").toLowerCase().match(/[a-z]+/g) || ["all"]; + return tokens.some((token) => token !== "config" && active.has(token)); + }; + + const detectScope = () => { + const overlay = selectorHit("overlay-menu") || selectorHit("overlay-dialog") || + selectorHit("overlay-popper"); + let baseState = "thread"; + if (selectorHit("appearance-radio") || stableTestidHit("theme-preview")) baseState = "settings"; + else if (selectorHit("home-icon") || selectorHit("home-route")) baseState = "home"; + else if (!selectorHit("shell-main")) baseState = "settings"; + const missingL1 = SELECTOR_CONTRACT.selectors + .filter((entry) => entry.tier === "L1" && entry.required && + scopeMatches(entry.scope, baseState, overlay) && !selectorHit(entry.key)) + .map((entry) => entry.key); + return { + state: overlay ? "overlay" : baseState, + baseState, + overlay, + // Settings replaces (or partially replaces) the app shell on macOS and + // can retain a shell on Windows. It is therefore always an L0 scope; + // never treat the absence of the home/thread L1 anchors as a failure. + level: baseState === "settings" || missingL1.length ? "L0" : "L1", + missingL1, + }; + }; + + const refreshScope = () => { + metrics.routePasses += 1; + const scope = detectScope(); + const state = window[STATE_KEY]; + if (state?.installToken === installToken) state.scope = scope; + return scope; + }; + + const ensure = ({ root: rootPass = true, scope: scopePass = false, parts: partPass = false } = {}) => { if (window[DISABLED_KEY]) return; const root = document.documentElement; if (!root) return; metrics.ensureCalls += 1; - const shell = rootPass ? applyRootState(root) : null; - if (route) syncRouteState(shell, { layout }); + if (rootPass) applyRootState(root); + if (partPass) refreshParts(); + if (scopePass) refreshScope(); }; const cleanup = () => { const state = window[STATE_KEY]; if (state?.installToken !== installToken) return false; window[DISABLED_KEY] = true; - document.documentElement?.classList.remove("codex-dream-skin"); - document.documentElement?.removeAttribute(SHELL_ATTR); - for (const name of ART_ATTRS) document.documentElement?.removeAttribute(name); - document.documentElement?.style.removeProperty("--dream-skin-art"); - for (const name of THEME_VARIABLES) document.documentElement?.style.removeProperty(name); - document.querySelectorAll(".dream-skin-home").forEach((node) => node.classList.remove("dream-skin-home")); - document.querySelectorAll(".dream-skin-home-shell").forEach((node) => node.classList.remove("dream-skin-home-shell")); - document.querySelectorAll(".dream-skin-home-utility").forEach((node) => node.classList.remove("dream-skin-home-utility")); - document.getElementById(STYLE_ID)?.remove(); - document.getElementById(CHROME_ID)?.remove(); - state?.observer?.disconnect(); + const root = document.documentElement; + for (const name of ROOT_ATTRS) root?.removeAttribute(name); + for (const attribute of [...(root?.attributes || [])]) { + if (attribute.name.startsWith("data-dream-")) root.removeAttribute(attribute.name); + } + for (const name of THEME_VARIABLES) root?.style.removeProperty(name); + for (const property of [...(root?.style || [])]) { + if (property.startsWith("--dream-") || property.startsWith("--ds-")) { + root.style.removeProperty(property); + } + } + removeParts(); state?.rootObserver?.disconnect(); - state?.resizeObserver?.disconnect(); + state?.partObserver?.disconnect(); + if (bodyReadyHandler && typeof document.removeEventListener === "function") { + document.removeEventListener("DOMContentLoaded", bodyReadyHandler); + } if (state?.timer) clearInterval(state.timer); if (state?.scheduler?.timeout) clearTimeout(state.scheduler.timeout); - if (state?.scheduler?.frame != null && typeof cancelAnimationFrame === "function") { - cancelAnimationFrame(state.scheduler.frame); - } if (analysisTimer) clearTimeout(analysisTimer); - if (state?.resizeHandler) window.removeEventListener("resize", state.resizeHandler); if (state?.mediaHandler && state?.mediaQuery) { try { state.mediaQuery.removeEventListener("change", state.mediaHandler); } catch {} } + if (state?.navigationHandler && state?.navigation) { + try { state.navigation.removeEventListener("navigate", state.navigationHandler); } catch {} + } + if (styleSheet) { + try { + document.adoptedStyleSheets = [...document.adoptedStyleSheets] + .filter((candidate) => candidate !== styleSheet); + } catch {} + styleRegistry.delete(styleSheet); + } + styleNode?.remove(); + if (document.getElementById(STYLE_ID) === styleNode) document.getElementById(STYLE_ID)?.remove(); + if (styleRegistry.size === 0) delete window[STYLE_REGISTRY_KEY]; if (state?.artUrl) URL.revokeObjectURL(state.artUrl); delete window[STATE_KEY]; return true; }; - const scheduler = { timeout: null, frame: null, root: false, route: false, layout: false }; + const scheduler = { timeout: null, root: false, scope: false, parts: false }; const flushScheduledEnsure = () => { - if (scheduler.frame !== null && typeof cancelAnimationFrame === "function") { - cancelAnimationFrame(scheduler.frame); - } if (scheduler.timeout) clearTimeout(scheduler.timeout); - scheduler.frame = null; scheduler.timeout = null; - const pending = { root: scheduler.root, route: scheduler.route, layout: scheduler.layout }; + const pending = { root: scheduler.root, scope: scheduler.scope, parts: scheduler.parts }; scheduler.root = false; - scheduler.route = false; - scheduler.layout = false; + scheduler.scope = false; + scheduler.parts = false; ensure(pending); }; - const scheduleEnsure = ({ root = false, route = true, layout = false } = {}) => { + const scheduleEnsure = ({ root = false, scope = false, parts = false } = {}, delay = 64) => { scheduler.root ||= root; - scheduler.route ||= route; - scheduler.layout ||= layout; - if (scheduler.timeout || scheduler.frame !== null) return; - if (typeof requestAnimationFrame === "function") { - scheduler.frame = requestAnimationFrame(flushScheduledEnsure); - scheduler.timeout = setTimeout(flushScheduledEnsure, 96); - } else { - scheduler.timeout = setTimeout(flushScheduledEnsure, 64); - } + scheduler.scope ||= scope; + scheduler.parts ||= parts; + if (scheduler.timeout) return; + scheduler.timeout = setTimeout(flushScheduledEnsure, delay); }; - const observer = new MutationObserver(() => scheduleEnsure({ route: true })); - rootObserver = new MutationObserver(() => { - if (samplingNativeShell) return; - scheduleEnsure({ root: true, route: true }); - }); - const resizeHandler = () => scheduleEnsure({ route: true, layout: true }); - if (typeof ResizeObserver === "function") { - resizeObserver = new ResizeObserver(() => scheduleEnsure({ route: true, layout: true })); + if (typeof MutationObserver === "function") { + rootObserver = new MutationObserver(() => scheduleEnsure({ root: true })); + partObserver = new MutationObserver(() => scheduleEnsure({ parts: true }, 80)); } let mediaQuery = null; let mediaHandler = null; try { mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); - mediaHandler = () => scheduleEnsure({ root: true, route: true }); + mediaHandler = () => scheduleEnsure({ root: true }); } catch {} + const navigationApi = window.navigation && typeof window.navigation.addEventListener === "function" + ? window.navigation : null; + const navigationHandler = navigationApi ? () => { + metrics.navigationEvents += 1; + scheduleEnsure({ scope: true, parts: true }, 180); + } : null; + window[STATE_KEY] = { ensure, cleanup, - observer, rootObserver, - resizeObserver, + partObserver, timer: null, scheduler, - resizeHandler, mediaQuery, mediaHandler, + navigation: navigationApi, + navigationHandler, artUrl, installToken, + styleMode, + styleNode, + styleSheet, + styleRevision: STYLE_REVISION, analysis: artAnalysis, artMetadata: ART_METADATA, + scope: null, + selectorsSchema: SELECTOR_CONTRACT.schema, metrics, version: VERSION, themeId: THEME.id || "custom", revision: PAYLOAD_REVISION, - detectShellMode, + detectShellAppearance, }; const firstEnsureStartedAt = now(); - ensure({ layout: !previous || !document.getElementById(CHROME_ID) }); + ensure({ root: true, parts: true }); + const initialScope = refreshScope(); metrics.firstEnsureMs = Number((now() - firstEnsureStartedAt).toFixed(3)); - if (previous?.artUrl && previous.artUrl !== artUrl) URL.revokeObjectURL(previous.artUrl); - observer.observe(document.documentElement, { - childList: true, - subtree: true, - }); - rootObserver.observe(document.documentElement, { - attributes: true, - attributeFilter: ["class", "data-theme", "data-appearance", "data-color-mode"], - }); - if (document.body) { - rootObserver.observe(document.body, { + const observeAttributes = (node) => { + if (!rootObserver || !node) return; + rootObserver.observe(node, { attributes: true, attributeFilter: ["class", "data-theme", "data-appearance", "data-color-mode"], }); + }; + const observePartTree = (node) => { + if (!partObserver || !node) return; + partObserver.observe(node, { childList: true, subtree: true }); + }; + observeAttributes(document.documentElement); + const observeBody = () => { + observeAttributes(document.body); + observePartTree(document.body); + }; + if (document.body) observeBody(); + else if (typeof document.addEventListener === "function") { + bodyReadyHandler = () => { + if (!window[DISABLED_KEY]) { + observeBody(); + scheduleEnsure({ parts: true }, 0); + } + }; + document.addEventListener("DOMContentLoaded", bodyReadyHandler, { once: true }); } - const timer = setInterval(() => ensure(), 4000); + const timer = setInterval(() => { + metrics.safetyPasses += 1; + ensure({ root: true }); + }, 30000); window[STATE_KEY].timer = timer; - window.addEventListener("resize", resizeHandler, { passive: true }); - if (mediaHandler && mediaQuery) { + if (mediaHandler && mediaQuery && typeof mediaQuery.addEventListener === "function") { mediaQuery.addEventListener("change", mediaHandler); } + if (navigationHandler && navigationApi) { + navigationApi.addEventListener("navigate", navigationHandler); + } const analysisPromise = artAnalysis ? Promise.resolve(null) : analyzeArt(); window[STATE_KEY].analysisTimer = analysisTimer; analysisPromise.then((analysis) => { @@ -776,7 +830,7 @@ analysisCache.set(THEME.artKey, analysis); while (analysisCache.size > 8) analysisCache.delete(analysisCache.keys().next().value); } - ensure({ root: true, route: false, layout: false }); + ensure({ root: true }); }).catch(() => {}); return { installed: true, @@ -784,6 +838,8 @@ themeId: THEME.id || "custom", revision: PAYLOAD_REVISION, shell: resolvedShell(), + scope: initialScope, + styleMode, analysis: artAnalysis, }; })(__DREAM_SKIN_CSS_JSON__, __DREAM_SKIN_ART_JSON__, __DREAM_SKIN_THEME_JSON__) diff --git a/assets/inject/upstream/dream-skin/windows/dream-skin.css b/assets/inject/upstream/dream-skin/windows/dream-skin.css index 7e8d061bb..d35fe2ac0 100644 --- a/assets/inject/upstream/dream-skin/windows/dream-skin.css +++ b/assets/inject/upstream/dream-skin/windows/dream-skin.css @@ -1,662 +1,1216 @@ -:root.codex-dream-skin { - color-scheme: light; - --dream-accent: oklch(0.66 0.15 18); - --dream-accent-ink: oklch(0.98 0.006 20); - --dream-art-position: 72% 45%; - --dream-canvas: color-mix(in oklab, oklch(0.975 0.006 35) 96%, var(--dream-accent)); - --dream-surface: color-mix(in oklab, oklch(0.988 0.004 35) 97%, var(--dream-accent)); - --dream-surface-raised: color-mix(in oklab, oklch(0.995 0.003 35) 98%, var(--dream-accent)); - --dream-sidebar: color-mix(in oklab, oklch(0.965 0.006 35) 94%, var(--dream-accent)); - --dream-text: oklch(0.25 0.018 30); - --dream-text-muted: oklch(0.48 0.018 30); - --dream-line: color-mix(in oklab, oklch(0.75 0.012 30) 80%, var(--dream-accent)); - --dream-line-soft: color-mix(in oklab, transparent 76%, var(--dream-accent)); - --dream-accent-soft: color-mix(in oklab, var(--dream-accent) 14%, var(--dream-surface)); - --dream-accent-hover: color-mix(in oklab, var(--dream-accent) 20%, var(--dream-surface)); - --dream-hero-shade: color-mix(in oklab, var(--dream-surface) 93%, transparent); - --dream-ambient-opacity: .18; - --dream-shadow: 0 16px 42px color-mix(in oklab, var(--dream-accent) 9%, transparent); - --dream-immersive-edge: color-mix(in oklab, var(--dream-surface) 46%, transparent); - --dream-immersive-mid: color-mix(in oklab, var(--dream-surface) 28%, transparent); - --dream-immersive-far: color-mix(in oklab, var(--dream-surface) 14%, transparent); - --dream-immersive-sidebar: color-mix(in oklab, var(--dream-sidebar) 50%, transparent); - --dream-task-immersive-sidebar: color-mix(in oklab, var(--dream-sidebar) 72%, transparent); - --dream-immersive-composer: color-mix(in oklab, var(--dream-surface-raised) 76%, var(--dream-accent) 4%); - --dream-immersive-line: color-mix(in oklab, var(--dream-line) 76%, transparent); - --dream-task-immersive-edge: color-mix(in oklab, var(--dream-surface) 86%, transparent); - --dream-task-immersive-mid: color-mix(in oklab, var(--dream-surface) 78%, transparent); - --dream-task-immersive-far: color-mix(in oklab, var(--dream-surface) 66%, transparent); -} - -:root.codex-dream-skin.dream-theme-dark { - color-scheme: dark; - --dream-canvas: color-mix(in oklab, oklch(0.16 0.012 245) 96%, var(--dream-accent)); - --dream-surface: color-mix(in oklab, oklch(0.19 0.012 245) 95%, var(--dream-accent)); - --dream-surface-raised: color-mix(in oklab, oklch(0.23 0.012 245) 95%, var(--dream-accent)); - --dream-sidebar: color-mix(in oklab, oklch(0.145 0.014 245) 94%, var(--dream-accent)); - --dream-text: oklch(0.93 0.008 245); - --dream-text-muted: oklch(0.7 0.012 245); - --dream-line: color-mix(in oklab, oklch(0.42 0.018 245) 76%, var(--dream-accent)); - --dream-line-soft: color-mix(in oklab, transparent 78%, var(--dream-accent)); - --dream-accent-soft: color-mix(in oklab, var(--dream-accent) 18%, var(--dream-surface)); - --dream-accent-hover: color-mix(in oklab, var(--dream-accent) 27%, var(--dream-surface)); - --dream-hero-shade: color-mix(in oklab, var(--dream-surface) 91%, transparent); - --dream-ambient-opacity: .22; - --dream-shadow: 0 18px 48px color-mix(in oklab, oklch(0.06 0.01 245) 72%, transparent); - --dream-immersive-edge: color-mix(in oklab, var(--dream-surface) 43%, transparent); - --dream-immersive-mid: color-mix(in oklab, var(--dream-surface) 27%, transparent); - --dream-immersive-far: color-mix(in oklab, var(--dream-surface) 16%, transparent); - --dream-immersive-sidebar: color-mix(in oklab, var(--dream-sidebar) 48%, transparent); - --dream-task-immersive-sidebar: color-mix(in oklab, var(--dream-sidebar) 70%, transparent); - --dream-immersive-composer: color-mix(in oklab, var(--dream-surface-raised) 88%, var(--dream-accent) 5%); - --dream-immersive-line: color-mix(in oklab, var(--dream-line) 82%, transparent); - --dream-task-immersive-edge: color-mix(in oklab, var(--dream-surface) 82%, transparent); - --dream-task-immersive-mid: color-mix(in oklab, var(--dream-surface) 74%, transparent); - --dream-task-immersive-far: color-mix(in oklab, var(--dream-surface) 60%, transparent); -} - -html.codex-dream-skin body { - background: var(--dream-canvas) !important; - color: var(--dream-text) !important; - font-family: "Segoe UI Variable Text", "Segoe UI", "Microsoft YaHei UI", system-ui, sans-serif !important; -} - -html.codex-dream-skin aside.app-shell-left-panel { - color: var(--dream-text) !important; - background: var(--dream-sidebar) !important; - border-color: var(--dream-line-soft) !important; - box-shadow: inset -1px 0 var(--dream-line-soft) !important; +/* Canonical cross-platform skin. Run tools/sync-runtime-assets.mjs after editing. */ +:root[data-dream-skin="active"] { + color-scheme: dark !important; + --ds-bg: #111318; + --ds-panel: #191c22; + --ds-panel-2: #20242b; + --ds-green: #8298a3; + --ds-lime: #a0adb3; + --ds-cyan: #8da397; + --ds-purple: #9d94a3; + --ds-text: #edf0f1; + --ds-muted: #a3aaae; + --ds-line: rgba(130, 152, 163, .24); + --ds-bg-rgb: 17 19 24; + --ds-panel-rgb: 25 28 34; + --ds-panel-2-rgb: 32 36 43; + --ds-text-rgb: 237 240 241; + --ds-muted-rgb: 163 170 174; + --ds-accent-rgb: 130 152 163; + --ds-secondary-rgb: 141 163 151; + --ds-highlight-rgb: var(--ds-secondary-rgb); + --ds-accent: var(--ds-green); + --ds-accent-soft: var(--ds-lime); + --ds-secondary: var(--ds-cyan); + --ds-highlight: var(--ds-purple); + --ds-on-accent: rgb(var(--ds-bg-rgb) / 1); + --ds-focus-x: var(--dream-skin-focus-x, var(--dream-art-focus-x, 50%)); + --ds-focus-y: var(--dream-skin-focus-y, var(--dream-art-focus-y, 50%)); + --ds-art-position: var(--dream-art-position, var(--dream-skin-art-position, var(--ds-focus-x) var(--ds-focus-y))); + --ds-safe-side: left; + --ds-hero-scrim: linear-gradient(90deg, + rgb(var(--ds-bg-rgb) / .90) 0%, + rgb(var(--ds-bg-rgb) / .76) 50%, + rgb(var(--ds-bg-rgb) / .18) 84%, + transparent 100%); + --ds-task-shade: linear-gradient(90deg, + rgb(var(--ds-bg-rgb) / .56) 0%, + rgb(var(--ds-bg-rgb) / .36) 48%, + rgb(var(--ds-bg-rgb) / .12) 100%); + --ds-task-fade: linear-gradient(180deg, + rgb(var(--ds-bg-rgb) / .10) 0%, + rgb(var(--ds-bg-rgb) / .18) 32%, + rgb(var(--ds-bg-rgb) / .76) 68%, + rgb(var(--ds-bg-rgb) / 1) 100%); + --ds-task-full-veil: linear-gradient( + rgb(var(--ds-bg-rgb) / .10), + rgb(var(--ds-bg-rgb) / .10)); + --ds-immersive-edge: rgb(var(--ds-bg-rgb) / .40); + --ds-immersive-mid: rgb(var(--ds-bg-rgb) / .26); + --ds-immersive-far: rgb(var(--ds-bg-rgb) / .16); + --ds-immersive-sidebar: rgb(var(--ds-panel-rgb) / .46); + --ds-task-immersive-sidebar: rgb(var(--ds-panel-rgb) / .70); + --ds-immersive-chrome: rgb(var(--ds-panel-rgb) / .28); + --ds-immersive-composer: rgb(var(--ds-panel-rgb) / .44); + --ds-immersive-composer-solid: color-mix(in srgb, + rgb(var(--ds-panel-2-rgb)) 88%, + rgb(var(--ds-muted-rgb)) 12%); + --ds-immersive-line: rgb(var(--ds-muted-rgb) / .42); + --ds-task-immersive-edge: rgb(var(--ds-bg-rgb) / .82); + --ds-task-immersive-mid: rgb(var(--ds-bg-rgb) / .74); + --ds-task-immersive-far: rgb(var(--ds-bg-rgb) / .60); +} + +html[data-dream-skin="active"][data-dream-shell="light"] { + color-scheme: light !important; + --ds-bg: #f3f5f6; + --ds-panel: #fafbfb; + --ds-panel-2: #e9edef; + --ds-text: #22272a; + --ds-muted: #687176; + --ds-line: rgba(91, 111, 121, .18); + --ds-bg-rgb: 243 245 246; + --ds-panel-rgb: 250 251 251; + --ds-panel-2-rgb: 233 237 239; + --ds-text-rgb: 34 39 42; + --ds-muted-rgb: 104 113 118; + --ds-accent-rgb: 84 112 126; + --ds-secondary-rgb: 102 128 116; + --ds-highlight-rgb: var(--ds-secondary-rgb); + --ds-on-accent: rgb(var(--ds-panel-rgb) / 1); + --ds-hero-scrim: linear-gradient(90deg, + rgb(var(--ds-panel-rgb) / .96) 0%, + rgb(var(--ds-panel-rgb) / .82) 50%, + rgb(var(--ds-panel-rgb) / .20) 84%, + transparent 100%); + --ds-task-shade: linear-gradient(90deg, + rgb(var(--ds-bg-rgb) / .68) 0%, + rgb(var(--ds-bg-rgb) / .40) 48%, + rgb(var(--ds-bg-rgb) / .12) 100%); + --ds-task-fade: linear-gradient(180deg, + rgb(var(--ds-bg-rgb) / .08) 0%, + rgb(var(--ds-bg-rgb) / .22) 34%, + rgb(var(--ds-bg-rgb) / .78) 70%, + rgb(var(--ds-bg-rgb) / 1) 100%); + --ds-task-full-veil: linear-gradient( + rgb(var(--ds-panel-rgb) / .10), + rgb(var(--ds-panel-rgb) / .10)); + --ds-immersive-edge: rgb(var(--ds-panel-rgb) / .44); + --ds-immersive-mid: rgb(var(--ds-panel-rgb) / .28); + --ds-immersive-far: rgb(var(--ds-panel-rgb) / .14); + --ds-immersive-sidebar: rgb(var(--ds-panel-rgb) / .48); + --ds-task-immersive-sidebar: rgb(var(--ds-panel-rgb) / .72); + --ds-immersive-chrome: rgb(var(--ds-panel-rgb) / .34); + --ds-immersive-composer: rgb(var(--ds-panel-rgb) / .46); + --ds-immersive-composer-solid: rgb(var(--ds-panel-rgb) / .74); + --ds-immersive-line: rgb(var(--ds-accent-rgb) / .24); + --ds-task-immersive-edge: rgb(var(--ds-panel-rgb) / .86); + --ds-task-immersive-mid: rgb(var(--ds-panel-rgb) / .78); + --ds-task-immersive-far: rgb(var(--ds-panel-rgb) / .66); +} + +/* Native token surfaces (dropdown/popover) resolve from Codex's own + appearanceTheme, not the skin; when that disagrees with data-dream-shell the + popover keeps the opposite palette (#233). Remap the dropdown token family to + skin variables so those surfaces inherit the theme in both shells. */ +html[data-dream-skin="active"] { + --color-token-dropdown-background: var(--ds-panel); +} + +html[data-dream-skin="active"] [class~="bg-token-dropdown-background"] { + color: var(--ds-text); + --color-token-foreground: var(--ds-text); + --color-token-text-secondary: var(--ds-muted); + --color-token-muted-foreground: var(--ds-muted); + --color-token-description-foreground: var(--ds-muted); + --color-token-list-hover-background: rgb(var(--ds-panel-2-rgb) / .9); + --color-token-border: var(--ds-line); + --color-token-border-default: var(--ds-line); +} + +html[data-dream-skin="active"]:is([data-dream-art-safe="left"], [data-dream-art-safe-area="left"]) { + --ds-safe-side: left; + --ds-art-position: 100% var(--ds-focus-y); +} + +html[data-dream-skin="active"]:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"]) { + --ds-safe-side: right; + --ds-art-position: 0% var(--ds-focus-y); + --ds-hero-scrim: linear-gradient(270deg, + rgb(var(--ds-bg-rgb) / .90) 0%, + rgb(var(--ds-bg-rgb) / .76) 50%, + rgb(var(--ds-bg-rgb) / .18) 84%, + transparent 100%); + --ds-task-shade: linear-gradient(270deg, + rgb(var(--ds-bg-rgb) / .56) 0%, + rgb(var(--ds-bg-rgb) / .36) 48%, + rgb(var(--ds-bg-rgb) / .12) 100%); +} + +html[data-dream-skin="active"][data-dream-shell="light"]:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"]) { + --ds-hero-scrim: linear-gradient(270deg, + rgb(var(--ds-panel-rgb) / .96) 0%, + rgb(var(--ds-panel-rgb) / .82) 50%, + rgb(var(--ds-panel-rgb) / .20) 84%, + transparent 100%); + --ds-task-shade: linear-gradient(270deg, + rgb(var(--ds-bg-rgb) / .68) 0%, + rgb(var(--ds-bg-rgb) / .40) 48%, + rgb(var(--ds-bg-rgb) / .12) 100%); +} + +html[data-dream-skin="active"]:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"]) { + --ds-safe-side: center; + --ds-hero-scrim: linear-gradient(90deg, + transparent 0%, + rgb(var(--ds-bg-rgb) / .68) 25%, + rgb(var(--ds-bg-rgb) / .78) 50%, + rgb(var(--ds-bg-rgb) / .68) 75%, + transparent 100%); + --ds-task-shade: radial-gradient(ellipse at center, + rgb(var(--ds-bg-rgb) / .48) 0%, + rgb(var(--ds-bg-rgb) / .30) 52%, + rgb(var(--ds-bg-rgb) / .10) 100%); +} + +html[data-dream-skin="active"][data-dream-shell="light"]:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"]) { + --ds-hero-scrim: linear-gradient(90deg, + transparent 0%, + rgb(var(--ds-panel-rgb) / .72) 25%, + rgb(var(--ds-panel-rgb) / .88) 50%, + rgb(var(--ds-panel-rgb) / .72) 75%, + transparent 100%); + --ds-task-shade: radial-gradient(ellipse at center, + rgb(var(--ds-bg-rgb) / .58) 0%, + rgb(var(--ds-bg-rgb) / .34) 52%, + rgb(var(--ds-bg-rgb) / .10) 100%); +} + +html[data-dream-skin="active"]:is([data-dream-art-safe="none"], [data-dream-art-safe-area="none"]) { + --ds-safe-side: none; + --ds-hero-scrim: linear-gradient(180deg, + rgb(var(--ds-bg-rgb) / .34), + rgb(var(--ds-bg-rgb) / .20)); + --ds-task-shade: linear-gradient(180deg, + rgb(var(--ds-bg-rgb) / .28), + rgb(var(--ds-bg-rgb) / .14)); +} + +html[data-dream-skin="active"][data-dream-shell="light"]:is([data-dream-art-safe="none"], [data-dream-art-safe-area="none"]) { + --ds-hero-scrim: linear-gradient(180deg, + rgb(var(--ds-panel-rgb) / .46), + rgb(var(--ds-panel-rgb) / .28)); + --ds-task-shade: linear-gradient(180deg, + rgb(var(--ds-bg-rgb) / .34), + rgb(var(--ds-bg-rgb) / .16)); +} + +html[data-dream-skin="active"] body { + background: var(--ds-bg) !important; + color: var(--ds-text) !important; + font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC", "Microsoft YaHei UI", "Segoe UI", system-ui, sans-serif !important; +} + +html[data-dream-skin="active"] body::before { + content: none; +} + +html[data-dream-skin="active"] aside.app-shell-left-panel { + background: + linear-gradient(180deg, + rgb(var(--ds-panel-rgb) / .98), + rgb(var(--ds-bg-rgb) / .96)) !important; + border: 1px solid var(--ds-line) !important; + border-left: 0 !important; + border-radius: 0 16px 16px 0 !important; + box-shadow: 10px 0 30px rgb(var(--ds-bg-rgb) / .22) !important; + color: var(--ds-text) !important; backdrop-filter: none !important; } -html.codex-dream-skin aside.app-shell-left-panel nav { - background: transparent !important; -} +html[data-dream-skin="active"] aside.app-shell-left-panel nav { background: transparent !important; } -html.codex-dream-skin aside.app-shell-left-panel button { - color: var(--dream-text) !important; - transition: background-color 180ms cubic-bezier(.22, 1, .36, 1), color 180ms cubic-bezier(.22, 1, .36, 1) !important; +html[data-dream-skin="active"] aside.app-shell-left-panel button, +html[data-dream-skin="active"] aside.app-shell-left-panel a { + color: var(--ds-text) !important; + transition: background-color .18s cubic-bezier(.22, 1, .36, 1), + border-color .18s cubic-bezier(.22, 1, .36, 1), + color .18s cubic-bezier(.22, 1, .36, 1) !important; } -html.codex-dream-skin aside.app-shell-left-panel button:hover { - background: var(--dream-accent-soft) !important; +html[data-dream-skin="active"] aside.app-shell-left-panel [class*="text-token-foreground"] { + color: var(--ds-text) !important; } -html.codex-dream-skin aside.app-shell-left-panel [class~="bg-token-list-hover-background"], -html.codex-dream-skin aside.app-shell-left-panel [aria-current="page"] { - color: var(--dream-text) !important; - background: var(--dream-accent-hover) !important; - box-shadow: inset 0 0 0 1px var(--dream-line-soft) !important; +html[data-dream-skin="active"] aside.app-shell-left-panel [class*="text-token-input-placeholder-foreground"] { + color: rgb(var(--ds-muted-rgb) / .92) !important; } -html.codex-dream-skin aside.app-shell-left-panel svg, -html.codex-dream-skin aside.app-shell-left-panel [class*="text-token"] { - color: currentColor; +html[data-dream-skin="active"] aside.app-shell-left-panel svg { + color: rgb(var(--ds-muted-rgb) / .96) !important; } -html.codex-dream-skin main.main-surface { - position: relative; - isolation: isolate; - overflow: hidden !important; - color: var(--dream-text) !important; - background: var(--dream-surface) !important; - border-color: var(--dream-line-soft) !important; - box-shadow: inset 0 1px color-mix(in oklab, var(--dream-surface-raised) 72%, transparent) !important; +html[data-dream-skin="active"] aside.app-shell-left-panel button:hover, +html[data-dream-skin="active"] aside.app-shell-left-panel a:hover { + background: rgb(var(--ds-accent-rgb) / .09) !important; + color: var(--ds-text) !important; + transform: none !important; } -html.codex-dream-skin main.main-surface > header.app-header-tint { - /* Preserve Codex's native fixed header geometry and side-panel controls. */ - color: var(--dream-text) !important; - background: color-mix(in oklab, var(--dream-surface) 94%, transparent) !important; - border-color: var(--dream-line-soft) !important; - backdrop-filter: blur(12px) saturate(1.05) !important; +html[data-dream-skin="active"] aside.app-shell-left-panel button:hover svg, +html[data-dream-skin="active"] aside.app-shell-left-panel a:hover svg { + color: var(--ds-accent) !important; } -/* Wide art also reaches the native application menu, which sits outside the - main surface and needs its own dark-theme legibility layer. */ -html.codex-dream-skin.dream-theme-dark [class~="group/application-menu-top-bar"] { - color: var(--dream-text) !important; - background: color-mix(in oklab, var(--dream-sidebar) 90%, transparent) !important; - border-bottom: 1px solid var(--dream-line-soft) !important; - box-shadow: 0 1px 12px color-mix(in oklab, var(--dream-canvas) 72%, transparent) !important; - backdrop-filter: blur(16px) saturate(.9) !important; +html[data-dream-skin="active"] aside.app-shell-left-panel button[aria-label^="切换模式"] { + background: transparent !important; + border: 0 !important; + color: var(--ds-accent) !important; + font-family: inherit !important; + font-size: 19px !important; + font-weight: 800 !important; + text-shadow: none !important; } -html.codex-dream-skin.dream-theme-dark [class~="group/application-menu-top-bar"] button, -html.codex-dream-skin.dream-theme-dark [class~="group/application-menu-top-bar"] svg { - color: var(--dream-text) !important; - text-shadow: 0 1px 2px color-mix(in oklab, var(--dream-canvas) 82%, transparent) !important; +html[data-dream-skin="active"] aside.app-shell-left-panel button[aria-label^="切换模式"]::after { + content: " ·"; + color: var(--ds-secondary); } -#codex-dream-skin-chrome { - display: none !important; - pointer-events: none !important; +html[data-dream-skin="active"] aside.app-shell-left-panel [class~="bg-token-list-hover-background"], +html[data-dream-skin="active"] aside.app-shell-left-panel [aria-current="page"] { + background: rgb(var(--ds-accent-rgb) / .12) !important; + border: 1px solid rgb(var(--ds-accent-rgb) / .22) !important; + box-shadow: 0 4px 16px rgb(var(--ds-bg-rgb) / .12) !important; + color: var(--ds-text) !important; } -html.codex-dream-skin [role="main"] { - color: var(--dream-text); +html[data-dream-skin="active"] aside.app-shell-left-panel [aria-current="page"] svg { + color: var(--ds-accent) !important; } -/* Task routes use the art as a quiet atmospheric layer. Wide banners are shown - at their natural width instead of being enlarged and center-cropped. */ -html.codex-dream-skin .dream-task { - position: relative; +html[data-dream-skin="active"] main.main-surface { + position: relative !important; isolation: isolate; - min-height: 100%; - background: var(--dream-surface) !important; + overflow: hidden !important; + background: var(--ds-bg) !important; + border: 1px solid var(--ds-line) !important; + border-right: 0 !important; + border-bottom: 0 !important; + border-radius: 16px 0 0 0 !important; + box-shadow: -8px 0 28px rgb(var(--ds-bg-rgb) / .18) !important; } -html.codex-dream-skin .dream-task::before { +html[data-dream-skin="active"] main.main-surface:not(:has([role="main"]))::before { content: ""; position: absolute; - z-index: 0; inset: 0; + z-index: 0; pointer-events: none; - opacity: var(--dream-ambient-opacity); - background-image: var(--dream-art); + background-image: var(--ds-task-fade), var(--ds-task-shade), var(--dream-skin-art); background-repeat: no-repeat; - background-position: var(--dream-art-position); - background-size: cover; - mask-image: linear-gradient(to bottom, oklch(0 0 0) 0, oklch(0 0 0 / .92) 38%, transparent 88%); + background-position: center, center, var(--ds-art-position); + background-size: 100% 100%, 100% 100%, cover; + opacity: .78; } -html.codex-dream-skin.dream-art-wide .dream-task::before { - background-position: center top; - background-size: 100% auto; +html[data-dream-skin="active"][data-dream-shell="light"] main.main-surface:not(:has([role="main"]))::before { + opacity: .72; } -html.codex-dream-skin.dream-art-wide:not(.dream-task-banner) .dream-task::before { - background-position: var(--dream-art-position); - background-size: cover; - mask-image: linear-gradient(to bottom, oklch(0 0 0) 0, oklch(0 0 0 / .92) 44%, oklch(0 0 0 / .72) 100%); +html[data-dream-skin="active"]:is([data-dream-art-wide="true"], [data-dream-art-aspect="wide"], [data-dream-art-aspect="ultrawide"]) + main.main-surface:not(:has([role="main"]))::before { + background-position: center, center, var(--ds-art-position); + background-size: 100% 100%, 100% 100%, cover; } -html.codex-dream-skin.dream-task-banner .dream-task::before { - bottom: auto; - height: min(46vh, 520px); - opacity: calc(var(--dream-ambient-opacity) * 1.8); - mask-image: linear-gradient(to bottom, oklch(0 0 0) 0, oklch(0 0 0 / .9) 52%, transparent 100%); +/* Studio's full mode keeps the task artwork at normal strength with only the + baseline readability veil. Ambient mode deliberately uses the heavier task + fade/shade below, while off removes the artwork entirely. */ +html[data-dream-skin="active"]:is([data-dream-task-mode="full"], [data-dream-art-task-mode="full"]) + main.main-surface:not(:has([role="main"]))::before { + inset: 0; + height: auto; + background-image: var(--ds-task-full-veil), var(--dream-skin-art); + background-position: center, var(--ds-art-position); + background-size: 100% 100%, cover; + opacity: 1; } -html.codex-dream-skin.dream-task-off .dream-task::before { - display: none; +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"]) + main.main-surface:not(:has([role="main"]))::before { + inset: 0; + height: auto; +} + +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"]:has(main.main-surface):not(:has(main.main-surface [role="main"])) body { + background-image: var(--dream-skin-art) !important; + background-position: var(--ds-art-position) !important; + background-size: cover !important; + background-repeat: no-repeat !important; + background-attachment: fixed !important; } -html.codex-dream-skin .dream-task > * { +html[data-dream-skin="active"]:is([data-dream-task-mode="full"], [data-dream-art-task-mode="full"])[data-dream-art-wide="true"]:has(main.main-surface):not(:has(main.main-surface [role="main"])) body { + background-image: var(--dream-skin-art) !important; + background-position: var(--ds-art-position) !important; + background-size: cover !important; + background-repeat: no-repeat !important; + background-attachment: fixed !important; +} + +html[data-dream-skin="active"]:is([data-dream-task-mode="full"], [data-dream-art-task-mode="full"])[data-dream-art-wide="true"]:has(main.main-surface):not(:has(main.main-surface [role="main"])) + aside.app-shell-left-panel { + background: linear-gradient(90deg, + var(--ds-immersive-sidebar), + var(--ds-immersive-edge) 100%) !important; + border: 0 !important; + border-radius: 0 !important; + box-shadow: none !important; + backdrop-filter: none !important; +} + +html[data-dream-skin="active"]:is([data-dream-task-mode="full"], [data-dream-art-task-mode="full"])[data-dream-art-wide="true"]:has(main.main-surface):not(:has(main.main-surface [role="main"])) + aside.app-shell-left-panel::after { + background: transparent !important; +} + +html[data-dream-skin="active"]:is([data-dream-task-mode="full"], [data-dream-art-task-mode="full"])[data-dream-art-wide="true"]:has(main.main-surface):not(:has(main.main-surface [role="main"])) + main.main-surface:not(:has([role="main"])) { + background: linear-gradient(90deg, + var(--ds-immersive-edge), + var(--ds-immersive-mid) 64%, + var(--ds-immersive-far)) !important; + border: 0 !important; + border-radius: 0 !important; + box-shadow: none !important; +} + +html[data-dream-skin="active"]:is([data-dream-task-mode="full"], [data-dream-art-task-mode="full"])[data-dream-art-wide="true"] + main.main-surface:not(:has([role="main"]))::before { + content: none; +} + +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"]:has(main.main-surface):not(:has(main.main-surface [role="main"])) + aside.app-shell-left-panel { + background: linear-gradient(90deg, + var(--ds-task-immersive-sidebar), + var(--ds-task-immersive-edge) 100%) !important; + border: 0 !important; + border-radius: 0 !important; + box-shadow: none !important; + backdrop-filter: none !important; +} + +/* Native sidebar resize chrome inherits the sidebar background and extends + 20px into the main surface. Keeping it clear preserves one continuous image. */ +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"]:has(main.main-surface):not(:has(main.main-surface [role="main"])) + aside.app-shell-left-panel::after { + background: transparent !important; +} + +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"]:has(main.main-surface):not(:has(main.main-surface [role="main"])) + main.main-surface:not(:has([role="main"])) { + background: linear-gradient(90deg, + var(--ds-task-immersive-edge), + var(--ds-task-immersive-mid) 64%, + var(--ds-task-immersive-far)) !important; + border: 0 !important; + border-radius: 0 !important; + box-shadow: none !important; +} + +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"] + main.main-surface:not(:has([role="main"]))::before { + content: none; +} + +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"]:has(main.main-surface):not(:has(main.main-surface [role="main"])) + .composer-surface-chrome { + background: var(--ds-immersive-composer-solid) !important; + border: 0 !important; + box-shadow: + 0 10px 30px rgb(var(--ds-bg-rgb) / .20), + inset 0 0 0 1px var(--ds-immersive-line), + inset 0 1px rgb(var(--ds-text-rgb) / .12) !important; + backdrop-filter: none !important; +} + +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"]:has(main.main-surface):not(:has(main.main-surface [role="main"])) + main.main-surface > header.app-header-tint { + background: transparent !important; + border-bottom: 0 !important; + box-shadow: none !important; + backdrop-filter: none !important; +} + +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"], [data-dream-task-mode="full"], [data-dream-art-task-mode="full"])[data-dream-art-wide="true"] + main.main-surface:not(:has([role="main"])) [class*="_markdown"] { + color: var(--ds-text) !important; + text-shadow: + 0 1px 2px rgb(var(--ds-bg-rgb) / .82), + 0 0 10px rgb(var(--ds-bg-rgb) / .58); +} + +html[data-dream-skin="active"][data-dream-shell="light"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"], [data-dream-task-mode="full"], [data-dream-art-task-mode="full"])[data-dream-art-wide="true"] + main.main-surface:not(:has([role="main"])) [class*="_markdown"] { + text-shadow: + 0 1px 2px rgb(var(--ds-panel-rgb) / .92), + 0 0 10px rgb(var(--ds-panel-rgb) / .72); +} + +html[data-dream-skin="active"]:is([data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"]) + main.main-surface:not(:has([role="main"]))::before { + inset: 0 0 auto; + height: clamp(280px, 46vh, 520px); + background-position: center top, center top, var(--ds-art-position); + background-size: 100% 100%, 100% 100%, cover; +} + +html[data-dream-skin="active"]:is([data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"]):is([data-dream-art-wide="true"], [data-dream-art-aspect="wide"], [data-dream-art-aspect="ultrawide"]) + main.main-surface:not(:has([role="main"]))::before { + inset: 0; + height: auto; + background-position: center, center, var(--ds-art-position); + background-size: 100% 100%, 100% 100%, cover; +} + +html[data-dream-skin="active"]:is([data-dream-task-mode="off"], [data-dream-art-task-mode="off"]) + main.main-surface:not(:has([role="main"]))::before { + content: none; +} + +html[data-dream-skin="active"] main.main-surface:not(:has([role="main"])) > :not(header.app-header-tint) { position: relative; z-index: 1; } -html.codex-dream-skin .dream-task article, -html.codex-dream-skin .dream-task [data-message-author-role] { - color: var(--dream-text); +html[data-dream-skin="active"] main.main-surface:not(:has([role="main"])) [role="main"] { + background: transparent !important; + color: var(--ds-text) !important; + text-shadow: + 0 1px 2px rgb(var(--ds-bg-rgb) / .72), + 0 0 10px rgb(var(--ds-bg-rgb) / .46); +} + +html[data-dream-skin="active"][data-dream-shell="light"] main.main-surface:not(:has([role="main"])) [role="main"] { + text-shadow: none; +} + +/* Newer Codex builds paint the list/detail panes (pull requests, the chat + review/terminal/browser/files side panel) with token surfaces instead of + [role="main"]. The side panel can coexist with a visible home shell, so this + transparency is deliberately not gated on the home-route selector. The nested + surfaces all carry the same token, so they are cleared everywhere and the + translucent pane tint is painted once, on the bordered outer wrapper. */ +html[data-dream-skin="active"] main.main-surface + :is(div, section, aside)[class~="bg-token-main-surface-primary"] { + background: transparent !important; +} + +html[data-dream-skin="active"] main.main-surface + div[class~="bg-token-main-surface-primary"][class~="border-l"] { + background: rgb(var(--ds-panel-rgb) / .62) !important; + backdrop-filter: blur(10px) saturate(106%) !important; +} + +html[data-dream-skin="active"] main.main-surface:not(:has([role="main"])) article { + border: 1px solid rgb(var(--ds-muted-rgb) / .12); + background: rgb(var(--ds-panel-rgb) / .44); + box-shadow: 0 8px 24px rgb(var(--ds-bg-rgb) / .10); + backdrop-filter: blur(7px) saturate(105%); +} + +html[data-dream-skin="active"][data-dream-shell="light"] main.main-surface:not(:has([role="main"])) article { + background: rgb(var(--ds-panel-rgb) / .72); +} + +html[data-dream-skin="active"][data-dream-shell="light"] + main.main-surface:has([role="main"])::after, +html[data-dream-skin="active"][data-dream-shell="light"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"]::before { + text-shadow: none; +} + +html[data-dream-skin="active"] main.main-surface > header.app-header-tint { + /* Preserve Codex's native fixed header geometry and side-panel controls. */ + background: rgb(var(--ds-panel-rgb) / .90) !important; + border-bottom: 1px solid var(--ds-line) !important; + backdrop-filter: blur(14px) saturate(108%) !important; +} + +/* Decorative chrome lives on the native shell; no injected positioning host is needed. */ +html[data-dream-skin="active"] + main.main-surface:not(:has([role="main"])) > header.app-header-tint::before { + content: var(--dream-skin-name, "Codex Dream Skin") " · " + var(--dream-skin-brand-subtitle, "CODEX DREAM SKIN"); + position: absolute; + left: 22px; + top: 4px; + z-index: 2; + max-width: min(42%, 360px); + overflow: hidden; + color: var(--ds-accent); + font: 800 11px/1.3 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + letter-spacing: .03em; + text-overflow: ellipsis; + text-shadow: 0 1px 12px rgb(var(--ds-bg-rgb) / .32); + white-space: nowrap; + pointer-events: none; +} + +html[data-dream-skin="active"] + main.main-surface:not(:has([role="main"])) > header.app-header-tint::after { + content: var(--dream-skin-status, "DREAM SKIN ONLINE"); + position: absolute; + right: 84px; + top: 13px; + z-index: 2; + max-width: 28%; + overflow: hidden; + color: var(--ds-muted); + font: 700 9px/1.2 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + letter-spacing: .08em; + text-overflow: ellipsis; + text-shadow: 0 1px 10px rgb(var(--ds-bg-rgb) / .32); + white-space: nowrap; + pointer-events: none; +} + +html[data-dream-skin="active"] + main.main-surface:has([role="main"])::after { + content: var(--dream-skin-quote, "MAKE SOMETHING WONDERFUL"); + position: absolute; + right: 28px; + bottom: 72px; + z-index: 2; + pointer-events: none; + color: rgb(var(--ds-accent-rgb) / .74); + font: italic 14px/1.2 "Segoe Print", "Comic Sans MS", cursive; + letter-spacing: 0; + text-shadow: 0 0 13px rgb(var(--ds-accent-rgb) / .30); + transform: rotate(-3deg); +} + +html[data-dream-skin="active"][data-dream-shell="light"] + main.main-surface:not(:has([role="main"])) > header.app-header-tint::before, +html[data-dream-skin="active"][data-dream-shell="light"] + main.main-surface:not(:has([role="main"])) > header.app-header-tint::after { + text-shadow: none; +} + +/* Windows keeps this native bar outside main; macOS simply has no match. */ +html[data-dream-skin="active"][data-dream-shell="dark"] + [class~="group/application-menu-top-bar"] { + color: var(--ds-text) !important; + background: rgb(var(--ds-panel-rgb) / .90) !important; + border-bottom: 1px solid var(--ds-line) !important; + box-shadow: 0 1px 12px rgb(var(--ds-bg-rgb) / .72) !important; + backdrop-filter: blur(16px) saturate(90%) !important; } -/* The home route keeps the source image expressive, with a semantic safe-area - wash selected from image focus metadata. */ -html.codex-dream-skin .dream-home { - --thread-content-max-width: min(1180px, calc(100cqw - 56px)) !important; +html[data-dream-skin="active"][data-dream-shell="dark"] + [class~="group/application-menu-top-bar"] :is(button, svg) { + color: var(--ds-text) !important; + text-shadow: 0 1px 2px rgb(var(--ds-bg-rgb) / .82) !important; +} + +html[data-dream-skin="active"] main.main-surface [role="main"] { + background: transparent !important; + scrollbar-color: rgb(var(--ds-accent-rgb) / .38) transparent; +} + +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) { + --thread-content-max-width: min(1180px, calc(100cqw - 44px)) !important; overflow-x: hidden !important; - background: var(--dream-surface) !important; } -.dream-home > div:first-child { +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child { min-height: 100% !important; - padding-top: 24px !important; + padding-top: 15px !important; } -.dream-home > div:first-child > div:first-child { - flex: 0 0 clamp(390px, 42cqw, 510px) !important; - min-height: 390px !important; +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child { + flex: 0 0 440px !important; + min-height: 440px !important; align-items: flex-start !important; padding-bottom: 0 !important; } -.dream-home > div:first-child > div:first-child > div:first-child { +/* Codex 26.721+: home-route's first child now only wraps the (usually empty) + native .home-banners slot; the real content column moved out to become + this wrapper's sibling instead of its descendant. The two rules above + still assume the old nesting and force this now-mostly-empty wrapper to + 100% height / 440px, which pushes the real sibling content off-screen + (see #244). Override with higher specificity only when that new slot is + present, so pre-26.721 layouts are untouched. */ +html[data-dream-skin="active"] + [role="main"]:has([data-testid="home-icon"]) > div:first-child:has(> .home-banners) { + min-height: 0 !important; + padding-top: 0 !important; +} + +html[data-dream-skin="active"] + [role="main"]:has([data-testid="home-icon"]) > div:first-child:has(> .home-banners) > .home-banners { + flex: 0 1 auto !important; + min-height: 0 !important; +} + +html[data-dream-skin="active"] + [role="main"]:has([data-testid="home-icon"]) > div:first-child:has(> .home-banners) > .home-banners > div:first-child { + height: auto !important; + min-height: 0 !important; + border: 0 !important; + border-radius: 0 !important; + background: transparent !important; + box-shadow: none !important; +} + +html[data-dream-skin="active"] + [role="main"]:has([data-testid="home-icon"]) > div:first-child:has(> .home-banners) > .home-banners > div:first-child::before { + content: none !important; +} + +/* Codex 26.721+: this chain (3-6 first-child levels deep) targets the old + compact hero-art card that used to live inside div:first-child's second + level. That level is now the empty .home-banners slot (see #244), so on + the new DOM these rules currently match nothing and the compact hero card + is dormant rather than misrendered. Left as-is pending a real fixture of + the new nested shape; the critical content-invisible bug is the block + above this one. */ +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child { position: relative !important; isolation: isolate; - width: calc(100% - 56px) !important; - max-width: 1180px !important; - height: clamp(230px, 27cqw, 326px) !important; - min-height: 230px !important; + width: calc(100% - 44px) !important; + max-width: none !important; + height: clamp(248px, 31cqw, 376px) !important; + min-height: 248px !important; flex: 0 1 auto !important; + overflow: visible !important; padding: 0 !important; - overflow: hidden !important; - border: 1px solid var(--dream-line) !important; - border-radius: 20px !important; - background-image: var(--dream-art) !important; + border: 1px solid rgb(var(--ds-accent-rgb) / .30) !important; + border-radius: 22px !important; + background-color: var(--ds-panel) !important; + background-image: var(--dream-skin-art) !important; background-repeat: no-repeat !important; + background-position: var(--ds-art-position) !important; background-size: cover !important; - background-position: var(--dream-art-position) !important; - box-shadow: var(--dream-shadow) !important; -} - -html.codex-dream-skin.dream-art-wide .dream-home > div:first-child > div:first-child > div:first-child { - background-size: cover !important; + box-shadow: 0 16px 38px rgb(var(--ds-bg-rgb) / .30) !important; } -.dream-home > div:first-child > div:first-child > div:first-child::before { +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child::before { content: ""; position: absolute; + inset: 0 auto 0 0; z-index: 0; - inset: 0; + width: 62%; + border-radius: 21px 0 0 21px; pointer-events: none; - background: linear-gradient(90deg, var(--dream-hero-shade) 0 32%, transparent 70%); + background: var(--ds-hero-scrim); } -html.codex-dream-skin.dream-safe-right .dream-home > div:first-child > div:first-child > div:first-child::before { - background: linear-gradient(270deg, var(--dream-hero-shade) 0 32%, transparent 70%); +html[data-dream-skin="active"]:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"]) + [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child::before { + inset: 0 0 0 auto; } -html.codex-dream-skin.dream-safe-center .dream-home > div:first-child > div:first-child > div:first-child::before { - background: radial-gradient(ellipse at center, var(--dream-hero-shade) 0 23%, transparent 72%); +html[data-dream-skin="active"]:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"]) + [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child::before { + inset: 0 14%; + width: auto; + border-radius: 0; } -html.codex-dream-skin.dream-safe-none .dream-home > div:first-child > div:first-child > div:first-child::before { - background: transparent; +html[data-dream-skin="active"]:is([data-dream-art-safe="none"], [data-dream-art-safe-area="none"]) + [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child::before { + inset: 0; + width: auto; + border-radius: 21px; } -.dream-home > div:first-child > div:first-child > div:first-child > div:first-child { +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child { position: relative; z-index: 1; - box-sizing: border-box; height: 100%; align-items: center !important; justify-content: flex-start !important; - padding: 30px 40px; + padding: 0 38px; } -.dream-home > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { - width: min(48%, 520px) !important; - align-items: flex-start !important; - gap: 0 !important; +html[data-dream-skin="active"]:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"]) + [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child { + justify-content: flex-end !important; } -html.codex-dream-skin.dream-safe-right .dream-home > div:first-child > div:first-child > div:first-child > div:first-child { - justify-content: flex-end !important; +html[data-dream-skin="active"]:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"]) + [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child { + justify-content: center !important; } -html.codex-dream-skin.dream-safe-right .dream-home > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { - align-items: flex-end !important; - text-align: right !important; +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { + width: min(46%, 520px) !important; + align-items: flex-start !important; + gap: 0 !important; } -html.codex-dream-skin.dream-safe-center .dream-home > div:first-child > div:first-child > div:first-child > div:first-child { - justify-content: center !important; +html[data-dream-skin="active"]:is([data-dream-art-safe="right"], [data-dream-art-safe-area="right"]) + [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { + align-items: flex-end !important; + text-align: right !important; } -html.codex-dream-skin.dream-safe-center .dream-home > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { +html[data-dream-skin="active"]:is([data-dream-art-safe="center"], [data-dream-art-safe-area="center"]) + [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { + width: min(58%, 640px) !important; align-items: center !important; text-align: center !important; } -.dream-home [data-testid="home-icon"] { - display: none !important; -} +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-testid="home-icon"] { display: none !important; } -.dream-home [data-feature="game-source"] { - display: flex !important; - flex-direction: column !important; - align-items: inherit !important; - justify-content: flex-start !important; +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"] { + display: block !important; max-width: 100% !important; - color: var(--dream-text) !important; - font-size: 28px !important; - line-height: 1.25 !important; - font-weight: 720 !important; + color: var(--ds-text) !important; + font-size: clamp(19px, 1.85vw, 27px) !important; + line-height: 1.28 !important; + font-weight: 760 !important; text-align: inherit !important; - text-shadow: 0 1px 16px var(--dream-surface); + text-shadow: none !important; opacity: 1 !important; pointer-events: auto !important; } -.dream-home [data-feature="game-source"] button { - margin: 0 4px; - padding: 2px 7px; - border: 1px solid var(--dream-line); +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"]::before { + content: var(--dream-skin-name, "Codex Dream Skin"); + display: block; + margin-bottom: 9px; + color: var(--ds-accent); + font: 800 12px/1.3 "Microsoft YaHei UI", sans-serif; + letter-spacing: .10em; + text-shadow: 0 0 14px rgb(var(--ds-accent-rgb) / .26); +} + +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"]::after { + content: var(--dream-skin-tagline, "把喜欢的画面变成可交互的 Codex 工作台。"); + display: block; + margin-top: 13px; + color: rgb(var(--ds-text-rgb) / .76); + font-size: 13px; + line-height: 1.5; + font-weight: 500; + letter-spacing: 0; +} + +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"] button { + margin: 0 5px; + padding: 2px 9px 3px; + border: 1px solid rgb(var(--ds-accent-rgb) / .36); border-radius: 999px; - color: var(--dream-text) !important; - background: var(--dream-accent-soft) !important; - text-underline-offset: 4px; + background: rgb(var(--ds-accent-rgb) / .10); + color: var(--ds-accent) !important; + text-decoration-color: rgb(var(--ds-accent-rgb) / .56) !important; + text-underline-offset: 5px; } -.dream-home > div:first-child > div:first-child > div:first-child > div:nth-child(2) { - left: 0 !important; - right: 0 !important; +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"] button::before { + content: var(--dream-skin-project-prefix, "选择项目 · "); + font-size: .46em; + font-weight: 700; + vertical-align: middle; + opacity: .86; +} + +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:nth-child(2) { + left: 14px !important; + right: 14px !important; top: 100% !important; - margin-top: 16px !important; + margin-top: 13px !important; } -.dream-home .group\/home-suggestions { - overflow: visible !important; +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions { overflow: visible !important; } + +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button { + position: relative !important; + min-height: 118px !important; + padding: 14px 13px 12px !important; + align-items: stretch !important; + justify-content: flex-start !important; + text-align: center !important; + border: 1px solid rgb(var(--ds-muted-rgb) / .18) !important; + border-radius: 18px !important; + background: rgb(var(--ds-panel-rgb) / .90) !important; + color: var(--ds-text) !important; + font-weight: 600 !important; + line-height: 1.45 !important; + box-shadow: 0 8px 22px rgb(var(--ds-bg-rgb) / .18) !important; + backdrop-filter: blur(10px) saturate(105%) !important; + transform: translateY(0); + transition: transform .18s cubic-bezier(.22, 1, .36, 1), + box-shadow .18s cubic-bezier(.22, 1, .36, 1), + border-color .18s cubic-bezier(.22, 1, .36, 1) !important; } -.dream-home .group\/home-suggestions button { - min-height: 112px !important; - padding: 16px 14px !important; - color: var(--dream-text) !important; - border: 1px solid var(--dream-line-soft) !important; - border-radius: 16px !important; - background: var(--dream-surface-raised) !important; - box-shadow: 0 8px 24px color-mix(in oklab, var(--dream-accent) 6%, transparent) !important; - transition: transform 180ms cubic-bezier(.22, 1, .36, 1), border-color 180ms cubic-bezier(.22, 1, .36, 1), background-color 180ms cubic-bezier(.22, 1, .36, 1) !important; +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button [class~="text-token-text-primary"] { + color: var(--ds-text) !important; } -.dream-home .group\/home-suggestions button:hover { +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button:hover { transform: translateY(-2px) !important; - border-color: var(--dream-line) !important; - background: var(--dream-accent-soft) !important; + border-color: rgb(var(--ds-accent-rgb) / .42) !important; + box-shadow: 0 12px 28px rgb(var(--ds-bg-rgb) / .24) !important; } -.dream-home .group\/home-suggestions button > span:first-child > span:first-child { +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button > span:first-child > span:first-child { width: 38px; height: 38px; - display: grid !important; - place-items: center; - border-radius: 12px; - color: var(--dream-accent-ink) !important; - background: var(--dream-accent) !important; - box-shadow: 0 0 0 6px var(--dream-accent-soft); + /* Native spans carry justify-start; grid + place-items cannot override + justify-content, so the glyph parks at the circle's left edge. (#176) */ + display: flex !important; + align-items: center !important; + justify-content: center !important; + margin: 0 auto; + border: 1px solid rgb(var(--ds-accent-rgb) / .24); + border-radius: 50%; + color: var(--ds-accent) !important; + background: rgb(var(--ds-accent-rgb) / .12); + box-shadow: 0 0 0 6px rgb(var(--ds-accent-rgb) / .05); } -.dream-home .group\/home-suggestions button svg { - width: 20px !important; - height: 20px !important; - color: currentColor !important; +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button svg { + width: 21px !important; + height: 21px !important; + display: block !important; + margin: 0 !important; + flex: 0 0 auto !important; + color: var(--ds-accent) !important; } -html.codex-dream-skin .composer-surface-chrome { - color: var(--dream-text) !important; - border: 1px solid var(--dream-line) !important; - border-radius: 18px !important; - background: color-mix(in oklab, var(--dream-surface-raised) 95%, transparent) !important; - box-shadow: 0 12px 34px color-mix(in oklab, var(--dream-accent) 8%, transparent) !important; - backdrop-filter: blur(14px) saturate(1.06) !important; -} +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button > span:first-child { justify-content: center !important; } +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button > span:last-child { align-items: center !important; text-align: center !important; } -html.codex-dream-skin .ProseMirror { - color: var(--dream-text) !important; - caret-color: var(--dream-accent) !important; -} - -html.codex-dream-skin button[class~="bg-token-foreground"] { - color: var(--dream-accent-ink) !important; - background: var(--dream-accent) !important; - box-shadow: none !important; +html[data-dream-skin="active"] .composer-surface-chrome { + overflow: visible !important; + border: 1px solid rgb(var(--ds-muted-rgb) / .18) !important; + border-radius: 22px !important; + background: rgb(var(--ds-panel-rgb) / .94) !important; + box-shadow: 0 10px 28px rgb(var(--ds-bg-rgb) / .24) !important; + backdrop-filter: blur(16px) saturate(108%) !important; } -html.codex-dream-skin [data-message-author-role], -html.codex-dream-skin article { - color: var(--dream-text); -} +html[data-dream-skin="active"] .composer-surface-chrome::before { content: none !important; } -/* A 16:9 or wider image is painted once on the native window. The sidebar, - title bar and route surface become coordinated readability layers instead - of repeating or cropping the image inside individual panels. */ -html.codex-dream-skin.dream-art-wide:has(main.main-surface.dream-home-shell) body, -html.codex-dream-skin.dream-art-wide:is(.dream-task-ambient, .dream-task-banner):has(main.main-surface:not(.dream-home-shell)) body { - background-color: var(--dream-canvas) !important; - background-image: var(--dream-art) !important; - background-position: var(--dream-art-position) !important; +/* A wide image can carry the home screen too. The native hero remains live, + but the artwork is painted once on the window instead of repeated in a card. */ +html[data-dream-skin="active"][data-dream-art-wide="true"]:has(main.main-surface [role="main"]) body { + background-image: var(--dream-skin-art) !important; + background-position: var(--ds-art-position) !important; background-size: cover !important; background-repeat: no-repeat !important; background-attachment: fixed !important; } -html.codex-dream-skin.dream-art-wide:has(main.main-surface.dream-home-shell) - aside.app-shell-left-panel, -html.codex-dream-skin.dream-art-wide:is(.dream-task-ambient, .dream-task-banner):has(main.main-surface:not(.dream-home-shell)) +html[data-dream-skin="active"][data-dream-art-wide="true"]:has(main.main-surface [role="main"]) aside.app-shell-left-panel { background: linear-gradient(90deg, - var(--dream-immersive-sidebar), - var(--dream-immersive-edge)) !important; + var(--ds-immersive-sidebar), + var(--ds-immersive-edge) 100%) !important; border: 0 !important; border-radius: 0 !important; box-shadow: none !important; backdrop-filter: none !important; } -html.codex-dream-skin.dream-art-wide:is(.dream-task-ambient, .dream-task-banner):has(main.main-surface:not(.dream-home-shell)) - aside.app-shell-left-panel { - background: linear-gradient(90deg, - var(--dream-task-immersive-sidebar), - var(--dream-task-immersive-edge)) !important; -} - -html.codex-dream-skin.dream-art-wide:has(main.main-surface.dream-home-shell) - aside.app-shell-left-panel::after, -html.codex-dream-skin.dream-art-wide:is(.dream-task-ambient, .dream-task-banner):has(main.main-surface:not(.dream-home-shell)) +html[data-dream-skin="active"][data-dream-art-wide="true"]:has(main.main-surface [role="main"]) aside.app-shell-left-panel::after { background: transparent !important; } -html.codex-dream-skin.dream-art-wide main.main-surface.dream-home-shell, -html.codex-dream-skin.dream-art-wide:is(.dream-task-ambient, .dream-task-banner) main.main-surface:not(.dream-home-shell) { +html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface:has([role="main"]) { background: linear-gradient(90deg, - var(--dream-immersive-edge), - var(--dream-immersive-mid) 64%, - var(--dream-immersive-far)) !important; + var(--ds-immersive-edge), + var(--ds-immersive-mid) 64%, + var(--ds-immersive-far)) !important; border: 0 !important; border-radius: 0 !important; box-shadow: none !important; } -html.codex-dream-skin.dream-art-wide:is(.dream-task-ambient, .dream-task-banner) main.main-surface:not(.dream-home-shell) { - background: linear-gradient(90deg, - var(--dream-task-immersive-edge), - var(--dream-task-immersive-mid) 64%, - var(--dream-task-immersive-far)) !important; -} - -html.codex-dream-skin.dream-art-wide:is(.dream-task-ambient, .dream-task-banner) .dream-task { - background: transparent !important; -} - -html.codex-dream-skin.dream-art-wide:is(.dream-task-ambient, .dream-task-banner) .dream-task::before { - content: none !important; -} - -html.codex-dream-skin.dream-art-wide main.main-surface > header.app-header-tint { +html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface:has([role="main"]) + > header.app-header-tint { background: transparent !important; border-bottom: 0 !important; box-shadow: none !important; backdrop-filter: none !important; - text-shadow: 0 1px 2px color-mix(in oklab, var(--dream-canvas) 86%, transparent), - 0 0 8px color-mix(in oklab, var(--dream-canvas) 58%, transparent) !important; } -html.codex-dream-skin.dream-art-wide main.main-surface > header.app-header-tint svg { - filter: drop-shadow(0 1px 2px color-mix(in oklab, var(--dream-canvas) 72%, transparent)); +html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface + > header.app-header-tint { + color: var(--ds-text) !important; + text-shadow: + 0 1px 2px rgb(var(--ds-bg-rgb) / .86), + 0 0 8px rgb(var(--ds-bg-rgb) / .52) !important; } -html.codex-dream-skin.dream-art-wide main.main-surface [role="main"] { - background: transparent !important; +html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface + > header.app-header-tint svg { + color: rgb(var(--ds-muted-rgb) / .96) !important; + filter: drop-shadow(0 1px 2px rgb(var(--ds-bg-rgb) / .72)); } -html.codex-dream-skin.dream-art-wide main.main-surface.dream-home-shell - .dream-home > div:first-child > div:first-child > div:first-child { +html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface:has([role="main"]) + [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child { border: 0 !important; border-radius: 0 !important; background: transparent !important; box-shadow: none !important; } -html.codex-dream-skin.dream-art-wide main.main-surface.dream-home-shell - .dream-home > div:first-child > div:first-child > div:first-child::before { +html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface:has([role="main"]) + [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child::before { content: none !important; } -html.codex-dream-skin.dream-art-wide main.main-surface.dream-home-shell - .dream-home .group\/home-suggestions button { - background: color-mix(in oklab, var(--dream-surface-raised) 58%, transparent) !important; - box-shadow: 0 8px 22px color-mix(in oklab, var(--dream-canvas) 18%, transparent), - inset 0 0 0 1px var(--dream-immersive-line) !important; - backdrop-filter: blur(8px) saturate(1.04) !important; +html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface:has([role="main"]) + [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button { + background: rgb(var(--ds-panel-rgb) / .56) !important; + box-shadow: + 0 8px 22px rgb(var(--ds-bg-rgb) / .16), + inset 0 0 0 1px rgb(var(--ds-muted-rgb) / .20) !important; + backdrop-filter: blur(8px) saturate(104%) !important; } -/* The composer owns exactly one solid readability surface. Native bottom - gradients are cleared below so the control never appears double stacked. */ -html.codex-dream-skin.dream-art-wide .composer-surface-chrome { - color: var(--dream-text) !important; - background: var(--dream-immersive-composer) !important; +html[data-dream-skin="active"][data-dream-art-wide="true"] .composer-surface-chrome { + background: var(--ds-immersive-composer-solid) !important; border: 0 !important; - box-shadow: 0 10px 30px color-mix(in oklab, var(--dream-canvas) 20%, transparent), - inset 0 0 0 1px var(--dream-immersive-line) !important; + box-shadow: + 0 10px 30px rgb(var(--ds-bg-rgb) / .20), + inset 0 0 0 1px var(--ds-immersive-line), + inset 0 1px rgb(var(--ds-text-rgb) / .12) !important; backdrop-filter: none !important; } -html.codex-dream-skin.dream-art-wide .composer-surface-chrome::before { - content: none !important; +html[data-dream-skin="active"][data-dream-shell="light"][data-dream-art-wide="true"] + .composer-surface-chrome { + box-shadow: + 0 10px 28px rgb(var(--ds-bg-rgb) / .14), + inset 0 0 0 1px var(--ds-immersive-line), + inset 0 1px rgb(var(--ds-panel-rgb) / .72) !important; + backdrop-filter: blur(8px) saturate(102%) !important; } -/* Current Codex builds render the home project picker in a hashed utility bar. - Merge it with the composer so the bottom control reads as one surface. */ -html.codex-dream-skin .dream-home-utility { - border: 1px solid var(--dream-line) !important; - border-bottom: 0 !important; - border-radius: 18px 18px 0 0 !important; - background: var(--dream-immersive-composer) !important; - box-shadow: none !important; +html[data-dream-skin="active"][data-dream-shell="light"] + .composer-surface-chrome p.placeholder::after { + color: rgb(var(--ds-muted-rgb) / .78) !important; + opacity: 1 !important; } -html.codex-dream-skin .dream-home:has(.dream-home-utility) .composer-surface-chrome { - border-radius: 0 0 18px 18px !important; - box-shadow: 0 10px 30px color-mix(in oklab, var(--dream-canvas) 20%, transparent), - inset 1px 0 var(--dream-immersive-line), - inset -1px 0 var(--dream-immersive-line), - inset 0 -1px var(--dream-immersive-line) !important; +/* Current Codex builds render the home project picker as a separate opaque + cap above the composer. Join both native controls into one continuous + surface while leaving task and utility-route inputs untouched. */ +html[data-dream-skin="active"] [class*="_homeUtilityBar_"] { + top: 0 !important; + width: 100% !important; + margin-inline: 0 !important; + padding-inline: 18px !important; + border-radius: 22px 22px 0 0 !important; + background: rgb(var(--ds-panel-rgb) / .94) !important; + box-shadow: + inset 1px 0 rgb(var(--ds-muted-rgb) / .18), + inset -1px 0 rgb(var(--ds-muted-rgb) / .18), + inset 0 1px rgb(var(--ds-muted-rgb) / .18) !important; + backdrop-filter: blur(16px) saturate(108%) !important; +} + +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]):has([class*="_homeUtilityBar_"]) + .composer-surface-chrome { + border: 0 !important; + border-radius: 0 0 22px 22px !important; + box-shadow: + 0 10px 28px rgb(var(--ds-bg-rgb) / .24), + inset 1px 0 rgb(var(--ds-muted-rgb) / .18), + inset -1px 0 rgb(var(--ds-muted-rgb) / .18), + inset 0 -1px rgb(var(--ds-muted-rgb) / .18) !important; +} + +html[data-dream-skin="active"][data-dream-art-wide="true"] [class*="_homeUtilityBar_"] { + background: var(--ds-immersive-composer-solid) !important; + box-shadow: + inset 1px 0 var(--ds-immersive-line), + inset -1px 0 var(--ds-immersive-line), + inset 0 1px rgb(var(--ds-text-rgb) / .12) !important; + backdrop-filter: none !important; } -html.codex-dream-skin.dream-art-wide:is(.dream-task-ambient, .dream-task-banner) - .dream-aux-panel-layer { - background: transparent !important; +html[data-dream-skin="active"][data-dream-art-wide="true"] + [role="main"]:has([data-testid="home-icon"]):has([class*="_homeUtilityBar_"]) .composer-surface-chrome { + box-shadow: + 0 10px 30px rgb(var(--ds-bg-rgb) / .20), + inset 1px 0 var(--ds-immersive-line), + inset -1px 0 var(--ds-immersive-line), + inset 0 -1px var(--ds-immersive-line) !important; +} + +html[data-dream-skin="active"][data-dream-shell="light"][data-dream-art-wide="true"] + [class*="_homeUtilityBar_"] { + box-shadow: + inset 1px 0 var(--ds-immersive-line), + inset -1px 0 var(--ds-immersive-line), + inset 0 1px rgb(var(--ds-panel-rgb) / .72) !important; + backdrop-filter: blur(8px) saturate(102%) !important; +} + +html[data-dream-skin="active"] [class*="_homeUtilityBar_"] button, +html[data-dream-skin="active"] .composer-surface-chrome button:not([class~="bg-token-foreground"]) { + color: var(--ds-muted) !important; +} + +html[data-dream-skin="active"] [class*="_homeUtilityBar_"] button svg, +html[data-dream-skin="active"] .composer-surface-chrome button svg { + color: currentColor !important; +} + +html[data-dream-skin="active"] [class*="_homeUtilityBar_"] button *, +html[data-dream-skin="active"] .composer-surface-chrome button:not([class~="bg-token-foreground"]) * { + color: currentColor !important; } -html.codex-dream-skin.dream-art-wide:is(.dream-task-ambient, .dream-task-banner) - .dream-aux-panel-right { - border-left-color: var(--dream-immersive-line) !important; +html[data-dream-skin="active"] [class*="_homeUtilityBar_"] button:hover, +html[data-dream-skin="active"] .composer-surface-chrome button:not([class~="bg-token-foreground"]):hover { + color: var(--ds-text) !important; + background: rgb(var(--ds-accent-rgb) / .10) !important; } -html.codex-dream-skin.dream-art-wide:is(.dream-task-ambient, .dream-task-banner) - .dream-aux-panel-bottom { - border-top-color: var(--dream-immersive-line) !important; +html[data-dream-skin="active"] .composer-surface-chrome p.placeholder::after { + color: rgb(var(--ds-muted-rgb) / .82) !important; + opacity: 1 !important; } -html.codex-dream-skin.dream-art-wide:is(.dream-task-ambient, .dream-task-banner) - .dream-aux-panel-bottom [data-codex-terminal="true"] { - background: color-mix(in oklab, var(--dream-surface-raised) 58%, transparent) !important; +/* Search routes ship an opaque sticky band and input surface. Keep the + control legible while allowing the selected image to remain continuous. */ +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"])[data-dream-art-wide="true"] + main.main-surface:not(:has([role="main"])) div.sticky:has(input[type="text"]) { + background: transparent !important; } -/* Plugins, scheduled tasks and search routes ship opaque sticky wrappers. - Clear only full-window or sticky route chrome, leaving functional cards and - their hover states intact. */ -html.codex-dream-skin.dream-art-wide:is(.dream-task-ambient, .dream-task-banner) - main.main-surface:not(.dream-home-shell) div.sticky:has(input[type="text"]), -html.codex-dream-skin.dream-art-wide:is(.dream-task-ambient, .dream-task-banner) - main.main-surface:not(.dream-home-shell) div.sticky:has(input[type="text"])::after { +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"])[data-dream-art-wide="true"] + main.main-surface:not(:has([role="main"])) div.sticky:has(input[type="text"])::after { background: transparent !important; } -html.codex-dream-skin.dream-art-wide:is(.dream-task-ambient, .dream-task-banner) - main.main-surface:not(.dream-home-shell) div.no-drag:has(> input[type="text"]) { - background: var(--dream-immersive-composer) !important; +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"])[data-dream-art-wide="true"] + main.main-surface:not(:has([role="main"])) div.no-drag:has(> input[type="text"]) { + background: var(--ds-immersive-composer) !important; border: 0 !important; - box-shadow: inset 0 0 0 1px var(--dream-immersive-line) !important; + box-shadow: + 0 8px 22px rgb(var(--ds-bg-rgb) / .16), + inset 0 0 0 1px var(--ds-immersive-line) !important; backdrop-filter: none !important; } -html.codex-dream-skin.dream-art-wide:is(.dream-task-ambient, .dream-task-banner) - main.main-surface:not(.dream-home-shell) +/* Pull Requests and similar utility routes wrap their content in a native + full-size opaque surface. Only clear full-window wrappers, not cards. */ +html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"])[data-dream-art-wide="true"] + main.main-surface:not(:has([role="main"])) [class~="bg-token-main-surface-primary"][class~="h-full"][class~="w-full"] { background: transparent !important; } -html.codex-dream-skin.dream-art-wide main.main-surface .app-shell-main-content-frame { +html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface + .app-shell-main-content-frame { border-top: 0 !important; } -html.codex-dream-skin.dream-art-wide main.main-surface .app-shell-main-content-top-fade { +html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface + .app-shell-main-content-top-fade, +html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface + [class*="_MainContentTopFade_"] { display: none !important; background: transparent !important; } -html.codex-dream-skin.dream-art-wide main.main-surface +/* Codex paints a second opaque fade behind the sticky composer. The composer + already owns its readable surface, so retaining this layer creates a false + bottom panel and makes the control look duplicated. */ +html[data-dream-skin="active"][data-dream-art-wide="true"] main.main-surface .thread-scroll-container .bg-gradient-to-t.from-token-main-surface-primary { background: transparent !important; } -@media (max-width: 1120px) { - html.codex-dream-skin .dream-home { - --thread-content-max-width: min(940px, calc(100cqw - 36px)) !important; - } - - .dream-home > div:first-child > div:first-child > div:first-child { - width: calc(100% - 36px) !important; - } +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) div:has(> .horizontal-scroll-fade-mask .group\/project-selector) { + position: relative; + padding-top: 28px !important; + border: 1px solid rgb(var(--ds-muted-rgb) / .16); + border-bottom: 0; + background: rgb(var(--ds-panel-rgb) / .92) !important; } -@media (max-width: 900px) { - .dream-home > div:first-child { - padding-top: 16px !important; - } - - .dream-home > div:first-child > div:first-child { - flex-basis: 370px !important; - min-height: 370px !important; - } +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) div:has(> .horizontal-scroll-fade-mask .group\/project-selector)::before { + content: var(--dream-skin-project-label, "选择项目"); + position: absolute; + left: 13px; + top: 6px; + z-index: 2; + color: var(--ds-muted); + font-size: 12px; + font-weight: 720; + letter-spacing: 0; + white-space: nowrap; +} - .dream-home > div:first-child > div:first-child > div:first-child { - height: 220px !important; - min-height: 220px !important; - border-radius: 16px !important; - } +html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/project-selector > button { + border-color: rgb(var(--ds-accent-rgb) / .22) !important; + background: rgb(var(--ds-accent-rgb) / .08) !important; + color: var(--ds-text) !important; + box-shadow: 0 3px 12px rgb(var(--ds-bg-rgb) / .12) !important; +} - .dream-home > div:first-child > div:first-child > div:first-child > div:first-child { - padding: 24px; - } +html[data-dream-skin="active"] .ProseMirror { + color: var(--ds-text) !important; + caret-color: var(--ds-accent) !important; +} - .dream-home > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { - width: 62% !important; - } +html[data-dream-skin="active"] button[class~="bg-token-foreground"] { + background: var(--ds-accent) !important; + color: var(--ds-on-accent) !important; + box-shadow: 0 5px 14px rgb(var(--ds-accent-rgb) / .20) !important; +} - .dream-home [data-feature="game-source"] { - font-size: 22px !important; - } +html[data-dream-skin="active"] article, +html[data-dream-skin="active"] [data-message-author-role] { border-radius: 16px; } - .dream-home .group\/home-suggestions button { - min-height: 104px !important; - padding: 12px 10px !important; - font-size: 12px !important; - } +@media (max-width: 1120px) { + html[data-dream-skin="active"] + main.main-surface:has([role="main"])::after { content: none; } + html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) { --thread-content-max-width: min(940px, calc(100cqw - 30px)) !important; } + html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child { width: calc(100% - 28px) !important; } } -@media (max-width: 680px) { - .dream-home > div:first-child > div:first-child > div:first-child::before, - html.codex-dream-skin.dream-safe-right .dream-home > div:first-child > div:first-child > div:first-child::before, - html.codex-dream-skin.dream-safe-center .dream-home > div:first-child > div:first-child > div:first-child::before { - background: color-mix(in oklab, var(--dream-surface) 78%, transparent); +@media (max-width: 900px) { + html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child { + flex-basis: 408px !important; + min-height: 408px !important; } - - .dream-home > div:first-child > div:first-child > div:first-child > div:first-child, - html.codex-dream-skin.dream-safe-right .dream-home > div:first-child > div:first-child > div:first-child > div:first-child, - html.codex-dream-skin.dream-safe-center .dream-home > div:first-child > div:first-child > div:first-child > div:first-child { - justify-content: center !important; + html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child { + height: 232px !important; + min-height: 232px !important; } - - .dream-home > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child, - html.codex-dream-skin.dream-safe-right .dream-home > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child, - html.codex-dream-skin.dream-safe-center .dream-home > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { - width: min(88%, 440px) !important; - align-items: center !important; - text-align: center !important; + html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child { padding: 0 26px; } + html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) > div:first-child > div:first-child > div:first-child > div:first-child > div:first-child { + width: min(58%, 460px) !important; } + html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"] { font-size: 18px !important; } + html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"]::before { margin-bottom: 7px; font-size: 10px; } + html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) [data-feature="game-source"]::after { margin-top: 9px; font-size: 11px; } + html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button { min-height: 112px !important; font-size: 12px !important; } } @media (prefers-reduced-motion: reduce) { - html.codex-dream-skin *, - html.codex-dream-skin *::before, - html.codex-dream-skin *::after { + html[data-dream-skin="active"] *, + html[data-dream-skin="active"] *::before, + html[data-dream-skin="active"] *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; + animation-duration: .01ms !important; + animation-iteration-count: 1 !important; } } diff --git a/assets/inject/upstream/dream-skin/windows/renderer-inject.js b/assets/inject/upstream/dream-skin/windows/renderer-inject.js index e4b6812c4..68e1e5c9b 100644 --- a/assets/inject/upstream/dream-skin/windows/renderer-inject.js +++ b/assets/inject/upstream/dream-skin/windows/renderer-inject.js @@ -1,459 +1,863 @@ -((cssText, artDataUrl, rawConfig) => { +// Canonical cross-platform renderer. Run tools/sync-runtime-assets.mjs after editing. +((cssText, artDataUrl, themeConfig) => { + const SELECTOR_CONTRACT = {"schema":"codex-dream-skin-selectors/1","selectors":[{"key":"shell-main","selector":"main.main-surface","tier":"L1","scope":"all","required":true},{"key":"left-panel","selector":"aside.app-shell-left-panel","tier":"L1","scope":"all","required":true},{"key":"header-tint","selector":"header.app-header-tint","tier":"L1","scope":"all","required":true},{"key":"home-icon","selector":"[data-testid=\"home-icon\"]","tier":"L1","scope":"home","required":true},{"key":"home-route","selector":"[role=\"main\"]:has([data-testid=\"home-icon\"])","tier":"L1","scope":"home","required":true},{"key":"home-route-css","selector":"[role=\"main\"]","tier":"L1","scope":"home","required":true},{"key":"home-banners","selector":".home-banners","tier":"L2","scope":"home","required":false},{"key":"composer-chrome","selector":".composer-surface-chrome","tier":"L2","scope":"home+thread","required":false},{"key":"composer-toolbar","selector":".composer-surface-chrome [class*=\"_footer_\"]","tier":"L2","scope":"home+thread","required":false},{"key":"home-utility","selector":"[class*=\"_homeUtilityBar_\"]","tier":"L2","scope":"home","required":false},{"key":"game-source","selector":"[data-feature=\"game-source\"]","tier":"L2","scope":"home","required":false},{"key":"home-suggestions","selector":".group\\/home-suggestions","tier":"L2","scope":"home","required":false},{"key":"project-selector","selector":".group\\/project-selector","tier":"L2","scope":"home config","required":false},{"key":"markdown","selector":"[class*=\"_markdown\"]","tier":"L2","scope":"thread","required":false},{"key":"thread-surface","selector":".thread-scroll-container","tier":"L2","scope":"thread","required":false},{"key":"message","selector":"[data-message-author-role]","tier":"L2","scope":"thread","required":false},{"key":"appearance-radio","selector":"input[name=\"appearance-theme\"]","tier":"L2","scope":"settings","required":false},{"key":"overlay-menu","selector":"[role=\"menu\"]","tier":"L2","scope":"overlay","required":false},{"key":"overlay-dialog","selector":"[role=\"dialog\"]","tier":"L2","scope":"overlay","required":false},{"key":"overlay-popper","selector":"[data-radix-popper-content-wrapper]","tier":"L2","scope":"overlay","required":false}],"stableTestids":["app-shell-header-context-menu-surface","home-icon","theme-preview"]}; const STATE_KEY = "__CODEX_DREAM_SKIN_STATE__"; + const DISABLED_KEY = "__CODEX_DREAM_SKIN_DISABLED__"; + const STYLE_REGISTRY_KEY = "__CODEX_DREAM_SKIN_STYLE_SHEETS__"; const STYLE_ID = "codex-dream-skin-style"; - const CHROME_ID = "codex-dream-skin-chrome"; - const ROOT_CLASSES = [ - "codex-dream-skin", - "dream-theme-light", - "dream-theme-dark", - "dream-art-wide", - "dream-art-standard", - "dream-focus-left", - "dream-focus-center", - "dream-focus-right", - "dream-safe-left", - "dream-safe-center", - "dream-safe-right", - "dream-safe-none", - "dream-task-ambient", - "dream-task-banner", - "dream-task-off", + const MAIN_COMPAT_ATTR = "data-codex-plus-dream-skin-main-compat"; + const SHELL_ATTR = "data-dream-shell"; + const PART_ATTR = "data-ds-part"; + const ROOT_ATTRS = [ + "data-dream-skin", SHELL_ATTR, + "data-dream-art-wide", "data-dream-art-safe", "data-dream-task-mode", + "data-dream-art-safe-area", "data-dream-art-task-mode", "data-dream-art-aspect", + "data-dream-art-ready", ]; - const ROOT_PROPERTIES = [ - "--dream-art", - "--dream-art-position", - "--dream-focus-x", - "--dream-focus-y", - "--dream-accent", - "--dream-accent-ink", - "--dream-image-luma", + const VERSION = __DREAM_SKIN_VERSION_JSON__; + const STYLE_REVISION = __DREAM_SKIN_STYLE_REVISION_JSON__; + const PAYLOAD_REVISION = __DREAM_SKIN_PAYLOAD_REVISION_JSON__; + const THEME = themeConfig && typeof themeConfig === "object" ? themeConfig : {}; + const ART = THEME.art && typeof THEME.art === "object" ? THEME.art : {}; + const ART_METADATA = THEME.artMetadata && typeof THEME.artMetadata === "object" + ? THEME.artMetadata : null; + const ANALYSIS_CACHE_KEY = "__CODEX_DREAM_SKIN_ANALYSIS_CACHE__"; + const THEME_VARIABLES = [ + "--ds-bg", "--ds-panel", "--ds-panel-2", "--ds-green", "--ds-lime", + "--ds-cyan", "--ds-purple", "--ds-text", "--ds-muted", "--ds-line", + "--ds-bg-rgb", "--ds-panel-rgb", "--ds-panel-2-rgb", "--ds-accent-rgb", + "--ds-accent-alt-rgb", "--ds-secondary-rgb", "--ds-highlight-rgb", + "--ds-text-rgb", "--ds-muted-rgb", "--ds-line-rgb", + "--dream-art-focus-x", "--dream-art-focus-y", "--dream-art-position", + "--dream-skin-focus-x", "--dream-skin-focus-y", "--dream-skin-art-position", + "--dream-skin-name", "--dream-skin-tagline", "--dream-skin-project-prefix", + "--dream-skin-project-label", "--dream-skin-brand-subtitle", "--dream-skin-status", + "--dream-skin-quote", "--dream-skin-art", + "--ds-theme-color-background", "--ds-theme-color-panel", + "--ds-theme-color-panel-alt", "--ds-theme-color-accent", + "--ds-theme-color-accent-alt", "--ds-theme-color-secondary", + "--ds-theme-color-highlight", "--ds-theme-color-text", + "--ds-theme-color-muted", "--ds-theme-color-line", + "--ds-theme-font-family", "--ds-theme-font-scale", + "--ds-theme-surface-radius", "--ds-theme-surface-opacity", + "--ds-theme-surface-blur", "--ds-theme-surface-border-alpha", + "--ds-theme-surface-shadow", "--ds-theme-image-focus-x", + "--ds-theme-image-focus-y", "--ds-theme-image-zoom", + "--ds-theme-image-dim", "--ds-theme-image-task-intensity", + "--ds-theme-density-scale", "--ds-theme-motion-level", ]; - const HOME_UTILITY_CLASS = "dream-home-utility"; - const AUX_PANEL_LAYER_CLASS = "dream-aux-panel-layer"; - const AUX_PANEL_RIGHT_CLASS = "dream-aux-panel-right"; - const AUX_PANEL_BOTTOM_CLASS = "dream-aux-panel-bottom"; - const AUX_PANEL_CLASSES = [AUX_PANEL_LAYER_CLASS, AUX_PANEL_RIGHT_CLASS, AUX_PANEL_BOTTOM_CLASS]; + const selectorByKey = new Map(SELECTOR_CONTRACT.selectors.map((entry) => [entry.key, entry])); + const stableTestidSelector = (testid) => SELECTOR_CONTRACT.stableTestids?.includes(testid) + ? `[data-testid="${testid}"]` : null; const installToken = {}; - let samplingNativeShell = false; - let observer = null; - window.__CODEX_DREAM_SKIN_DISABLED__ = false; - - const clamp = (value, min = 0, max = 1) => Math.min(max, Math.max(min, Number(value))); - const luminance = (red, green, blue) => { - const linear = [red, green, blue].map((value) => { - const channel = value / 255; - return channel <= .04045 ? channel / 12.92 : ((channel + .055) / 1.055) ** 2.4; - }); - return .2126 * linear[0] + .7152 * linear[1] + .0722 * linear[2]; - }; - const defaultProfile = { - appearance: "dark", - accent: [108, 131, 142], - focusX: .5, - focusY: .5, - aspect: 1.6, - luma: .32, - safeArea: "center", - }; - - const normalizeConfig = (value) => { - const config = value && typeof value === "object" ? value : {}; - const art = config.art && typeof config.art === "object" ? config.art : {}; - const hasNumber = (candidate) => - (typeof candidate === "number" || (typeof candidate === "string" && candidate.trim() !== "")) && - Number.isFinite(Number(candidate)); - const requestedAccent = typeof config?.palette?.accent === "string" - ? config.palette.accent.trim() - : ""; - const safeAccent = /^(?:#[\da-f]{3,8}|(?:rgb|hsl|oklch|oklab)\([^;{}]{1,96}\))$/i.test(requestedAccent) - ? requestedAccent - : null; - const appearance = ["auto", "light", "dark"].includes(config.appearance) - ? config.appearance - : "auto"; - const safeArea = ["auto", "left", "right", "center", "none"].includes(art.safeArea) - ? art.safeArea - : "auto"; - const taskMode = ["auto", "ambient", "banner", "off"].includes(art.taskMode) - ? art.taskMode - : "auto"; - const metadataRatio = Number(config?.artMetadata?.ratio); - return { - appearance, - safeArea, - taskMode, - focusX: hasNumber(art.focusX) ? clamp(art.focusX) : null, - focusY: hasNumber(art.focusY) ? clamp(art.focusY) : null, - accent: safeAccent, - initialAspect: Number.isFinite(metadataRatio) && metadataRatio > 0 ? metadataRatio : null, - }; + const existingAnalysisCache = window[ANALYSIS_CACHE_KEY]; + const analysisCache = existingAnalysisCache && typeof existingAnalysisCache.get === "function" && + typeof existingAnalysisCache.set === "function" ? existingAnalysisCache : new Map(); + window[ANALYSIS_CACHE_KEY] = analysisCache; + let artAnalysis = typeof THEME.artKey === "string" ? analysisCache.get(THEME.artKey) ?? null : null; + let analysisTimer = null; + let rootObserver = null; + let partObserver = null; + let bodyReadyHandler = null; + let styleMode = null; + let styleNode = null; + let styleSheet = null; + const now = () => typeof performance === "object" && typeof performance.now === "function" + ? performance.now() : Date.now(); + const metrics = { + ensureCalls: 0, + rootPasses: 0, + routePasses: 0, + layoutReads: 0, + attributeWrites: 0, + styleWrites: 0, + styleRepairs: 0, + partPasses: 0, + partWrites: 0, + navigationEvents: 0, + safetyPasses: 0, + analysisRuns: 0, + analysisCacheHits: artAnalysis ? 1 : 0, + firstEnsureMs: null, + analysisMs: null, }; const previous = window[STATE_KEY]; - if (previous?.observer) previous.observer.disconnect(); - if (previous?.timer) clearInterval(previous.timer); - if (previous?.scheduler?.timeout) clearTimeout(previous.scheduler.timeout); - if (previous?.artUrl) URL.revokeObjectURL(previous.artUrl); + if (typeof previous?.cleanup === "function") previous.cleanup(); + window[DISABLED_KEY] = false; + + const existingStyleRegistry = window[STYLE_REGISTRY_KEY]; + const styleRegistry = existingStyleRegistry instanceof Set ? existingStyleRegistry : new Set(); + window[STYLE_REGISTRY_KEY] = styleRegistry; const artUrl = (() => { const comma = artDataUrl.indexOf(","); + const mime = /^data:([^;,]+)/.exec(artDataUrl)?.[1] || "image/png"; const binary = atob(artDataUrl.slice(comma + 1)); const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index); - const mime = /^data:([^;,]+)/.exec(artDataUrl)?.[1] || "image/png"; return URL.createObjectURL(new Blob([bytes], { type: mime })); })(); - const config = normalizeConfig(rawConfig); - let profile = { - ...defaultProfile, - aspect: config.initialAspect ?? defaultProfile.aspect, - }; - const existingStyle = document.getElementById(STYLE_ID); - if (existingStyle) { - existingStyle.textContent = cssText; - existingStyle.dataset.dreamVersion = "3"; - } + + const cssString = (value) => JSON.stringify(String(value ?? "")); + + const setStyleProperty = (root, name, value) => { + if (root.style.getPropertyValue(name) !== value) { + root.style.setProperty(name, value); + metrics.styleWrites += 1; + } + }; + + const setAttribute = (root, name, value) => { + const normalized = String(value); + if (root.getAttribute(name) !== normalized) { + root.setAttribute(name, normalized); + metrics.attributeWrites += 1; + } + }; + + const parseRgb = (value) => { + if (!value || value === "transparent") return null; + const hex = String(value).trim().match(/^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i); + if (hex) { + const rgbHex = hex[1].length <= 4 + ? hex[1].slice(0, 3).split("").map((digit) => `${digit}${digit}`).join("") + : hex[1].slice(0, 6); + const number = Number.parseInt(rgbHex, 16); + return { r: number >> 16, g: (number >> 8) & 255, b: number & 255 }; + } + const m = String(value).match(/rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/i); + if (!m) return null; + return { r: Number(m[1]), g: Number(m[2]), b: Number(m[3]) }; + }; + + const clamp = (value, min, max) => Math.min(max, Math.max(min, value)); + + const rgbString = (value) => { + const rgb = parseRgb(value); + return rgb ? [rgb.r, rgb.g, rgb.b] + .map((channel) => Math.round(clamp(channel, 0, 255))) + .join(" ") : null; + }; + + const rgbToHex = ({ r, g, b }) => `#${[r, g, b] + .map((value) => clamp(Math.round(value), 0, 255).toString(16).padStart(2, "0")) + .join("")}`; + + const rgbToHsl = ({ r, g, b }) => { + const values = [r, g, b].map((value) => value / 255); + const max = Math.max(...values); + const min = Math.min(...values); + const lightness = (max + min) / 2; + if (max === min) return { h: 0, s: 0, l: lightness }; + const delta = max - min; + const saturation = lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min); + let hue; + if (max === values[0]) hue = (values[1] - values[2]) / delta + (values[1] < values[2] ? 6 : 0); + else if (max === values[1]) hue = (values[2] - values[0]) / delta + 2; + else hue = (values[0] - values[1]) / delta + 4; + return { h: hue * 60, s: saturation, l: lightness }; + }; + + const hslToRgb = ({ h, s, l }) => { + const hue = ((h % 360) + 360) % 360 / 360; + if (s === 0) { + const neutral = Math.round(l * 255); + return { r: neutral, g: neutral, b: neutral }; + } + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + const channel = (offset) => { + let t = hue + offset; + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + }; + return { r: channel(1 / 3) * 255, g: channel(0) * 255, b: channel(-1 / 3) * 255 }; + }; + + const detectShellAppearance = () => { + const root = document.documentElement; + if (root?.classList?.contains("electron-dark")) return "dark"; + if (root?.classList?.contains("electron-light")) return "light"; + try { return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; } catch {} + return "light"; + }; + + const makeAdaptivePalette = (sample, shell) => { + const source = sample || { r: 108, g: 126, b: 136 }; + const hsl = rgbToHsl(source); + const hue = hsl.s < 0.12 ? 214 : hsl.h; + const saturation = clamp(hsl.s, 0.38, 0.72); + const accent = hslToRgb({ h: hue, s: saturation, l: shell === "light" ? 0.42 : 0.66 }); + const accentAlt = hslToRgb({ h: hue + 12, s: saturation * 0.82, l: shell === "light" ? 0.52 : 0.73 }); + const secondary = hslToRgb({ h: hue - 24, s: saturation * 0.64, l: shell === "light" ? 0.56 : 0.62 }); + const highlight = hslToRgb({ h: hue + 24, s: saturation * 0.76, l: shell === "light" ? 0.36 : 0.58 }); + const neutral = (lightness, chroma = 0.08) => rgbToHex(hslToRgb({ h: hue, s: chroma, l: lightness })); + return shell === "light" ? { + background: neutral(0.965, 0.07), + panel: neutral(0.987, 0.035), + panelAlt: neutral(0.945, 0.09), + accent: rgbToHex(accent), + accentAlt: rgbToHex(accentAlt), + secondary: rgbToHex(secondary), + highlight: rgbToHex(highlight), + text: neutral(0.13, 0.10), + muted: neutral(0.42, 0.08), + line: `rgba(${Math.round(accent.r)}, ${Math.round(accent.g)}, ${Math.round(accent.b)}, .24)`, + } : { + background: neutral(0.055, 0.045), + panel: neutral(0.085, 0.04), + panelAlt: neutral(0.125, 0.05), + accent: rgbToHex(accent), + accentAlt: rgbToHex(accentAlt), + secondary: rgbToHex(secondary), + highlight: rgbToHex(highlight), + text: neutral(0.93, 0.025), + muted: neutral(0.69, 0.03), + line: `rgba(${Math.round(accent.r)}, ${Math.round(accent.g)}, ${Math.round(accent.b)}, .28)`, + }; + }; + + const resolvedShell = () => { + if (THEME.appearance === "light" || THEME.appearance === "dark") return THEME.appearance; + // Image luminance may tune accents and scrims, but auto appearance follows + // Codex/ChatGPT (or the OS fallback) so a bright wallpaper cannot flip a + // native dark session back to a light shell after analysis. + return detectShellAppearance(); + }; + + const applyTheme = (root, shell) => { + const declaredColors = THEME.colors && typeof THEME.colors === "object" ? THEME.colors : {}; + const legacyPalette = THEME.palette && typeof THEME.palette === "object" ? THEME.palette : {}; + // macOS themes use the full `colors` contract; older Windows themes used + // `palette.accent`. Accept both while keeping one renderer source. + const colors = Object.keys(declaredColors).length ? declaredColors : legacyPalette; + const hasExplicitKeyList = Array.isArray(THEME.explicitColorKeys); + const explicit = new Set(hasExplicitKeyList ? THEME.explicitColorKeys : []); + if (!hasExplicitKeyList && (THEME.colorMode === "explicit" || !Object.hasOwn(THEME, "colorMode"))) { + for (const key of Object.keys(declaredColors)) explicit.add(key); + } + if (typeof legacyPalette.accent === "string") explicit.add("accent"); + const adaptive = makeAdaptivePalette(artAnalysis?.accentRgb, shell); + const legacyLight = (THEME.appearance === undefined || THEME.appearance === "auto") + && THEME.colorMode !== "explicit" && shell === "light"; + const structural = new Set(["background", "panel", "panelAlt", "text", "muted"]); + const pick = (name) => { + const allowExplicit = explicit.has(name) && !(legacyLight && structural.has(name)); + return allowExplicit && typeof colors[name] === "string" ? colors[name] : adaptive[name]; + }; + const accent = pick("accent"); + const accentAlt = explicit.has("accentAlt") ? pick("accentAlt") : (explicit.has("accent") ? accent : adaptive.accentAlt); + const variables = { + "--ds-bg": pick("background"), + "--ds-panel": pick("panel"), + "--ds-panel-2": pick("panelAlt"), + "--ds-green": accent, + "--ds-lime": accentAlt, + "--ds-cyan": pick("secondary"), + "--ds-purple": pick("highlight"), + "--ds-text": pick("text"), + "--ds-muted": pick("muted"), + "--ds-line": explicit.has("line") && typeof colors.line === "string" ? colors.line : adaptive.line, + }; + + for (const [name, value] of Object.entries(variables)) { + if (typeof value === "string" && value) setStyleProperty(root, name, value); + } + const publicColors = { + "--ds-theme-color-background": variables["--ds-bg"], + "--ds-theme-color-panel": variables["--ds-panel"], + "--ds-theme-color-panel-alt": variables["--ds-panel-2"], + "--ds-theme-color-accent": variables["--ds-green"], + "--ds-theme-color-accent-alt": variables["--ds-lime"], + "--ds-theme-color-secondary": variables["--ds-cyan"], + "--ds-theme-color-highlight": variables["--ds-purple"], + "--ds-theme-color-text": variables["--ds-text"], + "--ds-theme-color-muted": variables["--ds-muted"], + "--ds-theme-color-line": variables["--ds-line"], + }; + for (const [name, value] of Object.entries(publicColors)) { + if (typeof value === "string" && value) setStyleProperty(root, name, value); + } + setStyleProperty(root, "--ds-theme-surface-radius", "12px"); + setStyleProperty(root, "--ds-theme-surface-opacity", "1"); + setStyleProperty(root, "--ds-theme-surface-blur", "0px"); + setStyleProperty(root, "--ds-theme-font-family", "system"); + setStyleProperty(root, "--ds-theme-font-scale", "1"); + setStyleProperty(root, "--ds-theme-surface-border-alpha", "0.14"); + setStyleProperty(root, "--ds-theme-surface-shadow", "soft"); + setStyleProperty(root, "--ds-theme-image-zoom", "1"); + setStyleProperty(root, "--ds-theme-image-dim", "0"); + setStyleProperty(root, "--ds-theme-image-task-intensity", "0.35"); + setStyleProperty(root, "--ds-theme-density-scale", "standard"); + setStyleProperty(root, "--ds-theme-motion-level", "standard"); + const rgbVariables = { + "--ds-bg-rgb": variables["--ds-bg"], + "--ds-panel-rgb": variables["--ds-panel"], + "--ds-panel-2-rgb": variables["--ds-panel-2"], + "--ds-accent-rgb": variables["--ds-green"], + "--ds-accent-alt-rgb": variables["--ds-lime"], + "--ds-secondary-rgb": variables["--ds-cyan"], + "--ds-highlight-rgb": variables["--ds-purple"], + "--ds-text-rgb": variables["--ds-text"], + "--ds-muted-rgb": variables["--ds-muted"], + "--ds-line-rgb": variables["--ds-line"], + }; + for (const [name, value] of Object.entries(rgbVariables)) { + const rgb = rgbString(value); + if (rgb) setStyleProperty(root, name, rgb); + } + setStyleProperty(root, "--dream-skin-name", cssString(THEME.name || "Codex Dream Skin")); + setStyleProperty(root, "--dream-skin-tagline", cssString(THEME.tagline || "Make something wonderful.")); + setStyleProperty(root, "--dream-skin-quote", cssString(THEME.quote || "MAKE SOMETHING WONDERFUL")); + setStyleProperty(root, "--dream-skin-brand-subtitle", cssString( + THEME.brandSubtitle || "CODEX DREAM SKIN", + )); + setStyleProperty(root, "--dream-skin-status", cssString(THEME.statusText || "DREAM SKIN ONLINE")); + setStyleProperty(root, "--dream-skin-project-prefix", cssString(THEME.projectPrefix || "选择项目 · ")); + setStyleProperty(root, "--dream-skin-project-label", cssString(THEME.projectLabel || "◉ 选择项目")); + }; + + const applyArtMetadata = (root) => { + const profile = artAnalysis || ART_METADATA; + const inferredSafe = profile?.safeArea || "center"; + const safeArea = ART.safeArea && ART.safeArea !== "auto" ? ART.safeArea : inferredSafe; + const canonicalSafe = ["left", "right", "center", "none"].includes(safeArea) + ? safeArea : "center"; + const focusX = typeof ART.focusX === "number" ? ART.focusX + : profile?.focusX ?? (safeArea === "left" ? 0.72 : safeArea === "right" ? 0.28 : 0.5); + const focusY = typeof ART.focusY === "number" ? ART.focusY : profile?.focusY ?? 0.5; + const taskMode = ART.taskMode && ART.taskMode !== "auto" + ? ART.taskMode : profile?.taskMode || "ambient"; + const wide = profile?.wide || false; + const aspect = profile?.aspect || "unknown"; + const focusXValue = `${(clamp(focusX, 0, 1) * 100).toFixed(2)}%`; + const focusYValue = `${(clamp(focusY, 0, 1) * 100).toFixed(2)}%`; + + setAttribute(root, "data-dream-art-wide", wide ? "true" : "false"); + setAttribute(root, "data-dream-art-safe", canonicalSafe); + setAttribute(root, "data-dream-task-mode", taskMode); + setAttribute(root, "data-dream-art-safe-area", safeArea); + setAttribute(root, "data-dream-art-task-mode", taskMode); + setAttribute(root, "data-dream-art-aspect", aspect); + setAttribute(root, "data-dream-art-ready", artAnalysis ? "true" : "false"); + setStyleProperty(root, "--dream-art-focus-x", focusXValue); + setStyleProperty(root, "--dream-art-focus-y", focusYValue); + setStyleProperty(root, "--dream-art-position", `${focusXValue} ${focusYValue}`); + setStyleProperty(root, "--dream-skin-focus-x", focusXValue); + setStyleProperty(root, "--dream-skin-focus-y", focusYValue); + setStyleProperty(root, "--dream-skin-art-position", `${focusXValue} ${focusYValue}`); + setStyleProperty(root, "--ds-theme-image-focus-x", String(Number(focusX.toFixed(4)))); + setStyleProperty(root, "--ds-theme-image-focus-y", String(Number(focusY.toFixed(4)))); + }; const analyzeArt = () => new Promise((resolve) => { - if (typeof Image !== "function") { - resolve(defaultProfile); + const startedAt = now(); + metrics.analysisRuns += 1; + if (typeof window.Image !== "function" || !document?.createElement) { + metrics.analysisMs = Number((now() - startedAt).toFixed(3)); + resolve(null); return; } - const image = new Image(); + const image = new window.Image(); + let settled = false; + const finish = (value) => { + if (settled) return; + settled = true; + if (analysisTimer) clearTimeout(analysisTimer); + analysisTimer = null; + metrics.analysisMs = Number((now() - startedAt).toFixed(3)); + resolve(value); + }; + analysisTimer = setTimeout(() => finish(null), 6000); + image.onerror = () => finish(null); image.onload = () => { try { - const width = 48; - const height = Math.max(12, Math.round(width * image.naturalHeight / image.naturalWidth)); + const ratio = image.naturalWidth / image.naturalHeight; + if (!Number.isFinite(ratio) || ratio <= 0) throw new Error("Invalid image dimensions"); + const maxDimension = 96; + const width = Math.max(16, Math.round(ratio >= 1 ? maxDimension : maxDimension * ratio)); + const height = Math.max(16, Math.round(ratio >= 1 ? maxDimension / ratio : maxDimension)); const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const context = canvas.getContext?.("2d", { willReadFrequently: true }); if (!context) throw new Error("Canvas is unavailable"); context.drawImage(image, 0, 0, width, height); - const pixels = context.getImageData(0, 0, width, height).data; + const data = context.getImageData(0, 0, width, height).data; + const samples = new Array(width * height); + const bins = Array.from({ length: 24 }, () => ({ weight: 0, r: 0, g: 0, b: 0 })); + let lightTotal = 0; let count = 0; - let totalRed = 0; - let totalGreen = 0; - let totalBlue = 0; - let totalBrightness = 0; - const samples = []; - const sampleMap = new Array(width * height); - for (let offset = 0; offset < pixels.length; offset += 4) { - if (pixels[offset + 3] < 96) continue; - const red = pixels[offset]; - const green = pixels[offset + 1]; - const blue = pixels[offset + 2]; - const light = (.2126 * red + .7152 * green + .0722 * blue) / 255; - const sample = { red, green, blue, light, index: offset / 4 }; - samples.push(sample); - sampleMap[sample.index] = sample; - totalRed += red; - totalGreen += green; - totalBlue += blue; - totalBrightness += light; - count += 1; + + for (let y = 0; y < height; y += 1) { + for (let x = 0; x < width; x += 1) { + const offset = (y * width + x) * 4; + if (data[offset + 3] < 32) continue; + const rgb = { r: data[offset], g: data[offset + 1], b: data[offset + 2] }; + const light = (0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b) / 255; + const hsl = rgbToHsl(rgb); + samples[y * width + x] = { light, saturation: hsl.s }; + lightTotal += light; + count += 1; + if (hsl.s >= 0.16 && hsl.l >= 0.16 && hsl.l <= 0.86) { + const bin = bins[Math.min(23, Math.floor(hsl.h / 15))]; + const weight = hsl.s * (1 - Math.abs(hsl.l - 0.52) * 0.85); + bin.weight += weight; + bin.r += rgb.r * weight; + bin.g += rgb.g * weight; + bin.b += rgb.b * weight; + } + } } - if (!count) throw new Error("Image contains no opaque pixels"); - const average = [totalRed / count, totalGreen / count, totalBlue / count]; - const averageBrightness = totalBrightness / count; + if (!count) throw new Error("Image has no visible pixels"); + const brightness = lightTotal / count; const information = (start, end) => { let total = 0; let totalSquared = 0; let edges = 0; let edgeCount = 0; - let sampleCount = 0; + let pixels = 0; for (let y = 0; y < height; y += 1) { for (let x = start; x < end; x += 1) { - const sample = sampleMap[y * width + x]; + const sample = samples[y * width + x]; if (!sample) continue; total += sample.light; totalSquared += sample.light * sample.light; - sampleCount += 1; - const previousSample = x > start ? sampleMap[y * width + x - 1] : null; - const above = y > 0 ? sampleMap[(y - 1) * width + x] : null; - if (previousSample) { edges += Math.abs(sample.light - previousSample.light); edgeCount += 1; } + pixels += 1; + const previous = x > start ? samples[y * width + x - 1] : null; + const above = y > 0 ? samples[(y - 1) * width + x] : null; + if (previous) { edges += Math.abs(sample.light - previous.light); edgeCount += 1; } if (above) { edges += Math.abs(sample.light - above.light); edgeCount += 1; } } } - const mean = sampleCount ? total / sampleCount : 0; - const variance = sampleCount ? Math.max(0, totalSquared / sampleCount - mean * mean) : 1; - return Math.sqrt(variance) * .58 + (edgeCount ? edges / edgeCount : 1) * .42; + const mean = pixels ? total / pixels : 0; + const variance = pixels ? Math.max(0, totalSquared / pixels - mean * mean) : 1; + return Math.sqrt(variance) * 0.58 + (edgeCount ? edges / edgeCount : 1) * 0.42; }; - const zoneWidth = Math.max(1, Math.floor(width * .38)); + const zoneWidth = Math.max(1, Math.floor(width * 0.38)); const leftInformation = information(0, zoneWidth); const rightInformation = information(width - zoneWidth, width); let safeArea = "center"; - if (leftInformation < rightInformation * .86) safeArea = "left"; - else if (rightInformation < leftInformation * .86) safeArea = "right"; - let focusWeight = 0; - let focusX = 0; - let focusY = 0; - let accentWeight = 0; - let accent = [0, 0, 0]; - for (const sample of samples) { - const x = sample.index % width; - const y = Math.floor(sample.index / width); - const difference = Math.sqrt( - (sample.red - average[0]) ** 2 + - (sample.green - average[1]) ** 2 + - (sample.blue - average[2]) ** 2, - ) / 441.7; - const saliency = .03 + difference ** 1.35; - focusX += (x / Math.max(1, width - 1)) * saliency; - focusY += (y / Math.max(1, height - 1)) * saliency; - focusWeight += saliency; - const max = Math.max(sample.red, sample.green, sample.blue); - const min = Math.min(sample.red, sample.green, sample.blue); - const saturation = max ? (max - min) / max : 0; - const usableLight = 1 - Math.min(1, Math.abs(sample.light - .46) / .54); - const weight = saturation ** 2 * (.15 + usableLight); - accent[0] += sample.red * weight; - accent[1] += sample.green * weight; - accent[2] += sample.blue * weight; - accentWeight += weight; + if (leftInformation < rightInformation * 0.86) safeArea = "left"; + else if (rightInformation < leftInformation * 0.86) safeArea = "right"; + + let saliencyTotal = 0; + let saliencyX = 0; + let saliencyY = 0; + for (let y = 0; y < height; y += 1) { + for (let x = 0; x < width; x += 1) { + const sample = samples[y * width + x]; + if (!sample) continue; + const previous = x > 0 ? samples[y * width + x - 1] : null; + const above = y > 0 ? samples[(y - 1) * width + x] : null; + const edge = (previous ? Math.abs(sample.light - previous.light) : 0) + + (above ? Math.abs(sample.light - above.light) : 0); + const weight = 0.01 + Math.abs(sample.light - brightness) * 0.48 + + sample.saturation * 0.34 + edge * 0.28; + saliencyTotal += weight; + saliencyX += (x + 0.5) / width * weight; + saliencyY += (y + 0.5) / height * weight; + } } - const resolvedAccent = accentWeight > 1 - ? accent.map((channel) => Math.round(channel / accentWeight)) - : average.map((channel) => Math.round(channel)); - let resolvedFocusX = clamp(focusX / focusWeight); - if (safeArea === "left") resolvedFocusX = Math.max(.64, resolvedFocusX); - if (safeArea === "right") resolvedFocusX = Math.min(.36, resolvedFocusX); - resolve({ - appearance: averageBrightness >= .58 ? "light" : "dark", - accent: resolvedAccent, - focusX: resolvedFocusX, - focusY: clamp(focusY / focusWeight), - aspect: image.naturalWidth / Math.max(1, image.naturalHeight), - luma: clamp(averageBrightness), + let focusX = saliencyTotal ? saliencyX / saliencyTotal : 0.5; + let focusY = saliencyTotal ? saliencyY / saliencyTotal : 0.5; + if (safeArea === "left") focusX = Math.max(0.64, focusX); + if (safeArea === "right") focusX = Math.min(0.36, focusX); + focusX = clamp(focusX, 0.12, 0.88); + focusY = clamp(focusY, 0.18, 0.82); + + const accentBin = bins.reduce((best, candidate) => candidate.weight > best.weight ? candidate : best, bins[0]); + const accentRgb = accentBin.weight > 0 ? { + r: accentBin.r / accentBin.weight, + g: accentBin.g / accentBin.weight, + b: accentBin.b / accentBin.weight, + } : null; + const aspect = ratio >= 2.25 ? "ultrawide" : ratio >= 1.45 ? "wide" + : ratio >= 1.08 ? "landscape" : ratio >= 0.9 ? "square" : "portrait"; + finish({ + width: image.naturalWidth, + height: image.naturalHeight, + ratio, + wide: ratio >= 1.75, + aspect, + brightness, + shell: brightness >= 0.58 ? "light" : "dark", safeArea, + focusX, + focusY, + taskMode: ratio >= 2.25 ? "banner" : "ambient", + accentRgb, }); } catch { - resolve(defaultProfile); + finish(null); } }; - image.onerror = () => resolve(defaultProfile); image.src = artUrl; }); - const detectShellAppearance = () => { - const root = document.documentElement; - const body = document.body; - const classes = `${root?.className || ""} ${body?.className || ""}` - .toLowerCase() - .replace(/\bdream-theme-(?:dark|light)\b/g, ""); - if (/\b(dark|electron-dark|theme-dark|appearance-dark)\b/.test(classes)) return "dark"; - if (/\b(light|electron-light|theme-light|appearance-light)\b/.test(classes)) return "light"; - - const dataTheme = ( - root?.getAttribute?.("data-theme") || - root?.getAttribute?.("data-appearance") || - root?.getAttribute?.("data-color-mode") || - body?.getAttribute?.("data-theme") || - body?.getAttribute?.("data-appearance") || - "" - ).toLowerCase(); - if (dataTheme.includes("dark")) return "dark"; - if (dataTheme.includes("light")) return "light"; - + const installStyle = () => { try { - const hadSkin = root?.classList?.contains?.("codex-dream-skin"); - const savedSkinClasses = hadSkin - ? ROOT_CLASSES.filter((className) => root.classList.contains(className)) - : []; - samplingNativeShell = true; - if (hadSkin) root.classList.remove(...ROOT_CLASSES); - try { - const colorScheme = getComputedStyle(root).colorScheme || ""; - if (colorScheme.includes("dark") && !colorScheme.includes("light")) return "dark"; - if (colorScheme.includes("light") && !colorScheme.includes("dark")) return "light"; - } finally { - if (hadSkin) root.classList.add(...savedSkinClasses); - observer?.takeRecords?.(); - samplingNativeShell = false; + if (!("adoptedStyleSheets" in document) || typeof CSSStyleSheet !== "function") { + throw new Error("Constructable stylesheets are unavailable"); } + const sheet = new CSSStyleSheet(); + if (typeof sheet.replaceSync !== "function") throw new Error("replaceSync is unavailable"); + sheet.replaceSync(cssText); + const retained = [...document.adoptedStyleSheets] + .filter((candidate) => !styleRegistry.has(candidate)); + document.adoptedStyleSheets = [...retained, sheet]; + styleRegistry.clear(); + styleRegistry.add(sheet); + document.getElementById(STYLE_ID)?.remove(); + styleSheet = sheet; + styleMode = "adopted"; + return; } catch { - samplingNativeShell = false; + styleSheet = null; } - try { - return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; - } catch {} - return "light"; - }; - const clearAuxiliaryPanelClasses = () => { - for (const candidate of document.querySelectorAll(`.${AUX_PANEL_LAYER_CLASS}`)) { - candidate.classList.remove(...AUX_PANEL_CLASSES); - } + styleNode = document.getElementById(STYLE_ID) || document.createElement("style"); + styleNode.id = STYLE_ID; + styleNode.textContent = cssText; + if (!styleNode.parentElement) (document.head || document.documentElement).appendChild(styleNode); + styleMode = "style"; }; - const clearSkinDom = () => { - const root = document.documentElement; - root?.classList.remove(...ROOT_CLASSES); - for (const property of ROOT_PROPERTIES) root?.style.removeProperty(property); - document.querySelectorAll(".dream-home").forEach((node) => node.classList.remove("dream-home")); - document.querySelectorAll(".dream-task").forEach((node) => node.classList.remove("dream-task")); - document.querySelectorAll(".dream-home-shell").forEach((node) => node.classList.remove("dream-home-shell")); - document.querySelectorAll(`.${HOME_UTILITY_CLASS}`).forEach((node) => node.classList.remove(HOME_UTILITY_CLASS)); - clearAuxiliaryPanelClasses(); - document.getElementById(STYLE_ID)?.remove(); - document.getElementById(CHROME_ID)?.remove(); - }; - - const applyProfile = (root) => { - const focusX = config.focusX ?? profile.focusX; - const focusY = config.focusY ?? profile.focusY; - const appearance = config.appearance === "auto" ? detectShellAppearance() : config.appearance; - const focus = focusX < .4 ? "left" : focusX > .6 ? "right" : "center"; - const safeArea = config.safeArea === "auto" ? (profile.safeArea || - (focus === "left" ? "right" : focus === "right" ? "left" : "center")) : config.safeArea; - const taskMode = config.taskMode === "auto" - ? profile.aspect >= 2.25 ? "banner" : "ambient" - : config.taskMode; - const accent = config.accent || `rgb(${profile.accent.join(" ")})`; - const accentInk = luminance(...profile.accent) > .42 ? "rgb(26 24 28)" : "rgb(250 248 251)"; - root.classList.toggle("dream-theme-light", appearance === "light"); - root.classList.toggle("dream-theme-dark", appearance === "dark"); - root.classList.toggle("dream-art-wide", profile.aspect >= 1.75); - root.classList.toggle("dream-art-standard", profile.aspect < 1.75); - for (const value of ["left", "center", "right"]) { - root.classList.toggle(`dream-focus-${value}`, focus === value); - } - for (const value of ["left", "center", "right", "none"]) { - root.classList.toggle(`dream-safe-${value}`, safeArea === value); - } - for (const value of ["ambient", "banner", "off"]) { - root.classList.toggle(`dream-task-${value}`, taskMode === value); - } - root.style.setProperty("--dream-art", `url("${artUrl}")`); - root.style.setProperty("--dream-art-position", `${Math.round(focusX * 100)}% ${Math.round(focusY * 100)}%`); - root.style.setProperty("--dream-focus-x", String(focusX)); - root.style.setProperty("--dream-focus-y", String(focusY)); - root.style.setProperty("--dream-accent", accent); - root.style.setProperty("--dream-accent-ink", accentInk); - root.style.setProperty("--dream-image-luma", profile.luma.toFixed(3)); - }; - - const reconcileAuxiliaryPanels = (shellMain) => { - const shellRect = shellMain.getBoundingClientRect(); - const activeLayers = new Set(); - - for (const tabs of document.querySelectorAll('[data-app-shell-tabs="true"]')) { - const rect = tabs.getBoundingClientRect(); - if (rect.width < 1 || rect.height < 1) continue; - - const roleClass = rect.top >= shellRect.top + shellRect.height * .5 - && rect.width >= shellRect.width * .65 - ? AUX_PANEL_BOTTOM_CLASS - : rect.left >= shellRect.left + shellRect.width * .45 - && rect.height >= shellRect.height * .45 - ? AUX_PANEL_RIGHT_CLASS - : null; - if (!roleClass) continue; - - for (let layer = tabs, depth = 0; layer && depth < 3; layer = layer.parentElement, depth += 1) { - const layerRect = layer.getBoundingClientRect(); - if (Math.abs(layerRect.x - rect.x) > 3 || Math.abs(layerRect.y - rect.y) > 3 - || Math.abs(layerRect.width - rect.width) > 3 || Math.abs(layerRect.height - rect.height) > 3) break; - layer.classList.add(AUX_PANEL_LAYER_CLASS, roleClass); - activeLayers.add(layer); + const ensureStyle = () => { + if (styleMode === "adopted" && styleSheet) { + const current = [...document.adoptedStyleSheets]; + if (!current.includes(styleSheet)) { + document.adoptedStyleSheets = [...current, styleSheet]; + metrics.styleRepairs += 1; } + return; } - - for (const candidate of document.querySelectorAll(`.${AUX_PANEL_LAYER_CLASS}`)) { - if (!activeLayers.has(candidate)) candidate.classList.remove(...AUX_PANEL_CLASSES); + if (styleNode && document.getElementById(STYLE_ID) !== styleNode) { + document.getElementById(STYLE_ID)?.remove(); + (document.head || document.documentElement).appendChild(styleNode); + metrics.styleRepairs += 1; } }; - const ensure = () => { - if (window.__CODEX_DREAM_SKIN_DISABLED__) return; - const root = document.documentElement; - if (!root || !document.body) return; + installStyle(); - const shellMain = document.querySelector("main.main-surface"); - if (!shellMain) { - clearSkinDom(); - return; - } + const applyRootState = (root) => { + metrics.rootPasses += 1; + ensureStyle(); + const shell = resolvedShell(); + setAttribute(root, "data-dream-skin", "active"); + setAttribute(root, SHELL_ATTR, shell); + setStyleProperty(root, "--dream-skin-art", `url("${artUrl}")`); + applyTheme(root, shell); + applyArtMetadata(root); + return shell; + }; - root.classList.add("codex-dream-skin"); - applyProfile(root); + const selectorHit = (key) => { + const selector = selectorByKey.get(key)?.selector; + if (!selector) return false; + try { return Boolean(document.querySelector(selector)); } catch { return false; } + }; - let style = document.getElementById(STYLE_ID); - if (!style) { - style = document.createElement("style"); - style.id = STYLE_ID; - (document.head || root).appendChild(style); - } - if (style.dataset.dreamVersion !== "3") { - style.textContent = cssText; - style.dataset.dreamVersion = "3"; + const ensureShellMainCompatibility = () => { + const shellMain = document.querySelector("main.main-surface") || document.querySelector("main"); + if (!shellMain) return null; + // Codex 26.727 moved this stable class behind CSS Modules. Preserve the + // DreamSkin selector contract while leaving Codex's own class untouched. + if (!shellMain.classList.contains("main-surface")) { + shellMain.classList.add("main-surface"); + shellMain.setAttribute(MAIN_COMPAT_ATTR, "true"); } + return shellMain; + }; - const home = document.querySelector('[role="main"]:has([data-testid="home-icon"])'); - for (const candidate of document.querySelectorAll('[role="main"]')) { - candidate.classList.toggle("dream-home", candidate === home); - candidate.classList.toggle("dream-task", candidate !== home); + const stableTestidHit = (testid) => { + const selector = stableTestidSelector(testid); + if (!selector) return false; + try { return Boolean(document.querySelector(selector)); } catch { return false; } + }; + + const partNodes = new Set(); + const queryAll = (selector) => { + if (!selector) return []; + try { return [...document.querySelectorAll(selector)]; } catch { return []; } + }; + const selectorNodes = (key) => queryAll(selectorByKey.get(key)?.selector); + const addPart = (desired, part, nodes) => { + for (const node of nodes) { + if (node && typeof node.setAttribute === "function" && !desired.has(node)) { + desired.set(node, part); + } } - const utilityBars = new Set(home ? home.querySelectorAll('[class*="_homeUtilityBar_"]') : []); - for (const candidate of document.querySelectorAll(`.${HOME_UTILITY_CLASS}`)) { - if (!utilityBars.has(candidate)) candidate.classList.remove(HOME_UTILITY_CLASS); + }; + const refreshParts = () => { + metrics.partPasses += 1; + const desired = new Map(); + addPart(desired, "root", [document.documentElement]); + addPart(desired, "sidebar", selectorNodes("left-panel")); + addPart(desired, "main", selectorNodes("shell-main")); + addPart(desired, "header", selectorNodes("header-tint")); + addPart(desired, "home", selectorNodes("home-route")); + addPart(desired, "project-list", selectorNodes("project-selector")); + addPart(desired, "thread", selectorNodes("thread-surface")); + addPart(desired, "message", selectorNodes("message")); + addPart(desired, "composer", selectorNodes("composer-chrome")); + addPart(desired, "composer-toolbar", selectorNodes("composer-toolbar")); + addPart(desired, "dialog", selectorNodes("overlay-dialog")); + const homeHero = selectorNodes("home-icon")[0]?.parentElement; + addPart(desired, "home-hero", homeHero ? [homeHero] : []); + + for (const node of partNodes) { + if (!desired.has(node)) { + node.removeAttribute?.(PART_ATTR); + metrics.partWrites += 1; + } } - for (const candidate of utilityBars) candidate.classList.add(HOME_UTILITY_CLASS); - shellMain.classList.toggle("dream-home-shell", Boolean(home)); - reconcileAuxiliaryPanels(shellMain); - - let chrome = document.getElementById(CHROME_ID); - if (!chrome || chrome.parentElement !== document.body) { - chrome?.remove(); - chrome = document.createElement("div"); - chrome.id = CHROME_ID; - chrome.setAttribute("aria-hidden", "true"); - document.body.appendChild(chrome); + partNodes.clear(); + for (const [node, part] of desired) { + if (node.getAttribute?.(PART_ATTR) !== part) { + node.setAttribute(PART_ATTR, part); + metrics.partWrites += 1; + } + partNodes.add(node); } - chrome.classList.toggle("dream-home-shell", Boolean(home)); + }; + + const removeParts = () => { + for (const node of partNodes) node.removeAttribute?.(PART_ATTR); + partNodes.clear(); + for (const node of queryAll(`[${PART_ATTR}]`)) node.removeAttribute?.(PART_ATTR); + }; + + const scopeMatches = (scope, baseState, overlay) => { + const active = new Set([baseState]); + if (baseState !== "settings") active.add("all"); + if (overlay) active.add("overlay"); + const tokens = String(scope || "all").toLowerCase().match(/[a-z]+/g) || ["all"]; + return tokens.some((token) => token !== "config" && active.has(token)); + }; + + const detectScope = () => { + const overlay = selectorHit("overlay-menu") || selectorHit("overlay-dialog") || + selectorHit("overlay-popper"); + let baseState = "thread"; + if (selectorHit("appearance-radio") || stableTestidHit("theme-preview")) baseState = "settings"; + else if (selectorHit("home-icon") || selectorHit("home-route")) baseState = "home"; + else if (!selectorHit("shell-main")) baseState = "settings"; + const missingL1 = SELECTOR_CONTRACT.selectors + .filter((entry) => entry.tier === "L1" && entry.required && + scopeMatches(entry.scope, baseState, overlay) && !selectorHit(entry.key)) + .map((entry) => entry.key); + return { + state: overlay ? "overlay" : baseState, + baseState, + overlay, + // Settings replaces (or partially replaces) the app shell on macOS and + // can retain a shell on Windows. It is therefore always an L0 scope; + // never treat the absence of the home/thread L1 anchors as a failure. + level: baseState === "settings" || missingL1.length ? "L0" : "L1", + missingL1, + }; + }; + + const refreshScope = () => { + metrics.routePasses += 1; + const scope = detectScope(); + const state = window[STATE_KEY]; + if (state?.installToken === installToken) state.scope = scope; + return scope; + }; + + const ensure = ({ root: rootPass = true, scope: scopePass = false, parts: partPass = false } = {}) => { + if (window[DISABLED_KEY]) return; + const root = document.documentElement; + if (!root) return; + metrics.ensureCalls += 1; + ensureShellMainCompatibility(); + if (rootPass) applyRootState(root); + if (partPass) refreshParts(); + if (scopePass) refreshScope(); }; const cleanup = () => { const state = window[STATE_KEY]; if (state?.installToken !== installToken) return false; - window.__CODEX_DREAM_SKIN_DISABLED__ = true; - clearSkinDom(); - state?.observer?.disconnect(); + window[DISABLED_KEY] = true; + const root = document.documentElement; + for (const name of ROOT_ATTRS) root?.removeAttribute(name); + for (const attribute of [...(root?.attributes || [])]) { + if (attribute.name.startsWith("data-dream-")) root.removeAttribute(attribute.name); + } + for (const name of THEME_VARIABLES) root?.style.removeProperty(name); + for (const property of [...(root?.style || [])]) { + if (property.startsWith("--dream-") || property.startsWith("--ds-")) { + root.style.removeProperty(property); + } + } + removeParts(); + for (const node of document.querySelectorAll(`main[${MAIN_COMPAT_ATTR}="true"]`)) { + node.classList.remove("main-surface"); + node.removeAttribute(MAIN_COMPAT_ATTR); + } + state?.rootObserver?.disconnect(); + state?.partObserver?.disconnect(); + if (bodyReadyHandler && typeof document.removeEventListener === "function") { + document.removeEventListener("DOMContentLoaded", bodyReadyHandler); + } if (state?.timer) clearInterval(state.timer); if (state?.scheduler?.timeout) clearTimeout(state.scheduler.timeout); + if (analysisTimer) clearTimeout(analysisTimer); + if (state?.mediaHandler && state?.mediaQuery) { + try { state.mediaQuery.removeEventListener("change", state.mediaHandler); } catch {} + } + if (state?.navigationHandler && state?.navigation) { + try { state.navigation.removeEventListener("navigate", state.navigationHandler); } catch {} + } + if (styleSheet) { + try { + document.adoptedStyleSheets = [...document.adoptedStyleSheets] + .filter((candidate) => candidate !== styleSheet); + } catch {} + styleRegistry.delete(styleSheet); + } + styleNode?.remove(); + if (document.getElementById(STYLE_ID) === styleNode) document.getElementById(STYLE_ID)?.remove(); + if (styleRegistry.size === 0) delete window[STYLE_REGISTRY_KEY]; if (state?.artUrl) URL.revokeObjectURL(state.artUrl); delete window[STATE_KEY]; return true; }; - const scheduler = { timeout: null }; - const scheduleEnsure = () => { + const scheduler = { timeout: null, root: false, scope: false, parts: false }; + const flushScheduledEnsure = () => { if (scheduler.timeout) clearTimeout(scheduler.timeout); - scheduler.timeout = setTimeout(() => { - scheduler.timeout = null; - ensure(); - }, 180); - }; - observer = new MutationObserver(() => { - if (samplingNativeShell) return; - scheduleEnsure(); - }); - observer.observe(document.documentElement, { - childList: true, - subtree: true, - attributes: true, - attributeFilter: ["class", "data-theme", "data-appearance", "data-color-mode"], - }); - const timer = setInterval(ensure, 5000); + scheduler.timeout = null; + const pending = { root: scheduler.root, scope: scheduler.scope, parts: scheduler.parts }; + scheduler.root = false; + scheduler.scope = false; + scheduler.parts = false; + ensure(pending); + }; + const scheduleEnsure = ({ root = false, scope = false, parts = false } = {}, delay = 64) => { + scheduler.root ||= root; + scheduler.scope ||= scope; + scheduler.parts ||= parts; + if (scheduler.timeout) return; + scheduler.timeout = setTimeout(flushScheduledEnsure, delay); + }; + if (typeof MutationObserver === "function") { + rootObserver = new MutationObserver(() => scheduleEnsure({ root: true })); + partObserver = new MutationObserver(() => scheduleEnsure({ parts: true }, 80)); + } + + let mediaQuery = null; + let mediaHandler = null; + try { + mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + mediaHandler = () => scheduleEnsure({ root: true }); + } catch {} + + const navigationApi = window.navigation && typeof window.navigation.addEventListener === "function" + ? window.navigation : null; + const navigationHandler = navigationApi ? () => { + metrics.navigationEvents += 1; + scheduleEnsure({ scope: true, parts: true }, 180); + } : null; + window[STATE_KEY] = { - ensure, cleanup, observer, timer, scheduler, artUrl, profile, config, installToken, version: "1.2.0", + ensure, + cleanup, + rootObserver, + partObserver, + timer: null, + scheduler, + mediaQuery, + mediaHandler, + navigation: navigationApi, + navigationHandler, + artUrl, + installToken, + styleMode, + styleNode, + styleSheet, + styleRevision: STYLE_REVISION, + analysis: artAnalysis, + artMetadata: ART_METADATA, + scope: null, + selectorsSchema: SELECTOR_CONTRACT.schema, + metrics, + version: VERSION, + themeId: THEME.id || "custom", + revision: PAYLOAD_REVISION, + detectShellAppearance, + }; + const firstEnsureStartedAt = now(); + ensure({ root: true, parts: true }); + const initialScope = refreshScope(); + metrics.firstEnsureMs = Number((now() - firstEnsureStartedAt).toFixed(3)); + + const observeAttributes = (node) => { + if (!rootObserver || !node) return; + rootObserver.observe(node, { + attributes: true, + attributeFilter: ["class", "data-theme", "data-appearance", "data-color-mode"], + }); + }; + const observePartTree = (node) => { + if (!partObserver || !node) return; + partObserver.observe(node, { childList: true, subtree: true }); + }; + observeAttributes(document.documentElement); + const observeBody = () => { + observeAttributes(document.body); + observePartTree(document.body); }; - ensure(); - analyzeArt().then((result) => { + if (document.body) observeBody(); + else if (typeof document.addEventListener === "function") { + bodyReadyHandler = () => { + if (!window[DISABLED_KEY]) { + observeBody(); + scheduleEnsure({ parts: true }, 0); + } + }; + document.addEventListener("DOMContentLoaded", bodyReadyHandler, { once: true }); + } + const timer = setInterval(() => { + metrics.safetyPasses += 1; + ensure({ root: true }); + }, 30000); + window[STATE_KEY].timer = timer; + if (mediaHandler && mediaQuery && typeof mediaQuery.addEventListener === "function") { + mediaQuery.addEventListener("change", mediaHandler); + } + if (navigationHandler && navigationApi) { + navigationApi.addEventListener("navigate", navigationHandler); + } + const analysisPromise = artAnalysis ? Promise.resolve(null) : analyzeArt(); + window[STATE_KEY].analysisTimer = analysisTimer; + analysisPromise.then((analysis) => { const state = window[STATE_KEY]; - if (state?.installToken !== installToken || window.__CODEX_DREAM_SKIN_DISABLED__) return; - profile = result; - state.profile = result; - ensure(); - }); - return { installed: true, version: "1.2.0", adaptive: true }; -})(__DREAM_CSS_JSON__, __DREAM_ART_JSON__, __DREAM_THEME_JSON__) + if (!analysis || state?.installToken !== installToken || window[DISABLED_KEY]) return; + artAnalysis = analysis; + state.analysis = analysis; + if (typeof THEME.artKey === "string") { + analysisCache.set(THEME.artKey, analysis); + while (analysisCache.size > 8) analysisCache.delete(analysisCache.keys().next().value); + } + ensure({ root: true }); + }).catch(() => {}); + return { + installed: true, + version: VERSION, + themeId: THEME.id || "custom", + revision: PAYLOAD_REVISION, + shell: resolvedShell(), + scope: initialScope, + styleMode, + analysis: artAnalysis, + }; +})(__DREAM_SKIN_CSS_JSON__, __DREAM_SKIN_ART_JSON__, __DREAM_SKIN_THEME_JSON__) diff --git a/crates/codex-plus-core/src/assets.rs b/crates/codex-plus-core/src/assets.rs index daa739724..4bc058b8a 100644 --- a/crates/codex-plus-core/src/assets.rs +++ b/crates/codex-plus-core/src/assets.rs @@ -49,7 +49,7 @@ const STEPWISE_SCRIPT: &str = include_str!("../../../assets/inject/stepwise-inje const SPONSOR_ALIPAY: &[u8] = include_bytes!("../../../assets/images/sponsor-alipay.jpg"); const SPONSOR_WECHAT: &[u8] = include_bytes!("../../../assets/images/sponsor-wechat.jpg"); pub const DIAGNOSTIC_BUILD_ID: &str = "diag-20260518-1"; -const DREAM_SKIN_RENDERER_REVISION: &str = "17"; +const DREAM_SKIN_RENDERER_REVISION: &str = "18"; pub fn renderer_script() -> &'static str { RENDERER_SCRIPT diff --git a/crates/codex-plus-core/src/dream_skin_runtime.rs b/crates/codex-plus-core/src/dream_skin_runtime.rs index ebada7887..715f4ed00 100644 --- a/crates/codex-plus-core/src/dream_skin_runtime.rs +++ b/crates/codex-plus-core/src/dream_skin_runtime.rs @@ -406,27 +406,46 @@ pub fn renderer_verification_script() -> &'static str { visible: rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden", }; }; + const root = document.documentElement; const homeSignal = document.querySelector('[data-testid="home-icon"]') || document.querySelector('[data-feature="game-source"]') || document.querySelector('.group\\/home-suggestions'); const homeRoute = homeSignal?.closest('[role="main"]') || null; - const home = document.querySelector('[role="main"].dream-home, [role="main"].dream-skin-home, [role="main"].glass-vision-home'); + const home = document.querySelector('[role="main"]:has([data-testid="home-icon"])') || + homeRoute || + document.querySelector('[role="main"].dream-home, [role="main"].dream-skin-home, [role="main"].glass-vision-home'); const suggestions = home?.querySelector('.group\\/home-suggestions') || null; - const cards = suggestions ? [...suggestions.querySelectorAll('button')].map(box) : []; + const cardButtons = suggestions ? [...suggestions.querySelectorAll('button')] : []; + const cards = cardButtons.map(box); const chrome = document.getElementById('codex-dream-skin-chrome') || document.getElementById('codex-glass-vision-skin-chrome'); + const runtime = window.__CODEX_DREAM_SKIN_STATE__ || window.__CODEX_GLASS_VISION_SKIN_STATE__; + const modernInstalled = root?.getAttribute('data-dream-skin') === 'active'; + const adopted = runtime?.styleMode === 'adopted' && runtime.styleSheet && + [...document.adoptedStyleSheets].includes(runtime.styleSheet); + const fallbackStyle = runtime?.styleMode === 'style' && + document.getElementById('codex-dream-skin-style') === runtime.styleNode; + const homeChildren = home?.children ? [...home.children] : []; + const bannerHolder = homeChildren.find((node) => node.querySelector?.('.home-banners')); + const siblingCandidates = homeChildren.filter((node) => node !== bannerHolder).map(box); + const heroChain = []; + for (let node = home?.firstElementChild || null; node && heroChain.length < 3; + node = node.firstElementChild) heroChain.push(node); + const heroCandidates = heroChain.map(box); + const hero = siblingCandidates.find((item) => item?.visible && item.width >= 280 && item.height >= 120) || + [...heroCandidates].reverse().find((item) => item?.visible) || + siblingCandidates.find((item) => item?.visible) || null; return JSON.stringify({ - installed: document.documentElement.classList.contains('codex-dream-skin') || - document.documentElement.classList.contains('codex-glass-vision-skin'), - version: window.__CODEX_DREAM_SKIN_STATE__?.version || - window.__CODEX_GLASS_VISION_SKIN_STATE__?.version || null, - stylePresent: Boolean(document.getElementById('codex-dream-skin-style') || + installed: modernInstalled || root?.classList.contains('codex-dream-skin') || + root?.classList.contains('codex-glass-vision-skin'), + version: runtime?.version || null, + stylePresent: Boolean(adopted || fallbackStyle || document.getElementById('codex-dream-skin-style') || document.getElementById('codex-glass-vision-skin-style')), - chromePresent: Boolean(chrome), - chromePointerEvents: getComputedStyle(chrome || document.body).pointerEvents, + chromePresent: Boolean(chrome || modernInstalled), + chromePointerEvents: chrome ? getComputedStyle(chrome).pointerEvents : (modernInstalled ? "none" : null), homeRoute: Boolean(homeRoute), homePresent: Boolean(home), - hero: box(home?.firstElementChild?.firstElementChild?.firstElementChild), + hero, visibleCardCount: cards.filter((item) => item?.visible).length, projectButton: box(home?.querySelector('.group\\/project-selector > button')), composer: box(document.querySelector('.composer-surface-chrome')), diff --git a/crates/codex-plus-core/src/settings.rs b/crates/codex-plus-core/src/settings.rs index 4a2d54bb9..7e4a07a39 100644 --- a/crates/codex-plus-core/src/settings.rs +++ b/crates/codex-plus-core/src/settings.rs @@ -316,11 +316,7 @@ impl Default for DreamSkinThemeConfig { project_label: default_dream_skin_project_label(), status_text: default_dream_skin_status_text(), quote: default_dream_skin_quote(), - colors: if cfg!(windows) { - None - } else { - Some(DreamSkinColors::default()) - }, + colors: Some(DreamSkinColors::default()), extra_fields, } } diff --git a/crates/codex-plus-core/src/watcher.rs b/crates/codex-plus-core/src/watcher.rs index 9ac74aa94..b3dda1c62 100644 --- a/crates/codex-plus-core/src/watcher.rs +++ b/crates/codex-plus-core/src/watcher.rs @@ -132,7 +132,9 @@ pub fn filter_killable_launcher_processes<'a>( processes .into_iter() .filter(|(process_id, _, exe_file)| { - !protected.contains(process_id) && exe_file.eq_ignore_ascii_case("codex-plus-plus.exe") + !protected.contains(process_id) + && (exe_file.eq_ignore_ascii_case("codex-plus-plus.exe") + || exe_file.eq_ignore_ascii_case("codex-plus.exe")) }) .map(|(process_id, _, _)| process_id) .collect() diff --git a/crates/codex-plus-core/tests/cdp_bridge.rs b/crates/codex-plus-core/tests/cdp_bridge.rs index d988f23f5..2ac7ebddb 100644 --- a/crates/codex-plus-core/tests/cdp_bridge.rs +++ b/crates/codex-plus-core/tests/cdp_bridge.rs @@ -536,11 +536,13 @@ fn injection_script_installs_dream_skin_from_backend_settings() { assert!(script.contains("state.observer?.disconnect?.()")); assert!(script.contains("window.__CODEX_PLUS_DREAM_SKIN_PAYLOAD_SIGNATURE__")); assert!(script.contains("window.__CODEX_PLUS_DREAM_SKIN_THEME__")); + assert!(script.contains("\"colors\"")); assert!(script.contains("data:image/webp;base64,UklGRg==")); assert!(script.contains("codex-dream-skin-companion")); assert!(script.contains("removeDreamSkinCompanion")); if cfg!(windows) { - assert!(script.contains(":root.codex-dream-skin")); + assert!(script.contains("data-dream-skin")); + assert!(script.contains("data-dream-shell")); assert!(!script.contains("薛凯琪专属定制皮肤")); } assert!(script.contains(".group\\\\/home-suggestions")); diff --git a/crates/codex-plus-core/tests/dream_skin.rs b/crates/codex-plus-core/tests/dream_skin.rs index 0356d1c63..dbc708bc7 100644 --- a/crates/codex-plus-core/tests/dream_skin.rs +++ b/crates/codex-plus-core/tests/dream_skin.rs @@ -15,7 +15,7 @@ fn backend_settings_defaults_to_upstream_platform_theme_config() { assert_eq!(theme.id, "preset-arina-hashimoto"); assert_eq!(theme.name, "桥本有菜"); assert_eq!(theme.tagline, "把柔光与玫瑰带进今天的工作台。"); - assert!(theme.colors.is_none()); + assert_eq!(theme.colors.as_ref().unwrap().accent, "#E25563"); assert_eq!(theme.extra_fields["appearance"], "auto"); assert_eq!(theme.extra_fields["art"]["safeArea"], "left"); } else { diff --git a/crates/codex-plus-core/tests/dream_skin_runtime.rs b/crates/codex-plus-core/tests/dream_skin_runtime.rs index 3e871e187..68d02260f 100644 --- a/crates/codex-plus-core/tests/dream_skin_runtime.rs +++ b/crates/codex-plus-core/tests/dream_skin_runtime.rs @@ -118,6 +118,28 @@ fn verification_accepts_target_project_live_contract() { assert!(result.pass); } +#[test] +fn verification_accepts_modern_runtime_without_legacy_chrome() { + let result = parse_renderer_verification(serde_json::json!({ + "installed": true, + "version": "codex-plus:windows:dream-skin:r18", + "stylePresent": true, + "chromePresent": true, + "chromePointerEvents": "none", + "homeRoute": false, + "homePresent": false, + "visibleCardCount": 0, + "projectButton": null, + "composer": { "visible": true }, + "sidebar": { "visible": true }, + "documentOverflow": { "x": false, "y": false } + })) + .unwrap(); + + assert_eq!(result.state, DreamSkinState::Pass); + assert!(result.pass); +} + #[test] fn windows_identity_requires_a_path_inside_the_registered_package_root() { let root = std::path::Path::new( diff --git a/crates/codex-plus-core/tests/upstream_theme_assets.rs b/crates/codex-plus-core/tests/upstream_theme_assets.rs index 4ffa044e2..060f1690e 100644 --- a/crates/codex-plus-core/tests/upstream_theme_assets.rs +++ b/crates/codex-plus-core/tests/upstream_theme_assets.rs @@ -19,19 +19,27 @@ fn bundled_target_renderers_and_styles_remain_byte_exact() { for (path, hash) in [ ( "assets/inject/upstream/dream-skin/windows/renderer-inject.js", - "74D3BFB0F0F55C138EE3B0933F7B55BC11F71D09F1B07794BCA51B6598DE203D", + "DB48AE78497EB9C1EB800F32C110778490A3913EDD969429697DC3EBD292DE97", ), ( "assets/inject/upstream/dream-skin/windows/dream-skin.css", - "12848DA7DDAACF1B0F18CD419B2B27A6355DCBCE01AF650F1BCE14D99FEBD532", + "AFF9433B526D7149DA99B7C07367B75BCA558F9D8114243331FB9099B54E2916", + ), + ( + "assets/inject/upstream/dream-skin/windows/theme.json", + "9068F781F190D213FE5BE180A12AB1ED534FAFDD0BFBA00276C951A178DFE72A", ), ( "assets/inject/upstream/dream-skin/macos/renderer-inject.js", - "2704C39506C66554C3529BF0D15B876B4AEC2DD9A36B1796AB43E19C33A046FC", + "DB48AE78497EB9C1EB800F32C110778490A3913EDD969429697DC3EBD292DE97", ), ( "assets/inject/upstream/dream-skin/macos/dream-skin.css", - "EC3C3BC5F6E10E20A3F2307796BD1E1350E80E5D23D37318EE5468833C95A6DF", + "AFF9433B526D7149DA99B7C07367B75BCA558F9D8114243331FB9099B54E2916", + ), + ( + "assets/inject/upstream/dream-skin/macos/theme.json", + "FCCE3F314500BE1A58381FFE7F9A6B212912D54FF9A0E7D17B6664C1516F750E", ), ( "assets/inject/upstream/cidala-tiger/windows/renderer-inject.js", diff --git a/crates/codex-plus-core/tests/watcher.rs b/crates/codex-plus-core/tests/watcher.rs index 03feecd24..6c547c4de 100644 --- a/crates/codex-plus-core/tests/watcher.rs +++ b/crates/codex-plus-core/tests/watcher.rs @@ -120,10 +120,11 @@ fn launcher_process_filter_protects_current_process_ancestry() { (20, 10, "codex-plus-plus.exe"), (30, 20, "codex-plus-plus.exe"), (40, 10, "codex-plus-plus.exe"), - (50, 10, "codex-plus-plus-manager.exe"), + (50, 10, "codex-plus.exe"), + (60, 10, "codex-plus-plus-manager.exe"), ]; - assert_eq!(filter_killable_launcher_processes(processes, 30), vec![40]); + assert_eq!(filter_killable_launcher_processes(processes, 30), vec![40, 50]); } #[test]