diff --git a/frontend/src/components/Layout/Sidebar/index.tsx b/frontend/src/components/Layout/Sidebar/index.tsx
index 49a0817..96b3505 100644
--- a/frontend/src/components/Layout/Sidebar/index.tsx
+++ b/frontend/src/components/Layout/Sidebar/index.tsx
@@ -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 }: {
-
+
{icon}
@@ -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("/")}
/>
) : (
- navigate("/")} sx={{ borderRadius: 1, mb: 0.5 }}>
+ navigate("/")} sx={{ borderRadius: 1, mb: 0.5, ...navRowSx(onDashboard) }}>
@@ -175,7 +192,7 @@ export default function Sidebar({ isAdmin, onSelectCategory }: Props) {
@@ -195,7 +212,7 @@ export default function Sidebar({ isAdmin, onSelectCategory }: Props) {
navigate("/models/collections")}
- sx={{ borderRadius: 1, mb: 0.5 }}
+ sx={{ borderRadius: 1, mb: 0.5, ...navRowSx(onCollections) }}
>
@@ -215,7 +232,7 @@ export default function Sidebar({ isAdmin, onSelectCategory }: Props) {
navigate("/downloads")}
- sx={{ borderRadius: 1, mb: 0.5 }}
+ sx={{ borderRadius: 1, mb: 0.5, ...navRowSx(onDownload) }}
>
@@ -265,7 +282,7 @@ export default function Sidebar({ isAdmin, onSelectCategory }: Props) {
setAdminExpanded(v => !v)}
- sx={{ borderRadius: 1, mb: 0.5 }}
+ sx={{ borderRadius: 1, mb: 0.5, ...navRowSx(onAdmin) }}
>
@@ -278,7 +295,7 @@ export default function Sidebar({ isAdmin, onSelectCategory }: Props) {
navigate("/admin-settings")}
- sx={{ borderRadius: 1, mb: 0.5, pl: 4 }}
+ sx={{ borderRadius: 1, mb: 0.5, pl: 4, ...navRowSx(onAdminSettings) }}
>
@@ -288,7 +305,7 @@ export default function Sidebar({ isAdmin, onSelectCategory }: Props) {
navigate("/admin-users")}
- sx={{ borderRadius: 1, mb: 0.5, pl: 4 }}
+ sx={{ borderRadius: 1, mb: 0.5, pl: 4, ...navRowSx(onAdminUsers) }}
>
@@ -298,7 +315,7 @@ export default function Sidebar({ isAdmin, onSelectCategory }: Props) {
navigate("/admin-logs")}
- sx={{ borderRadius: 1, mb: 0.5, pl: 4 }}
+ sx={{ borderRadius: 1, mb: 0.5, pl: 4, ...navRowSx(onAdminLogs) }}
>
@@ -308,7 +325,7 @@ export default function Sidebar({ isAdmin, onSelectCategory }: Props) {
navigate("/admin-triggers")}
- sx={{ borderRadius: 1, mb: 0.5, pl: 4 }}
+ sx={{ borderRadius: 1, mb: 0.5, pl: 4, ...navRowSx(onAdminTriggers) }}
>
@@ -318,7 +335,7 @@ export default function Sidebar({ isAdmin, onSelectCategory }: Props) {
navigate("/admin-connections")}
- sx={{ borderRadius: 1, mb: 0.5, pl: 4 }}
+ sx={{ borderRadius: 1, mb: 0.5, pl: 4, ...navRowSx(onAdminConnections) }}
>
diff --git a/frontend/src/pages/CollectionsPage/CollectionCard.tsx b/frontend/src/pages/CollectionsPage/CollectionCard.tsx
index f5f2943..f73c21b 100644
--- a/frontend/src/pages/CollectionsPage/CollectionCard.tsx
+++ b/frontend/src/pages/CollectionsPage/CollectionCard.tsx
@@ -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 && (
diff --git a/frontend/src/pages/ModelDetailPage/ModelSidePanel.tsx b/frontend/src/pages/ModelDetailPage/ModelSidePanel.tsx
index 86bea89..1a5fd56 100644
--- a/frontend/src/pages/ModelDetailPage/ModelSidePanel.tsx
+++ b/frontend/src/pages/ModelDetailPage/ModelSidePanel.tsx
@@ -60,7 +60,7 @@ export default function ModelSidePanel({ print, onSelectCategory, onUnauthorized
return (
Promise;
};
+// 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 (
<>
- onSelect(null)}
- sx={{
- borderRadius: 1.5,
- mb: 0.25,
- bgcolor: selectedId === null ? "action.selected" : "transparent",
- "&:hover": { bgcolor: "background.default" },
- }}
- >
-
+ onSelect(null)} sx={rowSx(selectedId === null)}>
+
{loading && (
@@ -115,23 +136,10 @@ export default function CategoriesPanel({ categories, loading, selectedId, onSel
const isRootActive = isOpen && selectedId === root.id;
return (
- 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" },
- }}
- >
+ handleRootClick(root.id)} sx={rowSx(isRootActive)}>
(isRootActive ? theme.thingport.selectedNavText : theme.thingport.navInactiveText),
}}
/>
@@ -152,21 +160,11 @@ export default function CategoriesPanel({ categories, loading, selectedId, onSel
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) }}
>
);
diff --git a/frontend/src/pages/ModelsPage/ModelCard.tsx b/frontend/src/pages/ModelsPage/ModelCard.tsx
index 6708cf3..68c50cf 100644
--- a/frontend/src/pages/ModelsPage/ModelCard.tsx
+++ b/frontend/src/pages/ModelsPage/ModelCard.tsx
@@ -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)}
diff --git a/frontend/src/theme.ts b/frontend/src/theme.ts
index ee9d18c..1ec0932 100644
--- a/frontend/src/theme.ts
+++ b/frontend/src/theme.ts
@@ -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 = {
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);