Files
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

444 lines
16 KiB
TypeScript

'use client'
import { useState, useEffect, useCallback } from 'react'
import { useTheme } from '@/lib/ThemeContext'
import { useLanguage } from '@/lib/LanguageContext'
import { useWorksheet } from '@/lib/worksheet-editor/WorksheetContext'
import { AVAILABLE_FONTS, DEFAULT_TYPOGRAPHY_PRESETS } from '@/app/worksheet-editor/types'
interface PropertiesPanelProps {
className?: string
}
export function PropertiesPanel({ className = '' }: PropertiesPanelProps) {
const { isDark } = useTheme()
const { t } = useLanguage()
const { selectedObjects, canvas, saveToHistory } = useWorksheet()
// Local state for properties
const [fontFamily, setFontFamily] = useState('Arial')
const [fontSize, setFontSize] = useState(16)
const [fontWeight, setFontWeight] = useState<'normal' | 'bold'>('normal')
const [fontStyle, setFontStyle] = useState<'normal' | 'italic'>('normal')
const [textAlign, setTextAlign] = useState<'left' | 'center' | 'right'>('left')
const [lineHeight, setLineHeight] = useState(1.4)
const [charSpacing, setCharSpacing] = useState(0)
const [fill, setFill] = useState('#000000')
const [stroke, setStroke] = useState('#000000')
const [strokeWidth, setStrokeWidth] = useState(2)
const [opacity, setOpacity] = useState(100)
// Get selected object (cast to any for Fabric.js properties)
const selectedObject = selectedObjects[0] as any
const objType = selectedObject?.type
const isText = objType === 'i-text' || objType === 'text' || objType === 'textbox'
const isShape = objType === 'rect' || objType === 'circle' || objType === 'line' || objType === 'path'
const isImage = objType === 'image'
// Update local state when selection changes
useEffect(() => {
if (!selectedObject) return
if (isText) {
setFontFamily(selectedObject.fontFamily || 'Arial')
setFontSize(selectedObject.fontSize || 16)
setFontWeight(selectedObject.fontWeight || 'normal')
setFontStyle(selectedObject.fontStyle || 'normal')
setTextAlign(selectedObject.textAlign || 'left')
setLineHeight(selectedObject.lineHeight || 1.4)
setCharSpacing(selectedObject.charSpacing || 0)
setFill(selectedObject.fill || '#000000')
}
if (isShape) {
setFill(selectedObject.fill || 'transparent')
setStroke(selectedObject.stroke || '#000000')
setStrokeWidth(selectedObject.strokeWidth || 2)
}
setOpacity(Math.round((selectedObject.opacity || 1) * 100))
}, [selectedObject, isText, isShape])
// Update object property
const updateProperty = useCallback((property: string, value: any) => {
if (!selectedObject || !canvas) return
selectedObject.set(property, value)
canvas.renderAll()
saveToHistory(`property:${property}`)
}, [selectedObject, canvas, saveToHistory])
// Glassmorphism styles
const panelStyle = isDark
? 'backdrop-blur-xl bg-white/10 border border-white/20'
: 'backdrop-blur-xl bg-white/70 border border-black/10 shadow-xl'
const inputStyle = isDark
? 'bg-white/10 border-white/20 text-white placeholder-white/40 focus:border-purple-400'
: 'bg-white/50 border-black/10 text-slate-900 placeholder-slate-400 focus:border-purple-500'
const labelStyle = isDark ? 'text-white/70' : 'text-slate-600'
const selectStyle = isDark
? 'bg-white/10 border-white/20 text-white'
: 'bg-white/50 border-black/10 text-slate-900'
// No selection
if (!selectedObject) {
return (
<div className={`flex flex-col p-4 rounded-2xl ${panelStyle} ${className}`}>
<h3 className={`font-semibold text-lg mb-4 ${isDark ? 'text-white' : 'text-slate-900'}`}>
Eigenschaften
</h3>
<p className={`text-sm ${labelStyle}`}>
Wählen Sie ein Element aus, um seine Eigenschaften zu bearbeiten.
</p>
</div>
)
}
return (
<div className={`flex flex-col p-4 rounded-2xl overflow-y-auto ${panelStyle} ${className}`}>
<h3 className={`font-semibold text-lg mb-4 ${isDark ? 'text-white' : 'text-slate-900'}`}>
Eigenschaften
</h3>
{/* Text Properties */}
{isText && (
<div className="space-y-4">
{/* Typography Presets */}
<div>
<label className={`block text-sm font-medium mb-2 ${labelStyle}`}>Vorlage</label>
<select
className={`w-full px-3 py-2 rounded-xl border text-sm ${selectStyle}`}
onChange={(e) => {
const preset = DEFAULT_TYPOGRAPHY_PRESETS.find(p => p.id === e.target.value)
if (preset) {
updateProperty('fontFamily', preset.fontFamily)
updateProperty('fontSize', preset.fontSize)
updateProperty('fontWeight', preset.fontWeight)
updateProperty('lineHeight', preset.lineHeight)
setFontFamily(preset.fontFamily)
setFontSize(preset.fontSize)
setFontWeight(preset.fontWeight as 'normal' | 'bold')
setLineHeight(preset.lineHeight)
}
}}
>
<option value="">Vorlage wählen...</option>
{DEFAULT_TYPOGRAPHY_PRESETS.map(preset => (
<option key={preset.id} value={preset.id}>{preset.name}</option>
))}
</select>
</div>
{/* Font Family */}
<div>
<label className={`block text-sm font-medium mb-2 ${labelStyle}`}>Schriftart</label>
<select
value={fontFamily}
onChange={(e) => {
setFontFamily(e.target.value)
updateProperty('fontFamily', e.target.value)
}}
className={`w-full px-3 py-2 rounded-xl border text-sm ${selectStyle}`}
>
{AVAILABLE_FONTS.map(font => (
<option key={font.name} value={font.name} style={{ fontFamily: font.family }}>
{font.name}
</option>
))}
</select>
</div>
{/* Font Size */}
<div>
<label className={`block text-sm font-medium mb-2 ${labelStyle}`}>Schriftgröße</label>
<div className="flex items-center gap-2">
<input
type="range"
min="8"
max="120"
value={fontSize}
onChange={(e) => {
const value = parseInt(e.target.value)
setFontSize(value)
updateProperty('fontSize', value)
}}
className="flex-1"
/>
<input
type="number"
min="8"
max="120"
value={fontSize}
onChange={(e) => {
const value = parseInt(e.target.value) || 16
setFontSize(value)
updateProperty('fontSize', value)
}}
className={`w-16 px-2 py-1 rounded-lg border text-sm text-center ${inputStyle}`}
/>
</div>
</div>
{/* Font Style Buttons */}
<div>
<label className={`block text-sm font-medium mb-2 ${labelStyle}`}>Stil</label>
<div className="flex gap-2">
<button
onClick={() => {
const newWeight = fontWeight === 'bold' ? 'normal' : 'bold'
setFontWeight(newWeight)
updateProperty('fontWeight', newWeight)
}}
className={`flex-1 py-2 rounded-xl font-bold transition-all ${
fontWeight === 'bold'
? isDark ? 'bg-purple-500/30 text-purple-300' : 'bg-purple-100 text-purple-700'
: isDark ? 'bg-white/10 text-white/70' : 'bg-slate-100 text-slate-600'
}`}
>
B
</button>
<button
onClick={() => {
const newStyle = fontStyle === 'italic' ? 'normal' : 'italic'
setFontStyle(newStyle)
updateProperty('fontStyle', newStyle)
}}
className={`flex-1 py-2 rounded-xl italic transition-all ${
fontStyle === 'italic'
? isDark ? 'bg-purple-500/30 text-purple-300' : 'bg-purple-100 text-purple-700'
: isDark ? 'bg-white/10 text-white/70' : 'bg-slate-100 text-slate-600'
}`}
>
I
</button>
</div>
</div>
{/* Text Alignment */}
<div>
<label className={`block text-sm font-medium mb-2 ${labelStyle}`}>Ausrichtung</label>
<div className="flex gap-2">
{(['left', 'center', 'right'] as const).map(align => (
<button
key={align}
onClick={() => {
setTextAlign(align)
updateProperty('textAlign', align)
}}
className={`flex-1 py-2 rounded-xl transition-all ${
textAlign === align
? isDark ? 'bg-purple-500/30 text-purple-300' : 'bg-purple-100 text-purple-700'
: isDark ? 'bg-white/10 text-white/70' : 'bg-slate-100 text-slate-600'
}`}
>
{align === 'left' && (
<svg className="w-5 h-5 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h10M4 18h14" />
</svg>
)}
{align === 'center' && (
<svg className="w-5 h-5 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M7 12h10M5 18h14" />
</svg>
)}
{align === 'right' && (
<svg className="w-5 h-5 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M10 12h10M6 18h14" />
</svg>
)}
</button>
))}
</div>
</div>
{/* Line Height */}
<div>
<label className={`block text-sm font-medium mb-2 ${labelStyle}`}>Zeilenhöhe</label>
<div className="flex items-center gap-2">
<input
type="range"
min="0.8"
max="3"
step="0.1"
value={lineHeight}
onChange={(e) => {
const value = parseFloat(e.target.value)
setLineHeight(value)
updateProperty('lineHeight', value)
}}
className="flex-1"
/>
<span className={`w-12 text-center text-sm ${labelStyle}`}>{lineHeight.toFixed(1)}</span>
</div>
</div>
{/* Text Color */}
<div>
<label className={`block text-sm font-medium mb-2 ${labelStyle}`}>Textfarbe</label>
<div className="flex items-center gap-2">
<input
type="color"
value={fill as string}
onChange={(e) => {
setFill(e.target.value)
updateProperty('fill', e.target.value)
}}
className="w-10 h-10 rounded-lg cursor-pointer"
/>
<input
type="text"
value={fill as string}
onChange={(e) => {
setFill(e.target.value)
updateProperty('fill', e.target.value)
}}
className={`flex-1 px-3 py-2 rounded-xl border text-sm ${inputStyle}`}
/>
</div>
</div>
</div>
)}
{/* Shape Properties */}
{isShape && (
<div className="space-y-4">
{/* Fill Color */}
<div>
<label className={`block text-sm font-medium mb-2 ${labelStyle}`}>Füllfarbe</label>
<div className="flex items-center gap-2">
<input
type="color"
value={fill === 'transparent' ? '#ffffff' : (fill as string)}
onChange={(e) => {
setFill(e.target.value)
updateProperty('fill', e.target.value)
}}
className="w-10 h-10 rounded-lg cursor-pointer"
/>
<input
type="text"
value={fill as string}
onChange={(e) => {
setFill(e.target.value)
updateProperty('fill', e.target.value)
}}
className={`flex-1 px-3 py-2 rounded-xl border text-sm ${inputStyle}`}
/>
<button
onClick={() => {
setFill('transparent')
updateProperty('fill', 'transparent')
}}
className={`px-3 py-2 rounded-xl text-sm ${
isDark ? 'bg-white/10 text-white/70 hover:bg-white/20' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
Keine
</button>
</div>
</div>
{/* Stroke Color */}
<div>
<label className={`block text-sm font-medium mb-2 ${labelStyle}`}>Rahmenfarbe</label>
<div className="flex items-center gap-2">
<input
type="color"
value={stroke}
onChange={(e) => {
setStroke(e.target.value)
updateProperty('stroke', e.target.value)
}}
className="w-10 h-10 rounded-lg cursor-pointer"
/>
<input
type="text"
value={stroke}
onChange={(e) => {
setStroke(e.target.value)
updateProperty('stroke', e.target.value)
}}
className={`flex-1 px-3 py-2 rounded-xl border text-sm ${inputStyle}`}
/>
</div>
</div>
{/* Stroke Width */}
<div>
<label className={`block text-sm font-medium mb-2 ${labelStyle}`}>Rahmenstärke</label>
<div className="flex items-center gap-2">
<input
type="range"
min="0"
max="20"
value={strokeWidth}
onChange={(e) => {
const value = parseInt(e.target.value)
setStrokeWidth(value)
updateProperty('strokeWidth', value)
}}
className="flex-1"
/>
<span className={`w-12 text-center text-sm ${labelStyle}`}>{strokeWidth}px</span>
</div>
</div>
</div>
)}
{/* Image Properties */}
{isImage && (
<div className="space-y-4">
<p className={`text-sm ${labelStyle}`}>
Bildgröße und Position können durch Ziehen der Eckpunkte angepasst werden.
</p>
</div>
)}
{/* Common Properties */}
<div className="mt-6 pt-4 border-t border-white/10 space-y-4">
{/* Opacity */}
<div>
<label className={`block text-sm font-medium mb-2 ${labelStyle}`}>Deckkraft</label>
<div className="flex items-center gap-2">
<input
type="range"
min="0"
max="100"
value={opacity}
onChange={(e) => {
const value = parseInt(e.target.value)
setOpacity(value)
updateProperty('opacity', value / 100)
}}
className="flex-1"
/>
<span className={`w-12 text-center text-sm ${labelStyle}`}>{opacity}%</span>
</div>
</div>
{/* Delete Button */}
<button
onClick={() => {
if (canvas && selectedObject) {
canvas.remove(selectedObject)
canvas.discardActiveObject()
canvas.renderAll()
saveToHistory('delete')
}
}}
className={`w-full py-3 rounded-xl text-sm font-medium transition-all ${
isDark
? 'bg-red-500/20 text-red-400 hover:bg-red-500/30'
: 'bg-red-50 text-red-600 hover:bg-red-100'
}`}
>
Element löschen
</button>
</div>
</div>
)
}