diff --git a/docs/plans/2025-01-27-number-converter-implementation.md b/docs/plans/2025-01-27-number-converter-implementation.md new file mode 100644 index 0000000..4f99bab --- /dev/null +++ b/docs/plans/2025-01-27-number-converter-implementation.md @@ -0,0 +1,485 @@ +# Number Converter Implementation Plan + +**Design Document:** `docs/plans/2025-01-27-number-converter-redesign.md` +**Goal:** Implement visual bit editor with parallel workstreams + +--- + +## Workstream Overview + +This implementation is divided into **7 parallel workstreams**. Each can be developed independently and tested in isolation before integration. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ WORKSTREAM 1 WORKSTREAM 2 WORKSTREAM 3 │ +│ State Management Utility Functions BitGrid │ +│ (Reducer) (Conversions) Component │ +│ │ +│ WORKSTREAM 4 WORKSTREAM 5 WORKSTREAM 6 │ +│ ConversionCard BitwiseToolbar Constants │ +│ Component Component & Types │ +│ │ +│ WORKSTREAM 7 │ +│ Main Integration │ +│ & Final Assembly │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Workstream 1: State Management (Reducer) +**File:** `frontend/src/pages/NumberConverter/numberConverterReducer.js` + +**Deliverables:** +```javascript +// Initial state +const initialState = { + value: 0, + inputMode: 'decimal', + customBase: 36, + errors: {} +}; + +// Action types +const actions = { + SET_VALUE, + TOGGLE_BIT, + SET_INPUT_MODE, + SET_CUSTOM_BASE, + SET_ERROR, + CLEAR_ERROR, + APPLY_BITWISE_OP +}; + +// Reducer function +function numberConverterReducer(state, action) { ... } + +// Action creators +const actionCreators = { + setValue, + toggleBit, + setInputMode, + setCustomBase, + setError, + clearError, + applyBitwiseOp +}; +``` + +**Test in isolation:** +```javascript +// Test cases needed: +- Toggle bit 0 on 0 → should be 1 +- Toggle bit 0 on 1 → should be 0 +- Set value to 255 → all conversions should update +- Set error on hex input → error object populated +- Clear error → error object empty +``` + +**Dependencies:** None (pure logic) + +--- + +## Workstream 2: Utility Functions +**File:** `frontend/src/pages/NumberConverter/utils.js` + +**Deliverables:** +```javascript +// Parsing (string → number) +export function parseInput(input, base); +export function parseHex(input); +export function parseBinary(input); +export function parseOctal(input); +export function parseDecimal(input); +export function parseCustomBase(input, base); + +// Formatting (number → string) +export function formatNumber(value, base); +export function formatHex(value); +export function formatBinary(value); +export function formatOctal(value); +export function formatDecimal(value); +export function formatCustomBase(value, base); + +// Bit manipulation +export function toggleBit(value, position); +export function shiftLeft(value, n); +export function shiftRight(value, n); +export function bitwiseNot(value); +export function bitwiseAnd(value, mask); +export function bitwiseOr(value, mask); + +// Validation +export function validateInput(input, base); +export function sanitizeInput(input); +``` + +**Edge Cases to Handle:** +- Empty string returns null +- Whitespace trimmed +- Case insensitivity for hex +- Overflow clamping to 32-bit +- Invalid characters throw with message + +**Test in isolation:** +```javascript +// Test matrix: +Input | Base | Expected Value +"FF" | 16 | 255 +"1010" | 2 | 10 +"377" | 8 | 255 +"255" | 10 | 255 +" 42 " | 10 | 42 (trimmed) +"GG" | 16 | throw Error +"99999999999" | 10 | 4294967295 (clamped) +"-5" | 10 | throw Error +``` + +**Dependencies:** None (pure logic) + +--- + +## Workstream 3: BitGrid Component +**File:** `frontend/src/pages/NumberConverter/components/BitGrid.jsx` + +**Deliverables:** +```jsx +// BitGrid.jsx +export function BitGrid({ value, onToggleBit, layout }) { + // Display 4 rows × 8 bits + // Row 0: bits 31-24 (MSB) + // Row 1: bits 23-16 + // Row 2: bits 15-8 + // Row 3: bits 7-0 (LSB) +} + +// BitCell.jsx (sub-component) +export function BitCell({ + bitValue, // 0 or 1 + position, // 0-31 + onToggle, // callback(position) + isHovered // hover state from parent +}) { + // Clickable 32×32px cell + // Hover: scale(1.1) + // Active: filled with primary color + // Inactive: outlined +} +``` + +**Props Interface:** +```typescript +interface BitGridProps { + value: number; // 32-bit value + onToggleBit: (position: number) => void; + layout: 'horizontal' | 'vertical'; +} +``` + +**Visual Specs:** +- Cell size: 32×32px +- Gap: 4px +- Active color: `var(--cds-interactive-01)` +- Inactive border: `var(--cds-border-strong)` +- Row label font: monospace, `var(--cds-text-secondary)` + +**Test in isolation:** +```javascript +- Renders 32 cells +- Clicking cell calls onToggleBit with correct position +- Value 0xFF shows bits 0-7 as active +- Value 0xFF000000 shows bits 24-31 as active +- Keyboard navigation works (Tab, Space, Enter) +``` + +**Dependencies:** None (uses only Carbon CSS variables) + +--- + +## Workstream 4: ConversionCard Component +**File:** `frontend/src/pages/NumberConverter/components/ConversionCard.jsx` + +**Deliverables:** +```jsx +// ConversionCard.jsx +export function ConversionCard({ + label, // "Decimal", "Hexadecimal", etc. + base, // 10, 16, 2, 8, or custom + value, // Current numeric value + error, // Error message or null + onChange, // callback(newValue) - parse and update + onCopy, // callback() - copy to clipboard + onSync // callback() - sync from this field +}) { + // TextInput with label + // Copy button + // Sync button (sets this as source) + // Error display +} +``` + +**Props Interface:** +```typescript +interface ConversionCardProps { + label: string; + base: number; + value: number; + error?: string; + onChange: (input: string) => void; + onCopy: () => void; + onSync: () => void; +} +``` + +**Features:** +- Monospace font for binary display +- Copy button uses Carbon `Button` with Copy icon +- Sync button to reverse-sync (input becomes source) +- Inline error display below input +- Placeholder shows example: "Enter decimal number..." + +**Test in isolation:** +```javascript +- Typing valid input calls onChange +- Typing invalid input shows error +- Copy button copies formatted value +- Sync button calls onSync +- Error state shows red border +``` + +**Dependencies:** Carbon `TextInput`, `Button`, `InlineNotification` + +--- + +## Workstream 5: BitwiseToolbar Component +**File:** `frontend/src/pages/NumberConverter/components/BitwiseToolbar.jsx` + +**Deliverables:** +```jsx +// BitwiseToolbar.jsx +export function BitwiseToolbar({ onOperation }) { + // Button group: + // [<< 1] [>> 1] [NOT] [& 0xFF] [| 1] +} + +// Operations supported: +// 'shiftLeft': value << 1 +// 'shiftRight': value >>> 1 (logical) +// 'not': ~value +// 'maskByte': value & 0xFF +// 'setLSB': value | 1 +``` + +**Props Interface:** +```typescript +interface BitwiseToolbarProps { + onOperation: (operation: string) => void; +} +``` + +**Visual Specs:** +- Button group with `kind="secondary"` +- Size: `sm` (small) +- Gap: 8px between buttons +- Tooltip on hover showing operation description + +**Test in isolation:** +```javascript +- Clicking [<< 1] calls onOperation('shiftLeft') +- Clicking [NOT] calls onOperation('not') +- All 5 buttons rendered +- Keyboard accessible (Tab, Enter) +``` + +**Dependencies:** Carbon `Button`, `ButtonSet` + +--- + +## Workstream 6: Constants & Types +**File:** `frontend/src/pages/NumberConverter/constants.js` + +**Deliverables:** +```javascript +// Base configurations +export const BASES = { + BINARY: { id: 'bin', label: 'Binary', base: 2 }, + OCTAL: { id: 'oct', label: 'Octal', base: 8 }, + DECIMAL: { id: 'dec', label: 'Decimal', base: 10 }, + HEXADECIMAL: { id: 'hex', label: 'Hexadecimal', base: 16 }, +}; + +// Custom base options (2-36) +export const CUSTOM_BASE_OPTIONS = Array.from({ length: 35 }, (_, i) => ({ + id: `${i + 2}`, + label: `Base ${i + 2}`, + value: i + 2, +})); + +// Bitwise operations +export const BITWISE_OPERATIONS = { + SHIFT_LEFT: { id: 'shiftLeft', label: '<< 1', description: 'Shift left by 1' }, + SHIFT_RIGHT: { id: 'shiftRight', label: '>> 1', description: 'Shift right by 1' }, + NOT: { id: 'not', label: 'NOT', description: 'Flip all bits' }, + MASK_BYTE: { id: 'maskByte', label: '& 0xFF', description: 'Keep lowest byte' }, + SET_LSB: { id: 'setLSB', label: '| 1', description: 'Set least significant bit' }, +}; + +// Validation messages +export const ERROR_MESSAGES = { + INVALID_CHAR: (char, base) => `Invalid character '${char}' for base ${base}`, + NEGATIVE: 'Negative numbers are not supported', + OVERFLOW: 'Value clamped to 32-bit maximum', + EMPTY: 'Input cannot be empty', +}; + +// Limits +export const MAX_32BIT = 0xFFFFFFFF; // 4,294,967,295 +export const MIN_32BIT = 0; +``` + +**Dependencies:** None + +--- + +## Workstream 7: Main Integration +**File:** `frontend/src/pages/NumberConverter/index.jsx` + +**Deliverables:** +Complete page component integrating all workstreams: + +```jsx +export default function NumberConverter() { + // State + const [state, dispatch] = useReducer(numberConverterReducer, initialState); + const layout = useLayoutToggle({ toolKey: 'number-converter', ... }); + + // Handlers + const handleToggleBit = (position) => dispatch(toggleBit(position)); + const handleConversionInput = (base, input) => { ... }; + const handleBitwiseOp = (operation) => { ... }; + const handleCopy = (value) => navigator.clipboard.writeText(value); + + // Render + return ( +
+ + + + +
+ + +
+ + +
+ {Object.values(BASES).map(baseConfig => ( + handleConversionInput(baseConfig.base, input)} + onCopy={() => handleCopy(formatNumber(state.value, baseConfig.base))} + onSync={() => dispatch(setInputMode(baseConfig.id))} + /> + ))} + +
+
+
+ ); +} +``` + +**Integration Checklist:** +- [ ] Import all workstream components +- [ ] Wire up reducer and actions +- [ ] Connect layout toggle +- [ ] Handle all user interactions +- [ ] Display errors from state +- [ ] Format values for display +- [ ] Add copy functionality + +**Dependencies:** ALL other workstreams + +--- + +## Implementation Order + +### Phase 1: Foundation (Parallel) +Workstreams 1, 2, and 6 can start immediately and run in parallel: +- **WS1** (Reducer) - No dependencies +- **WS2** (Utils) - No dependencies +- **WS6** (Constants) - No dependencies + +### Phase 2: Components (Parallel) +Once Phase 1 is done, workstreams 3, 4, and 5 can run in parallel: +- **WS3** (BitGrid) - Uses WS2 for bit manipulation +- **WS4** (ConversionCard) - Uses WS2 for formatting +- **WS5** (BitwiseToolbar) - No dependencies + +### Phase 3: Integration (Sequential) +Workstream 7 depends on ALL others: +- **WS7** (Main) - Uses WS1, WS2, WS3, WS4, WS5, WS6 + +--- + +## Testing Strategy + +### Unit Tests (Per Workstream) +Each workstream should include unit tests before integration: + +**WS1 (Reducer):** Test all state transitions +**WS2 (Utils):** Test conversion functions with edge cases +**WS3 (BitGrid):** Test rendering and interactions +**WS4 (ConversionCard):** Test input handling and validation +**WS5 (BitwiseToolbar):** Test button callbacks +**WS6 (Constants):** N/A - just definitions + +### Integration Tests +After WS7, test the complete flow: +- End-to-end conversion scenarios +- Bit manipulation workflows +- Error handling paths +- Accessibility compliance + +### Visual Regression Tests +- Screenshot tests for different values +- Layout toggle states +- Error states +- Dark/light mode + +--- + +## Success Criteria + +✅ All 7 workstreams complete +✅ Unit tests pass for each workstream +✅ Integration tests pass +✅ No console errors +✅ Accessibility audit passes +✅ Visual design matches spec +✅ Performance: <100ms for bit toggle feedback + +--- + +## Notes for Agents + +1. **Work independently** - Each workstream can be developed without blocking others +2. **Use mocks** - If a dependency isn't ready, mock it with the expected interface +3. **Write tests first** - Each workstream should be testable in isolation +4. **Document exports** - Make sure the interface is clear for integration +5. **Follow Carbon** - Use existing Carbon components and CSS variables +6. **Check AGENTS.md** - Follow project conventions for UI patterns + +**Ready to start?** Pick any Phase 1 workstream and begin implementation. diff --git a/docs/plans/2025-01-27-number-converter-redesign.md b/docs/plans/2025-01-27-number-converter-redesign.md new file mode 100644 index 0000000..d8b0bd1 --- /dev/null +++ b/docs/plans/2025-01-27-number-converter-redesign.md @@ -0,0 +1,306 @@ +# Number Converter Redesign - Visual Bit Editor + +**Date:** 2025-01-27 +**Status:** Design Complete +**Approach:** Visual Bit Editor with Interactive Bitwise Operations + +--- + +## 1. Overview & Architecture + +### Concept +Transform the Number Converter from a form-filling task into an interactive exploration tool. Users interact with a visual 32-bit representation where they can toggle individual bits, perform bitwise operations, and instantly see conversions across all bases. + +### Core Architecture +- **32-bit representation as source of truth** - All conversions derive from a single 32-bit unsigned integer +- **Bidirectional input** - Click bits in the grid OR type into conversion fields; changes sync both ways +- **Visual hierarchy** - Bit grid dominates (4 rows × 8 bits), color-coded by byte significance +- **Simple bitwise operations** - One-click toolbar for common operations + +### Technology Stack +- React with Carbon Design System (existing) +- No new dependencies +- Layout toggle with localStorage persistence + +--- + +## 2. Components & UI Structure + +### Layout +Split-pane with layout toggle (horizontal/vertical), following existing `useLayoutToggle` pattern. + +### Left Pane - Visual Bit Grid (60% width) +**Header:** +- "Bit Pattern" label +- Bit position indicators (31-0) +- Byte hex summaries + +**Grid:** +``` +Row 0 (31-24): ■ □ □ ■ □ □ ■ □ [0x4A] +Row 1 (23-16): □ ■ □ □ ■ □ □ ■ [0x25] +Row 2 (15-8): ■ ■ □ □ □ ■ □ □ [0xC3] +Row 3 (7-0): □ □ ■ ■ □ □ ■ □ [0x32] +``` + +**Bit Cells (32×32px):** +- `1` = filled with `--cds-interactive-01` (primary accent) +- `0` = outlined with `--cds-border-strong` +- Hover: scale(1.1) with shadow +- Click: toggle bit + +**Toolbar (above grid):** +- `<< 1` - Shift left +- `>> 1` - Shift right +- `NOT` - Flip all bits +- `& 0xFF` - Mask to byte +- `| 1` - Set LSB + +### Right Pane - Conversion Cards (40% width) +Stacked cards showing derived values: +1. **Decimal** (largest, most prominent) +2. **Hexadecimal** +3. **Binary** (monospace, wrapped) +4. **Octal** +5. **Custom Base** (dropdown 2-36) + +Each card: label, input field, copy button, "sync" button to reverse-sync + +--- + +## 3. Data Flow & State Management + +### State Shape +```typescript +interface NumberConverterState { + value: number; // 32-bit unsigned integer (0 to 2^32-1) + inputMode: 'decimal' | 'hex' | 'binary' | 'octal' | 'custom'; + customBase: number; // 2-36 + errors: { + decimal?: string; + hex?: string; + binary?: string; + octal?: string; + custom?: string; + }; + layout: 'horizontal' | 'vertical'; +} +``` + +### Data Flow Patterns + +**1. Bit Grid Click:** +``` +User clicks bit at position N +↓ +Calculate: newValue = currentValue ^ (1 << N) +↓ +Update state.value +↓ +Re-render all conversion displays +``` + +**2. Conversion Input:** +``` +User types in Hex input field +↓ +Parse with base 16 +↓ +Valid: Update state.value, clear error +Invalid: Show error inline, keep last valid value +↓ +Bit grid re-renders from new value +``` + +**3. Bitwise Operation:** +``` +User clicks "<< 1" button +↓ +Calculate: newValue = (currentValue << 1) & 0xFFFFFFFF +↓ +Update state.value +↓ +All displays update +``` + +### Performance Optimizations +- `useMemo` for expensive base conversions +- Bit grid uses CSS transforms (GPU-accelerated) +- No unnecessary re-renders + +--- + +## 4. Bitwise Operations + +### Operation Toolbar +Simple buttons above the bit grid: + +| Button | Operation | Example | +|--------|-----------|---------| +| `<< 1` | Shift left by 1 | `0x0F` → `0x1E` | +| `>> 1` | Logical shift right by 1 | `0xF0` → `0x78` | +| `NOT` | Bitwise NOT | `0x0F` → `0xFFFFFFF0` | +| `& 0xFF` | AND with 0xFF | `0xABCD` → `0xCD` | +| `\| 1` | OR with 1 | `0xFE` → `0xFF` | + +### Interaction +- Click button → operation applies immediately +- No preview mode, no operand input +- Simple undo: click opposite operation +- Visual feedback: button press animation + +--- + +## 5. Error Handling & Edge Cases + +### Input Validation Errors + +**Invalid Characters:** +- Hex input with non-hex chars (G-Z) +- Binary input with digits other than 0-1 +- Octal input with digits 8-9 +- Show: "Invalid character 'X' for base Y" + +**Format Errors:** +- Empty string → valid (no change) +- Whitespace-only → trim or error +- Leading/trailing whitespace → auto-trim + +**Range Errors:** +- Value > 2^32-1 (4,294,967,295) → clamp to max +- Value < 0 → error: "Negative numbers not supported" +- Scientific notation → parse or error + +### Bit Manipulation Edge Cases + +**Shift Operations:** +- Shift by 0 → no change +- Shift by 32+ → wraps (JS behavior: `x << 32 === x`) +- Shift negative → error + +**Identity Operations:** +- `x & x` = x +- `x | x` = x +- `x ^ 0` = x +- Handle gracefully + +**Boundary Values:** +- Bit 31 toggle (0x80000000) +- All zeros (0x00000000) +- All ones (0xFFFFFFFF) +- Single bit set (powers of 2) + +### Display Edge Cases + +**Binary Display:** +- Always show 32 bits for consistency +- Group by 4 bits: `0001 0010 0011 0100` +- Wrap in monospace textarea + +**Decimal Display:** +- Large values: show full precision +- Add thousand separators: `4,294,967,295` + +**Custom Base:** +- Base 36: 0-9, A-Z +- Handle uppercase/lowercase consistently + +### Accessibility + +**Keyboard Navigation:** +- Tab through bit cells +- Space/Enter to toggle bit +- Shift+Tab for reverse navigation + +**Screen Readers:** +- Bit cells: `aria-pressed` for state +- Error messages: `aria-live="polite"` +- Labels on all interactive elements + +### Race Conditions & Performance + +**Rapid Interactions:** +- Debounce rapid bit clicks (50ms) +- Prevent double-submission of operations + +**Copy Operations:** +- Handle copy failure gracefully +- Show toast on success/error + +**Layout Changes:** +- Maintain scroll position on layout toggle +- Responsive bit grid sizing + +--- + +## 6. Testing Checklist + +### Functional Tests +- [ ] All base conversions work bidirectionally +- [ ] Bit toggles update all displays instantly +- [ ] Each bitwise operation produces correct result +- [ ] Invalid input shows appropriate error +- [ ] Copy button copies correct value + +### Edge Case Tests +- [ ] Maximum value (0xFFFFFFFF) +- [ ] Zero value +- [ ] Negative input rejected +- [ ] Overflow clamped +- [ ] Whitespace trimmed +- [ ] Case insensitivity (hex) + +### UX Tests +- [ ] Layout toggle persists +- [ ] Keyboard navigation works +- [ ] Touch targets adequate size +- [ ] Hover states visible +- [ ] Error messages clear + +### Accessibility Tests +- [ ] Screen reader announces bit state +- [ ] Focus visible on all interactive elements +- [ ] Color not sole error indicator + +--- + +## 7. Implementation Notes + +### File Structure +``` +frontend/src/pages/NumberConverter/ +├── index.jsx # Main component +├── numberConverterReducer.js # State management +├── utils.js # Conversion utilities +├── constants.js # Base configs +└── components/ + ├── BitGrid.jsx # Visual bit grid + ├── BitCell.jsx # Individual bit toggle + ├── ConversionCard.jsx # Input with copy/sync + ├── BitwiseToolbar.jsx # Operation buttons + └── ByteLabel.jsx # Byte hex display +``` + +### Key Utilities Needed +```javascript +// utils.js +export const parseInput = (input, base) => { ... } +export const formatNumber = (value, base) => { ... } +export const toggleBit = (value, position) => { ... } +export const shiftLeft = (value, n) => { ... } +export const shiftRight = (value, n) => { ... } +export const bitwiseNot = (value) => { ... } +``` + +### Carbon Components Used +- `TextInput` - Conversion inputs +- `Button` - Operations, copy +- `Dropdown` - Custom base selector +- `Grid/Column` - Layout +- `InlineNotification` - Errors + +--- + +## Summary + +This redesign transforms the Number Converter from a passive form into an active exploration tool. The visual bit grid makes binary tangible, bitwise operations are one-click away, and all conversions update in real-time. The design maintains consistency with existing Carbon Design System usage while adding engaging interactivity. diff --git a/docs/plans/2026-03-01-datetime-converter-improvements-design.md b/docs/plans/2026-03-01-datetime-converter-improvements-design.md new file mode 100644 index 0000000..e7a4968 --- /dev/null +++ b/docs/plans/2026-03-01-datetime-converter-improvements-design.md @@ -0,0 +1,82 @@ +# 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/frontend/bun.lock b/frontend/bun.lock index 9e6ddad..fa39023 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -19,6 +19,7 @@ "qrcode": "^1.5.4", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.29.0", "sass": "^1.96.0", "sql-formatter": "^15.6.11", "ulid": "^3.0.2", @@ -220,6 +221,8 @@ "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + "@remix-run/router": ["@remix-run/router@1.23.2", "", {}, "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.0", "", { "os": "android", "cpu": "arm" }, "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA=="], @@ -494,6 +497,10 @@ "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], + "react-router": ["react-router@6.30.3", "", { "dependencies": { "@remix-run/router": "1.23.2" }, "peerDependencies": { "react": ">=16.8" } }, "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw=="], + + "react-router-dom": ["react-router-dom@6.30.3", "", { "dependencies": { "@remix-run/router": "1.23.2", "react-router": "6.30.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag=="], + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], diff --git a/frontend/package.json b/frontend/package.json index d701f84..80d0706 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,6 +26,7 @@ "php-serialize": "^5.1.3", "qrcode": "^1.5.4", "react": "^18.2.0", + "react-router-dom": "^6.29.0", "react-dom": "^18.2.0", "sass": "^1.96.0", "sql-formatter": "^15.6.11", diff --git a/frontend/src/App.css b/frontend/src/App.css index 074e77f..be74ba5 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -83,6 +83,9 @@ transition: all 0.2s; border: 1px solid transparent; position: relative; + /* Reset anchor tag defaults for NavLink */ + text-decoration: none; + cursor: pointer; } .nav-button:hover { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e7eafa1..17e4ea0 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,22 +1,10 @@ import React, { useState, useEffect } from 'react'; +import { Routes, Route, Navigate } from 'react-router-dom'; import './App.css'; import { Sidebar } from './components/Sidebar'; import { TitleBar } from './components/TitleBar'; import { Theme } from '@carbon/react'; - -// Tools Imports -import DateTimeConverter from './pages/DateTimeConverter'; -import JwtDebugger from './pages/JwtDebugger'; -import RegExpTester from './pages/RegExpTester'; -import CronJobParser from './pages/CronJobParser'; -import TextDiffChecker from './pages/TextDiffChecker'; -import NumberConverter from './pages/NumberConverter'; -import TextConverter from './pages/TextConverter'; -import StringUtilities from './pages/StringUtilities'; -import BarcodeGenerator from './pages/BarcodeGenerator'; -import DataGenerator from './pages/DataGenerator'; -import CodeFormatter from './pages/CodeFormatter'; -import ColorConverter from './pages/ColorConverter'; +import ToolRouter from './ToolRouter'; // Error boundary for catching React rendering errors class ErrorBoundary extends React.Component { @@ -65,7 +53,6 @@ class ErrorBoundary extends React.Component { function App() { console.log('App mounting'); - const [activeTool, setActiveTool] = useState('text-converter'); const [isSidebarOpen, setIsSidebarOpen] = useState(true); const [theme, setTheme] = useState('g100'); // 'white', 'g10', 'g90', 'g100' const [themeMode, setThemeMode] = useState('dark'); // 'system', 'light', 'dark' @@ -103,37 +90,6 @@ function App() { return () => window.removeEventListener('keydown', handleKeyDown); }, [isSidebarOpen]); - const renderTool = () => { - switch (activeTool) { - case 'text-converter': - return ; - case 'string-utilities': - return ; - case 'datetime-converter': - return ; - case 'jwt': - return ; - case 'barcode': - return ; - case 'data-generator': - return ; - case 'code-formatter': - return ; - case 'color-converter': - return ; - case 'regexp': - return ; - case 'cron': - return ; - case 'diff': - return ; - case 'number-converter': - return ; - default: - return
Select a tool
; - } - }; - return ( @@ -146,14 +102,16 @@ function App() { />
- +
-
{renderTool()}
+
+ + } /> + } /> + } /> + +
diff --git a/frontend/src/ToolRouter.jsx b/frontend/src/ToolRouter.jsx new file mode 100644 index 0000000..88676ba --- /dev/null +++ b/frontend/src/ToolRouter.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; + +// Tool Imports +import DateTimeConverter from './pages/DateTimeConverter'; +import JwtDebugger from './pages/JwtDebugger'; +import RegExpTester from './pages/RegExpTester'; +import CronJobParser from './pages/CronJobParser'; +import TextDiffChecker from './pages/TextDiffChecker'; +import NumberConverter from './pages/NumberConverter'; +import TextConverter from './pages/TextConverter'; +import StringUtilities from './pages/StringUtilities'; +import BarcodeGenerator from './pages/BarcodeGenerator'; +import DataGenerator from './pages/DataGenerator'; +import CodeFormatter from './pages/CodeFormatter'; +import ColorConverter from './pages/ColorConverter'; + +const toolComponents = { + 'text-converter': TextConverter, + 'string-utilities': StringUtilities, + 'datetime-converter': DateTimeConverter, + 'jwt': JwtDebugger, + 'barcode': BarcodeGenerator, + 'data-generator': DataGenerator, + 'code-formatter': CodeFormatter, + 'color-converter': ColorConverter, + 'regexp': RegExpTester, + 'cron': CronJobParser, + 'diff': TextDiffChecker, + 'number-converter': NumberConverter, +}; + +function ToolRouter() { + const { toolId } = useParams(); + const ToolComponent = toolComponents[toolId]; + + if (!ToolComponent) { + return ( +
+

Tool Not Found

+

The tool "{toolId}" doesn't exist.

+
+ ); + } + + return ; +} + +export default ToolRouter; \ No newline at end of file diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index 2b843cd..578739c 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from 'react'; +import { NavLink } from 'react-router-dom'; -export function Sidebar({ activeTool, setActiveTool, isVisible }) { +export function Sidebar({ isVisible }) { const [searchTerm, setSearchTerm] = useState(''); const [pinned, setPinned] = useState(() => { try { @@ -80,9 +81,9 @@ export function Sidebar({ activeTool, setActiveTool, isVisible }) {
    {pinnedTools.map((tool) => (
  • - +
  • ))}
@@ -107,9 +108,9 @@ export function Sidebar({ activeTool, setActiveTool, isVisible }) {
    {regularTools.map((tool) => (
  • - +
  • ))}
diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index da3e04e..f57881d 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,5 +1,6 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; import './index.scss'; // Use our new SCSS import App from './App'; @@ -9,6 +10,8 @@ const root = createRoot(container); root.render( - + + + ); diff --git a/frontend/src/pages/BarcodeGenerator.jsx b/frontend/src/pages/BarcodeGenerator.jsx index 84f49c6..8bdf472 100644 --- a/frontend/src/pages/BarcodeGenerator.jsx +++ b/frontend/src/pages/BarcodeGenerator.jsx @@ -1,5 +1,5 @@ import React, { useState, useCallback, useRef } from 'react'; -import { Button, Dropdown, InlineLoading } from '@carbon/react'; +import { Grid, Column, Button, Dropdown, InlineLoading } from '@carbon/react'; import { Renew, Download } from '@carbon/icons-react'; import { ToolHeader, ToolPane, ToolSplitPane, ToolLayoutToggle } from '../components/ToolUI'; import useLayoutToggle from '../hooks/useLayoutToggle'; @@ -233,27 +233,30 @@ export default function BarcodeGenerator() { const isQR = standard === 'QR'; return ( -
- + + + {/* Controls */} -
+ +
+
+ {/* Input Pane */}
-
+ + ); } diff --git a/frontend/src/pages/CodeFormatter/index.jsx b/frontend/src/pages/CodeFormatter/index.jsx index f29f576..b6ab448 100644 --- a/frontend/src/pages/CodeFormatter/index.jsx +++ b/frontend/src/pages/CodeFormatter/index.jsx @@ -1,5 +1,5 @@ import React, { useState, useCallback, useEffect } from 'react'; -import { Button, Select, SelectItem, TextInput, IconButton } from '@carbon/react'; +import { Grid, Column, Button, Select, SelectItem, TextInput, IconButton } from '@carbon/react'; import { Code, TrashCan, Close } from '@carbon/icons-react'; import { ToolHeader, @@ -202,16 +202,19 @@ export default function CodeFormatter() { }, [filter, formattedOutput]); return ( -
- + + + - + +