UX Standards — 39: New Page Scaffolding Guide
Governs: End-to-end checklist for building a new feature page from scratch. Parent rules: Read ALL referenced documents before building.
Overview
Every new page in any Anshin Health frontend follows this exact sequence. Do not skip steps. Steps 1–5 must be done before writing any component code.
Step 1: Create the Route File
# Next.js App Router — create the directory and page file
mkdir -p "src/app/(dashboard)/my-feature"
touch "src/app/(dashboard)/my-feature/page.tsx"
# For detail pages:
mkdir -p "src/app/(dashboard)/my-feature/[id]"
touch "src/app/(dashboard)/my-feature/[id]/page.tsx"
// src/app/(dashboard)/my-feature/page.tsx
export default function MyFeaturePage() {
return <div>TODO</div>;
}
Step 2: Register in Nav Config
// packages/shared/components/layouts/nav-main-config.ts
import { MyFeatureIcon } from 'lucide-react';
export const navMainConfig: NavItem[] = [
// ... existing items
{
title: 'my-feature.title', // i18n key — NOT the translated string
href: '/my-feature',
icon: MyFeatureIcon,
},
];
Step 3: Register Page Title
// packages/shared/components/layouts/page-titles-main.ts
export const pageTitlesMain: PageTitleMap = {
// ... existing entries
'/my-feature': 'my-feature.title', // i18n key
'/my-feature/[id]': 'my-feature.detailTitle', // for detail pages
};
Step 4: Add i18n Keys
// packages/shared/lib/i18n/locales/en/my-feature.json
{
"my-feature.title": "My Feature",
"my-feature.detailTitle": "My Feature Detail",
"my-feature.description": "Manage your features",
"my-feature.create": "Create Feature",
"my-feature.createFirst": "Create your first feature",
"my-feature.noFeaturesDescription": "Get started by creating a feature.",
"my-feature.confirmDelete": "Delete this feature?",
"my-feature.confirmDeleteDescription": "This will permanently delete {{name}}. This cannot be undone."
}
// packages/shared/lib/i18n/locales/en/index.ts — add import
import myFeature from './my-feature.json';
export const en = { ...existing, ...myFeature };
Step 5: Define the Zod Schema and Types
// src/types/my-feature.ts
import { z } from 'zod';
export const myFeatureSchema = z.object({
name: z.string().min(1),
status: z.enum(['Active', 'Inactive', 'Draft']),
created_by: z.string(),
creation: z.string(),
modified: z.string(),
});
export type MyFeature = z.infer<typeof myFeatureSchema>;
Step 6: Build the List Page
// src/app/(dashboard)/my-feature/page.tsx
'use client';
import { useAnnaTableContext } from '@/components/anna/anna-provider';
import { useTranslation } from '@/lib/i18n';
import { useDocTypeTable } from '@/components/data-table/hooks';
import { DocTypeListPage } from '@/components/doctype-list-page';
import { myFeatureColumns } from './columns';
export default function MyFeaturePage() {
const { t } = useTranslation();
const { table, isLoading, refetch } = useDocTypeTable({
doctype: 'MyFeature',
columns: myFeatureColumns,
});
// ★ MANDATORY — Anna context
useAnnaTableContext({ doctype: 'MyFeature', table });
const statCards = [
{ label: t('my-feature.totalCount'), value: table.getRowCount(), icon: Package },
{ label: t('my-feature.activeCount'), value: activeCount, icon: CheckCircle2, variant: 'primary' },
];
return (
<DocTypeListPage
title={t('my-feature.title')}
description={t('my-feature.description')}
table={table}
statCards={statCards}
isLoading={isLoading}
onRefresh={refetch}
createButton={{
label: t('my-feature.create'),
href: '/my-feature/create',
}}
/>
);
}
Step 7: Define Columns
// src/app/(dashboard)/my-feature/columns.tsx
import { ColumnDef } from '@tanstack/react-table';
import { DataTableColumnHeader } from '@/components/data-table/data-table-column-header';
import { StatusBadge } from '@/components/shared/status-badge';
import { MyFeature } from '@/types/my-feature';
export const myFeatureColumns: ColumnDef<MyFeature>[] = [
{
accessorKey: 'name',
header: ({ column }) => <DataTableColumnHeader column={column} title="my-feature.name" />,
cell: ({ row }) => (
<Link href={`/my-feature/${row.original.name}`}
className="font-medium text-primary hover:underline">
{row.original.name}
</Link>
),
},
{
accessorKey: 'status',
header: ({ column }) => <DataTableColumnHeader column={column} title="common.status" />,
cell: ({ row }) => <StatusBadge status={row.original.status} />,
},
// ... more columns
{
id: 'actions',
cell: ({ row }) => <DataTableRowActions row={row} />,
},
];
Step 8: Build the Detail Page
// src/app/(dashboard)/my-feature/[id]/page.tsx
'use client';
import { useAnnaPageContext } from '@/components/anna/anna-provider';
import { usePageTitle } from '@/components/layouts/page-title-override-context';
export default function MyFeatureDetailPage({ params }: { params: { id: string } }) {
const { t } = useTranslation();
const { data: record, isLoading } = useMyFeature(params.id);
const { setPageTitle } = usePageTitle();
const { formatDate } = useFormatters();
// ★ MANDATORY — Anna context
useAnnaPageContext({ entityType: 'MyFeature', entityId: params.id, page: record ?? null });
// Dynamic page title
useEffect(() => {
if (record?.name) setPageTitle(record.name);
return () => setPageTitle(null);
}, [record?.name]);
if (isLoading) return <LoadingState type="detail" />;
if (!record) return <ErrorState />;
return (
<div className="space-y-6">
{/* Header */}
<DetailPageHeader record={record} onEdit={...} onDelete={...} />
{/* Two-column body */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
<Card>
<CardHeader><CardTitle className="card-title">{t('my-feature.details')}</CardTitle></CardHeader>
<CardContent>
<dl className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<DetailRow label={t('my-feature.name')} value={record.name} />
<DetailRow label={t('common.status')} value={<StatusBadge status={record.status} />} />
</dl>
</CardContent>
</Card>
</div>
<div>
<MetadataCard record={record} />
</div>
</div>
</div>
);
}
Step 9: Pre-Commit Checklist
Run these before every commit on a new page:
# 1. No hardcoded colors
grep -r "bg-green-\|bg-red-\|text-green-\|text-red-\|bg-blue-" src/app/\(dashboard\)/my-feature/ --include="*.tsx"
# 2. No missing Anna context
grep -rL "useAnnaTableContext\|useAnnaPageContext" src/app/\(dashboard\)/my-feature/ --include="page.tsx"
# 3. No hardcoded strings (rough check)
grep -r '"[A-Z][a-z][a-z]' src/app/\(dashboard\)/my-feature/ --include="*.tsx" | grep -v "//\|className\|variant\|type=\|placeholder="
# 4. i18n key added to English locale
grep -r "my-feature" packages/shared/lib/i18n/locales/en/
# 5. Nav config and page title registered
grep "my-feature" packages/shared/components/layouts/nav-main-config.ts
grep "my-feature" packages/shared/components/layouts/page-titles-main.ts
Complete File Checklist
src/app/(dashboard)/my-feature/
├── page.tsx ← List page (useAnnaTableContext)
├── columns.tsx ← Column definitions
├── [id]/
│ └── page.tsx ← Detail page (useAnnaPageContext + setPageTitle)
└── create/
└── page.tsx ← Create form (useAnnaPageContext)
src/types/
└── my-feature.ts ← Zod schema + TypeScript type
packages/shared/lib/i18n/locales/en/
└── my-feature.json ← i18n strings (MUST be added)
packages/shared/components/layouts/
├── nav-main-config.ts ← Nav item added
└── page-titles-main.ts ← Page title registered