refactor(frontend): remove typescript frontend in favor of svelte gui
- commit
- 802f0d09c26dc69d8ed818dd906a020a88d80f00
- author
- bamen · · 2025-09-15 05:21 UTC
- parent
- 68e80d7
- signature
- unsigned
18 files · +0 -5389
D frontend/bindings/database/sql/index.ts +0 -8
| @@ -1,8 +0,0 @@ | |||
| 1 | - | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL | |
| 2 | - | // This file is automatically generated. DO NOT EDIT | |
| 3 | - | ||
| 4 | - | export { | |
| 5 | - | NullInt64, | |
| 6 | - | NullString, | |
| 7 | - | NullTime | |
| 8 | - | } from "./models.js"; | |
| @@ -1,8 +0,0 @@ | |||||
| 1 | - | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL | |||
| 2 | - | // This file is automatically generated. DO NOT EDIT | |||
| 3 | - | ||||
| 4 | - | export { | |||
| 5 | - | NullInt64, | |||
| 6 | - | NullString, | |||
| 7 | - | NullTime | |||
| 8 | - | } from "./models.js"; | |||
D frontend/bindings/time/index.ts +0 -10
| @@ -1,10 +0,0 @@ | |||
| 1 | - | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL | |
| 2 | - | // This file is automatically generated. DO NOT EDIT | |
| 3 | - | ||
| 4 | - | export { | |
| 5 | - | Duration | |
| 6 | - | } from "./models.js"; | |
| 7 | - | ||
| 8 | - | export type { | |
| 9 | - | Time | |
| 10 | - | } from "./models.js"; | |
| @@ -1,10 +0,0 @@ | |||||
| 1 | - | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL | |||
| 2 | - | // This file is automatically generated. DO NOT EDIT | |||
| 3 | - | ||||
| 4 | - | export { | |||
| 5 | - | Duration | |||
| 6 | - | } from "./models.js"; | |||
| 7 | - | ||||
| 8 | - | export type { | |||
| 9 | - | Time | |||
| 10 | - | } from "./models.js"; | |||
D frontend/eslint.config.js +0 -35
| @@ -1,35 +0,0 @@ | |||
| 1 | - | import js from '@eslint/js'; | |
| 2 | - | import tseslint from 'typescript-eslint'; | |
| 3 | - | ||
| 4 | - | export default tseslint.config( | |
| 5 | - | js.configs.recommended, | |
| 6 | - | ...tseslint.configs.recommended, | |
| 7 | - | { | |
| 8 | - | files: ['**/*.{js,mjs,cjs,ts}'], | |
| 9 | - | languageOptions: { | |
| 10 | - | ecmaVersion: 2024, | |
| 11 | - | sourceType: 'module', | |
| 12 | - | }, | |
| 13 | - | rules: { | |
| 14 | - | // Modern TypeScript best practices | |
| 15 | - | '@typescript-eslint/no-unused-vars': ['error', { | |
| 16 | - | 'argsIgnorePattern': '^_', | |
| 17 | - | 'varsIgnorePattern': '^_' | |
| 18 | - | }], | |
| 19 | - | '@typescript-eslint/no-explicit-any': 'warn', | |
| 20 | - | '@typescript-eslint/no-empty-function': 'off', | |
| 21 | - | '@typescript-eslint/consistent-type-imports': 'error', | |
| 22 | - | '@typescript-eslint/no-import-type-side-effects': 'error', | |
| 23 | - | ||
| 24 | - | // Modern JavaScript/TypeScript patterns | |
| 25 | - | 'prefer-const': 'error', | |
| 26 | - | 'no-var': 'error', | |
| 27 | - | 'object-shorthand': 'error', | |
| 28 | - | 'prefer-template': 'error', | |
| 29 | - | }, | |
| 30 | - | }, | |
| 31 | - | { | |
| 32 | - | files: ['**/*.js', '**/*.mjs'], | |
| 33 | - | ...tseslint.configs.disableTypeChecked, | |
| 34 | - | } | |
| 35 | - | );⏎̸ | |
| @@ -1,35 +0,0 @@ | |||||
| 1 | - | import js from '@eslint/js'; | |||
| 2 | - | import tseslint from 'typescript-eslint'; | |||
| 3 | - | ||||
| 4 | - | export default tseslint.config( | |||
| 5 | - | js.configs.recommended, | |||
| 6 | - | ...tseslint.configs.recommended, | |||
| 7 | - | { | |||
| 8 | - | files: ['**/*.{js,mjs,cjs,ts}'], | |||
| 9 | - | languageOptions: { | |||
| 10 | - | ecmaVersion: 2024, | |||
| 11 | - | sourceType: 'module', | |||
| 12 | - | }, | |||
| 13 | - | rules: { | |||
| 14 | - | // Modern TypeScript best practices | |||
| 15 | - | '@typescript-eslint/no-unused-vars': ['error', { | |||
| 16 | - | 'argsIgnorePattern': '^_', | |||
| 17 | - | 'varsIgnorePattern': '^_' | |||
| 18 | - | }], | |||
| 19 | - | '@typescript-eslint/no-explicit-any': 'warn', | |||
| 20 | - | '@typescript-eslint/no-empty-function': 'off', | |||
| 21 | - | '@typescript-eslint/consistent-type-imports': 'error', | |||
| 22 | - | '@typescript-eslint/no-import-type-side-effects': 'error', | |||
| 23 | - | ||||
| 24 | - | // Modern JavaScript/TypeScript patterns | |||
| 25 | - | 'prefer-const': 'error', | |||
| 26 | - | 'no-var': 'error', | |||
| 27 | - | 'object-shorthand': 'error', | |||
| 28 | - | 'prefer-template': 'error', | |||
| 29 | - | }, | |||
| 30 | - | }, | |||
| 31 | - | { | |||
| 32 | - | files: ['**/*.js', '**/*.mjs'], | |||
| 33 | - | ...tseslint.configs.disableTypeChecked, | |||
| 34 | - | } | |||
| 35 | - | );⏎̸ | |||
D frontend/package-lock.json +0 -3831 generated file: collapsed
D frontend/package.json +0 -23
| @@ -1,23 +0,0 @@ | |||
| 1 | - | { | |
| 2 | - | "name": "@dumber/frontend", | |
| 3 | - | "version": "0.1.0", | |
| 4 | - | "private": true, | |
| 5 | - | "type": "module", | |
| 6 | - | "scripts": { | |
| 7 | - | "build": "tsup && node dist/build-html.js", | |
| 8 | - | "dev": "tsup --watch", | |
| 9 | - | "type-check": "tsc --noEmit", | |
| 10 | - | "lint": "eslint src --fix", | |
| 11 | - | "generate:types": "node ../scripts/generate-types.js" | |
| 12 | - | }, | |
| 13 | - | "devDependencies": { | |
| 14 | - | "@types/node": "^20.0.0", | |
| 15 | - | "@typescript-eslint/eslint-plugin": "^7.0.0", | |
| 16 | - | "@typescript-eslint/parser": "^7.0.0", | |
| 17 | - | "eslint": "^8.57.0", | |
| 18 | - | "tsup": "^8.0.0", | |
| 19 | - | "typescript": "^5.3.0", | |
| 20 | - | "typescript-eslint": "^8.43.0" | |
| 21 | - | }, | |
| 22 | - | "dependencies": {} | |
| 23 | - | } | |
| @@ -1,23 +0,0 @@ | |||||
| 1 | - | { | |||
| 2 | - | "name": "@dumber/frontend", | |||
| 3 | - | "version": "0.1.0", | |||
| 4 | - | "private": true, | |||
| 5 | - | "type": "module", | |||
| 6 | - | "scripts": { | |||
| 7 | - | "build": "tsup && node dist/build-html.js", | |||
| 8 | - | "dev": "tsup --watch", | |||
| 9 | - | "type-check": "tsc --noEmit", | |||
| 10 | - | "lint": "eslint src --fix", | |||
| 11 | - | "generate:types": "node ../scripts/generate-types.js" | |||
| 12 | - | }, | |||
| 13 | - | "devDependencies": { | |||
| 14 | - | "@types/node": "^20.0.0", | |||
| 15 | - | "@typescript-eslint/eslint-plugin": "^7.0.0", | |||
| 16 | - | "@typescript-eslint/parser": "^7.0.0", | |||
| 17 | - | "eslint": "^8.57.0", | |||
| 18 | - | "tsup": "^8.0.0", | |||
| 19 | - | "typescript": "^5.3.0", | |||
| 20 | - | "typescript-eslint": "^8.43.0" | |||
| 21 | - | }, | |||
| 22 | - | "dependencies": {} | |||
| 23 | - | } | |||
D frontend/src/build-html.ts +0 -24
| @@ -1,24 +0,0 @@ | |||
| 1 | - | import { writeFileSync } from 'fs'; | |
| 2 | - | import { join } from 'path'; | |
| 3 | - | import { HTMLBuilder } from './utils/html-builder.js'; | |
| 4 | - | ||
| 5 | - | // Generate the HTML file | |
| 6 | - | function buildHTML() { | |
| 7 | - | try { | |
| 8 | - | const html = HTMLBuilder.createBaseHTML(); | |
| 9 | - | const outputPath = join(process.cwd(), 'dist', 'index.html'); | |
| 10 | - | writeFileSync(outputPath, html); | |
| 11 | - | // Also emit favicon.svg as a file for dumb:// scheme lookups | |
| 12 | - | const favPath = join(process.cwd(), 'dist', 'favicon.svg'); | |
| 13 | - | writeFileSync(favPath, HTMLBuilder.getFaviconSVG()); | |
| 14 | - | console.log('✓ Generated index.html'); | |
| 15 | - | } catch (error) { | |
| 16 | - | console.error('✗ Failed to generate HTML:', error); | |
| 17 | - | process.exit(1); | |
| 18 | - | } | |
| 19 | - | } | |
| 20 | - | ||
| 21 | - | // Only run when called directly (not imported) | |
| 22 | - | if (import.meta.url === `file://${process.argv[1]}`) { | |
| 23 | - | buildHTML(); | |
| 24 | - | } | |
| @@ -1,24 +0,0 @@ | |||||
| 1 | - | import { writeFileSync } from 'fs'; | |||
| 2 | - | import { join } from 'path'; | |||
| 3 | - | import { HTMLBuilder } from './utils/html-builder.js'; | |||
| 4 | - | ||||
| 5 | - | // Generate the HTML file | |||
| 6 | - | function buildHTML() { | |||
| 7 | - | try { | |||
| 8 | - | const html = HTMLBuilder.createBaseHTML(); | |||
| 9 | - | const outputPath = join(process.cwd(), 'dist', 'index.html'); | |||
| 10 | - | writeFileSync(outputPath, html); | |||
| 11 | - | // Also emit favicon.svg as a file for dumb:// scheme lookups | |||
| 12 | - | const favPath = join(process.cwd(), 'dist', 'favicon.svg'); | |||
| 13 | - | writeFileSync(favPath, HTMLBuilder.getFaviconSVG()); | |||
| 14 | - | console.log('✓ Generated index.html'); | |||
| 15 | - | } catch (error) { | |||
| 16 | - | console.error('✗ Failed to generate HTML:', error); | |||
| 17 | - | process.exit(1); | |||
| 18 | - | } | |||
| 19 | - | } | |||
| 20 | - | ||||
| 21 | - | // Only run when called directly (not imported) | |||
| 22 | - | if (import.meta.url === `file://${process.argv[1]}`) { | |||
| 23 | - | buildHTML(); | |||
| 24 | - | } | |||
D frontend/src/injected-controls.ts +0 -107
| @@ -1,107 +0,0 @@ | |||
| 1 | - | /** | |
| 2 | - | * Injectable Controls Script | |
| 3 | - | * | |
| 4 | - | * This script is injected into every page loaded in the browser to provide | |
| 5 | - | * global keyboard and mouse controls that work on all domains. | |
| 6 | - | * | |
| 7 | - | * Only includes essential controls that can work cross-origin: | |
| 8 | - | * - Navigation (back/forward) | |
| 9 | - | * - Mouse navigation buttons | |
| 10 | - | * | |
| 11 | - | * Zoom controls are handled natively by the GTK backend | |
| 12 | - | */ | |
| 13 | - | ||
| 14 | - | (function() { | |
| 15 | - | 'use strict'; | |
| 16 | - | ||
| 17 | - | // Prevent multiple injections with a unique timestamp | |
| 18 | - | const injectionId = '__dumberControlsInjected_' + Date.now(); | |
| 19 | - | if ((window as any).__dumberControlsInjected) { | |
| 20 | - | console.log('🔄 Dumber Browser controls already injected, skipping'); | |
| 21 | - | return; | |
| 22 | - | } | |
| 23 | - | (window as any).__dumberControlsInjected = injectionId; | |
| 24 | - | ||
| 25 | - | console.log('🚀 Dumber Browser controls injected on:', window.location.href); | |
| 26 | - | ||
| 27 | - | // Keyboard event handler for global controls | |
| 28 | - | function handleKeyboardEvent(event: KeyboardEvent): void { | |
| 29 | - | const { ctrlKey, metaKey, shiftKey, altKey, key } = event; | |
| 30 | - | const isCmd = ctrlKey || metaKey; | |
| 31 | - | ||
| 32 | - | try { | |
| 33 | - | // Navigation: Alt + Left Arrow (Back) | |
| 34 | - | if (altKey && key === 'ArrowLeft') { | |
| 35 | - | event.preventDefault(); | |
| 36 | - | event.stopPropagation(); | |
| 37 | - | ||
| 38 | - | if (window.history.length > 1) { | |
| 39 | - | console.log('⬅️ Navigating back'); | |
| 40 | - | window.history.back(); | |
| 41 | - | } | |
| 42 | - | return; | |
| 43 | - | } | |
| 44 | - | ||
| 45 | - | // Navigation: Alt + Right Arrow (Forward) | |
| 46 | - | if (altKey && key === 'ArrowRight') { | |
| 47 | - | event.preventDefault(); | |
| 48 | - | event.stopPropagation(); | |
| 49 | - | ||
| 50 | - | console.log('➡️ Navigating forward'); | |
| 51 | - | window.history.forward(); | |
| 52 | - | return; | |
| 53 | - | } | |
| 54 | - | ||
| 55 | - | } catch (error) { | |
| 56 | - | console.error('❌ Error in keyboard handler:', error); | |
| 57 | - | } | |
| 58 | - | } | |
| 59 | - | ||
| 60 | - | // Mouse event handler for navigation buttons | |
| 61 | - | function handleMouseEvent(event: MouseEvent): void { | |
| 62 | - | try { | |
| 63 | - | // Mouse button 3 (back button) | |
| 64 | - | if (event.button === 3) { | |
| 65 | - | event.preventDefault(); | |
| 66 | - | event.stopPropagation(); | |
| 67 | - | ||
| 68 | - | if (window.history.length > 1) { | |
| 69 | - | console.log('⬅️ Mouse back button pressed'); | |
| 70 | - | window.history.back(); | |
| 71 | - | } | |
| 72 | - | return; | |
| 73 | - | } | |
| 74 | - | ||
| 75 | - | // Mouse button 4 (forward button) | |
| 76 | - | if (event.button === 4) { | |
| 77 | - | event.preventDefault(); | |
| 78 | - | event.stopPropagation(); | |
| 79 | - | ||
| 80 | - | console.log('➡️ Mouse forward button pressed'); | |
| 81 | - | window.history.forward(); | |
| 82 | - | return; | |
| 83 | - | } | |
| 84 | - | ||
| 85 | - | } catch (error) { | |
| 86 | - | console.error('❌ Error in mouse handler:', error); | |
| 87 | - | } | |
| 88 | - | } | |
| 89 | - | ||
| 90 | - | // Add event listeners with high priority (capture phase) | |
| 91 | - | try { | |
| 92 | - | document.addEventListener('keydown', handleKeyboardEvent, true); | |
| 93 | - | document.addEventListener('mousedown', handleMouseEvent, true); | |
| 94 | - | ||
| 95 | - | console.log('✅ Dumber Browser global controls active'); | |
| 96 | - | ||
| 97 | - | // Cleanup function for when page is unloaded | |
| 98 | - | window.addEventListener('beforeunload', () => { | |
| 99 | - | document.removeEventListener('keydown', handleKeyboardEvent, true); | |
| 100 | - | document.removeEventListener('mousedown', handleMouseEvent, true); | |
| 101 | - | console.log('🧹 Dumber Browser controls cleaned up'); | |
| 102 | - | }); | |
| 103 | - | ||
| 104 | - | } catch (error) { | |
| 105 | - | console.error('❌ Failed to initialize Dumber Browser controls:', error); | |
| 106 | - | } | |
| 107 | - | })(); | |
| @@ -1,107 +0,0 @@ | |||||
| 1 | - | /** | |||
| 2 | - | * Injectable Controls Script | |||
| 3 | - | * | |||
| 4 | - | * This script is injected into every page loaded in the browser to provide | |||
| 5 | - | * global keyboard and mouse controls that work on all domains. | |||
| 6 | - | * | |||
| 7 | - | * Only includes essential controls that can work cross-origin: | |||
| 8 | - | * - Navigation (back/forward) | |||
| 9 | - | * - Mouse navigation buttons | |||
| 10 | - | * | |||
| 11 | - | * Zoom controls are handled natively by the GTK backend | |||
| 12 | - | */ | |||
| 13 | - | ||||
| 14 | - | (function() { | |||
| 15 | - | 'use strict'; | |||
| 16 | - | ||||
| 17 | - | // Prevent multiple injections with a unique timestamp | |||
| 18 | - | const injectionId = '__dumberControlsInjected_' + Date.now(); | |||
| 19 | - | if ((window as any).__dumberControlsInjected) { | |||
| 20 | - | console.log('🔄 Dumber Browser controls already injected, skipping'); | |||
| 21 | - | return; | |||
| 22 | - | } | |||
| 23 | - | (window as any).__dumberControlsInjected = injectionId; | |||
| 24 | - | ||||
| 25 | - | console.log('🚀 Dumber Browser controls injected on:', window.location.href); | |||
| 26 | - | ||||
| 27 | - | // Keyboard event handler for global controls | |||
| 28 | - | function handleKeyboardEvent(event: KeyboardEvent): void { | |||
| 29 | - | const { ctrlKey, metaKey, shiftKey, altKey, key } = event; | |||
| 30 | - | const isCmd = ctrlKey || metaKey; | |||
| 31 | - | ||||
| 32 | - | try { | |||
| 33 | - | // Navigation: Alt + Left Arrow (Back) | |||
| 34 | - | if (altKey && key === 'ArrowLeft') { | |||
| 35 | - | event.preventDefault(); | |||
| 36 | - | event.stopPropagation(); | |||
| 37 | - | ||||
| 38 | - | if (window.history.length > 1) { | |||
| 39 | - | console.log('⬅️ Navigating back'); | |||
| 40 | - | window.history.back(); | |||
| 41 | - | } | |||
| 42 | - | return; | |||
| 43 | - | } | |||
| 44 | - | ||||
| 45 | - | // Navigation: Alt + Right Arrow (Forward) | |||
| 46 | - | if (altKey && key === 'ArrowRight') { | |||
| 47 | - | event.preventDefault(); | |||
| 48 | - | event.stopPropagation(); | |||
| 49 | - | ||||
| 50 | - | console.log('➡️ Navigating forward'); | |||
| 51 | - | window.history.forward(); | |||
| 52 | - | return; | |||
| 53 | - | } | |||
| 54 | - | ||||
| 55 | - | } catch (error) { | |||
| 56 | - | console.error('❌ Error in keyboard handler:', error); | |||
| 57 | - | } | |||
| 58 | - | } | |||
| 59 | - | ||||
| 60 | - | // Mouse event handler for navigation buttons | |||
| 61 | - | function handleMouseEvent(event: MouseEvent): void { | |||
| 62 | - | try { | |||
| 63 | - | // Mouse button 3 (back button) | |||
| 64 | - | if (event.button === 3) { | |||
| 65 | - | event.preventDefault(); | |||
| 66 | - | event.stopPropagation(); | |||
| 67 | - | ||||
| 68 | - | if (window.history.length > 1) { | |||
| 69 | - | console.log('⬅️ Mouse back button pressed'); | |||
| 70 | - | window.history.back(); | |||
| 71 | - | } | |||
| 72 | - | return; | |||
| 73 | - | } | |||
| 74 | - | ||||
| 75 | - | // Mouse button 4 (forward button) | |||
| 76 | - | if (event.button === 4) { | |||
| 77 | - | event.preventDefault(); | |||
| 78 | - | event.stopPropagation(); | |||
| 79 | - | ||||
| 80 | - | console.log('➡️ Mouse forward button pressed'); | |||
| 81 | - | window.history.forward(); | |||
| 82 | - | return; | |||
| 83 | - | } | |||
| 84 | - | ||||
| 85 | - | } catch (error) { | |||
| 86 | - | console.error('❌ Error in mouse handler:', error); | |||
| 87 | - | } | |||
| 88 | - | } | |||
| 89 | - | ||||
| 90 | - | // Add event listeners with high priority (capture phase) | |||
| 91 | - | try { | |||
| 92 | - | document.addEventListener('keydown', handleKeyboardEvent, true); | |||
| 93 | - | document.addEventListener('mousedown', handleMouseEvent, true); | |||
| 94 | - | ||||
| 95 | - | console.log('✅ Dumber Browser global controls active'); | |||
| 96 | - | ||||
| 97 | - | // Cleanup function for when page is unloaded | |||
| 98 | - | window.addEventListener('beforeunload', () => { | |||
| 99 | - | document.removeEventListener('keydown', handleKeyboardEvent, true); | |||
| 100 | - | document.removeEventListener('mousedown', handleMouseEvent, true); | |||
| 101 | - | console.log('🧹 Dumber Browser controls cleaned up'); | |||
| 102 | - | }); | |||
| 103 | - | ||||
| 104 | - | } catch (error) { | |||
| 105 | - | console.error('❌ Failed to initialize Dumber Browser controls:', error); | |||
| 106 | - | } | |||
| 107 | - | })(); | |||
D frontend/src/main.ts +0 -73
| @@ -1,73 +0,0 @@ | |||
| 1 | - | import { AppService } from './services/app.js'; | |
| 2 | - | import { BrowserControlsService } from './services/browser-controls.js'; | |
| 3 | - | import { DOMRenderer } from './utils/dom.js'; | |
| 4 | - | ||
| 5 | - | class DumberBrowserApp { | |
| 6 | - | private appService: AppService; | |
| 7 | - | private browserControls: BrowserControlsService; | |
| 8 | - | private domRenderer: DOMRenderer; | |
| 9 | - | ||
| 10 | - | constructor() { | |
| 11 | - | this.appService = new AppService(); | |
| 12 | - | this.browserControls = new BrowserControlsService(); | |
| 13 | - | this.domRenderer = new DOMRenderer(); | |
| 14 | - | } | |
| 15 | - | ||
| 16 | - | async initialize(): Promise<void> { | |
| 17 | - | try { | |
| 18 | - | // Show loading states | |
| 19 | - | this.domRenderer.showLoading('history'); | |
| 20 | - | this.domRenderer.showLoading('shortcuts'); | |
| 21 | - | ||
| 22 | - | // Initialize services | |
| 23 | - | await this.appService.initialize(); | |
| 24 | - | ||
| 25 | - | // Initialize browser controls | |
| 26 | - | await this.browserControls.initialize(); | |
| 27 | - | ||
| 28 | - | // Render the UI | |
| 29 | - | this.render(); | |
| 30 | - | ||
| 31 | - | console.log('✓ Dumber Browser initialized successfully'); | |
| 32 | - | } catch (error) { | |
| 33 | - | console.error('Failed to initialize Dumber Browser:', error); | |
| 34 | - | this.handleInitializationError(); | |
| 35 | - | } | |
| 36 | - | } | |
| 37 | - | ||
| 38 | - | private render(): void { | |
| 39 | - | try { | |
| 40 | - | // Render history | |
| 41 | - | const history = this.appService.getHistory(); | |
| 42 | - | this.domRenderer.displayHistory(history); | |
| 43 | - | ||
| 44 | - | // Render shortcuts | |
| 45 | - | const shortcuts = this.appService.getShortcuts(); | |
| 46 | - | this.domRenderer.displayShortcuts(shortcuts); | |
| 47 | - | } catch (error) { | |
| 48 | - | console.error('Failed to render UI:', error); | |
| 49 | - | this.domRenderer.showError('history', 'Failed to load history'); | |
| 50 | - | this.domRenderer.showError('shortcuts', 'Failed to load shortcuts'); | |
| 51 | - | } | |
| 52 | - | } | |
| 53 | - | ||
| 54 | - | private handleInitializationError(): void { | |
| 55 | - | this.domRenderer.showError('history', 'Failed to initialize application'); | |
| 56 | - | this.domRenderer.showError('shortcuts', 'Failed to initialize application'); | |
| 57 | - | } | |
| 58 | - | } | |
| 59 | - | ||
| 60 | - | // Initialize the application when the DOM is ready | |
| 61 | - | function initializeApp(): void { | |
| 62 | - | const app = new DumberBrowserApp(); | |
| 63 | - | app.initialize().catch(error => { | |
| 64 | - | console.error('Critical error during app initialization:', error); | |
| 65 | - | }); | |
| 66 | - | } | |
| 67 | - | ||
| 68 | - | // Wait for DOM to be ready | |
| 69 | - | if (document.readyState === 'loading') { | |
| 70 | - | document.addEventListener('DOMContentLoaded', initializeApp); | |
| 71 | - | } else { | |
| 72 | - | initializeApp(); | |
| 73 | - | }⏎̸ | |
| @@ -1,73 +0,0 @@ | |||||
| 1 | - | import { AppService } from './services/app.js'; | |||
| 2 | - | import { BrowserControlsService } from './services/browser-controls.js'; | |||
| 3 | - | import { DOMRenderer } from './utils/dom.js'; | |||
| 4 | - | ||||
| 5 | - | class DumberBrowserApp { | |||
| 6 | - | private appService: AppService; | |||
| 7 | - | private browserControls: BrowserControlsService; | |||
| 8 | - | private domRenderer: DOMRenderer; | |||
| 9 | - | ||||
| 10 | - | constructor() { | |||
| 11 | - | this.appService = new AppService(); | |||
| 12 | - | this.browserControls = new BrowserControlsService(); | |||
| 13 | - | this.domRenderer = new DOMRenderer(); | |||
| 14 | - | } | |||
| 15 | - | ||||
| 16 | - | async initialize(): Promise<void> { | |||
| 17 | - | try { | |||
| 18 | - | // Show loading states | |||
| 19 | - | this.domRenderer.showLoading('history'); | |||
| 20 | - | this.domRenderer.showLoading('shortcuts'); | |||
| 21 | - | ||||
| 22 | - | // Initialize services | |||
| 23 | - | await this.appService.initialize(); | |||
| 24 | - | ||||
| 25 | - | // Initialize browser controls | |||
| 26 | - | await this.browserControls.initialize(); | |||
| 27 | - | ||||
| 28 | - | // Render the UI | |||
| 29 | - | this.render(); | |||
| 30 | - | ||||
| 31 | - | console.log('✓ Dumber Browser initialized successfully'); | |||
| 32 | - | } catch (error) { | |||
| 33 | - | console.error('Failed to initialize Dumber Browser:', error); | |||
| 34 | - | this.handleInitializationError(); | |||
| 35 | - | } | |||
| 36 | - | } | |||
| 37 | - | ||||
| 38 | - | private render(): void { | |||
| 39 | - | try { | |||
| 40 | - | // Render history | |||
| 41 | - | const history = this.appService.getHistory(); | |||
| 42 | - | this.domRenderer.displayHistory(history); | |||
| 43 | - | ||||
| 44 | - | // Render shortcuts | |||
| 45 | - | const shortcuts = this.appService.getShortcuts(); | |||
| 46 | - | this.domRenderer.displayShortcuts(shortcuts); | |||
| 47 | - | } catch (error) { | |||
| 48 | - | console.error('Failed to render UI:', error); | |||
| 49 | - | this.domRenderer.showError('history', 'Failed to load history'); | |||
| 50 | - | this.domRenderer.showError('shortcuts', 'Failed to load shortcuts'); | |||
| 51 | - | } | |||
| 52 | - | } | |||
| 53 | - | ||||
| 54 | - | private handleInitializationError(): void { | |||
| 55 | - | this.domRenderer.showError('history', 'Failed to initialize application'); | |||
| 56 | - | this.domRenderer.showError('shortcuts', 'Failed to initialize application'); | |||
| 57 | - | } | |||
| 58 | - | } | |||
| 59 | - | ||||
| 60 | - | // Initialize the application when the DOM is ready | |||
| 61 | - | function initializeApp(): void { | |||
| 62 | - | const app = new DumberBrowserApp(); | |||
| 63 | - | app.initialize().catch(error => { | |||
| 64 | - | console.error('Critical error during app initialization:', error); | |||
| 65 | - | }); | |||
| 66 | - | } | |||
| 67 | - | ||||
| 68 | - | // Wait for DOM to be ready | |||
| 69 | - | if (document.readyState === 'loading') { | |||
| 70 | - | document.addEventListener('DOMContentLoaded', initializeApp); | |||
| 71 | - | } else { | |||
| 72 | - | initializeApp(); | |||
| 73 | - | }⏎̸ | |||
D frontend/src/services/app.ts +0 -105
| @@ -1,105 +0,0 @@ | |||
| 1 | - | type HistoryEntry = { | |
| 2 | - | id: number; | |
| 3 | - | url: string; | |
| 4 | - | title: string; | |
| 5 | - | visit_count: number; | |
| 6 | - | last_visited: string | null; | |
| 7 | - | created_at: string | null; | |
| 8 | - | } | |
| 9 | - | ||
| 10 | - | export class AppService { | |
| 11 | - | private history: HistoryEntry[] = []; | |
| 12 | - | private shortcuts: Record<string, any> = {}; | |
| 13 | - | ||
| 14 | - | constructor() {} | |
| 15 | - | ||
| 16 | - | async initialize(): Promise<void> { | |
| 17 | - | try { | |
| 18 | - | await Promise.all([ | |
| 19 | - | this.loadHistory(), | |
| 20 | - | this.loadShortcuts() | |
| 21 | - | ]); | |
| 22 | - | } catch (error) { | |
| 23 | - | console.error('Failed to initialize app:', error); | |
| 24 | - | } | |
| 25 | - | } | |
| 26 | - | ||
| 27 | - | async loadHistory(): Promise<void> { | |
| 28 | - | try { | |
| 29 | - | const recentHistory: HistoryEntry[] = await fetch('/api/history/recent?limit=50').then(r => r.json()); | |
| 30 | - | this.history = recentHistory; | |
| 31 | - | } catch (error) { | |
| 32 | - | console.error('Failed to load history:', error); | |
| 33 | - | // Mock history for development/fallback | |
| 34 | - | this.history = [ | |
| 35 | - | { | |
| 36 | - | id: 1, | |
| 37 | - | url: 'https://github.com/wailsapp/wails', | |
| 38 | - | title: 'Wails Framework', | |
| 39 | - | visit_count: 3, | |
| 40 | - | last_visited: new Date().toISOString(), | |
| 41 | - | created_at: new Date().toISOString() | |
| 42 | - | }, | |
| 43 | - | { | |
| 44 | - | id: 2, | |
| 45 | - | url: 'https://go.dev', | |
| 46 | - | title: 'The Go Programming Language', | |
| 47 | - | visit_count: 2, | |
| 48 | - | last_visited: new Date().toISOString(), | |
| 49 | - | created_at: new Date().toISOString() | |
| 50 | - | }, | |
| 51 | - | { | |
| 52 | - | id: 3, | |
| 53 | - | url: 'https://developer.mozilla.org', | |
| 54 | - | title: 'MDN Web Docs', | |
| 55 | - | visit_count: 1, | |
| 56 | - | last_visited: new Date().toISOString(), | |
| 57 | - | created_at: new Date().toISOString() | |
| 58 | - | } | |
| 59 | - | ]; | |
| 60 | - | } | |
| 61 | - | } | |
| 62 | - | ||
| 63 | - | async loadShortcuts(): Promise<void> { | |
| 64 | - | try { | |
| 65 | - | const cfg = await fetch('/api/config').then(r => r.json()); | |
| 66 | - | const raw = cfg?.search_shortcuts || {}; | |
| 67 | - | // Normalize field casing from backend (supports URL/Description and url/description) | |
| 68 | - | const normalized: Record<string, { description: string; url: string }> = {}; | |
| 69 | - | for (const [key, value] of Object.entries(raw)) { | |
| 70 | - | const v: any = value as any; | |
| 71 | - | normalized[key] = { | |
| 72 | - | description: v.description ?? v.Description ?? '', | |
| 73 | - | url: v.url ?? v.URL ?? '' | |
| 74 | - | }; | |
| 75 | - | } | |
| 76 | - | this.shortcuts = normalized; | |
| 77 | - | } catch (error) { | |
| 78 | - | console.error('Failed to load shortcuts:', error); | |
| 79 | - | // Mock shortcuts for development/fallback | |
| 80 | - | this.shortcuts = { | |
| 81 | - | 'g': { description: 'Google Search', url: 'https://google.com/search?q={query}' }, | |
| 82 | - | 'gh': { description: 'GitHub Search', url: 'https://github.com/search?q={query}' }, | |
| 83 | - | 'so': { description: 'Stack Overflow', url: 'https://stackoverflow.com/search?q={query}' }, | |
| 84 | - | 'w': { description: 'Wikipedia', url: 'https://en.wikipedia.org/wiki/Special:Search?search={query}' } | |
| 85 | - | }; | |
| 86 | - | } | |
| 87 | - | } | |
| 88 | - | ||
| 89 | - | getHistory(): HistoryEntry[] { | |
| 90 | - | return this.history; | |
| 91 | - | } | |
| 92 | - | ||
| 93 | - | getShortcuts(): Record<string, any> { | |
| 94 | - | return this.shortcuts; | |
| 95 | - | } | |
| 96 | - | ||
| 97 | - | async copyToClipboard(text: string): Promise<void> { | |
| 98 | - | try { | |
| 99 | - | await navigator.clipboard.writeText(text); | |
| 100 | - | console.log('Copied to clipboard:', text); | |
| 101 | - | } catch (err) { | |
| 102 | - | console.error('Failed to copy:', err); | |
| 103 | - | } | |
| 104 | - | } | |
| 105 | - | } | |
| @@ -1,105 +0,0 @@ | |||||
| 1 | - | type HistoryEntry = { | |||
| 2 | - | id: number; | |||
| 3 | - | url: string; | |||
| 4 | - | title: string; | |||
| 5 | - | visit_count: number; | |||
| 6 | - | last_visited: string | null; | |||
| 7 | - | created_at: string | null; | |||
| 8 | - | } | |||
| 9 | - | ||||
| 10 | - | export class AppService { | |||
| 11 | - | private history: HistoryEntry[] = []; | |||
| 12 | - | private shortcuts: Record<string, any> = {}; | |||
| 13 | - | ||||
| 14 | - | constructor() {} | |||
| 15 | - | ||||
| 16 | - | async initialize(): Promise<void> { | |||
| 17 | - | try { | |||
| 18 | - | await Promise.all([ | |||
| 19 | - | this.loadHistory(), | |||
| 20 | - | this.loadShortcuts() | |||
| 21 | - | ]); | |||
| 22 | - | } catch (error) { | |||
| 23 | - | console.error('Failed to initialize app:', error); | |||
| 24 | - | } | |||
| 25 | - | } | |||
| 26 | - | ||||
| 27 | - | async loadHistory(): Promise<void> { | |||
| 28 | - | try { | |||
| 29 | - | const recentHistory: HistoryEntry[] = await fetch('/api/history/recent?limit=50').then(r => r.json()); | |||
| 30 | - | this.history = recentHistory; | |||
| 31 | - | } catch (error) { | |||
| 32 | - | console.error('Failed to load history:', error); | |||
| 33 | - | // Mock history for development/fallback | |||
| 34 | - | this.history = [ | |||
| 35 | - | { | |||
| 36 | - | id: 1, | |||
| 37 | - | url: 'https://github.com/wailsapp/wails', | |||
| 38 | - | title: 'Wails Framework', | |||
| 39 | - | visit_count: 3, | |||
| 40 | - | last_visited: new Date().toISOString(), | |||
| 41 | - | created_at: new Date().toISOString() | |||
| 42 | - | }, | |||
| 43 | - | { | |||
| 44 | - | id: 2, | |||
| 45 | - | url: 'https://go.dev', | |||
| 46 | - | title: 'The Go Programming Language', | |||
| 47 | - | visit_count: 2, | |||
| 48 | - | last_visited: new Date().toISOString(), | |||
| 49 | - | created_at: new Date().toISOString() | |||
| 50 | - | }, | |||
| 51 | - | { | |||
| 52 | - | id: 3, | |||
| 53 | - | url: 'https://developer.mozilla.org', | |||
| 54 | - | title: 'MDN Web Docs', | |||
| 55 | - | visit_count: 1, | |||
| 56 | - | last_visited: new Date().toISOString(), | |||
| 57 | - | created_at: new Date().toISOString() | |||
| 58 | - | } | |||
| 59 | - | ]; | |||
| 60 | - | } | |||
| 61 | - | } | |||
| 62 | - | ||||
| 63 | - | async loadShortcuts(): Promise<void> { | |||
| 64 | - | try { | |||
| 65 | - | const cfg = await fetch('/api/config').then(r => r.json()); | |||
| 66 | - | const raw = cfg?.search_shortcuts || {}; | |||
| 67 | - | // Normalize field casing from backend (supports URL/Description and url/description) | |||
| 68 | - | const normalized: Record<string, { description: string; url: string }> = {}; | |||
| 69 | - | for (const [key, value] of Object.entries(raw)) { | |||
| 70 | - | const v: any = value as any; | |||
| 71 | - | normalized[key] = { | |||
| 72 | - | description: v.description ?? v.Description ?? '', | |||
| 73 | - | url: v.url ?? v.URL ?? '' | |||
| 74 | - | }; | |||
| 75 | - | } | |||
| 76 | - | this.shortcuts = normalized; | |||
| 77 | - | } catch (error) { | |||
| 78 | - | console.error('Failed to load shortcuts:', error); | |||
| 79 | - | // Mock shortcuts for development/fallback | |||
| 80 | - | this.shortcuts = { | |||
| 81 | - | 'g': { description: 'Google Search', url: 'https://google.com/search?q={query}' }, | |||
| 82 | - | 'gh': { description: 'GitHub Search', url: 'https://github.com/search?q={query}' }, | |||
| 83 | - | 'so': { description: 'Stack Overflow', url: 'https://stackoverflow.com/search?q={query}' }, | |||
| 84 | - | 'w': { description: 'Wikipedia', url: 'https://en.wikipedia.org/wiki/Special:Search?search={query}' } | |||
| 85 | - | }; | |||
| 86 | - | } | |||
| 87 | - | } | |||
| 88 | - | ||||
| 89 | - | getHistory(): HistoryEntry[] { | |||
| 90 | - | return this.history; | |||
| 91 | - | } | |||
| 92 | - | ||||
| 93 | - | getShortcuts(): Record<string, any> { | |||
| 94 | - | return this.shortcuts; | |||
| 95 | - | } | |||
| 96 | - | ||||
| 97 | - | async copyToClipboard(text: string): Promise<void> { | |||
| 98 | - | try { | |||
| 99 | - | await navigator.clipboard.writeText(text); | |||
| 100 | - | console.log('Copied to clipboard:', text); | |||
| 101 | - | } catch (err) { | |||
| 102 | - | console.error('Failed to copy:', err); | |||
| 103 | - | } | |||
| 104 | - | } | |||
| 105 | - | } | |||
D frontend/src/services/browser-controls.ts +0 -179
| @@ -1,179 +0,0 @@ | |||
| 1 | - | import { NotificationService } from './notifications.js'; | |
| 2 | - | import { DebugConsoleService } from './debug-console.js'; | |
| 3 | - | ||
| 4 | - | export class BrowserControlsService { | |
| 5 | - | private currentURL = ''; | |
| 6 | - | private notifications = new NotificationService(); | |
| 7 | - | private debugConsole = new DebugConsoleService(); | |
| 8 | - | ||
| 9 | - | constructor() { | |
| 10 | - | // Debug logging with enhanced details | |
| 11 | - | this.debugConsole.addLog('info', 'frontend', 'init', 'BrowserControlsService initializing...'); | |
| 12 | - | this.debugConsole.addLog('info', 'frontend', 'init', 'Running without Wails bindings'); | |
| 13 | - | ||
| 14 | - | this.initializeEventListeners(); | |
| 15 | - | this.debugConsole.addLog('info', 'frontend', 'init', 'BrowserControlsService initialized ✓'); | |
| 16 | - | } | |
| 17 | - | ||
| 18 | - | ||
| 19 | - | private initializeEventListeners(): void { | |
| 20 | - | // Keyboard event handler for browser controls | |
| 21 | - | document.addEventListener('keydown', this.handleKeyboardEvent.bind(this)); | |
| 22 | - | ||
| 23 | - | // Mouse button handler for navigation | |
| 24 | - | document.addEventListener('mousedown', this.handleMouseEvent.bind(this)); | |
| 25 | - | ||
| 26 | - | // URL change listener (for SPA navigation) | |
| 27 | - | window.addEventListener('popstate', this.handleURLChange.bind(this)); | |
| 28 | - | } | |
| 29 | - | ||
| 30 | - | private async handleKeyboardEvent(event: KeyboardEvent): Promise<void> { | |
| 31 | - | // Skip F12 (debug console toggle) | |
| 32 | - | if (event.key === 'F12') { | |
| 33 | - | return; | |
| 34 | - | } | |
| 35 | - | ||
| 36 | - | // Log all keyboard events to debug console | |
| 37 | - | this.debugConsole.logKeyboardEvent(event); | |
| 38 | - | ||
| 39 | - | const { ctrlKey, metaKey, shiftKey, altKey, key } = event; | |
| 40 | - | const isCmd = ctrlKey || metaKey; | |
| 41 | - | ||
| 42 | - | // Zoom controls handled natively by GTK; don't intercept | |
| 43 | - | if (isCmd && (key === '+' || key === '=' || key === '-' || key === '0')) { | |
| 44 | - | return; // allow native handler to process | |
| 45 | - | } | |
| 46 | - | ||
| 47 | - | // Copy URL | |
| 48 | - | if (isCmd && shiftKey && key === 'C') { | |
| 49 | - | this.debugConsole.addLog('info', 'frontend', 'action', 'Copy URL triggered'); | |
| 50 | - | event.preventDefault(); | |
| 51 | - | await this.copyCurrentURL(); | |
| 52 | - | return; | |
| 53 | - | } | |
| 54 | - | ||
| 55 | - | // Navigation | |
| 56 | - | if (altKey && key === 'ArrowLeft') { | |
| 57 | - | this.debugConsole.addLog('info', 'frontend', 'action', 'Go Back triggered'); | |
| 58 | - | event.preventDefault(); | |
| 59 | - | history.back(); | |
| 60 | - | return; | |
| 61 | - | } | |
| 62 | - | ||
| 63 | - | if (altKey && key === 'ArrowRight') { | |
| 64 | - | this.debugConsole.addLog('info', 'frontend', 'action', 'Go Forward triggered'); | |
| 65 | - | event.preventDefault(); | |
| 66 | - | history.forward(); | |
| 67 | - | return; | |
| 68 | - | } | |
| 69 | - | } | |
| 70 | - | ||
| 71 | - | private async handleMouseEvent(event: MouseEvent): Promise<void> { | |
| 72 | - | // Log all mouse events to debug console | |
| 73 | - | this.debugConsole.logMouseEvent(event); | |
| 74 | - | ||
| 75 | - | // Mouse button 3 (back button) | |
| 76 | - | if (event.button === 3) { | |
| 77 | - | this.debugConsole.addLog('info', 'frontend', 'action', 'Mouse Back button triggered'); | |
| 78 | - | event.preventDefault(); | |
| 79 | - | await this.goBack(); | |
| 80 | - | return; | |
| 81 | - | } | |
| 82 | - | ||
| 83 | - | // Mouse button 4 (forward button) | |
| 84 | - | if (event.button === 4) { | |
| 85 | - | this.debugConsole.addLog('info', 'frontend', 'action', 'Mouse Forward button triggered'); | |
| 86 | - | event.preventDefault(); | |
| 87 | - | await this.goForward(); | |
| 88 | - | return; | |
| 89 | - | } | |
| 90 | - | } | |
| 91 | - | ||
| 92 | - | private handleURLChange(): void { | |
| 93 | - | this.setCurrentURL(window.location.href); | |
| 94 | - | this.loadSavedZoom(); | |
| 95 | - | } | |
| 96 | - | ||
| 97 | - | // Zoom is handled natively; no JS-based zoom adjustments needed. | |
| 98 | - | ||
| 99 | - | async copyCurrentURL(): Promise<void> { | |
| 100 | - | try { | |
| 101 | - | const url = this.getCurrentURL(); | |
| 102 | - | await navigator.clipboard.writeText(url); | |
| 103 | - | ||
| 104 | - | // URL copied successfully - no backend notification needed | |
| 105 | - | ||
| 106 | - | this.notifications.show('URL copied to clipboard!', 'success'); | |
| 107 | - | } catch (error) { | |
| 108 | - | console.error('Failed to copy URL:', error); | |
| 109 | - | this.notifications.show('Failed to copy URL', 'error'); | |
| 110 | - | } | |
| 111 | - | } | |
| 112 | - | ||
| 113 | - | async goBack(): Promise<void> { | |
| 114 | - | this.debugConsole.addLog('info', 'frontend', 'service', 'Calling GoBack'); | |
| 115 | - | ||
| 116 | - | try { | |
| 117 | - | await BrowserService.GoBack(); | |
| 118 | - | this.debugConsole.logServiceCall('BrowserService', 'GoBack', []); | |
| 119 | - | history.back(); | |
| 120 | - | this.debugConsole.addLog('info', 'frontend', 'navigation', 'Browser navigated back'); | |
| 121 | - | } catch (error) { | |
| 122 | - | this.debugConsole.logServiceCall('BrowserService', 'GoBack', [], null, error as Error); | |
| 123 | - | console.error('Failed to go back:', error); | |
| 124 | - | } | |
| 125 | - | } | |
| 126 | - | ||
| 127 | - | async goForward(): Promise<void> { | |
| 128 | - | this.debugConsole.addLog('info', 'frontend', 'service', 'Calling GoForward'); | |
| 129 | - | ||
| 130 | - | try { | |
| 131 | - | await BrowserService.GoForward(); | |
| 132 | - | this.debugConsole.logServiceCall('BrowserService', 'GoForward', []); | |
| 133 | - | history.forward(); | |
| 134 | - | this.debugConsole.addLog('info', 'frontend', 'navigation', 'Browser navigated forward'); | |
| 135 | - | } catch (error) { | |
| 136 | - | this.debugConsole.logServiceCall('BrowserService', 'GoForward', [], null, error as Error); | |
| 137 | - | console.error('Failed to go forward:', error); | |
| 138 | - | } | |
| 139 | - | } | |
| 140 | - | ||
| 141 | - | getCurrentURL(): string { | |
| 142 | - | return this.currentURL || window.location.href; | |
| 143 | - | } | |
| 144 | - | ||
| 145 | - | setCurrentURL(url: string): void { | |
| 146 | - | this.currentURL = url; | |
| 147 | - | this.updateWindowTitle(url); | |
| 148 | - | } | |
| 149 | - | ||
| 150 | - | private async updateWindowTitle(url: string): Promise<void> { | |
| 151 | - | try { | |
| 152 | - | // Extract domain or path for title | |
| 153 | - | let title = 'Dumber Browser'; | |
| 154 | - | if (url && url !== '/') { | |
| 155 | - | const urlObj = new URL(url); | |
| 156 | - | title = `Dumber - ${urlObj.hostname}`; | |
| 157 | - | } | |
| 158 | - | ||
| 159 | - | document.title = title; | |
| 160 | - | ||
| 161 | - | // Backend title updates handled natively; no frontend call | |
| 162 | - | } catch (error) { | |
| 163 | - | console.error('Failed to update title:', error); | |
| 164 | - | } | |
| 165 | - | } | |
| 166 | - | ||
| 167 | - | async loadSavedZoom(): Promise<void> { | |
| 168 | - | // Zoom persistence applied natively on page load | |
| 169 | - | } | |
| 170 | - | ||
| 171 | - | async initialize(): Promise<void> { | |
| 172 | - | // Check if there's an initial URL from backend (direct navigation) | |
| 173 | - | // Set initial URL from browser location (homepage) | |
| 174 | - | this.setCurrentURL(window.location.href); | |
| 175 | - | ||
| 176 | - | // Load saved zoom level (only if we're staying on homepage) | |
| 177 | - | // Zoom handled natively | |
| 178 | - | } | |
| 179 | - | } | |
| @@ -1,179 +0,0 @@ | |||||
| 1 | - | import { NotificationService } from './notifications.js'; | |||
| 2 | - | import { DebugConsoleService } from './debug-console.js'; | |||
| 3 | - | ||||
| 4 | - | export class BrowserControlsService { | |||
| 5 | - | private currentURL = ''; | |||
| 6 | - | private notifications = new NotificationService(); | |||
| 7 | - | private debugConsole = new DebugConsoleService(); | |||
| 8 | - | ||||
| 9 | - | constructor() { | |||
| 10 | - | // Debug logging with enhanced details | |||
| 11 | - | this.debugConsole.addLog('info', 'frontend', 'init', 'BrowserControlsService initializing...'); | |||
| 12 | - | this.debugConsole.addLog('info', 'frontend', 'init', 'Running without Wails bindings'); | |||
| 13 | - | ||||
| 14 | - | this.initializeEventListeners(); | |||
| 15 | - | this.debugConsole.addLog('info', 'frontend', 'init', 'BrowserControlsService initialized ✓'); | |||
| 16 | - | } | |||
| 17 | - | ||||
| 18 | - | ||||
| 19 | - | private initializeEventListeners(): void { | |||
| 20 | - | // Keyboard event handler for browser controls | |||
| 21 | - | document.addEventListener('keydown', this.handleKeyboardEvent.bind(this)); | |||
| 22 | - | ||||
| 23 | - | // Mouse button handler for navigation | |||
| 24 | - | document.addEventListener('mousedown', this.handleMouseEvent.bind(this)); | |||
| 25 | - | ||||
| 26 | - | // URL change listener (for SPA navigation) | |||
| 27 | - | window.addEventListener('popstate', this.handleURLChange.bind(this)); | |||
| 28 | - | } | |||
| 29 | - | ||||
| 30 | - | private async handleKeyboardEvent(event: KeyboardEvent): Promise<void> { | |||
| 31 | - | // Skip F12 (debug console toggle) | |||
| 32 | - | if (event.key === 'F12') { | |||
| 33 | - | return; | |||
| 34 | - | } | |||
| 35 | - | ||||
| 36 | - | // Log all keyboard events to debug console | |||
| 37 | - | this.debugConsole.logKeyboardEvent(event); | |||
| 38 | - | ||||
| 39 | - | const { ctrlKey, metaKey, shiftKey, altKey, key } = event; | |||
| 40 | - | const isCmd = ctrlKey || metaKey; | |||
| 41 | - | ||||
| 42 | - | // Zoom controls handled natively by GTK; don't intercept | |||
| 43 | - | if (isCmd && (key === '+' || key === '=' || key === '-' || key === '0')) { | |||
| 44 | - | return; // allow native handler to process | |||
| 45 | - | } | |||
| 46 | - | ||||
| 47 | - | // Copy URL | |||
| 48 | - | if (isCmd && shiftKey && key === 'C') { | |||
| 49 | - | this.debugConsole.addLog('info', 'frontend', 'action', 'Copy URL triggered'); | |||
| 50 | - | event.preventDefault(); | |||
| 51 | - | await this.copyCurrentURL(); | |||
| 52 | - | return; | |||
| 53 | - | } | |||
| 54 | - | ||||
| 55 | - | // Navigation | |||
| 56 | - | if (altKey && key === 'ArrowLeft') { | |||
| 57 | - | this.debugConsole.addLog('info', 'frontend', 'action', 'Go Back triggered'); | |||
| 58 | - | event.preventDefault(); | |||
| 59 | - | history.back(); | |||
| 60 | - | return; | |||
| 61 | - | } | |||
| 62 | - | ||||
| 63 | - | if (altKey && key === 'ArrowRight') { | |||
| 64 | - | this.debugConsole.addLog('info', 'frontend', 'action', 'Go Forward triggered'); | |||
| 65 | - | event.preventDefault(); | |||
| 66 | - | history.forward(); | |||
| 67 | - | return; | |||
| 68 | - | } | |||
| 69 | - | } | |||
| 70 | - | ||||
| 71 | - | private async handleMouseEvent(event: MouseEvent): Promise<void> { | |||
| 72 | - | // Log all mouse events to debug console | |||
| 73 | - | this.debugConsole.logMouseEvent(event); | |||
| 74 | - | ||||
| 75 | - | // Mouse button 3 (back button) | |||
| 76 | - | if (event.button === 3) { | |||
| 77 | - | this.debugConsole.addLog('info', 'frontend', 'action', 'Mouse Back button triggered'); | |||
| 78 | - | event.preventDefault(); | |||
| 79 | - | await this.goBack(); | |||
| 80 | - | return; | |||
| 81 | - | } | |||
| 82 | - | ||||
| 83 | - | // Mouse button 4 (forward button) | |||
| 84 | - | if (event.button === 4) { | |||
| 85 | - | this.debugConsole.addLog('info', 'frontend', 'action', 'Mouse Forward button triggered'); | |||
| 86 | - | event.preventDefault(); | |||
| 87 | - | await this.goForward(); | |||
| 88 | - | return; | |||
| 89 | - | } | |||
| 90 | - | } | |||
| 91 | - | ||||
| 92 | - | private handleURLChange(): void { | |||
| 93 | - | this.setCurrentURL(window.location.href); | |||
| 94 | - | this.loadSavedZoom(); | |||
| 95 | - | } | |||
| 96 | - | ||||
| 97 | - | // Zoom is handled natively; no JS-based zoom adjustments needed. | |||
| 98 | - | ||||
| 99 | - | async copyCurrentURL(): Promise<void> { | |||
| 100 | - | try { | |||
| 101 | - | const url = this.getCurrentURL(); | |||
| 102 | - | await navigator.clipboard.writeText(url); | |||
| 103 | - | ||||
| 104 | - | // URL copied successfully - no backend notification needed | |||
| 105 | - | ||||
| 106 | - | this.notifications.show('URL copied to clipboard!', 'success'); | |||
| 107 | - | } catch (error) { | |||
| 108 | - | console.error('Failed to copy URL:', error); | |||
| 109 | - | this.notifications.show('Failed to copy URL', 'error'); | |||
| 110 | - | } | |||
| 111 | - | } | |||
| 112 | - | ||||
| 113 | - | async goBack(): Promise<void> { | |||
| 114 | - | this.debugConsole.addLog('info', 'frontend', 'service', 'Calling GoBack'); | |||
| 115 | - | ||||
| 116 | - | try { | |||
| 117 | - | await BrowserService.GoBack(); | |||
| 118 | - | this.debugConsole.logServiceCall('BrowserService', 'GoBack', []); | |||
| 119 | - | history.back(); | |||
| 120 | - | this.debugConsole.addLog('info', 'frontend', 'navigation', 'Browser navigated back'); | |||
| 121 | - | } catch (error) { | |||
| 122 | - | this.debugConsole.logServiceCall('BrowserService', 'GoBack', [], null, error as Error); | |||
| 123 | - | console.error('Failed to go back:', error); | |||
| 124 | - | } | |||
| 125 | - | } | |||
| 126 | - | ||||
| 127 | - | async goForward(): Promise<void> { | |||
| 128 | - | this.debugConsole.addLog('info', 'frontend', 'service', 'Calling GoForward'); | |||
| 129 | - | ||||
| 130 | - | try { | |||
| 131 | - | await BrowserService.GoForward(); | |||
| 132 | - | this.debugConsole.logServiceCall('BrowserService', 'GoForward', []); | |||
| 133 | - | history.forward(); | |||
| 134 | - | this.debugConsole.addLog('info', 'frontend', 'navigation', 'Browser navigated forward'); | |||
| 135 | - | } catch (error) { | |||
| 136 | - | this.debugConsole.logServiceCall('BrowserService', 'GoForward', [], null, error as Error); | |||
| 137 | - | console.error('Failed to go forward:', error); | |||
| 138 | - | } | |||
| 139 | - | } | |||
| 140 | - | ||||
| 141 | - | getCurrentURL(): string { | |||
| 142 | - | return this.currentURL || window.location.href; | |||
| 143 | - | } | |||
| 144 | - | ||||
| 145 | - | setCurrentURL(url: string): void { | |||
| 146 | - | this.currentURL = url; | |||
| 147 | - | this.updateWindowTitle(url); | |||
| 148 | - | } | |||
| 149 | - | ||||
| 150 | - | private async updateWindowTitle(url: string): Promise<void> { | |||
| 151 | - | try { | |||
| 152 | - | // Extract domain or path for title | |||
| 153 | - | let title = 'Dumber Browser'; | |||
| 154 | - | if (url && url !== '/') { | |||
| 155 | - | const urlObj = new URL(url); | |||
| 156 | - | title = `Dumber - ${urlObj.hostname}`; | |||
| 157 | - | } | |||
| 158 | - | ||||
| 159 | - | document.title = title; | |||
| 160 | - | ||||
| 161 | - | // Backend title updates handled natively; no frontend call | |||
| 162 | - | } catch (error) { | |||
| 163 | - | console.error('Failed to update title:', error); | |||
| 164 | - | } | |||
| 165 | - | } | |||
| 166 | - | ||||
| 167 | - | async loadSavedZoom(): Promise<void> { | |||
| 168 | - | // Zoom persistence applied natively on page load | |||
| 169 | - | } | |||
| 170 | - | ||||
| 171 | - | async initialize(): Promise<void> { | |||
| 172 | - | // Check if there's an initial URL from backend (direct navigation) | |||
| 173 | - | // Set initial URL from browser location (homepage) | |||
| 174 | - | this.setCurrentURL(window.location.href); | |||
| 175 | - | ||||
| 176 | - | // Load saved zoom level (only if we're staying on homepage) | |||
| 177 | - | // Zoom handled natively | |||
| 178 | - | } | |||
| 179 | - | } | |||
D frontend/src/services/debug-console.ts +0 -258
| @@ -1,258 +0,0 @@ | |||
| 1 | - | export interface DebugLogEntry { | |
| 2 | - | timestamp: string; | |
| 3 | - | level: 'info' | 'warn' | 'error' | 'debug'; | |
| 4 | - | source: 'frontend' | 'backend'; | |
| 5 | - | category: string; | |
| 6 | - | message: string; | |
| 7 | - | data?: any; | |
| 8 | - | } | |
| 9 | - | ||
| 10 | - | export class DebugConsoleService { | |
| 11 | - | private logs: DebugLogEntry[] = []; | |
| 12 | - | private maxLogs = 1000; | |
| 13 | - | private isVisible = false; | |
| 14 | - | private debugElement: HTMLElement | null = null; | |
| 15 | - | private logsContainer: HTMLElement | null = null; | |
| 16 | - | ||
| 17 | - | constructor() { | |
| 18 | - | this.createDebugUI(); | |
| 19 | - | this.setupKeyboardShortcut(); | |
| 20 | - | this.interceptConsole(); | |
| 21 | - | } | |
| 22 | - | ||
| 23 | - | private createDebugUI(): void { | |
| 24 | - | // Create debug panel | |
| 25 | - | this.debugElement = document.createElement('div'); | |
| 26 | - | this.debugElement.id = 'debug-console'; | |
| 27 | - | this.debugElement.style.cssText = ` | |
| 28 | - | position: fixed; | |
| 29 | - | top: 0; | |
| 30 | - | right: 0; | |
| 31 | - | width: 50%; | |
| 32 | - | height: 100%; | |
| 33 | - | background: rgba(0, 0, 0, 0.95); | |
| 34 | - | color: #00ff00; | |
| 35 | - | font-family: 'Courier New', monospace; | |
| 36 | - | font-size: 12px; | |
| 37 | - | z-index: 10000; | |
| 38 | - | display: none; | |
| 39 | - | border-left: 2px solid #00ff00; | |
| 40 | - | `; | |
| 41 | - | ||
| 42 | - | // Create header | |
| 43 | - | const header = document.createElement('div'); | |
| 44 | - | header.style.cssText = ` | |
| 45 | - | padding: 10px; | |
| 46 | - | background: rgba(0, 255, 0, 0.1); | |
| 47 | - | border-bottom: 1px solid #00ff00; | |
| 48 | - | font-weight: bold; | |
| 49 | - | display: flex; | |
| 50 | - | justify-content: space-between; | |
| 51 | - | align-items: center; | |
| 52 | - | `; | |
| 53 | - | header.innerHTML = ` | |
| 54 | - | <span>🐛 DUMBER DEBUG CONSOLE</span> | |
| 55 | - | <div style="font-size: 10px;"> | |
| 56 | - | <span style="color: #888; margin-right: 10px;">F11: Inspect Services | F12: Toggle</span> | |
| 57 | - | <button id="clear-logs" style="background: none; border: 1px solid #00ff00; color: #00ff00; padding: 2px 8px; margin-right: 5px; cursor: pointer;">Clear</button> | |
| 58 | - | <button id="close-debug" style="background: none; border: 1px solid #00ff00; color: #00ff00; padding: 2px 8px; cursor: pointer;">Close</button> | |
| 59 | - | </div> | |
| 60 | - | `; | |
| 61 | - | ||
| 62 | - | // Create logs container | |
| 63 | - | this.logsContainer = document.createElement('div'); | |
| 64 | - | this.logsContainer.style.cssText = ` | |
| 65 | - | height: calc(100% - 50px); | |
| 66 | - | overflow-y: auto; | |
| 67 | - | padding: 10px; | |
| 68 | - | `; | |
| 69 | - | ||
| 70 | - | this.debugElement.appendChild(header); | |
| 71 | - | this.debugElement.appendChild(this.logsContainer); | |
| 72 | - | document.body.appendChild(this.debugElement); | |
| 73 | - | ||
| 74 | - | // Setup button handlers | |
| 75 | - | header.querySelector('#clear-logs')?.addEventListener('click', () => this.clearLogs()); | |
| 76 | - | header.querySelector('#close-debug')?.addEventListener('click', () => this.hide()); | |
| 77 | - | } | |
| 78 | - | ||
| 79 | - | private setupKeyboardShortcut(): void { | |
| 80 | - | document.addEventListener('keydown', (event) => { | |
| 81 | - | // F12 to toggle debug console | |
| 82 | - | if (event.key === 'F12') { | |
| 83 | - | event.preventDefault(); | |
| 84 | - | this.toggle(); | |
| 85 | - | } | |
| 86 | - | }); | |
| 87 | - | } | |
| 88 | - | ||
| 89 | - | private interceptConsole(): void { | |
| 90 | - | const originalConsole = { | |
| 91 | - | log: console.log, | |
| 92 | - | warn: console.warn, | |
| 93 | - | error: console.error, | |
| 94 | - | info: console.info, | |
| 95 | - | debug: console.debug | |
| 96 | - | }; | |
| 97 | - | ||
| 98 | - | console.log = (...args) => { | |
| 99 | - | this.addLog('info', 'frontend', 'console', args.join(' ')); | |
| 100 | - | originalConsole.log(...args); | |
| 101 | - | }; | |
| 102 | - | ||
| 103 | - | console.warn = (...args) => { | |
| 104 | - | this.addLog('warn', 'frontend', 'console', args.join(' ')); | |
| 105 | - | originalConsole.warn(...args); | |
| 106 | - | }; | |
| 107 | - | ||
| 108 | - | console.error = (...args) => { | |
| 109 | - | this.addLog('error', 'frontend', 'console', args.join(' ')); | |
| 110 | - | originalConsole.error(...args); | |
| 111 | - | }; | |
| 112 | - | ||
| 113 | - | console.info = (...args) => { | |
| 114 | - | this.addLog('info', 'frontend', 'console', args.join(' ')); | |
| 115 | - | originalConsole.info(...args); | |
| 116 | - | }; | |
| 117 | - | ||
| 118 | - | console.debug = (...args) => { | |
| 119 | - | this.addLog('debug', 'frontend', 'console', args.join(' ')); | |
| 120 | - | originalConsole.debug(...args); | |
| 121 | - | }; | |
| 122 | - | } | |
| 123 | - | ||
| 124 | - | public addLog(level: DebugLogEntry['level'], source: DebugLogEntry['source'], category: string, message: string, data?: any): void { | |
| 125 | - | const entry: DebugLogEntry = { | |
| 126 | - | timestamp: new Date().toISOString().split('T')[1].split('.')[0], // HH:MM:SS format | |
| 127 | - | level, | |
| 128 | - | source, | |
| 129 | - | category, | |
| 130 | - | message, | |
| 131 | - | data | |
| 132 | - | }; | |
| 133 | - | ||
| 134 | - | this.logs.push(entry); | |
| 135 | - | ||
| 136 | - | // Keep only the last maxLogs entries | |
| 137 | - | if (this.logs.length > this.maxLogs) { | |
| 138 | - | this.logs.shift(); | |
| 139 | - | } | |
| 140 | - | ||
| 141 | - | this.updateDisplay(); | |
| 142 | - | } | |
| 143 | - | ||
| 144 | - | private updateDisplay(): void { | |
| 145 | - | if (!this.logsContainer) return; | |
| 146 | - | ||
| 147 | - | const logHTML = this.logs.map(log => { | |
| 148 | - | const levelColor = { | |
| 149 | - | info: '#00ff00', | |
| 150 | - | warn: '#ffff00', | |
| 151 | - | error: '#ff0000', | |
| 152 | - | debug: '#00ffff' | |
| 153 | - | }[log.level]; | |
| 154 | - | ||
| 155 | - | const sourceIcon = log.source === 'backend' ? '🖥️' : '🌐'; | |
| 156 | - | ||
| 157 | - | return ` | |
| 158 | - | <div style="margin-bottom: 2px; font-size: 11px;"> | |
| 159 | - | <span style="color: #888;">[${log.timestamp}]</span> | |
| 160 | - | <span style="color: ${levelColor};">${sourceIcon} ${log.level.toUpperCase()}</span> | |
| 161 | - | <span style="color: #aaa;">[${log.category}]</span> | |
| 162 | - | <span style="color: #fff;">${log.message}</span> | |
| 163 | - | ${log.data ? `<pre style="color: #ccc; font-size: 10px; margin: 2px 0;">${JSON.stringify(log.data, null, 2)}</pre>` : ''} | |
| 164 | - | </div> | |
| 165 | - | `; | |
| 166 | - | }).join(''); | |
| 167 | - | ||
| 168 | - | this.logsContainer.innerHTML = logHTML; | |
| 169 | - | ||
| 170 | - | // Auto-scroll to bottom | |
| 171 | - | this.logsContainer.scrollTop = this.logsContainer.scrollHeight; | |
| 172 | - | } | |
| 173 | - | ||
| 174 | - | public logKeyboardEvent(event: KeyboardEvent): void { | |
| 175 | - | const modifiers = []; | |
| 176 | - | if (event.ctrlKey) modifiers.push('Ctrl'); | |
| 177 | - | if (event.shiftKey) modifiers.push('Shift'); | |
| 178 | - | if (event.altKey) modifiers.push('Alt'); | |
| 179 | - | if (event.metaKey) modifiers.push('Meta'); | |
| 180 | - | ||
| 181 | - | const shortcut = modifiers.length > 0 ? `${modifiers.join('+')}+${event.key}` : event.key; | |
| 182 | - | ||
| 183 | - | this.addLog('info', 'frontend', 'keyboard', `Key pressed: ${shortcut}`, { | |
| 184 | - | key: event.key, | |
| 185 | - | code: event.code, | |
| 186 | - | modifiers: modifiers, | |
| 187 | - | target: event.target?.constructor.name | |
| 188 | - | }); | |
| 189 | - | } | |
| 190 | - | ||
| 191 | - | public logMouseEvent(event: MouseEvent): void { | |
| 192 | - | const buttonNames = { | |
| 193 | - | 0: 'Left', | |
| 194 | - | 1: 'Middle', | |
| 195 | - | 2: 'Right', | |
| 196 | - | 3: 'Back', | |
| 197 | - | 4: 'Forward' | |
| 198 | - | }; | |
| 199 | - | ||
| 200 | - | this.addLog('info', 'frontend', 'mouse', `Mouse ${event.type}: ${buttonNames[event.button] || `Button${event.button}`}`, { | |
| 201 | - | button: event.button, | |
| 202 | - | clientX: event.clientX, | |
| 203 | - | clientY: event.clientY, | |
| 204 | - | target: event.target?.constructor.name | |
| 205 | - | }); | |
| 206 | - | } | |
| 207 | - | ||
| 208 | - | public logServiceCall(serviceName: string, methodName: string, params: any[], result?: any, error?: Error): void { | |
| 209 | - | if (error) { | |
| 210 | - | this.addLog('error', 'frontend', 'service', `${serviceName}.${methodName}() failed`, { | |
| 211 | - | params, | |
| 212 | - | error: error.message | |
| 213 | - | }); | |
| 214 | - | } else { | |
| 215 | - | this.addLog('info', 'frontend', 'service', `${serviceName}.${methodName}() called`, { | |
| 216 | - | params, | |
| 217 | - | result | |
| 218 | - | }); | |
| 219 | - | } | |
| 220 | - | } | |
| 221 | - | ||
| 222 | - | public logBackendMessage(message: string, data?: any): void { | |
| 223 | - | this.addLog('info', 'backend', 'service', message, data); | |
| 224 | - | } | |
| 225 | - | ||
| 226 | - | public show(): void { | |
| 227 | - | if (this.debugElement) { | |
| 228 | - | this.debugElement.style.display = 'block'; | |
| 229 | - | this.isVisible = true; | |
| 230 | - | this.addLog('info', 'frontend', 'debug', 'Debug console opened'); | |
| 231 | - | } | |
| 232 | - | } | |
| 233 | - | ||
| 234 | - | public hide(): void { | |
| 235 | - | if (this.debugElement) { | |
| 236 | - | this.debugElement.style.display = 'none'; | |
| 237 | - | this.isVisible = false; | |
| 238 | - | } | |
| 239 | - | } | |
| 240 | - | ||
| 241 | - | public toggle(): void { | |
| 242 | - | if (this.isVisible) { | |
| 243 | - | this.hide(); | |
| 244 | - | } else { | |
| 245 | - | this.show(); | |
| 246 | - | } | |
| 247 | - | } | |
| 248 | - | ||
| 249 | - | public clearLogs(): void { | |
| 250 | - | this.logs = []; | |
| 251 | - | this.updateDisplay(); | |
| 252 | - | this.addLog('info', 'frontend', 'debug', 'Logs cleared'); | |
| 253 | - | } | |
| 254 | - | ||
| 255 | - | public isDebugVisible(): boolean { | |
| 256 | - | return this.isVisible; | |
| 257 | - | } | |
| 258 | - | } | |
| @@ -1,258 +0,0 @@ | |||||
| 1 | - | export interface DebugLogEntry { | |||
| 2 | - | timestamp: string; | |||
| 3 | - | level: 'info' | 'warn' | 'error' | 'debug'; | |||
| 4 | - | source: 'frontend' | 'backend'; | |||
| 5 | - | category: string; | |||
| 6 | - | message: string; | |||
| 7 | - | data?: any; | |||
| 8 | - | } | |||
| 9 | - | ||||
| 10 | - | export class DebugConsoleService { | |||
| 11 | - | private logs: DebugLogEntry[] = []; | |||
| 12 | - | private maxLogs = 1000; | |||
| 13 | - | private isVisible = false; | |||
| 14 | - | private debugElement: HTMLElement | null = null; | |||
| 15 | - | private logsContainer: HTMLElement | null = null; | |||
| 16 | - | ||||
| 17 | - | constructor() { | |||
| 18 | - | this.createDebugUI(); | |||
| 19 | - | this.setupKeyboardShortcut(); | |||
| 20 | - | this.interceptConsole(); | |||
| 21 | - | } | |||
| 22 | - | ||||
| 23 | - | private createDebugUI(): void { | |||
| 24 | - | // Create debug panel | |||
| 25 | - | this.debugElement = document.createElement('div'); | |||
| 26 | - | this.debugElement.id = 'debug-console'; | |||
| 27 | - | this.debugElement.style.cssText = ` | |||
| 28 | - | position: fixed; | |||
| 29 | - | top: 0; | |||
| 30 | - | right: 0; | |||
| 31 | - | width: 50%; | |||
| 32 | - | height: 100%; | |||
| 33 | - | background: rgba(0, 0, 0, 0.95); | |||
| 34 | - | color: #00ff00; | |||
| 35 | - | font-family: 'Courier New', monospace; | |||
| 36 | - | font-size: 12px; | |||
| 37 | - | z-index: 10000; | |||
| 38 | - | display: none; | |||
| 39 | - | border-left: 2px solid #00ff00; | |||
| 40 | - | `; | |||
| 41 | - | ||||
| 42 | - | // Create header | |||
| 43 | - | const header = document.createElement('div'); | |||
| 44 | - | header.style.cssText = ` | |||
| 45 | - | padding: 10px; | |||
| 46 | - | background: rgba(0, 255, 0, 0.1); | |||
| 47 | - | border-bottom: 1px solid #00ff00; | |||
| 48 | - | font-weight: bold; | |||
| 49 | - | display: flex; | |||
| 50 | - | justify-content: space-between; | |||
| 51 | - | align-items: center; | |||
| 52 | - | `; | |||
| 53 | - | header.innerHTML = ` | |||
| 54 | - | <span>🐛 DUMBER DEBUG CONSOLE</span> | |||
| 55 | - | <div style="font-size: 10px;"> | |||
| 56 | - | <span style="color: #888; margin-right: 10px;">F11: Inspect Services | F12: Toggle</span> | |||
| 57 | - | <button id="clear-logs" style="background: none; border: 1px solid #00ff00; color: #00ff00; padding: 2px 8px; margin-right: 5px; cursor: pointer;">Clear</button> | |||
| 58 | - | <button id="close-debug" style="background: none; border: 1px solid #00ff00; color: #00ff00; padding: 2px 8px; cursor: pointer;">Close</button> | |||
| 59 | - | </div> | |||
| 60 | - | `; | |||
| 61 | - | ||||
| 62 | - | // Create logs container | |||
| 63 | - | this.logsContainer = document.createElement('div'); | |||
| 64 | - | this.logsContainer.style.cssText = ` | |||
| 65 | - | height: calc(100% - 50px); | |||
| 66 | - | overflow-y: auto; | |||
| 67 | - | padding: 10px; | |||
| 68 | - | `; | |||
| 69 | - | ||||
| 70 | - | this.debugElement.appendChild(header); | |||
| 71 | - | this.debugElement.appendChild(this.logsContainer); | |||
| 72 | - | document.body.appendChild(this.debugElement); | |||
| 73 | - | ||||
| 74 | - | // Setup button handlers | |||
| 75 | - | header.querySelector('#clear-logs')?.addEventListener('click', () => this.clearLogs()); | |||
| 76 | - | header.querySelector('#close-debug')?.addEventListener('click', () => this.hide()); | |||
| 77 | - | } | |||
| 78 | - | ||||
| 79 | - | private setupKeyboardShortcut(): void { | |||
| 80 | - | document.addEventListener('keydown', (event) => { | |||
| 81 | - | // F12 to toggle debug console | |||
| 82 | - | if (event.key === 'F12') { | |||
| 83 | - | event.preventDefault(); | |||
| 84 | - | this.toggle(); | |||
| 85 | - | } | |||
| 86 | - | }); | |||
| 87 | - | } | |||
| 88 | - | ||||
| 89 | - | private interceptConsole(): void { | |||
| 90 | - | const originalConsole = { | |||
| 91 | - | log: console.log, | |||
| 92 | - | warn: console.warn, | |||
| 93 | - | error: console.error, | |||
| 94 | - | info: console.info, | |||
| 95 | - | debug: console.debug | |||
| 96 | - | }; | |||
| 97 | - | ||||
| 98 | - | console.log = (...args) => { | |||
| 99 | - | this.addLog('info', 'frontend', 'console', args.join(' ')); | |||
| 100 | - | originalConsole.log(...args); | |||
| 101 | - | }; | |||
| 102 | - | ||||
| 103 | - | console.warn = (...args) => { | |||
| 104 | - | this.addLog('warn', 'frontend', 'console', args.join(' ')); | |||
| 105 | - | originalConsole.warn(...args); | |||
| 106 | - | }; | |||
| 107 | - | ||||
| 108 | - | console.error = (...args) => { | |||
| 109 | - | this.addLog('error', 'frontend', 'console', args.join(' ')); | |||
| 110 | - | originalConsole.error(...args); | |||
| 111 | - | }; | |||
| 112 | - | ||||
| 113 | - | console.info = (...args) => { | |||
| 114 | - | this.addLog('info', 'frontend', 'console', args.join(' ')); | |||
| 115 | - | originalConsole.info(...args); | |||
| 116 | - | }; | |||
| 117 | - | ||||
| 118 | - | console.debug = (...args) => { | |||
| 119 | - | this.addLog('debug', 'frontend', 'console', args.join(' ')); | |||
| 120 | - | originalConsole.debug(...args); | |||
| 121 | - | }; | |||
| 122 | - | } | |||
| 123 | - | ||||
| 124 | - | public addLog(level: DebugLogEntry['level'], source: DebugLogEntry['source'], category: string, message: string, data?: any): void { | |||
| 125 | - | const entry: DebugLogEntry = { | |||
| 126 | - | timestamp: new Date().toISOString().split('T')[1].split('.')[0], // HH:MM:SS format | |||
| 127 | - | level, | |||
| 128 | - | source, | |||
| 129 | - | category, | |||
| 130 | - | message, | |||
| 131 | - | data | |||
| 132 | - | }; | |||
| 133 | - | ||||
| 134 | - | this.logs.push(entry); | |||
| 135 | - | ||||
| 136 | - | // Keep only the last maxLogs entries | |||
| 137 | - | if (this.logs.length > this.maxLogs) { | |||
| 138 | - | this.logs.shift(); | |||
| 139 | - | } | |||
| 140 | - | ||||
| 141 | - | this.updateDisplay(); | |||
| 142 | - | } | |||
| 143 | - | ||||
| 144 | - | private updateDisplay(): void { | |||
| 145 | - | if (!this.logsContainer) return; | |||
| 146 | - | ||||
| 147 | - | const logHTML = this.logs.map(log => { | |||
| 148 | - | const levelColor = { | |||
| 149 | - | info: '#00ff00', | |||
| 150 | - | warn: '#ffff00', | |||
| 151 | - | error: '#ff0000', | |||
| 152 | - | debug: '#00ffff' | |||
| 153 | - | }[log.level]; | |||
| 154 | - | ||||
| 155 | - | const sourceIcon = log.source === 'backend' ? '🖥️' : '🌐'; | |||
| 156 | - | ||||
| 157 | - | return ` | |||
| 158 | - | <div style="margin-bottom: 2px; font-size: 11px;"> | |||
| 159 | - | <span style="color: #888;">[${log.timestamp}]</span> | |||
| 160 | - | <span style="color: ${levelColor};">${sourceIcon} ${log.level.toUpperCase()}</span> | |||
| 161 | - | <span style="color: #aaa;">[${log.category}]</span> | |||
| 162 | - | <span style="color: #fff;">${log.message}</span> | |||
| 163 | - | ${log.data ? `<pre style="color: #ccc; font-size: 10px; margin: 2px 0;">${JSON.stringify(log.data, null, 2)}</pre>` : ''} | |||
| 164 | - | </div> | |||
| 165 | - | `; | |||
| 166 | - | }).join(''); | |||
| 167 | - | ||||
| 168 | - | this.logsContainer.innerHTML = logHTML; | |||
| 169 | - | ||||
| 170 | - | // Auto-scroll to bottom | |||
| 171 | - | this.logsContainer.scrollTop = this.logsContainer.scrollHeight; | |||
| 172 | - | } | |||
| 173 | - | ||||
| 174 | - | public logKeyboardEvent(event: KeyboardEvent): void { | |||
| 175 | - | const modifiers = []; | |||
| 176 | - | if (event.ctrlKey) modifiers.push('Ctrl'); | |||
| 177 | - | if (event.shiftKey) modifiers.push('Shift'); | |||
| 178 | - | if (event.altKey) modifiers.push('Alt'); | |||
| 179 | - | if (event.metaKey) modifiers.push('Meta'); | |||
| 180 | - | ||||
| 181 | - | const shortcut = modifiers.length > 0 ? `${modifiers.join('+')}+${event.key}` : event.key; | |||
| 182 | - | ||||
| 183 | - | this.addLog('info', 'frontend', 'keyboard', `Key pressed: ${shortcut}`, { | |||
| 184 | - | key: event.key, | |||
| 185 | - | code: event.code, | |||
| 186 | - | modifiers: modifiers, | |||
| 187 | - | target: event.target?.constructor.name | |||
| 188 | - | }); | |||
| 189 | - | } | |||
| 190 | - | ||||
| 191 | - | public logMouseEvent(event: MouseEvent): void { | |||
| 192 | - | const buttonNames = { | |||
| 193 | - | 0: 'Left', | |||
| 194 | - | 1: 'Middle', | |||
| 195 | - | 2: 'Right', | |||
| 196 | - | 3: 'Back', | |||
| 197 | - | 4: 'Forward' | |||
| 198 | - | }; | |||
| 199 | - | ||||
| 200 | - | this.addLog('info', 'frontend', 'mouse', `Mouse ${event.type}: ${buttonNames[event.button] || `Button${event.button}`}`, { | |||
| 201 | - | button: event.button, | |||
| 202 | - | clientX: event.clientX, | |||
| 203 | - | clientY: event.clientY, | |||
| 204 | - | target: event.target?.constructor.name | |||
| 205 | - | }); | |||
| 206 | - | } | |||
| 207 | - | ||||
| 208 | - | public logServiceCall(serviceName: string, methodName: string, params: any[], result?: any, error?: Error): void { | |||
| 209 | - | if (error) { | |||
| 210 | - | this.addLog('error', 'frontend', 'service', `${serviceName}.${methodName}() failed`, { | |||
| 211 | - | params, | |||
| 212 | - | error: error.message | |||
| 213 | - | }); | |||
| 214 | - | } else { | |||
| 215 | - | this.addLog('info', 'frontend', 'service', `${serviceName}.${methodName}() called`, { | |||
| 216 | - | params, | |||
| 217 | - | result | |||
| 218 | - | }); | |||
| 219 | - | } | |||
| 220 | - | } | |||
| 221 | - | ||||
| 222 | - | public logBackendMessage(message: string, data?: any): void { | |||
| 223 | - | this.addLog('info', 'backend', 'service', message, data); | |||
| 224 | - | } | |||
| 225 | - | ||||
| 226 | - | public show(): void { | |||
| 227 | - | if (this.debugElement) { | |||
| 228 | - | this.debugElement.style.display = 'block'; | |||
| 229 | - | this.isVisible = true; | |||
| 230 | - | this.addLog('info', 'frontend', 'debug', 'Debug console opened'); | |||
| 231 | - | } | |||
| 232 | - | } | |||
| 233 | - | ||||
| 234 | - | public hide(): void { | |||
| 235 | - | if (this.debugElement) { | |||
| 236 | - | this.debugElement.style.display = 'none'; | |||
| 237 | - | this.isVisible = false; | |||
| 238 | - | } | |||
| 239 | - | } | |||
| 240 | - | ||||
| 241 | - | public toggle(): void { | |||
| 242 | - | if (this.isVisible) { | |||
| 243 | - | this.hide(); | |||
| 244 | - | } else { | |||
| 245 | - | this.show(); | |||
| 246 | - | } | |||
| 247 | - | } | |||
| 248 | - | ||||
| 249 | - | public clearLogs(): void { | |||
| 250 | - | this.logs = []; | |||
| 251 | - | this.updateDisplay(); | |||
| 252 | - | this.addLog('info', 'frontend', 'debug', 'Logs cleared'); | |||
| 253 | - | } | |||
| 254 | - | ||||
| 255 | - | public isDebugVisible(): boolean { | |||
| 256 | - | return this.isVisible; | |||
| 257 | - | } | |||
| 258 | - | } | |||
D frontend/src/services/notifications.ts +0 -91
| @@ -1,91 +0,0 @@ | |||
| 1 | - | import type { NotificationOptions } from '../types/app.js'; | |
| 2 | - | ||
| 3 | - | export class NotificationService { | |
| 4 | - | private zoomNotification: HTMLElement | null = null; | |
| 5 | - | private generalNotification: HTMLElement | null = null; | |
| 6 | - | ||
| 7 | - | showZoom(message: string, duration = 2000): void { | |
| 8 | - | // Create or update zoom notification | |
| 9 | - | if (!this.zoomNotification) { | |
| 10 | - | this.zoomNotification = this.createNotificationElement('zoom-notification', { | |
| 11 | - | top: '20px', | |
| 12 | - | right: '20px', | |
| 13 | - | background: 'rgba(0, 0, 0, 0.8)', | |
| 14 | - | color: 'white' | |
| 15 | - | }); | |
| 16 | - | } | |
| 17 | - | ||
| 18 | - | this.updateNotification(this.zoomNotification, message, duration); | |
| 19 | - | } | |
| 20 | - | ||
| 21 | - | show(message: string, type: NotificationOptions['type'] = 'info', duration = 3000): void { | |
| 22 | - | // Create or update general notification | |
| 23 | - | if (!this.generalNotification) { | |
| 24 | - | this.generalNotification = this.createNotificationElement('general-notification', { | |
| 25 | - | top: '60px', | |
| 26 | - | right: '20px' | |
| 27 | - | }); | |
| 28 | - | } | |
| 29 | - | ||
| 30 | - | // Set colors based on type | |
| 31 | - | const colors = this.getNotificationColors(type); | |
| 32 | - | this.generalNotification.style.background = colors.background; | |
| 33 | - | this.generalNotification.style.color = colors.color; | |
| 34 | - | ||
| 35 | - | this.updateNotification(this.generalNotification, message, duration); | |
| 36 | - | } | |
| 37 | - | ||
| 38 | - | private createNotificationElement(id: string, baseStyles: Record<string, string>): HTMLElement { | |
| 39 | - | const notification = document.createElement('div'); | |
| 40 | - | notification.id = id; | |
| 41 | - | ||
| 42 | - | const styles = { | |
| 43 | - | position: 'fixed', | |
| 44 | - | padding: '10px 15px', | |
| 45 | - | borderRadius: '5px', | |
| 46 | - | fontSize: '14px', | |
| 47 | - | zIndex: '10000', | |
| 48 | - | transition: 'opacity 0.3s', | |
| 49 | - | opacity: '0', | |
| 50 | - | ...baseStyles | |
| 51 | - | }; | |
| 52 | - | ||
| 53 | - | notification.style.cssText = Object.entries(styles) | |
| 54 | - | .map(([key, value]) => `${this.camelToKebab(key)}: ${value}`) | |
| 55 | - | .join('; '); | |
| 56 | - | ||
| 57 | - | document.body.appendChild(notification); | |
| 58 | - | return notification; | |
| 59 | - | } | |
| 60 | - | ||
| 61 | - | private updateNotification(element: HTMLElement, message: string, duration: number): void { | |
| 62 | - | element.textContent = message; | |
| 63 | - | element.style.opacity = '1'; | |
| 64 | - | ||
| 65 | - | // Clear any existing timeout | |
| 66 | - | const existingTimeout = (element as any).timeout; | |
| 67 | - | if (existingTimeout) { | |
| 68 | - | clearTimeout(existingTimeout); | |
| 69 | - | } | |
| 70 | - | ||
| 71 | - | // Hide after duration | |
| 72 | - | (element as any).timeout = setTimeout(() => { | |
| 73 | - | element.style.opacity = '0'; | |
| 74 | - | }, duration); | |
| 75 | - | } | |
| 76 | - | ||
| 77 | - | private getNotificationColors(type: NotificationOptions['type']): { background: string; color: string } { | |
| 78 | - | switch (type) { | |
| 79 | - | case 'error': | |
| 80 | - | return { background: 'rgba(220, 53, 69, 0.9)', color: 'white' }; | |
| 81 | - | case 'success': | |
| 82 | - | return { background: 'rgba(25, 135, 84, 0.9)', color: 'white' }; | |
| 83 | - | default: | |
| 84 | - | return { background: 'rgba(13, 110, 253, 0.9)', color: 'white' }; | |
| 85 | - | } | |
| 86 | - | } | |
| 87 | - | ||
| 88 | - | private camelToKebab(str: string): string { | |
| 89 | - | return str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`); | |
| 90 | - | } | |
| 91 | - | }⏎̸ | |
| @@ -1,91 +0,0 @@ | |||||
| 1 | - | import type { NotificationOptions } from '../types/app.js'; | |||
| 2 | - | ||||
| 3 | - | export class NotificationService { | |||
| 4 | - | private zoomNotification: HTMLElement | null = null; | |||
| 5 | - | private generalNotification: HTMLElement | null = null; | |||
| 6 | - | ||||
| 7 | - | showZoom(message: string, duration = 2000): void { | |||
| 8 | - | // Create or update zoom notification | |||
| 9 | - | if (!this.zoomNotification) { | |||
| 10 | - | this.zoomNotification = this.createNotificationElement('zoom-notification', { | |||
| 11 | - | top: '20px', | |||
| 12 | - | right: '20px', | |||
| 13 | - | background: 'rgba(0, 0, 0, 0.8)', | |||
| 14 | - | color: 'white' | |||
| 15 | - | }); | |||
| 16 | - | } | |||
| 17 | - | ||||
| 18 | - | this.updateNotification(this.zoomNotification, message, duration); | |||
| 19 | - | } | |||
| 20 | - | ||||
| 21 | - | show(message: string, type: NotificationOptions['type'] = 'info', duration = 3000): void { | |||
| 22 | - | // Create or update general notification | |||
| 23 | - | if (!this.generalNotification) { | |||
| 24 | - | this.generalNotification = this.createNotificationElement('general-notification', { | |||
| 25 | - | top: '60px', | |||
| 26 | - | right: '20px' | |||
| 27 | - | }); | |||
| 28 | - | } | |||
| 29 | - | ||||
| 30 | - | // Set colors based on type | |||
| 31 | - | const colors = this.getNotificationColors(type); | |||
| 32 | - | this.generalNotification.style.background = colors.background; | |||
| 33 | - | this.generalNotification.style.color = colors.color; | |||
| 34 | - | ||||
| 35 | - | this.updateNotification(this.generalNotification, message, duration); | |||
| 36 | - | } | |||
| 37 | - | ||||
| 38 | - | private createNotificationElement(id: string, baseStyles: Record<string, string>): HTMLElement { | |||
| 39 | - | const notification = document.createElement('div'); | |||
| 40 | - | notification.id = id; | |||
| 41 | - | ||||
| 42 | - | const styles = { | |||
| 43 | - | position: 'fixed', | |||
| 44 | - | padding: '10px 15px', | |||
| 45 | - | borderRadius: '5px', | |||
| 46 | - | fontSize: '14px', | |||
| 47 | - | zIndex: '10000', | |||
| 48 | - | transition: 'opacity 0.3s', | |||
| 49 | - | opacity: '0', | |||
| 50 | - | ...baseStyles | |||
| 51 | - | }; | |||
| 52 | - | ||||
| 53 | - | notification.style.cssText = Object.entries(styles) | |||
| 54 | - | .map(([key, value]) => `${this.camelToKebab(key)}: ${value}`) | |||
| 55 | - | .join('; '); | |||
| 56 | - | ||||
| 57 | - | document.body.appendChild(notification); | |||
| 58 | - | return notification; | |||
| 59 | - | } | |||
| 60 | - | ||||
| 61 | - | private updateNotification(element: HTMLElement, message: string, duration: number): void { | |||
| 62 | - | element.textContent = message; | |||
| 63 | - | element.style.opacity = '1'; | |||
| 64 | - | ||||
| 65 | - | // Clear any existing timeout | |||
| 66 | - | const existingTimeout = (element as any).timeout; | |||
| 67 | - | if (existingTimeout) { | |||
| 68 | - | clearTimeout(existingTimeout); | |||
| 69 | - | } | |||
| 70 | - | ||||
| 71 | - | // Hide after duration | |||
| 72 | - | (element as any).timeout = setTimeout(() => { | |||
| 73 | - | element.style.opacity = '0'; | |||
| 74 | - | }, duration); | |||
| 75 | - | } | |||
| 76 | - | ||||
| 77 | - | private getNotificationColors(type: NotificationOptions['type']): { background: string; color: string } { | |||
| 78 | - | switch (type) { | |||
| 79 | - | case 'error': | |||
| 80 | - | return { background: 'rgba(220, 53, 69, 0.9)', color: 'white' }; | |||
| 81 | - | case 'success': | |||
| 82 | - | return { background: 'rgba(25, 135, 84, 0.9)', color: 'white' }; | |||
| 83 | - | default: | |||
| 84 | - | return { background: 'rgba(13, 110, 253, 0.9)', color: 'white' }; | |||
| 85 | - | } | |||
| 86 | - | } | |||
| 87 | - | ||||
| 88 | - | private camelToKebab(str: string): string { | |||
| 89 | - | return str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`); | |||
| 90 | - | } | |||
| 91 | - | }⏎̸ | |||
D frontend/src/types/app.ts +0 -26
| @@ -1,26 +0,0 @@ | |||
| 1 | - | import type { HistoryEntry, SearchShortcut } from './generated.js'; | |
| 2 | - | ||
| 3 | - | export interface AppState { | |
| 4 | - | currentURL: string; | |
| 5 | - | currentZoom: number; | |
| 6 | - | history: HistoryEntry[]; | |
| 7 | - | shortcuts: Record<string, SearchShortcut>; | |
| 8 | - | isLoading: boolean; | |
| 9 | - | } | |
| 10 | - | ||
| 11 | - | export interface NotificationOptions { | |
| 12 | - | message: string; | |
| 13 | - | type?: 'info' | 'success' | 'error'; | |
| 14 | - | duration?: number; | |
| 15 | - | } | |
| 16 | - | ||
| 17 | - | export interface KeyboardShortcut { | |
| 18 | - | key: string; | |
| 19 | - | ctrlKey?: boolean; | |
| 20 | - | metaKey?: boolean; | |
| 21 | - | shiftKey?: boolean; | |
| 22 | - | altKey?: boolean; | |
| 23 | - | handler: () => void | Promise<void>; | |
| 24 | - | } | |
| 25 | - | ||
| 26 | - | export { HistoryEntry, SearchShortcut } from './generated.js';⏎̸ | |
| @@ -1,26 +0,0 @@ | |||||
| 1 | - | import type { HistoryEntry, SearchShortcut } from './generated.js'; | |||
| 2 | - | ||||
| 3 | - | export interface AppState { | |||
| 4 | - | currentURL: string; | |||
| 5 | - | currentZoom: number; | |||
| 6 | - | history: HistoryEntry[]; | |||
| 7 | - | shortcuts: Record<string, SearchShortcut>; | |||
| 8 | - | isLoading: boolean; | |||
| 9 | - | } | |||
| 10 | - | ||||
| 11 | - | export interface NotificationOptions { | |||
| 12 | - | message: string; | |||
| 13 | - | type?: 'info' | 'success' | 'error'; | |||
| 14 | - | duration?: number; | |||
| 15 | - | } | |||
| 16 | - | ||||
| 17 | - | export interface KeyboardShortcut { | |||
| 18 | - | key: string; | |||
| 19 | - | ctrlKey?: boolean; | |||
| 20 | - | metaKey?: boolean; | |||
| 21 | - | shiftKey?: boolean; | |||
| 22 | - | altKey?: boolean; | |||
| 23 | - | handler: () => void | Promise<void>; | |||
| 24 | - | } | |||
| 25 | - | ||||
| 26 | - | export { HistoryEntry, SearchShortcut } from './generated.js';⏎̸ | |||
D frontend/src/types/generated.ts +0 -80
| @@ -1,80 +0,0 @@ | |||
| 1 | - | // GENERATED FROM GO FILES: SEE THE GENERATOR SCRIPT | |
| 2 | - | // Auto-generated TypeScript types from Go structs | |
| 3 | - | // Generated on: 2025-09-09T09:57:41.059Z | |
| 4 | - | // Generator script: scripts/generate-types.js | |
| 5 | - | // Source files: internal/db/models.go, internal/parser/types.go | |
| 6 | - | // Do not edit manually - run 'node scripts/generate-types.js' to regenerate | |
| 7 | - | ||
| 8 | - | export interface History { | |
| 9 | - | id: number; | |
| 10 | - | url: string; | |
| 11 | - | title: string | null; | |
| 12 | - | visit_count: number | null; | |
| 13 | - | last_visited: string | null; | |
| 14 | - | created_at: string | null; | |
| 15 | - | } | |
| 16 | - | ||
| 17 | - | export interface Shortcut { | |
| 18 | - | id: number; | |
| 19 | - | shortcut: string; | |
| 20 | - | url_template: string; | |
| 21 | - | description: string | null; | |
| 22 | - | created_at: string | null; | |
| 23 | - | } | |
| 24 | - | ||
| 25 | - | export interface ZoomSetting { | |
| 26 | - | id: number; | |
| 27 | - | url: string; | |
| 28 | - | zoom_level: number; | |
| 29 | - | last_updated: string | null; | |
| 30 | - | } | |
| 31 | - | ||
| 32 | - | export interface ParseResult { | |
| 33 | - | type: InputType; | |
| 34 | - | url: string; | |
| 35 | - | query: string; | |
| 36 | - | confidence: number; | |
| 37 | - | fuzzy_matches?: FuzzyMatch[]; | |
| 38 | - | shortcut?: DetectedShortcut; | |
| 39 | - | processing_time: number; | |
| 40 | - | } | |
| 41 | - | ||
| 42 | - | export interface DetectedShortcut { | |
| 43 | - | key: string; | |
| 44 | - | query: string; | |
| 45 | - | url: string; | |
| 46 | - | description: string; | |
| 47 | - | } | |
| 48 | - | ||
| 49 | - | export interface FuzzyMatch { | |
| 50 | - | history_entry?: History; | |
| 51 | - | score: number; | |
| 52 | - | url_score: number; | |
| 53 | - | title_score: number; | |
| 54 | - | recency_score: number; | |
| 55 | - | visit_score: number; | |
| 56 | - | matched_field: string; | |
| 57 | - | } | |
| 58 | - | ||
| 59 | - | export interface FuzzyConfig { | |
| 60 | - | min_similarity_threshold: number; | |
| 61 | - | max_results: number; | |
| 62 | - | url_weight: number; | |
| 63 | - | title_weight: number; | |
| 64 | - | recency_weight: number; | |
| 65 | - | visit_weight: number; | |
| 66 | - | recency_decay_days: number; | |
| 67 | - | } | |
| 68 | - | ||
| 69 | - | // Manual types and enums | |
| 70 | - | export enum InputType { | |
| 71 | - | InputTypeDirectURL = 0, | |
| 72 | - | InputTypeSearchShortcut = 1, | |
| 73 | - | InputTypeHistorySearch = 2, | |
| 74 | - | InputTypeFallbackSearch = 3 | |
| 75 | - | } | |
| 76 | - | ||
| 77 | - | // Type aliases for frontend usage | |
| 78 | - | export type HistoryEntry = History; | |
| 79 | - | export type SearchShortcut = Shortcut; | |
| 80 | - | export type ZoomEntry = ZoomSetting; | |
| @@ -1,80 +0,0 @@ | |||||
| 1 | - | // GENERATED FROM GO FILES: SEE THE GENERATOR SCRIPT | |||
| 2 | - | // Auto-generated TypeScript types from Go structs | |||
| 3 | - | // Generated on: 2025-09-09T09:57:41.059Z | |||
| 4 | - | // Generator script: scripts/generate-types.js | |||
| 5 | - | // Source files: internal/db/models.go, internal/parser/types.go | |||
| 6 | - | // Do not edit manually - run 'node scripts/generate-types.js' to regenerate | |||
| 7 | - | ||||
| 8 | - | export interface History { | |||
| 9 | - | id: number; | |||
| 10 | - | url: string; | |||
| 11 | - | title: string | null; | |||
| 12 | - | visit_count: number | null; | |||
| 13 | - | last_visited: string | null; | |||
| 14 | - | created_at: string | null; | |||
| 15 | - | } | |||
| 16 | - | ||||
| 17 | - | export interface Shortcut { | |||
| 18 | - | id: number; | |||
| 19 | - | shortcut: string; | |||
| 20 | - | url_template: string; | |||
| 21 | - | description: string | null; | |||
| 22 | - | created_at: string | null; | |||
| 23 | - | } | |||
| 24 | - | ||||
| 25 | - | export interface ZoomSetting { | |||
| 26 | - | id: number; | |||
| 27 | - | url: string; | |||
| 28 | - | zoom_level: number; | |||
| 29 | - | last_updated: string | null; | |||
| 30 | - | } | |||
| 31 | - | ||||
| 32 | - | export interface ParseResult { | |||
| 33 | - | type: InputType; | |||
| 34 | - | url: string; | |||
| 35 | - | query: string; | |||
| 36 | - | confidence: number; | |||
| 37 | - | fuzzy_matches?: FuzzyMatch[]; | |||
| 38 | - | shortcut?: DetectedShortcut; | |||
| 39 | - | processing_time: number; | |||
| 40 | - | } | |||
| 41 | - | ||||
| 42 | - | export interface DetectedShortcut { | |||
| 43 | - | key: string; | |||
| 44 | - | query: string; | |||
| 45 | - | url: string; | |||
| 46 | - | description: string; | |||
| 47 | - | } | |||
| 48 | - | ||||
| 49 | - | export interface FuzzyMatch { | |||
| 50 | - | history_entry?: History; | |||
| 51 | - | score: number; | |||
| 52 | - | url_score: number; | |||
| 53 | - | title_score: number; | |||
| 54 | - | recency_score: number; | |||
| 55 | - | visit_score: number; | |||
| 56 | - | matched_field: string; | |||
| 57 | - | } | |||
| 58 | - | ||||
| 59 | - | export interface FuzzyConfig { | |||
| 60 | - | min_similarity_threshold: number; | |||
| 61 | - | max_results: number; | |||
| 62 | - | url_weight: number; | |||
| 63 | - | title_weight: number; | |||
| 64 | - | recency_weight: number; | |||
| 65 | - | visit_weight: number; | |||
| 66 | - | recency_decay_days: number; | |||
| 67 | - | } | |||
| 68 | - | ||||
| 69 | - | // Manual types and enums | |||
| 70 | - | export enum InputType { | |||
| 71 | - | InputTypeDirectURL = 0, | |||
| 72 | - | InputTypeSearchShortcut = 1, | |||
| 73 | - | InputTypeHistorySearch = 2, | |||
| 74 | - | InputTypeFallbackSearch = 3 | |||
| 75 | - | } | |||
| 76 | - | ||||
| 77 | - | // Type aliases for frontend usage | |||
| 78 | - | export type HistoryEntry = History; | |||
| 79 | - | export type SearchShortcut = Shortcut; | |||
| 80 | - | export type ZoomEntry = ZoomSetting; | |||
D frontend/src/utils/dom.ts +0 -212
| @@ -1,212 +0,0 @@ | |||
| 1 | - | type HistoryEntry = { id: number; url: string; title: string }; | |
| 2 | - | type SearchShortcut = { description: string; url: string }; | |
| 3 | - | ||
| 4 | - | export class DOMRenderer { | |
| 5 | - | private historyListElement: HTMLElement | null = null; | |
| 6 | - | private shortcutsElement: HTMLElement | null = null; | |
| 7 | - | ||
| 8 | - | constructor() { | |
| 9 | - | this.historyListElement = document.getElementById('historyList'); | |
| 10 | - | this.shortcutsElement = document.getElementById('shortcuts'); | |
| 11 | - | } | |
| 12 | - | ||
| 13 | - | displayHistory(historyEntries: HistoryEntry[]): void { | |
| 14 | - | if (!this.historyListElement) return; | |
| 15 | - | ||
| 16 | - | if (!historyEntries || historyEntries.length === 0) { | |
| 17 | - | this.historyListElement.innerHTML = ` | |
| 18 | - | <div class="empty-state"> | |
| 19 | - | <h3>No history yet</h3> | |
| 20 | - | <p>Use CLI commands to browse websites</p> | |
| 21 | - | </div> | |
| 22 | - | `; | |
| 23 | - | return; | |
| 24 | - | } | |
| 25 | - | ||
| 26 | - | this.historyListElement.innerHTML = ''; | |
| 27 | - | historyEntries.forEach(item => { | |
| 28 | - | const historyItem = this.createHistoryItem(item); | |
| 29 | - | this.historyListElement!.appendChild(historyItem); | |
| 30 | - | }); | |
| 31 | - | } | |
| 32 | - | ||
| 33 | - | displayShortcuts(shortcutsData: Record<string, SearchShortcut>): void { | |
| 34 | - | if (!this.shortcutsElement) return; | |
| 35 | - | ||
| 36 | - | if (!shortcutsData || Object.keys(shortcutsData).length === 0) { | |
| 37 | - | this.shortcutsElement.innerHTML = ` | |
| 38 | - | <div class="empty-state"> | |
| 39 | - | <h3>No shortcuts configured</h3> | |
| 40 | - | <p>Add shortcuts to your config file</p> | |
| 41 | - | </div> | |
| 42 | - | `; | |
| 43 | - | return; | |
| 44 | - | } | |
| 45 | - | ||
| 46 | - | this.shortcutsElement.innerHTML = ''; | |
| 47 | - | Object.entries(shortcutsData).forEach(([key, shortcut]) => { | |
| 48 | - | const shortcutEl = this.createShortcutElement(key, shortcut); | |
| 49 | - | this.shortcutsElement!.appendChild(shortcutEl); | |
| 50 | - | }); | |
| 51 | - | } | |
| 52 | - | ||
| 53 | - | showLoading(container: 'history' | 'shortcuts'): void { | |
| 54 | - | const element = container === 'history' ? this.historyListElement : this.shortcutsElement; | |
| 55 | - | if (element) { | |
| 56 | - | element.innerHTML = '<div class="loading">Loading...</div>'; | |
| 57 | - | } | |
| 58 | - | } | |
| 59 | - | ||
| 60 | - | showError(container: 'history' | 'shortcuts', message: string): void { | |
| 61 | - | const element = container === 'history' ? this.historyListElement : this.shortcutsElement; | |
| 62 | - | if (element) { | |
| 63 | - | element.innerHTML = ` | |
| 64 | - | <div class="empty-state"> | |
| 65 | - | <h3>Error</h3> | |
| 66 | - | <p>${message}</p> | |
| 67 | - | </div> | |
| 68 | - | `; | |
| 69 | - | } | |
| 70 | - | } | |
| 71 | - | ||
| 72 | - | private createHistoryItem(item: HistoryEntry): HTMLElement { | |
| 73 | - | const historyItem = document.createElement('div'); | |
| 74 | - | historyItem.className = 'history-item'; | |
| 75 | - | const parsed = this.safeParseURL(item.url); | |
| 76 | - | const title = item.title && item.title.trim() !== '' ? item.title : (parsed.host || 'Untitled'); | |
| 77 | - | ||
| 78 | - | const line = document.createElement('div'); | |
| 79 | - | line.className = 'history-line'; | |
| 80 | - | ||
| 81 | - | // Favicon chip (wrapper ensures perfect circle sizing) | |
| 82 | - | const iconWrap = document.createElement('div'); | |
| 83 | - | iconWrap.className = 'history-favicon-chip'; | |
| 84 | - | const icon = document.createElement('img'); | |
| 85 | - | icon.className = 'history-favicon-img'; | |
| 86 | - | icon.width = 16; icon.height = 16; | |
| 87 | - | icon.loading = 'lazy'; | |
| 88 | - | icon.referrerPolicy = 'no-referrer'; | |
| 89 | - | icon.src = this.buildFaviconURL(item.url); | |
| 90 | - | icon.addEventListener('error', () => { iconWrap.style.display = 'none'; }); | |
| 91 | - | iconWrap.appendChild(icon); | |
| 92 | - | ||
| 93 | - | const titleEl = document.createElement('span'); | |
| 94 | - | titleEl.className = 'history-title'; | |
| 95 | - | titleEl.textContent = title; | |
| 96 | - | ||
| 97 | - | const sep1 = document.createElement('span'); | |
| 98 | - | sep1.className = 'history-sep'; | |
| 99 | - | sep1.textContent = ' - '; | |
| 100 | - | ||
| 101 | - | const domainEl = document.createElement('span'); | |
| 102 | - | domainEl.className = 'history-domain'; | |
| 103 | - | domainEl.textContent = parsed.host || ''; | |
| 104 | - | ||
| 105 | - | const sep2 = document.createElement('span'); | |
| 106 | - | sep2.className = 'history-sep'; | |
| 107 | - | sep2.textContent = ' - '; | |
| 108 | - | ||
| 109 | - | const urlEl = document.createElement('span'); | |
| 110 | - | urlEl.className = 'history-url'; | |
| 111 | - | urlEl.textContent = item.url; | |
| 112 | - | ||
| 113 | - | line.appendChild(iconWrap); | |
| 114 | - | line.appendChild(titleEl); | |
| 115 | - | line.appendChild(sep1); | |
| 116 | - | line.appendChild(domainEl); | |
| 117 | - | line.appendChild(sep2); | |
| 118 | - | line.appendChild(urlEl); | |
| 119 | - | ||
| 120 | - | historyItem.appendChild(line); | |
| 121 | - | ||
| 122 | - | historyItem.addEventListener('click', () => { | |
| 123 | - | this.navigateToUrl(item.url); | |
| 124 | - | }); | |
| 125 | - | ||
| 126 | - | return historyItem; | |
| 127 | - | } | |
| 128 | - | ||
| 129 | - | private createShortcutElement(key: string, shortcut: SearchShortcut): HTMLElement { | |
| 130 | - | const shortcutEl = document.createElement('div'); | |
| 131 | - | shortcutEl.className = 'shortcut'; | |
| 132 | - | shortcutEl.innerHTML = ` | |
| 133 | - | <div class="shortcut-key">${this.escapeHtml(key)}:</div> | |
| 134 | - | <div class="shortcut-desc">${this.escapeHtml(shortcut.description)}</div> | |
| 135 | - | `; | |
| 136 | - | ||
| 137 | - | shortcutEl.addEventListener('click', () => { | |
| 138 | - | // Navigate to the base URL of the shortcut (remove %s template parameter) | |
| 139 | - | const baseUrl = this.extractBaseUrl(shortcut.url); | |
| 140 | - | this.navigateToUrl(baseUrl); | |
| 141 | - | }); | |
| 142 | - | ||
| 143 | - | return shortcutEl; | |
| 144 | - | } | |
| 145 | - | ||
| 146 | - | private async navigateToUrl(url: string): Promise<void> { | |
| 147 | - | console.log('Navigating to:', url); | |
| 148 | - | window.location.href = url; | |
| 149 | - | } | |
| 150 | - | ||
| 151 | - | private extractBaseUrl(templateUrl: string): string { | |
| 152 | - | try { | |
| 153 | - | // Support both %s and {query} placeholders | |
| 154 | - | const candidate = templateUrl.replace(/%s|\{query\}/g, ""); | |
| 155 | - | const url = new URL(candidate); | |
| 156 | - | // Remove empty query params left after placeholder removal | |
| 157 | - | const params = new URLSearchParams(url.search); | |
| 158 | - | for (const key of Array.from(params.keys())) { | |
| 159 | - | const v = params.get(key); | |
| 160 | - | if (v === null || v === "") { | |
| 161 | - | params.delete(key); | |
| 162 | - | } | |
| 163 | - | } | |
| 164 | - | url.search = params.toString(); | |
| 165 | - | return url.toString(); | |
| 166 | - | } catch (error) { | |
| 167 | - | // Fallbacks: try to return origin or stripped placeholder | |
| 168 | - | console.warn('Failed to parse shortcut URL:', templateUrl); | |
| 169 | - | try { | |
| 170 | - | const pre = templateUrl.split('%s')[0] || templateUrl.split('{query}')[0] || templateUrl; | |
| 171 | - | const u = new URL(pre); | |
| 172 | - | return `${u.protocol}//${u.host}`; | |
| 173 | - | } catch { | |
| 174 | - | return templateUrl.replace(/%s|\{query\}/g, ""); | |
| 175 | - | } | |
| 176 | - | } | |
| 177 | - | } | |
| 178 | - | ||
| 179 | - | private async copyToClipboard(text: string): Promise<void> { | |
| 180 | - | try { | |
| 181 | - | await navigator.clipboard.writeText(text); | |
| 182 | - | console.log('Copied to clipboard:', text); | |
| 183 | - | } catch (err) { | |
| 184 | - | console.error('Failed to copy:', err); | |
| 185 | - | } | |
| 186 | - | } | |
| 187 | - | ||
| 188 | - | private escapeHtml(text: string): string { | |
| 189 | - | const div = document.createElement('div'); | |
| 190 | - | div.textContent = text; | |
| 191 | - | return div.innerHTML; | |
| 192 | - | } | |
| 193 | - | ||
| 194 | - | private safeParseURL(raw: string): { host: string } { | |
| 195 | - | try { | |
| 196 | - | const u = new URL(raw); | |
| 197 | - | return { host: u.host }; | |
| 198 | - | } catch { | |
| 199 | - | return { host: '' }; | |
| 200 | - | } | |
| 201 | - | } | |
| 202 | - | ||
| 203 | - | private buildFaviconURL(raw: string): string { | |
| 204 | - | try { | |
| 205 | - | const u = new URL(raw); | |
| 206 | - | const scheme = u.protocol && u.protocol !== ':' ? u.protocol : 'https:'; | |
| 207 | - | return `${scheme}//${u.host}/favicon.ico`; | |
| 208 | - | } catch { | |
| 209 | - | return ''; | |
| 210 | - | } | |
| 211 | - | } | |
| 212 | - | } | |
| @@ -1,212 +0,0 @@ | |||||
| 1 | - | type HistoryEntry = { id: number; url: string; title: string }; | |||
| 2 | - | type SearchShortcut = { description: string; url: string }; | |||
| 3 | - | ||||
| 4 | - | export class DOMRenderer { | |||
| 5 | - | private historyListElement: HTMLElement | null = null; | |||
| 6 | - | private shortcutsElement: HTMLElement | null = null; | |||
| 7 | - | ||||
| 8 | - | constructor() { | |||
| 9 | - | this.historyListElement = document.getElementById('historyList'); | |||
| 10 | - | this.shortcutsElement = document.getElementById('shortcuts'); | |||
| 11 | - | } | |||
| 12 | - | ||||
| 13 | - | displayHistory(historyEntries: HistoryEntry[]): void { | |||
| 14 | - | if (!this.historyListElement) return; | |||
| 15 | - | ||||
| 16 | - | if (!historyEntries || historyEntries.length === 0) { | |||
| 17 | - | this.historyListElement.innerHTML = ` | |||
| 18 | - | <div class="empty-state"> | |||
| 19 | - | <h3>No history yet</h3> | |||
| 20 | - | <p>Use CLI commands to browse websites</p> | |||
| 21 | - | </div> | |||
| 22 | - | `; | |||
| 23 | - | return; | |||
| 24 | - | } | |||
| 25 | - | ||||
| 26 | - | this.historyListElement.innerHTML = ''; | |||
| 27 | - | historyEntries.forEach(item => { | |||
| 28 | - | const historyItem = this.createHistoryItem(item); | |||
| 29 | - | this.historyListElement!.appendChild(historyItem); | |||
| 30 | - | }); | |||
| 31 | - | } | |||
| 32 | - | ||||
| 33 | - | displayShortcuts(shortcutsData: Record<string, SearchShortcut>): void { | |||
| 34 | - | if (!this.shortcutsElement) return; | |||
| 35 | - | ||||
| 36 | - | if (!shortcutsData || Object.keys(shortcutsData).length === 0) { | |||
| 37 | - | this.shortcutsElement.innerHTML = ` | |||
| 38 | - | <div class="empty-state"> | |||
| 39 | - | <h3>No shortcuts configured</h3> | |||
| 40 | - | <p>Add shortcuts to your config file</p> | |||
| 41 | - | </div> | |||
| 42 | - | `; | |||
| 43 | - | return; | |||
| 44 | - | } | |||
| 45 | - | ||||
| 46 | - | this.shortcutsElement.innerHTML = ''; | |||
| 47 | - | Object.entries(shortcutsData).forEach(([key, shortcut]) => { | |||
| 48 | - | const shortcutEl = this.createShortcutElement(key, shortcut); | |||
| 49 | - | this.shortcutsElement!.appendChild(shortcutEl); | |||
| 50 | - | }); | |||
| 51 | - | } | |||
| 52 | - | ||||
| 53 | - | showLoading(container: 'history' | 'shortcuts'): void { | |||
| 54 | - | const element = container === 'history' ? this.historyListElement : this.shortcutsElement; | |||
| 55 | - | if (element) { | |||
| 56 | - | element.innerHTML = '<div class="loading">Loading...</div>'; | |||
| 57 | - | } | |||
| 58 | - | } | |||
| 59 | - | ||||
| 60 | - | showError(container: 'history' | 'shortcuts', message: string): void { | |||
| 61 | - | const element = container === 'history' ? this.historyListElement : this.shortcutsElement; | |||
| 62 | - | if (element) { | |||
| 63 | - | element.innerHTML = ` | |||
| 64 | - | <div class="empty-state"> | |||
| 65 | - | <h3>Error</h3> | |||
| 66 | - | <p>${message}</p> | |||
| 67 | - | </div> | |||
| 68 | - | `; | |||
| 69 | - | } | |||
| 70 | - | } | |||
| 71 | - | ||||
| 72 | - | private createHistoryItem(item: HistoryEntry): HTMLElement { | |||
| 73 | - | const historyItem = document.createElement('div'); | |||
| 74 | - | historyItem.className = 'history-item'; | |||
| 75 | - | const parsed = this.safeParseURL(item.url); | |||
| 76 | - | const title = item.title && item.title.trim() !== '' ? item.title : (parsed.host || 'Untitled'); | |||
| 77 | - | ||||
| 78 | - | const line = document.createElement('div'); | |||
| 79 | - | line.className = 'history-line'; | |||
| 80 | - | ||||
| 81 | - | // Favicon chip (wrapper ensures perfect circle sizing) | |||
| 82 | - | const iconWrap = document.createElement('div'); | |||
| 83 | - | iconWrap.className = 'history-favicon-chip'; | |||
| 84 | - | const icon = document.createElement('img'); | |||
| 85 | - | icon.className = 'history-favicon-img'; | |||
| 86 | - | icon.width = 16; icon.height = 16; | |||
| 87 | - | icon.loading = 'lazy'; | |||
| 88 | - | icon.referrerPolicy = 'no-referrer'; | |||
| 89 | - | icon.src = this.buildFaviconURL(item.url); | |||
| 90 | - | icon.addEventListener('error', () => { iconWrap.style.display = 'none'; }); | |||
| 91 | - | iconWrap.appendChild(icon); | |||
| 92 | - | ||||
| 93 | - | const titleEl = document.createElement('span'); | |||
| 94 | - | titleEl.className = 'history-title'; | |||
| 95 | - | titleEl.textContent = title; | |||
| 96 | - | ||||
| 97 | - | const sep1 = document.createElement('span'); | |||
| 98 | - | sep1.className = 'history-sep'; | |||
| 99 | - | sep1.textContent = ' - '; | |||
| 100 | - | ||||
| 101 | - | const domainEl = document.createElement('span'); | |||
| 102 | - | domainEl.className = 'history-domain'; | |||
| 103 | - | domainEl.textContent = parsed.host || ''; | |||
| 104 | - | ||||
| 105 | - | const sep2 = document.createElement('span'); | |||
| 106 | - | sep2.className = 'history-sep'; | |||
| 107 | - | sep2.textContent = ' - '; | |||
| 108 | - | ||||
| 109 | - | const urlEl = document.createElement('span'); | |||
| 110 | - | urlEl.className = 'history-url'; | |||
| 111 | - | urlEl.textContent = item.url; | |||
| 112 | - | ||||
| 113 | - | line.appendChild(iconWrap); | |||
| 114 | - | line.appendChild(titleEl); | |||
| 115 | - | line.appendChild(sep1); | |||
| 116 | - | line.appendChild(domainEl); | |||
| 117 | - | line.appendChild(sep2); | |||
| 118 | - | line.appendChild(urlEl); | |||
| 119 | - | ||||
| 120 | - | historyItem.appendChild(line); | |||
| 121 | - | ||||
| 122 | - | historyItem.addEventListener('click', () => { | |||
| 123 | - | this.navigateToUrl(item.url); | |||
| 124 | - | }); | |||
| 125 | - | ||||
| 126 | - | return historyItem; | |||
| 127 | - | } | |||
| 128 | - | ||||
| 129 | - | private createShortcutElement(key: string, shortcut: SearchShortcut): HTMLElement { | |||
| 130 | - | const shortcutEl = document.createElement('div'); | |||
| 131 | - | shortcutEl.className = 'shortcut'; | |||
| 132 | - | shortcutEl.innerHTML = ` | |||
| 133 | - | <div class="shortcut-key">${this.escapeHtml(key)}:</div> | |||
| 134 | - | <div class="shortcut-desc">${this.escapeHtml(shortcut.description)}</div> | |||
| 135 | - | `; | |||
| 136 | - | ||||
| 137 | - | shortcutEl.addEventListener('click', () => { | |||
| 138 | - | // Navigate to the base URL of the shortcut (remove %s template parameter) | |||
| 139 | - | const baseUrl = this.extractBaseUrl(shortcut.url); | |||
| 140 | - | this.navigateToUrl(baseUrl); | |||
| 141 | - | }); | |||
| 142 | - | ||||
| 143 | - | return shortcutEl; | |||
| 144 | - | } | |||
| 145 | - | ||||
| 146 | - | private async navigateToUrl(url: string): Promise<void> { | |||
| 147 | - | console.log('Navigating to:', url); | |||
| 148 | - | window.location.href = url; | |||
| 149 | - | } | |||
| 150 | - | ||||
| 151 | - | private extractBaseUrl(templateUrl: string): string { | |||
| 152 | - | try { | |||
| 153 | - | // Support both %s and {query} placeholders | |||
| 154 | - | const candidate = templateUrl.replace(/%s|\{query\}/g, ""); | |||
| 155 | - | const url = new URL(candidate); | |||
| 156 | - | // Remove empty query params left after placeholder removal | |||
| 157 | - | const params = new URLSearchParams(url.search); | |||
| 158 | - | for (const key of Array.from(params.keys())) { | |||
| 159 | - | const v = params.get(key); | |||
| 160 | - | if (v === null || v === "") { | |||
| 161 | - | params.delete(key); | |||
| 162 | - | } | |||
| 163 | - | } | |||
| 164 | - | url.search = params.toString(); | |||
| 165 | - | return url.toString(); | |||
| 166 | - | } catch (error) { | |||
| 167 | - | // Fallbacks: try to return origin or stripped placeholder | |||
| 168 | - | console.warn('Failed to parse shortcut URL:', templateUrl); | |||
| 169 | - | try { | |||
| 170 | - | const pre = templateUrl.split('%s')[0] || templateUrl.split('{query}')[0] || templateUrl; | |||
| 171 | - | const u = new URL(pre); | |||
| 172 | - | return `${u.protocol}//${u.host}`; | |||
| 173 | - | } catch { | |||
| 174 | - | return templateUrl.replace(/%s|\{query\}/g, ""); | |||
| 175 | - | } | |||
| 176 | - | } | |||
| 177 | - | } | |||
| 178 | - | ||||
| 179 | - | private async copyToClipboard(text: string): Promise<void> { | |||
| 180 | - | try { | |||
| 181 | - | await navigator.clipboard.writeText(text); | |||
| 182 | - | console.log('Copied to clipboard:', text); | |||
| 183 | - | } catch (err) { | |||
| 184 | - | console.error('Failed to copy:', err); | |||
| 185 | - | } | |||
| 186 | - | } | |||
| 187 | - | ||||
| 188 | - | private escapeHtml(text: string): string { | |||
| 189 | - | const div = document.createElement('div'); | |||
| 190 | - | div.textContent = text; | |||
| 191 | - | return div.innerHTML; | |||
| 192 | - | } | |||
| 193 | - | ||||
| 194 | - | private safeParseURL(raw: string): { host: string } { | |||
| 195 | - | try { | |||
| 196 | - | const u = new URL(raw); | |||
| 197 | - | return { host: u.host }; | |||
| 198 | - | } catch { | |||
| 199 | - | return { host: '' }; | |||
| 200 | - | } | |||
| 201 | - | } | |||
| 202 | - | ||||
| 203 | - | private buildFaviconURL(raw: string): string { | |||
| 204 | - | try { | |||
| 205 | - | const u = new URL(raw); | |||
| 206 | - | const scheme = u.protocol && u.protocol !== ':' ? u.protocol : 'https:'; | |||
| 207 | - | return `${scheme}//${u.host}/favicon.ico`; | |||
| 208 | - | } catch { | |||
| 209 | - | return ''; | |||
| 210 | - | } | |||
| 211 | - | } | |||
| 212 | - | } | |||
D frontend/src/utils/html-builder.ts +0 -251
| @@ -1,251 +0,0 @@ | |||
| 1 | - | export class HTMLBuilder { | |
| 2 | - | static createBaseHTML(): string { | |
| 3 | - | const faviconData = this.getFaviconDataURL(); | |
| 4 | - | return ` | |
| 5 | - | <!DOCTYPE html> | |
| 6 | - | <html lang="en"> | |
| 7 | - | <head> | |
| 8 | - | <meta charset="UTF-8"> | |
| 9 | - | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| 10 | - | <title>Dumber Browser</title> | |
| 11 | - | <link rel="icon" type="image/svg+xml" href="${faviconData}"> | |
| 12 | - | <style> | |
| 13 | - | ${this.getStyles()} | |
| 14 | - | </style> | |
| 15 | - | </head> | |
| 16 | - | <body> | |
| 17 | - | <div class="container"> | |
| 18 | - | <div class="history-section"> | |
| 19 | - | <h2 class="section-title">Recent History</h2> | |
| 20 | - | <div id="historyList" class="history-list"> | |
| 21 | - | <div class="loading">Loading history...</div> | |
| 22 | - | </div> | |
| 23 | - | </div> | |
| 24 | - | ||
| 25 | - | <div class="shortcuts-section"> | |
| 26 | - | <h2 class="section-title">Search Shortcuts</h2> | |
| 27 | - | <div id="shortcuts" class="shortcuts-grid"> | |
| 28 | - | <div class="loading">Loading shortcuts...</div> | |
| 29 | - | </div> | |
| 30 | - | </div> | |
| 31 | - | </div> | |
| 32 | - | <script type="module" src="./main.js"></script> | |
| 33 | - | </body> | |
| 34 | - | </html>`; | |
| 35 | - | } | |
| 36 | - | ||
| 37 | - | private static getFaviconDataURL(): string { | |
| 38 | - | const svg = HTMLBuilder.getFaviconSVG(); | |
| 39 | - | // Minimal encoding for safe inline usage | |
| 40 | - | const encoded = encodeURIComponent(svg) | |
| 41 | - | .replace(/'/g, '%27') | |
| 42 | - | .replace(/\(/g, '%28') | |
| 43 | - | .replace(/\)/g, '%29'); | |
| 44 | - | return `data:image/svg+xml;utf8,${encoded}`; | |
| 45 | - | } | |
| 46 | - | ||
| 47 | - | static getFaviconSVG(): string { | |
| 48 | - | // Stylized D with speed bolt on a subtle dark rounded card | |
| 49 | - | return `<?xml version="1.0" encoding="UTF-8"?> | |
| 50 | - | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"> | |
| 51 | - | <defs> | |
| 52 | - | <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"> | |
| 53 | - | <stop offset="0%" stop-color="#121212"/> | |
| 54 | - | <stop offset="100%" stop-color="#1e1e1e"/> | |
| 55 | - | </linearGradient> | |
| 56 | - | <filter id="inset" x="-20%" y="-20%" width="140%" height="140%"> | |
| 57 | - | <feOffset dx="0" dy="1"/> | |
| 58 | - | <feGaussianBlur stdDeviation="0.6" result="blur"/> | |
| 59 | - | <feComposite in2="SourceAlpha" operator="out"/> | |
| 60 | - | <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/> | |
| 61 | - | <feBlend in="SourceGraphic" mode="normal"/> | |
| 62 | - | </filter> | |
| 63 | - | </defs> | |
| 64 | - | <rect x="4" y="4" width="56" height="56" rx="14" ry="14" fill="url(#bg)" filter="url(#inset)"/> | |
| 65 | - | <rect x="4.5" y="4.5" width="55" height="55" rx="13.5" ry="13.5" fill="none" stroke="#252a2f" stroke-width="1"/> | |
| 66 | - | <!-- D glyph (slightly larger and wider) --> | |
| 67 | - | <path d="M20 16 L20 48 M20 16 H34 q12 0 12 16 t-12 16 H20" fill="none" stroke="#e9eef3" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round" transform="translate(33,32) scale(1.12,1.10) translate(-33,-32)"/> | |
| 68 | - | <!-- Speed bolt accent (significantly larger, centered) --> | |
| 69 | - | <path d="M36 22 L32 34 h6 l-8 16 4-12 h-6 z" fill="#ff9800" opacity="0.95" transform="translate(33,32) scale(1.8) translate(-33,-32) translate(0,-3)"/> | |
| 70 | - | </svg>`; | |
| 71 | - | } | |
| 72 | - | ||
| 73 | - | private static getStyles(): string { | |
| 74 | - | return ` | |
| 75 | - | * { | |
| 76 | - | margin: 0; | |
| 77 | - | padding: 0; | |
| 78 | - | box-sizing: border-box; | |
| 79 | - | } | |
| 80 | - | ||
| 81 | - | body { | |
| 82 | - | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| 83 | - | background: #1a1a1a; | |
| 84 | - | color: #ffffff; | |
| 85 | - | height: 100vh; | |
| 86 | - | display: flex; | |
| 87 | - | flex-direction: column; | |
| 88 | - | overflow-x: hidden; /* prevent horizontal scroll from long content */ | |
| 89 | - | } | |
| 90 | - | ||
| 91 | - | .container { | |
| 92 | - | flex: 1; | |
| 93 | - | display: flex; | |
| 94 | - | max-width: 1200px; | |
| 95 | - | margin: 0 auto; | |
| 96 | - | width: 100%; | |
| 97 | - | padding: 2rem; | |
| 98 | - | gap: 2rem; | |
| 99 | - | overflow-x: hidden; | |
| 100 | - | } | |
| 101 | - | ||
| 102 | - | .history-section { | |
| 103 | - | flex: 1; | |
| 104 | - | min-height: 0; | |
| 105 | - | min-width: 0; /* allow flex child to shrink */ | |
| 106 | - | } | |
| 107 | - | ||
| 108 | - | .shortcuts-section { | |
| 109 | - | flex: 1; | |
| 110 | - | min-height: 0; | |
| 111 | - | min-width: 0; /* allow flex child to shrink */ | |
| 112 | - | } | |
| 113 | - | ||
| 114 | - | .section-title { | |
| 115 | - | font-size: 1.5rem; | |
| 116 | - | margin-bottom: 1rem; | |
| 117 | - | color: #ffffff; | |
| 118 | - | border-bottom: 2px solid #404040; | |
| 119 | - | padding-bottom: 0.5rem; | |
| 120 | - | } | |
| 121 | - | ||
| 122 | - | .history-list { | |
| 123 | - | overflow-y: auto; | |
| 124 | - | overflow-x: hidden; | |
| 125 | - | max-height: calc(100vh - 8rem); | |
| 126 | - | max-width: 100%; | |
| 127 | - | } | |
| 128 | - | ||
| 129 | - | .history-item { | |
| 130 | - | padding: 0.75rem; | |
| 131 | - | margin-bottom: 0.5rem; | |
| 132 | - | background: #2d2d2d; | |
| 133 | - | border-radius: 6px; | |
| 134 | - | cursor: pointer; | |
| 135 | - | transition: background 0.2s; | |
| 136 | - | border-left: 3px solid #404040; | |
| 137 | - | overflow: hidden; | |
| 138 | - | max-width: 100%; | |
| 139 | - | } | |
| 140 | - | ||
| 141 | - | .history-item:hover { | |
| 142 | - | background: #3d3d3d; | |
| 143 | - | border-left-color: #0066cc; | |
| 144 | - | } | |
| 145 | - | ||
| 146 | - | .history-line { | |
| 147 | - | display: flex; | |
| 148 | - | gap: 0.5rem; | |
| 149 | - | white-space: nowrap; | |
| 150 | - | align-items: center; | |
| 151 | - | overflow: hidden; | |
| 152 | - | width: 100%; | |
| 153 | - | min-width: 0; /* allow flex children to shrink */ | |
| 154 | - | } | |
| 155 | - | .history-favicon-chip { | |
| 156 | - | flex: 0 0 20px; | |
| 157 | - | width: 20px; height: 20px; | |
| 158 | - | border-radius: 50%; | |
| 159 | - | background: #ccc; | |
| 160 | - | border: 1px solid rgba(0,0,0,.12); | |
| 161 | - | box-shadow: 0 1px 2px rgba(0,0,0,.12); | |
| 162 | - | display: flex; | |
| 163 | - | align-items: center; | |
| 164 | - | justify-content: center; | |
| 165 | - | } | |
| 166 | - | .history-favicon-img { | |
| 167 | - | width: 16px; height: 16px; | |
| 168 | - | filter: brightness(1.06) contrast(1.03); | |
| 169 | - | image-rendering: -webkit-optimize-contrast; | |
| 170 | - | } | |
| 171 | - | ||
| 172 | - | .history-title { font-size: 0.95rem; color: #e6e6e6; flex: 0 0 auto; } | |
| 173 | - | .history-domain { font-size: 0.9rem; color: #a5a5a5; flex: 0 0 auto; } | |
| 174 | - | .history-sep { color: #666; flex: 0 0 auto; } | |
| 175 | - | .history-url { | |
| 176 | - | font-size: 0.9rem; | |
| 177 | - | color: #7a7a7a; /* darker than domain */ | |
| 178 | - | flex: 1 1 auto; | |
| 179 | - | min-width: 0; /* critical for flex truncation */ | |
| 180 | - | overflow: hidden; | |
| 181 | - | text-overflow: ellipsis; /* fallback */ | |
| 182 | - | -webkit-mask-image: linear-gradient(to right, rgba(0,0,0,1) 85%, rgba(0,0,0,0) 100%); | |
| 183 | - | mask-image: linear-gradient(to right, rgba(0,0,0,1) 85%, rgba(0,0,0,0) 100%); | |
| 184 | - | } | |
| 185 | - | ||
| 186 | - | .shortcuts-grid { | |
| 187 | - | display: grid; | |
| 188 | - | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| 189 | - | gap: 1rem; | |
| 190 | - | max-height: calc(100vh - 8rem); | |
| 191 | - | overflow-y: auto; | |
| 192 | - | } | |
| 193 | - | ||
| 194 | - | .shortcut { | |
| 195 | - | padding: 1rem; | |
| 196 | - | background: #2d2d2d; | |
| 197 | - | border: 2px solid #404040; | |
| 198 | - | border-radius: 8px; | |
| 199 | - | cursor: pointer; | |
| 200 | - | transition: all 0.2s; | |
| 201 | - | text-align: center; | |
| 202 | - | } | |
| 203 | - | ||
| 204 | - | .shortcut:hover { | |
| 205 | - | background: #3d3d3d; | |
| 206 | - | border-color: #0066cc; | |
| 207 | - | transform: translateY(-2px); | |
| 208 | - | } | |
| 209 | - | ||
| 210 | - | .shortcut-key { | |
| 211 | - | font-weight: bold; | |
| 212 | - | color: #0066cc; | |
| 213 | - | margin-bottom: 0.5rem; | |
| 214 | - | font-size: 1.1rem; | |
| 215 | - | } | |
| 216 | - | ||
| 217 | - | .shortcut-desc { | |
| 218 | - | color: #888; | |
| 219 | - | font-size: 0.9rem; | |
| 220 | - | } | |
| 221 | - | ||
| 222 | - | .loading { | |
| 223 | - | padding: 2rem; | |
| 224 | - | text-align: center; | |
| 225 | - | color: #888; | |
| 226 | - | } | |
| 227 | - | ||
| 228 | - | .empty-state { | |
| 229 | - | padding: 3rem 2rem; | |
| 230 | - | text-align: center; | |
| 231 | - | color: #666; | |
| 232 | - | } | |
| 233 | - | ||
| 234 | - | .empty-state h3 { | |
| 235 | - | margin-bottom: 1rem; | |
| 236 | - | color: #888; | |
| 237 | - | } | |
| 238 | - | ||
| 239 | - | @media (max-width: 768px) { | |
| 240 | - | .container { | |
| 241 | - | flex-direction: column; | |
| 242 | - | padding: 1rem; | |
| 243 | - | } | |
| 244 | - | ||
| 245 | - | .shortcuts-grid { | |
| 246 | - | grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); | |
| 247 | - | } | |
| 248 | - | } | |
| 249 | - | `; | |
| 250 | - | } | |
| 251 | - | } | |
| @@ -1,251 +0,0 @@ | |||||
| 1 | - | export class HTMLBuilder { | |||
| 2 | - | static createBaseHTML(): string { | |||
| 3 | - | const faviconData = this.getFaviconDataURL(); | |||
| 4 | - | return ` | |||
| 5 | - | <!DOCTYPE html> | |||
| 6 | - | <html lang="en"> | |||
| 7 | - | <head> | |||
| 8 | - | <meta charset="UTF-8"> | |||
| 9 | - | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |||
| 10 | - | <title>Dumber Browser</title> | |||
| 11 | - | <link rel="icon" type="image/svg+xml" href="${faviconData}"> | |||
| 12 | - | <style> | |||
| 13 | - | ${this.getStyles()} | |||
| 14 | - | </style> | |||
| 15 | - | </head> | |||
| 16 | - | <body> | |||
| 17 | - | <div class="container"> | |||
| 18 | - | <div class="history-section"> | |||
| 19 | - | <h2 class="section-title">Recent History</h2> | |||
| 20 | - | <div id="historyList" class="history-list"> | |||
| 21 | - | <div class="loading">Loading history...</div> | |||
| 22 | - | </div> | |||
| 23 | - | </div> | |||
| 24 | - | ||||
| 25 | - | <div class="shortcuts-section"> | |||
| 26 | - | <h2 class="section-title">Search Shortcuts</h2> | |||
| 27 | - | <div id="shortcuts" class="shortcuts-grid"> | |||
| 28 | - | <div class="loading">Loading shortcuts...</div> | |||
| 29 | - | </div> | |||
| 30 | - | </div> | |||
| 31 | - | </div> | |||
| 32 | - | <script type="module" src="./main.js"></script> | |||
| 33 | - | </body> | |||
| 34 | - | </html>`; | |||
| 35 | - | } | |||
| 36 | - | ||||
| 37 | - | private static getFaviconDataURL(): string { | |||
| 38 | - | const svg = HTMLBuilder.getFaviconSVG(); | |||
| 39 | - | // Minimal encoding for safe inline usage | |||
| 40 | - | const encoded = encodeURIComponent(svg) | |||
| 41 | - | .replace(/'/g, '%27') | |||
| 42 | - | .replace(/\(/g, '%28') | |||
| 43 | - | .replace(/\)/g, '%29'); | |||
| 44 | - | return `data:image/svg+xml;utf8,${encoded}`; | |||
| 45 | - | } | |||
| 46 | - | ||||
| 47 | - | static getFaviconSVG(): string { | |||
| 48 | - | // Stylized D with speed bolt on a subtle dark rounded card | |||
| 49 | - | return `<?xml version="1.0" encoding="UTF-8"?> | |||
| 50 | - | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"> | |||
| 51 | - | <defs> | |||
| 52 | - | <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"> | |||
| 53 | - | <stop offset="0%" stop-color="#121212"/> | |||
| 54 | - | <stop offset="100%" stop-color="#1e1e1e"/> | |||
| 55 | - | </linearGradient> | |||
| 56 | - | <filter id="inset" x="-20%" y="-20%" width="140%" height="140%"> | |||
| 57 | - | <feOffset dx="0" dy="1"/> | |||
| 58 | - | <feGaussianBlur stdDeviation="0.6" result="blur"/> | |||
| 59 | - | <feComposite in2="SourceAlpha" operator="out"/> | |||
| 60 | - | <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/> | |||
| 61 | - | <feBlend in="SourceGraphic" mode="normal"/> | |||
| 62 | - | </filter> | |||
| 63 | - | </defs> | |||
| 64 | - | <rect x="4" y="4" width="56" height="56" rx="14" ry="14" fill="url(#bg)" filter="url(#inset)"/> | |||
| 65 | - | <rect x="4.5" y="4.5" width="55" height="55" rx="13.5" ry="13.5" fill="none" stroke="#252a2f" stroke-width="1"/> | |||
| 66 | - | <!-- D glyph (slightly larger and wider) --> | |||
| 67 | - | <path d="M20 16 L20 48 M20 16 H34 q12 0 12 16 t-12 16 H20" fill="none" stroke="#e9eef3" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round" transform="translate(33,32) scale(1.12,1.10) translate(-33,-32)"/> | |||
| 68 | - | <!-- Speed bolt accent (significantly larger, centered) --> | |||
| 69 | - | <path d="M36 22 L32 34 h6 l-8 16 4-12 h-6 z" fill="#ff9800" opacity="0.95" transform="translate(33,32) scale(1.8) translate(-33,-32) translate(0,-3)"/> | |||
| 70 | - | </svg>`; | |||
| 71 | - | } | |||
| 72 | - | ||||
| 73 | - | private static getStyles(): string { | |||
| 74 | - | return ` | |||
| 75 | - | * { | |||
| 76 | - | margin: 0; | |||
| 77 | - | padding: 0; | |||
| 78 | - | box-sizing: border-box; | |||
| 79 | - | } | |||
| 80 | - | ||||
| 81 | - | body { | |||
| 82 | - | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |||
| 83 | - | background: #1a1a1a; | |||
| 84 | - | color: #ffffff; | |||
| 85 | - | height: 100vh; | |||
| 86 | - | display: flex; | |||
| 87 | - | flex-direction: column; | |||
| 88 | - | overflow-x: hidden; /* prevent horizontal scroll from long content */ | |||
| 89 | - | } | |||
| 90 | - | ||||
| 91 | - | .container { | |||
| 92 | - | flex: 1; | |||
| 93 | - | display: flex; | |||
| 94 | - | max-width: 1200px; | |||
| 95 | - | margin: 0 auto; | |||
| 96 | - | width: 100%; | |||
| 97 | - | padding: 2rem; | |||
| 98 | - | gap: 2rem; | |||
| 99 | - | overflow-x: hidden; | |||
| 100 | - | } | |||
| 101 | - | ||||
| 102 | - | .history-section { | |||
| 103 | - | flex: 1; | |||
| 104 | - | min-height: 0; | |||
| 105 | - | min-width: 0; /* allow flex child to shrink */ | |||
| 106 | - | } | |||
| 107 | - | ||||
| 108 | - | .shortcuts-section { | |||
| 109 | - | flex: 1; | |||
| 110 | - | min-height: 0; | |||
| 111 | - | min-width: 0; /* allow flex child to shrink */ | |||
| 112 | - | } | |||
| 113 | - | ||||
| 114 | - | .section-title { | |||
| 115 | - | font-size: 1.5rem; | |||
| 116 | - | margin-bottom: 1rem; | |||
| 117 | - | color: #ffffff; | |||
| 118 | - | border-bottom: 2px solid #404040; | |||
| 119 | - | padding-bottom: 0.5rem; | |||
| 120 | - | } | |||
| 121 | - | ||||
| 122 | - | .history-list { | |||
| 123 | - | overflow-y: auto; | |||
| 124 | - | overflow-x: hidden; | |||
| 125 | - | max-height: calc(100vh - 8rem); | |||
| 126 | - | max-width: 100%; | |||
| 127 | - | } | |||
| 128 | - | ||||
| 129 | - | .history-item { | |||
| 130 | - | padding: 0.75rem; | |||
| 131 | - | margin-bottom: 0.5rem; | |||
| 132 | - | background: #2d2d2d; | |||
| 133 | - | border-radius: 6px; | |||
| 134 | - | cursor: pointer; | |||
| 135 | - | transition: background 0.2s; | |||
| 136 | - | border-left: 3px solid #404040; | |||
| 137 | - | overflow: hidden; | |||
| 138 | - | max-width: 100%; | |||
| 139 | - | } | |||
| 140 | - | ||||
| 141 | - | .history-item:hover { | |||
| 142 | - | background: #3d3d3d; | |||
| 143 | - | border-left-color: #0066cc; | |||
| 144 | - | } | |||
| 145 | - | ||||
| 146 | - | .history-line { | |||
| 147 | - | display: flex; | |||
| 148 | - | gap: 0.5rem; | |||
| 149 | - | white-space: nowrap; | |||
| 150 | - | align-items: center; | |||
| 151 | - | overflow: hidden; | |||
| 152 | - | width: 100%; | |||
| 153 | - | min-width: 0; /* allow flex children to shrink */ | |||
| 154 | - | } | |||
| 155 | - | .history-favicon-chip { | |||
| 156 | - | flex: 0 0 20px; | |||
| 157 | - | width: 20px; height: 20px; | |||
| 158 | - | border-radius: 50%; | |||
| 159 | - | background: #ccc; | |||
| 160 | - | border: 1px solid rgba(0,0,0,.12); | |||
| 161 | - | box-shadow: 0 1px 2px rgba(0,0,0,.12); | |||
| 162 | - | display: flex; | |||
| 163 | - | align-items: center; | |||
| 164 | - | justify-content: center; | |||
| 165 | - | } | |||
| 166 | - | .history-favicon-img { | |||
| 167 | - | width: 16px; height: 16px; | |||
| 168 | - | filter: brightness(1.06) contrast(1.03); | |||
| 169 | - | image-rendering: -webkit-optimize-contrast; | |||
| 170 | - | } | |||
| 171 | - | ||||
| 172 | - | .history-title { font-size: 0.95rem; color: #e6e6e6; flex: 0 0 auto; } | |||
| 173 | - | .history-domain { font-size: 0.9rem; color: #a5a5a5; flex: 0 0 auto; } | |||
| 174 | - | .history-sep { color: #666; flex: 0 0 auto; } | |||
| 175 | - | .history-url { | |||
| 176 | - | font-size: 0.9rem; | |||
| 177 | - | color: #7a7a7a; /* darker than domain */ | |||
| 178 | - | flex: 1 1 auto; | |||
| 179 | - | min-width: 0; /* critical for flex truncation */ | |||
| 180 | - | overflow: hidden; | |||
| 181 | - | text-overflow: ellipsis; /* fallback */ | |||
| 182 | - | -webkit-mask-image: linear-gradient(to right, rgba(0,0,0,1) 85%, rgba(0,0,0,0) 100%); | |||
| 183 | - | mask-image: linear-gradient(to right, rgba(0,0,0,1) 85%, rgba(0,0,0,0) 100%); | |||
| 184 | - | } | |||
| 185 | - | ||||
| 186 | - | .shortcuts-grid { | |||
| 187 | - | display: grid; | |||
| 188 | - | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |||
| 189 | - | gap: 1rem; | |||
| 190 | - | max-height: calc(100vh - 8rem); | |||
| 191 | - | overflow-y: auto; | |||
| 192 | - | } | |||
| 193 | - | ||||
| 194 | - | .shortcut { | |||
| 195 | - | padding: 1rem; | |||
| 196 | - | background: #2d2d2d; | |||
| 197 | - | border: 2px solid #404040; | |||
| 198 | - | border-radius: 8px; | |||
| 199 | - | cursor: pointer; | |||
| 200 | - | transition: all 0.2s; | |||
| 201 | - | text-align: center; | |||
| 202 | - | } | |||
| 203 | - | ||||
| 204 | - | .shortcut:hover { | |||
| 205 | - | background: #3d3d3d; | |||
| 206 | - | border-color: #0066cc; | |||
| 207 | - | transform: translateY(-2px); | |||
| 208 | - | } | |||
| 209 | - | ||||
| 210 | - | .shortcut-key { | |||
| 211 | - | font-weight: bold; | |||
| 212 | - | color: #0066cc; | |||
| 213 | - | margin-bottom: 0.5rem; | |||
| 214 | - | font-size: 1.1rem; | |||
| 215 | - | } | |||
| 216 | - | ||||
| 217 | - | .shortcut-desc { | |||
| 218 | - | color: #888; | |||
| 219 | - | font-size: 0.9rem; | |||
| 220 | - | } | |||
| 221 | - | ||||
| 222 | - | .loading { | |||
| 223 | - | padding: 2rem; | |||
| 224 | - | text-align: center; | |||
| 225 | - | color: #888; | |||
| 226 | - | } | |||
| 227 | - | ||||
| 228 | - | .empty-state { | |||
| 229 | - | padding: 3rem 2rem; | |||
| 230 | - | text-align: center; | |||
| 231 | - | color: #666; | |||
| 232 | - | } | |||
| 233 | - | ||||
| 234 | - | .empty-state h3 { | |||
| 235 | - | margin-bottom: 1rem; | |||
| 236 | - | color: #888; | |||
| 237 | - | } | |||
| 238 | - | ||||
| 239 | - | @media (max-width: 768px) { | |||
| 240 | - | .container { | |||
| 241 | - | flex-direction: column; | |||
| 242 | - | padding: 1rem; | |||
| 243 | - | } | |||
| 244 | - | ||||
| 245 | - | .shortcuts-grid { | |||
| 246 | - | grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); | |||
| 247 | - | } | |||
| 248 | - | } | |||
| 249 | - | `; | |||
| 250 | - | } | |||
| 251 | - | } | |||
D frontend/tsconfig.json +0 -22
| @@ -1,22 +0,0 @@ | |||
| 1 | - | { | |
| 2 | - | "compilerOptions": { | |
| 3 | - | "target": "ES2022", | |
| 4 | - | "module": "ESNext", | |
| 5 | - | "moduleResolution": "bundler", | |
| 6 | - | "lib": ["ES2022", "DOM", "DOM.Iterable"], | |
| 7 | - | "allowImportingTsExtensions": true, | |
| 8 | - | "moduleDetection": "force", | |
| 9 | - | "noEmit": true, | |
| 10 | - | "strict": true, | |
| 11 | - | "noUnusedLocals": true, | |
| 12 | - | "noUnusedParameters": true, | |
| 13 | - | "noFallthroughCasesInSwitch": true, | |
| 14 | - | "noUncheckedIndexedAccess": true, | |
| 15 | - | "skipLibCheck": true, | |
| 16 | - | "esModuleInterop": true, | |
| 17 | - | "allowSyntheticDefaultImports": true, | |
| 18 | - | "forceConsistentCasingInFileNames": true | |
| 19 | - | }, | |
| 20 | - | "include": ["src/**/*"], | |
| 21 | - | "exclude": ["node_modules", "dist"] | |
| 22 | - | }⏎̸ | |
| @@ -1,22 +0,0 @@ | |||||
| 1 | - | { | |||
| 2 | - | "compilerOptions": { | |||
| 3 | - | "target": "ES2022", | |||
| 4 | - | "module": "ESNext", | |||
| 5 | - | "moduleResolution": "bundler", | |||
| 6 | - | "lib": ["ES2022", "DOM", "DOM.Iterable"], | |||
| 7 | - | "allowImportingTsExtensions": true, | |||
| 8 | - | "moduleDetection": "force", | |||
| 9 | - | "noEmit": true, | |||
| 10 | - | "strict": true, | |||
| 11 | - | "noUnusedLocals": true, | |||
| 12 | - | "noUnusedParameters": true, | |||
| 13 | - | "noFallthroughCasesInSwitch": true, | |||
| 14 | - | "noUncheckedIndexedAccess": true, | |||
| 15 | - | "skipLibCheck": true, | |||
| 16 | - | "esModuleInterop": true, | |||
| 17 | - | "allowSyntheticDefaultImports": true, | |||
| 18 | - | "forceConsistentCasingInFileNames": true | |||
| 19 | - | }, | |||
| 20 | - | "include": ["src/**/*"], | |||
| 21 | - | "exclude": ["node_modules", "dist"] | |||
| 22 | - | }⏎̸ | |||
D frontend/tsup.config.ts +0 -54
| @@ -1,54 +0,0 @@ | |||
| 1 | - | import { defineConfig } from 'tsup' | |
| 2 | - | ||
| 3 | - | export default defineConfig([ | |
| 4 | - | // Browser bundle | |
| 5 | - | { | |
| 6 | - | entry: ['src/main.ts'], | |
| 7 | - | format: ['esm'], | |
| 8 | - | outDir: 'dist', | |
| 9 | - | outExtension: () => ({ js: '.js' }), | |
| 10 | - | target: 'es2024', | |
| 11 | - | platform: 'browser', | |
| 12 | - | minify: false, | |
| 13 | - | sourcemap: true, | |
| 14 | - | clean: true, | |
| 15 | - | bundle: true, | |
| 16 | - | splitting: false, | |
| 17 | - | external: [], | |
| 18 | - | // noExternal: [], | |
| 19 | - | }, | |
| 20 | - | // Injectable controls script (minified for injection) | |
| 21 | - | { | |
| 22 | - | entry: ['src/injected-controls.ts'], | |
| 23 | - | format: ['iife'], | |
| 24 | - | outDir: 'dist', | |
| 25 | - | outExtension: () => ({ js: '.min.js' }), | |
| 26 | - | target: 'es2024', | |
| 27 | - | platform: 'browser', | |
| 28 | - | minify: true, | |
| 29 | - | sourcemap: false, | |
| 30 | - | bundle: true, | |
| 31 | - | splitting: false, | |
| 32 | - | globalName: '__dumberControls', | |
| 33 | - | external: [], | |
| 34 | - | onSuccess: async () => { | |
| 35 | - | console.log('✓ Injectable controls script compiled successfully') | |
| 36 | - | } | |
| 37 | - | }, | |
| 38 | - | // Node.js build script | |
| 39 | - | { | |
| 40 | - | entry: ['src/build-html.ts', 'src/utils/html-builder.ts'], | |
| 41 | - | format: ['esm'], | |
| 42 | - | outDir: 'dist', | |
| 43 | - | outExtension: () => ({ js: '.js' }), | |
| 44 | - | target: 'es2024', | |
| 45 | - | platform: 'node', | |
| 46 | - | minify: false, | |
| 47 | - | sourcemap: false, | |
| 48 | - | bundle: true, | |
| 49 | - | splitting: false, | |
| 50 | - | onSuccess: async () => { | |
| 51 | - | console.log('✓ Frontend TypeScript compiled successfully') | |
| 52 | - | } | |
| 53 | - | } | |
| 54 | - | ]) | |
| @@ -1,54 +0,0 @@ | |||||
| 1 | - | import { defineConfig } from 'tsup' | |||
| 2 | - | ||||
| 3 | - | export default defineConfig([ | |||
| 4 | - | // Browser bundle | |||
| 5 | - | { | |||
| 6 | - | entry: ['src/main.ts'], | |||
| 7 | - | format: ['esm'], | |||
| 8 | - | outDir: 'dist', | |||
| 9 | - | outExtension: () => ({ js: '.js' }), | |||
| 10 | - | target: 'es2024', | |||
| 11 | - | platform: 'browser', | |||
| 12 | - | minify: false, | |||
| 13 | - | sourcemap: true, | |||
| 14 | - | clean: true, | |||
| 15 | - | bundle: true, | |||
| 16 | - | splitting: false, | |||
| 17 | - | external: [], | |||
| 18 | - | // noExternal: [], | |||
| 19 | - | }, | |||
| 20 | - | // Injectable controls script (minified for injection) | |||
| 21 | - | { | |||
| 22 | - | entry: ['src/injected-controls.ts'], | |||
| 23 | - | format: ['iife'], | |||
| 24 | - | outDir: 'dist', | |||
| 25 | - | outExtension: () => ({ js: '.min.js' }), | |||
| 26 | - | target: 'es2024', | |||
| 27 | - | platform: 'browser', | |||
| 28 | - | minify: true, | |||
| 29 | - | sourcemap: false, | |||
| 30 | - | bundle: true, | |||
| 31 | - | splitting: false, | |||
| 32 | - | globalName: '__dumberControls', | |||
| 33 | - | external: [], | |||
| 34 | - | onSuccess: async () => { | |||
| 35 | - | console.log('✓ Injectable controls script compiled successfully') | |||
| 36 | - | } | |||
| 37 | - | }, | |||
| 38 | - | // Node.js build script | |||
| 39 | - | { | |||
| 40 | - | entry: ['src/build-html.ts', 'src/utils/html-builder.ts'], | |||
| 41 | - | format: ['esm'], | |||
| 42 | - | outDir: 'dist', | |||
| 43 | - | outExtension: () => ({ js: '.js' }), | |||
| 44 | - | target: 'es2024', | |||
| 45 | - | platform: 'node', | |||
| 46 | - | minify: false, | |||
| 47 | - | sourcemap: false, | |||
| 48 | - | bundle: true, | |||
| 49 | - | splitting: false, | |||
| 50 | - | onSuccess: async () => { | |||
| 51 | - | console.log('✓ Frontend TypeScript compiled successfully') | |||
| 52 | - | } | |||
| 53 | - | } | |||
| 54 | - | ]) | |||