2
0

fix: sync liang skin theme and dark controls

This commit is contained in:
kingOfSoySauce
2026-08-15 12:54:08 +08:00
parent 77c781e7ff
commit b875332a8c
5 changed files with 685 additions and 13 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -65,6 +65,7 @@
"@deepseek-ai/dsh-client-ui-conversation", "@deepseek-ai/dsh-client-ui-conversation",
"@deepseek-ai/dsh-client-ui-model-selection", "@deepseek-ai/dsh-client-ui-model-selection",
"@deepseek-ai/dsh-client-ui-settings", "@deepseek-ai/dsh-client-ui-settings",
"@deepseek-ai/dsh-client-ui-theme",
"@deepseek-ai/dsh-client-ui-settings-general", "@deepseek-ai/dsh-client-ui-settings-general",
"@deepseek-ai/dsh-client-locale", "@deepseek-ai/dsh-client-locale",
"@deepseek-ai/dsh-api-remotes" "@deepseek-ai/dsh-api-remotes"

View File

@@ -65,6 +65,13 @@ interface PreferenceStore {
dispose(): void; dispose(): void;
} }
type NativeThemeId = "light" | "dark";
interface ThemeService {
getTheme(): { preference: string };
setTheme(id: NativeThemeId): void;
}
interface ModelSelection { interface ModelSelection {
provider: string; provider: string;
model: string; model: string;
@@ -126,6 +133,7 @@ const cssVariables = [
class SkinPresenter { class SkinPresenter {
private readonly scope: PreferenceStore; private readonly scope: PreferenceStore;
private readonly theme: ThemeService;
private readonly root: HTMLDivElement; private readonly root: HTMLDivElement;
private readonly video: HTMLVideoElement; private readonly video: HTMLVideoElement;
private readonly poster: HTMLImageElement; private readonly poster: HTMLImageElement;
@@ -138,8 +146,9 @@ class SkinPresenter {
private disposed = false; private disposed = false;
private unsubscribe: () => void; private unsubscribe: () => void;
constructor(scope: PreferenceStore) { constructor(scope: PreferenceStore, theme: ThemeService) {
this.scope = scope; this.scope = scope;
this.theme = theme;
this.root = document.createElement("div"); this.root = document.createElement("div");
this.root.className = "liang-skin-backdrop"; this.root.className = "liang-skin-backdrop";
this.root.dataset.plugin = PACKAGE_ID; this.root.dataset.plugin = PACKAGE_ID;
@@ -242,6 +251,7 @@ class SkinPresenter {
private applyFrame() { private applyFrame() {
const palette = paletteForFrame(this.frame); const palette = paletteForFrame(this.frame);
const body = document.body; const body = document.body;
this.syncNativeTheme(palette.stage === 5 ? "dark" : "light");
body.dataset.liangStage = String(palette.stage); body.dataset.liangStage = String(palette.stage);
body.style.setProperty("--liang-strength", String(palette.strength)); body.style.setProperty("--liang-strength", String(palette.strength));
body.style.setProperty("--liang-page", palette.page); body.style.setProperty("--liang-page", palette.page);
@@ -262,6 +272,11 @@ class SkinPresenter {
this.seek(this.frame); this.seek(this.frame);
} }
syncNativeTheme(theme: NativeThemeId = paletteForFrame(this.frame).stage === 5 ? "dark" : "light") {
if (!this.enabled || this.theme.getTheme().preference === theme) return;
this.theme.setTheme(theme);
}
private updatePortrait(level: number) { private updatePortrait(level: number) {
const { lowerIndex, upperIndex, mix } = portraitBlendForLevel( const { lowerIndex, upperIndex, mix } = portraitBlendForLevel(
level, level,
@@ -515,6 +530,7 @@ export const inject = [
"sessions", "sessions",
"modelDirectories", "modelDirectories",
"locale", "locale",
"theme",
]; ];
function createPreferenceStore(): PreferenceStore { function createPreferenceStore(): PreferenceStore {
@@ -558,8 +574,13 @@ export function apply(ctx: ClientContext) {
const scope = createPreferenceStore(); const scope = createPreferenceStore();
ctx.effect(() => () => scope.dispose(), "liang-intensity-skin: appearance preference"); ctx.effect(() => () => scope.dispose(), "liang-intensity-skin: appearance preference");
const presenter = new SkinPresenter(scope); const theme = ctx.get("theme") as ThemeService;
const presenter = new SkinPresenter(scope, theme);
ctx.effect(() => () => presenter.dispose(), "liang-intensity-skin: backdrop presenter"); ctx.effect(() => () => presenter.dispose(), "liang-intensity-skin: backdrop presenter");
ctx.effect(
() => ctx.on("theme/change", () => presenter.syncNativeTheme()),
"liang-intensity-skin: native theme synchronization",
);
ctx.effect(() => ctx.locale.register(LOCALE_NAMESPACE, { ctx.effect(() => ctx.locale.register(LOCALE_NAMESPACE, {
zh: { zh: {

View File

@@ -100,6 +100,14 @@ body[data-liang-skin="on"] {
--dsw-specific-input-major: var(--liang-layer-1) !important; --dsw-specific-input-major: var(--liang-layer-1) !important;
--dsw-specific-menu: var(--liang-layer-3) !important; --dsw-specific-menu: var(--liang-layer-3) !important;
--dsw-specific-selector: var(--liang-layer-2) !important; --dsw-specific-selector: var(--liang-layer-2) !important;
--dsw-alias-markdown-citation: var(--liang-layer-2) !important;
--dsw-alias-markdown-code-block-banner: var(--liang-layer-1) !important;
--dsw-alias-markdown-code-block: var(--liang-layer-1) !important;
--dsw-alias-markdown-code-segment-selected: var(--liang-layer-2) !important;
--dsw-alias-markdown-code-segment-unselected: var(--liang-layer-1) !important;
--dsw-alias-markdown-inline-code: var(--liang-layer-2) !important;
--dsw-alias-markdown-placeholder: var(--liang-layer-1) !important;
--dsw-alias-markdown-tag: var(--liang-layer-2) !important;
background: var(--liang-page) !important; background: var(--liang-page) !important;
} }
@@ -135,10 +143,85 @@ body[data-liang-skin="on"][data-liang-stage="5"] {
--dsw-alias-button-info-fill: #c19a49 !important; --dsw-alias-button-info-fill: #c19a49 !important;
--dsw-alias-button-info-hover: #d5b56e !important; --dsw-alias-button-info-hover: #d5b56e !important;
--dsw-alias-state-business-primary: #c19a49 !important; --dsw-alias-state-business-primary: #c19a49 !important;
--dsw-alias-bg-module-platform: var(--liang-layer-3) !important;
--dsw-specific-sidebar-nav-item-active: var(--liang-layer-3) !important;
--dsw-specific-sidebar-nav-item-hover: var(--liang-hover) !important;
} }
/* Preserve the wordmark badge contrast even when the underlying Harness /* The low-intensity skin is light by default, but it must not turn a dark
preference is dark while our low-intensity shell is light. */ Harness document into a light surface. Keep the skin's portrait progression
while switching the shared UI layers back to a readable charcoal palette. */
body[data-liang-skin="on"][data-ds-dark-theme]:is(
[data-liang-stage="0"],
[data-liang-stage="1"],
[data-liang-stage="2"],
[data-liang-stage="3"]
) {
--liang-page: rgb(17 17 17) !important;
--liang-bg-base: rgb(17 17 17 / 42%) !important;
--liang-layer-1: rgb(30 27 25 / 94%) !important;
--liang-layer-2: rgb(41 35 30 / 96%) !important;
--liang-layer-3: rgb(41 35 30 / 99%) !important;
--liang-sidebar: rgb(23 20 18 / 96%) !important;
--liang-ink: rgb(244 241 232) !important;
--liang-secondary: rgb(184 180 169) !important;
--liang-tertiary: rgb(137 134 126) !important;
--liang-border: rgb(244 241 232 / 15%) !important;
--liang-hover: rgb(244 241 232 / 9%) !important;
--dsw-alias-brand-primary: var(--liang-accent) !important;
--dsw-alias-brand-text: var(--liang-accent) !important;
--dsw-alias-button-primary-fill: var(--liang-accent) !important;
--dsw-alias-button-primary-hover: var(--liang-accent-hover) !important;
--dsw-alias-button-info-fill: var(--liang-accent) !important;
--dsw-alias-button-info-hover: var(--liang-accent-hover) !important;
--dsw-alias-state-business-primary: var(--liang-accent) !important;
--dsw-alias-button-elevated-fill: var(--liang-layer-1) !important;
--dsw-alias-button-floating-fill: var(--liang-layer-2) !important;
--dsw-alias-button-floating-hover: var(--liang-layer-3) !important;
--dsw-alias-interactive-bg-hover-solid: var(--liang-layer-2) !important;
--dsw-specific-bubble: var(--liang-layer-2) !important;
--dsw-specific-input-major: var(--liang-layer-1) !important;
--dsw-specific-menu: var(--liang-layer-3) !important;
--dsw-specific-selector: var(--liang-layer-2) !important;
background: var(--liang-page) !important;
}
body[data-liang-skin="on"][data-ds-dark-theme]:is(
[data-liang-stage="0"],
[data-liang-stage="1"],
[data-liang-stage="2"],
[data-liang-stage="3"]
) .liang-skin-backdrop {
background: var(--liang-page) !important;
}
body[data-liang-skin="on"][data-ds-dark-theme]:is(
[data-liang-stage="0"],
[data-liang-stage="1"],
[data-liang-stage="2"],
[data-liang-stage="3"]
) .liang-skin-backdrop::after {
background: linear-gradient(90deg, var(--liang-page) 0 28%, transparent 57% 100%);
}
/* The host's running-turn label uses a transparent blue shimmer. In the
custom dark skin it is a semantic status accent, so keep both the label
and its elapsed clock gold without changing the original light theme. */
body[data-liang-skin="on"][data-liang-stage="5"] [class*="_turnStatus"] {
color: #c19a49 !important;
background: #c19a49 !important;
background-clip: initial !important;
-webkit-background-clip: initial !important;
-webkit-text-fill-color: #c19a49 !important;
animation: none !important;
}
body[data-liang-skin="on"][data-liang-stage="5"] [class*="_turnStatusClock"] {
color: #c19a49 !important;
-webkit-text-fill-color: #c19a49 !important;
}
/* Preserve the wordmark badge contrast in the low-intensity shell. */
body[data-liang-skin="on"]:is( body[data-liang-skin="on"]:is(
[data-liang-stage="0"], [data-liang-stage="0"],
[data-liang-stage="1"], [data-liang-stage="1"],
@@ -219,9 +302,8 @@ body[data-liang-skin="on"] textarea {
transition: color 160ms linear, background-color 160ms linear, border-color 160ms linear; transition: color 160ms linear, background-color 160ms linear, border-color 160ms linear;
} }
/* Harness can itself be in dark mode while the low intensity stages are light. /* The composer card has a native theme-mode fill, so keep its surface and
The composer card has a native theme-mode fill, so explicitly bring only readable ink aligned with the active Liang palette. */
those light stages back to the original light surface and readable ink. */
body[data-liang-skin="on"]:is( body[data-liang-skin="on"]:is(
[data-liang-stage="0"], [data-liang-stage="0"],
[data-liang-stage="1"], [data-liang-stage="1"],