Patterns
Reusable Code Patterns - CleanTextLab
Last Updated: January 5, 2026 Purpose: Document common patterns used throughout the codebase Audience: Developers (human or AI) working on CleanTextLab
Table of Contents
- Tool Component Pattern
- Custom Hooks Pattern
- Keyboard Shortcuts Pattern
- Share/Export Pattern
- Magic Input Pattern
- Workflow Builder Pattern
- Theme System Pattern
- Analytics Tracking Pattern
- Error Handling Pattern
- Pro Feature Gating Pattern
- UI Density (Compact Mode) Pattern
- Mobile Options Sheet Pattern
Tool Component Pattern
Use Case
Every tool in CleanTextLab follows a standardized component structure for consistency, maintainability, and UX uniformity.
Full Pattern
"use client";
import { useState, useMemo, useCallback } from "react";
import { Copy, Check, Download, Share2, History, Undo2, Redo2 } from "lucide-react";
import { useUndoRedo } from "@/hooks/useUndoRedo";
import { useToolHistory } from "@/hooks/useToolHistory";
import { useShareableTool } from "@/hooks/useShareableTool";
import { trackToolUse } from "@/lib/analytics";
const TOOL_SLUG = "example-tool";
const TOOL_NAME = "Example Tool";
interface ProcessingOptions {
option1: boolean;
option2: string;
}
export function ExampleTool() {
// 1. State Management with Undo/Redo
const { state: input, setState: setInput, undo, redo, canUndo, canRedo } = useUndoRedo("");
// 2. Tool-Specific Options
const [options, setOptions] = useState<ProcessingOptions>({
option1: true,
option2: "default",
});
// 3. UI State
const [copied, setCopied] = useState(false);
const [toastMessage, setToastMessage] = useState("");
// 4. Processing Logic (Pure Function)
const output = useMemo(() => {
if (!input) return "";
try {
// Tool-specific processing logic here
let result = input;
if (options.option1) {
result = result.trim();
}
// Track usage
trackToolUse(TOOL_SLUG, {
inputLength: input.length,
...options
});
return result;
} catch (error) {
console.error(`[${TOOL_NAME}] Processing error:`, error);
return "";
}
}, [input, options]);
// 5. History Tracking
const { history, clearHistory } = useToolHistory(
TOOL_SLUG,
() => ({
input,
output,
metadata: options,
}),
[input, output, options]
);
// 6. Share Functionality
const { handleShare, shareStatus } = useShareableTool({
toolSlug: TOOL_SLUG,
sharePayload: output || input,
});
// 7. Action Handlers
const handleCopy = useCallback(async () => {
if (!output) return;
try {
await navigator.clipboard.writeText(output);
setCopied(true);
setToastMessage("Copied to clipboard");
setTimeout(() => setCopied(false), 2000);
} catch (error) {
console.error("Copy failed:", error);
setToastMessage("Failed to copy");
}
}, [output]);
const handleDownload = useCallback(() => {
if (!output) return;
const blob = new Blob([output], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${TOOL_SLUG}-output.txt`;
a.click();
URL.revokeObjectURL(url);
}, [output]);
const handleUndo = useCallback(() => {
if (canUndo) {
undo();
setToastMessage("Undone");
}
}, [canUndo, undo]);
const handleRedo = useCallback(() => {
if (canRedo) {
redo();
setToastMessage("Redone");
}
}, [canRedo, redo]);
// 8. Render
return (
<div className="mx-auto w-full max-w-5xl space-y-6 p-4">
{/* Toast Notification */}
{toastMessage && (
<div className="fixed bottom-4 right-4 z-50 rounded-lg bg-green-600 px-4 py-2 text-white">
{toastMessage}
</div>
)}
{/* Input Section */}
<div className="space-y-3">
<label className="block text-sm font-medium">Input</label>
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Paste your text here..."
className="h-64 w-full rounded-lg border p-4 font-mono text-sm"
/>
{/* Options */}
<div className="flex items-center gap-4">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={options.option1}
onChange={(e) => setOptions({ ...options, option1: e.target.checked })}
/>
<span className="text-sm">Option 1</span>
</label>
</div>
{/* Undo/Redo Buttons */}
<div className="flex gap-2">
<button
onClick={handleUndo}
disabled={!canUndo}
className="inline-flex h-9 items-center gap-2 rounded-lg border px-3 disabled:opacity-50"
>
<Undo2 className="h-4 w-4" />
Undo
</button>
<button
onClick={handleRedo}
disabled={!canRedo}
className="inline-flex h-9 items-center gap-2 rounded-lg border px-3 disabled:opacity-50"
>
<Redo2 className="h-4 w-4" />
Redo
</button>
</div>
</div>
{/* Output Section */}
{output && (
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="block text-sm font-medium">Output</label>
{/* Action Buttons */}
<div className="flex gap-2">
<button
onClick={handleCopy}
className="inline-flex h-9 items-center gap-2 rounded-lg bg-blue-600 px-4 text-white"
>
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
{copied ? "Copied" : "Copy"}
</button>
<button
onClick={handleDownload}
className="inline-flex h-9 items-center gap-2 rounded-lg border px-3"
>
<Download className="h-4 w-4" />
</button>
<button
onClick={handleShare}
className="inline-flex h-9 items-center gap-2 rounded-lg border px-3"
>
<Share2 className="h-4 w-4" />
</button>
</div>
</div>
<textarea
value={output}
readOnly
className="h-64 w-full rounded-lg border bg-gray-50 p-4 font-mono text-sm"
/>
</div>
)}
</div>
);
}
When to Use
- Always when creating a new tool
- When refactoring existing tools for consistency
Common Pitfalls
- ❌ Using
useStateinstead ofuseUndoRedofor main input state - ❌ Forgetting to memoize
outputcalculation (causes re-renders) - ❌ Not tracking analytics in processing logic
- ❌ Hardcoding tool slug/name (use constants)
Related Patterns
Custom Hooks Pattern
1. useUndoRedo Hook
Purpose: Provide undo/redo functionality with keyboard shortcuts for any stateful component.
Full Code:
"use client";
import { useState, useCallback, useEffect } from "react";
interface UndoRedoState<T> {
past: T[];
present: T;
future: T[];
}
interface UseUndoRedoReturn<T> {
state: T;
setState: (newState: T | ((prev: T) => T)) => void;
undo: () => void;
redo: () => void;
canUndo: boolean;
canRedo: boolean;
reset: (initialState: T) => void;
}
export function useUndoRedo<T>(
initialState: T,
maxHistory: number = 50
): UseUndoRedoReturn<T> {
const [history, setHistory] = useState<UndoRedoState<T>>({
past: [],
present: initialState,
future: [],
});
const setState = useCallback(
(newState: T | ((prev: T) => T)) => {
setHistory((currentHistory) => {
const resolvedState =
typeof newState === "function"
? (newState as (prev: T) => T)(currentHistory.present)
: newState;
// Deduplicate: Don't add to history if state hasn't changed
if (JSON.stringify(resolvedState) === JSON.stringify(currentHistory.present)) {
return currentHistory;
}
const newPast = [...currentHistory.past, currentHistory.present];
// Limit history stack size
if (newPast.length > maxHistory) {
newPast.shift();
}
return {
past: newPast,
present: resolvedState,
future: [], // Clear future on new state
};
});
},
[maxHistory]
);
const undo = useCallback(() => {
setHistory((currentHistory) => {
if (currentHistory.past.length === 0) return currentHistory;
const previous = currentHistory.past[currentHistory.past.length - 1];
const newPast = currentHistory.past.slice(0, currentHistory.past.length - 1);
return {
past: newPast,
present: previous,
future: [currentHistory.present, ...currentHistory.future],
};
});
}, []);
const redo = useCallback(() => {
setHistory((currentHistory) => {
if (currentHistory.future.length === 0) return currentHistory;
const next = currentHistory.future[0];
const newFuture = currentHistory.future.slice(1);
return {
past: [...currentHistory.past, currentHistory.present],
present: next,
future: newFuture,
};
});
}, []);
const reset = useCallback((newInitialState: T) => {
setHistory({
past: [],
present: newInitialState,
future: [],
});
}, []);
// Global keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Cmd+Z (undo)
if ((e.metaKey || e.ctrlKey) && e.key === "z" && !e.shiftKey) {
const target = e.target as HTMLElement;
// Don't interfere with input fields
if (target.tagName === "INPUT" && target.getAttribute("type") !== "text") return;
if (target.getAttribute("data-undo-redo-enabled") === "false") return;
e.preventDefault();
undo();
}
// Cmd+Shift+Z (redo)
if ((e.metaKey || e.ctrlKey) && e.key === "z" && e.shiftKey) {
const target = e.target as HTMLElement;
if (target.tagName === "INPUT" && target.getAttribute("type") !== "text") return;
if (target.getAttribute("data-undo-redo-enabled") === "false") return;
e.preventDefault();
redo();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [undo, redo]);
return {
state: history.present,
setState,
undo,
redo,
canUndo: history.past.length > 0,
canRedo: history.future.length > 0,
reset,
};
}
Usage:
const { state: input, setState: setInput, undo, redo, canUndo, canRedo } = useUndoRedo("");
// Update state
setInput("new value");
// Check if undo/redo available
if (canUndo) {
undo();
}
Common Pitfalls:
- ❌ Storing large objects (use maxHistory to limit memory)
- ❌ Not preventing keyboard shortcuts in input fields
- ❌ Forgetting to handle function updates in setState
2. useToolHistory Hook
Purpose: Track tool usage history with localStorage and cloud sync.
Implementation Pattern:
"use client";
import { useState, useEffect, useCallback } from "react";
interface HistoryEntry<T> {
id: string;
timestamp: number;
data: T;
}
export function useToolHistory<T>(
toolSlug: string,
getCurrentState: () => T,
maxEntries: number = 50
) {
const [history, setHistory] = useState<HistoryEntry<T>[]>([]);
// Load history from localStorage on mount
useEffect(() => {
const key = `clt-history-${toolSlug}`;
const stored = localStorage.getItem(key);
if (stored) {
try {
const parsed = JSON.parse(stored);
setHistory(parsed);
} catch (error) {
console.error(`Failed to parse history for ${toolSlug}:`, error);
}
}
}, [toolSlug]);
// Save to history
const saveToHistory = useCallback(() => {
const entry: HistoryEntry<T> = {
id: crypto.randomUUID(),
timestamp: Date.now(),
data: getCurrentState(),
};
setHistory((prev) => {
const updated = [entry, ...prev].slice(0, maxEntries);
// Persist to localStorage
const key = `clt-history-${toolSlug}`;
localStorage.setItem(key, JSON.stringify(updated));
return updated;
});
}, [toolSlug, getCurrentState, maxEntries]);
// Clear all history
const clearHistory = useCallback(() => {
setHistory([]);
const key = `clt-history-${toolSlug}`;
localStorage.removeItem(key);
}, [toolSlug]);
return {
history,
saveToHistory,
clearHistory,
};
}
3. useShareableTool Hook
Purpose: Generate shareable links with compressed state in URL.
Pattern:
"use client";
import { useState, useCallback } from "react";
import { useRouter } from "next/navigation";
interface UseShareableToolParams {
toolSlug: string;
sharePayload: string;
}
export function useShareableTool({ toolSlug, sharePayload }: UseShareableToolParams) {
const [shareStatus, setShareStatus] = useState<"idle" | "copying" | "copied">("idle");
const router = useRouter();
const handleShare = useCallback(async () => {
try {
// Compress payload (base64 + gzip in production)
const encoded = btoa(sharePayload);
// Generate share URL
const shareUrl = `${window.location.origin}/tools/${toolSlug}?share=${encoded}`;
// Copy to clipboard
await navigator.clipboard.writeText(shareUrl);
setShareStatus("copied");
setTimeout(() => setShareStatus("idle"), 2000);
} catch (error) {
console.error("Share failed:", error);
setShareStatus("idle");
}
}, [toolSlug, sharePayload]);
return {
handleShare,
shareStatus,
};
}
Keyboard Shortcuts Pattern
Global Shortcuts Registration
Location: src/hooks/useKeyboardShortcuts.ts
"use client";
import { useEffect } from "react";
interface KeyboardShortcut {
key: string;
ctrlOrCmd?: boolean;
shift?: boolean;
alt?: boolean;
action: () => void;
description?: string;
}
export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]) {
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
for (const shortcut of shortcuts) {
const modifierMatch =
(!shortcut.ctrlOrCmd || (e.metaKey || e.ctrlKey)) &&
(!shortcut.shift || e.shiftKey) &&
(!shortcut.alt || e.altKey);
if (modifierMatch && e.key.toLowerCase() === shortcut.key.toLowerCase()) {
// Don't interfere with inputs
const target = e.target as HTMLElement;
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
return;
}
e.preventDefault();
shortcut.action();
break;
}
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [shortcuts]);
}
Usage:
useKeyboardShortcuts([
{ key: "z", ctrlOrCmd: true, action: undo, description: "Undo" },
{ key: "z", ctrlOrCmd: true, shift: true, action: redo, description: "Redo" },
{ key: "c", ctrlOrCmd: true, action: handleCopy, description: "Copy output" },
{ key: "k", ctrlOrCmd: true, action: handleClear, description: "Clear input" },
]);
Share/Export Pattern
Standard Share/Copy UI
- Prefer
ToolOutputActions(Share + Copy output) for consistent UX and stable Playwright selectors. - If a tool needs a lightweight share menu, render
ShareActionssomewhere visible in the tool (soShareis always present). - Use
ActionToastfor copy/download feedback (it rendersrole="status"for accessibility and testability).
Mobile Options Sheet Pattern
Use Case
Some tools have many secondary settings (toggles, dropdowns) that add visual clutter on mobile. Use a bottom-sheet “Options” dialog on mobile to keep the default screen focused on Input → Output → actions.
Implementation
- Component:
src/components/ToolOptionsSheet.tsx - Behavior:
- Mobile (
md:hidden): renders anOptionsbutton that opens a bottom-sheet (role="dialog",aria-modal="true") - Desktop (
md:block): renders the options inline (no UX change)
- Mobile (
- Test coverage:
e2e/tool-options-sheet.spec.ts
Guideline: only move secondary settings into the sheet. Keep core actions (Copy output / Share) visible.
Multi-Format Export
interface ExportFormat {
format: "txt" | "json" | "csv" | "pdf";
mimeType: string;
extension: string;
}
const EXPORT_FORMATS: Record<string, ExportFormat> = {
txt: { format: "txt", mimeType: "text/plain", extension: "txt" },
json: { format: "json", mimeType: "application/json", extension: "json" },
csv: { format: "csv", mimeType: "text/csv", extension: "csv" },
pdf: { format: "pdf", mimeType: "application/pdf", extension: "pdf" },
};
async function handleExport(
data: string,
format: keyof typeof EXPORT_FORMATS,
filename: string
) {
const exportFormat = EXPORT_FORMATS[format];
let blob: Blob;
if (format === "pdf") {
// PDF generation requires server-side or library
const response = await fetch("/api/v1/export/pdf", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: data }),
});
blob = await response.blob();
} else {
blob = new Blob([data], { type: exportFormat.mimeType });
}
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${filename}.${exportFormat.extension}`;
a.click();
URL.revokeObjectURL(url);
}
Magic Input Pattern
Content Detection & Suggestion
Location: src/lib/magicInput.ts
interface ContentDetectionResult {
type: "json" | "url" | "email" | "phone" | "code" | "text";
confidence: number;
suggestedTool: string;
}
export function detectContentType(input: string): ContentDetectionResult {
const trimmed = input.trim();
// JSON detection
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
try {
JSON.parse(trimmed);
return {
type: "json",
confidence: 1.0,
suggestedTool: "json-formatter"
};
} catch {
return {
type: "text",
confidence: 0.3,
suggestedTool: "json-formatter"
};
}
}
// URL detection
if (/^https?:\/\//i.test(trimmed)) {
return {
type: "url",
confidence: 0.95,
suggestedTool: "sanitize-url"
};
}
// Phone number detection
if (/^[\d\s\-\+\(\)]{10,}$/.test(trimmed)) {
return {
type: "phone",
confidence: 0.8,
suggestedTool: "phone-number-formatter"
};
}
// Base64 detection
if (/^[A-Za-z0-9+/]+=*$/.test(trimmed) && trimmed.length % 4 === 0) {
return {
type: "code",
confidence: 0.7,
suggestedTool: "base64-encode-decode"
};
}
// Default to text
return {
type: "text",
confidence: 0.5,
suggestedTool: "word-counter"
};
}
Workflow Builder Pattern
Tool Chain Execution
Location: src/lib/workflowEngine.ts
interface WorkflowStep {
tool: string;
config: Record<string, any>;
}
interface WorkflowResult {
success: boolean;
output: string;
steps: {
tool: string;
input: string;
output: string;
duration: number;
}[];
error?: string;
}
export async function executeWorkflow(
input: string,
steps: WorkflowStep[]
): Promise<WorkflowResult> {
const result: WorkflowResult = {
success: true,
output: input,
steps: [],
};
let currentOutput = input;
for (const step of steps) {
const startTime = performance.now();
try {
// Import tool processor
const processor = await import(`./tools/${step.tool}`);
// Execute step
const stepOutput = processor.process(currentOutput, step.config);
const duration = performance.now() - startTime;
result.steps.push({
tool: step.tool,
input: currentOutput,
output: stepOutput,
duration,
});
currentOutput = stepOutput;
} catch (error) {
result.success = false;
result.error = `Step "${step.tool}" failed: ${error}`;
break;
}
}
result.output = currentOutput;
return result;
}
Theme System Pattern
CSS Variables + Data Attributes
Location: src/app/globals.css
/* Light theme (default) */
:root {
--bg: #f8fafc;
--surface: #ffffff;
--surface-muted: #f4f4f5;
--border: #e4e4e7;
--text-primary: #0b0b0e;
--text-secondary: #4b5563;
--accent: #0f172a;
--accent-contrast: #ffffff;
}
/* Dark theme */
[data-theme="dark"] {
color-scheme: dark;
--bg: #0b1220;
--surface: #0f172a;
--surface-muted: #111827;
--border: #1f2937;
--text-primary: #f3f4f6;
--text-secondary: #cbd5e1;
--accent: #e5e7eb;
--accent-contrast: #0b1220;
}
/* Additional themes: warm/midnight/ocean/rose/forest/amber/slate/neon/sunset */
Theme Switcher Pattern:
"use client";
import { getUserPreferences, updatePreferences } from "@/lib/userPreferences";
export function ThemeSwitcher() {
const [theme, setTheme] = useState(() => {
const prefs = getUserPreferences();
return prefs.theme;
});
const handleThemeChange = (newTheme: string) => {
setTheme(newTheme);
updatePreferences({ theme: newTheme as any });
};
return (
<select value={theme} onChange={(e) => handleThemeChange(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">System</option>
<option value="ocean">Ocean</option>
{/* 8 more themes... */}
</select>
);
}
UI Density (Compact Mode) Pattern
Use Case
Users can opt into a denser layout (“Compact Mode”) without breaking tap targets or causing layout shift.
Implementation
- Preference:
compactModestored inclt_preferences(src/lib/userPreferences.ts) - Applied to the document root:
data-compact="true"/"false"- Initialized early (before first paint) in
src/app/layout.tsx - Kept in sync client-side in
src/components/PreferencesInitializer.tsx
- Initialized early (before first paint) in
- CSS: token-based spacing and typography in
src/app/globals.css(--page-px,--layout-gap,--card-padding, etc.)
Guideline: compact mode reduces whitespace, but should not reduce mobile tap targets below 44px.
Analytics Tracking Pattern
Client-Side Event Tracking
Location: src/lib/analytics/client.ts
interface ToolUsageEvent {
toolSlug: string;
action: "process" | "copy" | "download" | "share";
metadata?: Record<string, any>;
}
export function trackToolUsage(
toolSlug: string,
metadata?: Record<string, any>
) {
// Google Analytics 4
if (typeof window !== "undefined" && window.gtag) {
window.gtag("event", "tool_usage", {
tool_name: toolSlug,
...metadata,
});
}
// Custom analytics (optional)
fetch("/api/v1/analytics/track", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
event: "tool_usage",
toolSlug,
metadata,
timestamp: Date.now(),
}),
}).catch(() => {
// Fail silently
});
}
export function trackKeyboardShortcut(toolSlug: string, shortcut: string) {
if (typeof window !== "undefined" && window.gtag) {
window.gtag("event", "keyboard_shortcut", {
tool_name: toolSlug,
shortcut,
});
}
}
Error Handling Pattern
Try-Catch with User Feedback
async function handleToolProcessing(input: string) {
try {
// Validate input
if (!input || input.trim().length === 0) {
throw new Error("Input cannot be empty");
}
// Process
const result = await processInput(input);
// Track success
trackToolUsage(TOOL_SLUG, { success: true });
return result;
} catch (error) {
// Log error
console.error(`[${TOOL_SLUG}] Processing failed:`, error);
// Track failure
trackToolUsage(TOOL_SLUG, {
success: false,
error: error instanceof Error ? error.message : "Unknown error"
});
// User-friendly error message
if (error instanceof SyntaxError) {
setToastMessage("Invalid input format");
} else if (error instanceof Error) {
setToastMessage(error.message);
} else {
setToastMessage("An unexpected error occurred");
}
return "";
}
}
Pro Feature Gating Pattern
Client-Side Entitlement Check
Location: src/lib/entitlements.ts
interface UserEntitlements {
isPro: boolean;
apiAccess: boolean;
batchProcessing: boolean;
cloudSync: boolean;
features: {
maxLines: number;
maxApiCalls: number;
};
}
export async function getUserEntitlements(): Promise<UserEntitlements> {
const { userId } = auth();
if (!userId) {
return {
isPro: false,
apiAccess: false,
batchProcessing: false,
cloudSync: false,
features: {
maxLines: 100,
maxApiCalls: 0,
},
};
}
// Check Stripe subscription via Clerk metadata
const user = await currentUser();
const metadata = user?.publicMetadata as { stripePlan?: string };
return {
isPro: metadata?.stripePlan === "pro",
apiAccess: metadata?.stripePlan === "pro",
batchProcessing: metadata?.stripePlan === "pro",
cloudSync: metadata?.stripePlan === "pro",
features: {
maxLines: metadata?.stripePlan === "pro" ? 5000 : 100,
maxApiCalls: metadata?.stripePlan === "pro" ? 5000 : 0,
},
};
}
Usage in Component:
export function ExampleTool() {
const [entitlements, setEntitlements] = useState<UserEntitlements | null>(null);
useEffect(() => {
getUserEntitlements().then(setEntitlements);
}, []);
const handleProcess = () => {
if (!entitlements) return;
const lineCount = input.split("\n").length;
if (lineCount > entitlements.features.maxLines) {
setToastMessage(
`Free tier limited to ${entitlements.features.maxLines} lines. Upgrade to Pro for 5,000 lines.`
);
return;
}
// Process input...
};
return (
<div>
{/* Show upgrade prompt if needed */}
{entitlements && !entitlements.isPro && (
<div className="rounded-lg border border-yellow-300 bg-yellow-50 p-4">
<p>Free tier: {entitlements.features.maxLines} lines max</p>
<a href="/pricing" className="text-blue-600 underline">
Upgrade to Pro →
</a>
</div>
)}
{/* Tool UI... */}
</div>
);
}
Summary Table
| Pattern | Use Case | Key Files | Critical? |
|---|---|---|---|
| Tool Component | All tools | src/components/tools/*.tsx | ✅ Yes |
| useUndoRedo | Text input tools | src/hooks/useUndoRedo.ts | ✅ Yes |
| useToolHistory | History tracking | src/hooks/useToolHistory.ts | ⚠️ Recommended |
| useShareableTool | Share links | src/hooks/useShareableTool.ts | ⚠️ Recommended |
| Keyboard Shortcuts | All interactive tools | src/hooks/useKeyboardShortcuts.ts | ✅ Yes |
| Share/Export | Download features | Various tool components | ⚠️ Recommended |
| Magic Input | Homepage | src/lib/magicInput.ts | ⚠️ Optional |
| Workflow Builder | Automation | src/lib/workflowEngine.ts | ✅ Yes |
| Theme System | Global styling | src/app/globals.css | ✅ Yes |
| UI Density | Compact mode | src/app/globals.css | ⚠️ Recommended |
| Analytics | Usage tracking | src/lib/analytics/client.ts | ⚠️ Recommended |
| Error Handling | All async operations | All components | ✅ Yes |
| Pro Feature Gating | Monetization | src/lib/entitlements.ts | ✅ Yes |
| Mobile Options Sheet | Reduce mobile clutter | src/components/ToolOptionsSheet.tsx | ⚠️ Optional |
Document Owner: Tyson K. Last Updated: December 14, 2025 Next Review: March 1, 2026