Merge pull request 'feat(ui): dark navy sidebar + restrained kanban status colors' (#23) from feature/ui-rework-sidebar-kanban into main
This commit was merged in pull request #23.
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user