diff --git a/.entire/.gitignore b/.entire/.gitignore new file mode 100644 index 0000000..2cffdef --- /dev/null +++ b/.entire/.gitignore @@ -0,0 +1,4 @@ +tmp/ +settings.local.json +metadata/ +logs/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f7d0c0f..72e4b72 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,7 +42,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: "1.24.0" + go-version: "1.25.0" check-latest: true - name: Setup Bun diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md new file mode 100644 index 0000000..e838eb9 --- /dev/null +++ b/KNOWN_ISSUES.md @@ -0,0 +1,64 @@ +# Known Issues + +## macOS: Tray "Show DevToolbox" doesn't restore hidden window + +**GitHub Issue:** [#51](https://github.com/vuon9/devtoolbox/issues/51) +**Status:** Open +**Platform:** macOS only +**Severity:** Medium +**Component:** System Tray / Window Management + +### Description +When the window is hidden to the system tray (via close button with "Close minimizes to tray" setting enabled), clicking "Show DevToolbox" from the tray menu does not restore the window. + +### Steps to Reproduce +1. Enable "Close button minimizes to tray" in Settings +2. Click the window's close button (X) +3. Window hides to tray (app continues running) +4. Click the tray icon and select "Show DevToolbox" +5. Window does not appear (logs show `Window visible: false`) + +### Expected Behavior +Window should be restored and shown when clicking "Show DevToolbox" from the tray menu. + +### Actual Behavior +Window remains hidden. Logs show: +``` +Tray menu 'Show DevToolbox' clicked +Window visible: false, minimized: false +Activating application +Window is not visible, showing it +Focusing window +After show - Window visible: false, minimized: false +``` + +### Technical Details +The current implementation attempts: +1. `app.Show()` - calls `[NSApp unhide:nil]` to activate the app +2. `mainWindow.Show()` - show the window +3. `mainWindow.Focus()` - focus the window + +However, on macOS, when a window is hidden via `Hide()` (which calls `[window orderOut:nil]`), the standard `Show()` and `Focus()` methods are insufficient to restore it. + +### Potential Solutions + +1. **Use `AttachedWindow` pattern** (recommended by Wails docs): + - Attach the window to the system tray + - Let Wails handle the show/hide toggle automatically + - This is the built-in mechanism for tray-attached windows + +2. **Platform-specific handling**: + - On macOS, may need to use `makeKeyAndOrderFront` directly + - Or use different window state management + +3. **Window state tracking**: + - Instead of `Hide()`, minimize the window + - `Minimise()` + `Restore()` works correctly on macOS + +### References +- Wails v3 System Tray docs: https://v3alpha.wails.io/features/menus/systray/ +- Wails v3 Window docs: https://v3alpha.wails.io/reference/window/ +- Related code: `main.go` tray menu click handler + +### Workaround +Users can use the global hotkey `Cmd+Ctrl+M` to open the command palette, which will also show the window. diff --git a/README.md b/README.md index 38d9a12..6f84219 100644 --- a/README.md +++ b/README.md @@ -1,149 +1,42 @@ # DevToolbox -[![Tests & Build](https://github.com/vuon9/devtoolbox/actions/workflows/ci.yml/badge.svg)](https://github.com/vuon9/devtoolbox/actions/workflows/ci.yml) -[![Wails Build](https://github.com/vuon9/devtoolbox/actions/workflows/release.yml/badge.svg)](https://github.com/vuon9/devtoolbox/actions/workflows/release.yml) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Go Version](https://img.shields.io/badge/Go-1.25+-00ADD8?style=flat&logo=go&logoColor=white)](https://go.dev) +> [Download for macOS](https://github.com/vuon9/devtoolbox/releases) Β· [Windows](https://github.com/vuon9/devtoolbox/releases) Β· [Linux](https://github.com/vuon9/devtoolbox/releases) -Essential software development tools for everyday tasks. +A single app for 45+ common development tasks. Works offline, zero setup. -image +Base64, JWT, JSON formatting, hashing, encoding, escaping, color conversion, regex testing, cron parsing, diff checking, Unix time conversion, barcode generation, mock data, and 30+ more. +DevToolbox interface -## Features +No browser tabs. No data sent to servers. Just open and use. -### **Browser Support** +## Desktop or Browser -DevToolbox now works in both desktop and browser modes: - -- **Desktop**: Native Wails application with native performance (default) -- **Browser**: Access via `http://localhost:8081` when the desktop app is running - -The frontend automatically detects the environment and uses the appropriate API (Wails runtime for desktop, HTTP for browser). See [docs/BROWSER_MODE.md](docs/BROWSER_MODE.md) for details. - -### **Text Based Converter** (Unified Tool) -The central hub with 45+ algorithms across 5 categories: - -| Category | Algorithms | -|----------|------------| -| **πŸ” Encrypt / Decrypt** | AES, AES-GCM, DES, Triple DES, ChaCha20, Salsa20, XOR, RC4 | -| **πŸ”€ Encode / Decode** | Base64, Base64URL, Base32, Base58, Base16 (Hex), URL, HTML Entities, Binary, Morse Code, ROT13, ROT47, Quoted-Printable | -| **βœ‚οΈ Escape / Unescape** | String Literal, Unicode/Hex, HTML/XML, URL, Regex | -| **πŸ”„ Convert** | JSON ↔ YAML, JSON ↔ XML, JSON ↔ CSV, YAML ↔ TOML, CSV ↔ TSV, Properties ↔ JSON, INI ↔ JSON, Key-Value ↔ Query String, Number Bases, Case Swapping, Color Codes | -| **#️⃣ Hash** | MD5, SHA-1, SHA-224, SHA-256, SHA-384, SHA-512, SHA-3, BLAKE2b, BLAKE3, RIPEMD-160, bcrypt, scrypt, Argon2, HMAC, CRC32, Adler-32, MurmurHash3, xxHash, FNV-1a | - -**Special Features:** -- **"All Hashes" view** - Compute all 19 hash algorithms at once with copy buttons for each -- **Quick Action Tags** - Save frequently used conversions for instant access -- **Base64 Image Preview** - Automatically displays base64 images in output pane -- **Smart key/IV detection** - Automatically shows configuration pane when needed -- **Auto-run mode** - Results update instantly as you type -- **Horizontal/Vertical layout toggle** - Customize the workspace layout - -### **Other Tools** - -| Tool | Description | -|------|-------------| -| **JWT Debugger** | Decode and verify JWT tokens with header/payload inspection | -| **Barcode / QR Code Generator** | Create QR codes and 1D barcodes (EAN-13, EAN-8, Code 128, Code 39) with preview and download | -| **Data Generator** | Generate mock data with templates using Faker library (UUID, ULID, Random String, Lorem Ipsum, User Profiles, API responses, SQL inserts, and more) | -| **Code Formatter** | Format and minify JSON, XML, HTML, SQL, CSS, and JavaScript with advanced filtering support (jq for JSON, XPath for XML, CSS selectors for HTML) | -| **Color Converter** | Pick colors with eyedropper and generate code snippets for 11+ programming languages (CSS, Swift, .NET, Java, Android, Obj-C, Flutter, Unity, React Native, OpenGL, SVG) | -| **RegExp Tester** | Test regular expressions with real-time matching | -| **Unix Time Converter** | Convert between Unix timestamps and human-readable dates | -| **String Utilities** | Sort/Dedupe lines, Case conversion (camelCase, snake_case, etc.), String Inspector | -| **Cron Job Parser** | Parse and explain cron expressions | -| **Text Diff Checker** | Compare two text blocks and highlight differences | -| **Number Converter** | Convert between Decimal, Hex, Octal, and Binary | +- **Desktop:** Native app (default). Fast, works offline. +- **Browser:** Access at `http://localhost:8081` when desktop app is running. ## Installation -### Download Pre-built Binaries -Download the latest release for your platform from the [Releases](https://github.com/vuon9/devtoolbox/releases) page. - -**Supported Platforms:** -- Windows (x64) -- macOS (Intel & Apple Silicon) -- Linux (x64) - -### Build from Source - -**Prerequisites:** -- Bun (>= 1.0) - Required for frontend dependencies -- Go (>= 1.22) -- Wails CLI: `go install github.com/wailsapp/wails/v2/cmd/wails@latest` +### macOS -**Build Steps:** ```bash -# Clone the repository -git clone https://github.com/your-org/devtoolbox.git -cd devtoolbox - -# Install dependencies and build -wails build - -# Or run in development mode -wails dev +# Download from releases, then: +xattr -cr /Applications/devtoolbox.app # First run only ``` -## Installation - -Download the latest release for your platform from the [Releases](https://github.com/vuon9/devtoolbox/releases) page. - -### macOS - -⚠️ **Note:** The macOS build is not signed with an Apple Developer certificate (requires $99/year). You may see a security warning when opening the app. - -**To bypass the security warning:** - -1. Download the `devtoolbox-macos.dmg` file -2. Open the DMG and drag the app to your Applications folder -3. **First time only:** Open Terminal and run: - ```bash - xattr -cr /Applications/devtoolbox.app - ``` - Or alternatively: - - Go to **System Settings** β†’ **Privacy & Security** - - Scroll down to the "Security" section - - Click **"Open Anyway"** next to the message about "devtoolbox" - - Click **"Open"** in the dialog that appears - -4. The app will now open normally - ### Windows -1. Download `devtoolbox-windows.exe` -2. Run the executable -3. If Windows Defender shows a warning, click **"More info"** β†’ **"Run anyway"** +Download `devtoolbox-windows.exe` from releases and run. ### Linux -1. Download `devtoolbox-linux.tar.gz` -2. Extract: `tar -xzf devtoolbox-linux.tar.gz` -3. Run: `./devtoolbox` - -## Key Features - -βœ… **Works Offline** - All tools run locally, no internet connection required -βœ… **Dark/Light Themes** - Switch between themes or use system preference -βœ… **Pin Tools** - Pin frequently used tools to the top of the sidebar -βœ… **Keyboard Shortcuts** - `Cmd/Ctrl + B` to toggle sidebar -βœ… **Copy to Clipboard** - One-click copy buttons on all output fields -βœ… **Auto-run** - See results instantly as you type (can be disabled) -βœ… **Responsive Layout** - Horizontal or vertical split panes - -## UI Design +```bash +tar -xzf devtoolbox-linux.tar.gz +./devtoolbox +``` -Built with **Carbon Design System** for a consistent, professional look: -- Clean, modern interface -- Accessible components -- Consistent spacing and typography -- Monospace fonts for code/data +Build from source: [docs/BUILD.md](docs/BUILD.md) ## License -MIT License - free to use, modify, and distribute. - ---- - -*Built with ❀️ using Go, React, and Wails.* +MIT diff --git a/docs/BUILD.md b/docs/BUILD.md new file mode 100644 index 0000000..f6844b9 --- /dev/null +++ b/docs/BUILD.md @@ -0,0 +1,53 @@ +# Build from Source + +## Prerequisites + +- Go 1.25+ +- Bun 1.0+ +- Wails CLI: `go install github.com/wailsapp/wails/v2/cmd/wails@latest` + +## Quick Build + +```bash +# Clone +git clone https://github.com/vuon9/devtoolbox.git +cd devtoolbox + +# Build +wails build + +# Or run in development mode +wails dev +``` + +## Development + +```bash +# Frontend only (hot reload) +cd frontend && bun dev + +# Backend only +go run . + +# Both (separate terminals) +wails dev # Terminal 1 +cd frontend && bun dev # Terminal 2 +``` + +## Output + +Built binaries are in `build/bin/`: +- `devtoolbox` (Linux/macOS) +- `devtoolbox.exe` (Windows) + +## Troubleshooting + +**Frontend build fails:** +```bash +cd frontend && bun install +``` + +**Wails not found:** +```bash +go install github.com/wailsapp/wails/v2/cmd/wails@latest +``` diff --git a/docs/plans/2025-03-01-syntax-highlighting-design.md b/docs/plans/2025-03-01-syntax-highlighting-design.md new file mode 100644 index 0000000..8af2261 --- /dev/null +++ b/docs/plans/2025-03-01-syntax-highlighting-design.md @@ -0,0 +1,334 @@ +# Syntax Highlighting Design + +**Date:** 2025-03-01 +**Status:** Ready for Implementation +**Scope:** Add syntax highlighting to code display and editing components across the devtoolbox + +--- + +## Overview + +Add syntax highlighting capabilities to the devtoolbox using CodeMirror 6, with per-tool toggle persistence and Carbon Design System theming. + +**Key Principles:** +- Highlighting ON by default (opt-out) +- Per-tool persistence (each tool remembers its own setting) +- Lazy loading of language modules for performance +- Graceful fallback to plain TextArea when disabled or on error + +--- + +## Goals + +1. Replace plain text areas with syntax-highlighted code editors in CodeFormatter +2. Update CodeSnippetsPanel to use highlighted display for all 11 language tabs +3. Add toggle controls for enabling/disabling highlighting per tool +4. Maintain Carbon Design System visual consistency +5. Support 8 languages initially: JSON, JavaScript, HTML, XML, CSS, SQL, Swift, Java + +--- + +## Architecture + +### Components + +Three new components in `/frontend/src/components/inputs/`: + +#### 1. CodeEditor.jsx +Editable code editor with CodeMirror integration. + +```jsx + {}} // Optional callback + readOnly={false} // Edit or view-only mode + highlight={true} // Controlled by tool toggle + placeholder="Paste code..." + className="optional-class" +/> +``` + +**Responsibilities:** +- Load CodeMirror dynamically when highlight=true +- Apply Carbon dark theme (g100) to editor +- Manage editor lifecycle (create/destroy on toggle) +- Fall back to native TextArea when highlight=false + +#### 2. HighlightedCode.jsx +Read-only code display for snippets and output panes. + +```jsx + +``` + +**Replaces:** `
` tags in CodeSnippetsPanel and read-only TextArea in ToolPane
+
+#### 3. EditorToggle.jsx
+Toggle button for ToolControls section.
+
+```jsx
+
+```
+
+**UI:**
+- Icon button with Code icon
+- Tooltip: "Syntax highlighting: On/Off"
+- Persists to localStorage immediately on toggle
+
+### State Management
+
+Each tool manages its own highlighting state:
+
+```javascript
+// localStorage key pattern
+`${toolKey}-editor-highlight`: 'true' | 'false'
+
+// Examples
+'codeFormatter-editor-highlight': 'true'
+'colorConverter-editor-highlight': 'false'
+```
+
+**Default:** ON for new users (opt-out model)
+
+**Persistence:** Immediate write to localStorage on toggle
+
+### Lazy Loading Strategy
+
+Language modules load on-demand via dynamic imports:
+
+```javascript
+const loadLanguage = async (lang) => {
+  const languageModules = {
+    json: () => import('@codemirror/lang-json'),
+    javascript: () => import('@codemirror/lang-javascript'),
+    typescript: () => import('@codemirror/lang-javascript'), // TS uses JS grammar
+    html: () => import('@codemirror/lang-html'),
+    xml: () => import('@codemirror/lang-xml'),
+    css: () => import('@codemirror/lang-css'),
+    sql: () => import('@codemirror/lang-sql'),
+    swift: () => import('@codemirror/legacy-modes/mode/swift'),
+    java: () => import('@codemirror/lang-java'),
+  };
+  
+  const loader = languageModules[lang];
+  if (!loader) return null;
+  
+  const module = await loader();
+  return module[lang === 'swift' ? 'swift' : `${lang}Language`];
+};
+```
+
+**Caching:** Modules cached after first load (browser cache + memory)
+
+### Tools to Enhance
+
+1. **CodeFormatter** (`/pages/CodeFormatter/index.jsx`)
+   - Add EditorToggle to ToolControls
+   - Replace input ToolPane with CodeEditor
+   - Replace output ToolPane with CodeEditor (readOnly)
+
+2. **ColorConverter** (`/pages/ColorConverter/components/CodeSnippetsPanel.jsx`)
+   - Replace `
` with HighlightedCode
+   - All 11 language tabs benefit automatically
+
+3. **JWTDebugger** (if exists)
+   - Add EditorToggle
+   - Highlight header/payload JSON display
+
+4. **TextDiffChecker** (if exists)
+   - Add EditorToggle
+   - Highlight diff output
+
+---
+
+## Data Flow
+
+```
+User loads tool
+    ↓
+Load persisted preference from localStorage (default: true)
+    ↓
+highlight=true?
+    β”œβ”€ YES β†’ Dynamically import CodeMirror + language module
+    β”‚        ↓
+    β”‚        Mount CodeMirror with Carbon theme
+    β”‚        ↓
+    β”‚        Render highlighted editor
+    β”‚
+    └─ NO  β†’ Render native TextArea (fallback)
+    
+User toggles EditorToggle
+    ↓
+Update state + persist to localStorage
+    ↓
+Re-render: mount/unmount CodeMirror accordingly
+```
+
+---
+
+## Error Handling
+
+### CodeMirror Load Failure
+- **Cause:** Network error, CDN unreachable
+- **Behavior:** Fall back to TextArea, show subtle warning icon with tooltip
+- **User message:** "Syntax highlighting unavailable"
+
+### Unsupported Language
+- **Cause:** Language prop not in supported list
+- **Behavior:** Render as plain text (no highlighting)
+- **Dev:** Console warning in development mode
+
+### Large Files (>1MB)
+- **Detection:** Check content length on value change
+- **Behavior:** Auto-disable highlighting, show info banner
+- **User message:** "Large file - highlighting disabled for performance"
+- **Override:** User can manually re-enable via toggle
+
+### Touch Devices
+- **Detection:** `window.matchMedia('(pointer: coarse)')`
+- **Behavior:** Default to OFF on mobile/tablet (better UX with native input)
+- **Override:** User can still enable if desired
+
+### localStorage Corruption
+- **Behavior:** Parse error caught, reset to default (ON)
+- **User impact:** None, silent recovery
+
+### Memory Cleanup
+- **Implementation:** `useEffect` cleanup function destroys CodeMirror instance
+- **Trigger:** Component unmount or toggle OFF
+
+---
+
+## Styling (Carbon Integration)
+
+CodeMirror theme maps to Carbon tokens:
+
+```javascript
+const carbonDarkTheme = EditorView.theme({
+  '&': {
+    backgroundColor: 'var(--cds-field)',
+    color: 'var(--cds-text-primary)',
+    fontFamily: "'IBM Plex Mono', monospace",
+    fontSize: '0.875rem',
+  },
+  '.cm-content': {
+    caretColor: 'var(--cds-focus)',
+  },
+  '.cm-cursor': {
+    borderLeftColor: 'var(--cds-focus)',
+  },
+  '.cm-selectionBackground': {
+    backgroundColor: 'var(--cds-highlight)',
+  },
+  // Syntax colors - subtle, accessible
+  '.cm-keyword': { color: 'var(--cds-text-primary)' },
+  '.cm-string': { color: 'var(--cds-support-success)' },
+  '.cm-number': { color: 'var(--cds-support-info)' },
+  '.cm-comment': { color: 'var(--cds-text-secondary)' },
+  // ... etc
+});
+```
+
+**Visual consistency:**
+- Matches Carbon g100 dark theme
+- Uses IBM Plex Mono for code
+- Respects `--cds-*` CSS custom properties
+
+---
+
+## Dependencies
+
+### New Packages
+```json
+{
+  "@codemirror/commands": "^6.0.0",
+  "@codemirror/lang-css": "^6.0.0",
+  "@codemirror/lang-html": "^6.0.0",
+  "@codemirror/lang-java": "^6.0.0",
+  "@codemirror/lang-javascript": "^6.0.0",
+  "@codemirror/lang-json": "^6.0.0",
+  "@codemirror/lang-sql": "^6.0.0",
+  "@codemirror/lang-xml": "^6.0.0",
+  "@codemirror/language": "^6.0.0",
+  "@codemirror/legacy-modes": "^6.0.0", // For Swift
+  "@codemirror/state": "^6.0.0",
+  "@codemirror/view": "^6.0.0",
+  "codemirror": "^6.0.0"
+}
+```
+
+**Estimated bundle impact:** ~200-300KB for all 8 languages (tree-shaken, loaded on demand)
+
+---
+
+## Testing Checklist
+
+### Manual Testing
+- [ ] Toggle persists across refresh for each tool
+- [ ] Language switching works (CodeFormatter JSON→XML)
+- [ ] Large file (>1MB) auto-disables highlighting
+- [ ] Copy button works in CodeEditor/HighlightedCode
+- [ ] Mobile defaults to OFF
+- [ ] Network failure shows graceful fallback
+
+### Component Tests
+- [ ] CodeEditor renders TextArea when highlight=false
+- [ ] CodeEditor loads CodeMirror when highlight=true
+- [ ] EditorToggle calls onToggle and persists
+- [ ] HighlightedCode displays with correct language
+
+### Bundle Analysis
+- [ ] Verify lazy loading (CodeMirror not in main chunk)
+- [ ] Target: <300KB size increase
+
+### Accessibility
+- [ ] Toggle has aria-label
+- [ ] Contrast ratios meet WCAG 2.1 AA
+- [ ] Keyboard navigation works
+
+---
+
+## Migration Path
+
+1. **Phase 1:** Create CodeEditor, HighlightedCode, EditorToggle components
+2. **Phase 2:** Update CodeFormatter (input + output panes)
+3. **Phase 3:** Update CodeSnippetsPanel (all language tabs)
+4. **Phase 4:** Add to JWTDebugger and TextDiffChecker
+5. **Phase 5:** Remove legacy ToolPane usage in favor of CodeEditor
+
+---
+
+## Open Questions
+
+1. Should we add TypeScript support explicitly or rely on JavaScript grammar?
+2. Do we want line numbers as a separate toggle or always on/off?
+3. Should the large file threshold be configurable?
+
+---
+
+## Appendix: File Locations
+
+**New files:**
+- `/frontend/src/components/inputs/CodeEditor.jsx`
+- `/frontend/src/components/inputs/HighlightedCode.jsx`
+- `/frontend/src/components/inputs/EditorToggle.jsx`
+
+**Modified files:**
+- `/frontend/src/components/inputs/index.js` - Add exports
+- `/frontend/src/pages/CodeFormatter/index.jsx` - Add EditorToggle, use CodeEditor
+- `/frontend/src/pages/ColorConverter/components/CodeSnippetsPanel.jsx` - Use HighlightedCode
+- `/frontend/package.json` - Add CodeMirror dependencies
+
+**Theme file (optional):**
+- `/frontend/src/components/inputs/carbonCodeMirrorTheme.js` - Extracted theme constants
diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.js b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.js
index 458def9..c61a5b2 100644
--- a/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.js
+++ b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventcreate.js
@@ -8,7 +8,11 @@ import { Create as $Create } from "@wailsio/runtime";
 
 function configure() {
     Object.freeze(Object.assign($Create.Events, {
+        "settings:changed": $$createType0,
     }));
 }
 
+// Private type creation functions
+const $$createType0 = $Create.Map($Create.Any, $Create.Any);
+
 configure();
diff --git a/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts
index 793ee3e..b1e8586 100644
--- a/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts
+++ b/frontend/bindings/github.com/wailsapp/wails/v3/internal/eventdata.d.ts
@@ -8,7 +8,11 @@ import type { Events } from "@wailsio/runtime";
 declare module "@wailsio/runtime" {
     namespace Events {
         interface CustomEvents {
+            "app:quit": string;
+            "command-palette:open": string;
+            "settings:changed": { [_ in string]?: any };
             "time": string;
+            "window:toggle": string;
         }
     }
 }
diff --git a/frontend/bun.lock b/frontend/bun.lock
index fa39023..ce3bbd1 100644
--- a/frontend/bun.lock
+++ b/frontend/bun.lock
@@ -7,7 +7,22 @@
         "@carbon/icons-react": "^11.76.0",
         "@carbon/react": "^1.102.0",
         "@carbon/styles": "^1.101.0",
+        "@codemirror/commands": "^6.10.2",
+        "@codemirror/lang-css": "^6.3.1",
+        "@codemirror/lang-html": "^6.4.11",
+        "@codemirror/lang-java": "^6.0.2",
+        "@codemirror/lang-javascript": "^6.2.4",
+        "@codemirror/lang-json": "^6.0.2",
+        "@codemirror/lang-sql": "^6.10.0",
+        "@codemirror/lang-xml": "^6.1.0",
+        "@codemirror/language": "^6.12.2",
+        "@codemirror/legacy-modes": "^6.5.2",
+        "@codemirror/rangeset": "^0.19.9",
+        "@codemirror/state": "^6.5.4",
+        "@codemirror/view": "^6.39.15",
+        "@lezer/highlight": "^1.2.3",
         "@wailsio/runtime": "^3.0.0-alpha.79",
+        "codemirror": "^6.0.2",
         "cronstrue": "^3.9.0",
         "diff": "^8.0.2",
         "js-beautify": "^1.15.4",
@@ -99,6 +114,40 @@
 
     "@carbon/utilities": ["@carbon/utilities@0.16.0", "", { "dependencies": { "@ibm/telemetry-js": "^1.6.1", "@internationalized/number": "^3.6.1" } }, "sha512-lEYSOpD8Dfh9NJ00uNSORcJ2Bl4+UugM11110SBpbFyes4bWSQkSoRLqoIfV2f1WwFBVDyGXV08O3FaWUgQk3w=="],
 
+    "@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg=="],
+
+    "@codemirror/commands": ["@codemirror/commands@6.10.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ=="],
+
+    "@codemirror/lang-css": ["@codemirror/lang-css@6.3.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.2", "@lezer/css": "^1.1.7" } }, "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg=="],
+
+    "@codemirror/lang-html": ["@codemirror/lang-html@6.4.11", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-javascript": "^6.0.0", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/css": "^1.1.0", "@lezer/html": "^1.3.12" } }, "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw=="],
+
+    "@codemirror/lang-java": ["@codemirror/lang-java@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/java": "^1.0.0" } }, "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ=="],
+
+    "@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.4", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA=="],
+
+    "@codemirror/lang-json": ["@codemirror/lang-json@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/json": "^1.0.0" } }, "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ=="],
+
+    "@codemirror/lang-sql": ["@codemirror/lang-sql@6.10.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w=="],
+
+    "@codemirror/lang-xml": ["@codemirror/lang-xml@6.1.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.0.0", "@lezer/xml": "^1.0.0" } }, "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg=="],
+
+    "@codemirror/language": ["@codemirror/language@6.12.2", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg=="],
+
+    "@codemirror/legacy-modes": ["@codemirror/legacy-modes@6.5.2", "", { "dependencies": { "@codemirror/language": "^6.0.0" } }, "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q=="],
+
+    "@codemirror/lint": ["@codemirror/lint@6.9.4", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-ABc9vJ8DEmvOWuH26P3i8FpMWPQkduD9Rvba5iwb6O3hxASgclm3T3krGo8NASXkHCidz6b++LWlzWIUfEPSWw=="],
+
+    "@codemirror/rangeset": ["@codemirror/rangeset@0.19.9", "", { "dependencies": { "@codemirror/state": "^0.19.0" } }, "sha512-V8YUuOvK+ew87Xem+71nKcqu1SXd5QROMRLMS/ljT5/3MCxtgrRie1Cvild0G/Z2f1fpWxzX78V0U4jjXBorBQ=="],
+
+    "@codemirror/search": ["@codemirror/search@6.6.0", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw=="],
+
+    "@codemirror/state": ["@codemirror/state@6.5.4", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw=="],
+
+    "@codemirror/text": ["@codemirror/text@0.19.6", "", {}, "sha512-T9jnREMIygx+TPC1bOuepz18maGq/92q2a+n4qTqObKwvNMg+8cMTslb8yxeEDEq7S3kpgGWxgO1UWbQRij0dA=="],
+
+    "@codemirror/view": ["@codemirror/view@6.39.15", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-aCWjgweIIXLBHh7bY6cACvXuyrZ0xGafjQ2VInjp4RM4gMfscK5uESiNdrH0pE+e1lZr2B4ONGsjchl2KsKZzg=="],
+
     "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
 
     "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
@@ -189,6 +238,26 @@
 
     "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
 
+    "@lezer/common": ["@lezer/common@1.5.1", "", {}, "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw=="],
+
+    "@lezer/css": ["@lezer/css@1.3.1", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.0" } }, "sha512-PYAKeUVBo3HFThruRyp/iK91SwiZJnzXh8QzkQlwijB5y+N5iB28+iLk78o2zmKqqV0uolNhCwFqB8LA7b0Svg=="],
+
+    "@lezer/highlight": ["@lezer/highlight@1.2.3", "", { "dependencies": { "@lezer/common": "^1.3.0" } }, "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g=="],
+
+    "@lezer/html": ["@lezer/html@1.3.13", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg=="],
+
+    "@lezer/java": ["@lezer/java@1.1.3", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw=="],
+
+    "@lezer/javascript": ["@lezer/javascript@1.5.4", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.1.3", "@lezer/lr": "^1.3.0" } }, "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA=="],
+
+    "@lezer/json": ["@lezer/json@1.0.3", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ=="],
+
+    "@lezer/lr": ["@lezer/lr@1.4.8", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA=="],
+
+    "@lezer/xml": ["@lezer/xml@1.0.6", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww=="],
+
+    "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="],
+
     "@one-ini/wasm": ["@one-ini/wasm@0.1.1", "", {}, "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw=="],
 
     "@parcel/watcher": ["@parcel/watcher@2.5.6", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="],
@@ -323,6 +392,8 @@
 
     "cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
 
+    "codemirror": ["codemirror@6.0.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw=="],
+
     "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
 
     "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
@@ -341,6 +412,8 @@
 
     "copy-to-clipboard": ["copy-to-clipboard@3.3.3", "", { "dependencies": { "toggle-selection": "^1.0.6" } }, "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA=="],
 
+    "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="],
+
     "cronstrue": ["cronstrue@3.9.0", "", { "bin": { "cronstrue": "bin/cli.js" } }, "sha512-T3S35zmD0Ai2B4ko6+mEM+k9C6tipe2nB9RLiGT6QL2Wn0Vsn2cCZAC8Oeuf4CaE00GZWVdpYitbpWCNlIWqdA=="],
 
     "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
@@ -539,6 +612,8 @@
 
     "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
 
+    "style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
+
     "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="],
 
     "toggle-selection": ["toggle-selection@1.0.6", "", {}, "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="],
@@ -551,6 +626,8 @@
 
     "vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
 
+    "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
+
     "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
 
     "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="],
@@ -571,6 +648,8 @@
 
     "yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
 
+    "@codemirror/rangeset/@codemirror/state": ["@codemirror/state@0.19.9", "", { "dependencies": { "@codemirror/text": "^0.19.0" } }, "sha512-psOzDolKTZkx4CgUqhBQ8T8gBc0xN5z4gzed109aF6x7D7umpDRoimacI/O6d9UGuyl4eYuDCZmDFr2Rq7aGOw=="],
+
     "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
 
     "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
diff --git a/frontend/package.json b/frontend/package.json
index 80d0706..acdc47e 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -15,7 +15,22 @@
     "@carbon/icons-react": "^11.76.0",
     "@carbon/react": "^1.102.0",
     "@carbon/styles": "^1.101.0",
+    "@codemirror/commands": "^6.10.2",
+    "@codemirror/lang-css": "^6.3.1",
+    "@codemirror/lang-html": "^6.4.11",
+    "@codemirror/lang-java": "^6.0.2",
+    "@codemirror/lang-javascript": "^6.2.4",
+    "@codemirror/lang-json": "^6.0.2",
+    "@codemirror/lang-sql": "^6.10.0",
+    "@codemirror/lang-xml": "^6.1.0",
+    "@codemirror/language": "^6.12.2",
+    "@codemirror/legacy-modes": "^6.5.2",
+    "@codemirror/rangeset": "^0.19.9",
+    "@codemirror/state": "^6.5.4",
+    "@codemirror/view": "^6.39.15",
+    "@lezer/highlight": "^1.2.3",
     "@wailsio/runtime": "^3.0.0-alpha.79",
+    "codemirror": "^6.0.2",
     "cronstrue": "^3.9.0",
     "diff": "^8.0.2",
     "js-beautify": "^1.15.4",
@@ -26,8 +41,8 @@
     "php-serialize": "^5.1.3",
     "qrcode": "^1.5.4",
     "react": "^18.2.0",
-    "react-router-dom": "^6.29.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",
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 17e4ea0..e654e56 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,8 +1,9 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
 import { Routes, Route, Navigate } from 'react-router-dom';
 import './App.css';
 import { Sidebar } from './components/Sidebar';
 import { TitleBar } from './components/TitleBar';
+import { CommandPalette } from './components/CommandPalette';
 import { Theme } from '@carbon/react';
 import ToolRouter from './ToolRouter';
 
@@ -54,10 +55,15 @@ class ErrorBoundary extends React.Component {
 function App() {
   console.log('App mounting');
   const [isSidebarOpen, setIsSidebarOpen] = useState(true);
+  const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false);
   const [theme, setTheme] = useState('g100'); // 'white', 'g10', 'g90', 'g100'
   const [themeMode, setThemeMode] = useState('dark'); // 'system', 'light', 'dark'
 
   const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen);
+  const toggleCommandPalette = useCallback(() => {
+    setIsCommandPaletteOpen(prev => !prev);
+  }, []);
+  const closeCommandPalette = useCallback(() => setIsCommandPaletteOpen(false), []);
 
   useEffect(() => {
     // Detect System Theme
@@ -84,11 +90,27 @@ function App() {
         e.preventDefault();
         toggleSidebar();
       }
+      // Command palette shortcuts - toggle on/off
+      if ((e.metaKey || e.ctrlKey) && (e.key === 'k' || (e.key === 'p' && e.shiftKey))) {
+        e.preventDefault();
+        toggleCommandPalette();
+      }
     };
 
     window.addEventListener('keydown', handleKeyDown);
     return () => window.removeEventListener('keydown', handleKeyDown);
-  }, [isSidebarOpen]);
+  }, [isSidebarOpen, toggleCommandPalette]);
+
+  // Listen for command palette toggle event from backend (global hotkey)
+  useEffect(() => {
+    const unsubscribe = window.runtime?.EventsOn?.('command-palette:open', () => {
+      toggleCommandPalette();
+    });
+
+    return () => {
+      if (unsubscribe) unsubscribe();
+    };
+  }, [toggleCommandPalette]);
 
   return (
     
@@ -108,12 +130,19 @@ function App() {
               
} /> - } /> + } /> } />
+ +
diff --git a/frontend/src/components/CommandPalette.css b/frontend/src/components/CommandPalette.css new file mode 100644 index 0000000..42007c1 --- /dev/null +++ b/frontend/src/components/CommandPalette.css @@ -0,0 +1,158 @@ +.command-palette-modal { + --command-palette-bg: var(--cds-layer); + --command-palette-border: var(--cds-border-subtle); + --command-palette-text: var(--cds-text-primary); + --command-palette-text-secondary: var(--cds-text-secondary); + --command-palette-accent: var(--cds-interactive); + --command-palette-hover: var(--cds-layer-hover); + --command-palette-selected: var(--cds-layer-selected); +} + +.command-palette-modal .cds--modal-container { + background: var(--command-palette-bg); + border: 1px solid var(--command-palette-border); + border-radius: 8px; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); + max-width: 600px; + width: 90%; + max-height: 80vh; +} + +.command-palette-modal .cds--modal-header { + padding: 1rem; + border-bottom: 1px solid var(--command-palette-border); + display: flex; + align-items: center; + gap: 0.75rem; +} + +.command-palette-modal .cds--modal-close-button { + display: none; +} + +.command-palette-search-icon { + color: var(--command-palette-text-secondary); + flex-shrink: 0; +} + +.command-palette-input { + flex: 1; +} + +.command-palette-input .cds--text-input { + background: transparent; + border: none; + color: var(--command-palette-text); + font-size: 1rem; + padding: 0; +} + +.command-palette-input .cds--text-input:focus { + outline: none; + box-shadow: none; +} + +.command-palette-input .cds--text-input::placeholder { + color: var(--command-palette-text-secondary); +} + +.command-palette-shortcuts { + display: flex; + gap: 0.5rem; + align-items: center; + flex-shrink: 0; +} + +.command-palette-shortcuts kbd { + background: var(--cds-layer-active); + border: 1px solid var(--command-palette-border); + border-radius: 4px; + padding: 0.125rem 0.375rem; + font-size: 0.75rem; + color: var(--command-palette-text-secondary); + font-family: var(--cds-font-mono); +} + +.command-palette-body { + padding: 0; + max-height: 60vh; + overflow: hidden; +} + +.command-palette-empty { + padding: 2rem; + text-align: center; + color: var(--command-palette-text-secondary); +} + +.command-palette-list { + overflow-y: auto; + max-height: 50vh; +} + +.command-palette-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1rem; + cursor: pointer; + transition: background-color 0.15s ease; + border-bottom: 1px solid transparent; +} + +.command-palette-item:hover, +.command-palette-item.selected { + background: var(--command-palette-hover); +} + +.command-palette-item.selected { + background: var(--command-palette-selected); +} + +.command-palette-item-content { + display: flex; + align-items: center; + gap: 0.75rem; + flex: 1; + min-width: 0; +} + +.command-palette-item-icon { + color: var(--command-palette-text-secondary); + flex-shrink: 0; +} + +.command-palette-item-label { + color: var(--command-palette-text); + font-size: 0.875rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.command-palette-item-category { + color: var(--command-palette-text-secondary); + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.025em; + flex-shrink: 0; + margin-left: 1rem; +} + +/* Scrollbar styling */ +.command-palette-list::-webkit-scrollbar { + width: 8px; +} + +.command-palette-list::-webkit-scrollbar-track { + background: transparent; +} + +.command-palette-list::-webkit-scrollbar-thumb { + background: var(--cds-layer-active); + border-radius: 4px; +} + +.command-palette-list::-webkit-scrollbar-thumb:hover { + background: var(--cds-border-subtle); +} diff --git a/frontend/src/components/CommandPalette.jsx b/frontend/src/components/CommandPalette.jsx new file mode 100644 index 0000000..5fa3a15 --- /dev/null +++ b/frontend/src/components/CommandPalette.jsx @@ -0,0 +1,425 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Search, Close, Application, Moon, Power } from '@carbon/icons-react'; +import { TextInput, ComposedModal, ModalHeader, ModalBody } from '@carbon/react'; +import './CommandPalette.css'; + +// Command definitions with tool presets +const COMMANDS = [ + // Code Formatter presets + { + id: 'formatter-json', + label: 'Code Formatter > JSON', + path: '/tool/code-formatter?format=json', + category: 'Formatter', + }, + { + id: 'formatter-xml', + label: 'Code Formatter > XML', + path: '/tool/code-formatter?format=xml', + category: 'Formatter', + }, + { + id: 'formatter-html', + label: 'Code Formatter > HTML', + path: '/tool/code-formatter?format=html', + category: 'Formatter', + }, + { + id: 'formatter-sql', + label: 'Code Formatter > SQL', + path: '/tool/code-formatter?format=sql', + category: 'Formatter', + }, + { + id: 'formatter-css', + label: 'Code Formatter > CSS', + path: '/tool/code-formatter?format=css', + category: 'Formatter', + }, + { + id: 'formatter-js', + label: 'Code Formatter > JavaScript', + path: '/tool/code-formatter?format=javascript', + category: 'Formatter', + }, + + // Text Converter - Encoding + { + id: 'converter-base64', + label: 'Text Converter > Base64', + path: '/tool/text-converter?category=Encode%20-%20Decode&method=Base64', + category: 'Converter', + }, + { + id: 'converter-url', + label: 'Text Converter > URL Encode', + path: '/tool/text-converter?category=Encode%20-%20Decode&method=URL', + category: 'Converter', + }, + { + id: 'converter-hex', + label: 'Text Converter > Hex', + path: '/tool/text-converter?category=Encode%20-%20Decode&method=Base16%20(Hex)', + category: 'Converter', + }, + { + id: 'converter-html', + label: 'Text Converter > HTML Entities', + path: '/tool/text-converter?category=Encode%20-%20Decode&method=HTML%20Entities', + category: 'Converter', + }, + + // Text Converter - Hashing + { + id: 'converter-md5', + label: 'Text Converter > MD5', + path: '/tool/text-converter?category=Hash&method=MD5', + category: 'Converter', + }, + { + id: 'converter-sha256', + label: 'Text Converter > SHA-256', + path: '/tool/text-converter?category=Hash&method=SHA-256', + category: 'Converter', + }, + { + id: 'converter-sha512', + label: 'Text Converter > SHA-512', + path: '/tool/text-converter?category=Hash&method=SHA-512', + category: 'Converter', + }, + { + id: 'converter-all-hashes', + label: 'Text Converter > All Hashes', + path: '/tool/text-converter?category=Hash&method=All', + category: 'Converter', + }, + + // Text Converter - Conversions + { + id: 'converter-json-yaml', + label: 'Text Converter > JSON ↔ YAML', + path: '/tool/text-converter?category=Convert&method=JSON%20%E2%86%94%20YAML', + category: 'Converter', + }, + { + id: 'converter-json-xml', + label: 'Text Converter > JSON ↔ XML', + path: '/tool/text-converter?category=Convert&method=JSON%20%E2%86%94%20XML', + category: 'Converter', + }, + { + id: 'converter-markdown-html', + label: 'Text Converter > Markdown ↔ HTML', + path: '/tool/text-converter?category=Convert&method=Markdown%20%E2%86%94%20HTML', + category: 'Converter', + }, + { + id: 'converter-csv-tsv', + label: 'Text Converter > CSV ↔ TSV', + path: '/tool/text-converter?category=Convert&method=CSV%20%E2%86%94%20TSV', + category: 'Converter', + }, + { + id: 'converter-case-swap', + label: 'Text Converter > Case Swap', + path: '/tool/text-converter?category=Convert&method=Case%20Swapping', + category: 'Converter', + }, + + // Direct navigation - no presets + { id: 'jwt', label: 'JWT Debugger', path: '/tool/jwt', category: 'Tools' }, + { id: 'barcode', label: 'Barcode Generator', path: '/tool/barcode', category: 'Tools' }, + { id: 'regexp', label: 'RegExp Tester', path: '/tool/regexp', category: 'Tools' }, + { id: 'cron', label: 'Cron Job Parser', path: '/tool/cron', category: 'Tools' }, + { id: 'diff', label: 'Text Diff Checker', path: '/tool/diff', category: 'Tools' }, + { id: 'number', label: 'Number Converter', path: '/tool/number-converter', category: 'Tools' }, + { id: 'color', label: 'Color Converter', path: '/tool/color-converter', category: 'Tools' }, + { id: 'string', label: 'String Utilities', path: '/tool/string-utilities', category: 'Tools' }, + { + id: 'datetime', + label: 'DateTime Converter', + path: '/tool/datetime-converter', + category: 'Tools', + }, + { id: 'text', label: 'Text Converter', path: '/tool/text-converter', category: 'Tools' }, + + // Data Generator presets (these will be populated dynamically) + { + id: 'data-user', + label: 'Data Generator > User', + path: '/tool/data-generator?preset=User', + category: 'Generator', + }, + { + id: 'data-address', + label: 'Data Generator > Address', + path: '/tool/data-generator?preset=Address', + category: 'Generator', + }, + + // System commands + { + id: 'theme-toggle', + label: 'Toggle Dark Mode', + action: 'toggle-theme', + category: 'System', + icon: Moon, + }, + { + id: 'window-toggle', + label: 'Show/Hide Window', + action: 'toggle-window', + category: 'System', + icon: Application, + }, + { id: 'app-quit', label: 'Quit DevToolbox', action: 'quit', category: 'System', icon: Power }, +]; + +export function CommandPalette({ isOpen, onClose, themeMode, setThemeMode }) { + const navigate = useNavigate(); + const [searchQuery, setSearchQuery] = useState(''); + const [selectedIndex, setSelectedIndex] = useState(0); + const [commands, setCommands] = useState(COMMANDS); + const inputRef = useRef(null); + const listRef = useRef(null); + + // Load recent commands from localStorage + const [recentCommands, setRecentCommands] = useState(() => { + try { + return JSON.parse(localStorage.getItem('commandPaletteRecent')) || []; + } catch { + return []; + } + }); + + // Fuzzy match function - checks if query characters appear in order in target + const fuzzyMatch = (target, query) => { + if (!query) return true; + + const targetLower = target.toLowerCase(); + const queryLower = query.toLowerCase(); + let targetIndex = 0; + let queryIndex = 0; + + while (targetIndex < targetLower.length && queryIndex < queryLower.length) { + if (targetLower[targetIndex] === queryLower[queryIndex]) { + queryIndex++; + } + targetIndex++; + } + + return queryIndex === queryLower.length; + }; + + // Calculate fuzzy match score (lower is better) + const fuzzyScore = (target, query) => { + if (!query) return 0; + + const targetLower = target.toLowerCase(); + const queryLower = query.toLowerCase(); + + // Exact match gets highest priority + if (targetLower === queryLower) return -1000; + + // Starts with query gets high priority + if (targetLower.startsWith(queryLower)) return -100; + + // Word boundary match gets medium priority + const words = targetLower.split(/[\s>]/); + for (let word of words) { + if (word.startsWith(queryLower)) return -50; + } + + // Calculate distance score for fuzzy match + let targetIndex = 0; + let queryIndex = 0; + let score = 0; + let lastMatchIndex = -1; + + while (targetIndex < targetLower.length && queryIndex < queryLower.length) { + if (targetLower[targetIndex] === queryLower[queryIndex]) { + if (lastMatchIndex !== -1) { + // Penalize gaps between matches + score += targetIndex - lastMatchIndex - 1; + } + lastMatchIndex = targetIndex; + queryIndex++; + } + targetIndex++; + } + + // If didn't match all query characters, return high score (bad match) + if (queryIndex < queryLower.length) return 9999; + + return score; + }; + + // Filter commands based on search query + useEffect(() => { + if (!searchQuery.trim()) { + // Show recent commands first when no search query + const recentIds = new Set(recentCommands); + const sortedCommands = [...COMMANDS].sort((a, b) => { + const aRecent = recentIds.has(a.id) ? 1 : 0; + const bRecent = recentIds.has(b.id) ? 1 : 0; + return bRecent - aRecent; + }); + setCommands(sortedCommands); + return; + } + + const query = searchQuery.toLowerCase(); + + // Filter and score commands + const scored = COMMANDS.map(cmd => { + const labelScore = fuzzyScore(cmd.label, query); + const categoryScore = fuzzyScore(cmd.category, query); + const bestScore = Math.min(labelScore, categoryScore); + return { cmd, score: bestScore }; + }).filter(item => item.score < 9999); + + // Sort by score (lower is better) + scored.sort((a, b) => a.score - b.score); + + setCommands(scored.map(item => item.cmd)); + setSelectedIndex(0); + }, [searchQuery, recentCommands]); + + // Reset selection when opening + useEffect(() => { + if (isOpen) { + setSearchQuery(''); + setSelectedIndex(0); + // Focus input after modal opens + setTimeout(() => inputRef.current?.focus(), 100); + } + }, [isOpen]); + + // Save recent command + const saveRecentCommand = useCallback((commandId) => { + setRecentCommands((prev) => { + const updated = [commandId, ...prev.filter((id) => id !== commandId)].slice(0, 10); + localStorage.setItem('commandPaletteRecent', JSON.stringify(updated)); + return updated; + }); + }, []); + + // Execute command + const executeCommand = useCallback( + (command) => { + saveRecentCommand(command.id); + + if (command.action) { + switch (command.action) { + case 'toggle-theme': + setThemeMode(themeMode === 'dark' ? 'light' : 'dark'); + break; + case 'toggle-window': + if (window.wails?.WindowHide) { + window.wails.WindowHide(); + } + break; + case 'quit': + if (window.wails?.Quit) { + window.wails.Quit(); + } + break; + default: + break; + } + } else if (command.path) { + navigate(command.path); + } + + onClose(); + }, + [navigate, onClose, themeMode, setThemeMode, saveRecentCommand] + ); + + // Handle keyboard navigation + const handleKeyDown = useCallback( + (e) => { + if (e.key === 'ArrowDown') { + e.preventDefault(); + setSelectedIndex((prev) => (prev + 1) % commands.length); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + setSelectedIndex((prev) => (prev - 1 + commands.length) % commands.length); + } else if (e.key === 'Enter') { + e.preventDefault(); + if (commands[selectedIndex]) { + executeCommand(commands[selectedIndex]); + } + } else if (e.key === 'Escape') { + e.preventDefault(); + onClose(); + } + }, + [commands, selectedIndex, executeCommand, onClose] + ); + + // Scroll selected item into view + useEffect(() => { + const selectedElement = listRef.current?.children[selectedIndex]; + if (selectedElement) { + selectedElement.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + } + }, [selectedIndex]); + + return ( + + + + setSearchQuery(e.target.value)} + hideLabel + className="command-palette-input" + autoComplete="off" + /> +
+ ↑↓ + Enter + Esc +
+
+ + {commands.length === 0 ? ( +
No commands found matching "{searchQuery}"
+ ) : ( +
+ {commands.map((command, index) => { + const Icon = command.icon || null; + return ( +
executeCommand(command)} + onMouseEnter={() => setSelectedIndex(index)} + > +
+ {Icon && } + {command.label} +
+ {command.category} +
+ ); + })} +
+ )} +
+
+ ); +} diff --git a/frontend/src/components/SettingsModal.css b/frontend/src/components/SettingsModal.css new file mode 100644 index 0000000..1f6491b --- /dev/null +++ b/frontend/src/components/SettingsModal.css @@ -0,0 +1,39 @@ +.settings-modal { + --settings-bg: var(--cds-layer); + --settings-border: var(--cds-border-subtle); +} + +.settings-modal .cds--modal-container { + background: var(--settings-bg); + border: 1px solid var(--settings-border); + border-radius: 8px; +} + +.settings-modal-body { + padding: 1.5rem; +} + +.settings-modal-body .cds--form-group { + margin-bottom: 1.5rem; +} + +.settings-modal-body .cds--form-group:last-child { + margin-bottom: 0; +} + +.settings-description { + color: var(--cds-text-secondary); + font-size: 0.875rem; + margin-top: 0.5rem; + margin-left: 1.75rem; + line-height: 1.5; +} + +.settings-modal-body .cds--radio-button-group { + display: flex; + gap: 1rem; +} + +.settings-modal-body .cds--checkbox-label-text { + font-weight: 500; +} diff --git a/frontend/src/components/SettingsModal.jsx b/frontend/src/components/SettingsModal.jsx new file mode 100644 index 0000000..ccb66d2 --- /dev/null +++ b/frontend/src/components/SettingsModal.jsx @@ -0,0 +1,104 @@ +import React, { useState, useEffect } from 'react'; +import { + ComposedModal, + ModalHeader, + ModalBody, + Checkbox, + FormGroup, + RadioButtonGroup, + RadioButton, +} from '@carbon/react'; +import { Settings } from '@carbon/icons-react'; +import { GetCloseMinimizesToTray, SetCloseMinimizesToTray } from '../generated'; +import './SettingsModal.css'; + +export function SettingsModal({ + isOpen, + onClose, + themeMode, + setThemeMode, +}) { + const [closeMinimizesToTray, setCloseMinimizesToTray] = useState(true); + const [isLoading, setIsLoading] = useState(false); + + // Load settings when modal opens + useEffect(() => { + if (isOpen) { + loadSettings(); + } + }, [isOpen]); + + const loadSettings = async () => { + try { + const value = await GetCloseMinimizesToTray(); + console.log('Loaded setting:', value); + setCloseMinimizesToTray(value); + } catch (err) { + console.error('Failed to load settings:', err); + } + }; + + return ( + + } + label="" + title="Application Settings" + /> + + + setThemeMode(value)} + > + + + + + + + + { + // Carbon Checkbox passes the event, we need to get checked from event.target + const checked = event.target.checked; + if (isLoading) { + console.log('Still loading, please wait'); + return; + } + + console.log('Setting close minimizes to:', checked); + setIsLoading(true); + SetCloseMinimizesToTray(checked) + .then(() => { + setCloseMinimizesToTray(checked); + console.log('Setting saved successfully'); + }) + .catch((err) => { + console.error('Failed to save setting:', err); + }) + .finally(() => { + setIsLoading(false); + }); + }} + disabled={isLoading} + id="close-minimizes" + /> +

+ When enabled, clicking the close button will minimize the app to the system tray instead of quitting. +

+
+
+
+ ); +} + +export default SettingsModal; diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index 578739c..cb1d8c8 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -56,7 +56,7 @@ export function Sidebar({ isVisible }) {
setSearchTerm(e.target.value)} style={{ diff --git a/frontend/src/components/TitleBar.jsx b/frontend/src/components/TitleBar.jsx index 3af320b..ddaa6d3 100644 --- a/frontend/src/components/TitleBar.jsx +++ b/frontend/src/components/TitleBar.jsx @@ -1,6 +1,8 @@ -import React from 'react'; -import { IconButton, OverflowMenu, OverflowMenuItem } from '@carbon/react'; +import React, { useState } from 'react'; +import { IconButton } from '@carbon/react'; import { Menu, Close, Subtract, Maximize, Settings } from '@carbon/icons-react'; +import { SettingsModal } from './SettingsModal'; +import { Minimise, Maximise, Close as WindowClose } from '../generated'; export function TitleBar({ isSidebarOpen, @@ -9,8 +11,9 @@ export function TitleBar({ themeMode, setThemeMode, }) { + const [isSettingsOpen, setIsSettingsOpen] = useState(false); // Detect if running in desktop mode - const isDesktop = typeof window !== 'undefined' && window.wails; + const isDesktop = typeof window !== 'undefined' && window.go?.devtoolbox?.service?.WindowControls; // Detect platform const userAgent = navigator.userAgent.toLowerCase(); @@ -23,21 +26,27 @@ export function TitleBar({ } // Window control handlers for non-macOS platforms - const handleMinimize = () => { - if (window.wails?.WindowMinimise) { - window.wails.WindowMinimise(); + const handleMinimize = async () => { + try { + await Minimise(); + } catch (err) { + console.error('Failed to minimise window:', err); } }; - const handleMaximize = () => { - if (window.wails?.WindowMaximise) { - window.wails.WindowMaximise(); + const handleMaximize = async () => { + try { + await Maximise(); + } catch (err) { + console.error('Failed to maximise window:', err); } }; - const handleClose = () => { - if (window.wails?.Quit) { - window.wails.Quit(); + const handleClose = async () => { + try { + await WindowClose(); + } catch (err) { + console.error('Failed to close window:', err); } }; @@ -64,32 +73,16 @@ export function TitleBar({ {/* Right section - Settings + Window controls */}
- {/* Settings menu */} - setIsSettingsOpen(true)} + label="Settings" className="titlebar-settings" > - setThemeMode('system')} - requireTitle - /> - setThemeMode('dark')} - requireTitle - /> - setThemeMode('light')} - requireTitle - /> - + + {/* Window controls for non-macOS platforms */} {!isMac && ( @@ -123,6 +116,14 @@ export function TitleBar({
)} + + {/* Settings Modal */} + setIsSettingsOpen(false)} + themeMode={themeMode} + setThemeMode={setThemeMode} + />
); diff --git a/frontend/src/components/ToolUI.jsx b/frontend/src/components/ToolUI.jsx index 3e79202..b57da13 100644 --- a/frontend/src/components/ToolUI.jsx +++ b/frontend/src/components/ToolUI.jsx @@ -5,7 +5,7 @@ import { Copy } from '@carbon/icons-react'; // Re-export new layout components export { ToolLayout, ToolLayoutToggle, ToolVerticalSplit } from './layout'; export { LAYOUT_DIRECTIONS, TOGGLE_POSITIONS } from './layout/constants'; -export { ToolCopyButton, ToolTextArea, ToolInput, ToolInputGroup, ToolTabBar } from './inputs'; +export { ToolCopyButton, ToolTextArea, ToolInput, ToolInputGroup, ToolTabBar, CodeEditor, HighlightedCode, EditorToggle } from './inputs'; export function ToolHeader({ title, description }) { return ( diff --git a/frontend/src/components/inputs/CodeEditor.jsx b/frontend/src/components/inputs/CodeEditor.jsx new file mode 100644 index 0000000..c7c54a5 --- /dev/null +++ b/frontend/src/components/inputs/CodeEditor.jsx @@ -0,0 +1,282 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { EditorView, keymap, lineNumbers } from '@codemirror/view'; +import { EditorState } from '@codemirror/state'; +import { defaultKeymap } from '@codemirror/commands'; +import { carbonCodeMirrorExtension } from './carbonCodeMirrorTheme'; +import { createSQLKeywordHighlighter } from './sqlHighlighter'; +import { TextArea } from '@carbon/react'; + +/** + * Maps language names to CodeMirror language modules + */ +const languageLoaders = { + json: () => import('@codemirror/lang-json').then((m) => m.json()), + javascript: () => import('@codemirror/lang-javascript').then((m) => m.javascript()), + typescript: () => + import('@codemirror/lang-javascript').then((m) => m.javascript({ typescript: true })), + html: () => import('@codemirror/lang-html').then((m) => m.html()), + xml: () => import('@codemirror/lang-xml').then((m) => m.xml()), + css: () => import('@codemirror/lang-css').then((m) => m.css()), + sql: () => import('@codemirror/lang-sql').then((m) => m.sql()), + java: () => import('@codemirror/lang-java').then((m) => m.java()), + swift: () => + import('@codemirror/legacy-modes/mode/swift').then((m) => + import('@codemirror/language').then((lang) => lang.StreamLanguage.define(m.swift)) + ), +}; + +/** + * Loads a CodeMirror language extension dynamically + */ +async function loadLanguageExtension(language) { + const loader = languageLoaders[language.toLowerCase()]; + if (!loader) { + console.warn(`Language "${language}" not supported for syntax highlighting`); + return null; + } + try { + return await loader(); + } catch (err) { + console.warn(`Failed to load language "${language}":`, err); + return null; + } +} + +/** + * Editable code editor with syntax highlighting + * Falls back to Carbon TextArea when highlighting is disabled + * + * @param {string} value - Editor content + * @param {function} onChange - Callback when content changes: (value) => void + * @param {string} language - Programming language for syntax highlighting + * @param {boolean} highlight - Whether to show syntax highlighting + * @param {boolean} [readOnly=false] - Read-only mode + * @param {string} [placeholder] - Placeholder text + * @param {string} [label] - Label for the field + * @param {boolean} [showLineNumbers=false] - Show line numbers + * @param {string} [className] - Optional CSS class + * @param {object} [style] - Optional inline styles + */ +export default function CodeEditor({ + value = '', + onChange, + language, + highlight = true, + readOnly = false, + placeholder, + label, + showLineNumbers = false, + className = '', + style = {}, +}) { + const containerRef = useRef(null); + const viewRef = useRef(null); + const [isLoading, setIsLoading] = useState(false); + const [loadError, setLoadError] = useState(false); + const onChangeRef = useRef(onChange); + const valueRef = useRef(value); + const languageRef = useRef(language); + const readOnlyRef = useRef(readOnly); + const showLineNumbersRef = useRef(showLineNumbers); + + // Keep refs in sync + useEffect(() => { + onChangeRef.current = onChange; + valueRef.current = value; + languageRef.current = language; + readOnlyRef.current = readOnly; + showLineNumbersRef.current = showLineNumbers; + }); + + // Initialize CodeMirror editor - runs once when highlight becomes true + useEffect(() => { + if (!highlight || !containerRef.current || viewRef.current) return; + + let isCancelled = false; + + const initEditor = async () => { + try { + setIsLoading(true); + setLoadError(false); + + // Load language support + const langExtension = await loadLanguageExtension(languageRef.current); + if (isCancelled) return; + + const extensions = [ + ...carbonCodeMirrorExtension, + keymap.of(defaultKeymap), + EditorView.updateListener.of((update) => { + if (update.docChanged && onChangeRef.current) { + onChangeRef.current(update.state.doc.toString()); + } + }), + ]; + + if (readOnlyRef.current) { + extensions.push(EditorState.readOnly.of(true)); + extensions.push(EditorView.editable.of(false)); + } + + if (langExtension) { + extensions.push(langExtension); + } + + // Add SQL keyword categorization if language is SQL + if (languageRef.current?.toLowerCase() === 'sql') { + extensions.push(createSQLKeywordHighlighter()); + } + + if (showLineNumbersRef.current) { + extensions.push(lineNumbers()); + } + + const state = EditorState.create({ + doc: valueRef.current, + extensions, + }); + + const view = new EditorView({ + state, + parent: containerRef.current, + }); + + if (!isCancelled) { + viewRef.current = view; + setIsLoading(false); + } else { + view.destroy(); + } + } catch (err) { + if (!isCancelled) { + console.error('Failed to initialize code editor:', err); + setLoadError(true); + setIsLoading(false); + } + } + }; + + initEditor(); + + return () => { + isCancelled = true; + }; + }, [highlight]); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (viewRef.current) { + viewRef.current.destroy(); + viewRef.current = null; + } + }; + }, []); + + // Destroy when highlight becomes false + useEffect(() => { + if (!highlight && viewRef.current) { + viewRef.current.destroy(); + viewRef.current = null; + } + }, [highlight]); + + // Update content when value prop changes (but NOT during initial render) + useEffect(() => { + const view = viewRef.current; + if (!view) return; + + const currentContent = view.state.doc.toString(); + if (value !== currentContent) { + const transaction = view.state.update({ + changes: { + from: 0, + to: view.state.doc.length, + insert: value, + }, + }); + view.dispatch(transaction); + } + }, [value]); + + // Common container style for both highlighted and fallback modes + const containerStyle = { + display: 'flex', + flexDirection: 'column', + height: '100%', + minHeight: '120px', + border: '1px solid var(--cds-border-strong)', + backgroundColor: 'var(--cds-field)', + position: 'relative', + overflow: 'hidden', + ...style, + }; + + const labelStyle = { + fontSize: '0.75rem', + fontWeight: 400, + textTransform: 'uppercase', + letterSpacing: '0.32px', + color: 'var(--cds-text-secondary)', + padding: '0.5rem 1rem', + borderBottom: '1px solid var(--cds-border-subtle)', + backgroundColor: 'var(--cds-layer)', + flexShrink: 0, + }; + + // Fallback to TextArea when highlighting is disabled or failed to load + if (!highlight || loadError) { + return ( +
+ {label &&
{label}
} +
+