1//go:build webkit_cgo
2
3package webkit
4
5// getToastScript returns the injected toast notification component script.
6// Provides a reusable toast system that can display messages across all pages.
7func 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}