fix(icons): fall back to Stepfun Mono when Color component is absent … (#7743)

* 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 <diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com>
This commit is contained in:
Daniel Dsouza
2026-07-19 23:07:56 +05:30
committed by GitHub
parent 0c9578dc1d
commit 474e4f5db9
3 changed files with 66 additions and 9 deletions

View File

@@ -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 &&

View File

@@ -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 },

View File

@@ -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\)/);
});