Step-by-step guide for adding a new feature page to your app.
Every feature requires exactly 3 touches:
- Create the page --
src/pages/features/MyFeaturePage.jsx - Register the route -- Add one line to
src/data/routeManifest.js - Register the metadata -- Add one entry to
src/data/registry.js
Routing, the home page, and search update automatically.
// src/pages/features/MyFeaturePage.jsx
import FeatureLayout from '../../components/layout/FeatureLayout';
import Card from '../../components/shared/Card';
import Button from '../../components/shared/Button';
export default function MyFeaturePage() {
return (
<FeatureLayout title="My Feature" description="A brief description of what this does.">
<Card title="Input">
{/* Your feature UI here */}
</Card>
</FeatureLayout>
);
}If the feature has logic, extract it to a hook:
// src/hooks/useMyFeature.js
import { useState, useCallback } from 'react';
import { useLocalStorage } from './useLocalStorage';
export function useMyFeature() {
const [input, setInput] = useState('');
const [output, setOutput] = useState('');
const [history, setHistory] = useLocalStorage('myapp:my-feature:history', []);
const process = useCallback(() => {
const result = /* your logic */;
setOutput(result);
setHistory(h => [{ input, output: result, timestamp: Date.now() }, ...h].slice(0, 50));
}, [input, setHistory]);
return { input, setInput, output, process, history };
}// src/data/routeManifest.js -- add one line
{ id: 'my-feature', path: '/my-feature', component: lazy(() => import('../pages/features/MyFeaturePage')) },// src/data/registry.js -- add one entry
{
id: 'my-feature',
name: 'My Feature',
description: 'A brief description for search and display',
icon: Wrench, // from lucide-react
category: 'utilities',
tags: ['keyword1', 'keyword2'],
status: 'stable', // 'stable', 'beta', or 'planned'
},Before considering your feature complete:
- Page uses
FeatureLayoutwrapper - Logic is in a custom hook, not in the page component
- Uses shared components (Button, Card, Input, etc.) -- check
src/components/shared/ - No imports from other feature pages
- Added to
routeManifest.js - Added to
registry.jswith icon, description, category, tags
- Works at 320px width (no horizontal scroll)
- All touch targets are 44x44px minimum
- Keyboard navigable (Tab through all interactive elements)
- Loading states for async operations
- Error states with clear messages
- No layout-pushing conditionals (use overlays, toasts, or reserved space)
- Inputs are full-width (
w-full min-w-0) - Content wraps properly (
flex-wrap) - Date/time inputs use
appearance-none min-h-[44px]
- Page is lazy-loaded (via routeManifest, not eager import)
- Debounced inputs for real-time processing (300ms default)
- Web Worker for operations >50ms
- localStorage keys namespaced (
myapp:feature:key) - No data sent to external servers (unless the feature explicitly requires it)
- Sensitive inputs sanitized (DOMPurify for HTML rendering)
- Use
SplitPanefor side-by-side input/output - Use
ModeTogglefor encode/decode switching - Use
CopyButtonon the output - Debounce real-time transforms
- Use
FileProcessorLayoutfor the full file handling pattern - Use
FileUploadfor drag-and-drop input - Use
DownloadButtonfor output files - Validate file type and size with
useFileUpload
- Use
Toggle,Select,RadioGroupfor options - Persist all settings with
useLocalStorage - Use
PresetButtonsfor quick presets
- Use
Cardfor content sections - Use
KeyValueListfor key-value pairs - Use
TabGroupfor multiple views of the same data
If your feature produces output that other features could consume, you can make it chainable:
- Create a core file at
src/core/tools/{feature-id}.js - Define the processing function:
import { registerCore } from '../registry.js'; import { TYPES, ok } from '../types.js'; function process(input, params = {}) { const result = /* your transformation */; return ok(result, TYPES.TEXT); } registerCore({ id: 'my-feature', name: 'My Feature', process, acceptsTypes: [TYPES.TEXT], outputType: TYPES.TEXT, });
- Use
ChainableOutputin your page instead ofCopyButton:<ChainableOutput text={result} />
- Done. The type graph auto-discovers the new core. Suggestions, compatibility, and pipeline validation update automatically.
Read more: Chaining & Pipelines