feat: Add notifications bell with GBAlerts infrastructure for all apps
- Added bell icon in header toolbar with badge counter - Created GBAlerts global API for notifications from any app - App-specific shortcuts: taskCompleted, newEmail, newChat, driveSync, calendarReminder - Integrated task completion with bell notifications - Added CSS for notifications panel and items - Documented in PROMPT.md Design System section
This commit is contained in:
parent
0f658fd7c5
commit
511cc24e4d
4 changed files with 478 additions and 0 deletions
29
PROMPT.md
29
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
|
||||
|
|
|
|||
|
|
@ -333,6 +333,62 @@
|
|||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Notifications Bell -->
|
||||
<div class="notifications-menu">
|
||||
<button
|
||||
class="header-btn notifications-btn"
|
||||
id="notifications-btn"
|
||||
aria-label="Notifications"
|
||||
aria-expanded="false"
|
||||
onclick="toggleNotificationsPanel()"
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"
|
||||
/>
|
||||
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
|
||||
</svg>
|
||||
<span
|
||||
class="notifications-badge"
|
||||
id="notifications-badge"
|
||||
style="display: none"
|
||||
>0</span
|
||||
>
|
||||
</button>
|
||||
<div
|
||||
class="notifications-panel"
|
||||
id="notifications-panel"
|
||||
role="menu"
|
||||
>
|
||||
<div class="notifications-panel-header">
|
||||
<span class="notifications-panel-title"
|
||||
>Notifications</span
|
||||
>
|
||||
<button
|
||||
class="notifications-clear-btn"
|
||||
onclick="clearAllNotifications()"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
</div>
|
||||
<div class="notifications-list" id="notifications-list">
|
||||
<div class="notifications-empty">
|
||||
<span>🔔</span>
|
||||
<p>No new notifications</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Panel (Gear Icon) -->
|
||||
<div class="settings-menu">
|
||||
<button
|
||||
|
|
@ -933,6 +989,192 @@
|
|||
}
|
||||
};
|
||||
|
||||
// ========== NOTIFICATIONS BELL INFRASTRUCTURE ==========
|
||||
// Global alert system - use from any app (drive, chat, email, tasks, etc.)
|
||||
|
||||
window.GBAlerts = {
|
||||
notifications: [],
|
||||
sound: new Audio('/assets/sounds/notification.mp3'),
|
||||
|
||||
// Add a notification to the bell
|
||||
add: function(title, message, type = 'info', appIcon = '🔔') {
|
||||
const notification = {
|
||||
id: Date.now(),
|
||||
title: title,
|
||||
message: message,
|
||||
type: type,
|
||||
appIcon: appIcon,
|
||||
timestamp: new Date(),
|
||||
read: false
|
||||
};
|
||||
|
||||
this.notifications.unshift(notification);
|
||||
this.updateBadge();
|
||||
this.renderNotifications();
|
||||
this.playSound();
|
||||
|
||||
// Also show toast notification
|
||||
window.showNotification(`${appIcon} ${title}: ${message}`, type, 5000);
|
||||
|
||||
return notification.id;
|
||||
},
|
||||
|
||||
// App-specific shortcuts
|
||||
taskCompleted: function(taskName, appUrl) {
|
||||
this.add('Task Completed', `"${taskName}" is ready`, 'success', '✅');
|
||||
if (appUrl) {
|
||||
// Store app URL for quick access
|
||||
this.notifications[0].actionUrl = appUrl;
|
||||
this.notifications[0].actionLabel = 'Open App';
|
||||
}
|
||||
},
|
||||
|
||||
newEmail: function(from, subject) {
|
||||
this.add('New Email', `From ${from}: ${subject}`, 'info', '📧');
|
||||
},
|
||||
|
||||
newChat: function(from, preview) {
|
||||
this.add('New Message', `${from}: ${preview}`, 'info', '💬');
|
||||
},
|
||||
|
||||
driveSync: function(filename, action) {
|
||||
this.add('Drive', `${filename} ${action}`, 'info', '📁');
|
||||
},
|
||||
|
||||
calendarReminder: function(event, time) {
|
||||
this.add('Reminder', `${event} in ${time}`, 'warning', '📅');
|
||||
},
|
||||
|
||||
error: function(source, message) {
|
||||
this.add(`Error in ${source}`, message, 'error', '⚠️');
|
||||
},
|
||||
|
||||
// Update badge count
|
||||
updateBadge: function() {
|
||||
const badge = document.getElementById('notifications-badge');
|
||||
const unreadCount = this.notifications.filter(n => !n.read).length;
|
||||
|
||||
if (badge) {
|
||||
badge.textContent = unreadCount > 99 ? '99+' : unreadCount;
|
||||
badge.style.display = unreadCount > 0 ? 'flex' : 'none';
|
||||
}
|
||||
},
|
||||
|
||||
// Render notifications list
|
||||
renderNotifications: function() {
|
||||
const list = document.getElementById('notifications-list');
|
||||
if (!list) return;
|
||||
|
||||
if (this.notifications.length === 0) {
|
||||
list.innerHTML = `
|
||||
<div class="notifications-empty">
|
||||
<span>🔔</span>
|
||||
<p>No new notifications</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = this.notifications.slice(0, 20).map(n => `
|
||||
<div class="notification-item ${n.read ? 'read' : 'unread'} ${n.type}" data-id="${n.id}" onclick="GBAlerts.markAsRead(${n.id})">
|
||||
<div class="notification-item-icon">${n.appIcon}</div>
|
||||
<div class="notification-item-content">
|
||||
<div class="notification-item-title">${n.title}</div>
|
||||
<div class="notification-item-message">${n.message}</div>
|
||||
<div class="notification-item-time">${this.formatTime(n.timestamp)}</div>
|
||||
</div>
|
||||
${n.actionUrl ? `<a href="${n.actionUrl}" class="notification-item-action" onclick="event.stopPropagation()">${n.actionLabel || 'Open'}</a>` : ''}
|
||||
<button class="notification-item-dismiss" onclick="event.stopPropagation(); GBAlerts.remove(${n.id})">×</button>
|
||||
</div>
|
||||
`).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,
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
/* ============================================ */
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue