Frontend: locks vom Backend, uiError sichtbar, Pausiert-Badge, Inline-Progress, Mobile-Exklusivität, Fehler-Persistenz
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, onMounted, nextTick } from 'vue'
|
import { ref, computed, watch, onMounted, nextTick } from 'vue'
|
||||||
import { fetchGuides, fetchTopics, createTopic as apiCreateTopic, deleteTopic as apiDeleteTopic, createGuide as apiCreate, deleteGuide, cancelGuide as apiCancel, fetchBausteineStatus, fetchActiveBausteine, createBausteine as apiCreateBausteine, cancelBausteine as apiCancelBausteine, deleteBausteine as apiDeleteBausteine, fetchProjects, deleteProject as apiDeleteProject, fetchProviders, fetchStats, fetchTopicFortschritt } from './api.js'
|
import { fetchGuides, fetchTopics, createTopic as apiCreateTopic, deleteTopic as apiDeleteTopic, createGuide as apiCreate, deleteGuide, cancelGuide as apiCancel, fetchBausteineStatus, fetchActiveBausteine, createBausteine as apiCreateBausteine, cancelBausteine as apiCancelBausteine, deleteBausteine as apiDeleteBausteine, fetchProjects, deleteProject as apiDeleteProject, fetchProviders, fetchStats, fetchTopicFortschritt, fetchGuideLocks } from './api.js'
|
||||||
import { usePolling } from './composables/usePolling.js'
|
import { usePolling } from './composables/usePolling.js'
|
||||||
import TopicSidebar from './components/TopicSidebar.vue'
|
import TopicSidebar from './components/TopicSidebar.vue'
|
||||||
import TopicDetail from './components/TopicDetail.vue'
|
import TopicDetail from './components/TopicDetail.vue'
|
||||||
@@ -26,6 +26,8 @@ const provider = ref(localStorage.getItem('provider') || 'claude')
|
|||||||
const providers = ref([])
|
const providers = ref([])
|
||||||
const stats = ref(null)
|
const stats = ref(null)
|
||||||
const fortschritt = ref({})
|
const fortschritt = ref({})
|
||||||
|
const locks = ref({}) // Sperr-Gründe pro Format (Backend = einzige Regel-Quelle)
|
||||||
|
const uiError = ref(null) // abgewiesene Aktionen (409/400) sichtbar machen
|
||||||
const elementsOpen = ref(false) // rechte Sidebar
|
const elementsOpen = ref(false) // rechte Sidebar
|
||||||
const elementsView = ref(false) // Übersicht im Hauptbereich
|
const elementsView = ref(false) // Übersicht im Hauptbereich
|
||||||
const elementsVersion = ref(0) // Erhöhung = Übersicht neu laden
|
const elementsVersion = ref(0) // Erhöhung = Übersicht neu laden
|
||||||
@@ -126,21 +128,27 @@ async function loadTopics() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fehlermeldungen verhalten sich wie Flash-Messages: × blendet aus,
|
// Weggeklickte Fehler bleiben weggeklickt — auch über Reloads (localStorage).
|
||||||
// beim Reload sind Alt-Fehler von vornherein ausgeblendet.
|
// Nicht weggeklickte Fehler bleiben sichtbar, bis der Nutzer sie schließt.
|
||||||
const dismissedErrors = ref(new Set())
|
const dismissedErrors = ref(new Set(JSON.parse(localStorage.getItem('dismissedErrors') || '[]')))
|
||||||
let errorsInitialized = false
|
|
||||||
|
function persistDismissed() {
|
||||||
|
localStorage.setItem('dismissedErrors', JSON.stringify([...dismissedErrors.value]))
|
||||||
|
}
|
||||||
|
|
||||||
function handleDismissError(guideId) {
|
function handleDismissError(guideId) {
|
||||||
dismissedErrors.value = new Set([...dismissedErrors.value, guideId])
|
dismissedErrors.value = new Set([...dismissedErrors.value, guideId])
|
||||||
|
persistDismissed()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadGuides() {
|
async function loadGuides() {
|
||||||
try {
|
try {
|
||||||
guides.value = await fetchGuides()
|
guides.value = await fetchGuides()
|
||||||
if (!errorsInitialized) {
|
// IDs prunen, deren Guide nicht mehr als Fehler existiert
|
||||||
errorsInitialized = true
|
const errorIds = new Set(guides.value.filter((g) => g.status === 'error').map((g) => g.id))
|
||||||
dismissedErrors.value = new Set(guides.value.filter((g) => g.status === 'error').map((g) => g.id))
|
if ([...dismissedErrors.value].some((id) => !errorIds.has(id))) {
|
||||||
|
dismissedErrors.value = new Set([...dismissedErrors.value].filter((id) => errorIds.has(id)))
|
||||||
|
persistDismissed()
|
||||||
}
|
}
|
||||||
loadStats()
|
loadStats()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -154,9 +162,11 @@ async function loadBausteine() {
|
|||||||
if (selectedTopic.value) {
|
if (selectedTopic.value) {
|
||||||
bausteine.value = await fetchBausteineStatus(selectedTopic.value)
|
bausteine.value = await fetchBausteineStatus(selectedTopic.value)
|
||||||
fortschritt.value = await fetchTopicFortschritt(selectedTopic.value)
|
fortschritt.value = await fetchTopicFortschritt(selectedTopic.value)
|
||||||
|
locks.value = await fetchGuideLocks(selectedTopic.value)
|
||||||
} else {
|
} else {
|
||||||
bausteine.value = { ...EMPTY_BAUSTEINE }
|
bausteine.value = { ...EMPTY_BAUSTEINE }
|
||||||
fortschritt.value = {}
|
fortschritt.value = {}
|
||||||
|
locks.value = {}
|
||||||
}
|
}
|
||||||
if (activeBausteine.value.length && !polling.running()) startPolling()
|
if (activeBausteine.value.length && !polling.running()) startPolling()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -224,7 +234,13 @@ async function handleResetBausteine() {
|
|||||||
|
|
||||||
async function handleBausteineClick({ instructions }) {
|
async function handleBausteineClick({ instructions }) {
|
||||||
if (!selectedTopic.value) return
|
if (!selectedTopic.value) return
|
||||||
|
uiError.value = null
|
||||||
|
try {
|
||||||
await apiCreateBausteine(selectedTopic.value, instructions, provider.value)
|
await apiCreateBausteine(selectedTopic.value, instructions, provider.value)
|
||||||
|
} catch (e) {
|
||||||
|
uiError.value = e.message
|
||||||
|
return
|
||||||
|
}
|
||||||
await loadBausteine()
|
await loadBausteine()
|
||||||
startPolling()
|
startPolling()
|
||||||
}
|
}
|
||||||
@@ -237,7 +253,13 @@ async function handleFormatClick({ format, instructions }) {
|
|||||||
&& (g.status === 'generating' || g.status === 'queued'),
|
&& (g.status === 'generating' || g.status === 'queued'),
|
||||||
)
|
)
|
||||||
if (running) return
|
if (running) return
|
||||||
|
uiError.value = null
|
||||||
|
try {
|
||||||
await apiCreate(selectedTopic.value, format, instructions, provider.value)
|
await apiCreate(selectedTopic.value, format, instructions, provider.value)
|
||||||
|
} catch (e) {
|
||||||
|
uiError.value = e.message
|
||||||
|
return
|
||||||
|
}
|
||||||
await loadGuides()
|
await loadGuides()
|
||||||
startPolling()
|
startPolling()
|
||||||
}
|
}
|
||||||
@@ -327,6 +349,8 @@ onMounted(async () => {
|
|||||||
:selectedTopic="selectedTopic"
|
:selectedTopic="selectedTopic"
|
||||||
:stats="stats"
|
:stats="stats"
|
||||||
:fortschritt="fortschritt"
|
:fortschritt="fortschritt"
|
||||||
|
:locks="locks"
|
||||||
|
:uiError="uiError"
|
||||||
:doneByFormat="doneByFormat"
|
:doneByFormat="doneByFormat"
|
||||||
:latestByFormat="latestByFormat"
|
:latestByFormat="latestByFormat"
|
||||||
:allGuides="guides"
|
:allGuides="guides"
|
||||||
@@ -350,6 +374,7 @@ onMounted(async () => {
|
|||||||
@cancelGuide="handleCancel"
|
@cancelGuide="handleCancel"
|
||||||
@deleteGuide="handleDeleteGuide"
|
@deleteGuide="handleDeleteGuide"
|
||||||
@dismissError="handleDismissError"
|
@dismissError="handleDismissError"
|
||||||
|
@dismissUiError="uiError = null"
|
||||||
@preview="handlePreview"
|
@preview="handlePreview"
|
||||||
@openElements="handleOpenElements"
|
@openElements="handleOpenElements"
|
||||||
@togglePin="toggleSidebarPin"
|
@togglePin="toggleSidebarPin"
|
||||||
|
|||||||
@@ -1,17 +1,35 @@
|
|||||||
const BASE = '/api'
|
const BASE = '/api'
|
||||||
|
|
||||||
|
// Backend-Fehler (400/409 mit detail) als Error werfen statt sie zu verschlucken
|
||||||
|
async function jsonOrThrow(res) {
|
||||||
|
if (!res.ok) {
|
||||||
|
let detail = `Fehler (HTTP ${res.status})`
|
||||||
|
try {
|
||||||
|
const data = await res.json()
|
||||||
|
if (data.detail) detail = typeof data.detail === 'string' ? data.detail : JSON.stringify(data.detail)
|
||||||
|
} catch { /* kein JSON-Body */ }
|
||||||
|
throw new Error(detail)
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchGuides() {
|
export async function fetchGuides() {
|
||||||
const res = await fetch(`${BASE}/guides`)
|
const res = await fetch(`${BASE}/guides`)
|
||||||
return res.json()
|
return res.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchGuideLocks(topic) {
|
||||||
|
const res = await fetch(`${BASE}/guides/locks?topic=${encodeURIComponent(topic)}`)
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
export async function createGuide(topic, format, instructions = '', provider = 'claude') {
|
export async function createGuide(topic, format, instructions = '', provider = 'claude') {
|
||||||
const res = await fetch(`${BASE}/guides`, {
|
const res = await fetch(`${BASE}/guides`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ topic, format, instructions, provider }),
|
body: JSON.stringify({ topic, format, instructions, provider }),
|
||||||
})
|
})
|
||||||
return res.json()
|
return jsonOrThrow(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchActiveBausteine() {
|
export async function fetchActiveBausteine() {
|
||||||
@@ -30,7 +48,7 @@ export async function createBausteine(topic, instructions = '', provider = 'clau
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ topic, instructions, provider }),
|
body: JSON.stringify({ topic, instructions, provider }),
|
||||||
})
|
})
|
||||||
return res.json()
|
return jsonOrThrow(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cancelBausteine(topic) {
|
export async function cancelBausteine(topic) {
|
||||||
|
|||||||
@@ -95,6 +95,12 @@ function closeChat() {
|
|||||||
chat.reset()
|
chat.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mobil schließen sich Chat und Elemente-Sidebar gegenseitig aus —
|
||||||
|
// nebeneinander ist kein Platz, die Sidebar würde den Chat überdecken.
|
||||||
|
watch(() => props.elementsOpen, (open) => {
|
||||||
|
if (open && chatOpen.value && window.matchMedia('(max-width: 768px)').matches) closeChat()
|
||||||
|
})
|
||||||
|
|
||||||
function onDocMouseDown(e) {
|
function onDocMouseDown(e) {
|
||||||
if (!chatOpen.value) return
|
if (!chatOpen.value) return
|
||||||
if (panelEl.value && panelEl.value.contains(e.target)) return
|
if (panelEl.value && panelEl.value.contains(e.target)) return
|
||||||
@@ -540,6 +546,14 @@ function extractContext() {
|
|||||||
right: calc(1.5rem + 320px);
|
right: calc(1.5rem + 320px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobil liegt die Elemente-Sidebar als Overlay über dem Chat — FAB/Panel ausblenden */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.chat-fab.shifted,
|
||||||
|
.chat-panel.shifted {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.chat-panel {
|
.chat-panel {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 1.5rem;
|
right: 1.5rem;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ const props = defineProps({
|
|||||||
selectedTopic: { type: String, default: null },
|
selectedTopic: { type: String, default: null },
|
||||||
stats: { type: Object, default: null },
|
stats: { type: Object, default: null },
|
||||||
fortschritt: { type: Object, default: () => ({}) },
|
fortschritt: { type: Object, default: () => ({}) },
|
||||||
|
locks: { type: Object, default: () => ({}) },
|
||||||
|
uiError: { type: String, default: null },
|
||||||
doneByFormat: { type: Object, default: () => ({}) },
|
doneByFormat: { type: Object, default: () => ({}) },
|
||||||
latestByFormat: { type: Object, default: () => ({}) },
|
latestByFormat: { type: Object, default: () => ({}) },
|
||||||
allGuides: { type: Array, default: () => [] },
|
allGuides: { type: Array, default: () => [] },
|
||||||
@@ -20,7 +22,7 @@ const props = defineProps({
|
|||||||
providers: { type: Array, default: () => [] },
|
providers: { type: Array, default: () => [] },
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['select', 'create', 'formatClick', 'bausteineClick', 'cancelBausteine', 'resetBausteine', 'deleteTopic', 'deleteProject', 'cancelGuide', 'deleteGuide', 'dismissError', 'preview', 'openElements', 'togglePin', 'sidebarLeave', 'toggleDark', 'setProvider'])
|
const emit = defineEmits(['select', 'create', 'formatClick', 'bausteineClick', 'cancelBausteine', 'resetBausteine', 'deleteTopic', 'deleteProject', 'cancelGuide', 'deleteGuide', 'dismissError', 'dismissUiError', 'preview', 'openElements', 'togglePin', 'sidebarLeave', 'toggleDark', 'setProvider'])
|
||||||
|
|
||||||
function providerAvailable(id) {
|
function providerAvailable(id) {
|
||||||
const p = props.providers.find((x) => x.id === id)
|
const p = props.providers.find((x) => x.id === id)
|
||||||
@@ -54,12 +56,13 @@ const bausteineState = computed(() => {
|
|||||||
return props.bausteine.ready ? 'done' : 'none'
|
return props.bausteine.ready ? 'done' : 'none'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Nur FREMDE Themen — das gewählte Thema zeigt seinen Fortschritt inline an der Zeile
|
||||||
const activeGenerations = computed(() => {
|
const activeGenerations = computed(() => {
|
||||||
const bausteinLines = props.activeBausteine.map(
|
const bausteinLines = props.activeBausteine
|
||||||
(b) => `${b.topic} – Bausteine: ${b.progress || 'Wartend…'}`,
|
.filter((b) => b.topic !== props.selectedTopic)
|
||||||
)
|
.map((b) => `${b.topic} – Bausteine: ${b.progress || 'Wartend…'}`)
|
||||||
const guideLines = props.allGuides
|
const guideLines = props.allGuides
|
||||||
.filter((g) => g.status === 'generating' || g.status === 'queued')
|
.filter((g) => (g.status === 'generating' || g.status === 'queued') && g.topic !== props.selectedTopic)
|
||||||
.map((g) => `${g.topic} – ${g.format}: ${g.progress || 'Wartend…'}`)
|
.map((g) => `${g.topic} – ${g.format}: ${g.progress || 'Wartend…'}`)
|
||||||
return [...bausteinLines, ...guideLines]
|
return [...bausteinLines, ...guideLines]
|
||||||
})
|
})
|
||||||
@@ -118,6 +121,7 @@ function guideSteps(format) {
|
|||||||
function errorMsg(format) {
|
function errorMsg(format) {
|
||||||
const latest = props.latestByFormat[format]
|
const latest = props.latestByFormat[format]
|
||||||
if (latest?.status !== 'error' || props.dismissedErrors.has(latest.id)) return ''
|
if (latest?.status !== 'error' || props.dismissedErrors.has(latest.id)) return ''
|
||||||
|
if (abgebrochen(format)) return '' // kein roter Fehler — das Pausiert-Badge zeigt den Zustand
|
||||||
return latest.error_msg || 'Fehler bei der Generierung'
|
return latest.error_msg || 'Fehler bei der Generierung'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,28 +138,11 @@ function handleFormatClick(format) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lernschulden-Regeln (nur Neu-Erstellungen; Resume + Neu-Generieren bestehender erlaubt):
|
// Sperr-Gründe kommen vom Backend (GET /guides/locks) — die Regeln existieren
|
||||||
// Progression pro Thema (MiniGuide → Guide → FullGuide) + max. 3 offene je Format.
|
// nur noch dort. Solange locks noch nicht geladen sind: Button frei, das
|
||||||
const VORSTUFE = { Guide: 'MiniGuide', FullGuide: 'Guide' }
|
// Backend weist ungültige Starts ohnehin ab (sichtbar über uiError).
|
||||||
|
|
||||||
function offeneGuides(format) {
|
|
||||||
const f = props.stats?.formate?.[format]
|
|
||||||
return (f?.erstellt ?? 0) - (f?.absolviert ?? 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function playLock(format) {
|
function playLock(format) {
|
||||||
if (format === 'OnePager') return null
|
return props.locks?.[format] ?? null
|
||||||
if (!props.bausteine.ready) return 'Erst Bausteine erstellen'
|
|
||||||
if (props.doneByFormat[format] || abgebrochen(format)) return null
|
|
||||||
const vorstufe = VORSTUFE[format]
|
|
||||||
if (vorstufe && !props.fortschritt?.[vorstufe]) {
|
|
||||||
return `Erst den ${vorstufe} dieses Themas absolvieren`
|
|
||||||
}
|
|
||||||
const offen = offeneGuides(format)
|
|
||||||
if (offen >= 3) {
|
|
||||||
return `Erst ${format}s absolvieren — ${offen} offen (max. 3)`
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePlay(format) {
|
function handlePlay(format) {
|
||||||
@@ -240,12 +227,21 @@ function confirmDeleteProject(name) {
|
|||||||
>{{ PROVIDER_LABELS[p.id] || p.id }}</button>
|
>{{ PROVIDER_LABELS[p.id] || p.id }}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="format-section" v-if="selectedTopic">
|
<div class="format-section" v-if="selectedTopic">
|
||||||
|
<div class="format-error ui-error" v-if="uiError">
|
||||||
|
<span class="format-error-text">{{ uiError }}</span>
|
||||||
|
<button class="format-error-x" title="Ausblenden" @click="emit('dismissUiError')">×</button>
|
||||||
|
</div>
|
||||||
<div class="progress-info" v-if="activeGenerations.length">
|
<div class="progress-info" v-if="activeGenerations.length">
|
||||||
<div v-for="(line, i) in activeGenerations" :key="i">{{ line }}</div>
|
<div v-for="(line, i) in activeGenerations" :key="i">{{ line }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="format-row bausteine-row ord-bausteine">
|
<div class="format-row bausteine-row ord-bausteine">
|
||||||
<div class="format-name bausteine-name">
|
<div class="format-name bausteine-name">
|
||||||
<span class="format-label">Bausteine</span>
|
<span class="format-label">Bausteine</span>
|
||||||
|
<span
|
||||||
|
v-if="bausteine.partial && bausteineState !== 'generating'"
|
||||||
|
class="resume-badge"
|
||||||
|
title="Abgebrochen — ▶ setzt fort"
|
||||||
|
>Pausiert</span>
|
||||||
<span class="step-dots">
|
<span class="step-dots">
|
||||||
<span
|
<span
|
||||||
v-for="s in (bausteine.steps || [])"
|
v-for="s in (bausteine.steps || [])"
|
||||||
@@ -280,7 +276,10 @@ function confirmDeleteProject(name) {
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="bausteine.error" class="format-error ord-bausteine">
|
<div v-if="bausteineState === 'generating'" class="format-progress ord-bausteine">
|
||||||
|
{{ bausteine.progress || 'Wartend…' }}
|
||||||
|
</div>
|
||||||
|
<div v-if="bausteine.error && !bausteine.error.startsWith('Abgebrochen')" class="format-error ord-bausteine">
|
||||||
<span class="format-error-text">{{ bausteine.error }}</span>
|
<span class="format-error-text">{{ bausteine.error }}</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- OnePager (unabhängig von Bausteinen) steht per CSS-order vor der Bausteine-Zeile -->
|
<!-- OnePager (unabhängig von Bausteinen) steht per CSS-order vor der Bausteine-Zeile -->
|
||||||
@@ -288,6 +287,11 @@ function confirmDeleteProject(name) {
|
|||||||
<div :class="['format-row', 'fmt-' + guideStatus(f.key)]">
|
<div :class="['format-row', 'fmt-' + guideStatus(f.key)]">
|
||||||
<button class="format-name" @click="handleFormatClick(f.key)">
|
<button class="format-name" @click="handleFormatClick(f.key)">
|
||||||
<span class="format-label">{{ f.label }}</span>
|
<span class="format-label">{{ f.label }}</span>
|
||||||
|
<span
|
||||||
|
v-if="abgebrochen(f.key)"
|
||||||
|
class="resume-badge"
|
||||||
|
title="Abgebrochen — ▶ setzt fort"
|
||||||
|
>Pausiert</span>
|
||||||
<span class="step-dots" v-if="guideSteps(f.key).length">
|
<span class="step-dots" v-if="guideSteps(f.key).length">
|
||||||
<span
|
<span
|
||||||
v-for="s in guideSteps(f.key)"
|
v-for="s in guideSteps(f.key)"
|
||||||
@@ -316,6 +320,10 @@ function confirmDeleteProject(name) {
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="guideStatus(f.key) === 'generating' || guideStatus(f.key) === 'queued'"
|
||||||
|
class="format-progress"
|
||||||
|
>{{ latestByFormat[f.key]?.progress || 'Wartend…' }}</div>
|
||||||
<div v-if="errorMsg(f.key)" class="format-error">
|
<div v-if="errorMsg(f.key)" class="format-error">
|
||||||
<span class="format-error-text">{{ errorMsg(f.key) }}</span>
|
<span class="format-error-text">{{ errorMsg(f.key) }}</span>
|
||||||
<button class="format-error-x" title="Ausblenden" @click="dismissError(f.key)">×</button>
|
<button class="format-error-x" title="Ausblenden" @click="dismissError(f.key)">×</button>
|
||||||
@@ -653,6 +661,35 @@ function confirmDeleteProject(name) {
|
|||||||
animation: pulse 1.5s ease-in-out infinite;
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Abgewiesene Aktion (409/400) — oberhalb aller Format-Zeilen */
|
||||||
|
.ui-error {
|
||||||
|
order: 0;
|
||||||
|
padding: 0.4rem 0.75rem;
|
||||||
|
background: var(--warning-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fortschritts-Text direkt unter der laufenden Format-Zeile */
|
||||||
|
.format-progress {
|
||||||
|
padding: 0 0.75rem 5px calc(0.75rem + 8px);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--warning);
|
||||||
|
line-height: 1.3;
|
||||||
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resume-badge {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
color: var(--warning);
|
||||||
|
background: var(--warning-soft);
|
||||||
|
border: 1px solid var(--warning-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.format-row {
|
.format-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user