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
|
- [ ] Fixed terminal/status panels at bottom
|
||||||
- [ ] Variable content area with internal scroll
|
- [ ] 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
|
## Remember
|
||||||
|
|
|
||||||
|
|
@ -333,6 +333,62 @@
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</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) -->
|
<!-- Settings Panel (Gear Icon) -->
|
||||||
<div class="settings-menu">
|
<div class="settings-menu">
|
||||||
<button
|
<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
|
// Global HTMX error handling with retry mechanism
|
||||||
const htmxRetryConfig = {
|
const htmxRetryConfig = {
|
||||||
maxRetries: 3,
|
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 */
|
/* PRINT STYLES */
|
||||||
/* ============================================ */
|
/* ============================================ */
|
||||||
|
|
|
||||||
|
|
@ -2526,6 +2526,11 @@ function onTaskCompleted(data, appUrl) {
|
||||||
const title = data.title || data.message || "Task";
|
const title = data.title || data.message || "Task";
|
||||||
const taskId = data.task_id || data.id;
|
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) {
|
if (appUrl) {
|
||||||
showToast(`App ready! Click to open: ${appUrl}`, "success", 10000, () => {
|
showToast(`App ready! Click to open: ${appUrl}`, "success", 10000, () => {
|
||||||
window.open(appUrl, "_blank");
|
window.open(appUrl, "_blank");
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue