Editor Command Center — Phase 1: Shadcn Setup + Shell
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Install shadcn/ui and build the command center layout shell with tree sidebar, tabbed detail panel, and desktop-only gate.
Architecture: New /admin page that bypasses the site Layout (TopNav/Footer) and uses its own full-screen admin layout. shadcn components installed under @/components/admin-ui/ to avoid conflicts with existing @/components/ui/ barrel. Per-page layout pattern in _app.tsx lets the admin page opt out of the site chrome.
Tech Stack: shadcn/ui, Radix UI primitives, Tailwind CSS v4, Next.js 16 Pages Router, React 19
Task 1: Install shadcn/ui and Dependencies
Files:
- Modify:
frontend/package.json - Create:
frontend/components.json - Modify:
frontend/src/styles/globals.css(add shadcn CSS variables) - Modify:
frontend/src/lib/utils.ts(shadcn cn helper)
Step 1: Install shadcn dependencies
cd /Users/ghostmonk/Documents/code/field-notes/.worktrees/editor-command-center/frontend
npm install class-variance-authority clsx tailwind-merge lucide-reactStep 2: Create the cn utility
Create frontend/src/lib/utils.ts:
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}Step 3: Create components.json for shadcn CLI
Create frontend/components.json:
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/styles/globals.css",
"baseColor": "zinc",
"cssVariables": true
},
"aliases": {
"components": "@/components/admin-ui",
"utils": "@/lib/utils",
"hooks": "@/hooks",
"ui": "@/components/admin-ui"
}
}Step 4: Add shadcn CSS variables to globals.css
Add to frontend/src/styles/globals.css after the existing body styles:
:root {
--radius: 0.5rem;
}
.admin-theme {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.965 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.965 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.965 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--sidebar-background: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.965 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.admin-theme.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--sidebar-background: oklch(0.175 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.985 0 0);
--sidebar-primary-foreground: oklch(0.205 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
}Step 5: Commit
git add frontend/package.json frontend/package-lock.json frontend/components.json frontend/src/lib/utils.ts frontend/src/styles/globals.css
git commit -m "chore: install shadcn/ui dependencies and configure for admin-ui"Task 2: Add Core shadcn Components
Files:
- Create:
frontend/src/components/admin-ui/button.tsx - Create:
frontend/src/components/admin-ui/tabs.tsx - Create:
frontend/src/components/admin-ui/scroll-area.tsx - Create:
frontend/src/components/admin-ui/separator.tsx - Create:
frontend/src/components/admin-ui/tooltip.tsx - Create:
frontend/src/components/admin-ui/input.tsx
Step 1: Add shadcn components via CLI
cd /Users/ghostmonk/Documents/code/field-notes/.worktrees/editor-command-center/frontend
npx shadcn@latest add button tabs scroll-area separator tooltip input --yesVerify files were created in src/components/admin-ui/ (not src/components/ui/).
Step 2: Verify imports resolve
Create a temporary test — open any admin-ui component file and confirm it imports from @/lib/utils correctly.
Step 3: Commit
git add frontend/src/components/admin-ui/
git commit -m "feat: add core shadcn components for admin command center"Task 3: Per-Page Layout Pattern in _app.tsx
The command center must bypass the site Layout (TopNav, Footer). Next.js Pages Router supports this via a getLayout pattern on page components.
Files:
- Modify:
frontend/src/pages/_app.tsx - Create:
frontend/src/shared/types/page.ts
Step 1: Define the NextPageWithLayout type
Create frontend/src/shared/types/page.ts:
import { NextPage } from "next";
import { ReactElement, ReactNode } from "react";
export type NextPageWithLayout<P = object, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode;
};Step 2: Update _app.tsx to support per-page layouts
In frontend/src/pages/_app.tsx, modify the MyApp function to check for Component.getLayout. If present, use it instead of the default Layout wrapper.
Current code wraps everything in <Layout>:
<Layout>
<BackendWarmupBanner ... />
<div ref={mainRef}>
<Component {...pageProps} />
</div>
</Layout>Change to:
import { NextPageWithLayout } from "@/shared/types/page";
// Update AppProps type
function MyApp({ Component, pageProps }: AppProps & { Component: NextPageWithLayout }) {
// ... existing code ...
const getLayout = Component.getLayout ?? ((page) => (
<Layout>
<BackendWarmupBanner
isWarming={isWarming}
warmupFailed={warmupFailed}
onRetry={handleWarmupRetry}
/>
<div ref={mainRef}>
{page}
</div>
</Layout>
));
return (
<SessionProvider session={pageProps.session} refetchInterval={4 * 60}>
<ToastProvider>
<SessionGuard>
<ConfirmProvider>
<Head>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0, user-scalable=yes"
/>
<meta name="author" content={getSiteConfig().site.author}/>
<link rel="canonical" href="https://ghostmonk.com/"/>
</Head>
<NetworkStatus />
{getLayout(<Component {...pageProps} />)}
</ConfirmProvider>
</SessionGuard>
</ToastProvider>
</SessionProvider>
);
}Step 3: Verify existing pages still render with Layout
Run: cd /Users/ghostmonk/Documents/code/field-notes/.worktrees/editor-command-center && make dev-local
Load any page (home, a section, editor) — confirm TopNav/Footer still appear. The default fallback layout handles all existing pages.
Step 4: Commit
git add frontend/src/shared/types/page.ts frontend/src/pages/_app.tsx
git commit -m "feat: add per-page layout support to _app.tsx"Task 4: Admin Command Center Layout Component
Files:
- Create:
frontend/src/modules/admin/components/AdminLayout.tsx - Create:
frontend/src/modules/admin/components/AdminSidebar.tsx - Create:
frontend/src/modules/admin/components/AdminDetailPanel.tsx - Create:
frontend/src/modules/admin/components/AdminDesktopGate.tsx
Step 1: Create the desktop-only gate
Create frontend/src/modules/admin/components/AdminDesktopGate.tsx:
import { useEffect, useState } from "react";
const DESKTOP_MIN_WIDTH = 1024;
export function AdminDesktopGate({ children }: { children: React.ReactNode }) {
const [isDesktop, setIsDesktop] = useState(true);
useEffect(() => {
const check = () => setIsDesktop(window.innerWidth >= DESKTOP_MIN_WIDTH);
check();
window.addEventListener("resize", check);
return () => window.removeEventListener("resize", check);
}, []);
if (!isDesktop) {
return (
<div className="flex h-dvh items-center justify-center p-8 text-center" style={{ backgroundColor: 'var(--background)', color: 'var(--foreground)' }}>
<div className="max-w-md space-y-4">
<h1 className="text-2xl font-semibold">Command Center</h1>
<p className="text-muted-foreground">
The command center requires a desktop browser. Use the site navigation to manage content on mobile.
</p>
</div>
</div>
);
}
return <>{children}</>;
}Step 2: Create the sidebar placeholder
Create frontend/src/modules/admin/components/AdminSidebar.tsx:
import { ScrollArea } from "@/components/admin-ui/scroll-area";
import { Input } from "@/components/admin-ui/input";
import { Button } from "@/components/admin-ui/button";
import { Separator } from "@/components/admin-ui/separator";
import { Plus, Search } from "lucide-react";
interface AdminSidebarProps {
selectedSectionId: string | null;
onSelectSection: (id: string | null) => void;
}
export function AdminSidebar({ selectedSectionId, onSelectSection }: AdminSidebarProps) {
return (
<div
className="flex h-full w-72 flex-col border-r"
style={{ borderColor: 'var(--sidebar-border)', backgroundColor: 'var(--sidebar-background)' }}
>
<div className="flex items-center justify-between p-4">
<h2 className="text-lg font-semibold" style={{ color: 'var(--sidebar-foreground)' }}>Sections</h2>
</div>
<div className="px-4 pb-3">
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4" style={{ color: 'var(--muted-foreground)' }} />
<Input
placeholder="Filter sections..."
className="pl-9"
data-testid="admin-section-filter"
/>
</div>
</div>
<Separator />
<ScrollArea className="flex-1">
<div className="p-4">
<p className="text-sm" style={{ color: 'var(--muted-foreground)' }} data-testid="admin-tree-placeholder">
Section tree will render here
</p>
</div>
</ScrollArea>
<Separator />
<div className="p-4">
<Button
variant="outline"
size="sm"
className="w-full"
data-testid="admin-add-root-section"
>
<Plus className="mr-2 h-4 w-4" />
Add Section
</Button>
</div>
</div>
);
}Step 3: Create the detail panel placeholder
Create frontend/src/modules/admin/components/AdminDetailPanel.tsx:
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/admin-ui/tabs";
import { ScrollArea } from "@/components/admin-ui/scroll-area";
interface AdminDetailPanelProps {
selectedSectionId: string | null;
}
export function AdminDetailPanel({ selectedSectionId }: AdminDetailPanelProps) {
if (!selectedSectionId) {
return (
<div className="flex flex-1 items-center justify-center" data-testid="admin-dashboard">
<div className="text-center space-y-4">
<h1 className="text-3xl font-semibold" style={{ color: 'var(--foreground)' }}>Command Center</h1>
<p className="text-lg" style={{ color: 'var(--muted-foreground)' }}>
Select a section to manage its content, or view site-wide stats here.
</p>
</div>
</div>
);
}
return (
<div className="flex flex-1 flex-col" data-testid="admin-detail-panel">
<Tabs defaultValue="content" className="flex flex-1 flex-col">
<div className="border-b px-6 pt-4" style={{ borderColor: 'var(--border)' }}>
<TabsList>
<TabsTrigger value="content" data-testid="admin-tab-content">Content</TabsTrigger>
<TabsTrigger value="assets" data-testid="admin-tab-assets">Assets</TabsTrigger>
<TabsTrigger value="settings" data-testid="admin-tab-settings">Settings</TabsTrigger>
</TabsList>
</div>
<ScrollArea className="flex-1">
<TabsContent value="content" className="p-6 mt-0">
<p style={{ color: 'var(--muted-foreground)' }} data-testid="admin-content-placeholder">
Content list will render here
</p>
</TabsContent>
<TabsContent value="assets" className="p-6 mt-0">
<p style={{ color: 'var(--muted-foreground)' }} data-testid="admin-assets-placeholder">
Assets grid will render here
</p>
</TabsContent>
<TabsContent value="settings" className="p-6 mt-0">
<p style={{ color: 'var(--muted-foreground)' }} data-testid="admin-settings-placeholder">
Section settings will render here
</p>
</TabsContent>
</ScrollArea>
</Tabs>
</div>
);
}Step 4: Create the main AdminLayout
Create frontend/src/modules/admin/components/AdminLayout.tsx:
import { AdminDesktopGate } from "./AdminDesktopGate";
import { AdminSidebar } from "./AdminSidebar";
import { AdminDetailPanel } from "./AdminDetailPanel";
import { useState, useEffect } from "react";
import { useRouter } from "next/router";
export function AdminLayout() {
const router = useRouter();
const [selectedSectionId, setSelectedSectionId] = useState<string | null>(null);
useEffect(() => {
const sectionId = router.query.section as string | undefined;
setSelectedSectionId(sectionId ?? null);
}, [router.query.section]);
const handleSelectSection = (id: string | null) => {
setSelectedSectionId(id);
const query = id ? { section: id } : {};
router.replace({ pathname: "/admin", query }, undefined, { shallow: true });
};
return (
<AdminDesktopGate>
<div
className="admin-theme flex h-dvh"
style={{ backgroundColor: 'var(--background)', color: 'var(--foreground)' }}
data-testid="admin-command-center"
>
<AdminSidebar
selectedSectionId={selectedSectionId}
onSelectSection={handleSelectSection}
/>
<AdminDetailPanel selectedSectionId={selectedSectionId} />
</div>
</AdminDesktopGate>
);
}Step 5: Commit
git add frontend/src/modules/admin/
git commit -m "feat: add admin command center layout components"Task 5: Admin Page with Custom Layout
Files:
- Create:
frontend/src/pages/admin/index.tsx
Step 1: Create the admin index page
The existing /admin/sections stays. This new /admin (index) is the command center entry point. It uses getLayout to bypass the site chrome.
Create frontend/src/pages/admin/index.tsx:
import { ReactElement } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { AdminLayout } from "@/modules/admin/components/AdminLayout";
import { NextPageWithLayout } from "@/shared/types/page";
import Head from "next/head";
const AdminPage: NextPageWithLayout = () => {
const { data: session, status } = useSession();
const router = useRouter();
useEffect(() => {
if (status === "unauthenticated") {
router.replace("/");
}
}, [status, router]);
if (status === "loading") {
return (
<div className="admin-theme flex h-dvh items-center justify-center" style={{ backgroundColor: 'var(--background)' }}>
<p style={{ color: 'var(--muted-foreground)' }}>Loading...</p>
</div>
);
}
if (!session) {
return null;
}
return (
<>
<Head>
<title>Command Center</title>
</Head>
<AdminLayout />
</>
);
};
AdminPage.getLayout = (page: ReactElement) => page;
export default AdminPage;Step 2: Run the dev server and verify
cd /Users/ghostmonk/Documents/code/field-notes/.worktrees/editor-command-center && make dev-localVerify:
http://localhost:3000/admin— Shows the command center (full screen, no TopNav/Footer, sidebar + detail panel)http://localhost:3000/adminon narrow viewport — Shows desktop-only messagehttp://localhost:3000/admin/sections— Existing admin sections page still works with normal Layouthttp://localhost:3000/— Home page still has TopNav/Footer
Step 3: Commit
git add frontend/src/pages/admin/index.tsx
git commit -m "feat: add admin command center page with custom layout"Task 6: Run Formatting and Tests
Step 1: Run formatter
cd /Users/ghostmonk/Documents/code/field-notes/.worktrees/editor-command-center && make formatFix any lint issues.
Step 2: Run frontend unit tests
make test-frontend-unitFix any failures. Existing tests should pass — we haven’t modified existing components.
Step 3: Run TypeScript check
cd frontend && npx tsc --noEmitFix any type errors.
Step 4: Commit any fixes
git add -A
git commit -m "fix: formatting and type fixes for admin command center"Task 7: Dark Mode Support
The command center needs to respect the site’s dark mode toggle. The .admin-theme class provides light/dark variables, but needs to respond to the same .dark class mechanism.
Files:
- Modify:
frontend/src/modules/admin/components/AdminLayout.tsx
Step 1: Add dark class awareness
Modify AdminLayout.tsx — the admin-theme div needs to also carry the dark class when the site is in dark mode. Check how the existing theme toggle works.
Read frontend/src/components/ThemeToggle.tsx to see how dark mode class is applied (likely on document.documentElement or a root div).
If dark mode uses a .dark class on <html> or <body>, update the CSS in globals.css so .admin-theme.dark matches when .dark is on an ancestor. The CSS already handles .admin-theme.dark — verify it chains correctly.
Update the admin-theme div:
<div
className="admin-theme flex h-dvh"
// ...
>If the dark class is on <html>, change the CSS selector from .admin-theme.dark to .dark .admin-theme:
.dark .admin-theme {
/* dark mode variables */
}Step 2: Verify both modes
Toggle dark mode while on /admin. Both light and dark should work.
Step 3: Commit
git add frontend/src/styles/globals.css frontend/src/modules/admin/
git commit -m "fix: dark mode support for admin command center"