feat(ui): dark navy sidebar + restrained kanban status colors #23

Merged
cesnimda merged 1 commits from feature/ui-rework-sidebar-kanban into main 2026-07-12 01:53:28 +02:00
3 changed files with 66 additions and 62 deletions
Showing only changes of commit 7cfbdf504a - Show all commits
+9 -12
View File
@@ -1,21 +1,18 @@
import React from "react"; import React, { useId } from "react";
export default function JobbjaktMark(props: React.SVGProps<SVGSVGElement>) { export default function JobbjaktMark(props: React.SVGProps<SVGSVGElement>) {
const gradientId = useId();
return ( return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Job tracker" {...props}> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 34 34" role="img" aria-label="Jobbjakt" {...props}>
<defs> <defs>
<linearGradient id="briefcase-track" x1="0" x2="1" y1="0" y2="1"> <linearGradient id={gradientId} x1="0" x2="1" y1="0" y2="1">
<stop offset="0%" stopColor="#3b82f6" /> <stop offset="0%" stopColor="#6366f1" />
<stop offset="100%" stopColor="#14b8a6" /> <stop offset="100%" stopColor="#22d3ee" />
</linearGradient> </linearGradient>
</defs> </defs>
<rect x="8" y="12" width="48" height="40" rx="12" fill="#0f172a" /> <rect width="34" height="34" rx="9" fill={`url(#${gradientId})`} />
<path d="M22 20v-2c0-3.3 2.7-6 6-6h8c3.3 0 6 2.7 6 6v2" fill="none" stroke="url(#briefcase-track)" strokeWidth="4" strokeLinecap="round" /> <path d="M9 17.5l5 5 11-12" fill="none" stroke="#ffffff" strokeWidth="3.4" strokeLinecap="round" strokeLinejoin="round" />
<rect x="14" y="22" width="36" height="26" rx="8" fill="none" stroke="url(#briefcase-track)" strokeWidth="4" />
<path d="M14 31h14" stroke="url(#briefcase-track)" strokeWidth="4" strokeLinecap="round" />
<path d="M36 31h14" stroke="url(#briefcase-track)" strokeWidth="4" strokeLinecap="round" />
<circle cx="32" cy="31" r="4.5" fill="#e2e8f0" />
<path d="M24 40l5 5 11-12" fill="none" stroke="#e2e8f0" strokeWidth="4" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
); );
} }
+21 -29
View File
@@ -4,7 +4,6 @@ import {
Box, Box,
Card, Card,
CardContent, CardContent,
Chip,
IconButton, IconButton,
Menu, Menu,
MenuItem, MenuItem,
@@ -115,24 +114,21 @@ export default function KanbanBoard() {
p: 1.5, p: 1.5,
borderRadius: 3, borderRadius: 3,
minHeight: 220, minHeight: 220,
border: `1px solid ${alpha(c, theme.palette.mode === "dark" ? 0.25 : 0.18)}`, border: "1px solid",
background: alpha(c, theme.palette.mode === "dark" ? 0.10 : 0.06), borderColor: "divider",
background: theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.02) : alpha(theme.palette.text.primary, 0.015),
}} }}
> >
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1 }}> <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1.25, px: 0.25 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 800, color: theme.palette.mode === "dark" ? "#f8fafc" : "inherit" }}> <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<Box sx={{ width: 9, height: 9, borderRadius: "50%", backgroundColor: c, flexShrink: 0 }} />
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>
{statusLabel(t, status)} {statusLabel(t, status)}
</Typography> </Typography>
<Chip </Box>
size="small" <Typography variant="body2" sx={{ color: "text.secondary", fontWeight: 700 }}>
label={list.length} {list.length}
sx={{ </Typography>
fontWeight: 800,
color: alpha(c, theme.palette.mode === "dark" ? 0.95 : 0.9),
backgroundColor: alpha(c, theme.palette.mode === "dark" ? 0.18 : 0.12),
border: `1px solid ${alpha(c, theme.palette.mode === "dark" ? 0.35 : 0.22)}`,
}}
/>
</Box> </Box>
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}> <Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
@@ -144,21 +140,18 @@ export default function KanbanBoard() {
onDragEnd={() => setDragJobId(null)} onDragEnd={() => setDragJobId(null)}
sx={{ sx={{
cursor: "grab", cursor: "grab",
borderRadius: 3, borderRadius: 2.5,
border: `1px solid ${alpha(c, theme.palette.mode === "dark" ? 0.22 : 0.14)}`, borderLeft: `4px solid ${c}`,
background: theme.palette.mode === "dark" ? "rgba(15,23,42,0.82)" : "rgba(255,255,255,0.96)", boxShadow: theme.palette.mode === "dark" ? "none" : "0 1px 3px rgba(15,23,42,0.06)",
backdropFilter: "blur(8px)",
color: theme.palette.mode === "dark" ? "#e5eefc" : "#0f172a",
}} }}
> >
<CardContent sx={{ p: 1.25, "&:last-child": { pb: 1.25 } }}> <CardContent sx={{ p: 1.25, "&:last-child": { pb: 1.25 } }}>
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1 }}> <Box sx={{ display: "flex", justifyContent: "space-between", gap: 1 }}>
<Typography sx={{ fontWeight: 800, lineHeight: 1.25, color: theme.palette.mode === "dark" ? "#f8fafc" : "#0f172a" }}> <Typography sx={{ fontWeight: 800, lineHeight: 1.25 }}>
{j.company?.name ?? ""} {j.jobTitle}
</Typography> </Typography>
<IconButton <IconButton
size="small" size="small"
sx={{ color: theme.palette.mode === "dark" ? "#e2e8f0" : "#0f172a" }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setMenuJobId(j.id); setMenuJobId(j.id);
@@ -168,13 +161,12 @@ export default function KanbanBoard() {
<MoreHorizIcon fontSize="small" /> <MoreHorizIcon fontSize="small" />
</IconButton> </IconButton>
</Box> </Box>
<Typography variant="body2" sx={{ color: theme.palette.mode === "dark" ? "#cbd5e1" : "#475569" }}> <Typography variant="body2" sx={{ color: "text.secondary" }}>
{j.jobTitle} {[j.company?.name, j.location].filter(Boolean).join(" · ")}
</Typography>
<Typography variant="body2" sx={{ color: "text.secondary", mt: 0.75 }}>
{j.daysSince}d
</Typography> </Typography>
<Box sx={{ display: "flex", gap: 1, mt: 1, flexWrap: "wrap" }}>
<Chip size="small" label={`${j.daysSince}d`} sx={{ color: theme.palette.mode === "dark" ? "#e2e8f0" : "#0f172a", backgroundColor: theme.palette.mode === "dark" ? "rgba(148,163,184,0.18)" : "rgba(148,163,184,0.18)" }} />
{j.location ? <Chip size="small" label={j.location} sx={{ color: theme.palette.mode === "dark" ? "#e2e8f0" : "#0f172a", backgroundColor: theme.palette.mode === "dark" ? "rgba(148,163,184,0.18)" : "rgba(148,163,184,0.18)" }} /> : null}
</Box>
</CardContent> </CardContent>
</Card> </Card>
))} ))}
+35 -20
View File
@@ -47,6 +47,16 @@ function initialsFrom(s?: string) {
const DESKTOP_SIDEBAR_KEY = "appShellDesktopSidebarCollapsed"; const DESKTOP_SIDEBAR_KEY = "appShellDesktopSidebarCollapsed";
// The nav rail stays a fixed dark navy regardless of the app's light/dark theme toggle --
// a deliberate signature element, not derived from theme tokens.
const SIDEBAR_BG = "#0f172a";
const SIDEBAR_BORDER = "rgba(255,255,255,0.08)";
const SIDEBAR_TEXT_MUTED = "#94a3b8";
const SIDEBAR_TEXT = "#e2e8f0";
const SIDEBAR_SELECTED_BG = "rgba(99,102,241,0.18)";
const SIDEBAR_SELECTED_TEXT = "#ffffff";
const SIDEBAR_SELECTED_ICON = "#a5b4fc";
export default function AppShell({ export default function AppShell({
pageTitle, pageTitle,
breadcrumbs, breadcrumbs,
@@ -122,7 +132,7 @@ export default function AppShell({
{groups.map(([section, rows]) => ( {groups.map(([section, rows]) => (
<Box key={section || "_"} sx={{ mb: desktopNavCollapsed ? 1 : 1.25 }}> <Box key={section || "_"} sx={{ mb: desktopNavCollapsed ? 1 : 1.25 }}>
{section && !desktopNavCollapsed ? ( {section && !desktopNavCollapsed ? (
<Typography variant="caption" sx={{ px: 1.25, color: "text.secondary", fontWeight: 600, textTransform: "uppercase" }}> <Typography variant="caption" sx={{ px: 1.25, color: SIDEBAR_TEXT_MUTED, fontWeight: 600, textTransform: "uppercase" }}>
{section} {section}
</Typography> </Typography>
) : null} ) : null}
@@ -135,20 +145,25 @@ export default function AppShell({
selected={selected} selected={selected}
onClick={() => onNavigate(item.to)} onClick={() => onNavigate(item.to)}
title={desktopNavCollapsed ? item.label : undefined} title={desktopNavCollapsed ? item.label : undefined}
sx={(muiTheme: any) => ({ sx={{
borderRadius: 2, borderRadius: 2,
mb: 0.5, mb: 0.5,
minHeight: 44, minHeight: 44,
px: desktopNavCollapsed ? 1 : 1.5, px: desktopNavCollapsed ? 1 : 1.5,
justifyContent: desktopNavCollapsed ? "center" : "flex-start", justifyContent: desktopNavCollapsed ? "center" : "flex-start",
border: "1px solid transparent", border: "1px solid transparent",
color: SIDEBAR_TEXT_MUTED,
"&:hover": { backgroundColor: "rgba(255,255,255,0.06)", color: SIDEBAR_TEXT },
"&.Mui-selected": { "&.Mui-selected": {
backgroundColor: muiTheme.vars.palette.action.hover, backgroundColor: SIDEBAR_SELECTED_BG,
borderColor: muiTheme.vars.palette.divider, color: SIDEBAR_SELECTED_TEXT,
}, },
})} "&.Mui-selected:hover": {
backgroundColor: SIDEBAR_SELECTED_BG,
},
}}
> >
<ListItemIcon sx={{ minWidth: desktopNavCollapsed ? 0 : 36, justifyContent: "center" }}> <ListItemIcon sx={{ minWidth: desktopNavCollapsed ? 0 : 36, justifyContent: "center", color: selected ? SIDEBAR_SELECTED_ICON : SIDEBAR_TEXT_MUTED }}>
{item.badgeCount && item.badgeCount > 0 ? ( {item.badgeCount && item.badgeCount > 0 ? (
<Badge color="error" badgeContent={item.badgeCount > 99 ? "99+" : item.badgeCount}> <Badge color="error" badgeContent={item.badgeCount > 99 ? "99+" : item.badgeCount}>
{item.icon} {item.icon}
@@ -166,16 +181,16 @@ export default function AppShell({
); );
const drawerContent = ( const drawerContent = (
<Box sx={{ height: "100%", display: "flex", flexDirection: "column" }}> <Box sx={{ height: "100%", display: "flex", flexDirection: "column", backgroundColor: SIDEBAR_BG }}>
<Box sx={{ px: desktopNavCollapsed ? 1.5 : 2.25, py: desktopNavCollapsed ? 2 : 2.5, display: "flex", justifyContent: desktopNavCollapsed ? "center" : "flex-start" }}> <Box sx={{ px: desktopNavCollapsed ? 1.5 : 2.25, py: desktopNavCollapsed ? 2 : 2.5, display: "flex", justifyContent: desktopNavCollapsed ? "center" : "flex-start" }}>
<Box sx={{ display: "flex", alignItems: "center", gap: 1, justifyContent: desktopNavCollapsed ? "center" : "flex-start" }}> <Box sx={{ display: "flex", alignItems: "center", gap: 1, justifyContent: desktopNavCollapsed ? "center" : "flex-start" }}>
<JobbjaktMark style={{ width: 22, height: 22 }} /> <JobbjaktMark style={{ width: 30, height: 30, flexShrink: 0 }} />
{!desktopNavCollapsed ? ( {!desktopNavCollapsed ? (
<Box> <Box>
<Typography variant="h6" sx={{ fontWeight: 600 }}> <Typography variant="h6" sx={{ fontWeight: 700, color: SIDEBAR_SELECTED_TEXT }}>
Jobbjakt Jobbjakt
</Typography> </Typography>
<Typography variant="caption" sx={{ color: "text.secondary" }}> <Typography variant="caption" sx={{ color: SIDEBAR_TEXT_MUTED }}>
{t("appTagline")} {t("appTagline")}
</Typography> </Typography>
</Box> </Box>
@@ -183,13 +198,13 @@ export default function AppShell({
</Box> </Box>
</Box> </Box>
<Divider /> <Divider sx={{ borderColor: SIDEBAR_BORDER }} />
{renderNavList(grouped.top)} {renderNavList(grouped.top)}
<Box sx={{ flex: 1 }} /> <Box sx={{ flex: 1 }} />
<Divider /> <Divider sx={{ borderColor: SIDEBAR_BORDER }} />
{renderNavList(grouped.bottom)} {renderNavList(grouped.bottom)}
</Box> </Box>
@@ -406,19 +421,19 @@ export default function AppShell({
<Drawer <Drawer
variant="permanent" variant="permanent"
sx={(muiTheme: any) => ({ sx={{
display: { xs: "none", md: "block" }, display: { xs: "none", md: "block" },
width: drawerWidth, width: drawerWidth,
flexShrink: 0, flexShrink: 0,
[`& .MuiDrawer-paper`]: { [`& .MuiDrawer-paper`]: {
width: drawerWidth, width: drawerWidth,
boxSizing: "border-box", boxSizing: "border-box",
borderRight: `1px solid ${muiTheme.vars.palette.grey[300]}`, borderRight: `1px solid ${SIDEBAR_BORDER}`,
backgroundColor: muiTheme.vars.palette.background.default, backgroundColor: SIDEBAR_BG,
backgroundImage: "none", backgroundImage: "none",
boxShadow: "none", boxShadow: "none",
}, },
})} }}
open open
> >
<Toolbar sx={{ minHeight: { xs: 68, md: 76 } }} /> <Toolbar sx={{ minHeight: { xs: 68, md: 76 } }} />
@@ -430,15 +445,15 @@ export default function AppShell({
open={drawerOpen} open={drawerOpen}
onClose={() => onToggleDrawer(false)} onClose={() => onToggleDrawer(false)}
ModalProps={{ keepMounted: true }} ModalProps={{ keepMounted: true }}
sx={(muiTheme: any) => ({ sx={{
display: { xs: "block", md: "none" }, display: { xs: "block", md: "none" },
[`& .MuiDrawer-paper`]: { [`& .MuiDrawer-paper`]: {
width: drawerWidth, width: drawerWidth,
borderRight: `1px solid ${muiTheme.vars.palette.grey[300]}`, borderRight: `1px solid ${SIDEBAR_BORDER}`,
backgroundColor: muiTheme.vars.palette.background.default, backgroundColor: SIDEBAR_BG,
backgroundImage: "none", backgroundImage: "none",
}, },
})} }}
> >
{drawerContent} {drawerContent}
</Drawer> </Drawer>