From 4fbce2de223761ad607aad260ff32f9eea0b1fef Mon Sep 17 00:00:00 2001 From: Dan-ex-hub Date: Sun, 19 Jul 2026 13:35:40 +0530 Subject: [PATCH 1/3] fix(icons): fall back to Stepfun Mono when Color component is absent in @lobehub/icons v5.13 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @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. --- src/shared/components/lobeProviderIcons.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/components/lobeProviderIcons.ts b/src/shared/components/lobeProviderIcons.ts index 1da9c26e5b7a..435450ca3c2b 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 }, From 0b33f5576d96c4bb341d62e20752565108b5af3c Mon Sep 17 00:00:00 2001 From: Dan-ex-hub Date: Sun, 19 Jul 2026 13:45:13 +0530 Subject: [PATCH 2/3] 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. --- src/shared/components/layouts/DashboardLayout.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/shared/components/layouts/DashboardLayout.tsx b/src/shared/components/layouts/DashboardLayout.tsx index 505f5279c021..1016e7daa147 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 && From b942be5430040bc9a9e1f48d462068fa3038bdca Mon Sep 17 00:00:00 2001 From: Dan-ex-hub Date: Sun, 19 Jul 2026 12:10:45 -0300 Subject: [PATCH 3/3] 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 --- ...allback-and-sidebar-hydration-7743.test.ts | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 tests/unit/stepfun-mono-fallback-and-sidebar-hydration-7743.test.ts 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 000000000000..9c766c83ca2a --- /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\)/); +});