diff --git a/src/app/(app)/AppNav.tsx b/src/app/(app)/AppNav.tsx
new file mode 100644
index 0000000..5f6e673
--- /dev/null
+++ b/src/app/(app)/AppNav.tsx
@@ -0,0 +1,19 @@
+"use client";
+
+import { usePathname } from "next/navigation";
+import { TabBar, type TabKey } from "@/ds";
+
+const TAB_ORDER: TabKey[] = ["home", "accounting", "planner", "assets", "profile"];
+
+function activeTabFor(pathname: string | null): TabKey {
+ const match = TAB_ORDER.find((key) => pathname?.startsWith(`/${key}`));
+ return match ?? "home";
+}
+
+/** Client-only sliver of the shell: reads the current route so the layout
+ * (a server component) can stay server-rendered while `TabBar` still knows
+ * which tab is active. */
+export function AppNav() {
+ const pathname = usePathname();
+ return ;
+}
diff --git a/src/app/(app)/home/page.tsx b/src/app/(app)/home/page.tsx
new file mode 100644
index 0000000..63682dd
--- /dev/null
+++ b/src/app/(app)/home/page.tsx
@@ -0,0 +1,6 @@
+import { t } from "@/i18n/common";
+
+// Placeholder — Task 9 replaces this with the real dashboard.
+export default function HomePage() {
+ return
{t.tabs.home}
;
+}
diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx
new file mode 100644
index 0000000..8292f93
--- /dev/null
+++ b/src/app/(app)/layout.tsx
@@ -0,0 +1,20 @@
+import type { ReactNode } from "react";
+import { AppQueryProvider } from "@/api/queryClient";
+import { AppNav } from "./AppNav";
+
+/** Shell for every authed route (guarded by middleware). Bottom tab bar on
+ * mobile, left sidebar on desktop — `TabBar` handles its own responsive
+ * switch, this just positions it next to a centered, max-width content
+ * column and wraps everything in the shared React Query provider. */
+export default function AppLayout({ children }: { children: ReactNode }) {
+ return (
+
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/src/app/(app)/shell.test.tsx b/src/app/(app)/shell.test.tsx
new file mode 100644
index 0000000..3d8dcfa
--- /dev/null
+++ b/src/app/(app)/shell.test.tsx
@@ -0,0 +1,16 @@
+import { render, screen } from "@testing-library/react";
+import { it, expect, vi } from "vitest";
+import { t } from "@/i18n/common";
+
+vi.mock("next/navigation", () => ({
+ usePathname: () => "/home",
+}));
+
+import { AppNav } from "./AppNav";
+
+it("renders all five tab labels", () => {
+ render();
+ for (const label of Object.values(t.tabs)) {
+ expect(screen.getByText(label)).toBeInTheDocument();
+ }
+});
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 24dd4ef..3719d42 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,21 +1,5 @@
-import { ActionButton } from "@seed-design/react";
+import { redirect } from "next/navigation";
-export default function Home() {
- return (
-
-
Mercury
-
- Seed OK
-
- );
+export default function RootPage() {
+ redirect("/home");
}
diff --git a/vitest.config.ts b/vitest.config.ts
index 9828ce6..88094b5 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -1,7 +1,16 @@
+import { fileURLToPath } from "node:url";
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
+ // Mirrors tsconfig's "@/*" -> "./src/*" path mapping (Next resolves this
+ // natively; Vite/Vitest need it spelled out) so ds/ modules that import
+ // via "@/..." (e.g. TabBar.tsx -> "@/i18n/common") resolve under Vitest too.
+ resolve: {
+ alias: {
+ "@": fileURLToPath(new URL("./src", import.meta.url)),
+ },
+ },
test: {
environment: "jsdom",
setupFiles: ["./vitest.setup.ts"],