Skip to content

Commit c86fa61

Browse files
committed
Smooth dashboard navigation
1 parent 9d62178 commit c86fa61

3 files changed

Lines changed: 195 additions & 10 deletions

File tree

apps/web/app/globals.css

Lines changed: 156 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@
1010
--button-text: #334155;
1111
--button-hover-bg: #eef6fb;
1212
--nav-text: #475569;
13+
--nav-bg: rgba(255, 255, 255, 0.72);
14+
--nav-border: rgba(47, 143, 216, 0.12);
1315
--nav-hover-bg: #eef6fb;
16+
--nav-active-bg: #e0f2fe;
17+
--nav-active-border: #8bd3ec;
18+
--nav-active-text: #075985;
1419
--field-bg: #ffffff;
1520
--status-bg: #f8fbff;
1621
--soft-panel: #eef6fb;
@@ -23,6 +28,11 @@
2328
--rose: #e11d48;
2429
}
2530

31+
html[data-dashboard-theme="dark"] {
32+
--background: #061423;
33+
--foreground: #d9f7ff;
34+
}
35+
2636
* {
2737
box-sizing: border-box;
2838
}
@@ -67,7 +77,12 @@ textarea {
6777
--button-text: #334155;
6878
--button-hover-bg: #eef6fb;
6979
--nav-text: #475569;
80+
--nav-bg: rgba(255, 255, 255, 0.72);
81+
--nav-border: rgba(47, 143, 216, 0.12);
7082
--nav-hover-bg: #eef6fb;
83+
--nav-active-bg: #e0f2fe;
84+
--nav-active-border: #8bd3ec;
85+
--nav-active-text: #075985;
7186
--field-bg: #ffffff;
7287
--status-bg: #f8fbff;
7388
--soft-panel: #eef6fb;
@@ -80,7 +95,8 @@ textarea {
8095
color: var(--foreground);
8196
}
8297

83-
.shell[data-theme="dark"] {
98+
.shell[data-theme="dark"],
99+
html[data-dashboard-theme="dark"] .shell {
84100
--background: #061423;
85101
--foreground: #d9f7ff;
86102
--muted: #8fb6c8;
@@ -92,7 +108,12 @@ textarea {
92108
--button-text: #d9f7ff;
93109
--button-hover-bg: #123651;
94110
--nav-text: #b7d6e4;
111+
--nav-bg: rgba(13, 42, 66, 0.58);
112+
--nav-border: rgba(142, 231, 255, 0.11);
95113
--nav-hover-bg: #102f49;
114+
--nav-active-bg: linear-gradient(135deg, #153f5c, #0d2a42);
115+
--nav-active-border: #2f8fd8;
116+
--nav-active-text: #f8f4df;
96117
--field-bg: #071a2b;
97118
--status-bg: #071a2b;
98119
--soft-panel: #102f49;
@@ -106,6 +127,9 @@ textarea {
106127
border-right: 1px solid var(--line);
107128
background: var(--sidebar);
108129
padding: 20px 16px;
130+
transition:
131+
background-color 180ms ease,
132+
border-color 180ms ease;
109133
}
110134

111135
.sidebar-head {
@@ -135,31 +159,91 @@ textarea {
135159

136160
.nav {
137161
display: grid;
138-
gap: 6px;
162+
gap: 9px;
139163
min-width: 0;
140164
margin-top: 26px;
141165
}
142166

143167
.nav a {
168+
position: relative;
144169
display: flex;
145170
align-items: center;
146171
gap: 10px;
147-
min-height: 40px;
172+
min-height: 42px;
173+
border: 1px solid var(--nav-border);
148174
border-radius: 8px;
149-
padding: 0 10px;
175+
background: var(--nav-bg);
176+
padding: 0 12px;
150177
color: var(--nav-text);
178+
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.04);
179+
overflow: hidden;
180+
transform: translate3d(0, 0, 0);
181+
transition:
182+
background 180ms ease,
183+
border-color 180ms ease,
184+
box-shadow 180ms ease,
185+
color 180ms ease,
186+
transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1);
187+
}
188+
189+
.nav a::before {
190+
position: absolute;
191+
inset: 0;
192+
content: "";
193+
background: radial-gradient(
194+
circle at 18% 20%,
195+
rgba(255, 255, 255, 0.46),
196+
transparent 34%
197+
);
198+
opacity: 0;
199+
transition: opacity 180ms ease;
200+
}
201+
202+
.nav a svg,
203+
.nav a span {
204+
position: relative;
205+
z-index: 1;
151206
}
152207

153208
.nav a:hover {
154209
background: var(--nav-hover-bg);
155210
color: var(--ink);
211+
box-shadow: 0 12px 24px rgba(15, 23, 42, 0.08);
212+
transform: translateY(-2px) scale(1.012);
213+
}
214+
215+
.nav a:hover::before,
216+
.nav a.active::before {
217+
opacity: 1;
218+
}
219+
220+
.nav a:active {
221+
transform: translateY(0) scale(0.985);
222+
}
223+
224+
.nav a.active {
225+
border-color: var(--nav-active-border);
226+
background: var(--nav-active-bg);
227+
color: var(--nav-active-text);
228+
box-shadow:
229+
0 14px 28px rgba(47, 143, 216, 0.2),
230+
inset 0 1px 0 rgba(255, 255, 255, 0.24);
231+
}
232+
233+
.nav a.active svg {
234+
filter: drop-shadow(0 0 8px rgba(87, 199, 212, 0.45));
156235
}
157236

158237
.main {
159238
min-width: 0;
160239
padding: 24px;
161240
}
162241

242+
.page-surface {
243+
animation: page-settle 240ms cubic-bezier(0.2, 0.8, 0.2, 1);
244+
transform-origin: 50% 18px;
245+
}
246+
163247
.topbar {
164248
display: flex;
165249
align-items: flex-start;
@@ -208,11 +292,24 @@ textarea {
208292
padding: 0 12px;
209293
color: var(--button-text);
210294
font-weight: 700;
295+
cursor: pointer;
296+
transition:
297+
background 180ms ease,
298+
border-color 180ms ease,
299+
box-shadow 180ms ease,
300+
color 180ms ease,
301+
transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1);
211302
}
212303

213304
.button:hover {
214305
background: var(--button-hover-bg);
215306
color: var(--ink);
307+
box-shadow: 0 10px 20px rgba(15, 23, 42, 0.08);
308+
transform: translateY(-1px);
309+
}
310+
311+
.button:active {
312+
transform: translateY(0) scale(0.98);
216313
}
217314

218315
.button.primary {
@@ -236,15 +333,38 @@ textarea {
236333
min-height: 38px;
237334
border: 1px solid var(--line);
238335
border-radius: 8px;
239-
background: var(--button-bg);
336+
background:
337+
radial-gradient(
338+
circle at 18% 20%,
339+
rgba(255, 255, 255, 0.3),
340+
transparent 34%
341+
),
342+
var(--button-bg);
240343
color: var(--button-text);
344+
box-shadow:
345+
0 10px 22px rgba(47, 143, 216, 0.12),
346+
inset 0 1px 0 rgba(255, 255, 255, 0.18);
241347
font-weight: 800;
242348
cursor: pointer;
349+
transition:
350+
background 180ms ease,
351+
border-color 180ms ease,
352+
box-shadow 180ms ease,
353+
color 180ms ease,
354+
transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1);
243355
}
244356

245357
.theme-toggle:hover {
246358
background: var(--button-hover-bg);
247359
color: var(--ink);
360+
box-shadow:
361+
0 14px 26px rgba(47, 143, 216, 0.18),
362+
inset 0 1px 0 rgba(255, 255, 255, 0.24);
363+
transform: translateY(-1px) scale(1.01);
364+
}
365+
366+
.theme-toggle:active {
367+
transform: translateY(0) scale(0.985);
248368
}
249369

250370
.grid {
@@ -266,6 +386,10 @@ textarea {
266386
background: var(--panel);
267387
padding: 16px;
268388
box-shadow: var(--shadow-soft);
389+
transition:
390+
background-color 180ms ease,
391+
border-color 180ms ease,
392+
box-shadow 180ms ease;
269393
}
270394

271395
.card h2,
@@ -555,6 +679,11 @@ textarea {
555679
padding-bottom: 2px;
556680
}
557681

682+
.nav a {
683+
min-height: 38px;
684+
white-space: nowrap;
685+
}
686+
558687
.topbar {
559688
display: grid;
560689
}
@@ -572,6 +701,28 @@ textarea {
572701
}
573702
}
574703

704+
@media (prefers-reduced-motion: reduce) {
705+
*,
706+
*::before,
707+
*::after {
708+
animation-duration: 1ms !important;
709+
scroll-behavior: auto !important;
710+
transition-duration: 1ms !important;
711+
}
712+
}
713+
714+
@keyframes page-settle {
715+
from {
716+
opacity: 0;
717+
transform: translateY(8px) scale(0.992);
718+
}
719+
720+
to {
721+
opacity: 1;
722+
transform: translateY(0) scale(1);
723+
}
724+
}
725+
575726
.site {
576727
min-height: 100vh;
577728
background: #061423;

apps/web/app/layout.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,25 @@ export default function RootLayout({
7272
children,
7373
}: Readonly<{ children: React.ReactNode }>) {
7474
return (
75-
<html lang="en">
75+
<html lang="en" suppressHydrationWarning>
76+
<head>
77+
<script
78+
dangerouslySetInnerHTML={{
79+
__html: `
80+
try {
81+
var key = "piphacklup-dashboard-theme";
82+
var saved = window.localStorage.getItem(key);
83+
var theme = saved === "dark" || saved === "light"
84+
? saved
85+
: window.matchMedia("(prefers-color-scheme: dark)").matches
86+
? "dark"
87+
: "light";
88+
document.documentElement.dataset.dashboardTheme = theme;
89+
} catch (_) {}
90+
`,
91+
}}
92+
/>
93+
</head>
7694
<body>{children}</body>
7795
</html>
7896
);

apps/web/components/AppShell.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
Wrench,
1212
} from "lucide-react";
1313
import Link from "next/link";
14+
import { usePathname } from "next/navigation";
1415
import { useEffect, useState } from "react";
1516
import { ThemeToggle } from "@/components/ThemeToggle";
1617

@@ -32,6 +33,7 @@ export function AppShell({
3233
children,
3334
}: Readonly<{ children: React.ReactNode }>) {
3435
const [theme, setTheme] = useState<DashboardTheme>("light");
36+
const pathname = usePathname();
3537

3638
useEffect(() => {
3739
const saved = window.localStorage.getItem(themeStorageKey);
@@ -42,12 +44,14 @@ export function AppShell({
4244
? "dark"
4345
: "light";
4446
setTheme(initial);
47+
document.documentElement.dataset.dashboardTheme = initial;
4548
}, []);
4649

4750
function toggleTheme() {
4851
const next = theme === "dark" ? "light" : "dark";
4952
setTheme(next);
5053
window.localStorage.setItem(themeStorageKey, next);
54+
document.documentElement.dataset.dashboardTheme = next;
5155
}
5256

5357
return (
@@ -63,20 +67,32 @@ export function AppShell({
6367
<nav className="nav" aria-label="Main navigation">
6468
{navItems.map((item) => {
6569
const Icon = item.icon;
70+
const active =
71+
pathname === item.href || pathname.startsWith(`${item.href}/`);
72+
6673
return (
67-
<Link key={item.href} href={item.href}>
74+
<Link
75+
aria-current={active ? "page" : undefined}
76+
className={active ? "active" : undefined}
77+
key={item.href}
78+
href={item.href}
79+
>
6880
<Icon aria-hidden size={18} />
69-
{item.label}
81+
<span>{item.label}</span>
7082
</Link>
7183
);
7284
})}
7385
<a href="/api/export" title="Download CSV export">
7486
<Download aria-hidden size={18} />
75-
Export
87+
<span>Export</span>
7688
</a>
7789
</nav>
7890
</aside>
79-
<main className="main">{children}</main>
91+
<main className="main">
92+
<div className="page-surface" key={pathname}>
93+
{children}
94+
</div>
95+
</main>
8096
</div>
8197
);
8298
}

0 commit comments

Comments
 (0)