Generieren: Bild wie Annotieren-Seite groß, Prompt als eingeklappte Leiste

- Canvas-Struktur identisch zu DrawIt (canvas-area > canvas-frame > DrawCanvas)
  → kein Schrumpfen beim Bild-Wechsel mehr
- Prompt-Editor als absolute Leiste am unteren Rand des Canvas-Bereichs
- Eingeklappt: zeigt Titel + Layout-Name + Aktionen
- Ausgeklappt: Textarea + Speichern-Dialog

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 08:23:08 +02:00
parent 7f85b90a82
commit 99a8d7e0aa
2 changed files with 135 additions and 84 deletions

View File

@@ -339,6 +339,10 @@ body {
max-width: 100%; max-width: 100%;
} }
.canvas-area--relative {
position: relative;
}
canvas { canvas {
display: block; display: block;
max-width: 100%; max-width: 100%;
@@ -1003,33 +1007,76 @@ select:focus {
} }
/* ===================================================== /* =====================================================
PROMPT EDITOR PROMPT BAR (collapsible bottom panel in canvas-area)
===================================================== */ ===================================================== */
.prompt-editor { .prompt-bar {
width: 100%; position: absolute;
display: flex; bottom: 0;
flex-direction: column; left: 0;
gap: 6px; right: 0;
background: var(--surface); background: var(--surface);
border: 1px solid var(--border); border-top: 1px solid var(--border);
border-radius: var(--r-lg); z-index: 10;
padding: 10px; box-shadow: 0 -4px 16px rgba(13,21,38,.08);
flex: 1;
min-height: 0;
} }
.prompt-editor-toolbar { .prompt-bar-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
cursor: pointer;
user-select: none;
min-height: 40px;
}
.prompt-bar-header:hover {
background: var(--surface-2);
}
.prompt-bar-chevron {
font-size: 11px;
color: var(--text-3);
flex-shrink: 0;
width: 12px;
}
.prompt-bar-title {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text-3);
}
.prompt-bar-layout-name {
font-size: 12px;
color: var(--text-2);
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--r-full);
padding: 1px 8px;
}
.prompt-bar-actions {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
flex-wrap: wrap; margin-left: auto;
}
.prompt-bar-body {
padding: 0 14px 12px;
display: flex;
flex-direction: column;
gap: 8px;
} }
.prompt-layout-select { .prompt-layout-select {
width: auto !important; width: auto !important;
min-width: 120px; min-width: 110px;
max-width: 200px; max-width: 180px;
font-size: 12.5px !important; font-size: 12px !important;
padding: 4px 8px !important; padding: 4px 8px !important;
} }
@@ -1037,7 +1084,7 @@ select:focus {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
padding: 8px 10px; padding: 6px 10px;
background: var(--surface-2); background: var(--surface-2);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--r-md); border-radius: var(--r-md);
@@ -1051,8 +1098,8 @@ select:focus {
.prompt-textarea { .prompt-textarea {
width: 100%; width: 100%;
flex: 1; min-height: 160px;
min-height: 180px; max-height: 40vh;
resize: vertical; resize: vertical;
padding: 10px 12px; padding: 10px 12px;
background: var(--surface-2); background: var(--surface-2);

View File

@@ -139,6 +139,7 @@ export default function GenerateIt() {
const [promptText, setPromptText] = useState(() => loadLayouts()[0]?.prompt ?? DEFAULT_PROMPT) const [promptText, setPromptText] = useState(() => loadLayouts()[0]?.prompt ?? DEFAULT_PROMPT)
const [showSaveDialog, setShowSaveDialog] = useState(false) const [showSaveDialog, setShowSaveDialog] = useState(false)
const [newLayoutName, setNewLayoutName] = useState('') const [newLayoutName, setNewLayoutName] = useState('')
const [promptOpen, setPromptOpen] = useState(false)
const currentPicture: DirectusPicture | null = const currentPicture: DirectusPicture | null =
currentIndex >= 0 && currentIndex < pictureList.length ? pictureList[currentIndex] : null currentIndex >= 0 && currentIndex < pictureList.length ? pictureList[currentIndex] : null
@@ -288,78 +289,81 @@ export default function GenerateIt() {
</div> </div>
</aside> </aside>
{/* Center: Canvas + Prompt Editor */} {/* Center: Canvas + Prompt Bar */}
<main className="canvas-area" style={{ alignItems: 'flex-start', justifyContent: 'flex-start', flexDirection: 'column', gap: 12, padding: 16 }}> <main className="canvas-area canvas-area--relative">
<div style={{ width: '100%', maxHeight: 320, overflow: 'hidden' }}> <div className="canvas-frame">
<div className="canvas-frame" style={{ display: 'inline-flex', maxWidth: '100%' }}> <DrawCanvas
<DrawCanvas ref={canvasRef}
ref={canvasRef} imageSrc={currentPicture && token ? directusAssetUrl(currentPicture.media, token) : null}
imageSrc={currentPicture && token ? directusAssetUrl(currentPicture.media, token) : null} objects={canvasObjects}
objects={canvasObjects} selectedObjectId={selectedObjId}
selectedObjectId={selectedObjId} mode="rect"
mode="rect" onHasSelection={() => {}}
onHasSelection={() => {}} readOnly
readOnly />
/>
</div>
</div> </div>
{/* Prompt Editor */} {/* Collapsible Prompt Bar */}
<div className="prompt-editor"> <div className={`prompt-bar${promptOpen ? ' prompt-bar--open' : ''}`}>
<div className="prompt-editor-toolbar"> <div className="prompt-bar-header" onClick={() => setPromptOpen(o => !o)}>
<select <span className="prompt-bar-chevron">{promptOpen ? '▾' : '▸'}</span>
className="prompt-layout-select" <span className="prompt-bar-title">Prompt</span>
value={selectedLayoutName} <span className="prompt-bar-layout-name">{selectedLayoutName}</span>
onChange={e => handleSelectLayout(e.target.value)} <div className="prompt-bar-actions" onClick={e => e.stopPropagation()}>
> <select
{layouts.map(l => ( className="prompt-layout-select"
<option key={l.name} value={l.name}>{l.name}</option> value={selectedLayoutName}
))} onChange={e => handleSelectLayout(e.target.value)}
</select>
{selectedLayoutName !== 'Standard' && (
<button
className="btn-ghost btn-sm btn-danger"
onClick={() => handleDeleteLayout(selectedLayoutName)}
title="Layout löschen"
> >
{layouts.map(l => (
</button> <option key={l.name} value={l.name}>{l.name}</option>
)} ))}
<button </select>
className="btn-ghost btn-sm" {selectedLayoutName !== 'Standard' && (
style={{ marginLeft: 'auto' }} <button
onClick={() => { setNewLayoutName(''); setShowSaveDialog(s => !s) }} className="btn-ghost btn-sm btn-danger"
> onClick={() => handleDeleteLayout(selectedLayoutName)}
<SaveIcon /> title="Layout löschen"
Als Layout speichern ></button>
</button> )}
</div> <button
className="btn-ghost btn-sm"
{showSaveDialog && ( onClick={() => { setNewLayoutName(''); setShowSaveDialog(s => !s) }}
<div className="prompt-save-dialog"> >
<input <SaveIcon />
type="text"
placeholder="Layout-Name"
value={newLayoutName}
onChange={e => setNewLayoutName(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') handleSaveLayout(); if (e.key === 'Escape') setShowSaveDialog(false) }}
autoFocus
/>
<button className="btn-ghost btn-sm" onClick={handleSaveLayout} disabled={!newLayoutName.trim()}>
Speichern Speichern
</button> </button>
<button className="btn-ghost btn-sm" onClick={() => setShowSaveDialog(false)}> </div>
Abbrechen </div>
</button>
{promptOpen && (
<div className="prompt-bar-body">
{showSaveDialog && (
<div className="prompt-save-dialog">
<input
type="text"
placeholder="Layout-Name"
value={newLayoutName}
onChange={e => setNewLayoutName(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') handleSaveLayout(); if (e.key === 'Escape') setShowSaveDialog(false) }}
autoFocus
/>
<button className="btn-ghost btn-sm" onClick={handleSaveLayout} disabled={!newLayoutName.trim()}>
Speichern
</button>
<button className="btn-ghost btn-sm" onClick={() => setShowSaveDialog(false)}>
Abbrechen
</button>
</div>
)}
<textarea
className="prompt-textarea"
value={promptText}
onChange={e => setPromptText(e.target.value)}
placeholder="Prompt eingeben…"
/>
</div> </div>
)} )}
<textarea
className="prompt-textarea"
value={promptText}
onChange={e => setPromptText(e.target.value)}
placeholder="Prompt eingeben…"
/>
</div> </div>
</main> </main>