diff --git a/PROMPT.md b/PROMPT.md index 965e3cc..b6b2ee8 100644 --- a/PROMPT.md +++ b/PROMPT.md @@ -877,6 +877,35 @@ When creating a new screen, ensure it has: - [ ] Fixed terminal/status panels at bottom - [ ] Variable content area with internal scroll +### Alert Infrastructure (Bell Notifications) + +Use `window.GBAlerts` for app notifications that appear in the global bell icon: + +```javascript +// Task completed (with optional app URL) +window.GBAlerts.taskCompleted("My App", "/apps/my-app/"); + +// Email notification +window.GBAlerts.newEmail("john@example.com", "Meeting tomorrow"); + +// Chat message +window.GBAlerts.newChat("John", "Hey, are you there?"); + +// Drive sync +window.GBAlerts.driveSync("report.pdf", "uploaded"); + +// Calendar reminder +window.GBAlerts.calendarReminder("Team Meeting", "15 minutes"); + +// Error +window.GBAlerts.error("Drive", "Failed to sync file"); + +// Generic notification +window.GBAlerts.add("Title", "Message", "success", "🎉"); +``` + +All notifications appear in the bell dropdown with sound (if enabled). + --- ## Remember diff --git a/ui/suite/base.html b/ui/suite/base.html index 06a37cd..2670eae 100644 --- a/ui/suite/base.html +++ b/ui/suite/base.html @@ -333,6 +333,62 @@ + +
+ + +
+
+
+ `).join(''); + }, + + // Format timestamp + formatTime: function(date) { + const now = new Date(); + const diff = now - date; + + if (diff < 60000) return 'Just now'; + if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`; + if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`; + return date.toLocaleDateString(); + }, + + // Mark as read + markAsRead: function(id) { + const notification = this.notifications.find(n => n.id === id); + if (notification) { + notification.read = true; + this.updateBadge(); + this.renderNotifications(); + + if (notification.actionUrl) { + window.location.href = notification.actionUrl; + } + } + }, + + // Remove notification + remove: function(id) { + this.notifications = this.notifications.filter(n => n.id !== id); + this.updateBadge(); + this.renderNotifications(); + }, + + // Clear all + clearAll: function() { + this.notifications = []; + this.updateBadge(); + this.renderNotifications(); + }, + + // Play notification sound + playSound: function() { + const soundEnabled = localStorage.getItem('gb-sound') !== 'false'; + if (soundEnabled && this.sound) { + this.sound.volume = 0.3; + this.sound.play().catch(() => {}); + } + } + }; + + // Toggle notifications panel + function toggleNotificationsPanel() { + const panel = document.getElementById('notifications-panel'); + const btn = document.getElementById('notifications-btn'); + + if (panel.classList.contains('show')) { + panel.classList.remove('show'); + btn.setAttribute('aria-expanded', 'false'); + } else { + // Close other panels first + document.getElementById('settings-panel')?.classList.remove('show'); + document.getElementById('apps-dropdown')?.classList.remove('show'); + + panel.classList.add('show'); + btn.setAttribute('aria-expanded', 'true'); + + // Render latest notifications + window.GBAlerts.renderNotifications(); + } + } + + function clearAllNotifications() { + window.GBAlerts.clearAll(); + } + + // Close notifications panel when clicking outside + document.addEventListener('click', function(e) { + const panel = document.getElementById('notifications-panel'); + const btn = document.getElementById('notifications-btn'); + + if (panel && !panel.contains(e.target) && !btn.contains(e.target)) { + panel.classList.remove('show'); + btn?.setAttribute('aria-expanded', 'false'); + } + }); + }; + // Global HTMX error handling with retry mechanism const htmxRetryConfig = { maxRetries: 3, diff --git a/ui/suite/css/app.css b/ui/suite/css/app.css index 237ff38..6b9a923 100644 --- a/ui/suite/css/app.css +++ b/ui/suite/css/app.css @@ -919,6 +919,208 @@ body { } } +/* ============================================ */ +/* NOTIFICATIONS BELL & PANEL */ +/* ============================================ */ + +.notifications-menu { + position: relative; +} + +.notifications-btn { + position: relative; +} + +.notifications-badge { + position: absolute; + top: -4px; + right: -4px; + min-width: 18px; + height: 18px; + padding: 0 5px; + background: var(--error, #ef4444); + color: white; + font-size: 10px; + font-weight: 700; + border-radius: 9px; + display: flex; + align-items: center; + justify-content: center; +} + +.notifications-panel { + position: absolute; + top: calc(100% + 8px); + right: 0; + width: 360px; + max-height: 480px; + background: var(--surface, #161616); + border: 1px solid var(--border, #2a2a2a); + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + display: none; + flex-direction: column; + z-index: 1000; + overflow: hidden; +} + +.notifications-panel.show { + display: flex; +} + +.notifications-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + border-bottom: 1px solid var(--border, #2a2a2a); +} + +.notifications-panel-title { + font-size: 14px; + font-weight: 600; + color: var(--text, #fff); +} + +.notifications-clear-btn { + font-size: 12px; + color: var(--text-secondary, #888); + background: none; + border: none; + cursor: pointer; + padding: 4px 8px; + border-radius: 4px; + transition: all 0.2s; +} + +.notifications-clear-btn:hover { + background: var(--surface-hover, #1e1e1e); + color: var(--primary, #c5f82a); +} + +.notifications-list { + flex: 1; + overflow-y: auto; + max-height: 400px; +} + +.notifications-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 20px; + color: var(--text-secondary, #888); +} + +.notifications-empty span { + font-size: 32px; + margin-bottom: 12px; + opacity: 0.5; +} + +.notifications-empty p { + font-size: 13px; + margin: 0; +} + +.notification-item { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 12px 16px; + border-bottom: 1px solid var(--border, #2a2a2a); + cursor: pointer; + transition: background 0.2s; +} + +.notification-item:hover { + background: var(--surface-hover, #1e1e1e); +} + +.notification-item.unread { + background: rgba(197, 248, 42, 0.05); +} + +.notification-item.success .notification-item-icon { + color: var(--success, #22c55e); +} + +.notification-item.error .notification-item-icon { + color: var(--error, #ef4444); +} + +.notification-item.warning .notification-item-icon { + color: var(--warning, #f59e0b); +} + +.notification-item-icon { + font-size: 20px; + flex-shrink: 0; + width: 28px; + text-align: center; +} + +.notification-item-content { + flex: 1; + min-width: 0; +} + +.notification-item-title { + font-size: 13px; + font-weight: 600; + color: var(--text, #fff); + margin-bottom: 2px; +} + +.notification-item-message { + font-size: 12px; + color: var(--text-secondary, #888); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.notification-item-time { + font-size: 11px; + color: var(--text-tertiary, #666); + margin-top: 4px; +} + +.notification-item-action { + font-size: 11px; + color: var(--primary, #c5f82a); + text-decoration: none; + padding: 4px 8px; + border-radius: 4px; + background: rgba(197, 248, 42, 0.1); + white-space: nowrap; +} + +.notification-item-action:hover { + background: rgba(197, 248, 42, 0.2); +} + +.notification-item-dismiss { + font-size: 16px; + color: var(--text-tertiary, #666); + background: none; + border: none; + cursor: pointer; + padding: 4px; + line-height: 1; + opacity: 0; + transition: opacity 0.2s; +} + +.notification-item:hover .notification-item-dismiss { + opacity: 1; +} + +.notification-item-dismiss:hover { + color: var(--error, #ef4444); +} + /* ============================================ */ /* PRINT STYLES */ /* ============================================ */ diff --git a/ui/suite/tasks/tasks.js b/ui/suite/tasks/tasks.js index 25c4d09..976ac49 100644 --- a/ui/suite/tasks/tasks.js +++ b/ui/suite/tasks/tasks.js @@ -2526,6 +2526,11 @@ function onTaskCompleted(data, appUrl) { const title = data.title || data.message || "Task"; const taskId = data.task_id || data.id; + // Add to bell notifications using global GBAlerts infrastructure + if (window.GBAlerts) { + window.GBAlerts.taskCompleted(title, appUrl); + } + if (appUrl) { showToast(`App ready! Click to open: ${appUrl}`, "success", 10000, () => { window.open(appUrl, "_blank");