fix: restore native appearance styling
This commit is contained in:
@@ -69,7 +69,7 @@ type NativeThemeId = "light" | "dark";
|
||||
|
||||
interface ThemeService {
|
||||
getTheme(): { preference: string };
|
||||
setTheme(id: NativeThemeId): void;
|
||||
setTheme(id: NativeThemeId | "system"): void;
|
||||
}
|
||||
|
||||
interface ModelSelection {
|
||||
@@ -110,6 +110,8 @@ interface SliderProps {
|
||||
interface SettingsRowProps {
|
||||
scope: PreferenceStore;
|
||||
presenter: SkinPresenter;
|
||||
theme: ThemeService;
|
||||
subscribeTheme: (listener: () => void) => () => void;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
|
||||
@@ -480,19 +482,77 @@ function LiangEffortSlider({ directory, load, select, presenter, scope }: Slider
|
||||
);
|
||||
}
|
||||
|
||||
function AppearanceSkinRow({ scope, presenter, t }: SettingsRowProps) {
|
||||
type AppearanceChoice = NativeThemeId | "system" | "liang";
|
||||
|
||||
const APPEARANCE_CHOICES: readonly { id: AppearanceChoice; labelKey: string }[] = [
|
||||
{ id: "light", labelKey: "appearance.light" },
|
||||
{ id: "dark", labelKey: "appearance.dark" },
|
||||
{ id: "system", labelKey: "appearance.system" },
|
||||
{ id: "liang", labelKey: "appearance.liang" },
|
||||
];
|
||||
|
||||
function NativeAppearanceIcon({ id }: { id: Exclude<AppearanceChoice, "liang"> }) {
|
||||
if (id === "light") {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M11.3496 8C11.3496 6.14985 9.85015 4.65039 8 4.65039C6.14985 4.65039 4.65039 6.14985 4.65039 8C4.65039 9.85015 6.14985 11.3496 8 11.3496C9.85015 11.3496 11.3496 9.85015 11.3496 8ZM12.6504 8C12.6504 10.5681 10.5681 12.6504 8 12.6504C5.43188 12.6504 3.34961 10.5681 3.34961 8C3.34961 5.43188 5.43188 3.34961 8 3.34961C10.5681 3.34961 12.6504 5.43188 12.6504 8Z" fill="currentColor" />
|
||||
<path d="M8.65039 0.5V2.5H7.34961V0.5H8.65039Z" fill="currentColor" />
|
||||
<path d="M8.65039 13.5V15.5H7.34961V13.5H8.65039Z" fill="currentColor" />
|
||||
<path d="M3.15808 2.24035L4.57229 3.65456L3.6525 4.57435L2.23829 3.16014L3.15808 2.24035Z" fill="currentColor" />
|
||||
<path d="M12.3505 11.4327L13.7647 12.8469L12.8449 13.7667L11.4307 12.3525L12.3505 11.4327Z" fill="currentColor" />
|
||||
<path d="M2.24537 12.8469L3.65958 11.4327L4.57937 12.3525L3.16516 13.7667L2.24537 12.8469Z" fill="currentColor" />
|
||||
<path d="M11.4377 3.65455L12.852 2.24033L13.7718 3.16012L12.3575 4.57434L11.4377 3.65455Z" fill="currentColor" />
|
||||
<path d="M0.5 7.35461H2.5V8.6554H0.5L0.5 7.35461Z" fill="currentColor" />
|
||||
<path d="M13.5 7.35461H15.5V8.6554H13.5V7.35461Z" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
if (id === "dark") {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M13.2764 9.52324C12.5607 9.97754 11.7177 10.242 10.7812 10.242C8.11386 10.2419 5.95042 8.07997 5.9502 5.41289C5.9502 4.48128 6.21453 3.61071 6.67188 2.87285C4.30332 3.4658 2.54992 5.60845 2.5498 8.16093C2.5498 11.1712 4.99103 13.6102 8 13.6102C10.5383 13.6102 12.6709 11.8724 13.2764 9.52324ZM7.05078 5.41289C7.051 7.47224 8.72116 9.1423 10.7812 9.14238C11.9248 9.14238 12.887 8.63397 13.5781 7.8084C13.7266 7.63106 13.9701 7.56547 14.1875 7.64433C14.4049 7.72329 14.5497 7.9297 14.5498 8.16093C14.5498 11.7766 11.6161 14.7098 8 14.7098C4.38402 14.7098 1.4502 11.7792 1.4502 8.16093C1.45033 4.54322 4.3812 1.61015 8 1.61015C8.23027 1.61015 8.43585 1.75352 8.51562 1.96953C8.59536 2.18554 8.53241 2.42829 8.35742 2.57793C7.55573 3.26311 7.05078 4.27876 7.05078 5.41289Z" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M12.1665 13.5811V14.7803H3.66651V13.5811H12.1665Z" fill="currentColor" />
|
||||
<path d="M13.4453 7.02379C13.4453 6.04702 13.4452 5.3616 13.3887 4.83434C13.3333 4.31828 13.2302 4.02378 13.0723 3.80309C12.9446 3.62475 12.7877 3.46883 12.6094 3.34117C12.3887 3.18328 12.0942 3.08007 11.5781 3.02477C11.0508 2.96829 10.3655 2.96715 9.38867 2.96715H6.61035C5.63359 2.96715 4.94816 2.96827 4.4209 3.02477C3.90486 3.0801 3.61034 3.18321 3.38965 3.34117C3.21143 3.46878 3.05534 3.62487 2.92774 3.80309C2.76977 4.02377 2.66667 4.3183 2.61133 4.83434C2.55483 5.3616 2.55371 6.04702 2.55371 7.02379C2.55371 8.0006 2.55485 8.68596 2.61133 9.21324C2.66663 9.72936 2.76983 10.0238 2.92774 10.2445C3.0554 10.4228 3.21131 10.5797 3.38965 10.7074C3.61034 10.8654 3.90484 10.9685 4.4209 11.0238C4.94816 11.0803 5.63359 11.0804 6.61035 11.0804H9.38867C10.3654 11.0804 11.0508 11.0803 11.5781 11.0238C12.0941 10.9685 12.3887 10.8652 12.6094 10.7074C12.7877 10.5797 12.9446 10.4229 13.0723 10.2445C13.2301 10.0238 13.3334 9.72927 13.3887 9.21324C13.4452 8.68596 13.4453 8.00058 13.4453 7.02379ZM14.6455 7.02379C14.6455 7.97428 14.646 8.73509 14.5811 9.34117C14.5149 9.95828 14.3756 10.4858 14.0479 10.9437C13.8436 11.229 13.5938 11.4788 13.3086 11.683C12.8507 12.0108 12.3232 12.15 11.7061 12.2162C11.1 12.2811 10.3391 12.2806 9.38867 12.2806H6.61035C5.66018 12.2806 4.89991 12.2811 4.29395 12.2162C3.67684 12.15 3.14935 12.0108 2.69141 11.683C2.40613 11.4788 2.15639 11.229 1.95215 10.9437C1.62436 10.4858 1.4841 9.95828 1.41797 9.34117C1.35305 8.73511 1.35449 7.97424 1.35449 7.02379C1.35449 6.07366 1.35308 5.31333 1.41797 4.70738C1.4841 4.09028 1.62436 3.56279 1.95215 3.10485C2.15638 2.81956 2.40613 2.56982 2.69141 2.36559C3.14935 2.03779 3.67684 1.89753 4.29395 1.83141C4.8999 1.76652 5.66022 1.76793 6.61035 1.76793H9.38867C10.3391 1.76793 11.1 1.76649 11.7061 1.83141C12.3232 1.89753 12.8507 2.03779 13.3086 2.36559C13.5939 2.56982 13.8436 2.81957 14.0479 3.10485C14.3756 3.56279 14.5149 4.09028 14.5811 4.70738C14.646 5.31335 14.6455 6.07362 14.6455 7.02379Z" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function AppearanceSkinRow({ scope, presenter, theme, subscribeTheme, t }: SettingsRowProps) {
|
||||
const snapshot = useSyncExternalStore(
|
||||
(listener) => scope.subscribe(listener),
|
||||
() => scope.getSnapshot(),
|
||||
);
|
||||
const enabled = snapshot.enabled;
|
||||
const preference = useSyncExternalStore(
|
||||
subscribeTheme,
|
||||
() => theme.getTheme().preference,
|
||||
);
|
||||
const selected = snapshot.enabled
|
||||
? "liang"
|
||||
: preference === "light" || preference === "dark" || preference === "system"
|
||||
? preference
|
||||
: "system";
|
||||
const [pending, setPending] = useState(false);
|
||||
|
||||
const choose = async (next: boolean) => {
|
||||
if (next === enabled || pending) return;
|
||||
const choose = async (next: AppearanceChoice) => {
|
||||
if (next === selected || pending) return;
|
||||
setPending(true);
|
||||
try {
|
||||
await presenter.choose(next);
|
||||
if (next === "liang") {
|
||||
await presenter.choose(true);
|
||||
} else {
|
||||
// Disable the custom skin first. This is intentionally unconditional:
|
||||
// it makes the native choice the only writer after this click, even if
|
||||
// the external preference store is one render behind.
|
||||
await presenter.choose(false);
|
||||
theme.setTheme(next);
|
||||
}
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
@@ -502,29 +562,109 @@ function AppearanceSkinRow({ scope, presenter, t }: SettingsRowProps) {
|
||||
<div className="liang-settings-row" data-plugin={PACKAGE_ID}>
|
||||
<span className="liang-settings-row__title">{t("appearance.title")}</span>
|
||||
<div className="liang-settings-row__choices">
|
||||
<button
|
||||
className="liang-settings-row__choice"
|
||||
type="button"
|
||||
aria-pressed={!enabled}
|
||||
disabled={pending}
|
||||
onClick={() => void choose(false)}
|
||||
>
|
||||
{t("appearance.native")}
|
||||
</button>
|
||||
<button
|
||||
className="liang-settings-row__choice"
|
||||
type="button"
|
||||
aria-pressed={enabled}
|
||||
disabled={pending}
|
||||
onClick={() => void choose(true)}
|
||||
>
|
||||
{t("appearance.liang")}
|
||||
</button>
|
||||
{APPEARANCE_CHOICES.map(({ id, labelKey }) => (
|
||||
<button
|
||||
className={`liang-settings-row__choice${id === "liang" ? " liang-settings-row__choice--liang" : ""}`}
|
||||
type="button"
|
||||
aria-pressed={selected === id}
|
||||
disabled={pending}
|
||||
onClick={() => void choose(id)}
|
||||
key={id}
|
||||
>
|
||||
{id === "liang" ? (
|
||||
<span className="liang-settings-row__liang-icon" aria-hidden="true">◈</span>
|
||||
) : (
|
||||
<NativeAppearanceIcon id={id} />
|
||||
)}
|
||||
{t(labelKey)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const NATIVE_APPEARANCE_GROUP = '[class*="_8HJdBW_group"]';
|
||||
const NATIVE_APPEARANCE_ROW = '[class*="_8HJdBW_cubeRow"]';
|
||||
const LIANG_APPEARANCE_BUTTON = "liang-appearance-choice";
|
||||
|
||||
function installLiangAppearanceButton(scope: PreferenceStore, presenter: SkinPresenter) {
|
||||
let pending = false;
|
||||
const hookedNativeButtons = new Set<HTMLButtonElement>();
|
||||
const nativeClickHandlers = new Map<HTMLButtonElement, () => void>();
|
||||
|
||||
const sync = () => {
|
||||
const group = [...document.querySelectorAll<HTMLElement>(NATIVE_APPEARANCE_GROUP)]
|
||||
.find((node) => node.querySelector('[class*="_8HJdBW_themeCube"]'));
|
||||
const row = group?.querySelector<HTMLElement>(NATIVE_APPEARANCE_ROW);
|
||||
if (row === undefined || row === null) return;
|
||||
|
||||
let customButton = row.querySelector<HTMLButtonElement>(`.${LIANG_APPEARANCE_BUTTON}`);
|
||||
if (customButton === null) {
|
||||
customButton = document.createElement("button");
|
||||
customButton.className = LIANG_APPEARANCE_BUTTON;
|
||||
customButton.type = "button";
|
||||
customButton.dataset.plugin = PACKAGE_ID;
|
||||
customButton.setAttribute("aria-label", "滑动变祖");
|
||||
|
||||
const icon = document.createElement("span");
|
||||
icon.className = "liang-appearance-choice__icon";
|
||||
icon.setAttribute("aria-hidden", "true");
|
||||
icon.textContent = "◈";
|
||||
|
||||
const label = document.createElement("span");
|
||||
label.className = "liang-appearance-choice__label";
|
||||
label.textContent = document.documentElement.lang.toLowerCase().startsWith("en")
|
||||
? "Slider"
|
||||
: "滑动变祖";
|
||||
customButton.append(icon, label);
|
||||
customButton.addEventListener("click", () => {
|
||||
if (pending || scope.getSnapshot().enabled) return;
|
||||
pending = true;
|
||||
sync();
|
||||
void presenter.choose(true).finally(() => {
|
||||
pending = false;
|
||||
sync();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (customButton.parentElement !== row) row.append(customButton);
|
||||
customButton.disabled = pending;
|
||||
customButton.setAttribute("aria-pressed", String(scope.getSnapshot().enabled));
|
||||
|
||||
for (const nativeButton of row.querySelectorAll<HTMLButtonElement>('[class*="_8HJdBW_themeCube"]')) {
|
||||
if (hookedNativeButtons.has(nativeButton)) continue;
|
||||
hookedNativeButtons.add(nativeButton);
|
||||
const handleNativeClick = () => {
|
||||
if (pending || !scope.getSnapshot().enabled) return;
|
||||
pending = true;
|
||||
sync();
|
||||
void presenter.choose(false).finally(() => {
|
||||
pending = false;
|
||||
sync();
|
||||
});
|
||||
};
|
||||
nativeClickHandlers.set(nativeButton, handleNativeClick);
|
||||
nativeButton.addEventListener("click", handleNativeClick, { capture: true });
|
||||
}
|
||||
};
|
||||
|
||||
const observer = new MutationObserver(sync);
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
const unsubscribe = scope.subscribe(sync);
|
||||
sync();
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
unsubscribe();
|
||||
for (const [nativeButton, handleNativeClick] of nativeClickHandlers) {
|
||||
nativeButton.removeEventListener("click", handleNativeClick, { capture: true });
|
||||
}
|
||||
document.querySelectorAll(`.${LIANG_APPEARANCE_BUTTON}`).forEach((button) => button.remove());
|
||||
};
|
||||
}
|
||||
|
||||
export const inject = [
|
||||
"slots",
|
||||
"sessions",
|
||||
@@ -578,30 +718,26 @@ export function apply(ctx: ClientContext) {
|
||||
const presenter = new SkinPresenter(scope, theme);
|
||||
ctx.effect(() => () => presenter.dispose(), "liang-intensity-skin: backdrop presenter");
|
||||
ctx.effect(
|
||||
() => ctx.on("theme/change", () => presenter.syncNativeTheme()),
|
||||
"liang-intensity-skin: native theme synchronization",
|
||||
() => installLiangAppearanceButton(scope, presenter),
|
||||
"liang-intensity-skin: native appearance extension",
|
||||
);
|
||||
|
||||
ctx.effect(() => ctx.locale.register(LOCALE_NAMESPACE, {
|
||||
ctx.effect(
|
||||
() => ctx.locale.register(LOCALE_NAMESPACE, {
|
||||
zh: {
|
||||
"appearance.title": "外观皮肤",
|
||||
"appearance.native": "原生",
|
||||
"appearance.title": "外观",
|
||||
"appearance.light": "浅色",
|
||||
"appearance.dark": "深色",
|
||||
"appearance.system": "跟随系统",
|
||||
"appearance.liang": "滑动变祖",
|
||||
},
|
||||
en: {
|
||||
"appearance.title": "Visual skin",
|
||||
"appearance.native": "Native",
|
||||
"appearance.title": "Appearance",
|
||||
"appearance.light": "Light",
|
||||
"appearance.dark": "Dark",
|
||||
"appearance.system": "System",
|
||||
"appearance.liang": "Slider",
|
||||
},
|
||||
}), "liang-intensity-skin: settings locale");
|
||||
|
||||
ctx.slots.inject("settings.general.item", () => ctx.slots.register({
|
||||
name: "settings.general.item",
|
||||
id: "liang-intensity-appearance",
|
||||
order: 11,
|
||||
locale: LOCALE_NAMESPACE,
|
||||
inject: () => ({ scope, presenter }),
|
||||
}, AppearanceSkinRow));
|
||||
}), "liang-intensity-skin: settings locale");
|
||||
|
||||
ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
|
||||
name: "conversation.input.right",
|
||||
|
||||
@@ -477,47 +477,58 @@ body[data-liang-skin="on"] .liang-effort-control__range::-webkit-slider-thumb {
|
||||
--liang-control-accent: var(--dsw-alias-state-error-primary, #e43c3c);
|
||||
}
|
||||
|
||||
.liang-settings-row {
|
||||
display: flex;
|
||||
min-height: 82px;
|
||||
.liang-appearance-choice {
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--dsw-alias-border-l2);
|
||||
flex: 180px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid var(--dsw-alias-border-l1, rgb(0 0 0 / 6%));
|
||||
gap: 4px;
|
||||
padding: 20px 32px;
|
||||
border-radius: 16px;
|
||||
font: inherit;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.liang-settings-row__title {
|
||||
flex: 0 0 auto;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.liang-settings-row__choices {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.liang-settings-row__choice {
|
||||
min-width: 76px;
|
||||
height: 40px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--dsw-alias-border-l2, rgb(0 0 0 / 10%));
|
||||
border-radius: 10px;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
background: var(--dsw-alias-bg-layer-1);
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.liang-settings-row__choice:hover {
|
||||
[class*="_8HJdBW_cubeRow"]:has(.liang-appearance-choice) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.liang-appearance-choice:hover:not([aria-pressed="true"]) {
|
||||
background: var(--dsw-alias-interactive-bg-hover);
|
||||
}
|
||||
|
||||
.liang-settings-row__choice[aria-pressed="true"] {
|
||||
border-color: var(--dsw-alias-label-primary);
|
||||
color: var(--dsw-alias-label-primary);
|
||||
box-shadow: inset 0 0 0 1px var(--dsw-alias-label-primary);
|
||||
.liang-appearance-choice[aria-pressed="true"] {
|
||||
border-color: var(--dsw-static-neutral-bluish-400);
|
||||
background: var(--dsw-alias-bg-module-platform);
|
||||
}
|
||||
|
||||
.liang-appearance-choice__icon {
|
||||
display: block;
|
||||
flex: 0 0 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: currentColor;
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.liang-appearance-choice__label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Keep the host's three theme cubes untouched. These rules only style the
|
||||
fourth button that is appended to the host cube row. */
|
||||
.liang-appearance-choice:disabled {
|
||||
cursor: progress;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
@@ -532,11 +543,6 @@ body[data-liang-skin="on"] .liang-effort-control__range::-webkit-slider-thumb {
|
||||
opacity: calc(var(--liang-portrait-opacity, 0.45) * 0.72);
|
||||
}
|
||||
|
||||
.liang-settings-row {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
Reference in New Issue
Block a user