feat: implement Versionen, Teilen, Hilfe, Benachrichtigungen topbar features

This commit is contained in:
Agent Zero
2026-06-28 11:26:13 +02:00
parent 2552467476
commit 5d410f7a5b
7 changed files with 566 additions and 5 deletions
+23
View File
@@ -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>
);
};
+68
View File
@@ -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;
+71
View File
@@ -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;
+5 -5
View File
@@ -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}>
+301
View File
@@ -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;
}
}
+20
View File
@@ -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 {