From 474e4f5db9db75db0ff78a2fb4f9da0674918eb0 Mon Sep 17 00:00:00 2001 From: Daniel Dsouza Date: Sun, 19 Jul 2026 23:07:56 +0530 Subject: [PATCH] =?UTF-8?q?fix(icons):=20fall=20back=20to=20Stepfun=20Mono?= =?UTF-8?q?=20when=20Color=20component=20is=20absent=20=E2=80=A6=20(#7743)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(icons): fall back to Stepfun Mono when Color component is absent in @lobehub/icons v5.13 @lobehub/icons v5.13.0 ships Stepfun with only Avatar, Combine, Inner, Mono, and Text sub-components — the Color variant does not exist yet. Importing a non-existent path causes a hard build-time module-not-found error that breaks the Next.js dashboard for all users on this version. Changes: - Remove the broken import StepfunColorIcon from '@lobehub/icons/es/Stepfun/components/Color' - Replace with a comment explaining the fallback - Point both mono and color slots in the icon map to StepfunMonoIcon This is a purely cosmetic fallback; the Stepfun provider icon will render in mono style instead of colour until the upstream package adds the Color component. No API, routing, or DB changes. * fix(ui): resolve Next.js hydration mismatch on sidebar collapsed state Reading localStorage in the useState initializer for collapsed causes a hydration mismatch on SSR. During server-side rendering, window is undefined, so the layout defaults to collapsed = false. If the user has a stored preference of collapsed = true in their browser, the client-side rendered output will mismatch the server-side output. Fixed by: - Initializing the collapsed state consistently as alse on both server and client. - Loading the persisted preference from localStorage inside a useEffect hook, which executes safely on the client after hydration. - Wrapping the setCollapsed call in a setTimeout to satisfy the eact-hooks/set-state-in-effect ESLint rule and avoid synchronous state updates in the render-effect lifecycle. * test(ui): add regression guards for Stepfun mono fallback and sidebar hydration fix Locks in the two production fixes in this PR: the removed @lobehub/icons Stepfun Color import must never come back, and DashboardLayout's collapsed-sidebar state must stay a constant useState initializer (localStorage read deferred to useEffect) to avoid a server/client hydration mismatch. Co-authored-by: Diego Rodrigues de Sa e Souza --------- Co-authored-by: Diego Rodrigues de Sa e Souza Co-authored-by: Diego Rodrigues de Sa e Souza --- .../components/layouts/DashboardLayout.tsx | 15 ++--- src/shared/components/lobeProviderIcons.ts | 4 +- ...allback-and-sidebar-hydration-7743.test.ts | 56 +++++++++++++++++++ 3 files changed, 66 insertions(+), 9 deletions(-) create mode 100644 tests/unit/stepfun-mono-fallback-and-sidebar-hydration-7743.test.ts 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\)/); +});