Skip to content

Latest commit

 

History

History
178 lines (137 loc) · 5.12 KB

File metadata and controls

178 lines (137 loc) · 5.12 KB

Adding a Feature

Step-by-step guide for adding a new feature page to your app.


The 3-File Minimum

Every feature requires exactly 3 touches:

  1. Create the page -- src/pages/features/MyFeaturePage.jsx
  2. Register the route -- Add one line to src/data/routeManifest.js
  3. Register the metadata -- Add one entry to src/data/registry.js

Routing, the home page, and search update automatically.


Step 1: Create the Page

// 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 };
}

Step 2: Register the Route

// src/data/routeManifest.js -- add one line
{ id: 'my-feature', path: '/my-feature', component: lazy(() => import('../pages/features/MyFeaturePage')) },

Step 3: Register the Metadata

// 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'
},

Pre-Build Checklist

Before considering your feature complete:

Code Structure

  • Page uses FeatureLayout wrapper
  • 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

Registry

  • Added to routeManifest.js
  • Added to registry.js with icon, description, category, tags

UX / Accessibility

  • 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)

Mobile

  • Inputs are full-width (w-full min-w-0)
  • Content wraps properly (flex-wrap)
  • Date/time inputs use appearance-none min-h-[44px]

Performance

  • Page is lazy-loaded (via routeManifest, not eager import)
  • Debounced inputs for real-time processing (300ms default)
  • Web Worker for operations >50ms

Data / Privacy

  • 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)

Feature Patterns by Type

Text Transform (Input -> Output)

  • Use SplitPane for side-by-side input/output
  • Use ModeToggle for encode/decode switching
  • Use CopyButton on the output
  • Debounce real-time transforms

File Processor (File -> Result)

  • Use FileProcessorLayout for the full file handling pattern
  • Use FileUpload for drag-and-drop input
  • Use DownloadButton for output files
  • Validate file type and size with useFileUpload

Settings / Configuration

  • Use Toggle, Select, RadioGroup for options
  • Persist all settings with useLocalStorage
  • Use PresetButtons for quick presets

Data Display

  • Use Card for content sections
  • Use KeyValueList for key-value pairs
  • Use TabGroup for multiple views of the same data

Making Your Feature Chainable (Optional)

If your feature produces output that other features could consume, you can make it chainable:

  1. Create a core file at src/core/tools/{feature-id}.js
  2. 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,
    });
  3. Use ChainableOutput in your page instead of CopyButton:
    <ChainableOutput text={result} />
  4. Done. The type graph auto-discovers the new core. Suggestions, compatibility, and pipeline validation update automatically.

Read more: Chaining & Pipelines