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);