diff --git a/AGENTS.md b/AGENTS.md
index d90cd5c..d45b95e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -93,6 +93,11 @@ Expected high-level structure:
`apps/web` is the React frontend.
+Within `apps/web/src`, use `app/` for application-wide composition such as the
+protected shell, access boundary, and identity shared across routed features.
+Keep `features/` reserved for product capabilities such as dashboard, routines,
+workouts, and authentication.
+
`apps/api` is the Express backend.
`packages/contracts` contains code that represents contracts shared between the
diff --git a/apps/web/src/features/dashboard/dashboard-access-boundary.test.tsx b/apps/web/src/app/application-access-boundary.test.tsx
similarity index 89%
rename from apps/web/src/features/dashboard/dashboard-access-boundary.test.tsx
rename to apps/web/src/app/application-access-boundary.test.tsx
index 2dfbcfa..f9c17b5 100644
--- a/apps/web/src/features/dashboard/dashboard-access-boundary.test.tsx
+++ b/apps/web/src/app/application-access-boundary.test.tsx
@@ -1,4 +1,4 @@
-// Verifies dashboard access states with an isolated router and query cache for each case.
+// Verifies application access states with an isolated router and query cache for each case.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import {
createMemoryHistory,
@@ -11,10 +11,10 @@ import {
import { cleanup, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
-import { createPublicUser } from '../../test/fixtures/public-user.fixture';
-import { startGuestSession } from '../auth/guest-session';
+import { startGuestSession } from '../features/auth/guest-session';
+import { createPublicUser } from '../test/fixtures/public-user.fixture';
+import { ApplicationAccessBoundary } from './application-access-boundary';
import { ApplicationShell } from './application-shell';
-import { DashboardAccessBoundary } from './dashboard-access-boundary';
const publicUser = createPublicUser({
avatarUrl: 'https://images.example.com/avatar.png',
@@ -26,7 +26,7 @@ afterEach(() => {
vi.unstubAllGlobals();
});
-describe('dashboard access boundary', () => {
+describe('application access boundary', () => {
it('allows an authenticated visitor into the dashboard', async () => {
vi.stubGlobal(
'fetch',
@@ -115,13 +115,13 @@ function renderDashboard() {
getParentRoute: () => rootRoute,
path: '/dashboard',
component: () => (
-
+
{(identity) => (
Dashboard
)}
-
+
),
});
const testRouter = createRouter({
diff --git a/apps/web/src/features/dashboard/dashboard-access-boundary.tsx b/apps/web/src/app/application-access-boundary.tsx
similarity index 81%
rename from apps/web/src/features/dashboard/dashboard-access-boundary.tsx
rename to apps/web/src/app/application-access-boundary.tsx
index b0c534d..40d6b2e 100644
--- a/apps/web/src/features/dashboard/dashboard-access-boundary.tsx
+++ b/apps/web/src/app/application-access-boundary.tsx
@@ -2,18 +2,18 @@
import { Navigate } from '@tanstack/react-router';
import type { ReactNode } from 'react';
-import { Button } from '../../components/ui/button';
-import { useCurrentUser } from '../auth/current-user.query';
-import { isGuestSession } from '../auth/guest-session';
+import { Button } from '../components/ui/button';
+import { useCurrentUser } from '../features/auth/current-user.query';
+import { isGuestSession } from '../features/auth/guest-session';
import type { ApplicationIdentity } from './application-identity';
-type DashboardAccessBoundaryProps = {
+type ApplicationAccessBoundaryProps = {
children: (identity: ApplicationIdentity) => ReactNode;
};
-export function DashboardAccessBoundary({
+export function ApplicationAccessBoundary({
children,
-}: DashboardAccessBoundaryProps) {
+}: ApplicationAccessBoundaryProps) {
const currentUser = useCurrentUser();
if (isGuestSession()) {
diff --git a/apps/web/src/features/dashboard/application-identity.ts b/apps/web/src/app/application-identity.ts
similarity index 100%
rename from apps/web/src/features/dashboard/application-identity.ts
rename to apps/web/src/app/application-identity.ts
diff --git a/apps/web/src/features/dashboard/application-shell.test.tsx b/apps/web/src/app/application-shell.test.tsx
similarity index 95%
rename from apps/web/src/features/dashboard/application-shell.test.tsx
rename to apps/web/src/app/application-shell.test.tsx
index cabb60a..8778db2 100644
--- a/apps/web/src/features/dashboard/application-shell.test.tsx
+++ b/apps/web/src/app/application-shell.test.tsx
@@ -5,8 +5,11 @@ import { cleanup, render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it } from 'vitest';
-import { router } from '../../router';
-import { isGuestSession, startGuestSession } from '../auth/guest-session';
+import {
+ isGuestSession,
+ startGuestSession,
+} from '../features/auth/guest-session';
+import { router } from '../router';
afterEach(() => {
cleanup();
diff --git a/apps/web/src/features/dashboard/application-shell.tsx b/apps/web/src/app/application-shell.tsx
similarity index 98%
rename from apps/web/src/features/dashboard/application-shell.tsx
rename to apps/web/src/app/application-shell.tsx
index 46ca980..045d524 100644
--- a/apps/web/src/features/dashboard/application-shell.tsx
+++ b/apps/web/src/app/application-shell.tsx
@@ -11,8 +11,8 @@ import {
import { Link } from '@tanstack/react-router';
import { useState, type ReactNode } from 'react';
-import wordmarkUrl from '../../assets/goforlift-wordmark.png';
-import { cn } from '../../lib/utils';
+import wordmarkUrl from '../assets/goforlift-wordmark.png';
+import { cn } from '../lib/utils';
import type { ApplicationIdentity } from './application-identity';
const navigationItems = [
diff --git a/apps/web/src/features/dashboard/dashboard-landing.test.tsx b/apps/web/src/features/dashboard/dashboard-landing.test.tsx
index 83b7994..8ff7d7f 100644
--- a/apps/web/src/features/dashboard/dashboard-landing.test.tsx
+++ b/apps/web/src/features/dashboard/dashboard-landing.test.tsx
@@ -2,8 +2,8 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
+import { ApplicationIdentityContext } from '../../app/application-identity';
import { createPublicUser } from '../../test/fixtures/public-user.fixture';
-import { ApplicationIdentityContext } from './application-identity';
import { DashboardLanding } from './dashboard-landing';
describe('dashboard landing', () => {
diff --git a/apps/web/src/features/dashboard/dashboard-landing.tsx b/apps/web/src/features/dashboard/dashboard-landing.tsx
index 06969bd..f468908 100644
--- a/apps/web/src/features/dashboard/dashboard-landing.tsx
+++ b/apps/web/src/features/dashboard/dashboard-landing.tsx
@@ -1,5 +1,5 @@
// Introduces new dashboard visitors with identity-aware onboarding copy.
-import { useApplicationIdentity } from './application-identity';
+import { useApplicationIdentity } from '../../app/application-identity';
export function DashboardLanding() {
const identity = useApplicationIdentity();
diff --git a/apps/web/src/features/dashboard/section-landing.tsx b/apps/web/src/features/dashboard/section-landing.tsx
deleted file mode 100644
index 3b3f94b..0000000
--- a/apps/web/src/features/dashboard/section-landing.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-// Presents a minimal heading within the parent application route layout.
-
-type SectionLandingProps = {
- title: string;
-};
-
-export function SectionLanding({ title }: SectionLandingProps) {
- return (
-
- {title}
-
- );
-}
diff --git a/apps/web/src/routes/_app.history.tsx b/apps/web/src/routes/_app.history.tsx
index 3fb92a7..5048a72 100644
--- a/apps/web/src/routes/_app.history.tsx
+++ b/apps/web/src/routes/_app.history.tsx
@@ -1,8 +1,14 @@
// Maps the history URL into the protected application layout.
import { createFileRoute } from '@tanstack/react-router';
-import { SectionLanding } from '../features/dashboard/section-landing';
-
export const Route = createFileRoute('/_app/history')({
- component: () => ,
+ component: HistoryLanding,
});
+
+function HistoryLanding() {
+ return (
+
+ History
+
+ );
+}
diff --git a/apps/web/src/routes/_app.routines.tsx b/apps/web/src/routes/_app.routines.tsx
index 2a61935..87f2a35 100644
--- a/apps/web/src/routes/_app.routines.tsx
+++ b/apps/web/src/routes/_app.routines.tsx
@@ -1,8 +1,14 @@
// Maps the routines URL into the protected application layout.
import { createFileRoute } from '@tanstack/react-router';
-import { SectionLanding } from '../features/dashboard/section-landing';
-
export const Route = createFileRoute('/_app/routines')({
- component: () => ,
+ component: RoutinesLanding,
});
+
+function RoutinesLanding() {
+ return (
+
+ Routines
+
+ );
+}
diff --git a/apps/web/src/routes/_app.settings.tsx b/apps/web/src/routes/_app.settings.tsx
index d9ed0b2..e0af1e4 100644
--- a/apps/web/src/routes/_app.settings.tsx
+++ b/apps/web/src/routes/_app.settings.tsx
@@ -1,8 +1,14 @@
// Maps the settings URL into the protected application layout.
import { createFileRoute } from '@tanstack/react-router';
-import { SectionLanding } from '../features/dashboard/section-landing';
-
export const Route = createFileRoute('/_app/settings')({
- component: () => ,
+ component: SettingsLanding,
});
+
+function SettingsLanding() {
+ return (
+
+ Settings
+
+ );
+}
diff --git a/apps/web/src/routes/_app.tsx b/apps/web/src/routes/_app.tsx
index f575427..7fa9bc4 100644
--- a/apps/web/src/routes/_app.tsx
+++ b/apps/web/src/routes/_app.tsx
@@ -1,10 +1,10 @@
// Protects and frames every route inside the signed-in or guest application area.
import { createFileRoute, Outlet, useNavigate } from '@tanstack/react-router';
+import { ApplicationAccessBoundary } from '../app/application-access-boundary';
+import { ApplicationIdentityContext } from '../app/application-identity';
+import { ApplicationShell } from '../app/application-shell';
import { clearGuestSession } from '../features/auth/guest-session';
-import { ApplicationIdentityContext } from '../features/dashboard/application-identity';
-import { ApplicationShell } from '../features/dashboard/application-shell';
-import { DashboardAccessBoundary } from '../features/dashboard/dashboard-access-boundary';
export const Route = createFileRoute('/_app')({
component: ProtectedApplicationLayout,
@@ -19,7 +19,7 @@ function ProtectedApplicationLayout() {
}
return (
-
+
{(identity) => (
)}
-
+
);
}