detached 802f0d0

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-])

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close