feat: implement Versionen, Teilen, Hilfe, Benachrichtigungen topbar features
This commit is contained in:
@@ -18,6 +18,9 @@ import StatusBar from './components/StatusBar';
|
||||
import MobileDrawers from './components/MobileDrawers';
|
||||
import BackgroundImport from './components/BackgroundImport';
|
||||
import HistoryPanel from './components/HistoryPanel';
|
||||
import ShareDialog from './components/ShareDialog';
|
||||
import HelpPanel from './components/HelpPanel';
|
||||
import NotificationPanel from './components/NotificationPanel';
|
||||
import { BackgroundService, type BackgroundConfig } from './services/backgroundService';
|
||||
import { HistoryManager, type CADStateSnapshot, type HistoryEntry } from './history';
|
||||
import { getCommandRegistry } from './services/commandRegistry';
|
||||
@@ -162,6 +165,9 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
||||
|
||||
// History panel
|
||||
const [historyPanelOpen, setHistoryPanelOpen] = useState(false);
|
||||
const [shareOpen, setShareOpen] = useState(false);
|
||||
const [helpOpen, setHelpOpen] = useState(false);
|
||||
const [notifOpen, setNotifOpen] = useState(false);
|
||||
|
||||
// Elements + undo/redo (HistoryManager)
|
||||
const [elements, setElements] = useState<CADElement[]>([]);
|
||||
@@ -947,6 +953,10 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
||||
theme={theme}
|
||||
onOpenLeftDrawer={() => setMobileLeftOpen(true)}
|
||||
onNavigateBack={onNavigateBack}
|
||||
onOpenHistory={() => setHistoryPanelOpen(true)}
|
||||
onOpenShare={() => setShareOpen(true)}
|
||||
onOpenHelp={() => setHelpOpen(true)}
|
||||
onOpenNotifications={() => setNotifOpen(true)}
|
||||
/>
|
||||
<RibbonBar
|
||||
activeTab={activeRibbonTab}
|
||||
@@ -1120,6 +1130,19 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
||||
onClose={() => setHistoryPanelOpen(false)}
|
||||
/>
|
||||
)}
|
||||
<ShareDialog
|
||||
open={shareOpen}
|
||||
onClose={() => setShareOpen(false)}
|
||||
projectName={projectName}
|
||||
/>
|
||||
<HelpPanel
|
||||
open={helpOpen}
|
||||
onClose={() => setHelpOpen(false)}
|
||||
/>
|
||||
<NotificationPanel
|
||||
open={notifOpen}
|
||||
onClose={() => setNotifOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import type { HelpPanelProps } from '../types/ui.types';
|
||||
|
||||
const shortcuts: { key: string; label: string }[] = [
|
||||
{ key: 'V', label: 'Auswählen' },
|
||||
{ key: 'L', label: 'Linie' },
|
||||
{ key: 'C', label: 'Kreis' },
|
||||
{ key: 'R', label: 'Rechteck' },
|
||||
{ key: 'T', label: 'Text' },
|
||||
{ key: 'P', label: 'Pan' },
|
||||
{ key: 'Z', label: 'Zoom' },
|
||||
{ key: 'M', label: 'Messen' },
|
||||
{ key: 'Strg+Z', label: 'Rückgängig' },
|
||||
{ key: 'Strg+Y', label: 'Wiederherstellen' },
|
||||
{ key: 'Entf', label: 'Löschen' },
|
||||
{ key: 'Strg+C', label: 'Kopieren' },
|
||||
{ key: 'Strg+V', label: 'Einfügen' },
|
||||
];
|
||||
|
||||
const quickStart: { step: number; text: string }[] = [
|
||||
{ step: 1, text: 'Projekt öffnen oder neues Projekt erstellen' },
|
||||
{ step: 2, text: 'Werkzeug aus der linken Sidebar wählen' },
|
||||
{ step: 3, text: 'Auf der Zeichenfläche klicken und zeichnen' },
|
||||
{ step: 4, text: 'Projekt speichern mit Strg+S' },
|
||||
];
|
||||
|
||||
const HelpPanel: React.FC<HelpPanelProps> = ({ open, onClose }) => {
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop" onClick={onClose}>
|
||||
<div className="modal-panel help-panel" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<h2>Hilfe</h2>
|
||||
<button className="modal-close" aria-label="Schließen" onClick={onClose}>
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<section className="help-section">
|
||||
<h3>Tastenkürzel</h3>
|
||||
<div className="help-shortcuts-grid">
|
||||
{shortcuts.map((s) => (
|
||||
<div key={s.key} className="help-shortcut-row">
|
||||
<kbd className="help-key">{s.key}</kbd>
|
||||
<span className="help-label">{s.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<section className="help-section">
|
||||
<h3>Schnellstart</h3>
|
||||
<ol className="help-quickstart">
|
||||
{quickStart.map((q) => (
|
||||
<li key={q.step} className="help-quickstart-item">
|
||||
<span className="help-step-num">{q.step}</span>
|
||||
<span>{q.text}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HelpPanel;
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import type { NotificationPanelProps } from '../types/ui.types';
|
||||
|
||||
interface Notification {
|
||||
id: string;
|
||||
icon: string;
|
||||
title: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
const initialNotifications: Notification[] = [
|
||||
{ id: 'n1', icon: '💾', title: 'Projekt automatisch gespeichert', timestamp: 'vor 2 Min.' },
|
||||
{ id: 'n2', icon: '🔄', title: 'Neue Version verfügbar', timestamp: 'vor 15 Min.' },
|
||||
{ id: 'n3', icon: '👤', title: 'Benutzer ist beigetreten', timestamp: 'vor 1 Std.' },
|
||||
];
|
||||
|
||||
const NotificationPanel: React.FC<NotificationPanelProps> = ({ open, onClose }) => {
|
||||
const [notifications, setNotifications] = useState<Notification[]>(initialNotifications);
|
||||
const [allRead, setAllRead] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [open, onClose]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const handleDismiss = (id: string) => {
|
||||
setNotifications((prev) => prev.filter((n) => n.id !== id));
|
||||
};
|
||||
|
||||
const handleMarkAllRead = () => {
|
||||
setAllRead(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="notif-panel" ref={ref}>
|
||||
<div className="notif-header">
|
||||
<h3>Benachrichtigungen</h3>
|
||||
<button className="notif-close" aria-label="Schließen" onClick={onClose}>
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="notif-list">
|
||||
{notifications.length === 0 ? (
|
||||
<p className="notif-empty">Keine Benachrichtigungen</p>
|
||||
) : (
|
||||
notifications.map((n) => (
|
||||
<div key={n.id} className={`notif-item ${allRead ? 'read' : ''}`}>
|
||||
<span className="notif-icon">{n.icon}</span>
|
||||
<div className="notif-content">
|
||||
<span className="notif-title">{n.title}</span>
|
||||
<span className="notif-time">{n.timestamp}</span>
|
||||
</div>
|
||||
<button className="notif-dismiss" aria-label="Entfernen" onClick={() => handleDismiss(n.id)}>
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{notifications.length > 0 && (
|
||||
<button className="notif-mark-all" onClick={handleMarkAllRead}>
|
||||
Alle als gelesen markieren
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationPanel;
|
||||
@@ -0,0 +1,71 @@
|
||||
import React, { useState } from 'react';
|
||||
import type { ShareDialogProps } from '../types/ui.types';
|
||||
|
||||
const ShareDialog: React.FC<ShareDialogProps> = ({ open, onClose, projectName }) => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [invited, setInvited] = useState(false);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const shareLink = `${window.location.origin}/?project=${encodeURIComponent(projectName)}`;
|
||||
|
||||
const handleCopyLink = () => {
|
||||
navigator.clipboard.writeText(shareLink).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
});
|
||||
};
|
||||
|
||||
const handleInvite = () => {
|
||||
if (email.trim()) {
|
||||
setInvited(true);
|
||||
setEmail('');
|
||||
setTimeout(() => setInvited(false), 3000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop" onClick={onClose}>
|
||||
<div className="modal-panel share-dialog" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<h2>Projekt teilen</h2>
|
||||
<button className="modal-close" aria-label="Schließen" onClick={onClose}>
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<p className="share-project-name">{projectName}</p>
|
||||
<div className="share-section">
|
||||
<label className="share-label">Link kopieren</label>
|
||||
<div className="share-link-row">
|
||||
<input className="share-link-input" type="text" readOnly value={shareLink} />
|
||||
<button className="share-copy-btn" onClick={handleCopyLink}>
|
||||
{copied ? 'Kopiert!' : 'Kopieren'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="share-section">
|
||||
<label className="share-label">Per E-Mail einladen</label>
|
||||
<div className="share-email-row">
|
||||
<input
|
||||
className="share-email-input"
|
||||
type="email"
|
||||
placeholder="name@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleInvite()}
|
||||
/>
|
||||
<button className="share-invite-btn" onClick={handleInvite} disabled={!email.trim()}>
|
||||
Einladen
|
||||
</button>
|
||||
</div>
|
||||
{invited && <p className="share-invited-msg">Einladung gesendet!</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShareDialog;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import type { TopbarProps } from '../types/ui.types';
|
||||
|
||||
const Topbar: React.FC<TopbarProps> = ({ projectName, savedStatus, onUndo, onRedo, onThemeToggle, theme, onOpenSettings, onOpenLeftDrawer, onNavigateBack }) => {
|
||||
const Topbar: React.FC<TopbarProps> = ({ projectName, savedStatus, onUndo, onRedo, onThemeToggle, theme, onOpenSettings, onOpenLeftDrawer, onNavigateBack, onOpenHistory, onOpenShare, onOpenHelp, onOpenNotifications }) => {
|
||||
return (
|
||||
<header className="topbar" role="banner">
|
||||
<div className="topbar-left">
|
||||
@@ -28,17 +28,17 @@ const Topbar: React.FC<TopbarProps> = ({ projectName, savedStatus, onUndo, onRed
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 7v6h-6"/><path d="M3 17a9 9 0 0 1 15-6.7L21 13"/></svg>
|
||||
</button>
|
||||
<span style={{ width: '1px', height: '20px', background: 'var(--color-border)', margin: '0 4px' }}></span>
|
||||
<button className="icon-btn-top" aria-label="Versionen" title="Versionsverlauf" onClick={() => console.log('Versionen clicked')}>
|
||||
<button className="icon-btn-top" aria-label="Versionen" title="Versionsverlauf" onClick={onOpenHistory}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
</button>
|
||||
<button className="icon-btn-top" aria-label="Teilen" title="Projekt teilen" onClick={() => console.log('Teilen clicked')}>
|
||||
<button className="icon-btn-top" aria-label="Teilen" title="Projekt teilen" onClick={onOpenShare}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>
|
||||
</button>
|
||||
<span style={{ width: '1px', height: '20px', background: 'var(--color-border)', margin: '0 4px' }}></span>
|
||||
<button className="icon-btn-top" aria-label="Hell/Dunkel umschalten" title="Theme wechseln" onClick={onThemeToggle}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>
|
||||
</button>
|
||||
<button className="icon-btn-top" aria-label="Hilfe" title="Hilfe (F1)" onClick={() => console.log('Hilfe clicked')}>
|
||||
<button className="icon-btn-top" aria-label="Hilfe" title="Hilfe (F1)" onClick={onOpenHelp}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||
</button>
|
||||
<span style={{ width: '1px', height: '20px', background: 'var(--color-border)', margin: '0 4px' }}></span>
|
||||
@@ -46,7 +46,7 @@ const Topbar: React.FC<TopbarProps> = ({ projectName, savedStatus, onUndo, onRed
|
||||
<option value="de">DE</option>
|
||||
<option value="en">EN</option>
|
||||
</select>
|
||||
<button className="icon-btn-top" aria-label="Benachrichtigungen" title="Benachrichtigungen" onClick={() => console.log('Benachrichtigungen clicked')}>
|
||||
<button className="icon-btn-top" aria-label="Benachrichtigungen" title="Benachrichtigungen" onClick={onOpenNotifications}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>
|
||||
</button>
|
||||
<button className="icon-btn-top" aria-label="Einstellungen" title="Einstellungen" onClick={onOpenSettings}>
|
||||
|
||||
@@ -2010,3 +2010,304 @@ body.drawer-open { overflow: hidden; }
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* ─── Modal shared styles ─────────────────────────────── */
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.modal-panel {
|
||||
background: var(--color-surface);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-drawer, 0 8px 32px rgba(0,0,0,0.2));
|
||||
max-width: 480px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.modal-header h2 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-muted);
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.modal-close:hover {
|
||||
background: var(--color-surface-2);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* ─── ShareDialog ──────────────────────────────────────── */
|
||||
.share-dialog .share-project-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
.share-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.share-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.share-link-row, .share-email-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.share-link-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text);
|
||||
background: var(--color-surface-2);
|
||||
}
|
||||
.share-copy-btn, .share-invite-btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
background: var(--color-primary, #2563eb);
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.share-copy-btn:hover, .share-invite-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.share-invite-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.share-email-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text);
|
||||
background: var(--color-surface);
|
||||
}
|
||||
.share-invited-msg {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #16a34a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ─── HelpPanel ────────────────────────────────────────── */
|
||||
.help-panel {
|
||||
max-width: 420px;
|
||||
}
|
||||
.help-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.help-section h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
.help-shortcuts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
.help-shortcut-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.help-key {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 48px;
|
||||
padding: 3px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
background: var(--color-surface-2);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
font-family: monospace;
|
||||
}
|
||||
.help-label {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.help-quickstart {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.help-quickstart-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.help-step-num {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: var(--color-primary, #2563eb);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ─── NotificationPanel ───────────────────────────────── */
|
||||
.notif-panel {
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
right: 16px;
|
||||
width: 320px;
|
||||
max-width: calc(100vw - 32px);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.15);
|
||||
z-index: 1001;
|
||||
overflow: hidden;
|
||||
}
|
||||
.notif-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.notif-header h3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
.notif-close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-muted);
|
||||
padding: 2px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.notif-close:hover {
|
||||
background: var(--color-surface-2);
|
||||
}
|
||||
.notif-list {
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.notif-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.notif-item.read {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.notif-icon {
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.notif-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.notif-title {
|
||||
font-size: 13px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
.notif-time {
|
||||
font-size: 11px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.notif-dismiss {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-muted);
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
}
|
||||
.notif-dismiss:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
.notif-empty {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.notif-mark-all {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
background: var(--color-surface-2);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--color-primary, #2563eb);
|
||||
cursor: pointer;
|
||||
}
|
||||
.notif-mark-all:hover {
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
/* ─── Responsive: modals on mobile ──────────────────────── */
|
||||
@media (max-width: 768px) {
|
||||
.modal-panel {
|
||||
width: 95%;
|
||||
max-height: 85vh;
|
||||
}
|
||||
.help-shortcuts-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.notif-panel {
|
||||
right: 8px;
|
||||
left: 8px;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,26 @@ export interface TopbarProps {
|
||||
onOpenSettings?: () => void;
|
||||
onOpenLeftDrawer?: () => void;
|
||||
onNavigateBack?: () => void;
|
||||
onOpenHistory?: () => void;
|
||||
onOpenShare?: () => void;
|
||||
onOpenHelp?: () => void;
|
||||
onOpenNotifications?: () => void;
|
||||
}
|
||||
|
||||
export interface ShareDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
projectName: string;
|
||||
}
|
||||
|
||||
export interface HelpPanelProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export interface NotificationPanelProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export interface SettingsModalProps {
|
||||
|
||||
Reference in New Issue
Block a user