diff --git a/apps/web/src/features/dashboard/application-shell.test.tsx b/apps/web/src/features/dashboard/application-shell.test.tsx
new file mode 100644
index 0000000..38acf46
--- /dev/null
+++ b/apps/web/src/features/dashboard/application-shell.test.tsx
@@ -0,0 +1,37 @@
+// Verifies the application shell exposes accessible responsive navigation and active state.
+import { RouterProvider } from '@tanstack/react-router';
+import { cleanup, render, screen, within } from '@testing-library/react';
+import { afterEach, describe, expect, it } from 'vitest';
+
+import { router } from '../../router';
+
+afterEach(cleanup);
+
+describe('application shell', () => {
+ it('renders desktop and mobile navigation with the current page selected', async () => {
+ await router.navigate({ to: '/dashboard' });
+ render();
+
+ expect(screen.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
+
+ const navigationRegions = screen.getAllByRole('navigation', {
+ name: /primary navigation/i,
+ });
+ expect(navigationRegions).toHaveLength(2);
+
+ for (const navigation of navigationRegions) {
+ expect(
+ within(navigation).getByRole('link', { name: 'Dashboard' }),
+ ).toHaveAttribute('aria-current', 'page');
+ expect(
+ within(navigation).getByRole('link', { name: 'Routines' }),
+ ).toHaveAttribute('href', '/routines');
+ expect(
+ within(navigation).getByRole('link', { name: 'History' }),
+ ).toHaveAttribute('href', '/history');
+ expect(
+ within(navigation).getByRole('link', { name: 'Settings' }),
+ ).toHaveAttribute('href', '/settings');
+ }
+ });
+});
diff --git a/apps/web/src/features/dashboard/application-shell.tsx b/apps/web/src/features/dashboard/application-shell.tsx
new file mode 100644
index 0000000..bcb5d02
--- /dev/null
+++ b/apps/web/src/features/dashboard/application-shell.tsx
@@ -0,0 +1,133 @@
+// Provides the responsive desktop and mobile navigation frame for application pages.
+import {
+ Dumbbell,
+ History,
+ LayoutDashboard,
+ Settings,
+ type LucideIcon,
+} from 'lucide-react';
+import { Link } from '@tanstack/react-router';
+import type { ReactNode } from 'react';
+
+import wordmarkUrl from '../../assets/goforlift-wordmark.png';
+import { cn } from '../../lib/utils';
+
+const navigationItems = [
+ { href: '/dashboard', icon: LayoutDashboard, label: 'Dashboard' },
+ { href: '/routines', icon: Dumbbell, label: 'Routines' },
+ { href: '/history', icon: History, label: 'History' },
+ { href: '/settings', icon: Settings, label: 'Settings' },
+] as const;
+
+type ApplicationShellProps = {
+ children: ReactNode;
+};
+
+export function ApplicationShell({ children }: ApplicationShellProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+ );
+}
+
+function Brand({ className }: { className?: string }) {
+ return (
+
+
+
+ );
+}
+
+type NavigationProps = {
+ className?: string;
+};
+
+function Navigation({ className }: NavigationProps) {
+ return (
+
+ );
+}
+
+type NavigationItemProps = {
+ href: (typeof navigationItems)[number]['href'];
+ icon: LucideIcon;
+ label: string;
+};
+
+function NavigationItem({ href, icon: Icon, label }: NavigationItemProps) {
+ return (
+
+
+ {label}
+
+ );
+}
diff --git a/apps/web/src/features/dashboard/dashboard-landing.tsx b/apps/web/src/features/dashboard/dashboard-landing.tsx
index b9e3655..f946dcf 100644
--- a/apps/web/src/features/dashboard/dashboard-landing.tsx
+++ b/apps/web/src/features/dashboard/dashboard-landing.tsx
@@ -1,16 +1,6 @@
-// Presents the temporary shared landing state before the full dashboard is designed.
+// Presents the dashboard heading inside the shared responsive application shell.
+import { SectionLanding } from './section-landing';
+
export function DashboardLanding() {
- return (
-
-
-
- Dashboard
-
- Welcome to GoForLift
-
- Your session is ready. The full dashboard is the next step.
-
-
-
- );
+ return ;
}
diff --git a/apps/web/src/features/dashboard/section-landing.tsx b/apps/web/src/features/dashboard/section-landing.tsx
new file mode 100644
index 0000000..981c41a
--- /dev/null
+++ b/apps/web/src/features/dashboard/section-landing.tsx
@@ -0,0 +1,16 @@
+// Presents a minimal content heading inside the shared application shell.
+import { ApplicationShell } from './application-shell';
+
+type SectionLandingProps = {
+ title: string;
+};
+
+export function SectionLanding({ title }: SectionLandingProps) {
+ return (
+
+
+ {title}
+
+
+ );
+}
diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts
index 2819a83..0c6aef0 100644
--- a/apps/web/src/lib/utils.ts
+++ b/apps/web/src/lib/utils.ts
@@ -1,6 +1,9 @@
+// Combines conditional class names and resolves conflicting Tailwind utilities for reusable components.
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
+// `cn` means "class names"; this helper keeps conditional styles readable and
+// ensures later Tailwind utilities correctly override conflicting earlier ones.
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts
index 0cda756..7831fcd 100644
--- a/apps/web/src/routeTree.gen.ts
+++ b/apps/web/src/routeTree.gen.ts
@@ -11,6 +11,9 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
import { Route as DashboardRouteImport } from './routes/dashboard'
+import { Route as HistoryRouteImport } from './routes/history'
+import { Route as RoutinesRouteImport } from './routes/routines'
+import { Route as SettingsRouteImport } from './routes/settings'
const IndexRoute = IndexRouteImport.update({
id: '/',
@@ -22,31 +25,58 @@ const DashboardRoute = DashboardRouteImport.update({
path: '/dashboard',
getParentRoute: () => rootRouteImport,
} as any)
+const HistoryRoute = HistoryRouteImport.update({
+ id: '/history',
+ path: '/history',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const RoutinesRoute = RoutinesRouteImport.update({
+ id: '/routines',
+ path: '/routines',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const SettingsRoute = SettingsRouteImport.update({
+ id: '/settings',
+ path: '/settings',
+ getParentRoute: () => rootRouteImport,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/dashboard': typeof DashboardRoute
+ '/history': typeof HistoryRoute
+ '/routines': typeof RoutinesRoute
+ '/settings': typeof SettingsRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/dashboard': typeof DashboardRoute
+ '/history': typeof HistoryRoute
+ '/routines': typeof RoutinesRoute
+ '/settings': typeof SettingsRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/dashboard': typeof DashboardRoute
+ '/history': typeof HistoryRoute
+ '/routines': typeof RoutinesRoute
+ '/settings': typeof SettingsRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
- fullPaths: '/' | '/dashboard'
+ fullPaths: '/' | '/dashboard' | '/history' | '/routines' | '/settings'
fileRoutesByTo: FileRoutesByTo
- to: '/' | '/dashboard'
- id: '__root__' | '/' | '/dashboard'
+ to: '/' | '/dashboard' | '/history' | '/routines' | '/settings'
+ id: '__root__' | '/' | '/dashboard' | '/history' | '/routines' | '/settings'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
DashboardRoute: typeof DashboardRoute
+ HistoryRoute: typeof HistoryRoute
+ RoutinesRoute: typeof RoutinesRoute
+ SettingsRoute: typeof SettingsRoute
}
declare module '@tanstack/react-router' {
@@ -65,12 +95,36 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DashboardRouteImport
parentRoute: typeof rootRouteImport
}
+ '/history': {
+ id: '/history'
+ path: '/history'
+ fullPath: '/history'
+ preLoaderRoute: typeof HistoryRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/routines': {
+ id: '/routines'
+ path: '/routines'
+ fullPath: '/routines'
+ preLoaderRoute: typeof RoutinesRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/settings': {
+ id: '/settings'
+ path: '/settings'
+ fullPath: '/settings'
+ preLoaderRoute: typeof SettingsRouteImport
+ parentRoute: typeof rootRouteImport
+ }
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
DashboardRoute: DashboardRoute,
+ HistoryRoute: HistoryRoute,
+ RoutinesRoute: RoutinesRoute,
+ SettingsRoute: SettingsRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
diff --git a/apps/web/src/routes/history.tsx b/apps/web/src/routes/history.tsx
new file mode 100644
index 0000000..8ad2e9a
--- /dev/null
+++ b/apps/web/src/routes/history.tsx
@@ -0,0 +1,8 @@
+// Maps the history destination into the shared responsive application shell.
+import { createFileRoute } from '@tanstack/react-router';
+
+import { SectionLanding } from '../features/dashboard/section-landing';
+
+export const Route = createFileRoute('/history')({
+ component: () => ,
+});
diff --git a/apps/web/src/routes/index.test.tsx b/apps/web/src/routes/index.test.tsx
index 0663e63..8f325c3 100644
--- a/apps/web/src/routes/index.test.tsx
+++ b/apps/web/src/routes/index.test.tsx
@@ -42,7 +42,9 @@ describe('welcome page', () => {
fireEvent.click(screen.getByRole('button', { name: /continue as guest/i }));
- expect(await screen.findByText(/your session is ready/i)).toBeVisible();
+ expect(
+ await screen.findByRole('heading', { name: 'Dashboard' }),
+ ).toBeVisible();
expect(sessionStorage.getItem('goforlift.guest')).toBe('true');
expect(router.state.location.pathname).toBe('/dashboard');
diff --git a/apps/web/src/routes/routines.tsx b/apps/web/src/routes/routines.tsx
new file mode 100644
index 0000000..748764b
--- /dev/null
+++ b/apps/web/src/routes/routines.tsx
@@ -0,0 +1,8 @@
+// Maps the routines destination into the shared responsive application shell.
+import { createFileRoute } from '@tanstack/react-router';
+
+import { SectionLanding } from '../features/dashboard/section-landing';
+
+export const Route = createFileRoute('/routines')({
+ component: () => ,
+});
diff --git a/apps/web/src/routes/settings.tsx b/apps/web/src/routes/settings.tsx
new file mode 100644
index 0000000..d63288d
--- /dev/null
+++ b/apps/web/src/routes/settings.tsx
@@ -0,0 +1,8 @@
+// Maps the settings destination into the shared responsive application shell.
+import { createFileRoute } from '@tanstack/react-router';
+
+import { SectionLanding } from '../features/dashboard/section-landing';
+
+export const Route = createFileRoute('/settings')({
+ component: () => ,
+});
diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css
index 0a33600..8bbb3dd 100644
--- a/apps/web/src/styles.css
+++ b/apps/web/src/styles.css
@@ -6,19 +6,19 @@
:root {
color-scheme: dark;
- --background: oklch(0.145 0.01 285);
+ --background: oklch(0.14 0.018 255);
--foreground: oklch(0.96 0.01 285);
- --surface: oklch(0.205 0.015 285);
- --surface-elevated: oklch(0.26 0.02 285);
- --primary: oklch(0.72 0.16 175);
- --primary-foreground: oklch(0.15 0.02 175);
- --secondary: oklch(0.26 0.02 285);
+ --surface: oklch(0.18 0.022 255);
+ --surface-elevated: oklch(0.22 0.025 255);
+ --primary: oklch(0.59 0.22 27);
+ --primary-foreground: oklch(0.98 0.005 27);
+ --secondary: oklch(0.22 0.025 255);
--secondary-foreground: oklch(0.96 0.01 285);
- --accent: oklch(0.3 0.025 285);
+ --accent: oklch(0.24 0.025 255);
--accent-foreground: oklch(0.96 0.01 285);
- --muted-foreground: oklch(0.7 0.02 285);
- --border: oklch(0.32 0.02 285);
- --input: oklch(0.32 0.02 285);
+ --muted-foreground: oklch(0.7 0.018 255);
+ --border: oklch(0.3 0.025 255);
+ --input: oklch(0.3 0.025 255);
--ring: var(--primary);
--destructive: oklch(0.65 0.2 25);
}
diff --git a/docs/design.md b/docs/design.md
index 6543acc..6528e82 100644
--- a/docs/design.md
+++ b/docs/design.md
@@ -310,6 +310,19 @@ If the same meaningful visual pattern repeatedly appears, consider:
Do not abstract one-off styling simply to eliminate every repeated class.
+For readability, keep short Tailwind class lists inline. When a list becomes
+long or combines several responsibilities, pass multiple strings to `cn()` and
+group them in this order when applicable:
+
+1. layout and spacing
+2. typography and color
+3. interaction and focus states
+4. responsive overrides
+5. conditional state
+
+Extract a reusable component or `cva()` variant only when a meaningful styling
+pattern actually repeats.
+
# 14. Responsive Design
@@ -800,4 +813,4 @@ not:
+
workout functionality added afterward
-Usability comes first.
\ No newline at end of file
+Usability comes first.