diff --git a/src/shared/components/layouts/DashboardLayout.tsx b/src/shared/components/layouts/DashboardLayout.tsx index 505f5279c0..1016e7daa1 100644 --- a/src/shared/components/layouts/DashboardLayout.tsx +++ b/src/shared/components/layouts/DashboardLayout.tsx @@ -21,14 +21,15 @@ export default function DashboardLayout({ children }) { const [sidebarOpen, setSidebarOpen] = useState(false); const [commandPaletteOpen, setCommandPaletteOpen] = useState(false); const isElectron = useIsElectron(); - const [collapsed, setCollapsed] = useState(() => { - if (typeof globalThis.window === "undefined") return false; + const [collapsed, setCollapsed] = useState(false); + + useEffect(() => { try { - return localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === "true"; - } catch { - return false; - } - }); + if (localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === "true") { + setTimeout(() => setCollapsed(true), 0); + } + } catch {} + }, []); const isMacElectron = isElectron && diff --git a/src/shared/components/lobeProviderIcons.ts b/src/shared/components/lobeProviderIcons.ts index 1da9c26e5b..435450ca3c 100644 --- a/src/shared/components/lobeProviderIcons.ts +++ b/src/shared/components/lobeProviderIcons.ts @@ -140,7 +140,7 @@ import SenseNovaColorIcon from "@lobehub/icons/es/SenseNova/components/Color"; import SenseNovaMonoIcon from "@lobehub/icons/es/SenseNova/components/Mono"; import StabilityColorIcon from "@lobehub/icons/es/Stability/components/Color"; import StabilityMonoIcon from "@lobehub/icons/es/Stability/components/Mono"; -import StepfunColorIcon from "@lobehub/icons/es/Stepfun/components/Color"; +// Stepfun has no Color component in the installed @lobehub/icons version; use Mono as fallback import StepfunMonoIcon from "@lobehub/icons/es/Stepfun/components/Mono"; import SunoMonoIcon from "@lobehub/icons/es/Suno/components/Mono"; import TavilyColorIcon from "@lobehub/icons/es/Tavily/components/Color"; @@ -281,7 +281,7 @@ const LOBE_ICON_COMPONENTS = { SenseNova: { mono: SenseNovaMonoIcon, color: SenseNovaColorIcon }, Snowflake: { mono: SnowflakeMonoIcon, color: SnowflakeColorIcon }, Stability: { mono: StabilityMonoIcon, color: StabilityColorIcon }, - Stepfun: { mono: StepfunMonoIcon, color: StepfunColorIcon }, + Stepfun: { mono: StepfunMonoIcon, color: StepfunMonoIcon }, Suno: { mono: SunoMonoIcon }, Tavily: { mono: TavilyMonoIcon, color: TavilyColorIcon }, Tencent: { mono: TencentMonoIcon, color: TencentColorIcon }, diff --git a/tests/unit/stepfun-mono-fallback-and-sidebar-hydration-7743.test.ts b/tests/unit/stepfun-mono-fallback-and-sidebar-hydration-7743.test.ts new file mode 100644 index 0000000000..9c766c83ca --- /dev/null +++ b/tests/unit/stepfun-mono-fallback-and-sidebar-hydration-7743.test.ts @@ -0,0 +1,56 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; + +// Regression guards for PR #7743: +// 1) @lobehub/icons v5.13+ dropped the Stepfun `Color` sub-component. Importing it +// causes a build-time module-not-found error that breaks the whole dashboard. +// Lock in the Mono fallback so a future bump can't silently re-import it. +// 2) DashboardLayout must not read localStorage synchronously during the initial +// `useState` render — that produces a client/server markup mismatch (hydration +// error) because the server always renders the `false` (collapsed=false) branch. +// The fix defers the localStorage read to a `useEffect`. + +const lobeProviderIconsSrc = fs.readFileSync( + new URL("../../src/shared/components/lobeProviderIcons.ts", import.meta.url), + "utf8" +); + +const dashboardLayoutSrc = fs.readFileSync( + new URL("../../src/shared/components/layouts/DashboardLayout.tsx", import.meta.url), + "utf8" +); + +test("lobeProviderIcons never imports the removed Stepfun Color sub-component", () => { + assert.doesNotMatch( + lobeProviderIconsSrc, + /@lobehub\/icons\/es\/Stepfun\/components\/Color/, + "Stepfun/components/Color does not exist in @lobehub/icons v5.13+ and must not be imported" + ); +}); + +test("lobeProviderIcons maps both Stepfun mono and color slots to StepfunMonoIcon", () => { + const stepfunEntry = lobeProviderIconsSrc.match(/Stepfun:\s*{\s*mono:\s*(\w+),\s*color:\s*(\w+)\s*}/); + assert.ok(stepfunEntry, "Stepfun entry must exist in LOBE_ICON_COMPONENTS"); + const [, mono, color] = stepfunEntry; + assert.equal(mono, "StepfunMonoIcon"); + assert.equal(color, "StepfunMonoIcon", "color slot must fall back to the Mono icon"); +}); + +test("DashboardLayout does not read localStorage synchronously inside the collapsed useState initializer", () => { + const collapsedStateMatch = dashboardLayoutSrc.match(/const \[collapsed, setCollapsed\] = useState\(([^)]*)\)/); + assert.ok(collapsedStateMatch, "collapsed useState declaration must exist"); + assert.equal( + collapsedStateMatch[1].trim(), + "false", + "collapsed must initialize to a constant so server and first client render match (no hydration mismatch)" + ); +}); + +test("DashboardLayout defers the sidebar-collapsed localStorage read to a useEffect", () => { + const effectIndex = dashboardLayoutSrc.indexOf("useEffect(() => {"); + assert.ok(effectIndex >= 0, "a useEffect must exist"); + const effectBody = dashboardLayoutSrc.slice(effectIndex, dashboardLayoutSrc.indexOf("}, []);", effectIndex)); + assert.match(effectBody, /localStorage\.getItem\(SIDEBAR_COLLAPSED_KEY\)/); + assert.match(effectBody, /setCollapsed\(true\)/); +});