feat(cards/new): Live-Kartenvorschau neben dem Formular
- 2-Spalten-Layout: Formular links, sticky Preview rechts - CardSurface (hero, raised) als Preview-Container - Typ-spezifische Vorschau: - basic/basic-reverse: Flip-Toggle Vorderseite ↔ Rückseite - multiple-choice: Frage + Option-Buttons mit grüner Markierung - typing: Frage + deaktiviertes Eingabefeld - audio-front: Play-Button-Mockup + Rückseite nach Flip - cloze: Erste-Cluster-Vorschau live - image-occlusion: Platzhalter-Hinweis - Preview aktualisiert sich reaktiv beim Tippen - Responsive: auf < 800px Preview über Formular Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b5d3a29335
commit
9754718157
1 changed files with 609 additions and 246 deletions
|
|
@ -15,6 +15,7 @@
|
|||
import { toasts } from '$lib/stores/toasts.svelte.ts';
|
||||
import { t } from '$lib/i18n/index.svelte.ts';
|
||||
import ImageOcclusionEditor from '$lib/components/ImageOcclusionEditor.svelte';
|
||||
import CardSurface from '$lib/components/CardSurface.svelte';
|
||||
|
||||
type DeckLite = { id: string; name: string };
|
||||
|
||||
|
|
@ -30,6 +31,7 @@
|
|||
let answer = $state('');
|
||||
let audioFileRef = $state('');
|
||||
let saving = $state(false);
|
||||
let previewFlipped = $state(false);
|
||||
|
||||
// Multiple-Choice builder state
|
||||
let mcOptions = $state(['', '', '', '']);
|
||||
|
|
@ -37,13 +39,20 @@
|
|||
|
||||
const frontHtml = $derived(renderMarkdown(front));
|
||||
const backHtml = $derived(renderMarkdown(back));
|
||||
const answerHtml = $derived(renderMarkdown(answer));
|
||||
const clusterIds = $derived(extractClusterIds(text));
|
||||
const clozePreviewHtml = $derived.by(() => {
|
||||
const firstCluster = clusterIds[0];
|
||||
if (firstCluster === undefined) return '';
|
||||
if (firstCluster === undefined) return renderMarkdown(text);
|
||||
return renderMarkdown(renderClozePrompt(text, firstCluster));
|
||||
});
|
||||
|
||||
// Reset flip state when type changes
|
||||
$effect(() => {
|
||||
cardType;
|
||||
previewFlipped = false;
|
||||
});
|
||||
|
||||
const TYPE_DESCRIPTIONS: Record<CardType, string> = {
|
||||
basic: 'Klassische Karteikarte: Vorderseite → Rückseite.',
|
||||
'basic-reverse': 'Wie Basic, aber beide Richtungen werden abgefragt (2 Reviews).',
|
||||
|
|
@ -145,6 +154,8 @@
|
|||
<a href={deckId ? `/decks/${deckId}` : '/decks'} class="back-link">{t('card_new.back')}</a>
|
||||
<h1 class="page-title">{t('card_new.title')}</h1>
|
||||
|
||||
<div class="page-layout">
|
||||
<!-- LEFT: Form -->
|
||||
<form class="card-form" onsubmit={onSubmit}>
|
||||
<!-- Deck + Typ -->
|
||||
<section class="form-section">
|
||||
|
|
@ -174,7 +185,7 @@
|
|||
<p class="type-hint">{TYPE_DESCRIPTIONS[cardType]}</p>
|
||||
</section>
|
||||
|
||||
<!-- Type-specific content -->
|
||||
<!-- Type-specific fields -->
|
||||
<section class="form-section">
|
||||
{#if cardType === 'image-occlusion'}
|
||||
<ImageOcclusionEditor bind:imageRef bind:maskRegionsJson />
|
||||
|
|
@ -198,14 +209,6 @@
|
|||
</span>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
{#if clozePreviewHtml}
|
||||
<div class="preview-box">
|
||||
<span class="preview-label">{t('card_new.cloze_preview_label', { first: clusterIds[0] ?? 1 })}</span>
|
||||
<div class="prose prose-sm">{@html clozePreviewHtml}</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<label class="field">
|
||||
<span class="field-label">{t('card_new.cloze_extra_label')}</span>
|
||||
<textarea
|
||||
|
|
@ -226,17 +229,10 @@
|
|||
placeholder={t('card_new.front_placeholder')}
|
||||
class="input mono"
|
||||
></textarea>
|
||||
{#if front.trim()}
|
||||
<div class="preview-box mt-2">
|
||||
<span class="preview-label">{t('card_new.preview_label')}</span>
|
||||
<div class="prose prose-sm">{@html frontHtml}</div>
|
||||
</div>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<div class="field">
|
||||
<span class="field-label">Antwortoptionen</span>
|
||||
<span class="field-hint">Markiere die richtige Antwort. Nicht befüllte Optionen werden ignoriert — KI ergänzt fehlende Distractors beim Lernen.</span>
|
||||
<span class="field-hint">Markiere die richtige Antwort. Leere Optionen werden ignoriert — KI ergänzt fehlende Distractors automatisch.</span>
|
||||
<div class="mc-options">
|
||||
{#each mcOptions as opt, i}
|
||||
{@const letter = ['A', 'B', 'C', 'D'][i]}
|
||||
|
|
@ -247,7 +243,6 @@
|
|||
class="mc-radio"
|
||||
class:mc-radio-selected={isCorrect}
|
||||
onclick={() => { mcCorrectIdx = i; }}
|
||||
title="Als richtige Antwort markieren"
|
||||
aria-label="Option {letter} als richtig markieren"
|
||||
aria-pressed={isCorrect}
|
||||
>
|
||||
|
|
@ -264,7 +259,7 @@
|
|||
placeholder="Option {letter}"
|
||||
class="mc-input"
|
||||
/>
|
||||
{#if isCorrect}
|
||||
{#if isCorrect && opt.trim()}
|
||||
<span class="mc-badge">✓ Richtig</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -283,14 +278,7 @@
|
|||
placeholder={t('card_new.front_placeholder')}
|
||||
class="input mono"
|
||||
></textarea>
|
||||
{#if front.trim()}
|
||||
<div class="preview-box mt-2">
|
||||
<span class="preview-label">{t('card_new.preview_label')}</span>
|
||||
<div class="prose prose-sm">{@html frontHtml}</div>
|
||||
</div>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span class="field-label">{t('card_new.answer_label')}</span>
|
||||
<textarea
|
||||
|
|
@ -300,7 +288,7 @@
|
|||
placeholder={t('card_new.answer_placeholder')}
|
||||
class="input mono"
|
||||
></textarea>
|
||||
<span class="field-hint">Kommagetrennte Aliase sind erlaubt: <code>Paris, Paris (Frankreich)</code></span>
|
||||
<span class="field-hint">Kommagetrennte Aliase erlaubt: <code>Paris, Paris (Frankreich)</code></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
|
@ -326,12 +314,6 @@
|
|||
class="input mono"
|
||||
></textarea>
|
||||
</label>
|
||||
{#if back.trim()}
|
||||
<div class="preview-box">
|
||||
<span class="preview-label">{t('card_new.preview_label')}</span>
|
||||
<div class="prose prose-sm">{@html backHtml}</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{:else}
|
||||
<!-- basic / basic-reverse -->
|
||||
|
|
@ -341,33 +323,20 @@
|
|||
<textarea
|
||||
bind:value={front}
|
||||
required
|
||||
rows="8"
|
||||
rows="9"
|
||||
placeholder={t('card_new.front_placeholder')}
|
||||
class="input mono"
|
||||
></textarea>
|
||||
{#if front.trim()}
|
||||
<div class="preview-box mt-2">
|
||||
<span class="preview-label">{t('card_new.preview_label')}</span>
|
||||
<div class="prose prose-sm">{@html frontHtml}</div>
|
||||
</div>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span class="field-label">{t('card_new.back_label')}</span>
|
||||
<textarea
|
||||
bind:value={back}
|
||||
required
|
||||
rows="8"
|
||||
rows="9"
|
||||
placeholder={t('card_new.back_placeholder')}
|
||||
class="input mono"
|
||||
></textarea>
|
||||
{#if back.trim()}
|
||||
<div class="preview-box mt-2">
|
||||
<span class="preview-label">{t('card_new.preview_label')}</span>
|
||||
<div class="prose prose-sm">{@html backHtml}</div>
|
||||
</div>
|
||||
{/if}
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -381,11 +350,158 @@
|
|||
<a href={deckId ? `/decks/${deckId}` : '/decks'} class="btn-ghost">{t('card_new.cancel')}</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- RIGHT: Live-Vorschau -->
|
||||
<aside class="preview-col">
|
||||
<div class="preview-sticky">
|
||||
<p class="preview-pane-label">Live-Vorschau</p>
|
||||
|
||||
<CardSurface size="hero" raised class="preview-card-surface">
|
||||
{#snippet children()}
|
||||
<div class="preview-inner">
|
||||
{#if cardType === 'image-occlusion'}
|
||||
<div class="preview-placeholder">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" />
|
||||
<circle cx="8.5" cy="8.5" r="1.5" />
|
||||
<path d="m21 15-5-5L5 21" />
|
||||
</svg>
|
||||
<span>Bildvorschau nicht verfügbar</span>
|
||||
</div>
|
||||
|
||||
{:else if cardType === 'audio-front'}
|
||||
<div class="preview-audio">
|
||||
<div class="preview-audio-btn">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
{#if audioFileRef}
|
||||
<span class="preview-audio-ref">{audioFileRef}</span>
|
||||
{:else}
|
||||
<span class="preview-empty-hint">Audio-Datei</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if previewFlipped}
|
||||
<div class="preview-divider"></div>
|
||||
<div class="preview-prose">
|
||||
{#if back.trim()}
|
||||
{@html backHtml}
|
||||
{:else}
|
||||
<span class="preview-empty-hint">Antworttext…</span>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<button type="button" class="preview-reveal-btn" onclick={() => { previewFlipped = true; }}>
|
||||
Lösung zeigen
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{:else if cardType === 'cloze'}
|
||||
<div class="preview-prose">
|
||||
{#if text.trim()}
|
||||
{@html clozePreviewHtml}
|
||||
{:else}
|
||||
<span class="preview-empty-hint">Lückentext…</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{:else if cardType === 'multiple-choice'}
|
||||
<div class="preview-prose preview-prompt-mc">
|
||||
{#if front.trim()}
|
||||
{@html frontHtml}
|
||||
{:else}
|
||||
<span class="preview-empty-hint">Frage…</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="preview-mc">
|
||||
{#each mcOptions as opt, i}
|
||||
{@const letter = ['A', 'B', 'C', 'D'][i]}
|
||||
{@const isCorrect = mcCorrectIdx === i}
|
||||
{#if opt.trim()}
|
||||
<div class="preview-mc-opt" class:preview-mc-opt-correct={isCorrect}>
|
||||
<span class="preview-mc-key">{letter}</span>
|
||||
<span class="preview-mc-text">{opt}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if mcOptions.every((o) => !o.trim())}
|
||||
<span class="preview-empty-hint" style="font-size:0.8125rem">Optionen A–D eintragen…</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{:else if cardType === 'typing'}
|
||||
<div class="preview-prose">
|
||||
{#if front.trim()}
|
||||
{@html frontHtml}
|
||||
{:else}
|
||||
<span class="preview-empty-hint">Frage…</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="preview-typing">
|
||||
<input
|
||||
type="text"
|
||||
disabled
|
||||
placeholder="Antwort eingeben…"
|
||||
class="preview-typing-input"
|
||||
/>
|
||||
<button type="button" class="preview-typing-btn" disabled>↵</button>
|
||||
</div>
|
||||
{#if previewFlipped && answer.trim()}
|
||||
<div class="preview-divider"></div>
|
||||
<div class="preview-answer-badge preview-answer-badge-correct">✓ Richtig</div>
|
||||
<div class="preview-prose" style="font-size:0.875rem">
|
||||
{@html answerHtml}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{:else}
|
||||
<!-- basic / basic-reverse -->
|
||||
{#if previewFlipped}
|
||||
<div class="preview-side-label">Rückseite</div>
|
||||
<div class="preview-prose">
|
||||
{#if back.trim()}
|
||||
{@html backHtml}
|
||||
{:else}
|
||||
<span class="preview-empty-hint">Rückseite…</span>
|
||||
{/if}
|
||||
</div>
|
||||
<button type="button" class="preview-flip-link" onclick={() => { previewFlipped = false; }}>
|
||||
← Vorderseite
|
||||
</button>
|
||||
{:else}
|
||||
<div class="preview-side-label">Vorderseite</div>
|
||||
<div class="preview-prose">
|
||||
{#if front.trim()}
|
||||
{@html frontHtml}
|
||||
{:else}
|
||||
<span class="preview-empty-hint">Vorderseite…</span>
|
||||
{/if}
|
||||
</div>
|
||||
<button type="button" class="preview-reveal-btn" onclick={() => { previewFlipped = true; }}>
|
||||
Lösung zeigen
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</CardSurface>
|
||||
|
||||
{#if cardType === 'basic' || cardType === 'basic-reverse' || cardType === 'audio-front'}
|
||||
<p class="preview-flip-hint">Klicke „Lösung zeigen" um zu flippen</p>
|
||||
{:else if cardType === 'multiple-choice'}
|
||||
<p class="preview-flip-hint">Vorschau aktualisiert sich live</p>
|
||||
{:else if cardType === 'typing'}
|
||||
<p class="preview-flip-hint">Eingabe-Feld ist im Lern-Modus aktiv</p>
|
||||
{/if}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page-shell {
|
||||
max-width: 52rem;
|
||||
max-width: 72rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
|
|
@ -406,6 +522,22 @@
|
|||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
/* 2-column layout: form | preview */
|
||||
.page-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 22rem;
|
||||
gap: 1.75rem;
|
||||
align-items: start;
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
.page-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.preview-col {
|
||||
order: -1;
|
||||
}
|
||||
}
|
||||
|
||||
.card-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -449,6 +581,12 @@
|
|||
color: hsl(var(--color-muted-foreground));
|
||||
line-height: 1.4;
|
||||
}
|
||||
.field-hint code {
|
||||
font-size: 0.75rem;
|
||||
background: hsl(var(--color-muted) / 0.5);
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
.field-error {
|
||||
font-size: 0.75rem;
|
||||
|
|
@ -465,7 +603,7 @@
|
|||
padding: 0.5rem 0.625rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
transition: border-color 0.12s;
|
||||
transition: border-color 0.12s, box-shadow 0.12s;
|
||||
resize: vertical;
|
||||
}
|
||||
.input:focus {
|
||||
|
|
@ -479,27 +617,9 @@
|
|||
font-size: 0.8125rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
margin: 0;
|
||||
padding-top: 0.125rem;
|
||||
}
|
||||
|
||||
.preview-box {
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: 0.5rem;
|
||||
background: hsl(var(--color-surface));
|
||||
padding: 0.625rem 0.75rem;
|
||||
}
|
||||
|
||||
.preview-label {
|
||||
display: block;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
/* Multiple-Choice option builder */
|
||||
/* MC option builder */
|
||||
.mc-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -533,15 +653,14 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: hsl(var(--color-primary));
|
||||
transition: border-color 0.12s, background-color 0.12s;
|
||||
transition: border-color 0.12s;
|
||||
padding: 0;
|
||||
color: hsl(var(--color-success));
|
||||
}
|
||||
.mc-radio:hover { border-color: hsl(var(--color-primary) / 0.6); }
|
||||
.mc-radio-selected {
|
||||
border-color: hsl(var(--color-success));
|
||||
background: hsl(var(--color-success) / 0.12);
|
||||
color: hsl(var(--color-success));
|
||||
}
|
||||
|
||||
.mc-letter {
|
||||
|
|
@ -618,13 +737,257 @@
|
|||
}
|
||||
.btn-ghost:hover { color: hsl(var(--color-foreground)); }
|
||||
|
||||
/* prose reset inside preview-box */
|
||||
.prose :global(p) { margin: 0 0 0.5em; font-size: 0.875rem; }
|
||||
.prose :global(p:last-child) { margin-bottom: 0; }
|
||||
.prose :global(code) {
|
||||
/* Preview column */
|
||||
.preview-col {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.preview-sticky {
|
||||
position: sticky;
|
||||
top: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.preview-pane-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.preview-flip-hint {
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* CardSurface override: full width, auto height */
|
||||
:global(.preview-card-surface) {
|
||||
max-width: none !important;
|
||||
aspect-ratio: unset !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.preview-inner {
|
||||
padding: 1.375rem 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.875rem;
|
||||
min-height: 14rem;
|
||||
}
|
||||
|
||||
.preview-prose {
|
||||
font-size: 1rem;
|
||||
line-height: 1.55;
|
||||
color: hsl(var(--color-foreground));
|
||||
flex: 1;
|
||||
}
|
||||
.preview-prose :global(p) { margin: 0 0 0.5em; }
|
||||
.preview-prose :global(p:last-child) { margin-bottom: 0; }
|
||||
.preview-prose :global(h1) {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.preview-prose :global(table) {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.preview-prose :global(td) {
|
||||
padding: 0.25rem 0.375rem;
|
||||
border-bottom: 1px solid hsl(var(--color-border));
|
||||
}
|
||||
.preview-prose :global(thead) { display: none; }
|
||||
|
||||
.preview-prompt-mc {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.preview-side-label {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.preview-empty-hint {
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
font-style: italic;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.preview-divider {
|
||||
height: 1px;
|
||||
background: hsl(var(--color-border));
|
||||
margin: 0.125rem 0;
|
||||
}
|
||||
|
||||
.preview-reveal-btn {
|
||||
align-self: flex-start;
|
||||
padding: 0.4rem 0.875rem;
|
||||
border-radius: 0.5rem;
|
||||
background: hsl(var(--color-primary));
|
||||
color: hsl(var(--color-primary-foreground));
|
||||
border: none;
|
||||
font: inherit;
|
||||
font-size: 0.8125rem;
|
||||
background: hsl(var(--color-muted) / 0.5);
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 0.25rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.12s;
|
||||
margin-top: auto;
|
||||
}
|
||||
.preview-reveal-btn:hover { background: hsl(var(--color-primary) / 0.88); }
|
||||
|
||||
.preview-flip-link {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
align-self: flex-start;
|
||||
margin-top: auto;
|
||||
}
|
||||
.preview-flip-link:hover { color: hsl(var(--color-foreground)); }
|
||||
|
||||
/* Multiple-choice preview */
|
||||
.preview-mc {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.preview-mc-opt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.625rem;
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: 0.375rem;
|
||||
background: hsl(var(--color-surface));
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.preview-mc-opt-correct {
|
||||
background: hsl(var(--color-success) / 0.1);
|
||||
border-color: hsl(var(--color-success) / 0.5);
|
||||
}
|
||||
|
||||
.preview-mc-key {
|
||||
flex-shrink: 0;
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
border-radius: 0.2rem;
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
background: hsl(var(--color-surface));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 700;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
.preview-mc-opt-correct .preview-mc-key {
|
||||
border-color: hsl(var(--color-success) / 0.5);
|
||||
color: hsl(var(--color-success));
|
||||
background: hsl(var(--color-success) / 0.12);
|
||||
}
|
||||
|
||||
.preview-mc-text {
|
||||
flex: 1;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
/* Typing preview */
|
||||
.preview-typing {
|
||||
display: flex;
|
||||
gap: 0.375rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.preview-typing-input {
|
||||
flex: 1;
|
||||
padding: 0.4rem 0.625rem;
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: 0.375rem;
|
||||
background: hsl(var(--color-surface));
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
font: inherit;
|
||||
font-size: 0.875rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.preview-typing-btn {
|
||||
padding: 0.4rem 0.625rem;
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: 0.375rem;
|
||||
background: hsl(var(--color-surface));
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
font: inherit;
|
||||
font-size: 0.875rem;
|
||||
cursor: default;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.preview-answer-badge {
|
||||
display: inline-block;
|
||||
padding: 0.1875rem 0.5rem;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.preview-answer-badge-correct {
|
||||
background: hsl(var(--color-success) / 0.15);
|
||||
color: hsl(var(--color-success));
|
||||
}
|
||||
|
||||
/* Audio preview */
|
||||
.preview-audio {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.preview-audio-btn {
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
border-radius: 50%;
|
||||
background: hsl(var(--color-primary));
|
||||
color: hsl(var(--color-primary-foreground));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.preview-audio-ref {
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
font-family: ui-monospace, monospace;
|
||||
}
|
||||
|
||||
/* Placeholder */
|
||||
.preview-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.625rem;
|
||||
flex: 1;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
font-size: 0.875rem;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue