Files
xiaobaifupan/app/frontend/shared/theme.js
T
MS-01-Codexandmultica-agent 8ac3adbb5d feat: restore mentor page to final day/night spec
Rebuild the mentor workspace per the approved final design (day.png/night.png):
- mentorView gets a dedicated full-width immersive shell (independent 56px
  top bar with page title slot + theme mode text, hidden module-nav/market
  tape/overview/status bar) scoped to body[data-active-view=mentorView].
- Assistant messages become borderless body text with name/time above; only
  user messages use a blue bubble; keep loading/error/streaming caret states.
- 4 quick topics stay visible with history; composer restored to a framed
  ~94px card with bottom-left shortcut hint and bottom-right send button.
- Directory tools merged into one row (search + filter menu + sort); the
  filter menu still offers all/A/B/C; list selected state is an inset rounded
  fill; contact rows are borderless 75px items.
- Chat header always shows pin/note/profile/clear; pin reuses /api/mentors/
  preferences; theme toggle stays the single #themeToggle.
- Night tokens match the spec including the two distinct blues (#316FEF link,
  #5B8DEF quote/selected icon). Mobile (<768) stacks panels with no horizontal
  overflow; 1024+ follows the desktop spec.
- Update stale test baselines (300px sidebar, 94px composer, hint presence,
  night bubble color, centered disclaimer) and regenerate the architecture
  inventory.

Co-authored-by: multica-agent <github@multica.ai>
2026-08-18 11:54:57 +08:00

118 lines
4.2 KiB
JavaScript

const THEME_STORAGE_KEY = "xiaobaiTheme";
let activeThemeTransition = null;
let themeSwitchSequence = 0;
function syncThemeControl() {
const theme = document.documentElement.dataset.theme === "dark" ? "dark" : "light";
const button = document.querySelector("#themeToggle");
if (!button) return;
const dark = theme === "dark";
const label = dark ? "切换到日间模式" : "切换到夜间模式";
button.title = label;
button.setAttribute("aria-label", label);
button.setAttribute("aria-pressed", String(dark));
button.querySelector("i")?.setAttribute("data-lucide", dark ? "sun" : "moon");
const modeText = document.querySelector("#themeModeText");
if (modeText) modeText.textContent = dark ? "夜间模式" : "日间模式";
}
function clearThemeTransitionEffects() {
document.querySelectorAll(".row-enter, .row-pending, .view-entering").forEach((element) => {
element.classList.remove("row-enter", "row-pending", "view-entering");
element.style.removeProperty("--row-delay");
});
}
function redrawThemeSensitiveVisuals() {
if (!elements.stockPreview.hidden && state.stockPreviewPayload) {
selectStockPreviewChart(state.stockPreviewChart);
}
if (elements.stockDialog.open) {
if (state.stockDetailChartMode === "intraday" && state.stockDetailIntraday?.points?.length) {
drawIntradayCanvas(
elements.priceChart,
state.stockDetailIntraday.points,
[],
state.stockDetailIntraday.meta?.previous_close,
);
} else if (state.stockDetail?.prices) drawPriceChart(state.stockDetail.prices);
}
if (elements.entityDetailDialog.open) {
if (state.entityDetailChartMode === "intraday" && state.entityDetailIntraday?.points?.length) {
drawIntradayCanvas(
elements.entityDetailChart,
state.entityDetailIntraday.points,
[],
state.entityDetailIntraday.meta?.previous_close,
);
} else if (state.entityDetailPayload?.series) {
drawEntityDetailChart(state.entityDetailPayload.series);
}
}
if (state.activeView === "sentimentCycleView" && state.sentimentHistory) {
drawSentimentTrendChart(state.sentimentHistory.rows || []);
}
if (state.activeView === "heavenView") {
if (state.heavenPanel === "fortune" && state.heavenSetup?.field) {
renderQiFieldCanvas(state.heavenSetup.field.balance || [], { intro: false });
drawQiUseConnections(false);
}
if (state.heavenPanel === "heart") startHeartDust();
}
}
function commitTheme(normalized, persist) {
document.documentElement.dataset.theme = normalized;
document.documentElement.style.colorScheme = normalized;
if (persist) {
try {
localStorage.setItem(THEME_STORAGE_KEY, normalized);
} catch (_error) {
// The selected theme still applies for the current page when storage is unavailable.
}
}
syncThemeControl();
refreshIcons();
redrawThemeSensitiveVisuals();
}
function applyTheme(theme, persist = true) {
const normalized = theme === "dark" ? "dark" : "light";
const root = document.documentElement;
if (root.dataset.theme === normalized) {
commitTheme(normalized, persist);
return;
}
const sequence = ++themeSwitchSequence;
activeThemeTransition?.skipTransition?.();
clearThemeTransitionEffects();
root.classList.add("theme-switching");
const update = () => commitTheme(normalized, persist);
const finish = () => {
if (sequence !== themeSwitchSequence) return;
clearThemeTransitionEffects();
root.classList.remove("theme-switching");
activeThemeTransition = null;
};
const reducedMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
if (!reducedMotion && typeof document.startViewTransition === "function") {
activeThemeTransition = document.startViewTransition(update);
activeThemeTransition.finished.then(finish, finish);
return;
}
update();
requestAnimationFrame(() => requestAnimationFrame(finish));
}
function toggleTheme() {
applyTheme(document.documentElement.dataset.theme === "dark" ? "light" : "dark");
}
function bindThemeEvents() {
document.querySelector("#themeToggle").addEventListener("click", toggleTheme);
window.addEventListener("resize", redrawThemeSensitiveVisuals);
}