fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
312
studio-v2/components/worksheet-editor/EditorToolbar.tsx
Normal file
312
studio-v2/components/worksheet-editor/EditorToolbar.tsx
Normal file
@@ -0,0 +1,312 @@
|
||||
'use client'
|
||||
|
||||
import { useRef } from 'react'
|
||||
import { useTheme } from '@/lib/ThemeContext'
|
||||
import { useLanguage } from '@/lib/LanguageContext'
|
||||
import { useWorksheet } from '@/lib/worksheet-editor/WorksheetContext'
|
||||
import type { EditorTool } from '@/app/worksheet-editor/types'
|
||||
|
||||
interface EditorToolbarProps {
|
||||
onOpenAIGenerator: () => void
|
||||
onOpenDocumentImporter: () => void
|
||||
onOpenCleanupPanel?: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
interface ToolButtonProps {
|
||||
tool: EditorTool
|
||||
icon: React.ReactNode
|
||||
label: string
|
||||
isActive: boolean
|
||||
onClick: () => void
|
||||
isDark: boolean
|
||||
}
|
||||
|
||||
function ToolButton({ tool, icon, label, isActive, onClick, isDark }: ToolButtonProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
title={label}
|
||||
className={`w-12 h-12 flex items-center justify-center rounded-xl transition-all ${
|
||||
isActive
|
||||
? isDark
|
||||
? 'bg-purple-500/30 text-purple-300 shadow-lg'
|
||||
: 'bg-purple-100 text-purple-700 shadow-lg'
|
||||
: isDark
|
||||
? 'text-white/70 hover:bg-white/10 hover:text-white'
|
||||
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
|
||||
}`}
|
||||
>
|
||||
{icon}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export function EditorToolbar({ onOpenAIGenerator, onOpenDocumentImporter, onOpenCleanupPanel, className = '' }: EditorToolbarProps) {
|
||||
const { isDark } = useTheme()
|
||||
const { t } = useLanguage()
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const {
|
||||
activeTool,
|
||||
setActiveTool,
|
||||
canvas,
|
||||
canUndo,
|
||||
canRedo,
|
||||
undo,
|
||||
redo,
|
||||
} = useWorksheet()
|
||||
|
||||
const handleToolClick = (tool: EditorTool) => {
|
||||
setActiveTool(tool)
|
||||
}
|
||||
|
||||
const handleImageUpload = () => {
|
||||
fileInputRef.current?.click()
|
||||
}
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file || !canvas) return
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
const url = event.target?.result as string
|
||||
if ((canvas as any).addImage) {
|
||||
(canvas as any).addImage(url)
|
||||
}
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
|
||||
// Reset input
|
||||
e.target.value = ''
|
||||
}
|
||||
|
||||
// Glassmorphism styles
|
||||
const toolbarStyle = isDark
|
||||
? 'backdrop-blur-xl bg-white/10 border border-white/20'
|
||||
: 'backdrop-blur-xl bg-white/70 border border-black/10 shadow-xl'
|
||||
|
||||
const dividerStyle = isDark
|
||||
? 'border-white/20'
|
||||
: 'border-slate-200'
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col gap-2 p-2 rounded-2xl ${toolbarStyle} ${className}`}>
|
||||
{/* Selection Tool */}
|
||||
<ToolButton
|
||||
tool="select"
|
||||
isActive={activeTool === 'select'}
|
||||
onClick={() => handleToolClick('select')}
|
||||
isDark={isDark}
|
||||
label="Auswählen"
|
||||
icon={
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className={`border-t ${dividerStyle}`} />
|
||||
|
||||
{/* Text Tool */}
|
||||
<ToolButton
|
||||
tool="text"
|
||||
isActive={activeTool === 'text'}
|
||||
onClick={() => handleToolClick('text')}
|
||||
isDark={isDark}
|
||||
label="Text"
|
||||
icon={
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className={`border-t ${dividerStyle}`} />
|
||||
|
||||
{/* Shape Tools */}
|
||||
<ToolButton
|
||||
tool="rectangle"
|
||||
isActive={activeTool === 'rectangle'}
|
||||
onClick={() => handleToolClick('rectangle')}
|
||||
isDark={isDark}
|
||||
label="Rechteck"
|
||||
icon={
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 6h16M4 18h16M4 6v12M20 6v12" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
|
||||
<ToolButton
|
||||
tool="circle"
|
||||
isActive={activeTool === 'circle'}
|
||||
onClick={() => handleToolClick('circle')}
|
||||
isDark={isDark}
|
||||
label="Kreis"
|
||||
icon={
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="9" strokeWidth={1.5} />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
|
||||
<ToolButton
|
||||
tool="line"
|
||||
isActive={activeTool === 'line'}
|
||||
onClick={() => handleToolClick('line')}
|
||||
isDark={isDark}
|
||||
label="Linie"
|
||||
icon={
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 20L20 4" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
|
||||
<ToolButton
|
||||
tool="arrow"
|
||||
isActive={activeTool === 'arrow'}
|
||||
onClick={() => handleToolClick('arrow')}
|
||||
isDark={isDark}
|
||||
label="Pfeil"
|
||||
icon={
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className={`border-t ${dividerStyle}`} />
|
||||
|
||||
{/* Image Tools */}
|
||||
<ToolButton
|
||||
tool="image"
|
||||
isActive={activeTool === 'image'}
|
||||
onClick={handleImageUpload}
|
||||
isDark={isDark}
|
||||
label="Bild hochladen"
|
||||
icon={
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
{/* AI Image Generator */}
|
||||
<ToolButton
|
||||
tool="ai-image"
|
||||
isActive={activeTool === 'ai-image'}
|
||||
onClick={onOpenAIGenerator}
|
||||
isDark={isDark}
|
||||
label="KI-Bild generieren"
|
||||
icon={
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className={`border-t ${dividerStyle}`} />
|
||||
|
||||
{/* Document Import */}
|
||||
<button
|
||||
onClick={onOpenDocumentImporter}
|
||||
title="Dokument importieren"
|
||||
className={`w-12 h-12 flex items-center justify-center rounded-xl transition-all ${
|
||||
isDark
|
||||
? 'text-blue-300 hover:bg-blue-500/20 hover:text-blue-200'
|
||||
: 'text-blue-600 hover:bg-blue-100 hover:text-blue-700'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 3v6a1 1 0 001 1h6" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Cleanup Panel - Handwriting Removal */}
|
||||
{onOpenCleanupPanel && (
|
||||
<button
|
||||
onClick={onOpenCleanupPanel}
|
||||
title="Arbeitsblatt bereinigen (Handschrift entfernen)"
|
||||
className={`w-12 h-12 flex items-center justify-center rounded-xl transition-all ${
|
||||
isDark
|
||||
? 'text-orange-300 hover:bg-orange-500/20 hover:text-orange-200'
|
||||
: 'text-orange-600 hover:bg-orange-100 hover:text-orange-700'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className={`border-t ${dividerStyle}`} />
|
||||
|
||||
{/* Table Tool */}
|
||||
<ToolButton
|
||||
tool="table"
|
||||
isActive={activeTool === 'table'}
|
||||
onClick={() => handleToolClick('table')}
|
||||
isDark={isDark}
|
||||
label="Tabelle"
|
||||
icon={
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className={`border-t ${dividerStyle} mt-auto`} />
|
||||
|
||||
{/* Undo/Redo */}
|
||||
<button
|
||||
onClick={undo}
|
||||
disabled={!canUndo}
|
||||
title="Rückgängig (Ctrl+Z)"
|
||||
className={`w-12 h-12 flex items-center justify-center rounded-xl transition-all ${
|
||||
canUndo
|
||||
? isDark
|
||||
? 'text-white/70 hover:bg-white/10 hover:text-white'
|
||||
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
|
||||
: isDark
|
||||
? 'text-white/30 cursor-not-allowed'
|
||||
: 'text-slate-300 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={redo}
|
||||
disabled={!canRedo}
|
||||
title="Wiederholen (Ctrl+Y)"
|
||||
className={`w-12 h-12 flex items-center justify-center rounded-xl transition-all ${
|
||||
canRedo
|
||||
? isDark
|
||||
? 'text-white/70 hover:bg-white/10 hover:text-white'
|
||||
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
|
||||
: isDark
|
||||
? 'text-white/30 cursor-not-allowed'
|
||||
: 'text-slate-300 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 10h-10a8 8 0 00-8 8v2M21 10l-6 6m6-6l-6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user