detached 66a30d0

feat(webkit): migrate toast notifications from javascript to svelte components

commit
66a30d06e8974e4a8100ddf2a4a8d64da7849290
author
bamen · · 2025-09-15 05:21 UTC
parent
5e2466e
signature
unsigned
3 files · +73 -527
D pkg/webkit/toast_js_cgo.go +0 -199
@@ -1,199 +0,0 @@
1-//go:build webkit_cgo
2-
3-package webkit
4-
5-// getToastScript returns the injected toast notification component script.
6-// Provides a reusable toast system that can display messages across all pages.
7-func getToastScript() string {
8- return `(() => {
9- try {
10- if (window.__dumber_toast_loaded) return; // idempotent
11- window.__dumber_toast_loaded = true;
12-
13- const T = {
14- container: null,
15- toasts: [], // {id, element, timeout}
16- counter: 0,
17- zoomToastId: null,
18- zoomDebounceTimer: null,
19-
20- init() {
21- if (T.container) return;
22-
23- // Create toast container
24- const container = document.createElement('div');
25- container.id = 'dumber-toast-container';
26- container.style.cssText = 'position:fixed;bottom:20px;right:20px;z-index:2147483646;pointer-events:none;';
27- document.documentElement.appendChild(container);
28- T.container = container;
29-
30- // Add CSS styles
31- const style = document.createElement('style');
32- style.textContent = ` +
33- "`" + `
34- .dumber-toast {
35- background: rgba(18, 18, 18, 0.95);
36- color: #eee;
37- padding: 12px 16px;
38- border-radius: 8px;
39- border: 1px solid #444;
40- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
41- font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, "Helvetica Neue", Arial, sans-serif;
42- font-size: 14px;
43- line-height: 1.4;
44- margin-bottom: 8px;
45- max-width: 320px;
46- word-wrap: break-word;
47- backdrop-filter: blur(8px);
48- -webkit-backdrop-filter: blur(8px);
49- pointer-events: auto;
50- cursor: default;
51- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
52- transform: translateX(100%);
53- opacity: 0;
54- }
55-
56- .dumber-toast.show {
57- transform: translateX(0);
58- opacity: 1;
59- }
60-
61- .dumber-toast:hover {
62- background: rgba(27, 27, 27, 0.95);
63- border-color: #555;
64- }
65- ` + "`" + `;
66- document.documentElement.appendChild(style);
67- },
68-
69- show(message, duration = 2500) {
70- T.init();
71-
72- const id = ++T.counter;
73- const toast = document.createElement('div');
74- toast.className = 'dumber-toast';
75- toast.textContent = message || 'Notification';
76- toast.setAttribute('data-toast-id', id);
77-
78- // Click to dismiss
79- toast.addEventListener('click', () => T.dismiss(id));
80-
81- T.container.appendChild(toast);
82-
83- // Trigger show animation
84- requestAnimationFrame(() => {
85- toast.classList.add('show');
86- });
87-
88- // Auto-dismiss
89- const timeout = setTimeout(() => T.dismiss(id), duration);
90-
91- T.toasts.push({ id, element: toast, timeout });
92-
93- console.log('📋 Toast:', message);
94- return id;
95- },
96-
97- dismiss(id) {
98- const toastIndex = T.toasts.findIndex(t => t.id === id);
99- if (toastIndex === -1) return;
100-
101- const toast = T.toasts[toastIndex];
102- clearTimeout(toast.timeout);
103-
104- // Clear zoom toast tracking if this is the zoom toast
105- if (T.zoomToastId === id) {
106- T.zoomToastId = null;
107- }
108-
109- // Animate out
110- toast.element.style.transform = 'translateX(100%)';
111- toast.element.style.opacity = '0';
112-
113- setTimeout(() => {
114- if (toast.element && toast.element.parentNode) {
115- toast.element.parentNode.removeChild(toast.element);
116- }
117- T.toasts.splice(toastIndex, 1);
118- }, 300);
119- },
120-
121- clear() {
122- T.toasts.forEach(toast => {
123- clearTimeout(toast.timeout);
124- if (toast.element && toast.element.parentNode) {
125- toast.element.parentNode.removeChild(toast.element);
126- }
127- });
128- T.toasts = [];
129- T.zoomToastId = null;
130- clearTimeout(T.zoomDebounceTimer);
131- T.zoomDebounceTimer = null;
132- }
133- };
134-
135- // Public API for native integration
136- window.__dumber_showToast = (message, duration) => T.show(message, duration);
137- window.__dumber_dismissToast = (id) => T.dismiss(id);
138- window.__dumber_clearToasts = () => T.clear();
139- window.__dumber_showZoomToast = (zoomLevel) => {
140- console.log('[dumber] showZoomToast called with level:', zoomLevel);
141-
142- // Clear existing debounce timer
143- clearTimeout(T.zoomDebounceTimer);
144-
145- // Force remove ALL zoom toasts immediately (aggressive cleanup)
146- for (let i = T.toasts.length - 1; i >= 0; i--) {
147- const toast = T.toasts[i];
148- if (toast.element && (toast.element.textContent.includes('Zoom level:') || toast.id === T.zoomToastId)) {
149- clearTimeout(toast.timeout);
150- if (toast.element.parentNode) {
151- toast.element.parentNode.removeChild(toast.element);
152- }
153- T.toasts.splice(i, 1);
154- }
155- }
156- T.zoomToastId = null;
157-
158- // Also cleanup any orphaned zoom toast elements in DOM
159- const orphanedZoomToasts = document.querySelectorAll('.dumber-toast');
160- orphanedZoomToasts.forEach(el => {
161- if (el.textContent.includes('Zoom level:')) {
162- el.parentNode && el.parentNode.removeChild(el);
163- }
164- });
165-
166- // Debounce the toast display
167- T.zoomDebounceTimer = setTimeout(() => {
168- const percentage = Math.round(zoomLevel * 100);
169- const sign = percentage > 100 ? '+' : '';
170- const diff = percentage - 100;
171- const message = 'Zoom level: ' + sign + diff + '% (' + percentage + '%)';
172- console.log('[dumber] Showing zoom toast:', message);
173- T.zoomToastId = T.show(message, 2000);
174- }, 150);
175- };
176-
177- // Test function for manual debugging
178- window.__dumber_testToast = () => {
179- console.log('[dumber] Testing toast system...');
180- return T.show('Toast system is working!', 2000);
181- };
182-
183- // Emergency cleanup function for stuck toasts
184- window.__dumber_forceCleanupToasts = () => {
185- console.log('[dumber] Force cleaning up all toasts...');
186- T.clear();
187- // Also force remove any orphaned toast elements
188- const orphans = document.querySelectorAll('.dumber-toast');
189- orphans.forEach(el => el.parentNode && el.parentNode.removeChild(el));
190- console.log('[dumber] Cleanup complete, removed', orphans.length, 'orphaned toasts');
191- };
192-
193- console.log('✅ Dumber Browser toast system loaded');
194-
195- } catch (e) {
196- console.error('❌ Failed to initialize Dumber toast system:', e);
197- }
198-})();`
199-}
@@ -1,199 +0,0 @@
1-//go:build webkit_cgo
2-
3-package webkit
4-
5-// getToastScript returns the injected toast notification component script.
6-// Provides a reusable toast system that can display messages across all pages.
7-func getToastScript() string {
8- return `(() => {
9- try {
10- if (window.__dumber_toast_loaded) return; // idempotent
11- window.__dumber_toast_loaded = true;
12-
13- const T = {
14- container: null,
15- toasts: [], // {id, element, timeout}
16- counter: 0,
17- zoomToastId: null,
18- zoomDebounceTimer: null,
19-
20- init() {
21- if (T.container) return;
22-
23- // Create toast container
24- const container = document.createElement('div');
25- container.id = 'dumber-toast-container';
26- container.style.cssText = 'position:fixed;bottom:20px;right:20px;z-index:2147483646;pointer-events:none;';
27- document.documentElement.appendChild(container);
28- T.container = container;
29-
30- // Add CSS styles
31- const style = document.createElement('style');
32- style.textContent = ` +
33- "`" + `
34- .dumber-toast {
35- background: rgba(18, 18, 18, 0.95);
36- color: #eee;
37- padding: 12px 16px;
38- border-radius: 8px;
39- border: 1px solid #444;
40- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
41- font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, "Helvetica Neue", Arial, sans-serif;
42- font-size: 14px;
43- line-height: 1.4;
44- margin-bottom: 8px;
45- max-width: 320px;
46- word-wrap: break-word;
47- backdrop-filter: blur(8px);
48- -webkit-backdrop-filter: blur(8px);
49- pointer-events: auto;
50- cursor: default;
51- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
52- transform: translateX(100%);
53- opacity: 0;
54- }
55-
56- .dumber-toast.show {
57- transform: translateX(0);
58- opacity: 1;
59- }
60-
61- .dumber-toast:hover {
62- background: rgba(27, 27, 27, 0.95);
63- border-color: #555;
64- }
65- ` + "`" + `;
66- document.documentElement.appendChild(style);
67- },
68-
69- show(message, duration = 2500) {
70- T.init();
71-
72- const id = ++T.counter;
73- const toast = document.createElement('div');
74- toast.className = 'dumber-toast';
75- toast.textContent = message || 'Notification';
76- toast.setAttribute('data-toast-id', id);
77-
78- // Click to dismiss
79- toast.addEventListener('click', () => T.dismiss(id));
80-
81- T.container.appendChild(toast);
82-
83- // Trigger show animation
84- requestAnimationFrame(() => {
85- toast.classList.add('show');
86- });
87-
88- // Auto-dismiss
89- const timeout = setTimeout(() => T.dismiss(id), duration);
90-
91- T.toasts.push({ id, element: toast, timeout });
92-
93- console.log('📋 Toast:', message);
94- return id;
95- },
96-
97- dismiss(id) {
98- const toastIndex = T.toasts.findIndex(t => t.id === id);
99- if (toastIndex === -1) return;
100-
101- const toast = T.toasts[toastIndex];
102- clearTimeout(toast.timeout);
103-
104- // Clear zoom toast tracking if this is the zoom toast
105- if (T.zoomToastId === id) {
106- T.zoomToastId = null;
107- }
108-
109- // Animate out
110- toast.element.style.transform = 'translateX(100%)';
111- toast.element.style.opacity = '0';
112-
113- setTimeout(() => {
114- if (toast.element && toast.element.parentNode) {
115- toast.element.parentNode.removeChild(toast.element);
116- }
117- T.toasts.splice(toastIndex, 1);
118- }, 300);
119- },
120-
121- clear() {
122- T.toasts.forEach(toast => {
123- clearTimeout(toast.timeout);
124- if (toast.element && toast.element.parentNode) {
125- toast.element.parentNode.removeChild(toast.element);
126- }
127- });
128- T.toasts = [];
129- T.zoomToastId = null;
130- clearTimeout(T.zoomDebounceTimer);
131- T.zoomDebounceTimer = null;
132- }
133- };
134-
135- // Public API for native integration
136- window.__dumber_showToast = (message, duration) => T.show(message, duration);
137- window.__dumber_dismissToast = (id) => T.dismiss(id);
138- window.__dumber_clearToasts = () => T.clear();
139- window.__dumber_showZoomToast = (zoomLevel) => {
140- console.log('[dumber] showZoomToast called with level:', zoomLevel);
141-
142- // Clear existing debounce timer
143- clearTimeout(T.zoomDebounceTimer);
144-
145- // Force remove ALL zoom toasts immediately (aggressive cleanup)
146- for (let i = T.toasts.length - 1; i >= 0; i--) {
147- const toast = T.toasts[i];
148- if (toast.element && (toast.element.textContent.includes('Zoom level:') || toast.id === T.zoomToastId)) {
149- clearTimeout(toast.timeout);
150- if (toast.element.parentNode) {
151- toast.element.parentNode.removeChild(toast.element);
152- }
153- T.toasts.splice(i, 1);
154- }
155- }
156- T.zoomToastId = null;
157-
158- // Also cleanup any orphaned zoom toast elements in DOM
159- const orphanedZoomToasts = document.querySelectorAll('.dumber-toast');
160- orphanedZoomToasts.forEach(el => {
161- if (el.textContent.includes('Zoom level:')) {
162- el.parentNode && el.parentNode.removeChild(el);
163- }
164- });
165-
166- // Debounce the toast display
167- T.zoomDebounceTimer = setTimeout(() => {
168- const percentage = Math.round(zoomLevel * 100);
169- const sign = percentage > 100 ? '+' : '';
170- const diff = percentage - 100;
171- const message = 'Zoom level: ' + sign + diff + '% (' + percentage + '%)';
172- console.log('[dumber] Showing zoom toast:', message);
173- T.zoomToastId = T.show(message, 2000);
174- }, 150);
175- };
176-
177- // Test function for manual debugging
178- window.__dumber_testToast = () => {
179- console.log('[dumber] Testing toast system...');
180- return T.show('Toast system is working!', 2000);
181- };
182-
183- // Emergency cleanup function for stuck toasts
184- window.__dumber_forceCleanupToasts = () => {
185- console.log('[dumber] Force cleaning up all toasts...');
186- T.clear();
187- // Also force remove any orphaned toast elements
188- const orphans = document.querySelectorAll('.dumber-toast');
189- orphans.forEach(el => el.parentNode && el.parentNode.removeChild(el));
190- console.log('[dumber] Cleanup complete, removed', orphans.length, 'orphaned toasts');
191- };
192-
193- console.log('✅ Dumber Browser toast system loaded');
194-
195- } catch (e) {
196- console.error('❌ Failed to initialize Dumber toast system:', e);
197- }
198-})();`
199-}
A pkg/webkit/toast_svelte_cgo.go +73 -0
@@ -0,0 +1,73 @@
1+//go:build webkit_cgo
2+
3+package webkit
4+
5+import (
6+ "fmt"
7+ "log"
8+)
9+
10+// getToastScript returns the unified GUI bundle-based toast notification script with GTK theme integration.
11+// This function is deprecated and should use the new unified approach via BrowserService.InjectToastSystem
12+func getToastScript(assets interface{ ReadFile(string) ([]byte, error) }) string {
13+ // Read the unified GUI bundle from assets
14+ var guiBundle string
15+ if assets != nil {
16+ bundleBytes, err := assets.ReadFile("gui/gui.min.js")
17+ if err != nil {
18+ // Fallback to error message if file not found
19+ guiBundle = `console.error("❌ Failed to load unified GUI bundle: ` + err.Error() + `");`
20+ log.Printf("[webkit] Failed to read GUI bundle: %v", err)
21+ } else {
22+ guiBundle = string(bundleBytes)
23+ log.Printf("[webkit] Successfully loaded unified GUI bundle (%d bytes)", len(bundleBytes))
24+ }
25+ } else {
26+ guiBundle = `console.error("❌ Failed to load unified GUI bundle: assets not provided");`
27+ log.Printf("[webkit] GUI bundle error: assets not provided")
28+ }
29+
30+ // GTK Theme Integration with new unified approach
31+ themeIntegration := `
32+// Set up theme change listener for dynamic updates
33+window.__dumber_setTheme = (theme) => {
34+ window.__dumber_initial_theme = theme;
35+ if (theme === 'dark') {
36+ document.documentElement.classList.add('dark');
37+ } else {
38+ document.documentElement.classList.remove('dark');
39+ }
40+};
41+`
42+
43+ // Use unified GUI bundle approach
44+ return fmt.Sprintf(`(function() {
45+ function initializeToast() {
46+ try {
47+ %s
48+
49+ %s
50+
51+ // Initialize toast system via unified GUI bundle
52+ if (window.__dumber_gui && window.__dumber_gui.initializeToast) {
53+ window.__dumber_gui.initializeToast().then(() => {
54+ console.log('✅ Toast system initialized via unified GUI bundle');
55+ }).catch((e) => {
56+ console.error('❌ Toast initialization failed:', e);
57+ });
58+ } else {
59+ console.error('❌ Unified GUI bundle not properly loaded');
60+ }
61+ } catch (e) {
62+ console.error('❌ Failed to initialize toast system:', e);
63+ }
64+ }
65+
66+ // Ensure DOM is ready before initializing
67+ if (document.readyState === 'loading') {
68+ document.addEventListener('DOMContentLoaded', initializeToast);
69+ } else {
70+ initializeToast();
71+ }
72+})();`, themeIntegration, guiBundle)
73+}⏎̸
@@ -0,0 +1,73 @@
1+//go:build webkit_cgo
2+
3+package webkit
4+
5+import (
6+ "fmt"
7+ "log"
8+)
9+
10+// getToastScript returns the unified GUI bundle-based toast notification script with GTK theme integration.
11+// This function is deprecated and should use the new unified approach via BrowserService.InjectToastSystem
12+func getToastScript(assets interface{ ReadFile(string) ([]byte, error) }) string {
13+ // Read the unified GUI bundle from assets
14+ var guiBundle string
15+ if assets != nil {
16+ bundleBytes, err := assets.ReadFile("gui/gui.min.js")
17+ if err != nil {
18+ // Fallback to error message if file not found
19+ guiBundle = `console.error("❌ Failed to load unified GUI bundle: ` + err.Error() + `");`
20+ log.Printf("[webkit] Failed to read GUI bundle: %v", err)
21+ } else {
22+ guiBundle = string(bundleBytes)
23+ log.Printf("[webkit] Successfully loaded unified GUI bundle (%d bytes)", len(bundleBytes))
24+ }
25+ } else {
26+ guiBundle = `console.error("❌ Failed to load unified GUI bundle: assets not provided");`
27+ log.Printf("[webkit] GUI bundle error: assets not provided")
28+ }
29+
30+ // GTK Theme Integration with new unified approach
31+ themeIntegration := `
32+// Set up theme change listener for dynamic updates
33+window.__dumber_setTheme = (theme) => {
34+ window.__dumber_initial_theme = theme;
35+ if (theme === 'dark') {
36+ document.documentElement.classList.add('dark');
37+ } else {
38+ document.documentElement.classList.remove('dark');
39+ }
40+};
41+`
42+
43+ // Use unified GUI bundle approach
44+ return fmt.Sprintf(`(function() {
45+ function initializeToast() {
46+ try {
47+ %s
48+
49+ %s
50+
51+ // Initialize toast system via unified GUI bundle
52+ if (window.__dumber_gui && window.__dumber_gui.initializeToast) {
53+ window.__dumber_gui.initializeToast().then(() => {
54+ console.log('✅ Toast system initialized via unified GUI bundle');
55+ }).catch((e) => {
56+ console.error('❌ Toast initialization failed:', e);
57+ });
58+ } else {
59+ console.error('❌ Unified GUI bundle not properly loaded');
60+ }
61+ } catch (e) {
62+ console.error('❌ Failed to initialize toast system:', e);
63+ }
64+ }
65+
66+ // Ensure DOM is ready before initializing
67+ if (document.readyState === 'loading') {
68+ document.addEventListener('DOMContentLoaded', initializeToast);
69+ } else {
70+ initializeToast();
71+ }
72+})();`, themeIntegration, guiBundle)
73+}⏎̸
D pkg/webkit/ucm_js_cgo.go +0 -328
@@ -1,328 +0,0 @@
1-//go:build webkit_cgo
2-
3-package webkit
4-
5-// getOmniboxScript returns the injected omnibox/find component script.
6-// Kept in a cgo-tagged file to ensure availability when referenced
7-// from other cgo files in this package.
8-func getOmniboxScript() string {
9- return `(() => {
10- try {
11- if (window.__dumber_omnibox_loaded) return; // idempotent
12- window.__dumber_omnibox_loaded = true;
13-
14- const MAX_MATCHES = 2000;
15- const H = {
16- el: null,
17- box: null,
18- input: null,
19- list: null,
20- visible: false,
21- mode: 'omnibox', // 'omnibox' | 'find'
22- suggestions: [],
23- matches: [], // {el, context}
24- selectedIndex: -1,
25- activeIndex: -1,
26- debounceTimer: 0,
27- highlightNodes: [],
28- faded: false,
29- prevOverflow: '',
30- post(msg){ try { window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify(msg)); } catch(_){} },
31- render(){
32- if (!H.el) H.mount();
33- H.el.style.display = H.visible ? 'block' : 'none';
34- if (!H.visible) return;
35- H.input.placeholder = H.mode === 'find' ? 'Find in page…' : 'Type URL or search…';
36- H.input.focus();
37- },
38- setMode(m){ H.mode = m === 'find' ? 'find' : 'omnibox'; H.selectedIndex = -1; H.setFaded(false); H.paintList(); },
39- open(mode, initial){ H.setMode(mode||'omnibox'); H.toggle(true); if (typeof initial==='string') { H.input.value = initial; H.onInput(); } },
40- close(){ if (H.mode==='find') H.clearHighlights(); H.setFaded(false); H.toggle(false); },
41- toggle(v){
42- const newState = (typeof v==='boolean')? v : !H.visible;
43- if (newState && !H.visible) {
44- // Disable page scrolling while overlay is visible
45- H.prevOverflow = document.documentElement.style.overflow;
46- document.documentElement.style.overflow = 'hidden';
47- } else if (!newState && H.visible) {
48- // Restore page scrolling on close
49- document.documentElement.style.overflow = H.prevOverflow || '';
50- H.prevOverflow = '';
51- }
52- H.visible = newState;
53- H.render();
54- },
55- setFaded(v){
56- H.faded = !!v;
57- if (!H.box || !H.input || !H.list) return;
58- if (H.faded) {
59- H.box.style.background = 'rgba(27,27,27,0.25)';
60- H.box.style.backdropFilter = 'blur(2px) saturate(110%)';
61- H.box.style.webkitBackdropFilter = 'blur(2px) saturate(110%)';
62- H.input.style.background = 'rgba(18,18,18,0.35)';
63- H.input.style.color = '#eee';
64- } else {
65- H.box.style.background = '#1b1b1b';
66- H.box.style.backdropFilter = '';
67- H.box.style.webkitBackdropFilter = '';
68- H.input.style.background = '#121212';
69- H.input.style.color = '#eee';
70- }
71- },
72- scrollListToSelection(){
73- if (!H.list) return;
74- let idx = H.selectedIndex|0;
75- if (idx < 0) return;
76- // Account for header in find mode
77- const childIdx = (H.mode==='find' ? 1 : 0) + idx;
78- const el = H.list.children[childIdx];
79- if (el && el.scrollIntoView) {
80- try { el.scrollIntoView({block:'nearest'}); } catch(_) { el.scrollIntoView(); }
81- }
82- },
83- mount(){
84- const root = document.createElement('div');
85- root.id = 'dumber-omnibox-root';
86- root.style.cssText = 'position:fixed;inset:0;z-index:2147483647;display:none;';
87- const box = document.createElement('div');
88- box.style.cssText = 'width:min(90vw, 720px);margin:8vh auto;padding:8px 12px;background:#1b1b1b;color:#eee;border:1px solid #444;border-radius:8px;box-shadow:0 10px 30px rgba(0,0,0,.6);font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,"Helvetica Neue",Arial,sans-serif;';
89- const input = document.createElement('input');
90- input.type = 'text';
91- input.placeholder = 'Type URL or search…';
92- input.style.cssText = 'width:100%;box-sizing:border-box;padding:10px 12px;border-radius:6px;border:1px solid #555;background:#121212;color:#eee;font-size:16px;outline:none;';
93- const list = document.createElement('div');
94- list.style.cssText = 'margin-top:8px;max-height:50vh;overflow:auto;';
95- const style = document.createElement('style');
96- style.textContent = '.dumber-find-highlight{background:#ffeb3b;color:#000;padding:0 1px;border-radius:2px;box-shadow:0 0 0 1px #c8b900 inset}.dumber-find-active{background:#ff9800 !important;color:#000;box-shadow:0 0 0 1px #b36b00 inset}';
97- document.documentElement.appendChild(style);
98- box.appendChild(input); box.appendChild(list); root.appendChild(box); document.documentElement.appendChild(root);
99- H.box = box;
100- // Dynamic style updater for responsive behavior
101- const updateStyles = () => {
102- const vw = window.innerWidth;
103- const isSmall = vw < 640;
104- const isLarge = vw > 1920;
105-
106- if (box) {
107- box.style.width = isSmall ? '95vw' : isLarge ? 'min(90vw, 840px)' : 'min(90vw, 720px)';
108- box.style.padding = isSmall ? '6px 8px' : '8px 12px';
109- }
110-
111- if (input) {
112- input.style.fontSize = isSmall ? '14px' : isLarge ? '17px' : '16px';
113- input.style.padding = isSmall ? '8px 10px' : '10px 12px';
114- input.style.boxSizing = 'border-box';
115- }
116- };
117- updateStyles();
118- window.addEventListener('resize', updateStyles);
119- // Click outside the box closes the overlay
120- root.addEventListener('mousedown', (e)=>{
121- const tgt = e.target;
122- if (tgt && box && !box.contains(tgt)) {
123- e.preventDefault();
124- e.stopPropagation();
125- H.close();
126- }
127- }, true);
128- box.addEventListener('mousedown', (e)=>{ e.stopPropagation(); }, true);
129- // Focus and select input on hover for quick typing
130- input.addEventListener('mouseenter', ()=>{ if (H.visible) { try { input.focus({preventScroll:true}); input.select(); } catch(_) { input.focus(); } } });
131- root.addEventListener('mouseenter', ()=>{ if (H.visible) { try { input.focus({preventScroll:true}); } catch(_) { input.focus(); } H.setFaded(false); } });
132- // Clear fading when user goes back to input typing
133- input.addEventListener('focus', ()=> H.setFaded(false));
134- input.addEventListener('keydown', (e)=>{
135- if (e.key === 'Escape'){ H.close(); }
136- else if (H.mode==='omnibox' && e.key === 'Enter'){
137- const pick = H.suggestions && H.suggestions[H.selectedIndex|0];
138- const v = (pick && pick.url) || input.value || '';
139- if (v) H.post({type:'navigate', url:v});
140- H.toggle(false);
141- } else if (H.mode==='find' && e.key === 'Enter'){
142- e.preventDefault();
143- if (e.shiftKey) {
144- H.jump(-1);
145- } else if (e.altKey) {
146- // Center on current match but keep overlay open for further navigation
147- H.revealSelection();
148- } else {
149- // Default: center and close
150- H.revealSelection();
151- H.close();
152- }
153- } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp'){
154- e.preventDefault(); e.stopPropagation();
155- const n = (H.mode==='find'? H.matches.length : H.suggestions.length);
156- if (n){
157- H.selectedIndex = (H.selectedIndex||0) + (e.key==='ArrowDown'?1:-1);
158- if (H.selectedIndex<0) H.selectedIndex = n-1; if (H.selectedIndex>=n) H.selectedIndex = 0;
159- H.paintList();
160- H.scrollListToSelection();
161- // Fade overlay while navigating results; also reveal active match in find mode
162- H.setFaded(true);
163- if (H.mode==='find') { H.revealSelection(); }
164- }
165- } else {
166- // Any other key input should restore full opacity while editing
167- H.setFaded(false);
168- }
169- });
170- input.addEventListener('input', ()=> { H.onInput(); H.setFaded(false); });
171- input.addEventListener('mousedown', ()=> H.setFaded(false));
172- input.addEventListener('click', ()=> H.setFaded(false));
173- H.el = root; H.input = input; H.list = list; H.selectedIndex = -1; H.paintList();
174- },
175- onInput(){
176- const q = H.input.value || '';
177- if (H.mode === 'omnibox'){
178- clearTimeout(H.debounceTimer);
179- H.debounceTimer = setTimeout(()=> H.post({type:'query', q, limit:10}), 120);
180- } else {
181- H.find(q);
182- }
183- },
184- paintList(){
185- const list = H.list; if (!list) return; list.textContent = '';
186-
187- // Show/hide separator based on content
188- const hasContent = (H.mode==='omnibox' && H.suggestions.length > 0) || (H.mode==='find' && H.matches.length > 0);
189- list.style.borderTop = hasContent ? '1px solid #333' : 'none';
190-
191- if (H.mode==='omnibox'){
192- H.suggestions.forEach((s, i)=>{
193- const item = document.createElement('div');
194- item.style.cssText = 'padding:8px 10px;display:flex;gap:10px;align-items:center;cursor:pointer;border-bottom:1px solid #2a2a2a;'+(i===H.selectedIndex?'background:#0a0a0a;':'');
195- // Favicon chip (wrapper ensures perfect circle sizing)
196- const chip = document.createElement('div');
197- chip.style.cssText = 'flex:0 0 20px;width:20px;height:20px;border-radius:50%;background:#ccc;border:1px solid rgba(0,0,0,.12);box-shadow:0 1px 2px rgba(0,0,0,.12);display:flex;align-items:center;justify-content:center;';
198- const icon = document.createElement('img');
199- icon.src = s.favicon || '';
200- icon.loading = 'lazy';
201- icon.style.cssText = 'width:16px;height:16px;filter:brightness(1.06) contrast(1.03);image-rendering:-webkit-optimize-contrast;';
202- icon.onerror = ()=>{ chip.style.display='none'; };
203- chip.appendChild(icon);
204- // Text line: Domain | full path (one line, fade at end)
205- let domain = '', path = '';
206- try { const u = new URL(s.url, window.location.href); domain = u.hostname; path = (u.pathname||'') + (u.search||'') + (u.hash||''); } catch(_) { domain = s.url || ''; }
207- const text = document.createElement('div');
208- text.style.cssText = 'flex:1;min-width:0;display:flex;gap:8px;align-items:center;white-space:nowrap;overflow:hidden;';
209- // Apply gradient fade using mask for WebKit
210- text.style.webkitMaskImage = 'linear-gradient(90deg, black 85%, transparent 100%)';
211- text.style.maskImage = 'linear-gradient(90deg, black 85%, transparent 100%)';
212- const domainEl = document.createElement('span'); domainEl.textContent = domain; domainEl.style.cssText = 'color:#e6e6e6;opacity:.95;';
213- const sep = document.createElement('span'); sep.textContent = ' | '; sep.style.cssText = 'color:#777;';
214- const pathEl = document.createElement('span'); pathEl.textContent = path || '/'; pathEl.style.cssText = 'color:#9ad;';
215- text.appendChild(domainEl); text.appendChild(sep); text.appendChild(pathEl);
216- item.appendChild(chip); item.appendChild(text);
217- item.addEventListener('mouseenter', ()=>{ H.selectedIndex = i; H.paintList(); H.scrollListToSelection(); H.setFaded(true); });
218- item.addEventListener('click',()=>{ H.post({type:'navigate', url:s.url}); H.toggle(false); });
219- list.appendChild(item);
220- });
221- } else {
222- const total = H.matches.length;
223- const header = document.createElement('div');
224- header.textContent = total ? total + ' matches' : 'No matches';
225- header.style.cssText = 'padding:6px 10px;color:#bbb;font-size:12px;border-bottom:1px solid #2a2a2a;';
226- list.appendChild(header);
227- H.matches.forEach((m, i)=>{
228- const item = document.createElement('div');
229- item.style.cssText = 'padding:8px 10px;cursor:pointer;border-bottom:1px solid #2a2a2a;'+(i===H.selectedIndex?'background:#0a0a0a;':'');
230- const ctx = document.createElement('div'); ctx.textContent = m.context || ''; ctx.style.cssText = 'color:#ddd;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
231- item.appendChild(ctx);
232- item.addEventListener('mouseenter', ()=>{ H.selectedIndex = i; H.paintList(); H.scrollListToSelection(); H.setFaded(true); });
233- item.addEventListener('click',()=>{ H.selectedIndex=i; H.revealSelection(); H.paintList(); H.close(); });
234- list.appendChild(item);
235- });
236- }
237- },
238- setSuggestions(arr){ H.suggestions = Array.isArray(arr)? arr: []; H.selectedIndex = -1; H.paintList(); },
239- // FIND MODE IMPLEMENTATION
240- clearHighlights(){
241- try {
242- H.highlightNodes.forEach(({span, text})=>{
243- const p = span.parentNode; if (!p) return; p.replaceChild(text, span); p.normalize();
244- });
245- } catch(_){}
246- H.highlightNodes = [];
247- H.matches = [];
248- H.selectedIndex = -1;
249- H.activeIndex = -1;
250- H.paintList();
251- },
252- find(q){
253- H.clearHighlights();
254- q = (q||'').trim(); if (!q) return;
255- const body = document.body; if (!body) return;
256- const root = document.getElementById('dumber-omnibox-root');
257- const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, {
258- acceptNode(node){
259- if (!node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
260- let el = node.parentElement; if (!el) return NodeFilter.FILTER_REJECT;
261- if (root && root.contains(el)) return NodeFilter.FILTER_REJECT;
262- const name = el.tagName; if (name==='SCRIPT'||name==='STYLE'||name==='NOSCRIPT') return NodeFilter.FILTER_REJECT;
263- if (getComputedStyle(el).visibility==='hidden' || getComputedStyle(el).display==='none') return NodeFilter.FILTER_REJECT;
264- return NodeFilter.FILTER_ACCEPT;
265- }
266- });
267- const lc = q.toLowerCase();
268- while (walker.nextNode()){
269- const text = walker.currentNode;
270- let s = text.nodeValue; let i = 0;
271- while (s && (i = s.toLowerCase().indexOf(lc)) !== -1) {
272- const before = document.createTextNode(s.slice(0, i));
273- const match = document.createTextNode(s.slice(i, i+q.length));
274- const afterVal = s.slice(i+q.length);
275- const span = document.createElement('span'); span.className = 'dumber-find-highlight'; span.appendChild(match);
276- const parent = text.parentNode; if (!parent) break;
277- parent.insertBefore(before, text);
278- parent.insertBefore(span, text);
279- text.nodeValue = afterVal;
280- H.highlightNodes.push({span, text: match});
281- const leftCtx = before.nodeValue.slice(-30);
282- let rightRaw = afterVal.slice(0, 30);
283- const boundaryIdx = rightRaw.search(/[\.,;:\-]/);
284- if (boundaryIdx !== -1) {
285- rightRaw = rightRaw.slice(0, boundaryIdx+1);
286- }
287- const context = (leftCtx + match.nodeValue + rightRaw).replace(/\s+/g,' ').trim();
288- H.matches.push({el: span, context});
289- if (H.matches.length >= MAX_MATCHES) break;
290- s = afterVal;
291- }
292- if (H.matches.length >= MAX_MATCHES) break;
293- }
294- H.selectedIndex = H.matches.length ? 0 : -1;
295- H.paintList();
296- H.revealSelection();
297- },
298- revealSelection(){
299- const prev = H.matches[H.activeIndex|0];
300- if (prev && prev.el && prev.el.classList) { prev.el.classList.remove('dumber-find-active'); }
301- const m = H.matches[H.selectedIndex|0]; if (!m) return;
302- if (m.el && m.el.classList) { m.el.classList.add('dumber-find-active'); }
303- H.activeIndex = H.selectedIndex|0;
304- try { m.el.scrollIntoView({block:'center', inline:'nearest'}); } catch(_){ m.el.scrollIntoView(); }
305- },
306- jump(delta){
307- const n = H.matches.length; if (!n) return;
308- H.selectedIndex = ((H.selectedIndex||0) + delta) % n; if (H.selectedIndex<0) H.selectedIndex = n-1;
309- H.paintList(); H.revealSelection();
310- }
311- };
312-
313- // Keyboard hooks
314- window.addEventListener('keydown', (e)=>{
315- const mod = (e.ctrlKey||e.metaKey);
316- if (mod && (e.key==='l' || e.key==='L')) { e.preventDefault(); H.open('omnibox'); }
317- if (mod && (e.key==='f' || e.key==='F')) { e.preventDefault(); H.open('find'); }
318- }, true);
319-
320- // Public API for native
321- window.__dumber_setSuggestions = (arr)=> H.setSuggestions(arr);
322- window.__dumber_toggle = ()=> H.toggle();
323- window.__dumber_find_open = (q)=> H.open('find', q||'');
324- window.__dumber_find_close = ()=> H.close();
325- window.__dumber_find_query = (q)=> { if (H.mode!=='find') H.setMode('find'); if (!H.visible) H.toggle(true); H.input.value = q||''; H.find(q||''); };
326- } catch (e) { /* no-op */ }
327-})();`
328-}
@@ -1,328 +0,0 @@
1-//go:build webkit_cgo
2-
3-package webkit
4-
5-// getOmniboxScript returns the injected omnibox/find component script.
6-// Kept in a cgo-tagged file to ensure availability when referenced
7-// from other cgo files in this package.
8-func getOmniboxScript() string {
9- return `(() => {
10- try {
11- if (window.__dumber_omnibox_loaded) return; // idempotent
12- window.__dumber_omnibox_loaded = true;
13-
14- const MAX_MATCHES = 2000;
15- const H = {
16- el: null,
17- box: null,
18- input: null,
19- list: null,
20- visible: false,
21- mode: 'omnibox', // 'omnibox' | 'find'
22- suggestions: [],
23- matches: [], // {el, context}
24- selectedIndex: -1,
25- activeIndex: -1,
26- debounceTimer: 0,
27- highlightNodes: [],
28- faded: false,
29- prevOverflow: '',
30- post(msg){ try { window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify(msg)); } catch(_){} },
31- render(){
32- if (!H.el) H.mount();
33- H.el.style.display = H.visible ? 'block' : 'none';
34- if (!H.visible) return;
35- H.input.placeholder = H.mode === 'find' ? 'Find in page…' : 'Type URL or search…';
36- H.input.focus();
37- },
38- setMode(m){ H.mode = m === 'find' ? 'find' : 'omnibox'; H.selectedIndex = -1; H.setFaded(false); H.paintList(); },
39- open(mode, initial){ H.setMode(mode||'omnibox'); H.toggle(true); if (typeof initial==='string') { H.input.value = initial; H.onInput(); } },
40- close(){ if (H.mode==='find') H.clearHighlights(); H.setFaded(false); H.toggle(false); },
41- toggle(v){
42- const newState = (typeof v==='boolean')? v : !H.visible;
43- if (newState && !H.visible) {
44- // Disable page scrolling while overlay is visible
45- H.prevOverflow = document.documentElement.style.overflow;
46- document.documentElement.style.overflow = 'hidden';
47- } else if (!newState && H.visible) {
48- // Restore page scrolling on close
49- document.documentElement.style.overflow = H.prevOverflow || '';
50- H.prevOverflow = '';
51- }
52- H.visible = newState;
53- H.render();
54- },
55- setFaded(v){
56- H.faded = !!v;
57- if (!H.box || !H.input || !H.list) return;
58- if (H.faded) {
59- H.box.style.background = 'rgba(27,27,27,0.25)';
60- H.box.style.backdropFilter = 'blur(2px) saturate(110%)';
61- H.box.style.webkitBackdropFilter = 'blur(2px) saturate(110%)';
62- H.input.style.background = 'rgba(18,18,18,0.35)';
63- H.input.style.color = '#eee';
64- } else {
65- H.box.style.background = '#1b1b1b';
66- H.box.style.backdropFilter = '';
67- H.box.style.webkitBackdropFilter = '';
68- H.input.style.background = '#121212';
69- H.input.style.color = '#eee';
70- }
71- },
72- scrollListToSelection(){
73- if (!H.list) return;
74- let idx = H.selectedIndex|0;
75- if (idx < 0) return;
76- // Account for header in find mode
77- const childIdx = (H.mode==='find' ? 1 : 0) + idx;
78- const el = H.list.children[childIdx];
79- if (el && el.scrollIntoView) {
80- try { el.scrollIntoView({block:'nearest'}); } catch(_) { el.scrollIntoView(); }
81- }
82- },
83- mount(){
84- const root = document.createElement('div');
85- root.id = 'dumber-omnibox-root';
86- root.style.cssText = 'position:fixed;inset:0;z-index:2147483647;display:none;';
87- const box = document.createElement('div');
88- box.style.cssText = 'width:min(90vw, 720px);margin:8vh auto;padding:8px 12px;background:#1b1b1b;color:#eee;border:1px solid #444;border-radius:8px;box-shadow:0 10px 30px rgba(0,0,0,.6);font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,"Helvetica Neue",Arial,sans-serif;';
89- const input = document.createElement('input');
90- input.type = 'text';
91- input.placeholder = 'Type URL or search…';
92- input.style.cssText = 'width:100%;box-sizing:border-box;padding:10px 12px;border-radius:6px;border:1px solid #555;background:#121212;color:#eee;font-size:16px;outline:none;';
93- const list = document.createElement('div');
94- list.style.cssText = 'margin-top:8px;max-height:50vh;overflow:auto;';
95- const style = document.createElement('style');
96- style.textContent = '.dumber-find-highlight{background:#ffeb3b;color:#000;padding:0 1px;border-radius:2px;box-shadow:0 0 0 1px #c8b900 inset}.dumber-find-active{background:#ff9800 !important;color:#000;box-shadow:0 0 0 1px #b36b00 inset}';
97- document.documentElement.appendChild(style);
98- box.appendChild(input); box.appendChild(list); root.appendChild(box); document.documentElement.appendChild(root);
99- H.box = box;
100- // Dynamic style updater for responsive behavior
101- const updateStyles = () => {
102- const vw = window.innerWidth;
103- const isSmall = vw < 640;
104- const isLarge = vw > 1920;
105-
106- if (box) {
107- box.style.width = isSmall ? '95vw' : isLarge ? 'min(90vw, 840px)' : 'min(90vw, 720px)';
108- box.style.padding = isSmall ? '6px 8px' : '8px 12px';
109- }
110-
111- if (input) {
112- input.style.fontSize = isSmall ? '14px' : isLarge ? '17px' : '16px';
113- input.style.padding = isSmall ? '8px 10px' : '10px 12px';
114- input.style.boxSizing = 'border-box';
115- }
116- };
117- updateStyles();
118- window.addEventListener('resize', updateStyles);
119- // Click outside the box closes the overlay
120- root.addEventListener('mousedown', (e)=>{
121- const tgt = e.target;
122- if (tgt && box && !box.contains(tgt)) {
123- e.preventDefault();
124- e.stopPropagation();
125- H.close();
126- }
127- }, true);
128- box.addEventListener('mousedown', (e)=>{ e.stopPropagation(); }, true);
129- // Focus and select input on hover for quick typing
130- input.addEventListener('mouseenter', ()=>{ if (H.visible) { try { input.focus({preventScroll:true}); input.select(); } catch(_) { input.focus(); } } });
131- root.addEventListener('mouseenter', ()=>{ if (H.visible) { try { input.focus({preventScroll:true}); } catch(_) { input.focus(); } H.setFaded(false); } });
132- // Clear fading when user goes back to input typing
133- input.addEventListener('focus', ()=> H.setFaded(false));
134- input.addEventListener('keydown', (e)=>{
135- if (e.key === 'Escape'){ H.close(); }
136- else if (H.mode==='omnibox' && e.key === 'Enter'){
137- const pick = H.suggestions && H.suggestions[H.selectedIndex|0];
138- const v = (pick && pick.url) || input.value || '';
139- if (v) H.post({type:'navigate', url:v});
140- H.toggle(false);
141- } else if (H.mode==='find' && e.key === 'Enter'){
142- e.preventDefault();
143- if (e.shiftKey) {
144- H.jump(-1);
145- } else if (e.altKey) {
146- // Center on current match but keep overlay open for further navigation
147- H.revealSelection();
148- } else {
149- // Default: center and close
150- H.revealSelection();
151- H.close();
152- }
153- } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp'){
154- e.preventDefault(); e.stopPropagation();
155- const n = (H.mode==='find'? H.matches.length : H.suggestions.length);
156- if (n){
157- H.selectedIndex = (H.selectedIndex||0) + (e.key==='ArrowDown'?1:-1);
158- if (H.selectedIndex<0) H.selectedIndex = n-1; if (H.selectedIndex>=n) H.selectedIndex = 0;
159- H.paintList();
160- H.scrollListToSelection();
161- // Fade overlay while navigating results; also reveal active match in find mode
162- H.setFaded(true);
163- if (H.mode==='find') { H.revealSelection(); }
164- }
165- } else {
166- // Any other key input should restore full opacity while editing
167- H.setFaded(false);
168- }
169- });
170- input.addEventListener('input', ()=> { H.onInput(); H.setFaded(false); });
171- input.addEventListener('mousedown', ()=> H.setFaded(false));
172- input.addEventListener('click', ()=> H.setFaded(false));
173- H.el = root; H.input = input; H.list = list; H.selectedIndex = -1; H.paintList();
174- },
175- onInput(){
176- const q = H.input.value || '';
177- if (H.mode === 'omnibox'){
178- clearTimeout(H.debounceTimer);
179- H.debounceTimer = setTimeout(()=> H.post({type:'query', q, limit:10}), 120);
180- } else {
181- H.find(q);
182- }
183- },
184- paintList(){
185- const list = H.list; if (!list) return; list.textContent = '';
186-
187- // Show/hide separator based on content
188- const hasContent = (H.mode==='omnibox' && H.suggestions.length > 0) || (H.mode==='find' && H.matches.length > 0);
189- list.style.borderTop = hasContent ? '1px solid #333' : 'none';
190-
191- if (H.mode==='omnibox'){
192- H.suggestions.forEach((s, i)=>{
193- const item = document.createElement('div');
194- item.style.cssText = 'padding:8px 10px;display:flex;gap:10px;align-items:center;cursor:pointer;border-bottom:1px solid #2a2a2a;'+(i===H.selectedIndex?'background:#0a0a0a;':'');
195- // Favicon chip (wrapper ensures perfect circle sizing)
196- const chip = document.createElement('div');
197- chip.style.cssText = 'flex:0 0 20px;width:20px;height:20px;border-radius:50%;background:#ccc;border:1px solid rgba(0,0,0,.12);box-shadow:0 1px 2px rgba(0,0,0,.12);display:flex;align-items:center;justify-content:center;';
198- const icon = document.createElement('img');
199- icon.src = s.favicon || '';
200- icon.loading = 'lazy';
201- icon.style.cssText = 'width:16px;height:16px;filter:brightness(1.06) contrast(1.03);image-rendering:-webkit-optimize-contrast;';
202- icon.onerror = ()=>{ chip.style.display='none'; };
203- chip.appendChild(icon);
204- // Text line: Domain | full path (one line, fade at end)
205- let domain = '', path = '';
206- try { const u = new URL(s.url, window.location.href); domain = u.hostname; path = (u.pathname||'') + (u.search||'') + (u.hash||''); } catch(_) { domain = s.url || ''; }
207- const text = document.createElement('div');
208- text.style.cssText = 'flex:1;min-width:0;display:flex;gap:8px;align-items:center;white-space:nowrap;overflow:hidden;';
209- // Apply gradient fade using mask for WebKit
210- text.style.webkitMaskImage = 'linear-gradient(90deg, black 85%, transparent 100%)';
211- text.style.maskImage = 'linear-gradient(90deg, black 85%, transparent 100%)';
212- const domainEl = document.createElement('span'); domainEl.textContent = domain; domainEl.style.cssText = 'color:#e6e6e6;opacity:.95;';
213- const sep = document.createElement('span'); sep.textContent = ' | '; sep.style.cssText = 'color:#777;';
214- const pathEl = document.createElement('span'); pathEl.textContent = path || '/'; pathEl.style.cssText = 'color:#9ad;';
215- text.appendChild(domainEl); text.appendChild(sep); text.appendChild(pathEl);
216- item.appendChild(chip); item.appendChild(text);
217- item.addEventListener('mouseenter', ()=>{ H.selectedIndex = i; H.paintList(); H.scrollListToSelection(); H.setFaded(true); });
218- item.addEventListener('click',()=>{ H.post({type:'navigate', url:s.url}); H.toggle(false); });
219- list.appendChild(item);
220- });
221- } else {
222- const total = H.matches.length;
223- const header = document.createElement('div');
224- header.textContent = total ? total + ' matches' : 'No matches';
225- header.style.cssText = 'padding:6px 10px;color:#bbb;font-size:12px;border-bottom:1px solid #2a2a2a;';
226- list.appendChild(header);
227- H.matches.forEach((m, i)=>{
228- const item = document.createElement('div');
229- item.style.cssText = 'padding:8px 10px;cursor:pointer;border-bottom:1px solid #2a2a2a;'+(i===H.selectedIndex?'background:#0a0a0a;':'');
230- const ctx = document.createElement('div'); ctx.textContent = m.context || ''; ctx.style.cssText = 'color:#ddd;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
231- item.appendChild(ctx);
232- item.addEventListener('mouseenter', ()=>{ H.selectedIndex = i; H.paintList(); H.scrollListToSelection(); H.setFaded(true); });
233- item.addEventListener('click',()=>{ H.selectedIndex=i; H.revealSelection(); H.paintList(); H.close(); });
234- list.appendChild(item);
235- });
236- }
237- },
238- setSuggestions(arr){ H.suggestions = Array.isArray(arr)? arr: []; H.selectedIndex = -1; H.paintList(); },
239- // FIND MODE IMPLEMENTATION
240- clearHighlights(){
241- try {
242- H.highlightNodes.forEach(({span, text})=>{
243- const p = span.parentNode; if (!p) return; p.replaceChild(text, span); p.normalize();
244- });
245- } catch(_){}
246- H.highlightNodes = [];
247- H.matches = [];
248- H.selectedIndex = -1;
249- H.activeIndex = -1;
250- H.paintList();
251- },
252- find(q){
253- H.clearHighlights();
254- q = (q||'').trim(); if (!q) return;
255- const body = document.body; if (!body) return;
256- const root = document.getElementById('dumber-omnibox-root');
257- const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, {
258- acceptNode(node){
259- if (!node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
260- let el = node.parentElement; if (!el) return NodeFilter.FILTER_REJECT;
261- if (root && root.contains(el)) return NodeFilter.FILTER_REJECT;
262- const name = el.tagName; if (name==='SCRIPT'||name==='STYLE'||name==='NOSCRIPT') return NodeFilter.FILTER_REJECT;
263- if (getComputedStyle(el).visibility==='hidden' || getComputedStyle(el).display==='none') return NodeFilter.FILTER_REJECT;
264- return NodeFilter.FILTER_ACCEPT;
265- }
266- });
267- const lc = q.toLowerCase();
268- while (walker.nextNode()){
269- const text = walker.currentNode;
270- let s = text.nodeValue; let i = 0;
271- while (s && (i = s.toLowerCase().indexOf(lc)) !== -1) {
272- const before = document.createTextNode(s.slice(0, i));
273- const match = document.createTextNode(s.slice(i, i+q.length));
274- const afterVal = s.slice(i+q.length);
275- const span = document.createElement('span'); span.className = 'dumber-find-highlight'; span.appendChild(match);
276- const parent = text.parentNode; if (!parent) break;
277- parent.insertBefore(before, text);
278- parent.insertBefore(span, text);
279- text.nodeValue = afterVal;
280- H.highlightNodes.push({span, text: match});
281- const leftCtx = before.nodeValue.slice(-30);
282- let rightRaw = afterVal.slice(0, 30);
283- const boundaryIdx = rightRaw.search(/[\.,;:\-]/);
284- if (boundaryIdx !== -1) {
285- rightRaw = rightRaw.slice(0, boundaryIdx+1);
286- }
287- const context = (leftCtx + match.nodeValue + rightRaw).replace(/\s+/g,' ').trim();
288- H.matches.push({el: span, context});
289- if (H.matches.length >= MAX_MATCHES) break;
290- s = afterVal;
291- }
292- if (H.matches.length >= MAX_MATCHES) break;
293- }
294- H.selectedIndex = H.matches.length ? 0 : -1;
295- H.paintList();
296- H.revealSelection();
297- },
298- revealSelection(){
299- const prev = H.matches[H.activeIndex|0];
300- if (prev && prev.el && prev.el.classList) { prev.el.classList.remove('dumber-find-active'); }
301- const m = H.matches[H.selectedIndex|0]; if (!m) return;
302- if (m.el && m.el.classList) { m.el.classList.add('dumber-find-active'); }
303- H.activeIndex = H.selectedIndex|0;
304- try { m.el.scrollIntoView({block:'center', inline:'nearest'}); } catch(_){ m.el.scrollIntoView(); }
305- },
306- jump(delta){
307- const n = H.matches.length; if (!n) return;
308- H.selectedIndex = ((H.selectedIndex||0) + delta) % n; if (H.selectedIndex<0) H.selectedIndex = n-1;
309- H.paintList(); H.revealSelection();
310- }
311- };
312-
313- // Keyboard hooks
314- window.addEventListener('keydown', (e)=>{
315- const mod = (e.ctrlKey||e.metaKey);
316- if (mod && (e.key==='l' || e.key==='L')) { e.preventDefault(); H.open('omnibox'); }
317- if (mod && (e.key==='f' || e.key==='F')) { e.preventDefault(); H.open('find'); }
318- }, true);
319-
320- // Public API for native
321- window.__dumber_setSuggestions = (arr)=> H.setSuggestions(arr);
322- window.__dumber_toggle = ()=> H.toggle();
323- window.__dumber_find_open = (q)=> H.open('find', q||'');
324- window.__dumber_find_close = ()=> H.close();
325- window.__dumber_find_query = (q)=> { if (H.mode!=='find') H.setMode('find'); if (!H.visible) H.toggle(true); H.input.value = q||''; H.find(q||''); };
326- } catch (e) { /* no-op */ }
327-})();`
328-}

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