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 ( -
` 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 ( - -- ); -``` - -Replace with: -```jsx -return ( -- {/* Search input */} -- -- {/* Results list */} ---- ); -``` - -**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-- ---- - {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.category} -} - {command.label} - -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: +