TypeScript Frontend
This example demonstrates Contextia in a React + TypeScript frontend application. It shows how to spec UI components, record state management decisions, enforce accessibility norms, and annotate TSX files.
Project overview
Section titled “Project overview”The project is a dashboard application for analytics. It has a component library, data visualization views, and a settings panel. State management uses Zustand.
analytics-dashboard/├── .contextia/│ ├── config.yaml│ └── system/│ ├── identity.md│ ├── specs/│ │ ├── SPEC-010.md # Dashboard layout│ │ ├── SPEC-011.md # Chart components│ │ └── SPEC-012.md # Filter panel│ ├── rationale/│ │ ├── DEC-010.md # Zustand over Redux│ │ └── DEC-011.md # Recharts for data viz│ └── norms/│ ├── NORM-A11Y-001.md # Accessibility requirements│ └── NORM-UI-001.md # Component patterns├── src/│ ├── components/│ │ ├── charts/│ │ ├── filters/│ │ └── layout/│ ├── stores/│ ├── hooks/│ ├── pages/│ └── types/└── tests/Configuration
Section titled “Configuration”project: name: analytics-dashboard languages: - typescript source_paths: - src/
annotations: prefix: "@" comment_syntax: typescript: "//"
check: ignore_patterns: - "src/**/*.test.tsx" - "src/**/*.stories.tsx"Identity document
Section titled “Identity document”---project: analytics-dashboardversion: "1.0"---
## What is this
Analytics Dashboard is a React SPA for visualizing business metrics.Built with React 18, TypeScript, Zustand, and Recharts.
## Architecture
Component-based architecture with three layers:- Pages: route-level containers that compose components.- Components: reusable UI elements, either presentational or connected.- Stores: Zustand stores for client-side state (filters, preferences, cache).
Data flows one-way: API -> store -> component -> render.Server state managed by TanStack Query. Client state in Zustand.
## Conventions
- Components are functional with hooks. No class components.- All interactive elements must meet WCAG 2.1 AA.- Component files export a single default component.- Styles use CSS Modules with .module.css files.- Types are co-located in the component directory or in src/types/ for shared types.Dashboard layout spec
Section titled “Dashboard layout spec”---id: SPEC-010title: Dashboard layoutdescription: Responsive grid layout with sidebar navigation, header, and content area.status: approveddecisions: []norms: - NORM-A11Y-001 - NORM-UI-001paths: - src/components/layout/ - src/pages/DashboardPage.tsx---
## Objective
Provide a responsive layout shell that adapts to screen size and supportskeyboard navigation between major regions.
## Behaviors
WHEN the viewport width is 1024px or greaterTHEN the sidebar is visible and the content area uses a 12-column grid.
WHEN the viewport width is below 1024pxTHEN the sidebar collapses to a hamburger menu and the content area is single column.
WHEN the user presses TabTHEN focus moves through landmark regions in order: skip-link, navigation, main content.
WHEN the sidebar navigation item is activated (click or Enter)THEN the corresponding page loads and the navigation item shows active state.State management decision
Section titled “State management decision”---id: DEC-010title: Zustand over Redux for client statestatus: accepteddate: 2025-09-15specs: - SPEC-012---
## Context
Need client-side state management for filter selections, user preferences,and UI state (sidebar open/closed, active tab). Server state is handled byTanStack Query separately.
## Decision
Use Zustand for all client-side state. Create one store per domain (filters,preferences, ui) rather than a single global store.
## Alternatives considered
- **Redux Toolkit**: More boilerplate. Overkill for simple client state when server state is already handled by TanStack Query.- **React Context + useReducer**: Causes unnecessary re-renders without manual optimization. Zustand's selector pattern avoids this.- **Jotai**: Good for atomic state but less natural for grouped state like filter combinations.
## Consequences
- Minimal boilerplate per store.- Components subscribe to specific slices, avoiding re-render cascading.- Team needs to understand Zustand's selector pattern.Accessibility norm
Section titled “Accessibility norm”---id: NORM-A11Y-001title: Accessibility requirementsstatus: activepaths: - src/components/** - src/pages/**---
## Rules
1. All interactive elements MUST be operable with keyboard alone.2. All images MUST have alt text. Decorative images use `alt=""`.3. Color MUST NOT be the sole means of conveying information. Use icons or text labels alongside color indicators.4. Focus indicators MUST be visible. Never apply `outline: none` without a custom visible focus style.5. ARIA roles MUST be used when semantic HTML elements are insufficient. Prefer semantic HTML first.6. All form inputs MUST have associated labels (visible or `aria-label`).7. Dynamic content updates MUST use `aria-live` regions to announce changes to screen readers.8. Minimum contrast ratio: 4.5:1 for normal text, 3:1 for large text (WCAG AA).Annotated source code
Section titled “Annotated source code”Filter panel component
Section titled “Filter panel component”// @spec SPEC-012// @norm NORM-A11Y-001// @decision DEC-010import { useFilterStore } from '../../stores/filterStore';
interface FilterPanelProps { onApply: () => void;}
export default function FilterPanel({ onApply }: FilterPanelProps) { const { dateRange, setDateRange, metrics, toggleMetric } = useFilterStore( (state) => ({ dateRange: state.dateRange, setDateRange: state.setDateRange, metrics: state.selectedMetrics, toggleMetric: state.toggleMetric, }) );
return ( <aside role="complementary" aria-label="Dashboard filters"> <fieldset> <legend>Date Range</legend> <label htmlFor="start-date">Start</label> <input id="start-date" type="date" value={dateRange.start} onChange={(e) => setDateRange({ ...dateRange, start: e.target.value })} /> <label htmlFor="end-date">End</label> <input id="end-date" type="date" value={dateRange.end} onChange={(e) => setDateRange({ ...dateRange, end: e.target.value })} /> </fieldset>
<fieldset> <legend>Metrics</legend> {['revenue', 'users', 'sessions', 'conversion'].map((metric) => ( <label key={metric}> <input type="checkbox" checked={metrics.includes(metric)} onChange={() => toggleMetric(metric)} /> {metric.charAt(0).toUpperCase() + metric.slice(1)} </label> ))} </fieldset>
<button type="button" onClick={onApply}> Apply Filters </button> </aside> );}Zustand filter store
Section titled “Zustand filter store”// @spec SPEC-012// @decision DEC-010import { create } from 'zustand';
interface DateRange { start: string; end: string;}
interface FilterState { dateRange: DateRange; selectedMetrics: string[]; setDateRange: (range: DateRange) => void; toggleMetric: (metric: string) => void; resetFilters: () => void;}
const defaultState = { dateRange: { start: '', end: '' }, selectedMetrics: ['revenue', 'users'],};
export const useFilterStore = create<FilterState>((set) => ({ ...defaultState, setDateRange: (range) => set({ dateRange: range }), toggleMetric: (metric) => set((state) => ({ selectedMetrics: state.selectedMetrics.includes(metric) ? state.selectedMetrics.filter((m) => m !== metric) : [...state.selectedMetrics, metric], })), resetFilters: () => set(defaultState),}));Layout component
Section titled “Layout component”// @spec SPEC-010// @norm NORM-A11Y-001import { Outlet } from 'react-router-dom';import Sidebar from './Sidebar';import Header from './Header';import styles from './DashboardLayout.module.css';
export default function DashboardLayout() { return ( <div className={styles.container}> <a href="#main-content" className={styles.skipLink}> Skip to main content </a> <Header /> <div className={styles.body}> <Sidebar /> <main id="main-content" role="main" tabIndex={-1}> <Outlet /> </main> </div> </div> );}Context assembly for a task
Section titled “Context assembly for a task”When an agent receives a task to add a new chart type to the dashboard:
contextia context TASK-020The agent gets:
- TASK-020 (the work order)
- SPEC-011 (chart component behaviors)
- DEC-011 (why Recharts was chosen)
- NORM-A11Y-001 (accessibility rules for the chart)
- NORM-UI-001 (component patterns to follow)
With this context, the agent knows to use Recharts (not D3), follow the existing component pattern, and ensure the chart is keyboard-accessible with proper ARIA labels.
Next steps
Section titled “Next steps”- See the Python REST API example for a backend project.
- Explore the Monorepo Setup example for multi-package projects.
- Read the Annotation Reference for TSX-specific details.