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 MobileDrawers from './components/MobileDrawers';
|
||||||
import BackgroundImport from './components/BackgroundImport';
|
import BackgroundImport from './components/BackgroundImport';
|
||||||
import HistoryPanel from './components/HistoryPanel';
|
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 { BackgroundService, type BackgroundConfig } from './services/backgroundService';
|
||||||
import { HistoryManager, type CADStateSnapshot, type HistoryEntry } from './history';
|
import { HistoryManager, type CADStateSnapshot, type HistoryEntry } from './history';
|
||||||
import { getCommandRegistry } from './services/commandRegistry';
|
import { getCommandRegistry } from './services/commandRegistry';
|
||||||
@@ -162,6 +165,9 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
|
|
||||||
// History panel
|
// History panel
|
||||||
const [historyPanelOpen, setHistoryPanelOpen] = useState(false);
|
const [historyPanelOpen, setHistoryPanelOpen] = useState(false);
|
||||||
|
const [shareOpen, setShareOpen] = useState(false);
|
||||||
|
const [helpOpen, setHelpOpen] = useState(false);
|
||||||
|
const [notifOpen, setNotifOpen] = useState(false);
|
||||||
|
|
||||||
// Elements + undo/redo (HistoryManager)
|
// Elements + undo/redo (HistoryManager)
|
||||||
const [elements, setElements] = useState<CADElement[]>([]);
|
const [elements, setElements] = useState<CADElement[]>([]);
|
||||||
@@ -947,6 +953,10 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
theme={theme}
|
theme={theme}
|
||||||
onOpenLeftDrawer={() => setMobileLeftOpen(true)}
|
onOpenLeftDrawer={() => setMobileLeftOpen(true)}
|
||||||
onNavigateBack={onNavigateBack}
|
onNavigateBack={onNavigateBack}
|
||||||
|
onOpenHistory={() => setHistoryPanelOpen(true)}
|
||||||
|
onOpenShare={() => setShareOpen(true)}
|
||||||
|
onOpenHelp={() => setHelpOpen(true)}
|
||||||
|
onOpenNotifications={() => setNotifOpen(true)}
|
||||||
/>
|
/>
|
||||||
<RibbonBar
|
<RibbonBar
|
||||||
activeTab={activeRibbonTab}
|
activeTab={activeRibbonTab}
|
||||||
@@ -1120,6 +1130,19 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
onClose={() => setHistoryPanelOpen(false)}
|
onClose={() => setHistoryPanelOpen(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<ShareDialog
|
||||||
|
open={shareOpen}
|
||||||
|
onClose={() => setShareOpen(false)}
|
||||||
|
projectName={projectName}
|
||||||
|
/>
|
||||||
|
<HelpPanel
|
||||||
|
open={helpOpen}
|
||||||
|
onClose={() => setHelpOpen(false)}
|
||||||
|
/>
|
||||||
|
<NotificationPanel
|
||||||
|
open={notifOpen}
|
||||||
|
onClose={() => setNotifOpen(false)}
|
||||||
|
/>
|
||||||
</div>
|
</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 React from 'react';
|
||||||
import type { TopbarProps } from '../types/ui.types';
|
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 (
|
return (
|
||||||
<header className="topbar" role="banner">
|
<header className="topbar" role="banner">
|
||||||
<div className="topbar-left">
|
<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>
|
<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>
|
</button>
|
||||||
<span style={{ width: '1px', height: '20px', background: 'var(--color-border)', margin: '0 4px' }}></span>
|
<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>
|
<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>
|
||||||
<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>
|
<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>
|
</button>
|
||||||
<span style={{ width: '1px', height: '20px', background: 'var(--color-border)', margin: '0 4px' }}></span>
|
<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}>
|
<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>
|
<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>
|
||||||
<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>
|
<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>
|
</button>
|
||||||
<span style={{ width: '1px', height: '20px', background: 'var(--color-border)', margin: '0 4px' }}></span>
|
<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="de">DE</option>
|
||||||
<option value="en">EN</option>
|
<option value="en">EN</option>
|
||||||
</select>
|
</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>
|
<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>
|
||||||
<button className="icon-btn-top" aria-label="Einstellungen" title="Einstellungen" onClick={onOpenSettings}>
|
<button className="icon-btn-top" aria-label="Einstellungen" title="Einstellungen" onClick={onOpenSettings}>
|
||||||
|
|||||||
@@ -2010,3 +2010,304 @@ body.drawer-open { overflow: hidden; }
|
|||||||
padding: 3px 8px;
|
padding: 3px 8px;
|
||||||
border-radius: 4px;
|
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;
|
onOpenSettings?: () => void;
|
||||||
onOpenLeftDrawer?: () => void;
|
onOpenLeftDrawer?: () => void;
|
||||||
onNavigateBack?: () => 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 {
|
export interface SettingsModalProps {
|
||||||
|
|||||||
Reference in New Issue
Block a user