diff --git a/.gitignore b/.gitignore index b0558aa..f3278e2 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ runtime.js .task/* /package.json .worktrees/ +.superpowers +docs/plans/ \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 560a540..e53d07c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,16 +3,17 @@ Here are principles and guidelines for developing tools and agents in this proje - A short, practical reference for contributors and automation agents. - Focus: consistent UI, maintainable backend patterns, and a simple development workflow. - - Use the Carbon Design System and Carbon tokens for colors and theming. Avoid hardcoded color values. - - Default visual tone is dark; follow the project's theme provider. - - Prefer small, reusable components to maintain consistency. + - Use **Tailwind CSS 4.0** and **Radix UI** for component development. + - Follow the **high-density, native-feeling desktop UI** guidelines. + - Default visual tone is dark; use the centralized CSS variables in `globals.css`. + - Prefer small, reusable components in `src/components/ui/` (Radix primitives) and `src/components/inputs/` (shared tool components). ## Tool layout - Tools must present three areas: - Header (title + short purpose) - Controls (options + actions) - Workspace (content panes). -- Optional: Workspace commonly uses a split layout; provide a way for users to switch orientations and persist their preference. Please think first before deciding and confirm in case of doubt. +- Optional: Workspace commonly uses a split layout; provide a way for users to switch orientations and persist their preference. - Controls should be clearly separated from utility options. - Front-end code must be organized into components and helpers that reflect the UI structure, with clear naming and separation of concerns. @@ -20,9 +21,11 @@ Here are principles and guidelines for developing tools and agents in this proje - Buttons: group logically, use consistent visual hierarchy (primary vs secondary). - Input/output panes: visually identical, monospace for data/code, equal heights, visible borders, and accessible labels. - Copy actions: make copy/export controls discoverable and consistently placed near pane headers. +- Use **Lucide Icons** for all iconography. ## Reuse & consistency -- Centralize shared UI patterns into common helpers/components. +- Centralize shared UI patterns into common helpers/components in `src/components/inputs/` and `src/components/layout/`. +- Use the `cn()` utility (from `src/utils/cn.js`) for merging Tailwind classes. - Prefer composition over duplication—reuse helpers rather than reimplementing layout/controls. - Keep styles and tokens centralized so changes propagate cleanly. @@ -38,7 +41,7 @@ Here are principles and guidelines for developing tools and agents in this proje - Run formatting and vetting tools as part of local checks before committing. ## Developer workflow (summary) -- Keep local setup lightweight: install frontend and backend deps, run the dev server, iterate. +- Keep local setup lightweight: install frontend and backend deps, run the dev server (`npm run dev`), iterate. - Use centralized scripts for linting and formatting. - Run the app locally to verify UI consistency and interactions. diff --git a/color-converter-debug.html b/color-converter-debug.html new file mode 100644 index 0000000..0fe417e --- /dev/null +++ b/color-converter-debug.html @@ -0,0 +1 @@ +uid=4_0 RootWebArea "DevToolbox" url="http://localhost:9245/tool/color-converter" diff --git a/docs/plans/2025-01-27-number-converter-implementation.md b/docs/plans/2025-01-27-number-converter-implementation.md deleted file mode 100644 index 4f99bab..0000000 --- a/docs/plans/2025-01-27-number-converter-implementation.md +++ /dev/null @@ -1,485 +0,0 @@ -# Number Converter Implementation Plan - -**Design Document:** `docs/plans/2025-01-27-number-converter-redesign.md` -**Goal:** Implement visual bit editor with parallel workstreams - ---- - -## Workstream Overview - -This implementation is divided into **7 parallel workstreams**. Each can be developed independently and tested in isolation before integration. - -``` -┌─────────────────────────────────────────────────────────────┐ -│ WORKSTREAM 1 WORKSTREAM 2 WORKSTREAM 3 │ -│ State Management Utility Functions BitGrid │ -│ (Reducer) (Conversions) Component │ -│ │ -│ WORKSTREAM 4 WORKSTREAM 5 WORKSTREAM 6 │ -│ ConversionCard BitwiseToolbar Constants │ -│ Component Component & Types │ -│ │ -│ WORKSTREAM 7 │ -│ Main Integration │ -│ & Final Assembly │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## Workstream 1: State Management (Reducer) -**File:** `frontend/src/pages/NumberConverter/numberConverterReducer.js` - -**Deliverables:** -```javascript -// Initial state -const initialState = { - value: 0, - inputMode: 'decimal', - customBase: 36, - errors: {} -}; - -// Action types -const actions = { - SET_VALUE, - TOGGLE_BIT, - SET_INPUT_MODE, - SET_CUSTOM_BASE, - SET_ERROR, - CLEAR_ERROR, - APPLY_BITWISE_OP -}; - -// Reducer function -function numberConverterReducer(state, action) { ... } - -// Action creators -const actionCreators = { - setValue, - toggleBit, - setInputMode, - setCustomBase, - setError, - clearError, - applyBitwiseOp -}; -``` - -**Test in isolation:** -```javascript -// Test cases needed: -- Toggle bit 0 on 0 → should be 1 -- Toggle bit 0 on 1 → should be 0 -- Set value to 255 → all conversions should update -- Set error on hex input → error object populated -- Clear error → error object empty -``` - -**Dependencies:** None (pure logic) - ---- - -## Workstream 2: Utility Functions -**File:** `frontend/src/pages/NumberConverter/utils.js` - -**Deliverables:** -```javascript -// Parsing (string → number) -export function parseInput(input, base); -export function parseHex(input); -export function parseBinary(input); -export function parseOctal(input); -export function parseDecimal(input); -export function parseCustomBase(input, base); - -// Formatting (number → string) -export function formatNumber(value, base); -export function formatHex(value); -export function formatBinary(value); -export function formatOctal(value); -export function formatDecimal(value); -export function formatCustomBase(value, base); - -// Bit manipulation -export function toggleBit(value, position); -export function shiftLeft(value, n); -export function shiftRight(value, n); -export function bitwiseNot(value); -export function bitwiseAnd(value, mask); -export function bitwiseOr(value, mask); - -// Validation -export function validateInput(input, base); -export function sanitizeInput(input); -``` - -**Edge Cases to Handle:** -- Empty string returns null -- Whitespace trimmed -- Case insensitivity for hex -- Overflow clamping to 32-bit -- Invalid characters throw with message - -**Test in isolation:** -```javascript -// Test matrix: -Input | Base | Expected Value -"FF" | 16 | 255 -"1010" | 2 | 10 -"377" | 8 | 255 -"255" | 10 | 255 -" 42 " | 10 | 42 (trimmed) -"GG" | 16 | throw Error -"99999999999" | 10 | 4294967295 (clamped) -"-5" | 10 | throw Error -``` - -**Dependencies:** None (pure logic) - ---- - -## Workstream 3: BitGrid Component -**File:** `frontend/src/pages/NumberConverter/components/BitGrid.jsx` - -**Deliverables:** -```jsx -// BitGrid.jsx -export function BitGrid({ value, onToggleBit, layout }) { - // Display 4 rows × 8 bits - // Row 0: bits 31-24 (MSB) - // Row 1: bits 23-16 - // Row 2: bits 15-8 - // Row 3: bits 7-0 (LSB) -} - -// BitCell.jsx (sub-component) -export function BitCell({ - bitValue, // 0 or 1 - position, // 0-31 - onToggle, // callback(position) - isHovered // hover state from parent -}) { - // Clickable 32×32px cell - // Hover: scale(1.1) - // Active: filled with primary color - // Inactive: outlined -} -``` - -**Props Interface:** -```typescript -interface BitGridProps { - value: number; // 32-bit value - onToggleBit: (position: number) => void; - layout: 'horizontal' | 'vertical'; -} -``` - -**Visual Specs:** -- Cell size: 32×32px -- Gap: 4px -- Active color: `var(--cds-interactive-01)` -- Inactive border: `var(--cds-border-strong)` -- Row label font: monospace, `var(--cds-text-secondary)` - -**Test in isolation:** -```javascript -- Renders 32 cells -- Clicking cell calls onToggleBit with correct position -- Value 0xFF shows bits 0-7 as active -- Value 0xFF000000 shows bits 24-31 as active -- Keyboard navigation works (Tab, Space, Enter) -``` - -**Dependencies:** None (uses only Carbon CSS variables) - ---- - -## Workstream 4: ConversionCard Component -**File:** `frontend/src/pages/NumberConverter/components/ConversionCard.jsx` - -**Deliverables:** -```jsx -// ConversionCard.jsx -export function ConversionCard({ - label, // "Decimal", "Hexadecimal", etc. - base, // 10, 16, 2, 8, or custom - value, // Current numeric value - error, // Error message or null - onChange, // callback(newValue) - parse and update - onCopy, // callback() - copy to clipboard - onSync // callback() - sync from this field -}) { - // TextInput with label - // Copy button - // Sync button (sets this as source) - // Error display -} -``` - -**Props Interface:** -```typescript -interface ConversionCardProps { - label: string; - base: number; - value: number; - error?: string; - onChange: (input: string) => void; - onCopy: () => void; - onSync: () => void; -} -``` - -**Features:** -- Monospace font for binary display -- Copy button uses Carbon `Button` with Copy icon -- Sync button to reverse-sync (input becomes source) -- Inline error display below input -- Placeholder shows example: "Enter decimal number..." - -**Test in isolation:** -```javascript -- Typing valid input calls onChange -- Typing invalid input shows error -- Copy button copies formatted value -- Sync button calls onSync -- Error state shows red border -``` - -**Dependencies:** Carbon `TextInput`, `Button`, `InlineNotification` - ---- - -## Workstream 5: BitwiseToolbar Component -**File:** `frontend/src/pages/NumberConverter/components/BitwiseToolbar.jsx` - -**Deliverables:** -```jsx -// BitwiseToolbar.jsx -export function BitwiseToolbar({ onOperation }) { - // Button group: - // [<< 1] [>> 1] [NOT] [& 0xFF] [| 1] -} - -// Operations supported: -// 'shiftLeft': value << 1 -// 'shiftRight': value >>> 1 (logical) -// 'not': ~value -// 'maskByte': value & 0xFF -// 'setLSB': value | 1 -``` - -**Props Interface:** -```typescript -interface BitwiseToolbarProps { - onOperation: (operation: string) => void; -} -``` - -**Visual Specs:** -- Button group with `kind="secondary"` -- Size: `sm` (small) -- Gap: 8px between buttons -- Tooltip on hover showing operation description - -**Test in isolation:** -```javascript -- Clicking [<< 1] calls onOperation('shiftLeft') -- Clicking [NOT] calls onOperation('not') -- All 5 buttons rendered -- Keyboard accessible (Tab, Enter) -``` - -**Dependencies:** Carbon `Button`, `ButtonSet` - ---- - -## Workstream 6: Constants & Types -**File:** `frontend/src/pages/NumberConverter/constants.js` - -**Deliverables:** -```javascript -// Base configurations -export const BASES = { - BINARY: { id: 'bin', label: 'Binary', base: 2 }, - OCTAL: { id: 'oct', label: 'Octal', base: 8 }, - DECIMAL: { id: 'dec', label: 'Decimal', base: 10 }, - HEXADECIMAL: { id: 'hex', label: 'Hexadecimal', base: 16 }, -}; - -// Custom base options (2-36) -export const CUSTOM_BASE_OPTIONS = Array.from({ length: 35 }, (_, i) => ({ - id: `${i + 2}`, - label: `Base ${i + 2}`, - value: i + 2, -})); - -// Bitwise operations -export const BITWISE_OPERATIONS = { - SHIFT_LEFT: { id: 'shiftLeft', label: '<< 1', description: 'Shift left by 1' }, - SHIFT_RIGHT: { id: 'shiftRight', label: '>> 1', description: 'Shift right by 1' }, - NOT: { id: 'not', label: 'NOT', description: 'Flip all bits' }, - MASK_BYTE: { id: 'maskByte', label: '& 0xFF', description: 'Keep lowest byte' }, - SET_LSB: { id: 'setLSB', label: '| 1', description: 'Set least significant bit' }, -}; - -// Validation messages -export const ERROR_MESSAGES = { - INVALID_CHAR: (char, base) => `Invalid character '${char}' for base ${base}`, - NEGATIVE: 'Negative numbers are not supported', - OVERFLOW: 'Value clamped to 32-bit maximum', - EMPTY: 'Input cannot be empty', -}; - -// Limits -export const MAX_32BIT = 0xFFFFFFFF; // 4,294,967,295 -export const MIN_32BIT = 0; -``` - -**Dependencies:** None - ---- - -## Workstream 7: Main Integration -**File:** `frontend/src/pages/NumberConverter/index.jsx` - -**Deliverables:** -Complete page component integrating all workstreams: - -```jsx -export default function NumberConverter() { - // State - const [state, dispatch] = useReducer(numberConverterReducer, initialState); - const layout = useLayoutToggle({ toolKey: 'number-converter', ... }); - - // Handlers - const handleToggleBit = (position) => dispatch(toggleBit(position)); - const handleConversionInput = (base, input) => { ... }; - const handleBitwiseOp = (operation) => { ... }; - const handleCopy = (value) => navigator.clipboard.writeText(value); - - // Render - return ( -
- - - - -
- - -
- - -
- {Object.values(BASES).map(baseConfig => ( - handleConversionInput(baseConfig.base, input)} - onCopy={() => handleCopy(formatNumber(state.value, baseConfig.base))} - onSync={() => dispatch(setInputMode(baseConfig.id))} - /> - ))} - -
-
-
- ); -} -``` - -**Integration Checklist:** -- [ ] Import all workstream components -- [ ] Wire up reducer and actions -- [ ] Connect layout toggle -- [ ] Handle all user interactions -- [ ] Display errors from state -- [ ] Format values for display -- [ ] Add copy functionality - -**Dependencies:** ALL other workstreams - ---- - -## Implementation Order - -### Phase 1: Foundation (Parallel) -Workstreams 1, 2, and 6 can start immediately and run in parallel: -- **WS1** (Reducer) - No dependencies -- **WS2** (Utils) - No dependencies -- **WS6** (Constants) - No dependencies - -### Phase 2: Components (Parallel) -Once Phase 1 is done, workstreams 3, 4, and 5 can run in parallel: -- **WS3** (BitGrid) - Uses WS2 for bit manipulation -- **WS4** (ConversionCard) - Uses WS2 for formatting -- **WS5** (BitwiseToolbar) - No dependencies - -### Phase 3: Integration (Sequential) -Workstream 7 depends on ALL others: -- **WS7** (Main) - Uses WS1, WS2, WS3, WS4, WS5, WS6 - ---- - -## Testing Strategy - -### Unit Tests (Per Workstream) -Each workstream should include unit tests before integration: - -**WS1 (Reducer):** Test all state transitions -**WS2 (Utils):** Test conversion functions with edge cases -**WS3 (BitGrid):** Test rendering and interactions -**WS4 (ConversionCard):** Test input handling and validation -**WS5 (BitwiseToolbar):** Test button callbacks -**WS6 (Constants):** N/A - just definitions - -### Integration Tests -After WS7, test the complete flow: -- End-to-end conversion scenarios -- Bit manipulation workflows -- Error handling paths -- Accessibility compliance - -### Visual Regression Tests -- Screenshot tests for different values -- Layout toggle states -- Error states -- Dark/light mode - ---- - -## Success Criteria - -✅ All 7 workstreams complete -✅ Unit tests pass for each workstream -✅ Integration tests pass -✅ No console errors -✅ Accessibility audit passes -✅ Visual design matches spec -✅ Performance: <100ms for bit toggle feedback - ---- - -## Notes for Agents - -1. **Work independently** - Each workstream can be developed without blocking others -2. **Use mocks** - If a dependency isn't ready, mock it with the expected interface -3. **Write tests first** - Each workstream should be testable in isolation -4. **Document exports** - Make sure the interface is clear for integration -5. **Follow Carbon** - Use existing Carbon components and CSS variables -6. **Check AGENTS.md** - Follow project conventions for UI patterns - -**Ready to start?** Pick any Phase 1 workstream and begin implementation. diff --git a/docs/plans/2025-01-27-number-converter-redesign.md b/docs/plans/2025-01-27-number-converter-redesign.md deleted file mode 100644 index d8b0bd1..0000000 --- a/docs/plans/2025-01-27-number-converter-redesign.md +++ /dev/null @@ -1,306 +0,0 @@ -# Number Converter Redesign - Visual Bit Editor - -**Date:** 2025-01-27 -**Status:** Design Complete -**Approach:** Visual Bit Editor with Interactive Bitwise Operations - ---- - -## 1. Overview & Architecture - -### Concept -Transform the Number Converter from a form-filling task into an interactive exploration tool. Users interact with a visual 32-bit representation where they can toggle individual bits, perform bitwise operations, and instantly see conversions across all bases. - -### Core Architecture -- **32-bit representation as source of truth** - All conversions derive from a single 32-bit unsigned integer -- **Bidirectional input** - Click bits in the grid OR type into conversion fields; changes sync both ways -- **Visual hierarchy** - Bit grid dominates (4 rows × 8 bits), color-coded by byte significance -- **Simple bitwise operations** - One-click toolbar for common operations - -### Technology Stack -- React with Carbon Design System (existing) -- No new dependencies -- Layout toggle with localStorage persistence - ---- - -## 2. Components & UI Structure - -### Layout -Split-pane with layout toggle (horizontal/vertical), following existing `useLayoutToggle` pattern. - -### Left Pane - Visual Bit Grid (60% width) -**Header:** -- "Bit Pattern" label -- Bit position indicators (31-0) -- Byte hex summaries - -**Grid:** -``` -Row 0 (31-24): ■ □ □ ■ □ □ ■ □ [0x4A] -Row 1 (23-16): □ ■ □ □ ■ □ □ ■ [0x25] -Row 2 (15-8): ■ ■ □ □ □ ■ □ □ [0xC3] -Row 3 (7-0): □ □ ■ ■ □ □ ■ □ [0x32] -``` - -**Bit Cells (32×32px):** -- `1` = filled with `--cds-interactive-01` (primary accent) -- `0` = outlined with `--cds-border-strong` -- Hover: scale(1.1) with shadow -- Click: toggle bit - -**Toolbar (above grid):** -- `<< 1` - Shift left -- `>> 1` - Shift right -- `NOT` - Flip all bits -- `& 0xFF` - Mask to byte -- `| 1` - Set LSB - -### Right Pane - Conversion Cards (40% width) -Stacked cards showing derived values: -1. **Decimal** (largest, most prominent) -2. **Hexadecimal** -3. **Binary** (monospace, wrapped) -4. **Octal** -5. **Custom Base** (dropdown 2-36) - -Each card: label, input field, copy button, "sync" button to reverse-sync - ---- - -## 3. Data Flow & State Management - -### State Shape -```typescript -interface NumberConverterState { - value: number; // 32-bit unsigned integer (0 to 2^32-1) - inputMode: 'decimal' | 'hex' | 'binary' | 'octal' | 'custom'; - customBase: number; // 2-36 - errors: { - decimal?: string; - hex?: string; - binary?: string; - octal?: string; - custom?: string; - }; - layout: 'horizontal' | 'vertical'; -} -``` - -### Data Flow Patterns - -**1. Bit Grid Click:** -``` -User clicks bit at position N -↓ -Calculate: newValue = currentValue ^ (1 << N) -↓ -Update state.value -↓ -Re-render all conversion displays -``` - -**2. Conversion Input:** -``` -User types in Hex input field -↓ -Parse with base 16 -↓ -Valid: Update state.value, clear error -Invalid: Show error inline, keep last valid value -↓ -Bit grid re-renders from new value -``` - -**3. Bitwise Operation:** -``` -User clicks "<< 1" button -↓ -Calculate: newValue = (currentValue << 1) & 0xFFFFFFFF -↓ -Update state.value -↓ -All displays update -``` - -### Performance Optimizations -- `useMemo` for expensive base conversions -- Bit grid uses CSS transforms (GPU-accelerated) -- No unnecessary re-renders - ---- - -## 4. Bitwise Operations - -### Operation Toolbar -Simple buttons above the bit grid: - -| Button | Operation | Example | -|--------|-----------|---------| -| `<< 1` | Shift left by 1 | `0x0F` → `0x1E` | -| `>> 1` | Logical shift right by 1 | `0xF0` → `0x78` | -| `NOT` | Bitwise NOT | `0x0F` → `0xFFFFFFF0` | -| `& 0xFF` | AND with 0xFF | `0xABCD` → `0xCD` | -| `\| 1` | OR with 1 | `0xFE` → `0xFF` | - -### Interaction -- Click button → operation applies immediately -- No preview mode, no operand input -- Simple undo: click opposite operation -- Visual feedback: button press animation - ---- - -## 5. Error Handling & Edge Cases - -### Input Validation Errors - -**Invalid Characters:** -- Hex input with non-hex chars (G-Z) -- Binary input with digits other than 0-1 -- Octal input with digits 8-9 -- Show: "Invalid character 'X' for base Y" - -**Format Errors:** -- Empty string → valid (no change) -- Whitespace-only → trim or error -- Leading/trailing whitespace → auto-trim - -**Range Errors:** -- Value > 2^32-1 (4,294,967,295) → clamp to max -- Value < 0 → error: "Negative numbers not supported" -- Scientific notation → parse or error - -### Bit Manipulation Edge Cases - -**Shift Operations:** -- Shift by 0 → no change -- Shift by 32+ → wraps (JS behavior: `x << 32 === x`) -- Shift negative → error - -**Identity Operations:** -- `x & x` = x -- `x | x` = x -- `x ^ 0` = x -- Handle gracefully - -**Boundary Values:** -- Bit 31 toggle (0x80000000) -- All zeros (0x00000000) -- All ones (0xFFFFFFFF) -- Single bit set (powers of 2) - -### Display Edge Cases - -**Binary Display:** -- Always show 32 bits for consistency -- Group by 4 bits: `0001 0010 0011 0100` -- Wrap in monospace textarea - -**Decimal Display:** -- Large values: show full precision -- Add thousand separators: `4,294,967,295` - -**Custom Base:** -- Base 36: 0-9, A-Z -- Handle uppercase/lowercase consistently - -### Accessibility - -**Keyboard Navigation:** -- Tab through bit cells -- Space/Enter to toggle bit -- Shift+Tab for reverse navigation - -**Screen Readers:** -- Bit cells: `aria-pressed` for state -- Error messages: `aria-live="polite"` -- Labels on all interactive elements - -### Race Conditions & Performance - -**Rapid Interactions:** -- Debounce rapid bit clicks (50ms) -- Prevent double-submission of operations - -**Copy Operations:** -- Handle copy failure gracefully -- Show toast on success/error - -**Layout Changes:** -- Maintain scroll position on layout toggle -- Responsive bit grid sizing - ---- - -## 6. Testing Checklist - -### Functional Tests -- [ ] All base conversions work bidirectionally -- [ ] Bit toggles update all displays instantly -- [ ] Each bitwise operation produces correct result -- [ ] Invalid input shows appropriate error -- [ ] Copy button copies correct value - -### Edge Case Tests -- [ ] Maximum value (0xFFFFFFFF) -- [ ] Zero value -- [ ] Negative input rejected -- [ ] Overflow clamped -- [ ] Whitespace trimmed -- [ ] Case insensitivity (hex) - -### UX Tests -- [ ] Layout toggle persists -- [ ] Keyboard navigation works -- [ ] Touch targets adequate size -- [ ] Hover states visible -- [ ] Error messages clear - -### Accessibility Tests -- [ ] Screen reader announces bit state -- [ ] Focus visible on all interactive elements -- [ ] Color not sole error indicator - ---- - -## 7. Implementation Notes - -### File Structure -``` -frontend/src/pages/NumberConverter/ -├── index.jsx # Main component -├── numberConverterReducer.js # State management -├── utils.js # Conversion utilities -├── constants.js # Base configs -└── components/ - ├── BitGrid.jsx # Visual bit grid - ├── BitCell.jsx # Individual bit toggle - ├── ConversionCard.jsx # Input with copy/sync - ├── BitwiseToolbar.jsx # Operation buttons - └── ByteLabel.jsx # Byte hex display -``` - -### Key Utilities Needed -```javascript -// utils.js -export const parseInput = (input, base) => { ... } -export const formatNumber = (value, base) => { ... } -export const toggleBit = (value, position) => { ... } -export const shiftLeft = (value, n) => { ... } -export const shiftRight = (value, n) => { ... } -export const bitwiseNot = (value) => { ... } -``` - -### Carbon Components Used -- `TextInput` - Conversion inputs -- `Button` - Operations, copy -- `Dropdown` - Custom base selector -- `Grid/Column` - Layout -- `InlineNotification` - Errors - ---- - -## Summary - -This redesign transforms the Number Converter from a passive form into an active exploration tool. The visual bit grid makes binary tangible, bitwise operations are one-click away, and all conversions update in real-time. The design maintains consistency with existing Carbon Design System usage while adding engaging interactivity. diff --git a/docs/plans/2025-03-01-syntax-highlighting-design.md b/docs/plans/2025-03-01-syntax-highlighting-design.md deleted file mode 100644 index 8af2261..0000000 --- a/docs/plans/2025-03-01-syntax-highlighting-design.md +++ /dev/null @@ -1,334 +0,0 @@ -# Syntax Highlighting Design - -**Date:** 2025-03-01 -**Status:** Ready for Implementation -**Scope:** Add syntax highlighting to code display and editing components across the devtoolbox - ---- - -## Overview - -Add syntax highlighting capabilities to the devtoolbox using CodeMirror 6, with per-tool toggle persistence and Carbon Design System theming. - -**Key Principles:** -- Highlighting ON by default (opt-out) -- Per-tool persistence (each tool remembers its own setting) -- Lazy loading of language modules for performance -- Graceful fallback to plain TextArea when disabled or on error - ---- - -## Goals - -1. Replace plain text areas with syntax-highlighted code editors in CodeFormatter -2. Update CodeSnippetsPanel to use highlighted display for all 11 language tabs -3. Add toggle controls for enabling/disabling highlighting per tool -4. Maintain Carbon Design System visual consistency -5. Support 8 languages initially: JSON, JavaScript, HTML, XML, CSS, SQL, Swift, Java - ---- - -## Architecture - -### Components - -Three new components in `/frontend/src/components/inputs/`: - -#### 1. CodeEditor.jsx -Editable code editor with CodeMirror integration. - -```jsx - {}} // Optional callback - readOnly={false} // Edit or view-only mode - highlight={true} // Controlled by tool toggle - placeholder="Paste code..." - className="optional-class" -/> -``` - -**Responsibilities:** -- Load CodeMirror dynamically when highlight=true -- Apply Carbon dark theme (g100) to editor -- Manage editor lifecycle (create/destroy on toggle) -- Fall back to native TextArea when highlight=false - -#### 2. HighlightedCode.jsx -Read-only code display for snippets and output panes. - -```jsx - -``` - -**Replaces:** `
` tags in CodeSnippetsPanel and read-only TextArea in ToolPane
-
-#### 3. EditorToggle.jsx
-Toggle button for ToolControls section.
-
-```jsx
-
-```
-
-**UI:**
-- Icon button with Code icon
-- Tooltip: "Syntax highlighting: On/Off"
-- Persists to localStorage immediately on toggle
-
-### State Management
-
-Each tool manages its own highlighting state:
-
-```javascript
-// localStorage key pattern
-`${toolKey}-editor-highlight`: 'true' | 'false'
-
-// Examples
-'codeFormatter-editor-highlight': 'true'
-'colorConverter-editor-highlight': 'false'
-```
-
-**Default:** ON for new users (opt-out model)
-
-**Persistence:** Immediate write to localStorage on toggle
-
-### Lazy Loading Strategy
-
-Language modules load on-demand via dynamic imports:
-
-```javascript
-const loadLanguage = async (lang) => {
-  const languageModules = {
-    json: () => import('@codemirror/lang-json'),
-    javascript: () => import('@codemirror/lang-javascript'),
-    typescript: () => import('@codemirror/lang-javascript'), // TS uses JS grammar
-    html: () => import('@codemirror/lang-html'),
-    xml: () => import('@codemirror/lang-xml'),
-    css: () => import('@codemirror/lang-css'),
-    sql: () => import('@codemirror/lang-sql'),
-    swift: () => import('@codemirror/legacy-modes/mode/swift'),
-    java: () => import('@codemirror/lang-java'),
-  };
-  
-  const loader = languageModules[lang];
-  if (!loader) return null;
-  
-  const module = await loader();
-  return module[lang === 'swift' ? 'swift' : `${lang}Language`];
-};
-```
-
-**Caching:** Modules cached after first load (browser cache + memory)
-
-### Tools to Enhance
-
-1. **CodeFormatter** (`/pages/CodeFormatter/index.jsx`)
-   - Add EditorToggle to ToolControls
-   - Replace input ToolPane with CodeEditor
-   - Replace output ToolPane with CodeEditor (readOnly)
-
-2. **ColorConverter** (`/pages/ColorConverter/components/CodeSnippetsPanel.jsx`)
-   - Replace `
` with HighlightedCode
-   - All 11 language tabs benefit automatically
-
-3. **JWTDebugger** (if exists)
-   - Add EditorToggle
-   - Highlight header/payload JSON display
-
-4. **TextDiffChecker** (if exists)
-   - Add EditorToggle
-   - Highlight diff output
-
----
-
-## Data Flow
-
-```
-User loads tool
-    ↓
-Load persisted preference from localStorage (default: true)
-    ↓
-highlight=true?
-    ├─ YES → Dynamically import CodeMirror + language module
-    │        ↓
-    │        Mount CodeMirror with Carbon theme
-    │        ↓
-    │        Render highlighted editor
-    │
-    └─ NO  → Render native TextArea (fallback)
-    
-User toggles EditorToggle
-    ↓
-Update state + persist to localStorage
-    ↓
-Re-render: mount/unmount CodeMirror accordingly
-```
-
----
-
-## Error Handling
-
-### CodeMirror Load Failure
-- **Cause:** Network error, CDN unreachable
-- **Behavior:** Fall back to TextArea, show subtle warning icon with tooltip
-- **User message:** "Syntax highlighting unavailable"
-
-### Unsupported Language
-- **Cause:** Language prop not in supported list
-- **Behavior:** Render as plain text (no highlighting)
-- **Dev:** Console warning in development mode
-
-### Large Files (>1MB)
-- **Detection:** Check content length on value change
-- **Behavior:** Auto-disable highlighting, show info banner
-- **User message:** "Large file - highlighting disabled for performance"
-- **Override:** User can manually re-enable via toggle
-
-### Touch Devices
-- **Detection:** `window.matchMedia('(pointer: coarse)')`
-- **Behavior:** Default to OFF on mobile/tablet (better UX with native input)
-- **Override:** User can still enable if desired
-
-### localStorage Corruption
-- **Behavior:** Parse error caught, reset to default (ON)
-- **User impact:** None, silent recovery
-
-### Memory Cleanup
-- **Implementation:** `useEffect` cleanup function destroys CodeMirror instance
-- **Trigger:** Component unmount or toggle OFF
-
----
-
-## Styling (Carbon Integration)
-
-CodeMirror theme maps to Carbon tokens:
-
-```javascript
-const carbonDarkTheme = EditorView.theme({
-  '&': {
-    backgroundColor: 'var(--cds-field)',
-    color: 'var(--cds-text-primary)',
-    fontFamily: "'IBM Plex Mono', monospace",
-    fontSize: '0.875rem',
-  },
-  '.cm-content': {
-    caretColor: 'var(--cds-focus)',
-  },
-  '.cm-cursor': {
-    borderLeftColor: 'var(--cds-focus)',
-  },
-  '.cm-selectionBackground': {
-    backgroundColor: 'var(--cds-highlight)',
-  },
-  // Syntax colors - subtle, accessible
-  '.cm-keyword': { color: 'var(--cds-text-primary)' },
-  '.cm-string': { color: 'var(--cds-support-success)' },
-  '.cm-number': { color: 'var(--cds-support-info)' },
-  '.cm-comment': { color: 'var(--cds-text-secondary)' },
-  // ... etc
-});
-```
-
-**Visual consistency:**
-- Matches Carbon g100 dark theme
-- Uses IBM Plex Mono for code
-- Respects `--cds-*` CSS custom properties
-
----
-
-## Dependencies
-
-### New Packages
-```json
-{
-  "@codemirror/commands": "^6.0.0",
-  "@codemirror/lang-css": "^6.0.0",
-  "@codemirror/lang-html": "^6.0.0",
-  "@codemirror/lang-java": "^6.0.0",
-  "@codemirror/lang-javascript": "^6.0.0",
-  "@codemirror/lang-json": "^6.0.0",
-  "@codemirror/lang-sql": "^6.0.0",
-  "@codemirror/lang-xml": "^6.0.0",
-  "@codemirror/language": "^6.0.0",
-  "@codemirror/legacy-modes": "^6.0.0", // For Swift
-  "@codemirror/state": "^6.0.0",
-  "@codemirror/view": "^6.0.0",
-  "codemirror": "^6.0.0"
-}
-```
-
-**Estimated bundle impact:** ~200-300KB for all 8 languages (tree-shaken, loaded on demand)
-
----
-
-## Testing Checklist
-
-### Manual Testing
-- [ ] Toggle persists across refresh for each tool
-- [ ] Language switching works (CodeFormatter JSON→XML)
-- [ ] Large file (>1MB) auto-disables highlighting
-- [ ] Copy button works in CodeEditor/HighlightedCode
-- [ ] Mobile defaults to OFF
-- [ ] Network failure shows graceful fallback
-
-### Component Tests
-- [ ] CodeEditor renders TextArea when highlight=false
-- [ ] CodeEditor loads CodeMirror when highlight=true
-- [ ] EditorToggle calls onToggle and persists
-- [ ] HighlightedCode displays with correct language
-
-### Bundle Analysis
-- [ ] Verify lazy loading (CodeMirror not in main chunk)
-- [ ] Target: <300KB size increase
-
-### Accessibility
-- [ ] Toggle has aria-label
-- [ ] Contrast ratios meet WCAG 2.1 AA
-- [ ] Keyboard navigation works
-
----
-
-## Migration Path
-
-1. **Phase 1:** Create CodeEditor, HighlightedCode, EditorToggle components
-2. **Phase 2:** Update CodeFormatter (input + output panes)
-3. **Phase 3:** Update CodeSnippetsPanel (all language tabs)
-4. **Phase 4:** Add to JWTDebugger and TextDiffChecker
-5. **Phase 5:** Remove legacy ToolPane usage in favor of CodeEditor
-
----
-
-## Open Questions
-
-1. Should we add TypeScript support explicitly or rely on JavaScript grammar?
-2. Do we want line numbers as a separate toggle or always on/off?
-3. Should the large file threshold be configurable?
-
----
-
-## Appendix: File Locations
-
-**New files:**
-- `/frontend/src/components/inputs/CodeEditor.jsx`
-- `/frontend/src/components/inputs/HighlightedCode.jsx`
-- `/frontend/src/components/inputs/EditorToggle.jsx`
-
-**Modified files:**
-- `/frontend/src/components/inputs/index.js` - Add exports
-- `/frontend/src/pages/CodeFormatter/index.jsx` - Add EditorToggle, use CodeEditor
-- `/frontend/src/pages/ColorConverter/components/CodeSnippetsPanel.jsx` - Use HighlightedCode
-- `/frontend/package.json` - Add CodeMirror dependencies
-
-**Theme file (optional):**
-- `/frontend/src/components/inputs/carbonCodeMirrorTheme.js` - Extracted theme constants
diff --git a/docs/plans/2025-03-03-faster-ci-pipeline.md b/docs/plans/2025-03-03-faster-ci-pipeline.md
deleted file mode 100644
index d9a1150..0000000
--- a/docs/plans/2025-03-03-faster-ci-pipeline.md
+++ /dev/null
@@ -1,949 +0,0 @@
-# Faster CI Pipeline Implementation Plan
-
-> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
-
-**Goal:** Reduce PR check time from ~5-10 minutes to under 2 minutes by adding aggressive caching and frontend test infrastructure.
-
-**Architecture:** Add comprehensive GitHub Actions caching (Go modules, Bun dependencies, Wails CLI binary, APT packages) + Vitest frontend testing setup with utility and component tests. Three independent work streams allow parallel execution by different agents.
-
-**Tech Stack:** GitHub Actions, Vitest, React Testing Library, Bun, Wails v3
-
----
-
-## Parallel Work Streams
-
-This plan has 3 independent work streams that can be executed by different agents:
-- **Work Stream A:** CI Optimization (GitHub Actions caching)
-- **Work Stream B:** Frontend Testing Setup (Vitest configuration)
-- **Work Stream C:** Frontend Tests (Utility and component tests)
-
----
-
-## Work Stream A: CI Optimization (GitHub Actions Caching)
-
-**Dependencies:** None - can run independently
-
-### Task A1: Add Go Module Caching to Go Tests Job
-
-**Files:**
-- Modify: `.github/workflows/ci.yml:21-56`
-
-**Step 1: Add Go module caching to go-tests job**
-
-Update the go-tests job to enable caching:
-
-```yaml
-      - name: Setup Go
-        uses: actions/setup-go@v5
-        with:
-          go-version: "1.25.0"
-          check-latest: true
-          cache: true
-          cache-dependency-path: go.sum
-```
-
-**Step 2: Verify the change**
-
-Check that the Setup Go step now includes `cache: true`.
-
-**Step 3: Commit**
-
-```bash
-git add .github/workflows/ci.yml
-git commit -m "ci: add Go module caching to go-tests job"
-```
-
----
-
-### Task A2: Optimize Wails CLI Installation with Caching
-
-**Files:**
-- Modify: `.github/workflows/ci.yml:79-86`
-- Modify: `.github/workflows/ci.yml:88-90`
-
-**Step 1: Add Wails CLI binary caching**
-
-Replace the Wails CLI installation with cached version:
-
-```yaml
-      - name: Cache Wails CLI
-        id: cache-wails
-        uses: actions/cache@v4
-        with:
-          path: ~/go/bin/wails3
-          key: wails-cli-${{ runner.os }}-${{ hashFiles('go.mod') }}
-
-      - name: Install Wails CLI
-        if: steps.cache-wails.outputs.cache-hit != 'true'
-        run: |
-          go install github.com/wailsapp/wails/v3/cmd/wails3@latest
-
-      - name: Setup Wails CLI PATH
-        run: |
-          mkdir -p /usr/local/bin
-          cp $(go env GOPATH)/bin/wails3 /usr/local/bin/wails
-          chmod +x /usr/local/bin/wails
-          echo "/usr/local/bin" >> $GITHUB_PATH
-```
-
-**Step 2: Verify the change**
-
-Ensure caching logic and conditional installation are correct.
-
-**Step 3: Commit**
-
-```bash
-git add .github/workflows/ci.yml
-git commit -m "ci: cache Wails CLI binary to avoid recompiling"
-```
-
----
-
-### Task A3: Add APT Package Caching
-
-**Files:**
-- Modify: `.github/workflows/ci.yml:79-86`
-
-**Step 1: Add APT caching for native dependencies**
-
-Add caching before installing APT packages:
-
-```yaml
-      - name: Cache APT packages
-        uses: awalsh128/cache-apt-pkgs-action@latest
-        with:
-          packages: libgtk-3-dev libwebkit2gtk-4.1-dev
-          version: 1.0
-          execute_install_scripts: false
-```
-
-Remove the `apt-get update` and `apt-get install` commands since the cache action handles them.
-
-**Step 2: Remove old APT commands**
-
-Delete these lines:
-```yaml
-      - name: Install dependencies
-        run: |
-          sudo apt-get update
-          sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev
-```
-
-**Step 3: Commit**
-
-```bash
-git add .github/workflows/ci.yml
-git commit -m "ci: cache APT packages for faster native deps installation"
-```
-
----
-
-### Task A4: Add Bun Dependency Caching
-
-**Files:**
-- Modify: `.github/workflows/ci.yml:74-78`
-
-**Step 1: Add Bun cache configuration**
-
-Update Bun setup to enable caching:
-
-```yaml
-      - name: Setup Bun
-        uses: oven-sh/setup-bun@v1
-        with:
-          bun-version: latest
-
-      - name: Cache Bun dependencies
-        uses: actions/cache@v4
-        with:
-          path: |
-            frontend/node_modules
-            ~/.bun/install/cache
-          key: bun-deps-${{ runner.os }}-${{ hashFiles('frontend/bun.lockb') }}
-          restore-keys: |
-            bun-deps-${{ runner.os }}-
-```
-
-**Step 2: Commit**
-
-```bash
-git add .github/workflows/ci.yml
-git commit -m "ci: add Bun dependency caching"
-```
-
----
-
-### Task A5: Optimize Go Test Execution
-
-**Files:**
-- Modify: `.github/workflows/ci.yml:31-35`
-
-**Step 1: Optimize test execution**
-
-Replace test command with optimized version:
-
-```yaml
-      - name: Run Go Tests
-        run: |
-          go test -race -count=1 ./internal/... -coverprofile=coverage.out
-          go install github.com/boumenot/gocover-cobertura@latest
-          gocover-cobertura < coverage.out > coverage.xml
-```
-
-Changes:
-- Removed `-v` (verbose) flag for cleaner output
-- Added `-count=1` to disable test caching (ensures fresh runs)
-- Kept `-race` for race detection
-
-**Step 2: Commit**
-
-```bash
-git add .github/workflows/ci.yml
-git commit -m "ci: optimize Go test execution"
-```
-
----
-
-### Task A6: Parallelize Jobs and Add Frontend Checks
-
-**Files:**
-- Modify: `.github/workflows/ci.yml` (restructure jobs)
-
-**Step 1: Rename app-build to frontend-checks and restructure**
-
-```yaml
-  frontend-checks:
-    name: Frontend Checks
-    runs-on: ubuntu-latest
-    permissions:
-      pull-requests: write
-
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v4
-
-      - name: Setup Bun
-        uses: oven-sh/setup-bun@v1
-        with:
-          bun-version: latest
-
-      - name: Cache Bun dependencies
-        uses: actions/cache@v4
-        with:
-          path: |
-            frontend/node_modules
-            ~/.bun/install/cache
-          key: bun-deps-${{ runner.os }}-${{ hashFiles('frontend/bun.lockb') }}
-          restore-keys: |
-            bun-deps-${{ runner.os }}-
-
-      - name: Install frontend dependencies
-        run: |
-          cd frontend && bun install
-
-      - name: Format check
-        run: |
-          cd frontend && bun run format:check
-
-      - name: Build frontend
-        run: |
-          cd frontend && bun run build
-```
-
-**Step 2: Update job name from `app-build` to `frontend-checks`**
-
-**Step 3: Commit**
-
-```bash
-git add .github/workflows/ci.yml
-git commit -m "ci: restructure frontend job and add format checks"
-```
-
----
-
-## Work Stream B: Frontend Testing Setup (Vitest)
-
-**Dependencies:** None - can run independently
-
-### Task B1: Install Vitest and Testing Dependencies
-
-**Files:**
-- Modify: `frontend/package.json`
-
-**Step 1: Add test scripts and devDependencies**
-
-Add to `scripts` section:
-```json
-"test": "vitest run",
-"test:watch": "vitest",
-"test:coverage": "vitest run --coverage"
-```
-
-Add to `devDependencies`:
-```json
-"@testing-library/react": "^14.2.1",
-"@testing-library/jest-dom": "^6.4.2",
-"@testing-library/user-event": "^14.5.2",
-"@vitest/coverage-v8": "^1.3.1",
-"jsdom": "^24.0.0",
-"vitest": "^1.3.1"
-```
-
-**Step 2: Install dependencies**
-
-```bash
-cd frontend
-bun install
-```
-
-**Step 3: Verify installation**
-
-Check `frontend/node_modules` contains `vitest`, `@testing-library/react`.
-
-**Step 4: Commit**
-
-```bash
-git add frontend/package.json
-bun install
-git add bun.lockb
-git commit -m "chore: install Vitest and React Testing Library"
-```
-
----
-
-### Task B2: Configure Vitest
-
-**Files:**
-- Create: `frontend/vitest.config.js`
-
-**Step 1: Create Vitest configuration**
-
-```javascript
-import { defineConfig } from 'vitest/config';
-import react from '@vitejs/plugin-react';
-
-export default defineConfig({
-  plugins: [react()],
-  test: {
-    globals: true,
-    environment: 'jsdom',
-    setupFiles: ['./src/test/setup.js'],
-    include: ['src/**/*.{test,spec}.{js,jsx}'],
-    coverage: {
-      provider: 'v8',
-      reporter: ['text', 'json', 'html'],
-      exclude: [
-        'node_modules/',
-        'src/test/',
-        'src/**/*.d.ts',
-      ],
-    },
-  },
-});
-```
-
-**Step 2: Create test setup file**
-
-**Files:**
-- Create: `frontend/src/test/setup.js`
-
-```javascript
-import { expect, afterEach } from 'vitest';
-import { cleanup } from '@testing-library/react';
-import * as matchers from '@testing-library/jest-dom/matchers';
-
-// Extend Vitest's expect with jest-dom matchers
-expect.extend(matchers);
-
-// Cleanup after each test
-afterEach(() => {
-  cleanup();
-});
-```
-
-**Step 3: Verify configuration**
-
-```bash
-cd frontend
-bun run test --help
-```
-
-Expected: Shows Vitest help output without errors.
-
-**Step 4: Commit**
-
-```bash
-git add frontend/vitest.config.js frontend/src/test/setup.js
-git commit -m "chore: configure Vitest with jsdom and testing-library"
-```
-
----
-
-### Task B3: Add Frontend Tests to CI
-
-**Files:**
-- Modify: `.github/workflows/ci.yml` (frontend-checks job)
-
-**Step 1: Add frontend test step**
-
-Add after the "Build frontend" step in the `frontend-checks` job:
-
-```yaml
-      - name: Run frontend tests
-        run: |
-          cd frontend && bun run test
-```
-
-**Step 2: Update job name to reflect tests**
-
-Change job name from "Frontend Checks" to "Frontend Tests & Build".
-
-**Step 3: Commit**
-
-```bash
-git add .github/workflows/ci.yml
-git commit -m "ci: add frontend test execution to CI"
-```
-
----
-
-## Work Stream C: Frontend Tests (Utilities and Components)
-
-**Dependencies:** Work Stream B must be complete first
-
-### Task C1: Test Utility - storage.js
-
-**Files:**
-- Create: `frontend/src/utils/storage.test.js`
-
-**Step 1: Write failing tests**
-
-```javascript
-import { describe, it, expect, beforeEach, vi } from 'vitest';
-import storage from './storage';
-
-describe('storage', () => {
-  beforeEach(() => {
-    // Clear localStorage before each test
-    window.localStorage.clear();
-    vi.clearAllMocks();
-  });
-
-  describe('get', () => {
-    it('should return null for non-existent key', () => {
-      expect(storage.get('non-existent')).toBeNull();
-    });
-
-    it('should return parsed value for existing key', () => {
-      window.localStorage.setItem('test-key', JSON.stringify({ foo: 'bar' }));
-      expect(storage.get('test-key')).toEqual({ foo: 'bar' });
-    });
-
-    it('should return null and log error for invalid JSON', () => {
-      window.localStorage.setItem('invalid', 'not-json');
-      const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
-      
-      expect(storage.get('invalid')).toBeNull();
-      expect(consoleSpy).toHaveBeenCalled();
-      
-      consoleSpy.mockRestore();
-    });
-  });
-
-  describe('set', () => {
-    it('should store value as JSON', () => {
-      storage.set('test', { data: 'value' });
-      expect(window.localStorage.getItem('test')).toBe('{"data":"value"}');
-    });
-
-    it('should return true on success', () => {
-      expect(storage.set('test', 'value')).toBe(true);
-    });
-
-    it('should return false and log error on failure', () => {
-      const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
-      vi.spyOn(window.localStorage, 'setItem').mockImplementation(() => {
-        throw new Error('Storage full');
-      });
-
-      expect(storage.set('test', 'value')).toBe(false);
-      expect(consoleSpy).toHaveBeenCalled();
-      
-      consoleSpy.mockRestore();
-    });
-  });
-
-  describe('getArray', () => {
-    it('should return empty array for non-existent key', () => {
-      expect(storage.getArray('non-existent')).toEqual([]);
-    });
-
-    it('should return parsed array for existing key', () => {
-      window.localStorage.setItem('array-key', JSON.stringify([1, 2, 3]));
-      expect(storage.getArray('array-key')).toEqual([1, 2, 3]);
-    });
-
-    it('should return empty array for non-array value', () => {
-      window.localStorage.setItem('not-array', JSON.stringify({ foo: 'bar' }));
-      expect(storage.getArray('not-array')).toEqual([]);
-    });
-  });
-
-  describe('setArray', () => {
-    it('should store array as JSON', () => {
-      storage.setArray('test', [1, 2, 3]);
-      expect(window.localStorage.getItem('test')).toBe('[1,2,3]');
-    });
-
-    it('should return false for non-array value', () => {
-      const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
-      expect(storage.setArray('test', 'not-array')).toBe(false);
-      consoleSpy.mockRestore();
-    });
-  });
-});
-```
-
-**Step 2: Run tests to verify they pass**
-
-```bash
-cd frontend
-bun run test src/utils/storage.test.js
-```
-
-Expected: All 9 tests pass.
-
-**Step 3: Commit**
-
-```bash
-git add frontend/src/utils/storage.test.js
-git commit -m "test: add tests for storage utility"
-```
-
----
-
-### Task C2: Test Utility - inputUtils.js
-
-**Files:**
-- Create: `frontend/src/utils/inputUtils.test.js`
-
-**Step 1: Write tests**
-
-```javascript
-import { describe, it, expect } from 'vitest';
-import {
-  getMonospaceFontFamily,
-  getDataFontSize,
-  getTextareaResize,
-  validateJson,
-  formatJson,
-  objectToKeyValueString,
-} from './inputUtils';
-
-describe('inputUtils', () => {
-  describe('getMonospaceFontFamily', () => {
-    it('should return IBM Plex Mono font family', () => {
-      expect(getMonospaceFontFamily()).toBe("'IBM Plex Mono', monospace");
-    });
-  });
-
-  describe('getDataFontSize', () => {
-    it('should return 0.875rem', () => {
-      expect(getDataFontSize()).toBe('0.875rem');
-    });
-  });
-
-  describe('getTextareaResize', () => {
-    it('should return none when both are false', () => {
-      expect(getTextareaResize(false, false)).toBe('none');
-    });
-
-    it('should return vertical when only height is true', () => {
-      expect(getTextareaResize(true, false)).toBe('vertical');
-    });
-
-    it('should return horizontal when only width is true', () => {
-      expect(getTextareaResize(false, true)).toBe('horizontal');
-    });
-
-    it('should return both when both are true', () => {
-      expect(getTextareaResize(true, true)).toBe('both');
-    });
-
-    it('should default to vertical resize', () => {
-      expect(getTextareaResize()).toBe('vertical');
-    });
-  });
-
-  describe('validateJson', () => {
-    it('should return valid for empty string', () => {
-      const result = validateJson('');
-      expect(result.isValid).toBe(true);
-      expect(result.data).toBeNull();
-      expect(result.error).toBeNull();
-    });
-
-    it('should return valid for whitespace-only string', () => {
-      const result = validateJson('   ');
-      expect(result.isValid).toBe(true);
-    });
-
-    it('should parse valid JSON object', () => {
-      const result = validateJson('{"key": "value"}');
-      expect(result.isValid).toBe(true);
-      expect(result.data).toEqual({ key: 'value' });
-      expect(result.error).toBeNull();
-    });
-
-    it('should parse valid JSON array', () => {
-      const result = validateJson('[1, 2, 3]');
-      expect(result.isValid).toBe(true);
-      expect(result.data).toEqual([1, 2, 3]);
-    });
-
-    it('should return invalid for malformed JSON', () => {
-      const result = validateJson('{"key": value}');
-      expect(result.isValid).toBe(false);
-      expect(result.data).toBeNull();
-      expect(result.error).toContain('Unexpected token');
-    });
-  });
-
-  describe('formatJson', () => {
-    it('should format object with default indentation', () => {
-      const result = formatJson({ key: 'value' });
-      expect(result).toBe('{\n  "key": "value"\n}');
-    });
-
-    it('should format with custom indentation', () => {
-      const result = formatJson({ key: 'value' }, 4);
-      expect(result).toBe('{\n    "key": "value"\n}');
-    });
-
-    it('should return empty string for null', () => {
-      expect(formatJson(null)).toBe('');
-    });
-
-    it('should return empty string for undefined', () => {
-      expect(formatJson(undefined)).toBe('');
-    });
-  });
-
-  describe('objectToKeyValueString', () => {
-    it('should convert object to key-value string', () => {
-      const result = objectToKeyValueString({ foo: 'bar', num: 42 });
-      expect(result).toBe('foo: "bar"\nnum: 42');
-    });
-
-    it('should return empty string for null', () => {
-      expect(objectToKeyValueString(null)).toBe('');
-    });
-
-    it('should return empty string for non-object', () => {
-      expect(objectToKeyValueString('string')).toBe('');
-    });
-
-    it('should handle nested objects', () => {
-      const result = objectToKeyValueString({ nested: { a: 1 } });
-      expect(result).toBe('nested: {"a":1}');
-    });
-  });
-});
-```
-
-**Step 2: Run tests**
-
-```bash
-cd frontend
-bun run test src/utils/inputUtils.test.js
-```
-
-Expected: All tests pass.
-
-**Step 3: Commit**
-
-```bash
-git add frontend/src/utils/inputUtils.test.js
-git commit -m "test: add tests for inputUtils utility"
-```
-
----
-
-### Task C3: Test Utility - layoutUtils.js
-
-**Files:**
-- Read: `frontend/src/utils/layoutUtils.js`
-- Create: `frontend/src/utils/layoutUtils.test.js`
-
-**Step 1: Read existing layoutUtils.js**
-
-Check if file exists and understand its contents.
-
-**Step 2: Create tests**
-
-```javascript
-import { describe, it, expect } from 'vitest';
-// Import functions from layoutUtils.js once you read it
-
-describe('layoutUtils', () => {
-  it('should have tests for layout utilities', () => {
-    // Write tests based on actual functions in layoutUtils.js
-    expect(true).toBe(true);
-  });
-});
-```
-
-**Step 3: Run and commit**
-
-```bash
-cd frontend
-bun run test src/utils/layoutUtils.test.js
-git add frontend/src/utils/layoutUtils.test.js
-git commit -m "test: add tests for layoutUtils utility"
-```
-
----
-
-### Task C4: Test Component - ToolCopyButton
-
-**Files:**
-- Create: `frontend/src/components/inputs/ToolCopyButton.test.jsx`
-
-**Step 1: Write tests**
-
-```javascript
-import { describe, it, expect, vi } from 'vitest';
-import { render, screen, fireEvent } from '@testing-library/react';
-import ToolCopyButton from './ToolCopyButton';
-
-describe('ToolCopyButton', () => {
-  it('should render copy button', () => {
-    render();
-    expect(screen.getByRole('button')).toBeInTheDocument();
-  });
-
-  it('should have aria-label for accessibility', () => {
-    render();
-    expect(screen.getByLabelText(/copy/i)).toBeInTheDocument();
-  });
-
-  it('should call clipboard API when clicked', async () => {
-    const mockWriteText = vi.fn().mockResolvedValue(undefined);
-    Object.assign(navigator, {
-      clipboard: { writeText: mockWriteText },
-    });
-
-    render();
-    fireEvent.click(screen.getByRole('button'));
-
-    expect(mockWriteText).toHaveBeenCalledWith('test content');
-  });
-
-  it('should show checkmark after successful copy', async () => {
-    vi.useFakeTimers();
-    Object.assign(navigator, {
-      clipboard: {
-        writeText: vi.fn().mockResolvedValue(undefined),
-      },
-    });
-
-    render();
-    fireEvent.click(screen.getByRole('button'));
-
-    // Wait for async operation
-    await vi.advanceTimersByTimeAsync(0);
-
-    // Check that success state is shown (implementation dependent)
-    // This test may need adjustment based on actual component behavior
-    
-    vi.useRealTimers();
-  });
-});
-```
-
-**Step 2: Run tests**
-
-```bash
-cd frontend
-bun run test src/components/inputs/ToolCopyButton.test.jsx
-```
-
-**Step 3: Commit**
-
-```bash
-git add frontend/src/components/inputs/ToolCopyButton.test.jsx
-git commit -m "test: add tests for ToolCopyButton component"
-```
-
----
-
-### Task C5: Test Hook - useLayoutToggle
-
-**Files:**
-- Create: `frontend/src/hooks/useLayoutToggle.test.js`
-
-**Step 1: Write tests**
-
-```javascript
-import { describe, it, expect } from 'vitest';
-import { renderHook, act } from '@testing-library/react';
-import useLayoutToggle from './useLayoutToggle';
-
-describe('useLayoutToggle', () => {
-  it('should initialize with default layout', () => {
-    const { result } = renderHook(() => useLayoutToggle());
-    expect(result.current.layout).toBeDefined();
-  });
-
-  it('should toggle layout', () => {
-    const { result } = renderHook(() => useLayoutToggle());
-    const initialLayout = result.current.layout;
-
-    act(() => {
-      result.current.toggleLayout();
-    });
-
-    expect(result.current.layout).not.toBe(initialLayout);
-  });
-
-  it('should persist layout to storage', () => {
-    const { result } = renderHook(() => useLayoutToggle());
-    
-    act(() => {
-      result.current.setLayout('split');
-    });
-
-    // Re-render hook and check if persisted value is loaded
-    const { result: result2 } = renderHook(() => useLayoutToggle());
-    expect(result2.current.layout).toBe('split');
-  });
-});
-```
-
-**Step 2: Run tests**
-
-```bash
-cd frontend
-bun run test src/hooks/useLayoutToggle.test.js
-```
-
-**Step 3: Commit**
-
-```bash
-git add frontend/src/hooks/useLayoutToggle.test.js
-git commit -m "test: add tests for useLayoutToggle hook"
-```
-
----
-
-## Integration and Validation
-
-### Task I1: Run Full Test Suite Locally
-
-**Step 1: Run all frontend tests**
-
-```bash
-cd frontend
-bun run test
-```
-
-Expected: All tests pass.
-
-**Step 2: Run with coverage**
-
-```bash
-bun run test:coverage
-```
-
-Expected: Coverage report generated.
-
-**Step 3: Verify build still works**
-
-```bash
-bun run build
-```
-
-Expected: Build completes without errors.
-
----
-
-### Task I2: Validate CI Workflow
-
-**Step 1: Test locally with act (optional)**
-
-If `act` is installed:
-
-```bash
-act -j go-tests
-act -j frontend-checks
-```
-
-**Step 2: Push branch and create PR**
-
-```bash
-git push origin feature/faster-ci
-```
-
-Create PR and observe CI execution times.
-
----
-
-### Task I3: Performance Validation
-
-**Step 1: Record baseline timing**
-
-Before changes: Note current CI time (~5-10 minutes)
-
-**Step 2: Measure after changes**
-
-With all optimizations, expected times:
-- Go Tests: ~30-60 seconds (cached modules)
-- Frontend Tests & Build: ~45-90 seconds (cached deps)
-- Total PR check: ~1-2 minutes
-
-**Step 3: Document improvements**
-
-Update README or CONTRIBUTING with new CI times.
-
----
-
-## Summary of Changes
-
-### Files Created:
-- `frontend/vitest.config.js` - Vitest configuration
-- `frontend/src/test/setup.js` - Test setup file
-- `frontend/src/utils/storage.test.js` - Storage utility tests
-- `frontend/src/utils/inputUtils.test.js` - Input utility tests
-- `frontend/src/utils/layoutUtils.test.js` - Layout utility tests
-- `frontend/src/components/inputs/ToolCopyButton.test.jsx` - Component tests
-- `frontend/src/hooks/useLayoutToggle.test.js` - Hook tests
-
-### Files Modified:
-- `frontend/package.json` - Added test dependencies
-- `.github/workflows/ci.yml` - Added caching and frontend tests
-
-### Expected Outcomes:
-- PR check time reduced from 5-10 min to 1-2 min
-- Frontend tests running in CI
-- 15-20+ unit tests covering core utilities and components
-
----
-
-## Execution Options
-
-**This plan has 3 independent work streams:**
-
-1. **Work Stream A** (CI Optimization) - Modifies `.github/workflows/ci.yml`
-2. **Work Stream B** (Frontend Testing Setup) - Modifies `frontend/package.json` and creates config
-3. **Work Stream C** (Frontend Tests) - Creates test files (depends on Work Stream B)
-
-**Parallel execution:**
-- Agent 1: Work Stream A (independent)
-- Agent 2: Work Stream B (independent)
-- Agent 3: Work Stream C (waits for B)
-
-**Or serial execution:**
-- Complete Work Stream B first
-- Then Work Streams A and C can run in parallel
-
-Choose execution method based on available agents.
diff --git a/docs/plans/2026-02-09-browser-api-design.md b/docs/plans/2026-02-09-browser-api-design.md
deleted file mode 100644
index c8a4a7f..0000000
--- a/docs/plans/2026-02-09-browser-api-design.md
+++ /dev/null
@@ -1,1288 +0,0 @@
-# Browser API Support Implementation Plan
-
-> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
-
-**Goal:** Enable DevToolbox to work in web browsers by adding a Gin HTTP API alongside the existing Wails desktop app, with auto-discovery of service methods and auto-generated TypeScript clients.
-
-**Architecture:** A dual-mode system where Go services are registered once and exposed via both Wails runtime (desktop) and Gin HTTP server (browser). An auto-discovery router uses reflection to generate RESTful endpoints, and a code generator creates TypeScript clients for both modes with a unified facade that auto-detects the runtime environment.
-
-**Tech Stack:** Go 1.22+, Gin Gonic, Go AST (codegen), TypeScript, Wails v3
-
----
-
-## Task 1: Create Auto-Discovery Router Package
-
-**Files:**
-- Create: `pkg/router/router.go`
-- Create: `pkg/router/binding.go`
-- Test: `pkg/router/router_test.go`
-
-**Step 1: Create router.go with service registration and auto-discovery**
-
-Write file: `pkg/router/router.go`
-```go
-package router
-
-import (
-	"fmt"
-	"net/http"
-	"reflect"
-	"strings"
-	"unicode"
-
-	"github.com/gin-gonic/gin"
-)
-
-// Router automatically discovers and registers service methods as HTTP routes
-type Router struct {
-	engine *gin.Engine
-}
-
-// New creates a new Router with the given Gin engine
-func New(engine *gin.Engine) *Router {
-	return &Router{engine: engine}
-}
-
-// Register scans a service struct and auto-generates routes for all exported methods
-func (r *Router) Register(service interface{}) error {
-	serviceType := reflect.TypeOf(service)
-	serviceValue := reflect.ValueOf(service)
-
-	// Get service name and convert to kebab-case
-	serviceName := toKebabCase(serviceType.Elem().Name())
-
-	// Iterate through all methods
-	for i := 0; i < serviceType.NumMethod(); i++ {
-		method := serviceType.Method(i)
-		
-		// Skip unexported methods and lifecycle methods
-		if !method.IsExported() || isLifecycleMethod(method.Name) {
-			continue
-		}
-
-		// Convert method name to kebab-case
-		methodName := toKebabCase(method.Name)
-		path := fmt.Sprintf("/api/%s/%s", serviceName, methodName)
-
-		// Create handler
-		handler := r.createHandler(serviceValue.Method(i), method)
-		r.engine.POST(path, handler)
-	}
-
-	return nil
-}
-
-// createHandler creates a Gin handler for a method
-func (r *Router) createHandler(methodValue reflect.Value, method reflect.Method) gin.HandlerFunc {
-	return func(c *gin.Context) {
-		// Get method signature
-		methodType := method.Type
-		numIn := methodType.NumIn()
-
-		// Prepare arguments
-		args := make([]reflect.Value, numIn-1) // -1 because receiver is first
-
-		if numIn > 1 {
-			// First argument should be a struct for JSON binding
-			argType := methodType.In(1)
-			argValue := reflect.New(argType).Interface()
-
-			if err := c.ShouldBindJSON(argValue); err != nil {
-				c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
-				return
-			}
-
-			args[0] = reflect.ValueOf(argValue).Elem()
-		}
-
-		// Call the method
-		results := methodValue.Call(args)
-
-		// Handle return values (result, error)
-		if len(results) == 2 {
-			// Check for error
-			if !results[1].IsNil() {
-				err := results[1].Interface().(error)
-				c.JSON(http.StatusOK, gin.H{"error": err.Error()})
-				return
-			}
-
-			// Return result
-			c.JSON(http.StatusOK, results[0].Interface())
-		} else if len(results) == 1 {
-			c.JSON(http.StatusOK, results[0].Interface())
-		}
-	}
-}
-
-// toKebabCase converts PascalCase to kebab-case
-func toKebabCase(s string) string {
-	var result strings.Builder
-	for i, r := range s {
-		if unicode.IsUpper(r) {
-			if i > 0 {
-				result.WriteRune('-')
-			}
-			result.WriteRune(unicode.ToLower(r))
-		} else {
-			result.WriteRune(r)
-		}
-	}
-	return result.String()
-}
-
-// isLifecycleMethod checks if method is a Wails lifecycle method
-func isLifecycleMethod(name string) bool {
-	return name == "ServiceStartup" || name == "ServiceShutdown"
-}
-```
-
-**Step 2: Create binding.go for request/response utilities**
-
-Write file: `pkg/router/binding.go`
-```go
-package router
-
-// RequestWrapper wraps requests for single-parameter methods
-type RequestWrapper struct {
-	Args []interface{} `json:"args"`
-}
-
-// ResponseWrapper standardizes API responses
-type ResponseWrapper struct {
-	Data  interface{} `json:"data,omitempty"`
-	Error string      `json:"error,omitempty"`
-}
-```
-
-**Step 3: Write router tests**
-
-Write file: `pkg/router/router_test.go`
-```go
-package router
-
-import (
-	"bytes"
-	"encoding/json"
-	"net/http"
-	"net/http/httptest"
-	"testing"
-
-	"github.com/gin-gonic/gin"
-	"github.com/stretchr/testify/assert"
-)
-
-// Test service
-type TestService struct{}
-
-type EchoRequest struct {
-	Message string `json:"message"`
-}
-
-type EchoResponse struct {
-	Message string `json:"message"`
-}
-
-func (s *TestService) Echo(req EchoRequest) EchoResponse {
-	return EchoResponse{Message: req.Message}
-}
-
-func (s *TestService) ServiceStartup() error {
-	return nil
-}
-
-func TestRouter_Register(t *testing.T) {
-	gin.SetMode(gin.TestMode)
-	r := gin.New()
-	router := New(r)
-
-	service := &TestService{}
-	err := router.Register(service)
-	assert.NoError(t, err)
-
-	// Test echo endpoint
-	req := EchoRequest{Message: "hello"}
-	body, _ := json.Marshal(req)
-	
-	w := httptest.NewRecorder()
-	httpReq, _ := http.NewRequest("POST", "/api/test-service/echo", bytes.NewBuffer(body))
-	httpReq.Header.Set("Content-Type", "application/json")
-	
-	r.ServeHTTP(w, httpReq)
-
-	assert.Equal(t, http.StatusOK, w.Code)
-	
-	var resp EchoResponse
-	json.Unmarshal(w.Body.Bytes(), &resp)
-	assert.Equal(t, "hello", resp.Message)
-}
-
-func TestToKebabCase(t *testing.T) {
-	tests := []struct {
-		input    string
-		expected string
-	}{
-		{"JWTService", "jwt-service"},
-		{"Decode", "decode"},
-		{"VerifyToken", "verify-token"},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.input, func(t *testing.T) stretchr/testify {
-			result := toKebabCase(tt.input)
-			assert.Equal(t, tt.expected, result)
-		})
-	}
-}
-```
-
-**Step 4: Run tests**
-
-```bash
-cd pkg/router && go test -v
-```
-
-Expected: Tests pass
-
-**Step 5: Commit**
-
-```bash
-git add pkg/router/
-git commit -m "feat: add auto-discovery router with Gin integration"
-```
-
----
-
-## Task 2: Create HTTP Server Integration
-
-**Files:**
-- Create: `pkg/router/server.go`
-- Modify: `server.go` (replace existing implementation)
-
-**Step 1: Create server.go with Gin-based HTTP server**
-
-Write file: `pkg/router/server.go`
-```go
-package router
-
-import (
-	"fmt"
-	"net/http"
-	"time"
-
-	"github.com/gin-contrib/cors"
-	"github.com/gin-gonic/gin"
-)
-
-// Server represents the HTTP server with auto-discovery router
-type Server struct {
-	router *Router
-	engine *gin.Engine
-}
-
-// NewServer creates a new HTTP server
-func NewServer() *Server {
-	gin.SetMode(gin.ReleaseMode)
-	engine := gin.New()
-	engine.Use(gin.Recovery())
-	
-	// CORS configuration
-	config := cors.Config{
-		AllowOrigins:     []string{"*"},
-		AllowMethods:     []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
-		AllowHeaders:     []string{"Origin", "Content-Type", "Accept", "Authorization"},
-		ExposeHeaders:    []string{"Content-Length"},
-		AllowCredentials: true,
-		MaxAge:           12 * time.Hour,
-	}
-	engine.Use(cors.New(config))
-
-	// Health check
-	engine.GET("/health", func(c *gin.Context) {
-		c.JSON(http.StatusOK, gin.H{
-			"status": "ok",
-			"mode":   "web",
-			"time":   time.Now().Format(time.RFC3339),
-		})
-	})
-
-	return &Server{
-		router: New(engine),
-		engine: engine,
-	}
-}
-
-// Register adds a service to the router
-func (s *Server) Register(service interface{}) error {
-	return s.router.Register(service)
-}
-
-// Start starts the HTTP server on the specified port
-func (s *Server) Start(port int) error {
-	addr := fmt.Sprintf(":%d", port)
-	return s.engine.Run(addr)
-}
-
-// Engine returns the Gin engine for testing
-func (s *Server) Engine() *gin.Engine {
-	return s.engine
-}
-```
-
-**Step 2: Update main server.go to use the new router**
-
-Read existing: `server.go` (lines 1-50)
-
-Replace entire file content with:
-```go
-package main
-
-import (
-	"devtoolbox/pkg/router"
-	"devtoolbox/service"
-)
-
-// StartHTTPServer starts the HTTP server with all services registered
-func StartHTTPServer(port int) {
-	// Create services
-	jwtSvc := service.NewJWTService(nil)
-	conversionSvc := service.NewConversionService(nil)
-	barcodeSvc := service.NewBarcodeService(nil)
-	dataGenSvc := service.NewDataGeneratorService(nil)
-	codeFmtSvc := service.NewCodeFormatterService(nil)
-	dateTimeSvc := service.NewDateTimeService(nil)
-
-	// Create server and register services
-	server := router.NewServer()
-	server.Register(jwtSvc)
-	server.Register(conversionSvc)
-	server.Register(barcodeSvc)
-	server.Register(dataGenSvc)
-	server.Register(codeFmtSvc)
-	server.Register(dateTimeSvc)
-
-	// Start server
-	server.Start(port)
-}
-```
-
-**Step 3: Commit**
-
-```bash
-git add pkg/router/server.go server.go
-git commit -m "feat: integrate auto-discovery router into HTTP server"
-```
-
----
-
-## Task 3: Update Main.go for Dual-Mode Operation
-
-**Files:**
-- Modify: `main.go`
-
-**Step 1: Read current main.go**
-
-Read: `main.go`
-
-**Step 2: Modify main.go to start HTTP server in background**
-
-Add after service creation (around line 61):
-```go
-	// Start HTTP server for browser support (background)
-	go func() {
-		StartHTTPServer(8081)
-	}()
-```
-
-This should be placed right before the `app.Window.NewWithOptions()` call.
-
-**Step 3: Commit**
-
-```bash
-git add main.go
-git commit -m "feat: enable dual-mode with HTTP server on port 8081"
-```
-
----
-
-## Task 4: Create Code Generator Tool
-
-**Files:**
-- Create: `cmd/genservices/main.go`
-- Create: `cmd/genservices/parser.go`
-- Create: `cmd/genservices/generator.go`
-- Create: `cmd/genservices/templates/typescript.tmpl`
-
-**Step 1: Create parser.go to parse Go service files**
-
-Write file: `cmd/genservices/parser.go`
-```go
-package main
-
-import (
-	"go/ast"
-	"go/parser"
-	"go/token"
-	"path/filepath"
-	"strings"
-)
-
-// ServiceMethod represents a method in a service
-type ServiceMethod struct {
-	Name       string
-	Parameters []Parameter
-	Returns    []Parameter
-}
-
-// Parameter represents a method parameter
-type Parameter struct {
-	Name string
-	Type string
-}
-
-// Service represents a parsed service
-type Service struct {
-	Name    string
-	Methods []ServiceMethod
-}
-
-// Parser parses Go service files
-type Parser struct {
-	serviceDir string
-}
-
-// NewParser creates a new parser
-func NewParser(serviceDir string) *Parser {
-	return &Parser{serviceDir: serviceDir}
-}
-
-// ParseServices parses all service files in the directory
-func (p *Parser) ParseServices() ([]Service, error) {
-	fset := token.NewFileSet()
-	
-	// Parse all Go files in service directory
-	pkgs, err := parser.ParseDir(fset, p.serviceDir, nil, 0)
-	if err != nil {
-		return nil, err
-	}
-
-	var services []Service
-
-	for _, pkg := range pkgs {
-		for filename, file := range pkg.Files {
-			if strings.HasSuffix(filename, "_test.go") {
-				continue
-			}
-
-			service := p.parseFile(file)
-			if service != nil {
-				services = append(services, *service)
-			}
-		}
-	}
-
-	return services, nil
-}
-
-// parseFile parses a single Go file and extracts services
-func (p *Parser) parseFile(file *ast.File) *Service {
-	for _, decl := range file.Decls {
-		genDecl, ok := decl.(*ast.GenDecl)
-		if !ok || genDecl.Tok != token.TYPE {
-			continue
-		}
-
-		for _, spec := range genDecl.Specs {
-			typeSpec, ok := spec.(*ast.TypeSpec)
-			if !ok {
-				continue
-			}
-
-			structType, ok := typeSpec.Type.(*ast.StructType)
-			if !ok {
-				continue
-			}
-
-			// Check if it's a service (has Service suffix or contains service methods)
-			if strings.HasSuffix(typeSpec.Name.Name, "Service") {
-				service := &Service{
-					Name: typeSpec.Name.Name,
-				}
-				
-				// Find methods for this type
-				service.Methods = p.findMethods(file, typeSpec.Name.Name)
-				
-				return service
-			}
-			
-			_ = structType // Use the variable to avoid unused warning
-		}
-	}
-
-	return nil
-}
-
-// findMethods finds all methods for a given type
-func (p *Parser) findMethods(file *ast.File, typeName string) []ServiceMethod {
-	var methods []ServiceMethod
-
-	for _, decl := range file.Decls {
-		funcDecl, ok := decl.(*ast.FuncDecl)
-		if !ok || funcDecl.Recv == nil {
-			continue
-		}
-
-		// Check if this method belongs to our type
-		for _, recv := range funcDecl.Recv.List {
-			recvType := p.getTypeString(recv.Type)
-			if recvType == "*"+typeName || recvType == typeName {
-				method := ServiceMethod{
-					Name: funcDecl.Name.Name,
-				}
-				
-				// Parse parameters (skip receiver)
-				if funcDecl.Type.Params != nil {
-					for _, param := range funcDecl.Type.Params.List {
-						paramType := p.getTypeString(param.Type)
-						for _, name := range param.Names {
-							method.Parameters = append(method.Parameters, Parameter{
-								Name: name.Name,
-								Type: paramType,
-							})
-						}
-					}
-				}
-
-				// Parse returns
-				if funcDecl.Type.Results != nil {
-					for _, result := range funcDecl.Type.Results.List {
-						resultType := p.getTypeString(result.Type)
-						method.Returns = append(method.Returns, Parameter{
-							Type: resultType,
-						})
-					}
-				}
-
-				methods = append(methods, method)
-			}
-		}
-	}
-
-	return methods
-}
-
-// getTypeString converts an AST type to a string
-func (p *Parser) getTypeString(expr ast.Expr) string {
-	switch t := expr.(type) {
-	case *ast.Ident:
-		return t.Name
-	case *ast.StarExpr:
-		return "*" + p.getTypeString(t.X)
-	case *ast.SelectorExpr:
-		return p.getTypeString(t.X) + "." + t.Sel.Name
-	default:
-		return ""
-	}
-}
-```
-
-**Step 2: Create generator.go for TypeScript generation**
-
-Write file: `cmd/genservices/generator.go`
-```go
-package main
-
-import (
-	"fmt"
-	"os"
-	"path/filepath"
-	"strings"
-	"text/template"
-)
-
-// Generator generates TypeScript code
-type Generator struct {
-	outputDir string
-	tmpl      *template.Template
-}
-
-// NewGenerator creates a new generator
-func NewGenerator(outputDir string) (*Generator, error) {
-	tmplPath := filepath.Join("cmd", "genservices", "templates", "typescript.tmpl")
-	tmplContent, err := os.ReadFile(tmplPath)
-	if err != nil {
-		return nil, err
-	}
-
-	tmpl, err := template.New("typescript").Parse(string(tmplContent))
-	if err != nil {
-		return nil, err
-	}
-
-	return &Generator{
-		outputDir: outputDir,
-		tmpl:      tmpl,
-	}, nil
-}
-
-// Generate creates TypeScript files for all services
-func (g *Generator) Generate(services []Service) error {
-	// Create output directories
-	wailsDir := filepath.Join(g.outputDir, "wails")
-	httpDir := filepath.Join(g.outputDir, "http")
-
-	os.MkdirAll(wailsDir, 0755)
-	os.MkdirAll(httpDir, 0755)
-
-	// Generate individual service files
-	for _, service := range services {
-		if err := g.generateWailsService(wailsDir, service); err != nil {
-			return err
-		}
-		if err := g.generateHTTPService(httpDir, service); err != nil {
-			return err
-		}
-	}
-
-	// Generate index files
-	if err := g.generateWailsIndex(wailsDir, services); err != nil {
-		return err
-	}
-	if err := g.generateHTTPIndex(httpDir, services); err != nil {
-		return err
-	}
-
-	// Generate unified facade
-	return g.generateUnifiedFacade(g.outputDir, services)
-}
-
-func (g *Generator) generateWailsService(dir string, service Service) error {
-	filename := filepath.Join(dir, toCamelCase(service.Name)+".ts")
-	
-	data := struct {
-		ServiceName string
-		Methods     []ServiceMethod
-	}{
-		ServiceName: service.Name,
-		Methods:     service.Methods,
-	}
-
-	file, err := os.Create(filename)
-	if err != nil {
-		return err
-	}
-	defer file.Close()
-
-	return g.tmpl.ExecuteTemplate(file, "wails", data)
-}
-
-func (g *Generator) generateHTTPService(dir string, service Service) error {
-	filename := filepath.Join(dir, toCamelCase(service.Name)+".ts")
-	
-	data := struct {
-		ServiceName string
-		Methods     []ServiceMethod
-	}{
-		ServiceName: service.Name,
-		Methods:     service.Methods,
-	}
-
-	file, err := os.Create(filename)
-	if err != nil {
-		return err
-	}
-	defer file.Close()
-
-	return g.tmpl.ExecuteTemplate(file, "http", data)
-}
-
-func (g *Generator) generateWailsIndex(dir string, services []Service) error {
-	filename := filepath.Join(dir, "index.ts")
-	
-	var exports []string
-	for _, svc := range services {
-		exports = append(exports, fmt.Sprintf("export * as %s from './%s';", 
-			toCamelCase(svc.Name), toCamelCase(svc.Name)))
-	}
-
-	content := strings.Join(exports, "\n")
-	return os.WriteFile(filename, []byte(content), 0644)
-}
-
-func (g *Generator) generateHTTPIndex(dir string, services []Service) error {
-	filename := filepath.Join(dir, "index.ts")
-	
-	var exports []string
-	for _, svc := range services {
-		exports = append(exports, fmt.Sprintf("export * as %s from './%s';", 
-			toCamelCase(svc.Name), toCamelCase(svc.Name)))
-	}
-
-	content := strings.Join(exports, "\n")
-	return os.WriteFile(filename, []byte(content), 0644)
-}
-
-func (g *Generator) generateUnifiedFacade(dir string, services []Service) error {
-	filename := filepath.Join(dir, "index.ts")
-	
-	var serviceImports []string
-	var serviceMappings []string
-
-	for _, svc := range services {
-		camelName := toCamelCase(svc.Name)
-		serviceImports = append(serviceImports, fmt.Sprintf(
-			"import { %s as Wails%s } from './wails/%s';\n"+
-			"import { %s as HTTP%s } from './http/%s';",
-			svc.Name, svc.Name, camelName,
-			svc.Name, svc.Name, camelName))
-		
-		serviceMappings = append(serviceMappings, fmt.Sprintf(
-			"export const %s = isWails() ? Wails%s : HTTP%s;",
-			camelName, svc.Name, svc.Name))
-	}
-
-	content := fmt.Sprintf(`// Auto-generated unified service facade
-// Detects runtime environment and uses appropriate implementation
-
-const isWails = () => {
-  return typeof window !== 'undefined' && 
-         window.runtime && 
-         window.runtime.EventsOn !== undefined;
-};
-
-%s
-
-%s
-`, strings.Join(serviceImports, "\n"), strings.Join(serviceMappings, "\n"))
-
-	return os.WriteFile(filename, []byte(content), 0644)
-}
-
-// toCamelCase converts PascalCase to camelCase
-func toCamelCase(s string) string {
-	if s == "" {
-		return s
-	}
-	return strings.ToLower(s[:1]) + s[1:]
-}
-```
-
-**Step 3: Create TypeScript template**
-
-Write file: `cmd/genservices/templates/typescript.tmpl`
-```
-{{define "wails"}}// Auto-generated Wails client for {{.ServiceName}}
-// This file is auto-generated. DO NOT EDIT.
-
-import { {{.ServiceName}} } from '../../../bindings/devtoolbox/service';
-
-{{range .Methods}}
-export const {{toCamelCase .Name}} = ({{range $i, $p := .Parameters}}{{if $i}}, {{end}}{{$p.Name}}: {{goToTS $p.Type}}{{end}}): Promise<{{goToTS (index .Returns 0).Type}}> => {
-  return {{$.ServiceName}}.{{.Name}}({{range $i, $p := .Parameters}}{{if $i}}, {{end}}{{$p.Name}}{{end}});
-};
-{{end}}
-{{end}}
-
-{{define "http"}}// Auto-generated HTTP client for {{.ServiceName}}
-// This file is auto-generated. DO NOT EDIT.
-
-const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8081';
-
-{{range .Methods}}
-export const {{toCamelCase .Name}} = async ({{range $i, $p := .Parameters}}{{if $i}}, {{end}}{{$p.Name}}: {{goToTS $p.Type}}{{end}}): Promise<{{goToTS (index .Returns 0).Type}}> => {
-  const response = await fetch(`${API_BASE}/api/{{kebabCase $.ServiceName}}/{{kebabCase .Name}}`, {
-    method: 'POST',
-    headers: { 'Content-Type': 'application/json' },
-    body: JSON.stringify({{if eq (len .Parameters) 1}}{{index .Parameters 0).Name}}{{else}}{ {{range $i, $p := .Parameters}}{{if $i}}, {{end}}"{{$p.Name}}": {{$p.Name}}{{end}} }{{end}})
-  });
-  
-  if (!response.ok) {
-    throw new Error(`HTTP error! status: ${response.status}`);
-  }
-  
-  return await response.json();
-};
-{{end}}
-{{end}}
-```
-
-**Step 4: Create main.go for the generator tool**
-
-Write file: `cmd/genservices/main.go`
-```go
-package main
-
-import (
-	"flag"
-	"fmt"
-	"log"
-	"os"
-	"path/filepath"
-	"strings"
-)
-
-func main() {
-	var (
-		serviceDir = flag.String("services", "service", "Directory containing Go service files")
-		outputDir  = flag.String("output", "frontend/src/generated", "Output directory for generated TypeScript")
-	)
-	flag.Parse()
-
-	// Get absolute paths
-	absServiceDir, err := filepath.Abs(*serviceDir)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	absOutputDir, err := filepath.Abs(*outputDir)
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	fmt.Printf("Parsing services from: %s\n", absServiceDir)
-	fmt.Printf("Generating TypeScript to: %s\n", absOutputDir)
-
-	// Parse services
-	parser := NewParser(absServiceDir)
-	services, err := parser.ParseServices()
-	if err != nil {
-		log.Fatal("Failed to parse services:", err)
-	}
-
-	fmt.Printf("Found %d services\n", len(services))
-	for _, svc := range services {
-		fmt.Printf("  - %s (%d methods)\n", svc.Name, len(svc.Methods))
-	}
-
-	// Generate TypeScript
-	generator, err := NewGenerator(absOutputDir)
-	if err != nil {
-		log.Fatal("Failed to create generator:", err)
-	}
-
-	if err := generator.Generate(services); err != nil {
-		log.Fatal("Failed to generate TypeScript:", err)
-	}
-
-	fmt.Println("✓ Generation complete!")
-}
-
-// Helper functions for templates
-func init() {
-	// These would be registered as template functions
-	_ = toCamelCase
-	_ = kebabCase
-	_ = goToTS
-}
-
-func toCamelCase(s string) string {
-	if s == "" {
-		return s
-	}
-	return strings.ToLower(s[:1]) + s[1:]
-}
-
-func kebabCase(s string) string {
-	var result strings.Builder
-	for i, r := range s {
-		if i > 0 && r >= 'A' && r <= 'Z' {
-			result.WriteRune('-')
-		}
-		result.WriteRune(r)
-	}
-	return strings.ToLower(result.String())
-}
-
-func goToTS(goType string) string {
-	// Simple type mappings
-	switch goType {
-	case "string":
-		return "string"
-	case "int", "int64", "float64":
-		return "number"
-	case "bool":
-		return "boolean"
-	case "error":
-		return "Error"
-	default:
-		// For complex types, return as-is (would need proper type imports)
-		return goType
-	}
-}
-```
-
-**Step 5: Commit**
-
-```bash
-git add cmd/genservices/
-git commit -m "feat: add TypeScript client generator tool"
-```
-
----
-
-## Task 5: Run Generator and Create Frontend Clients
-
-**Files:**
-- Create: `frontend/src/generated/wails/*.ts`
-- Create: `frontend/src/generated/http/*.ts`
-- Create: `frontend/src/generated/index.ts`
-
-**Step 1: Run the generator**
-
-```bash
-go run cmd/genservices/main.go -services service -output frontend/src/generated
-```
-
-Expected output:
-```
-Parsing services from: /Users/vuong/workspace/vuon9/devtoolbox/.worktrees/browser-api/service
-Generating TypeScript to: /Users/vuong/workspace/vuon9/devtoolbox/.worktrees/browser-api/frontend/src/generated
-Found 6 services
-  - JWTService (3 methods)
-  - ConversionService (1 methods)
-  - BarcodeService (5 methods)
-  - DataGeneratorService (3 methods)
-  - CodeFormatterService (1 methods)
-  - DateTimeService (4 methods)
-✓ Generation complete!
-```
-
-**Step 2: Verify generated files exist**
-
-```bash
-ls -la frontend/src/generated/
-ls -la frontend/src/generated/wails/
-ls -la frontend/src/generated/http/
-```
-
-Expected: Files should exist
-
-**Step 3: Commit generated files**
-
-```bash
-git add frontend/src/generated/
-git commit -m "feat: generate TypeScript clients for all services"
-```
-
----
-
-## Task 6: Update Frontend Components to Use Generated Clients
-
-**Files:**
-- Modify: `frontend/src/pages/JwtDebugger/index.jsx` (as example)
-- Create: `frontend/src/services/api.ts` (migration helper)
-
-**Step 1: Create migration helper**
-
-Write file: `frontend/src/services/api.ts`
-```typescript
-// Re-export generated services for convenience
-export * from '../generated';
-```
-
-**Step 2: Update one component as proof of concept**
-
-Modify: `frontend/src/pages/JwtDebugger/index.jsx`
-
-Change line 9 from:
-```javascript
-import { JWTService } from '../../../bindings/devtoolbox/service';
-```
-
-To:
-```javascript
-import { jwtService } from '../../services/api';
-```
-
-Change line 32 from:
-```javascript
-const response = await JWTService.Decode(state.token);
-```
-
-To:
-```javascript
-const response = await jwtService.decode(state.token);
-```
-
-And line 69 from:
-```javascript
-const response = await JWTService.Verify(state.token, state.secret, state.encoding);
-```
-
-To:
-```javascript
-const response = await jwtService.verify(state.token, state.secret, state.encoding);
-```
-
-**Step 3: Test the component**
-
-```bash
-cd frontend && npm run build
-```
-
-Expected: Build succeeds
-
-**Step 4: Commit**
-
-```bash
-git add frontend/src/services/api.ts frontend/src/pages/JwtDebugger/index.jsx
-git commit -m "feat: migrate JwtDebugger to use generated API clients"
-```
-
----
-
-## Task 7: Testing and Validation
-
-**Files:**
-- Create: `pkg/router/integration_test.go`
-
-**Step 1: Create integration tests**
-
-Write file: `pkg/router/integration_test.go`
-```go
-package router
-
-import (
-	"bytes"
-	"encoding/json"
-	"net/http"
-	"net/http/httptest"
-	"testing"
-
-	"github.com/gin-gonic/gin"
-	"github.com/stretchr/testify/assert"
-)
-
-// Integration test with real services
-func TestIntegration_AllServices(t *testing.T) {
-	gin.SetMode(gin.TestMode)
-	
-	server := NewServer()
-	
-	// Register all services
-	jwtSvc := &mockJWTService{}
-	server.Register(jwtSvc)
-	
-	// Test health endpoint
-	w := httptest.NewRecorder()
-	req, _ := http.NewRequest("GET", "/health", nil)
-	server.Engine().ServeHTTP(w, req)
-	
-	assert.Equal(t, http.StatusOK, w.Code)
-	
-	var health map[string]interface{}
-	json.Unmarshal(w.Body.Bytes(), &health)
-	assert.Equal(t, "ok", health["status"])
-	assert.Equal(t, "web", health["mode"])
-}
-
-// Mock services for testing
-type mockJWTService struct{}
-
-type mockDecodeRequest struct {
-	Token string `json:"token"`
-}
-
-type mockDecodeResponse struct {
-	Valid bool   `json:"valid"`
-	Error string `json:"error,omitempty"`
-}
-
-func (s *mockJWTService) Decode(req mockDecodeRequest) mockDecodeResponse {
-	if req.Token == "" {
-		return mockDecodeResponse{Valid: false, Error: "empty token"}
-	}
-	return mockDecodeResponse{Valid: true}
-}
-
-func TestIntegration_JWTDecode(t *testing.T) {
-	gin.SetMode(gin.TestMode)
-	
-	server := NewServer()
-	jwtSvc := &mockJWTService{}
-	server.Register(jwtSvc)
-	
-	// Test decode endpoint
-	reqBody := map[string]string{"token": "test.jwt.token"}
-	body, _ := json.Marshal(reqBody)
-	
-	w := httptest.NewRecorder()
-	req, _ := http.NewRequest("POST", "/api/mock-jwt-service/decode", bytes.NewBuffer(body))
-	req.Header.Set("Content-Type", "application/json")
-	
-	server.Engine().ServeHTTP(w, req)
-	
-	assert.Equal(t, http.StatusOK, w.Code)
-	
-	var resp mockDecodeResponse
-	json.Unmarshal(w.Body.Bytes(), &resp)
-	assert.True(t, resp.Valid)
-}
-```
-
-**Step 2: Run integration tests**
-
-```bash
-cd pkg/router && go test -v -run Integration
-```
-
-Expected: Tests pass
-
-**Step 3: Test HTTP server startup**
-
-```bash
-go run . &
-sleep 2
-curl http://localhost:8081/health
-```
-
-Expected:
-```json
-{"mode":"web","status":"ok","time":"2026-02-09T..."}
-```
-
-**Step 4: Test API endpoint**
-
-```bash
-curl -X POST http://localhost:8081/api/jwt-service/decode \
-  -H "Content-Type: application/json" \
-  -d '{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"}'
-```
-
-Expected: Valid JWT response
-
-**Step 5: Kill background server and commit**
-
-```bash
-pkill -f "go run ."
-git add pkg/router/integration_test.go
-git commit -m "test: add integration tests for HTTP API"
-```
-
----
-
-## Task 8: Documentation and Final Steps
-
-**Files:**
-- Create: `docs/BROWSER_MODE.md`
-- Modify: `README.md`
-
-**Step 1: Create browser mode documentation**
-
-Write file: `docs/BROWSER_MODE.md`
-```markdown
-# Browser Mode
-
-DevToolbox can run in web browsers alongside the desktop application.
-
-## How It Works
-
-When you start the desktop app, it also starts an HTTP server on port 8081. You can open `http://localhost:8081` in any browser to use the tools.
-
-## Architecture
-
-- **Desktop Mode**: Uses Wails runtime bindings
-- **Browser Mode**: Uses HTTP API with Gin server
-- **Auto-Discovery**: New services are automatically exposed via HTTP
-- **Code Generation**: TypeScript clients are auto-generated from Go code
-
-## API Endpoints
-
-All services are available at `/api/{service-name}/{method-name}`:
-
-- `POST /api/jwt-service/decode` - Decode JWT tokens
-- `POST /api/conversion-service/convert` - Convert between formats
-- `POST /api/barcode-service/generate-barcode` - Generate barcodes
-- etc.
-
-## Development
-
-### Adding a New Service
-
-1. Create your service in `service/` directory
-2. Register it in `main.go`: `server.Register(&MyService{})`
-3. Run the generator: `go run cmd/genservices/main.go`
-4. Import the generated client: `import { myService } from '../generated'`
-
-### Regenerating Clients
-
-```bash
-go run cmd/genservices/main.go
-```
-
-This updates `frontend/src/generated/` with the latest TypeScript clients.
-
-### Testing Browser Mode
-
-1. Start the app: `go run .`
-2. Open browser: `http://localhost:8081`
-3. The same frontend works in both modes!
-```
-
-**Step 2: Update README.md**
-
-Add to README.md:
-```markdown
-## Browser Support
-
-DevToolbox works in both desktop and browser modes:
-
-- **Desktop**: Native Wails application with native performance
-- **Browser**: Access via `http://localhost:8081` when the app is running
-
-The frontend automatically detects the environment and uses the appropriate API (Wails runtime for desktop, HTTP for browser).
-```
-
-**Step 3: Commit documentation**
-
-```bash
-git add docs/BROWSER_MODE.md README.md
-git commit -m "docs: add browser mode documentation"
-```
-
-**Step 4: Final verification**
-
-```bash
-go test ./pkg/router/...
-go build .
-```
-
-Expected: All tests pass, build succeeds
-
-**Step 5: Final commit and summary**
-
-```bash
-git log --oneline -10
-```
-
-Expected: All commits visible
-
----
-
-## Summary of Changes
-
-1. **pkg/router/** - Auto-discovery Gin router with reflection
-2. **cmd/genservices/** - TypeScript client generator tool
-3. **server.go** - Updated to use new router
-4. **main.go** - Dual-mode startup (Wails + HTTP)
-5. **frontend/src/generated/** - Auto-generated TypeScript clients
-6. **frontend/src/services/api.ts** - Unified facade
-
-## Next Steps (Optional)
-
-1. Update remaining frontend components to use generated clients
-2. Add OpenAPI spec generation
-3. Add authentication for HTTP API
-4. Serve static frontend files from Gin for standalone web deployment
-
-## Testing Checklist
-
-- [ ] HTTP server starts on port 8081
-- [ ] Health endpoint returns 200
-- [ ] JWT decode endpoint works
-- [ ] All service endpoints accessible
-- [ ] Frontend builds successfully
-- [ ] Generated clients compile
-- [ ] Wails mode still works
-- [ ] Browser mode works
-```
\ No newline at end of file
diff --git a/docs/plans/2026-03-01-datetime-converter-improvements-design.md b/docs/plans/2026-03-01-datetime-converter-improvements-design.md
deleted file mode 100644
index e7a4968..0000000
--- a/docs/plans/2026-03-01-datetime-converter-improvements-design.md
+++ /dev/null
@@ -1,82 +0,0 @@
-# DateTime Converter Improvements Design
-
-## Overview
-Redesign the DateTime Converter tool to match reference design with labeled output fields, math operators support, and persistent custom timezones.
-
-## Goals
-- Replace grid layout with explicit labeled output fields
-- Add math operators (+, -, *, /) for timestamp calculations
-- Support persistent custom timezones across browser and Wails environments
-
-## Phase 1: Core Layout (Priority: High)
-
-### Layout Structure
-Three distinct zones following AGENTS.md guidelines:
-
-**Header Zone**
-- Tool title "DateTime Converter"
-- Description text
-
-**Control Zone**
-- Preset buttons: Now, Start of Day, End of Day, Tomorrow, Yesterday, Next Week
-- Input field with placeholder text
-- Input timezone selector
-- Clear button
-- Math operators helper text
-
-**Workspace Zone (Two-column layout)**
-- Left column: Primary outputs (Local, UTC ISO 8601, Relative, Unix time)
-- Right column: Metadata (Day of year, Week of year, Is leap year, Other formats)
-
-Each field gets labeled box with copy button.
-
-### Component Changes
-- New `OutputField` component: Label + value + copy button
-- Modify `DateTimeConverter/index.jsx`: Reorganize layout, add math parser
-- Update styling to match reference design proportions
-
-## Phase 2: Timezone Storage (Priority: High)
-
-### Storage Interface
-```javascript
-const storage = {
-  get: (key) => localStorage.getItem(key) || wailsGet(key),
-  set: (key, value) => { localStorage.setItem(key, value); wailsSet(key, value); }
-};
-```
-
-### Data Structure
-```javascript
-{
-  "datetime-converter.timezones": ["Asia/Tokyo", "Europe/London"]
-}
-```
-
-### Behavior
-- "Add timezone" dropdown below main outputs
-- Selected timezones render as additional output fields
-- Remove button (×) on each field
-- Persist to both localStorage and Wails backend
-
-## Phase 3: Math Operators (Priority: Medium)
-
-### Supported Operations
-- `+` addition (e.g., `1738412345 + 3600`)
-- `-` subtraction (e.g., `now - 86400`)
-- `*` multiplication
-- `/` division
-
-### Implementation
-- Regex parser for `number operator number` pattern
-- Real-time calculation on input change
-- Error display for invalid expressions
-
-## Error Handling
-- Invalid date: Red tag "Invalid date or timestamp"
-- Math error: Inline red text below input
-- Timezone error: Fallback to UTC with warning
-
-## Testing Plan
-- Unit tests for math parser
-- Integration tests for storage interface
-- Visual regression for layout changes
diff --git a/docs/plans/2026-03-06-spotlight-improvements-implementation.md b/docs/plans/2026-03-06-spotlight-improvements-implementation.md
deleted file mode 100644
index 6835049..0000000
--- a/docs/plans/2026-03-06-spotlight-improvements-implementation.md
+++ /dev/null
@@ -1,666 +0,0 @@
-# Spotlight Improvements Implementation Plan
-
-> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
-
-**Goal:** Fix global hotkey activation and redesign spotlight results as unified 400px scrollable panel
-
-**Architecture:** Update Go backend to register reliable global hotkey (`Cmd+Shift+Space`/`Ctrl+Shift+Space`), modify window dimensions, and redesign React frontend with unified glassmorphism panel (no gaps) with fixed 400px results area.
-
-**Tech Stack:** Go (Wails v3), React, Carbon Design System, CSS3
-
----
-
-## Overview
-
-This plan implements three key improvements:
-1. **Global hotkey fix** - Change from `Cmd+Ctrl+M` to `Cmd+Shift+Space` for reliability
-2. **Unified results panel** - Redesign as single glass panel with search box + 400px scrollable results
-3. **Navigation enhancement** - Ensure tool pages open with pre-filled options from query params
-
----
-
-## Task 1: Update Global Hotkey Registration
-
-**Files:**
-- Modify: `main.go:268-279`
-- Test: Manual testing (hotkey functionality)
-
-**Step 1: Update hotkey accelerator constant**
-
-Current code (lines 268-273):
-```go
-var hotkeyAccelerator string
-if runtime.GOOS == "darwin" {
-    hotkeyAccelerator = "Cmd+Ctrl+M"
-} else {
-    hotkeyAccelerator = "Ctrl+Alt+M"
-}
-```
-
-Replace with:
-```go
-var hotkeyAccelerator string
-if runtime.GOOS == "darwin" {
-    hotkeyAccelerator = "Cmd+Shift+Space"
-} else {
-    hotkeyAccelerator = "Ctrl+Shift+Space"
-}
-```
-
-**Step 2: Update tray menu hotkey label**
-
-Current code (line 256):
-```go
-trayMenu.Add("Open Spotlight (Cmd+Ctrl+M)").OnClick(func(ctx *application.Context) {
-```
-
-Replace with:
-```go
-trayMenu.Add("Open Spotlight (Cmd+Shift+Space)").OnClick(func(ctx *application.Context) {
-```
-
-**Step 3: Verify hotkey registration error handling**
-
-Ensure this comment exists (line 278):
-```go
-// Note: Wails v3 doesn't return an error from KeyBinding.Add - errors are logged internally
-```
-
-**Step 4: Test hotkey**
-
-Run: `go run .` or `wails dev`
-Expected: Application starts without errors
-Test: Press `Cmd+Shift+Space` (macOS) or `Ctrl+Shift+Space` (Windows/Linux)
-Expected: Spotlight window toggles (show/hide)
-
-**Step 5: Commit**
-
-```bash
-git add main.go
-git commit -m "fix(spotlight): change global hotkey to Cmd+Shift+Space for reliability"
-```
-
----
-
-## Task 2: Update Spotlight Window Dimensions
-
-**Files:**
-- Modify: `main.go:154-180`
-- Modify: `frontend/src/spotlight.css:12`
-- Test: Visual verification
-
-**Step 1: Update window height in main.go**
-
-Current code (line 157):
-```go
-Height:    80,
-```
-
-Replace with:
-```go
-Height:    480, // 80px search + 400px results
-```
-
-**Step 2: Add min/max height constraints**
-
-Add to `application.WebviewWindowOptions` (after line 158):
-```go
-MinHeight: 80,
-MaxHeight: 480,
-```
-
-**Step 3: Update CSS to center spotlight vertically**
-
-Current code (`frontend/src/spotlight.css` line 12):
-```css
-padding-top: 20vh;
-```
-
-Replace with:
-```css
-padding-top: 15vh; /* Adjusted for taller window */
-```
-
-**Step 4: Build and test**
-
-Run: `wails build` or test in dev mode
-Expected: Spotlight window opens at 480px height
-
-**Step 5: Commit**
-
-```bash
-git add main.go frontend/src/spotlight.css
-git commit -m "feat(spotlight): update window height to 480px for results panel"
-```
-
----
-
-## Task 3: Redesign Results Panel as Unified Component
-
-**Files:**
-- Modify: `frontend/src/components/SpotlightPalette.jsx:333-382`
-- Modify: `frontend/src/components/SpotlightPalette.css:1-157`
-- Test: Visual verification
-
-**Step 1: Update container structure in JSX**
-
-Current code (lines 333-382 in SpotlightPalette.jsx):
-```jsx
-return (
-    
-
- {/* Search input */} -
- -
- {/* Results list */} -
-
- ); -``` - -Replace with: -```jsx -return ( -
-
-
- - - {searchQuery && ( - - )} -
-
- -
- {commands.length === 0 ? ( -
No commands found matching "{searchQuery}"
- ) : ( -
- {commands.map((command, index) => { - const Icon = command.icon || null; - return ( -
executeCommand(command)} - onMouseEnter={() => setSelectedIndex(index)} - role="option" - aria-selected={index === selectedIndex} - > -
- {Icon && } - {command.label} -
- {command.category} -
- ); - })} -
- )} -
-
- ); -``` - -**Step 2: Update CSS for unified panel design** - -Replace entire content of `frontend/src/components/SpotlightPalette.css`: - -```css -/* Unified spotlight container */ -.spotlight-container { - width: 640px; - max-width: 90vw; - background: rgba(30, 30, 30, 0.35); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 16px; - box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.03); - overflow: hidden; - backdrop-filter: blur(32px) saturate(200%); - -webkit-backdrop-filter: blur(32px) saturate(200%); - display: flex; - flex-direction: column; -} - -/* Search section */ -.spotlight-search-section { - padding: 1.25rem 1.5rem; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); - flex-shrink: 0; -} - -.spotlight-search-box { - display: flex; - align-items: center; - gap: 0.75rem; -} - -.spotlight-search-icon { - color: var(--cds-text-secondary); - flex-shrink: 0; - opacity: 0.7; -} - -.spotlight-input { - flex: 1; - background: transparent; - border: none; - color: var(--cds-text-primary); - font-size: 1.25rem; - font-weight: 400; - padding: 0; - outline: none; - font-family: var(--cds-font-sans); - letter-spacing: -0.01em; -} - -.spotlight-input::placeholder { - color: var(--cds-text-secondary); - opacity: 0.6; -} - -.spotlight-clear-btn { - background: transparent; - border: none; - color: var(--cds-text-secondary); - cursor: pointer; - padding: 0.25rem; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - transition: all 0.15s ease; - opacity: 0.7; -} - -.spotlight-clear-btn:hover { - background: var(--cds-layer-hover); - opacity: 1; -} - -/* Results section - fixed 400px height */ -.spotlight-results-section { - height: 400px; - overflow: hidden; - flex-shrink: 0; -} - -.spotlight-empty { - padding: 2.5rem 1.5rem; - text-align: center; - color: var(--cds-text-secondary); - font-size: 0.875rem; - opacity: 0.8; - height: 100%; - display: flex; - align-items: center; - justify-content: center; -} - -.spotlight-list { - overflow-y: auto; - height: 100%; - padding: 0.5rem 0; -} - -.spotlight-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.625rem 1.5rem; - cursor: pointer; - transition: all 0.12s ease; - margin: 0 0.5rem; - border-radius: 6px; -} - -.spotlight-item:hover, -.spotlight-item.selected { - background: var(--cds-layer-hover); -} - -.spotlight-item.selected { - background: var(--cds-layer-selected); -} - -.spotlight-item-content { - display: flex; - align-items: center; - gap: 0.875rem; - flex: 1; - min-width: 0; -} - -.spotlight-item-icon { - color: var(--cds-text-secondary); - flex-shrink: 0; - opacity: 0.8; -} - -.spotlight-item-label { - color: var(--cds-text-primary); - font-size: 0.9375rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font-weight: 400; -} - -.spotlight-item-category { - color: var(--cds-text-secondary); - font-size: 0.6875rem; - text-transform: uppercase; - letter-spacing: 0.04em; - flex-shrink: 0; - margin-left: 1rem; - padding: 0.25rem 0.625rem; - background: var(--cds-layer-active); - border-radius: 4px; - font-weight: 500; - opacity: 0.8; -} - -/* Scrollbar styling */ -.spotlight-list::-webkit-scrollbar { - width: 6px; -} - -.spotlight-list::-webkit-scrollbar-track { - background: transparent; - margin: 0.5rem 0; -} - -.spotlight-list::-webkit-scrollbar-thumb { - background: var(--cds-border-subtle); - border-radius: 3px; -} - -.spotlight-list::-webkit-scrollbar-thumb:hover { - background: var(--cds-text-secondary); -} -``` - -**Step 3: Test visual appearance** - -Run: `wails dev` -Expected: -- Spotlight window opens at 480px height -- Search box at top with proper padding -- Results area below with 400px fixed height -- No gap between search and results sections -- Unified glassmorphism effect across both sections -- Border-radius applies to entire container - -**Step 4: Test scroll behavior** - -Test: Type "format" to filter results -Expected: -- Results list shows matching commands -- If results exceed visible area, scrollbar appears -- Scrolling works smoothly with mouse/trackpad -- Keyboard navigation (↑/↓) scrolls selected item into view - -**Step 5: Commit** - -```bash -git add frontend/src/components/SpotlightPalette.jsx frontend/src/components/SpotlightPalette.css -git commit -m "feat(spotlight): redesign results panel as unified 400px scrollable component" -``` - ---- - -## Task 4: Verify Navigation with Pre-filled Options - -**Files:** -- Review: `main.go:192-203` -- Review: `frontend/src/components/SpotlightPalette.jsx:267-294` -- Test: End-to-end functionality - -**Step 1: Review existing navigation flow** - -Verify `main.go` lines 192-203: -```go -// Listen for spotlight navigation events -app.Event.On("spotlight:command-selected", func(event *application.CustomEvent) { - path := event.Data.(string) - log.Printf("Spotlight command selected: %s", path) - - // Show and focus main window - mainWindow.Show() - mainWindow.Focus() - - // Emit navigation event to frontend - mainWindow.EmitEvent("navigate:to", path) -}) -``` - -**Step 2: Review command path format in SpotlightPalette.jsx** - -Verify commands include query params (lines 8-145): -- `formatter-json` path: `/tool/code-formatter?format=json` -- `converter-base64` path: `/tool/text-converter?category=Encode%20-%20Decode&method=Base64` -- etc. - -**Step 3: Verify spotlight closes immediately** - -Verify `executeCommand` function (lines 267-294): -```javascript -const executeCommand = useCallback( - (command) => { - saveRecentCommand(command.id); - - if (command.action) { - // Handle actions... - } else if (command.path) { - // Emit command selected event with path - window.runtime?.EventsEmit?.('spotlight:command-selected', command.path); - } - - // Close spotlight - window.runtime?.EventsEmit?.('spotlight:close'); - }, - [saveRecentCommand] -); -``` - -**Step 4: Test end-to-end navigation** - -Run: `wails dev` -Test: -1. Open spotlight with hotkey (`Cmd+Shift+Space`) -2. Type "json" and select "Format JSON" -3. **Expected behavior:** - - Spotlight closes immediately - - Main window opens/focuses - - Main window navigates to `/tool/code-formatter?format=json` - - Code formatter tool opens with "JSON" format pre-selected - -**Step 5: Test another command** - -Test: -1. Open spotlight -2. Select "Base64 Encode/Decode" -3. **Expected:** Main window opens with Text Converter tool, "Encode - Decode" category and "Base64" method pre-selected - -**Step 6: Commit (if any fixes needed)** - -If fixes were required: -```bash -git add -git commit -m "fix(spotlight): ensure navigation with pre-filled options works correctly" -``` - -Otherwise, no commit needed (functionality already working). - ---- - -## Task 5: Add CSS Transition for Smooth Appearance - -**Files:** -- Modify: `frontend/src/components/SpotlightPalette.css` -- Test: Visual smoothness - -**Step 1: Add transition to container** - -Add to `.spotlight-container` in CSS: -```css -.spotlight-container { - /* ... existing styles ... */ - transition: all 0.2s ease-out; -} -``` - -**Step 2: Add subtle fade-in for results** - -Add to `.spotlight-results-section`: -```css -.spotlight-results-section { - /* ... existing styles ... */ - animation: fadeIn 0.15s ease-out; -} - -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(-4px); - } - to { - opacity: 1; - transform: translateY(0); - } -} -``` - -**Step 3: Test transitions** - -Run: `wails dev` -Test: Open spotlight, type to filter results -Expected: Smooth fade-in animation when results appear - -**Step 4: Commit** - -```bash -git add frontend/src/components/SpotlightPalette.css -git commit -m "feat(spotlight): add smooth transitions for panel appearance" -``` - ---- - -## Task 6: Final Integration Testing - -**Files:** -- All modified files -- Test: Complete user flows - -**Step 1: Test complete hotkey flow** - -1. Start application fresh -2. Press `Cmd+Shift+Space` (macOS) or `Ctrl+Shift+Space` (Windows/Linux) -3. Expected: Spotlight opens centered on screen -4. Press same hotkey again -5. Expected: Spotlight closes -6. Repeat 3 times to ensure reliability - -**Step 2: Test search and selection flow** - -1. Open spotlight -2. Type "format" -3. Use arrow keys to navigate results -4. Press Enter to select -5. Expected: Spotlight closes, main window opens with correct tool pre-filled - -**Step 3: Test mouse interaction** - -1. Open spotlight -2. Type "base64" -3. Click on "Base64 Encode/Decode" result -4. Expected: Same behavior as keyboard selection - -**Step 4: Test empty state** - -1. Open spotlight -2. Type "xyz123" (nonsense query) -3. Expected: "No commands found" message displayed centered in results area - -**Step 5: Test Esc key** - -1. Open spotlight -2. Press Escape key -3. Expected: Spotlight closes - -**Step 6: Run build to ensure no errors** - -Run: `wails build` -Expected: Build completes successfully with no errors - -**Step 7: Final commit** - -```bash -git add . -git commit -m "feat(spotlight): complete improvements - reliable hotkey, unified panel, pre-filled navigation" -``` - ---- - -## Testing Checklist - -Verify all these before considering complete: - -- [ ] Hotkey `Cmd+Shift+Space` / `Ctrl+Shift+Space` toggles spotlight reliably -- [ ] Tray menu shows updated hotkey label -- [ ] Spotlight window opens at 480px height -- [ ] Results panel displays at fixed 400px height -- [ ] No gap between search and results sections -- [ ] Unified glassmorphism styling across entire panel -- [ ] Results scroll smoothly when exceeding visible area -- [ ] Selecting result closes spotlight immediately -- [ ] Main window opens/focuses after selection -- [ ] Tool opens with correct options pre-filled -- [ ] Keyboard navigation works (↑↓ arrows, Enter, Esc) -- [ ] Mouse click selection works -- [ ] Empty state displays correctly -- [ ] Build completes without errors - ---- - -## Notes - -**Query Params Handling:** -The tool pages should already handle query params (they were designed this way). If pre-filling doesn't work: -- Check that tools read query params on mount -- Verify `navigate:to` event is being received in main window -- Ensure React Router properly parses query strings - -**Hotkey Conflicts:** -If `Cmd+Shift+Space` conflicts with system shortcuts on any platform: -- Fallback options: `Cmd+Option+Space`, `Ctrl+Option+Space`, `F1` -- These would require updating both main.go and tray menu label - -**Performance:** -- Command list is static (no API calls) -- Fuzzy search runs client-side -- Should be instant even with 50+ commands - ---- - -## Success Criteria - -✅ **Done when:** -1. Global hotkey works reliably on all platforms -2. Results panel is unified glass panel with no gaps -3. Fixed 400px scrollable results area -4. Tool navigation with pre-filled options works end-to-end -5. All tests pass and build succeeds diff --git a/docs/superpowers/plans/2026-03-28-sidebar-styling-fix.md b/docs/superpowers/plans/2026-03-28-sidebar-styling-fix.md new file mode 100644 index 0000000..4a11592 --- /dev/null +++ b/docs/superpowers/plans/2026-03-28-sidebar-styling-fix.md @@ -0,0 +1,526 @@ +# Sidebar Tailwind/Radix Styling Fix Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix all sidebar styling issues to match the target design: darker background, gradient logo, search icon, re-enabled Quick Access, category icons, active state accents. + +**Architecture:** Modify Sidebar.jsx component with Tailwind classes, no new components needed. Use existing Lucide icons and Tailwind zinc/blue palette. + +**Tech Stack:** React, Tailwind CSS 4.0, Lucide React icons + +--- + +## File Structure + +| File | Action | Description | +|------|--------|-------------| +| `frontend/src/components/Sidebar.jsx` | Modify | All styling changes in one file | +| `frontend/src/components/ui/input.jsx` | Modify | Add icon prefix support (optional refactor) | + +--- + +## Task 1: Fix Sidebar Container Background + +**Files:** +- Modify: `frontend/src/components/Sidebar.jsx` + +- [ ] **Step 1: Update sidebar container classes** + +Replace the `aside` element's className to use darker background: + +```jsx +// Before: +