fix: dark theme

This commit is contained in:
Tautvydas Derzinskas committed 2026-09-12 02:16:31 +02:00
1 parent 7c411c9db9
commit c40639fe6e
6 files changed
+123 -73

No files matched your search

@@ -1,6 +1,7 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link as RouterLink, useLocation, useNavigate } from "react-router-dom";
import type { Theme } from "@mui/material/styles";
import Box from "@mui/material/Box";
import Link from "@mui/material/Link";
import Stack from "@mui/material/Stack";
@@ -32,6 +33,24 @@ const SIDEBAR_WIDTH = 240;
const SIDEBAR_COLLAPSED_WIDTH = 72;
const SIDEBAR_COLLAPSED_STORAGE_KEY = "thingport_sidebar_collapsed";
/** Shared color/background logic for every nav row (both the expanded ListItemButton and the
* collapsed icon-only variant below) -- selected rows get the theme's nav-selected background
* (a flat tint in light mode, a left-to-right gradient in dark) and text/icon color, unselected
* ones get the theme's dedicated (narrower-than-text.secondary) inactive nav color. */
function navRowSx(selected: boolean) {
const color = (theme: Theme) => (selected ? theme.thingport.selectedNavText : theme.thingport.navInactiveText);
return {
color,
"& .MuiListItemIcon-root": { color },
...(selected && {
background: (theme: Theme) => theme.thingport.selectedNavBackground,
"&.Mui-selected, &.Mui-selected:hover": {
background: (theme: Theme) => theme.thingport.selectedNavBackground,
},
}),
};
}
/** Icon-only rail row used for every nav item once the sidebar is collapsed -- a tooltip stands
* in for the label. */
function CollapsedNavIcon({ icon, label, selected, onClick }: {
@@ -45,9 +64,9 @@ function CollapsedNavIcon({ icon, label, selected, onClick }: {
<ListItemButton
selected={selected}
onClick={onClick}
sx={{ borderRadius: 1, mb: 0.5, justifyContent: "center", px: 0 }}
sx={{ borderRadius: 1, mb: 0.5, justifyContent: "center", px: 0, ...navRowSx(selected) }}
>
<ListItemIcon sx={{ minWidth: 0, color: selected ? "primary.main" : "text.secondary" }}>
<ListItemIcon sx={{ minWidth: 0 }}>
{icon}
</ListItemIcon>
</ListItemButton>
@@ -116,8 +135,6 @@ export default function Sidebar({ isAdmin, onSelectCategory }: Props) {
top: 0,
display: "flex",
flexDirection: "column",
borderRight: "1px solid",
borderColor: "divider",
bgcolor: "background.paper",
overflow: "hidden",
transition: (theme) => theme.transitions.create("width", { duration: theme.transitions.duration.shortest }),
@@ -156,7 +173,7 @@ export default function Sidebar({ isAdmin, onSelectCategory }: Props) {
onClick={() => navigate("/")}
/>
) : (
<ListItemButton selected={onDashboard} onClick={() => navigate("/")} sx={{ borderRadius: 1, mb: 0.5 }}>
<ListItemButton selected={onDashboard} onClick={() => navigate("/")} sx={{ borderRadius: 1, mb: 0.5, ...navRowSx(onDashboard) }}>
<ListItemIcon sx={{ minWidth: 30 }}>
<SpaceDashboardIcon fontSize="small" />
</ListItemIcon>
@@ -175,7 +192,7 @@ export default function Sidebar({ isAdmin, onSelectCategory }: Props) {
<ListItemButton
selected={onModels}
onClick={goToModelsRoot}
sx={{ borderRadius: 1, mb: 0.5 }}
sx={{ borderRadius: 1, mb: 0.5, ...navRowSx(onModels) }}
>
<ListItemIcon sx={{ minWidth: 30 }}>
<ViewInArIcon fontSize="small" />
@@ -195,7 +212,7 @@ export default function Sidebar({ isAdmin, onSelectCategory }: Props) {
<ListItemButton
selected={onCollections}
onClick={() => navigate("/models/collections")}
sx={{ borderRadius: 1, mb: 0.5 }}
sx={{ borderRadius: 1, mb: 0.5, ...navRowSx(onCollections) }}
>
<ListItemIcon sx={{ minWidth: 30 }}>
<CollectionsIcon fontSize="small" />
@@ -215,7 +232,7 @@ export default function Sidebar({ isAdmin, onSelectCategory }: Props) {
<ListItemButton
selected={onDownload}
onClick={() => navigate("/downloads")}
sx={{ borderRadius: 1, mb: 0.5 }}
sx={{ borderRadius: 1, mb: 0.5, ...navRowSx(onDownload) }}
>
<ListItemIcon sx={{ minWidth: 30 }}>
<DownloadIcon fontSize="small" />
@@ -265,7 +282,7 @@ export default function Sidebar({ isAdmin, onSelectCategory }: Props) {
<ListItemButton
selected={false}
onClick={() => setAdminExpanded(v => !v)}
sx={{ borderRadius: 1, mb: 0.5 }}
sx={{ borderRadius: 1, mb: 0.5, ...navRowSx(onAdmin) }}
>
<ListItemIcon sx={{ minWidth: 30 }}>
<AdminPanelSettingsIcon fontSize="small" />
@@ -278,7 +295,7 @@ export default function Sidebar({ isAdmin, onSelectCategory }: Props) {
<ListItemButton
selected={onAdminSettings}
onClick={() => navigate("/admin-settings")}
sx={{ borderRadius: 1, mb: 0.5, pl: 4 }}
sx={{ borderRadius: 1, mb: 0.5, pl: 4, ...navRowSx(onAdminSettings) }}
>
<ListItemIcon sx={{ minWidth: 30 }}>
<SettingsIcon fontSize="small" />
@@ -288,7 +305,7 @@ export default function Sidebar({ isAdmin, onSelectCategory }: Props) {
<ListItemButton
selected={onAdminUsers}
onClick={() => navigate("/admin-users")}
sx={{ borderRadius: 1, mb: 0.5, pl: 4 }}
sx={{ borderRadius: 1, mb: 0.5, pl: 4, ...navRowSx(onAdminUsers) }}
>
<ListItemIcon sx={{ minWidth: 30 }}>
<PeopleIcon fontSize="small" />
@@ -298,7 +315,7 @@ export default function Sidebar({ isAdmin, onSelectCategory }: Props) {
<ListItemButton
selected={onAdminLogs}
onClick={() => navigate("/admin-logs")}
sx={{ borderRadius: 1, mb: 0.5, pl: 4 }}
sx={{ borderRadius: 1, mb: 0.5, pl: 4, ...navRowSx(onAdminLogs) }}
>
<ListItemIcon sx={{ minWidth: 30 }}>
<HistoryIcon fontSize="small" />
@@ -308,7 +325,7 @@ export default function Sidebar({ isAdmin, onSelectCategory }: Props) {
<ListItemButton
selected={onAdminTriggers}
onClick={() => navigate("/admin-triggers")}
sx={{ borderRadius: 1, mb: 0.5, pl: 4 }}
sx={{ borderRadius: 1, mb: 0.5, pl: 4, ...navRowSx(onAdminTriggers) }}
>
<ListItemIcon sx={{ minWidth: 30 }}>
<BoltIcon fontSize="small" />
@@ -318,7 +335,7 @@ export default function Sidebar({ isAdmin, onSelectCategory }: Props) {
<ListItemButton
selected={onAdminConnections}
onClick={() => navigate("/admin-connections")}
sx={{ borderRadius: 1, mb: 0.5, pl: 4 }}
sx={{ borderRadius: 1, mb: 0.5, pl: 4, ...navRowSx(onAdminConnections) }}
>
<ListItemIcon sx={{ minWidth: 30 }}>
<CableIcon fontSize="small" />
@@ -88,7 +88,7 @@ export default function CollectionCard({ collection, theme, previewMode, onUpdat
overflow: "hidden",
borderRadius: "12px",
borderColor: "divider",
bgcolor: (muiTheme) => (muiTheme.palette.mode === "dark" ? muiTheme.palette.grey[800] : muiTheme.palette.grey[100]),
bgcolor: (muiTheme) => (muiTheme.palette.mode === "dark" ? muiTheme.thingport.pageBackground : muiTheme.palette.grey[100]),
"&:hover .collection-card-actions": { opacity: 1 },
}}
>
@@ -96,7 +96,7 @@ export default function CollectionCard({ collection, theme, previewMode, onUpdat
sx={{
width: "100%",
aspectRatio: "4 / 3",
bgcolor: (muiTheme) => (muiTheme.palette.mode === "dark" ? muiTheme.palette.grey[900] : muiTheme.palette.grey[200]),
bgcolor: (muiTheme) => (muiTheme.palette.mode === "dark" ? muiTheme.thingport.pageBackground : muiTheme.palette.grey[200]),
}}
>
{coverItems.length === 0 && (
@@ -60,7 +60,7 @@ export default function ModelSidePanel({ print, onSelectCategory, onUnauthorized
return (
<Paper
variant="outlined"
elevation={0}
sx={{
p: 2.5,
borderRadius: "12px",
@@ -1,5 +1,6 @@
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import type { Theme } from "@mui/material/styles";
import Paper from "@mui/material/Paper";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
@@ -28,6 +29,31 @@ type Props = {
onUpdateMeta: (id: string, meta: CategoryMetaInput) => Promise<void>;
};
// Every row (the pinned "All" row, a root category, or one of its children) is either the active
// filter (the theme's nav-selected gradient + accent text) or not (transparent, the theme's
// dedicated nav-inactive gray) -- a plain two-state system, rather than the extra "expanded but
// not the active one" light-grey/dark-grey tiers this used to hardcode, which were literal
// light-theme hex values (#ffffff, #5c5c5c, #212b36, #a3a3a3, and a translucent near-white row
// background) that read as invisible or wrong against a dark background.
function rowSx(active: boolean) {
return {
borderRadius: 1.5,
mb: 0.25,
background: (theme: Theme) => (active ? theme.thingport.selectedNavBackground : "transparent"),
"&:hover": { bgcolor: "action.hover" },
};
}
function rowTextSx(active: boolean, extra?: object) {
return {
noWrap: true,
variant: "body2" as const,
sx: {
color: (theme: Theme) => (active ? theme.thingport.selectedNavText : theme.thingport.navInactiveText),
...extra,
},
};
}
/** The Models page's own category browser: a pinned "All" row, then a strictly two-level tree --
* top-level categories expand to reveal their subcategories AND select themselves, filtering the
* grid to every model under any of their subcategories; subcategories narrow the filter down to
@@ -71,7 +97,7 @@ export default function CategoriesPanel({ categories, loading, selectedId, onSel
return (
<>
<Paper
variant="outlined"
elevation={0}
sx={{
width: 260,
flexShrink: 0,
@@ -91,16 +117,11 @@ export default function CategoriesPanel({ categories, loading, selectedId, onSel
</Stack>
<List disablePadding>
<ListItemButton
onClick={() => onSelect(null)}
sx={{
borderRadius: 1.5,
mb: 0.25,
bgcolor: selectedId === null ? "action.selected" : "transparent",
"&:hover": { bgcolor: "background.default" },
}}
>
<ListItemText primary={t("models:categories.all")} primaryTypographyProps={{ variant: "body2", fontWeight: 600 }} />
<ListItemButton onClick={() => onSelect(null)} sx={rowSx(selectedId === null)}>
<ListItemText
primary={t("models:categories.all")}
primaryTypographyProps={rowTextSx(selectedId === null, { fontWeight: 600 })}
/>
</ListItemButton>
{loading && (
@@ -115,23 +136,10 @@ export default function CategoriesPanel({ categories, loading, selectedId, onSel
const isRootActive = isOpen && selectedId === root.id;
return (
<Stack key={root.id}>
<ListItemButton
onClick={() => handleRootClick(root.id)}
sx={{
borderRadius: 1.5,
mb: 0.25,
bgcolor: isOpen ? (isRootActive ? "rgba(0, 174, 66, 0.08)" : "#ffffff") : "transparent",
"&:hover": { bgcolor: isRootActive ? "rgba(0, 174, 66, 0.08)" : "background.default" },
}}
>
<ListItemButton onClick={() => handleRootClick(root.id)} sx={rowSx(isRootActive)}>
<ListItemText
primary={displayName(root) || untitledLabel}
primaryTypographyProps={{
noWrap: true,
variant: "body2",
fontWeight: 600,
sx: { color: isOpen ? (isRootActive ? "primary.main" : "#5c5c5c") : "#212b36" },
}}
primaryTypographyProps={rowTextSx(isRootActive, { fontWeight: 600 })}
/>
<ChevronRightIcon
fontSize="small"
@@ -140,7 +148,7 @@ export default function CategoriesPanel({ categories, loading, selectedId, onSel
flexShrink: 0,
transform: isOpen ? "rotate(90deg)" : "none",
transition: "transform 0.15s",
color: isOpen ? (isRootActive ? "primary.main" : "#a3a3a3") : "#a3a3a3",
color: (theme) => (isRootActive ? theme.thingport.selectedNavText : theme.thingport.navInactiveText),
}}
/>
</ListItemButton>
@@ -152,21 +160,11 @@ export default function CategoriesPanel({ categories, loading, selectedId, onSel
<ListItemButton
key={child.id}
onClick={() => onSelect(child.id)}
sx={{
pl: 4,
borderRadius: 1.5,
mb: 0.25,
bgcolor: "rgba(242, 242, 242, 0.3)",
"&:hover": { bgcolor: "background.default" },
}}
sx={{ pl: 4, ...rowSx(isChildSelected) }}
>
<ListItemText
primary={displayName(child) || untitledLabel}
primaryTypographyProps={{
noWrap: true,
variant: "body2",
sx: isChildSelected ? { color: "primary.main", fontWeight: 700 } : undefined,
}}
primaryTypographyProps={rowTextSx(isChildSelected, isChildSelected ? { fontWeight: 700 } : undefined)}
/>
</ListItemButton>
);
+2 -2
View File
@@ -100,7 +100,7 @@ export default function ModelCard({
overflow: "hidden",
borderRadius: "12px",
borderColor: "transparent",
bgcolor: (muiTheme) => (muiTheme.palette.mode === "dark" ? muiTheme.palette.grey[800] : muiTheme.palette.grey[100]),
bgcolor: (muiTheme) => (muiTheme.palette.mode === "dark" ? muiTheme.thingport.pageBackground : muiTheme.palette.grey[100]),
transition: "background-color .15s ease, box-shadow .15s ease, transform .15s ease",
"&:hover": {
bgcolor: "background.paper",
@@ -115,7 +115,7 @@ export default function ModelCard({
sx={{
width: "100%",
aspectRatio: "4 / 3",
bgcolor: (muiTheme) => (muiTheme.palette.mode === "dark" ? muiTheme.palette.grey[900] : muiTheme.palette.grey[200]),
bgcolor: (muiTheme) => (muiTheme.palette.mode === "dark" ? muiTheme.thingport.pageBackground : muiTheme.palette.grey[200]),
}}
>
{renderPreviewContent(item, "card", theme, t, previewMode)}
+50 -15
View File
@@ -1,4 +1,4 @@
import { alpha, createTheme, darken, lighten, type Theme, type ThemeOptions } from "@mui/material/styles";
import { alpha, createTheme, darken, type Theme, type ThemeOptions } from "@mui/material/styles";
import type { ResolvedTheme } from "./constants/settingsOptions";
export type { ResolvedTheme };
@@ -26,6 +26,17 @@ declare module "@mui/material/styles" {
* (e.g. an attention/alert-style label) -- not one of MUI's error/warning palette
* roles, just a second brand-adjacent color for cases that call for it. */
altText: string;
/** Unselected nav-row label/icon color for the Sidebar rail and the Models page's
* Categories box -- narrower than text.secondary (which is used all over for ordinary
* muted text) so recoloring nav rows can't leak into unrelated UI. */
navInactiveText: string;
/** Selected nav row background (Sidebar rail rows, Categories box rows) -- a `background`
* (not `bgcolor`) value since dark mode's is a left-to-right gradient, not a flat color;
* light mode's is still just a flat tint expressed the same way. */
selectedNavBackground: string;
/** Selected nav row label/icon color -- the brand accent in dark mode, unchanged
* (inherited) text color in light mode. */
selectedNavText: string;
};
}
interface ThemeOptions {
@@ -34,6 +45,9 @@ declare module "@mui/material/styles" {
modelColor: string;
modelEmissive: string;
altText: string;
navInactiveText: string;
selectedNavBackground: string;
selectedNavText: string;
};
}
}
@@ -56,6 +70,9 @@ type ThemeDef = {
altText: string;
modelColor: string;
modelEmissive: string;
navInactiveText: string;
selectedNavBackground: string;
selectedNavText: string;
};
// Values ported 1:1 from the original CSS custom properties so the visual identity of each
@@ -79,25 +96,32 @@ const THEME_DEFS: Record<ResolvedTheme, ThemeDef> = {
altText: "rgb(255, 114, 32)",
modelColor: "#cbd5e1",
modelEmissive: "#94a3b8",
navInactiveText: "#858585",
selectedNavBackground: alpha("#5be584", 0.2),
// Matches the pre-existing collapsed-rail icon treatment (selected -> primary.main).
selectedNavText: "#00b800",
},
dark: {
mode: "dark",
pageBackground: "#0a0b0c",
panel: "rgba(15, 23, 42, 0.86)",
panelStrong: "#0f172a",
border: "#1f2937",
borderStrong: "#334155",
text: "#f8fafc",
textMuted: "#94a3b8",
textSubtle: "#64748b",
accent: "#34d399",
accentLight: lighten("#34d399", 0.25),
accentStrong: "#10b981",
accentSoft: "rgba(16, 185, 129, 0.2)",
pageBackground: "#181f39",
panel: "#1e2746",
panelStrong: "#1e2746",
border: "#333a54",
borderStrong: "#333a54",
text: "#828690",
textMuted: "#828690",
textSubtle: "#828690",
accent: "#00b800",
accentLight: "#5be584",
accentStrong: darken("#00b800", 0.15),
accentSoft: alpha("#00b800", 0.16),
accentContrast: "#ffffff",
altText: "rgb(255, 114, 32)",
modelColor: "#e2e8f0",
modelEmissive: "#475569",
navInactiveText: "#969ba0",
selectedNavBackground: "linear-gradient(to right, rgb(24, 31, 57) 0%, rgba(49, 206, 255, 0) 100%)",
selectedNavText: "#00b800",
},
};
@@ -106,7 +130,7 @@ export function buildTheme(id: ResolvedTheme): Theme {
const options: ThemeOptions = {
palette: {
mode: d.mode,
background: { default: d.mode === "light" ? d.pageBackground : d.panelStrong, paper: d.panel },
background: { default: d.pageBackground, paper: d.panel },
primary: { main: d.accent, light: d.accentLight, dark: d.accentStrong, contrastText: d.accentContrast },
text: { primary: d.text, secondary: d.textMuted },
divider: d.border,
@@ -115,7 +139,7 @@ export function buildTheme(id: ResolvedTheme): Theme {
hover: alpha(d.accent, 0.08),
},
},
shape: { borderRadius: 10 },
shape: { borderRadius: d.mode === "dark" ? 0 : 10 },
typography: {
fontSize: 13,
fontFamily: BODY_FONT_STACK,
@@ -130,12 +154,23 @@ export function buildTheme(id: ResolvedTheme): Theme {
MuiButton: { styleOverrides: { root: { borderRadius: 8 } } },
MuiChip: { styleOverrides: { root: { borderRadius: 6 } } },
MuiTooltip: { styleOverrides: { tooltip: { backgroundColor: d.panelStrong, color: d.text } } },
// Dark mode is deliberately square everywhere -- corners, chips, avatars, the back-to-top
// FAB, all of it. `sx`-set radii (the vast majority of them: cards, panels, the info box,
// etc.) beat theme.shape.borderRadius and component styleOverrides alike, so nothing short
// of an !important global rule reaches all of them; MuiCssBaseline's styleOverrides is
// exactly the escape hatch for page-level CSS the theme needs to own like this.
...(d.mode === "dark"
? { MuiCssBaseline: { styleOverrides: "*, *::before, *::after { border-radius: 0 !important; }" } }
: {}),
},
thingport: {
pageBackground: d.pageBackground,
modelColor: d.modelColor,
modelEmissive: d.modelEmissive,
altText: d.altText,
navInactiveText: d.navInactiveText,
selectedNavBackground: d.selectedNavBackground,
selectedNavText: d.selectedNavText,
},
};
return createTheme(options);