detached f328a4f

feat(webui): add TypeScript types and ESLint Svelte plugin

commit
f328a4f28a8984d44d992817dc6b46b9c21fcb1c
author
brice · · 2026-01-24 14:19 UTC
parent
11eef0c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +518 -35
M webui/eslint.config.js +21 -0
@@ -1,9 +1,30 @@
11 import js from "@eslint/js";
2+import svelte from "eslint-plugin-svelte";
3+import globals from "globals";
24 import tseslint from "typescript-eslint";
5+import svelteConfig from "./svelte.config.js";
36
47 export default tseslint.config(
58 js.configs.recommended,
69 ...tseslint.configs.recommended,
10+ ...svelte.configs.recommended,
11+ {
12+ languageOptions: {
13+ globals: {
14+ ...globals.browser,
15+ },
16+ },
17+ },
18+ {
19+ files: ["**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"],
20+ languageOptions: {
21+ parserOptions: {
22+ extraFileExtensions: [".svelte"],
23+ parser: tseslint.parser,
24+ svelteConfig,
25+ },
26+ },
27+ },
728 {
829 files: ["**/*.{js,mjs,cjs,ts}"],
930 languageOptions: {
@@ -1,9 +1,30 @@
1 import js from "@eslint/js";1 import js from "@eslint/js";
2+import svelte from "eslint-plugin-svelte";
3+import globals from "globals";
2 import tseslint from "typescript-eslint";4 import tseslint from "typescript-eslint";
5+import svelteConfig from "./svelte.config.js";
3 6
4 export default tseslint.config(7 export default tseslint.config(
5 js.configs.recommended,8 js.configs.recommended,
6 ...tseslint.configs.recommended,9 ...tseslint.configs.recommended,
10+ ...svelte.configs.recommended,
11+ {
12+ languageOptions: {
13+ globals: {
14+ ...globals.browser,
15+ },
16+ },
17+ },
18+ {
19+ files: ["**/*.svelte", "**/*.svelte.ts", "**/*.svelte.js"],
20+ languageOptions: {
21+ parserOptions: {
22+ extraFileExtensions: [".svelte"],
23+ parser: tseslint.parser,
24+ svelteConfig,
25+ },
26+ },
27+ },
7 {28 {
8 files: ["**/*.{js,mjs,cjs,ts}"],29 files: ["**/*.{js,mjs,cjs,ts}"],
9 languageOptions: {30 languageOptions: {
M webui/package-lock.json +308 -7 generated file: collapsed
M webui/package.json +2 -0
@@ -27,6 +27,8 @@
2727 "@typescript-eslint/parser": "^7.0.0",
2828 "autoprefixer": "^10.4.20",
2929 "eslint": "^8.57.0",
30+ "eslint-plugin-svelte": "^3.14.0",
31+ "globals": "^17.1.0",
3032 "postcss": "^8.4.47",
3133 "svelte": "^5.0.0",
3234 "svelte-awesome-color-picker": "^4.1.0",
@@ -27,6 +27,8 @@
27 "@typescript-eslint/parser": "^7.0.0",27 "@typescript-eslint/parser": "^7.0.0",
28 "autoprefixer": "^10.4.20",28 "autoprefixer": "^10.4.20",
29 "eslint": "^8.57.0",29 "eslint": "^8.57.0",
30+ "eslint-plugin-svelte": "^3.14.0",
31+ "globals": "^17.1.0",
30 "postcss": "^8.4.47",32 "postcss": "^8.4.47",
31 "svelte": "^5.0.0",33 "svelte": "^5.0.0",
32 "svelte-awesome-color-picker": "^4.1.0",34 "svelte-awesome-color-picker": "^4.1.0",
M webui/src/pages/homepage/messaging.ts +34 -27
@@ -23,8 +23,12 @@ import { homepageState } from './state.svelte';
2323 // REQUEST TRACKING
2424 // ═══════════════════════════════════════════════════════════════════════════════
2525
26-interface PendingRequest<T = unknown> {
27- resolve: (value: T) => void;
26+// Pending requests store resolvers for different response types.
27+// The resolve function accepts `unknown` since requests with different
28+// response types share the same map. Type safety is maintained at the
29+// sendMessage<T> API level.
30+interface PendingRequest {
31+ resolve: (value: unknown) => void;
2832 reject: (error: Error) => void;
2933 timeout: ReturnType<typeof setTimeout>;
3034 }
@@ -38,8 +42,8 @@ let callbacksInitialized = false;
3842 // WEBKIT BRIDGE UTILITIES
3943 // ═══════════════════════════════════════════════════════════════════════════════
4044
41-function getWebKitBridge(): { postMessage: (msg: unknown) => void } | null {
42- const bridge = (window as any).webkit?.messageHandlers?.dumber;
45+function getWebKitBridge(): DumberBridge | null {
46+ const bridge = window.webkit?.messageHandlers?.dumber;
4347 if (bridge && typeof bridge.postMessage === 'function') {
4448 return bridge;
4549 }
@@ -47,7 +51,7 @@ function getWebKitBridge(): { postMessage: (msg: unknown) => void } | null {
4751 }
4852
4953 function getWebViewId(): number {
50- return (window as any).__dumber_webview_id || 0;
54+ return window.__dumber_webview_id ?? 0;
5155 }
5256
5357 function generateRequestId(type: string): string {
@@ -62,9 +66,7 @@ function initializeCallbacks(): void {
6266 if (callbacksInitialized) return;
6367
6468 // Generic response handler - accepts a single Response object from Go backend
65- (window as any).__dumber_homepage_response = (
66- response: { requestId: string; success: boolean; data?: unknown; error?: string }
67- ) => {
69+ window.__dumber_homepage_response = (response) => {
6870 const request = pendingRequests.get(response.requestId);
6971 if (!request) return;
7072
@@ -74,61 +76,60 @@ function initializeCallbacks(): void {
7476 if (response.success) {
7577 request.resolve(response.data);
7678 } else {
77- request.reject(new Error(response.error || 'Unknown error'));
79+ request.reject(new Error(response.error ?? 'Unknown error'));
7880 }
7981 };
8082
8183 // Legacy callbacks for backward compatibility
8284 // These can receive either raw data or a Response object
83- (window as any).__dumber_history_timeline = (dataOrResponse: unknown) => {
85+ window.__dumber_history_timeline = (dataOrResponse) => {
8486 handleLegacyOrResponseCallback(dataOrResponse);
8587 };
8688
87- (window as any).__dumber_folders = (dataOrResponse: unknown) => {
89+ window.__dumber_folders = (dataOrResponse) => {
8890 handleLegacyOrResponseCallback(dataOrResponse);
8991 };
9092
91- (window as any).__dumber_tags = (dataOrResponse: unknown) => {
93+ window.__dumber_tags = (dataOrResponse) => {
9294 handleLegacyOrResponseCallback(dataOrResponse);
9395 };
9496
95- (window as any).__dumber_favorites = (dataOrResponse: unknown) => {
97+ window.__dumber_favorites = (dataOrResponse) => {
9698 handleLegacyOrResponseCallback(dataOrResponse);
9799 };
98100
99- (window as any).__dumber_analytics = (dataOrResponse: unknown) => {
101+ window.__dumber_analytics = (dataOrResponse) => {
100102 handleLegacyOrResponseCallback(dataOrResponse);
101103 };
102104
103- (window as any).__dumber_domain_stats = (dataOrResponse: unknown) => {
105+ window.__dumber_domain_stats = (dataOrResponse) => {
104106 handleLegacyOrResponseCallback(dataOrResponse);
105107 };
106108
107- (window as any).__dumber_history_search_results = (dataOrResponse: unknown) => {
109+ window.__dumber_history_search_results = (dataOrResponse) => {
108110 handleLegacyOrResponseCallback(dataOrResponse);
109111 };
110112
111- (window as any).__dumber_history_deleted = (dataOrResponse: unknown) => {
113+ window.__dumber_history_deleted = (dataOrResponse) => {
112114 handleLegacyOrResponseCallback(dataOrResponse);
113115 };
114116
115- (window as any).__dumber_history_cleared = (dataOrResponse: unknown) => {
117+ window.__dumber_history_cleared = (dataOrResponse) => {
116118 handleLegacyOrResponseCallback(dataOrResponse);
117119 };
118120
119- (window as any).__dumber_error = (errorOrResponse: unknown) => {
121+ window.__dumber_error = (errorOrResponse) => {
120122 // Handle both formats: direct error string or Response object with error
121- const response = errorOrResponse as { requestId?: string; error?: string } | string;
122- if (typeof response === 'string') {
123+ if (typeof errorOrResponse === 'string') {
123124 // Legacy format: direct error string
124- console.error('[messaging] Legacy error:', response);
125- } else if (response && typeof response === 'object') {
125+ console.error('[messaging] Legacy error:', errorOrResponse);
126+ } else if (errorOrResponse && typeof errorOrResponse === 'object') {
126127 // Response object format
127- const request = pendingRequests.get(response.requestId || 'default');
128+ const request = pendingRequests.get(errorOrResponse.requestId ?? 'default');
128129 if (request) {
129130 clearTimeout(request.timeout);
130- pendingRequests.delete(response.requestId || 'default');
131- request.reject(new Error(response.error || 'Unknown error'));
131+ pendingRequests.delete(errorOrResponse.requestId ?? 'default');
132+ request.reject(new Error(errorOrResponse.error ?? 'Unknown error'));
132133 }
133134 }
134135 };
@@ -188,7 +189,13 @@ async function sendMessage<T = unknown>(
188189 reject(new Error(`Request timeout: ${type}`));
189190 }, REQUEST_TIMEOUT_MS);
190191
191- pendingRequests.set(requestId, { resolve: resolve as any, reject, timeout });
192+ // Cast resolve to accept unknown - type safety is maintained at API level
193+ // since sendMessage<T> returns Promise<T> and callers expect typed responses
194+ pendingRequests.set(requestId, {
195+ resolve: resolve as (value: unknown) => void,
196+ reject,
197+ timeout,
198+ });
192199
193200 try {
194201 bridge.postMessage({
@@ -23,8 +23,12 @@ import { homepageState } from './state.svelte';
23 // REQUEST TRACKING23 // REQUEST TRACKING
24 // ═══════════════════════════════════════════════════════════════════════════════24 // ═══════════════════════════════════════════════════════════════════════════════
25 25
26-interface PendingRequest<T = unknown> {26+// Pending requests store resolvers for different response types.
27- resolve: (value: T) => void;27+// The resolve function accepts `unknown` since requests with different
28+// response types share the same map. Type safety is maintained at the
29+// sendMessage<T> API level.
30+interface PendingRequest {
31+ resolve: (value: unknown) => void;
28 reject: (error: Error) => void;32 reject: (error: Error) => void;
29 timeout: ReturnType<typeof setTimeout>;33 timeout: ReturnType<typeof setTimeout>;
30 }34 }
@@ -38,8 +42,8 @@ let callbacksInitialized = false;
38 // WEBKIT BRIDGE UTILITIES42 // WEBKIT BRIDGE UTILITIES
39 // ═══════════════════════════════════════════════════════════════════════════════43 // ═══════════════════════════════════════════════════════════════════════════════
40 44
41-function getWebKitBridge(): { postMessage: (msg: unknown) => void } | null {45+function getWebKitBridge(): DumberBridge | null {
42- const bridge = (window as any).webkit?.messageHandlers?.dumber;46+ const bridge = window.webkit?.messageHandlers?.dumber;
43 if (bridge && typeof bridge.postMessage === 'function') {47 if (bridge && typeof bridge.postMessage === 'function') {
44 return bridge;48 return bridge;
45 }49 }
@@ -47,7 +51,7 @@ function getWebKitBridge(): { postMessage: (msg: unknown) => void } | null {
47 }51 }
48 52
49 function getWebViewId(): number {53 function getWebViewId(): number {
50- return (window as any).__dumber_webview_id || 0;54+ return window.__dumber_webview_id ?? 0;
51 }55 }
52 56
53 function generateRequestId(type: string): string {57 function generateRequestId(type: string): string {
@@ -62,9 +66,7 @@ function initializeCallbacks(): void {
62 if (callbacksInitialized) return;66 if (callbacksInitialized) return;
63 67
64 // Generic response handler - accepts a single Response object from Go backend68 // Generic response handler - accepts a single Response object from Go backend
65- (window as any).__dumber_homepage_response = (69+ window.__dumber_homepage_response = (response) => {
66- response: { requestId: string; success: boolean; data?: unknown; error?: string }
67- ) => {
68 const request = pendingRequests.get(response.requestId);70 const request = pendingRequests.get(response.requestId);
69 if (!request) return;71 if (!request) return;
70 72
@@ -74,61 +76,60 @@ function initializeCallbacks(): void {
74 if (response.success) {76 if (response.success) {
75 request.resolve(response.data);77 request.resolve(response.data);
76 } else {78 } else {
77- request.reject(new Error(response.error || 'Unknown error'));79+ request.reject(new Error(response.error ?? 'Unknown error'));
78 }80 }
79 };81 };
80 82
81 // Legacy callbacks for backward compatibility83 // Legacy callbacks for backward compatibility
82 // These can receive either raw data or a Response object84 // These can receive either raw data or a Response object
83- (window as any).__dumber_history_timeline = (dataOrResponse: unknown) => {85+ window.__dumber_history_timeline = (dataOrResponse) => {
84 handleLegacyOrResponseCallback(dataOrResponse);86 handleLegacyOrResponseCallback(dataOrResponse);
85 };87 };
86 88
87- (window as any).__dumber_folders = (dataOrResponse: unknown) => {89+ window.__dumber_folders = (dataOrResponse) => {
88 handleLegacyOrResponseCallback(dataOrResponse);90 handleLegacyOrResponseCallback(dataOrResponse);
89 };91 };
90 92
91- (window as any).__dumber_tags = (dataOrResponse: unknown) => {93+ window.__dumber_tags = (dataOrResponse) => {
92 handleLegacyOrResponseCallback(dataOrResponse);94 handleLegacyOrResponseCallback(dataOrResponse);
93 };95 };
94 96
95- (window as any).__dumber_favorites = (dataOrResponse: unknown) => {97+ window.__dumber_favorites = (dataOrResponse) => {
96 handleLegacyOrResponseCallback(dataOrResponse);98 handleLegacyOrResponseCallback(dataOrResponse);
97 };99 };
98 100
99- (window as any).__dumber_analytics = (dataOrResponse: unknown) => {101+ window.__dumber_analytics = (dataOrResponse) => {
100 handleLegacyOrResponseCallback(dataOrResponse);102 handleLegacyOrResponseCallback(dataOrResponse);
101 };103 };
102 104
103- (window as any).__dumber_domain_stats = (dataOrResponse: unknown) => {105+ window.__dumber_domain_stats = (dataOrResponse) => {
104 handleLegacyOrResponseCallback(dataOrResponse);106 handleLegacyOrResponseCallback(dataOrResponse);
105 };107 };
106 108
107- (window as any).__dumber_history_search_results = (dataOrResponse: unknown) => {109+ window.__dumber_history_search_results = (dataOrResponse) => {
108 handleLegacyOrResponseCallback(dataOrResponse);110 handleLegacyOrResponseCallback(dataOrResponse);
109 };111 };
110 112
111- (window as any).__dumber_history_deleted = (dataOrResponse: unknown) => {113+ window.__dumber_history_deleted = (dataOrResponse) => {
112 handleLegacyOrResponseCallback(dataOrResponse);114 handleLegacyOrResponseCallback(dataOrResponse);
113 };115 };
114 116
115- (window as any).__dumber_history_cleared = (dataOrResponse: unknown) => {117+ window.__dumber_history_cleared = (dataOrResponse) => {
116 handleLegacyOrResponseCallback(dataOrResponse);118 handleLegacyOrResponseCallback(dataOrResponse);
117 };119 };
118 120
119- (window as any).__dumber_error = (errorOrResponse: unknown) => {121+ window.__dumber_error = (errorOrResponse) => {
120 // Handle both formats: direct error string or Response object with error122 // Handle both formats: direct error string or Response object with error
121- const response = errorOrResponse as { requestId?: string; error?: string } | string;123+ if (typeof errorOrResponse === 'string') {
122- if (typeof response === 'string') {
123 // Legacy format: direct error string124 // Legacy format: direct error string
124- console.error('[messaging] Legacy error:', response);125+ console.error('[messaging] Legacy error:', errorOrResponse);
125- } else if (response && typeof response === 'object') {126+ } else if (errorOrResponse && typeof errorOrResponse === 'object') {
126 // Response object format127 // Response object format
127- const request = pendingRequests.get(response.requestId || 'default');128+ const request = pendingRequests.get(errorOrResponse.requestId ?? 'default');
128 if (request) {129 if (request) {
129 clearTimeout(request.timeout);130 clearTimeout(request.timeout);
130- pendingRequests.delete(response.requestId || 'default');131+ pendingRequests.delete(errorOrResponse.requestId ?? 'default');
131- request.reject(new Error(response.error || 'Unknown error'));132+ request.reject(new Error(errorOrResponse.error ?? 'Unknown error'));
132 }133 }
133 }134 }
134 };135 };
@@ -188,7 +189,13 @@ async function sendMessage<T = unknown>(
188 reject(new Error(`Request timeout: ${type}`));189 reject(new Error(`Request timeout: ${type}`));
189 }, REQUEST_TIMEOUT_MS);190 }, REQUEST_TIMEOUT_MS);
190 191
191- pendingRequests.set(requestId, { resolve: resolve as any, reject, timeout });192+ // Cast resolve to accept unknown - type safety is maintained at API level
193+ // since sendMessage<T> returns Promise<T> and callers expect typed responses
194+ pendingRequests.set(requestId, {
195+ resolve: resolve as (value: unknown) => void,
196+ reject,
197+ timeout,
198+ });
192 199
193 try {200 try {
194 bridge.postMessage({201 bridge.postMessage({
M webui/src/pages/homepage/types.ts +3 -1
@@ -3,6 +3,8 @@
33 // Mirrors Go models from internal/db/models.go and related query files
44 // ═══════════════════════════════════════════════════════════════════════════════
55
6+import type { Component } from 'svelte';
7+
68 // ─────────────────────────────────────────────────────────────────────────────
79 // History Types
810 // ─────────────────────────────────────────────────────────────────────────────
@@ -110,7 +112,7 @@ export interface CommandPaletteItem {
110112 id: string;
111113 label: string;
112114 description?: string;
113- icon?: import('svelte').Component | string;
115+ icon?: Component | string;
114116 action: () => void;
115117 shortcut?: string;
116118 }
@@ -3,6 +3,8 @@
3 // Mirrors Go models from internal/db/models.go and related query files3 // Mirrors Go models from internal/db/models.go and related query files
4 // ═══════════════════════════════════════════════════════════════════════════════4 // ═══════════════════════════════════════════════════════════════════════════════
5 5
6+import type { Component } from 'svelte';
7+
6 // ─────────────────────────────────────────────────────────────────────────────8 // ─────────────────────────────────────────────────────────────────────────────
7 // History Types9 // History Types
8 // ─────────────────────────────────────────────────────────────────────────────10 // ─────────────────────────────────────────────────────────────────────────────
@@ -110,7 +112,7 @@ export interface CommandPaletteItem {
110 id: string;112 id: string;
111 label: string;113 label: string;
112 description?: string;114 description?: string;
113- icon?: import('svelte').Component | string;115+ icon?: Component | string;
114 action: () => void;116 action: () => void;
115 shortcut?: string;117 shortcut?: string;
116 }118 }
A webui/src/types/dumber.d.ts +150 -0
@@ -0,0 +1,150 @@
1+/**
2+ * Global type declarations for Dumber browser WebKit bridge
3+ */
4+
5+// ═══════════════════════════════════════════════════════════════════════════════
6+// WEBKIT BRIDGE TYPES
7+// ═══════════════════════════════════════════════════════════════════════════════
8+
9+interface DumberBridge {
10+ postMessage: (message: unknown) => void;
11+}
12+
13+interface DumberWebKit {
14+ messageHandlers?: {
15+ dumber?: DumberBridge;
16+ };
17+}
18+
19+// ═══════════════════════════════════════════════════════════════════════════════
20+// VIEW TRANSITION API (not yet in TypeScript DOM lib)
21+// ═══════════════════════════════════════════════════════════════════════════════
22+
23+interface ViewTransition {
24+ finished: Promise<void>;
25+ ready: Promise<void>;
26+ updateCallbackDone: Promise<void>;
27+}
28+
29+// ═══════════════════════════════════════════════════════════════════════════════
30+// THEME TYPES
31+// ═══════════════════════════════════════════════════════════════════════════════
32+
33+type ThemeMode = 'light' | 'dark';
34+
35+interface ColorSchemeManager {
36+ setUserPreference?: (theme: ThemeMode) => void;
37+}
38+
39+// ═══════════════════════════════════════════════════════════════════════════════
40+// HOMEPAGE MESSAGING TYPES
41+// ═══════════════════════════════════════════════════════════════════════════════
42+
43+/** Response from Go backend for homepage requests */
44+interface HomepageResponse {
45+ requestId: string;
46+ success: boolean;
47+ data?: unknown;
48+ error?: string;
49+}
50+
51+/** Error response format (can be string for legacy or object) */
52+type HomepageErrorResponse = string | { requestId?: string; error?: string };
53+
54+/** Callback type for homepage responses */
55+type HomepageResponseCallback = (response: HomepageResponse) => void;
56+
57+/** Callback type for legacy data responses (can receive raw data or Response object) */
58+type HomepageLegacyCallback = (dataOrResponse: unknown) => void;
59+
60+/** Callback type for error responses */
61+type HomepageErrorCallback = (errorOrResponse: HomepageErrorResponse) => void;
62+
63+// ═══════════════════════════════════════════════════════════════════════════════
64+// GLOBAL DECLARATIONS
65+// ═══════════════════════════════════════════════════════════════════════════════
66+
67+declare global {
68+ // Re-export bridge type for use in other modules
69+ type DumberBridge = {
70+ postMessage: (message: unknown) => void;
71+ };
72+ interface Window {
73+ // ─────────────────────────────────────────────────────────────────────────
74+ // Core WebKit Bridge
75+ // ─────────────────────────────────────────────────────────────────────────
76+ /** WebView ID assigned by the browser backend */
77+ __dumber_webview_id?: number;
78+ /** WebKit message handlers for native bridge */
79+ webkit?: DumberWebKit;
80+
81+ // ─────────────────────────────────────────────────────────────────────────
82+ // Config Page Callbacks
83+ // ─────────────────────────────────────────────────────────────────────────
84+ /** Callback invoked when config save succeeds */
85+ __dumber_config_saved?: (resp?: unknown) => void;
86+ /** Callback invoked when config save fails */
87+ __dumber_config_error?: (msg: unknown) => void;
88+
89+ // ─────────────────────────────────────────────────────────────────────────
90+ // Keybindings Callbacks
91+ // ─────────────────────────────────────────────────────────────────────────
92+ /** Callback invoked when keybindings are loaded */
93+ __dumber_keybindings_loaded?: (resp?: unknown) => void;
94+ /** Callback invoked when keybindings load fails */
95+ __dumber_keybindings_error?: (msg: unknown) => void;
96+ /** Callback invoked when a keybinding is set */
97+ __dumber_keybinding_set?: (resp?: unknown) => void;
98+ /** Callback invoked when keybinding set fails */
99+ __dumber_keybinding_set_error?: (msg: unknown) => void;
100+ /** Callback invoked when a keybinding is reset */
101+ __dumber_keybinding_reset?: (resp?: unknown) => void;
102+ /** Callback invoked when keybinding reset fails */
103+ __dumber_keybinding_reset_error?: (msg: unknown) => void;
104+ /** Callback invoked when all keybindings are reset */
105+ __dumber_keybindings_reset_all?: (resp?: unknown) => void;
106+ /** Callback invoked when reset all keybindings fails */
107+ __dumber_keybindings_reset_all_error?: (msg: unknown) => void;
108+
109+ // ─────────────────────────────────────────────────────────────────────────
110+ // Homepage Callbacks
111+ // ─────────────────────────────────────────────────────────────────────────
112+ /** Generic response handler for homepage requests */
113+ __dumber_homepage_response?: HomepageResponseCallback;
114+ /** History timeline data callback */
115+ __dumber_history_timeline?: HomepageLegacyCallback;
116+ /** Folders data callback */
117+ __dumber_folders?: HomepageLegacyCallback;
118+ /** Tags data callback */
119+ __dumber_tags?: HomepageLegacyCallback;
120+ /** Favorites data callback */
121+ __dumber_favorites?: HomepageLegacyCallback;
122+ /** Analytics data callback */
123+ __dumber_analytics?: HomepageLegacyCallback;
124+ /** Domain stats data callback */
125+ __dumber_domain_stats?: HomepageLegacyCallback;
126+ /** History search results callback */
127+ __dumber_history_search_results?: HomepageLegacyCallback;
128+ /** History deleted callback */
129+ __dumber_history_deleted?: HomepageLegacyCallback;
130+ /** History cleared callback */
131+ __dumber_history_cleared?: HomepageLegacyCallback;
132+ /** Error callback for homepage requests */
133+ __dumber_error?: HomepageErrorCallback;
134+
135+ // ─────────────────────────────────────────────────────────────────────────
136+ // Theme Management
137+ // ─────────────────────────────────────────────────────────────────────────
138+ /** Color scheme manager for theme switching */
139+ __dumber_color_scheme_manager?: ColorSchemeManager;
140+ /** Legacy theme setter */
141+ __dumber_setTheme?: (theme: ThemeMode) => void;
142+ }
143+
144+ interface Document {
145+ /** View Transition API */
146+ startViewTransition?: (callback: () => void | Promise<void>) => ViewTransition;
147+ }
148+}
149+
150+export {};
@@ -0,0 +1,150 @@
1+/**
2+ * Global type declarations for Dumber browser WebKit bridge
3+ */
4+
5+// ═══════════════════════════════════════════════════════════════════════════════
6+// WEBKIT BRIDGE TYPES
7+// ═══════════════════════════════════════════════════════════════════════════════
8+
9+interface DumberBridge {
10+ postMessage: (message: unknown) => void;
11+}
12+
13+interface DumberWebKit {
14+ messageHandlers?: {
15+ dumber?: DumberBridge;
16+ };
17+}
18+
19+// ═══════════════════════════════════════════════════════════════════════════════
20+// VIEW TRANSITION API (not yet in TypeScript DOM lib)
21+// ═══════════════════════════════════════════════════════════════════════════════
22+
23+interface ViewTransition {
24+ finished: Promise<void>;
25+ ready: Promise<void>;
26+ updateCallbackDone: Promise<void>;
27+}
28+
29+// ═══════════════════════════════════════════════════════════════════════════════
30+// THEME TYPES
31+// ═══════════════════════════════════════════════════════════════════════════════
32+
33+type ThemeMode = 'light' | 'dark';
34+
35+interface ColorSchemeManager {
36+ setUserPreference?: (theme: ThemeMode) => void;
37+}
38+
39+// ═══════════════════════════════════════════════════════════════════════════════
40+// HOMEPAGE MESSAGING TYPES
41+// ═══════════════════════════════════════════════════════════════════════════════
42+
43+/** Response from Go backend for homepage requests */
44+interface HomepageResponse {
45+ requestId: string;
46+ success: boolean;
47+ data?: unknown;
48+ error?: string;
49+}
50+
51+/** Error response format (can be string for legacy or object) */
52+type HomepageErrorResponse = string | { requestId?: string; error?: string };
53+
54+/** Callback type for homepage responses */
55+type HomepageResponseCallback = (response: HomepageResponse) => void;
56+
57+/** Callback type for legacy data responses (can receive raw data or Response object) */
58+type HomepageLegacyCallback = (dataOrResponse: unknown) => void;
59+
60+/** Callback type for error responses */
61+type HomepageErrorCallback = (errorOrResponse: HomepageErrorResponse) => void;
62+
63+// ═══════════════════════════════════════════════════════════════════════════════
64+// GLOBAL DECLARATIONS
65+// ═══════════════════════════════════════════════════════════════════════════════
66+
67+declare global {
68+ // Re-export bridge type for use in other modules
69+ type DumberBridge = {
70+ postMessage: (message: unknown) => void;
71+ };
72+ interface Window {
73+ // ─────────────────────────────────────────────────────────────────────────
74+ // Core WebKit Bridge
75+ // ─────────────────────────────────────────────────────────────────────────
76+ /** WebView ID assigned by the browser backend */
77+ __dumber_webview_id?: number;
78+ /** WebKit message handlers for native bridge */
79+ webkit?: DumberWebKit;
80+
81+ // ─────────────────────────────────────────────────────────────────────────
82+ // Config Page Callbacks
83+ // ─────────────────────────────────────────────────────────────────────────
84+ /** Callback invoked when config save succeeds */
85+ __dumber_config_saved?: (resp?: unknown) => void;
86+ /** Callback invoked when config save fails */
87+ __dumber_config_error?: (msg: unknown) => void;
88+
89+ // ─────────────────────────────────────────────────────────────────────────
90+ // Keybindings Callbacks
91+ // ─────────────────────────────────────────────────────────────────────────
92+ /** Callback invoked when keybindings are loaded */
93+ __dumber_keybindings_loaded?: (resp?: unknown) => void;
94+ /** Callback invoked when keybindings load fails */
95+ __dumber_keybindings_error?: (msg: unknown) => void;
96+ /** Callback invoked when a keybinding is set */
97+ __dumber_keybinding_set?: (resp?: unknown) => void;
98+ /** Callback invoked when keybinding set fails */
99+ __dumber_keybinding_set_error?: (msg: unknown) => void;
100+ /** Callback invoked when a keybinding is reset */
101+ __dumber_keybinding_reset?: (resp?: unknown) => void;
102+ /** Callback invoked when keybinding reset fails */
103+ __dumber_keybinding_reset_error?: (msg: unknown) => void;
104+ /** Callback invoked when all keybindings are reset */
105+ __dumber_keybindings_reset_all?: (resp?: unknown) => void;
106+ /** Callback invoked when reset all keybindings fails */
107+ __dumber_keybindings_reset_all_error?: (msg: unknown) => void;
108+
109+ // ─────────────────────────────────────────────────────────────────────────
110+ // Homepage Callbacks
111+ // ─────────────────────────────────────────────────────────────────────────
112+ /** Generic response handler for homepage requests */
113+ __dumber_homepage_response?: HomepageResponseCallback;
114+ /** History timeline data callback */
115+ __dumber_history_timeline?: HomepageLegacyCallback;
116+ /** Folders data callback */
117+ __dumber_folders?: HomepageLegacyCallback;
118+ /** Tags data callback */
119+ __dumber_tags?: HomepageLegacyCallback;
120+ /** Favorites data callback */
121+ __dumber_favorites?: HomepageLegacyCallback;
122+ /** Analytics data callback */
123+ __dumber_analytics?: HomepageLegacyCallback;
124+ /** Domain stats data callback */
125+ __dumber_domain_stats?: HomepageLegacyCallback;
126+ /** History search results callback */
127+ __dumber_history_search_results?: HomepageLegacyCallback;
128+ /** History deleted callback */
129+ __dumber_history_deleted?: HomepageLegacyCallback;
130+ /** History cleared callback */
131+ __dumber_history_cleared?: HomepageLegacyCallback;
132+ /** Error callback for homepage requests */
133+ __dumber_error?: HomepageErrorCallback;
134+
135+ // ─────────────────────────────────────────────────────────────────────────
136+ // Theme Management
137+ // ─────────────────────────────────────────────────────────────────────────
138+ /** Color scheme manager for theme switching */
139+ __dumber_color_scheme_manager?: ColorSchemeManager;
140+ /** Legacy theme setter */
141+ __dumber_setTheme?: (theme: ThemeMode) => void;
142+ }
143+
144+ interface Document {
145+ /** View Transition API */
146+ startViewTransition?: (callback: () => void | Promise<void>) => ViewTransition;
147+ }
148+}
149+
150+export {};

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