Projekte: Preview-Block ohne Kleinknoepfe, Statuspunkt als Badge, Log auf hellem Grund

Markup auf Bausteine (Button, Badge) und die Klassen pv-*, input, select, msg, link umgestellt; keine Inline-Stile, kein btn-sm, kein Farbliteral. Fehlertexte des Runners behalten ihren Zeilenumbruch ueber .pv-block .msg.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-06 14:34:29 +00:00
1 parent 92fef9d4a5
commit 154aea44a8
2 files changed
+46 -57

No files matched your search

+45 -57
View File
@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback, useRef } from 'react' import { useState, useEffect, useCallback, useRef } from 'react'
import { modeOptionsFor, previewStatus, modeHint, isDevUnavailableError, NO_DEV_HINT } from '../lib/previewView' import { modeOptionsFor, previewStatus, modeHint, isDevUnavailableError, NO_DEV_HINT } from '../lib/previewView'
import { isValidEnvName, buildEnvPayload, isDevPreview, dotColor, envValueTooLong, MAX_ENV_VARS } from '../lib/previewEnv' import { isValidEnvName, buildEnvPayload, isDevPreview, envValueTooLong, MAX_ENV_VARS } from '../lib/previewEnv'
import { import {
fetchPreview, fetchPreview,
setPreviewMode, setPreviewMode,
@@ -10,8 +10,9 @@ import {
stopPreviewDev, stopPreviewDev,
fetchPreviewLog, fetchPreviewLog,
} from '../lib/previewApi' } from '../lib/previewApi'
import { Button, Badge } from './ui'
import { previewColorTone } from './projectTones'
const STATE_OF_COLOR = { green: 'online', yellow: 'building', red: 'failed', grey: 'off' }
const NOT_ACTIVE = 'Preview-Runner ist nicht aktiv' const NOT_ACTIVE = 'Preview-Runner ist nicht aktiv'
const logDate = (at) => { const logDate = (at) => {
@@ -171,7 +172,7 @@ export default function PreviewBlock({ projectId }) {
} }
} }
if (loading) return <div className="text-grey" style={{ fontSize: 12 }}>Lädt …</div> if (loading) return <p className="muted xs">Lädt …</p>
const view = data ? { ...data } : { state: 'off' } const view = data ? { ...data } : { state: 'off' }
const status = previewStatus(view) const status = previewStatus(view)
@@ -182,18 +183,15 @@ export default function PreviewBlock({ projectId }) {
const dev = isDevPreview(data) const dev = isDevPreview(data)
const logs = [...(data?.logs || [])].sort((a, b) => new Date(b.at) - new Date(a.at)).slice(0, 5) const logs = [...(data?.logs || [])].sort((a, b) => new Date(b.at) - new Date(a.at)).slice(0, 5)
const small = { fontSize: 12 }
return ( return (
<div style={{ fontSize: 13 }}> <div className="pv-block">
{data && ( {data && (
<> <>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' }}> <div className="pv-row">
<label className="text-grey" htmlFor={`pv-mode-${projectId}`} style={small}>Modus</label> <label className="muted xs" htmlFor={`pv-mode-${projectId}`}>Modus</label>
<select <select
id={`pv-mode-${projectId}`} id={`pv-mode-${projectId}`}
className="form-control" className="select pv-mode"
style={{ maxWidth: 200 }}
value={mode} value={mode}
disabled={busy || inactive} disabled={busy || inactive}
onChange={(e) => run(() => setPreviewMode(projectId, e.target.value))} onChange={(e) => run(() => setPreviewMode(projectId, e.target.value))}
@@ -201,47 +199,45 @@ export default function PreviewBlock({ projectId }) {
{modeOptionsFor(data.devAvailable).map((o) => <option key={o.value} value={o.value} disabled={o.disabled}>{o.label}</option>)} {modeOptionsFor(data.devAvailable).map((o) => <option key={o.value} value={o.value} disabled={o.disabled}>{o.label}</option>)}
</select> </select>
</div> </div>
{data.devAvailable === false && <div className="text-grey" style={{ ...small, marginTop: 4 }}>{NO_DEV_HINT}</div>} {data.devAvailable === false && <p className="muted xs">{NO_DEV_HINT}</p>}
{hint && <div className="text-grey" style={{ ...small, marginTop: 4 }}>{hint}</div>} {hint && <p className="muted xs">{hint}</p>}
<div style={{ marginTop: 8, display: 'flex', gap: 6, alignItems: 'flex-start' }}> <div className="pv-row">
<span style={{ width: 10, height: 10, borderRadius: '50%', background: dotColor(STATE_OF_COLOR[status.color]), display: 'inline-block', flexShrink: 0, marginTop: 4 }} /> <Badge tone={previewColorTone(status.color)} className="wrap">{status.text}</Badge>
<span style={{ overflowWrap: 'anywhere' }}>{status.text}</span>
</div> </div>
{data.state === 'failed' && data.error && !isDevUnavailableError(data.error) && ( {data.state === 'failed' && data.error && !isDevUnavailableError(data.error) && (
<div style={{ color: 'var(--red-fg)', ...small, marginTop: 4, overflowWrap: 'anywhere' }}>{data.error}</div> <p className="msg err">{data.error}</p>
)} )}
</> </>
)} )}
{inactive && <div className="text-grey" style={{ ...small, marginTop: 8 }}>{NOT_ACTIVE}.</div>} {inactive && <p className="muted xs">{NOT_ACTIVE}.</p>}
{error && <div style={{ color: 'var(--red-fg)', ...small, marginTop: 8, overflowWrap: 'anywhere' }}>{error}</div>} {error && <p className="msg err" role="alert">{error}</p>}
{data && !inactive && ( {data && !inactive && (
<> <>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 10 }}> <div className="pv-row">
<button type="button" className="btn btn-sm" disabled={busy || state === 'building'} onClick={() => run(() => rebuildPreview(projectId))}> <Button type="button" disabled={busy || state === 'building'} onClick={() => run(() => rebuildPreview(projectId))}>
Neu bauen Neu bauen
</button> </Button>
<button type="button" className="btn btn-sm" disabled={busy || logs.length === 0} onClick={() => setShowLogs((v) => !v)}> <Button type="button" disabled={busy || logs.length === 0} onClick={() => setShowLogs((v) => !v)}>
Protokoll ansehen Protokoll ansehen
</button> </Button>
{dev && ( {dev && (
<> <>
<button type="button" className="btn btn-sm" disabled={busy} onClick={() => run(() => startPreviewDev(projectId))}> <Button type="button" disabled={busy} onClick={() => run(() => startPreviewDev(projectId))}>
Jetzt starten Jetzt starten
</button> </Button>
<button type="button" className="btn btn-sm" disabled={busy} onClick={() => run(() => stopPreviewDev(projectId))}> <Button type="button" disabled={busy} onClick={() => run(() => stopPreviewDev(projectId))}>
Stoppen Stoppen
</button> </Button>
</> </>
)} )}
</div> </div>
{showLogs && ( {showLogs && (
<div style={{ marginTop: 8 }}> <>
<select <select
className="form-control" className="select"
style={{ maxWidth: '100%' }}
aria-label="Lauf wählen" aria-label="Lauf wählen"
value={logFile} value={logFile}
disabled={busy} disabled={busy}
@@ -250,47 +246,39 @@ export default function PreviewBlock({ projectId }) {
<option value="">Lauf wählen …</option> <option value="">Lauf wählen …</option>
{logs.map((l) => <option key={l.runFile} value={l.runFile}>{logDate(l.at)}</option>)} {logs.map((l) => <option key={l.runFile} value={l.runFile}>{logDate(l.at)}</option>)}
</select> </select>
{logFile && ( {logFile && <pre className="pv-log">{logText || '…'}</pre>}
<pre style={{ marginTop: 6, maxHeight: 260, overflow: 'auto', background: 'var(--bg-subtle)', padding: 8, borderRadius: 6, fontFamily: 'monospace', fontSize: 11, whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}> </>
{logText || '…'}
</pre>
)}
</div>
)} )}
<div style={{ marginTop: 14 }}> <h4 className="pv-title">Variablen</h4>
<strong style={small}>Variablen</strong> {names.length === 0 && <p className="muted xs">Keine Variablen.</p>}
<div style={{ marginTop: 6 }}>
{names.length === 0 && <div className="text-grey" style={small}>Keine Variablen.</div>}
{names.map((n) => ( {names.map((n) => (
<div key={n} style={{ display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center', padding: '4px 0', borderBottom: '1px solid var(--line)' }}> <div key={n} className="pv-var">
<code style={{ flex: '1 1 140px', overflowWrap: 'anywhere' }}>{n}</code> <code>{n}</code>
<span className="text-grey">•••</span> <span className="muted">•••</span>
{editName === n ? ( {editName === n ? (
<> <>
<input className="form-control" style={{ flex: '1 1 120px', minWidth: 0 }} type="password" autoComplete="off" placeholder="Neuer Wert" value={editValue} onKeyDown={enter(handleChange)} onChange={(e) => setEditValue(e.target.value)} /> <input className="input" type="password" autoComplete="off" placeholder="Neuer Wert" value={editValue} onKeyDown={enter(handleChange)} onChange={(e) => setEditValue(e.target.value)} />
<button type="button" className="btn btn-sm" onClick={handleChange}>Übernehmen</button> <button type="button" className="link" onClick={handleChange}>Übernehmen</button>
</> </>
) : ( ) : (
<button type="button" className="btn btn-sm" disabled={busy} onClick={() => { setEditName(n); setEditValue(''); setEnvError('') }}>Ändern</button> <button type="button" className="link" disabled={busy} onClick={() => { setEditName(n); setEditValue(''); setEnvError('') }}>Ändern</button>
)} )}
<button type="button" className="btn btn-sm" disabled={busy} onClick={() => handleRemove(n)}>Entfernen</button> <button type="button" className="link" disabled={busy} onClick={() => handleRemove(n)}>Entfernen</button>
</div> </div>
))} ))}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center', marginTop: 8 }}> <div className="pv-add">
<input type="text" className="form-control" style={{ flex: '1 1 140px', minWidth: 0 }} placeholder="NAME" onKeyDown={enter(handleAdd)} value={newName} onChange={(e) => setNewName(e.target.value)} /> <input type="text" className="input" placeholder="NAME" onKeyDown={enter(handleAdd)} value={newName} onChange={(e) => setNewName(e.target.value)} />
<input type="password" autoComplete="off" className="form-control" style={{ flex: '1 1 140px', minWidth: 0 }} placeholder="Wert" onKeyDown={enter(handleAdd)} value={newValue} onChange={(e) => setNewValue(e.target.value)} /> <input type="password" autoComplete="off" className="input" placeholder="Wert" onKeyDown={enter(handleAdd)} value={newValue} onChange={(e) => setNewValue(e.target.value)} />
<button type="button" className="btn btn-sm" disabled={busy} onClick={handleAdd}>Hinzufügen</button> <Button type="button" disabled={busy} onClick={handleAdd}>Hinzufügen</Button>
</div> </div>
{envError && <div style={{ color: 'var(--red-fg)', ...small, marginTop: 4 }}>{envError}</div>} {envError && <p className="msg err">{envError}</p>}
{dirty && ( {dirty && (
<div style={{ marginTop: 8 }}> <div className="pv-row">
<button type="button" className="btn btn-sm" disabled={busy} onClick={handleSaveEnv}>Variablen speichern</button> <Button type="button" disabled={busy} onClick={handleSaveEnv}>Variablen speichern</Button>
</div> </div>
)} )}
<div className="text-grey" style={{ ...small, marginTop: 6 }}>Alles mit VITE_ / NEXT_PUBLIC_ ist später im Browser sichtbar.</div> <p className="muted xs">Alles mit VITE_ / NEXT_PUBLIC_ ist später im Browser sichtbar.</p>
</div>
</div>
</> </>
)} )}
</div> </div>
+1
View File
@@ -1150,6 +1150,7 @@ button.link { text-align: left; } /* Reset und Gewicht von button.link stehen im
.pv-var { padding-bottom: 8px; border-bottom: 1px solid var(--line); } .pv-var { padding-bottom: 8px; border-bottom: 1px solid var(--line); }
.pv-mode { width: 200px; max-width: 100%; } .pv-mode { width: 200px; max-width: 100%; }
.pv-title { font-size: 13px; font-weight: 600; } .pv-title { font-size: 13px; font-weight: 600; }
.pv-block .msg { overflow-wrap: anywhere; } /* Fehlertexte des Runners enthalten lange Pfade ohne Leerzeichen */
.pv-log { background: var(--bg-subtle); max-height: 260px; overflow: auto; white-space: pre-wrap; word-break: break-all; padding: 8px; border-radius: var(--radius); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; line-height: 16px; } .pv-log { background: var(--bg-subtle); max-height: 260px; overflow: auto; white-space: pre-wrap; word-break: break-all; padding: 8px; border-radius: var(--radius); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; line-height: 16px; }
.pv-var code, .pv-var .input, .pv-add .input { flex: 1 1 140px; min-width: 0; } .pv-var code, .pv-var .input, .pv-add .input { flex: 1 1 140px; min-width: 0; }
.pv-var code { overflow-wrap: anywhere; } .pv-var code { overflow-wrap: anywhere; }