fix(client): apply theme before plugin loading

This commit is contained in:
imccyu
2026-08-10 18:12:32 +08:00
parent 5d591e55c1
commit f0a6c25291
18 changed files with 336 additions and 15 deletions

View File

@@ -0,0 +1,40 @@
/**
* Host-rendered theme bootstrap for the browser's pre-plugin interval. Each
* index response embeds the current durable built-in preference; the browser
* resolves only `system`, then writes the same DOM fields ui-layout's
* ThemePresenter owns after the client plugin tree activates.
*/
import { DEFAULT_PREFERENCE, type ThemePreference } from './theme-settings.ts'
/** Build the inline script for one schema-validated built-in preference. */
function bootThemeScript(preference: ThemePreference): string {
return `<script>(() => {
const preference = ${JSON.stringify(preference)}
const systemDark = preference === 'system'
&& typeof matchMedia !== 'undefined'
&& matchMedia('(prefers-color-scheme: dark)').matches
const dark = preference === 'dark' || systemDark
document.documentElement.style.colorScheme = dark ? 'dark' : 'light'
document.body.toggleAttribute('data-ds-dark-theme', dark)
})()</script>`
}
/**
* Insert the theme bootstrap immediately after the opening body tag, before
* the shell mount and module script. Body-less fragments receive it at the
* end, where the HTML parser has already synthesized a body.
* @param html - Raw application index HTML.
* @param preference - Current Host-backed built-in preference.
* @returns HTML containing the theme bootstrap.
*/
export function injectBootTheme(
html: string,
preference: ThemePreference = DEFAULT_PREFERENCE,
): string {
const script = bootThemeScript(preference)
const body = /<body(?:\s[^>]*)?>/i.exec(html)
if (body === null) return `${html}${script}`
const at = body.index + body[0].length
return `${html.slice(0, at)}${script}${html.slice(at)}`
}

View File

@@ -1,23 +1,43 @@
/** Host registration for the browser theme preference. */
/** Host registration for the browser theme preference and pre-plugin palette. */
import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-host-webserver'
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
import { THEME_SETTINGS_NAMESPACE, ThemeSettingsSchema } from './theme-settings.ts'
import { injectBootTheme } from './boot-theme.ts'
import {
DEFAULT_PREFERENCE, THEME_SETTINGS_NAMESPACE, ThemeSettingsSchema,
type ThemePreference, type ThemeSettings,
} from './theme-settings.ts'
export {
DEFAULT_PREFERENCE, THEME_PREFERENCE_FIELD, THEME_PREFERENCES, THEME_SETTINGS_NAMESPACE,
type ThemePreference, type ThemeSettings,
} from './theme-settings.ts'
const THEME_NAMESPACE = settingsNamespace(THEME_SETTINGS_NAMESPACE)
/** Read the registered preference or use the schema default without a settings provider. */
function readPreference(ctx: Context): ThemePreference {
const settings = ctx.get('settings')
if (settings === undefined) return DEFAULT_PREFERENCE
const section = settings.get(THEME_NAMESPACE) as ThemeSettings | undefined
if (section === undefined) return DEFAULT_PREFERENCE
return section.preference
}
/**
* Register the durable theme section when a settings provider exists.
* @param ctx - Host context whose optional settings service owns the section.
* Register the durable theme section and initial-theme index transform when
* their optional Host services are composed.
* @param ctx - Host context that may acquire settings and HTTP services.
*/
export function apply(ctx: Context): void {
ctx.inject(['settings'], (settingsCtx) => {
settingsCtx.settings.register(
settingsNamespace(THEME_SETTINGS_NAMESPACE),
ThemeSettingsSchema,
settingsCtx.settings.register(THEME_NAMESPACE, ThemeSettingsSchema)
})
ctx.inject(['httpServer'], (httpCtx) => {
httpCtx.effect(
() => httpCtx.httpServer.tapIndex(html => injectBootTheme(html, readPreference(ctx))),
'client-ui-theme: initial theme bootstrap',
)
})
}