feat(theming): forest variant from @mana/themes (sprint 9m)
Cards is the first app on the new 12-token mana-vereinsweite theming system (mana/docs/THEMING.md). Forest-Variant aus @mana/themes/variants/forest.css konsumiert via app.css-Import, data-theme="forest" in app.html. Token-Welt umgestellt — 158 renames + 304 hsl-wraps in 17 Files (Python-Refactor, BSD-sed war zu unzuverlässig): - --color-bg → --color-background - --color-fg → --color-foreground - --color-muted → --color-muted-foreground - --color-primary-fg → --color-primary-foreground - --color-danger → --color-error - bare var(--color-X) → hsl(var(--color-X)) durchgängig Bridge-Aliase in app.css mappen die shared-ui@0.1.x-Erwartungen (card, accent, surface-elevated-*, …) auf das 12er-Set. Mit shared-ui@2.0-Refactor entfällt diese Sektion. --brand-cards-forest als App-Identitäts-Hex separiert von Theme-Tokens. Header konsumiert PillTabGroup aus @mana/shared-ui@0.1.1 für die Routen-Navigation (Decks/Lernen/Library/Import/Stats) und den DE/EN-Sprach-Switcher — visuell konsistent mit Vereins-Standard. Cards' primary-Grün wurde dabei von 142 76% 36% (alter Live-Stand) auf 142 76% 28% verdunkelt, damit primary-foreground/primary- Kontrast WCAG-AA-konform (≥4.5) ist. Der alte Live-Stand hatte Ratio 3.35. i18n: deck_stack.aria_label, deck_detail.fan_aria, deck_detail. card_open, decks.card_count_more, study_session.manage_link. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
404ddec62d
commit
19a0036b82
20 changed files with 323 additions and 261 deletions
|
|
@ -1,38 +1,41 @@
|
|||
{
|
||||
"name": "@cards/web",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Cards-Web — SvelteKit 2 + Svelte 5 Frontend für cardecky.mana.how.",
|
||||
"scripts": {
|
||||
"dev": "vite dev --port 3082 --host",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"lint": "echo 'lint configured later (eslint flat-config)'",
|
||||
"clean": "rm -rf .svelte-kit build .turbo"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cards/domain": "workspace:*",
|
||||
"dompurify": "^3.4.2",
|
||||
"jszip": "^3.10.1",
|
||||
"marked": "^18.0.3",
|
||||
"sql.js": "^1.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-node": "^5.2.0",
|
||||
"@sveltejs/kit": "^2.8.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||
"@tailwindcss/vite": "^4.2.4",
|
||||
"@types/dompurify": "^3.2.0",
|
||||
"@types/jszip": "^3.4.1",
|
||||
"@types/sql.js": "^1.4.11",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"tailwindcss": "^4.2.4",
|
||||
"vite": "^5.4.0",
|
||||
"vitest": "^2.1.0"
|
||||
}
|
||||
"name": "@cards/web",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Cards-Web — SvelteKit 2 + Svelte 5 Frontend für cardecky.mana.how.",
|
||||
"scripts": {
|
||||
"dev": "vite dev --port 3082 --host",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"lint": "echo 'lint configured later (eslint flat-config)'",
|
||||
"clean": "rm -rf .svelte-kit build .turbo"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cards/domain": "workspace:*",
|
||||
"@mana/shared-icons": "^1.0.0",
|
||||
"@mana/shared-ui": "^0.1.1",
|
||||
"dompurify": "^3.4.2",
|
||||
"jszip": "^3.10.1",
|
||||
"marked": "^18.0.3",
|
||||
"sql.js": "^1.14.1",
|
||||
"@mana/themes": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-node": "^5.2.0",
|
||||
"@sveltejs/kit": "^2.8.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||
"@tailwindcss/vite": "^4.2.4",
|
||||
"@types/dompurify": "^3.2.0",
|
||||
"@types/jszip": "^3.4.1",
|
||||
"@types/sql.js": "^1.4.11",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"tailwindcss": "^4.2.4",
|
||||
"vite": "^5.4.0",
|
||||
"vitest": "^2.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,53 @@
|
|||
@import 'tailwindcss';
|
||||
@import '@mana/themes/variants/forest.css';
|
||||
|
||||
/**
|
||||
* Cards — Theming-Setup.
|
||||
*
|
||||
* 12-Token-Vokabular kommt aus `@mana/themes/variants/forest.css`.
|
||||
* `data-theme="forest"` ist in `app.html` gesetzt; Light/Dark via
|
||||
* Class `.dark` auf <html>. Konvention: `hsl(var(--color-X))`. Bare
|
||||
* `var(--color-X)` ist verboten (silent inherit-fallback). Detail in
|
||||
* mana/docs/THEMING.md.
|
||||
*
|
||||
* Diese Datei trägt nur Cards-spezifische Ergänzungen:
|
||||
* 1. Bridge-Aliase für shared-ui@0.1.x — bis shared-ui@2.0 entfällt
|
||||
* 2. Brand-Literals (--brand-cards-forest)
|
||||
* 3. Layer-Base-Regeln (html, focus-ring, sr-only, skip-link)
|
||||
* 4. Reduced-Motion-Override
|
||||
*/
|
||||
|
||||
@theme {
|
||||
--color-bg: oklch(0.99 0.005 240);
|
||||
--color-fg: oklch(0.20 0.02 240);
|
||||
--color-muted: oklch(0.55 0.02 240);
|
||||
--color-border: oklch(0.92 0.01 240);
|
||||
--color-card: oklch(1 0 0);
|
||||
--color-primary: oklch(0.55 0.15 250);
|
||||
--color-primary-fg: oklch(1 0 0);
|
||||
--color-success: oklch(0.6 0.15 145);
|
||||
--color-warning: oklch(0.75 0.15 75);
|
||||
--color-danger: oklch(0.55 0.18 25);
|
||||
/* ===== Bridge-Aliase für shared-ui@0.1.x =====
|
||||
* shared-ui@0.1.x erwartet ein 30-Token-Vokabular aus dem alten
|
||||
* managarten-System. Damit Komponenten wie PillTabGroup nicht
|
||||
* silent-fallback brechen, mappen wir die fehlenden Tokens auf
|
||||
* unser 12er-Set. Mit shared-ui@2.0 entfällt diese Sektion.
|
||||
*/
|
||||
--color-card: var(--color-surface);
|
||||
--color-card-foreground: var(--color-foreground);
|
||||
--color-popover: var(--color-surface);
|
||||
--color-popover-foreground: var(--color-foreground);
|
||||
--color-secondary: var(--color-surface);
|
||||
--color-secondary-foreground: var(--color-foreground);
|
||||
--color-accent: var(--color-primary);
|
||||
--color-accent-foreground: var(--color-primary-foreground);
|
||||
--color-input: var(--color-border);
|
||||
--color-ring: var(--color-primary);
|
||||
--color-border-strong: var(--color-foreground);
|
||||
--color-surface-elevated: var(--color-surface);
|
||||
--color-surface-elevated-1: var(--color-surface);
|
||||
--color-surface-elevated-2: var(--color-surface);
|
||||
--color-surface-elevated-3: var(--color-surface);
|
||||
--color-app-accent: var(--color-primary);
|
||||
--color-mana: var(--color-primary);
|
||||
--color-destructive: var(--color-error);
|
||||
--color-destructive-foreground: var(--color-primary-foreground);
|
||||
|
||||
/* ===== Brand-Literals (NICHT theme-aware) ===== */
|
||||
--brand-cards-forest: #16a34a;
|
||||
|
||||
/* ===== Fonts ===== */
|
||||
--font-sans:
|
||||
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
|
|
@ -19,19 +55,20 @@
|
|||
|
||||
@layer base {
|
||||
html {
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-fg);
|
||||
background-color: hsl(var(--color-background));
|
||||
color: hsl(var(--color-foreground));
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100dvh;
|
||||
}
|
||||
|
||||
/* Sichtbarer Fokus-Ring für Tastatur-Nutzer. Tailwind 4 strippt
|
||||
die Browser-Defaults; wir setzen einen expliziten Outline.
|
||||
Nur :focus-visible, damit Maus-Klicks visuell sauber bleiben. */
|
||||
die Browser-Defaults; expliziter Outline. Nur :focus-visible,
|
||||
damit Maus-Klicks visuell sauber bleiben. */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline: 2px solid hsl(var(--color-primary));
|
||||
outline-offset: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
|
@ -56,8 +93,8 @@
|
|||
top: 0;
|
||||
left: 0;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--color-primary);
|
||||
color: var(--color-primary-fg);
|
||||
background: hsl(var(--color-primary));
|
||||
color: hsl(var(--color-primary-foreground));
|
||||
z-index: 50;
|
||||
transform: translateY(-200%);
|
||||
transition: transform 0.15s;
|
||||
|
|
@ -79,15 +116,3 @@
|
|||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@theme {
|
||||
--color-bg: oklch(0.18 0.02 240);
|
||||
--color-fg: oklch(0.95 0.01 240);
|
||||
--color-muted: oklch(0.65 0.02 240);
|
||||
--color-border: oklch(0.30 0.02 240);
|
||||
--color-card: oklch(0.22 0.02 240);
|
||||
--color-primary: oklch(0.70 0.18 250);
|
||||
--color-primary-fg: oklch(0.18 0.02 240);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!doctype html>
|
||||
<html lang="de">
|
||||
<html lang="de" data-theme="forest">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
|
|
|||
|
|
@ -84,14 +84,14 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="rounded-xl border border-[var(--color-border)] bg-[var(--color-card)] p-4">
|
||||
<div class="rounded-xl border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4">
|
||||
<div class="mb-2 text-sm font-medium">{t('import.anki_label')}</div>
|
||||
|
||||
{#if stage === 'idle'}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
class="cursor-pointer rounded-lg border-2 border-dashed border-[var(--color-border)] px-4 py-6 text-center text-sm text-[var(--color-muted)] transition-colors hover:border-[var(--color-primary)] hover:text-[var(--color-fg)]"
|
||||
class="cursor-pointer rounded-lg border-2 border-dashed border-[hsl(var(--color-border))] px-4 py-6 text-center text-sm text-[hsl(var(--color-muted-foreground))] transition-colors hover:border-[hsl(var(--color-primary))] hover:text-[hsl(var(--color-foreground))]"
|
||||
ondragover={(e) => e.preventDefault()}
|
||||
ondrop={onDrop}
|
||||
onclick={() => fileInput?.click()}
|
||||
|
|
@ -115,21 +115,21 @@
|
|||
onchange={onPick}
|
||||
/>
|
||||
{:else if stage === 'parsing'}
|
||||
<div class="py-6 text-center text-sm text-[var(--color-muted)]" aria-live="polite">
|
||||
<div class="py-6 text-center text-sm text-[hsl(var(--color-muted-foreground))]" aria-live="polite">
|
||||
{t('import.parsing', { file: fileName })}
|
||||
</div>
|
||||
{:else if stage === 'preview' && parsed}
|
||||
<div class="space-y-2 text-sm">
|
||||
<div>
|
||||
<span class="text-[var(--color-muted)]">{t('import.preview_found')}</span>
|
||||
<code class="rounded bg-[var(--color-border)]/40 px-1 text-xs">{fileName}</code>:
|
||||
<span class="text-[hsl(var(--color-muted-foreground))]">{t('import.preview_found')}</span>
|
||||
<code class="rounded bg-[hsl(var(--color-border))]/40 px-1 text-xs">{fileName}</code>:
|
||||
</div>
|
||||
<ul class="ml-4 list-disc">
|
||||
<li>{tn('import.preview_decks', parsed.decks.length)}</li>
|
||||
<li>
|
||||
{tn('import.preview_cards', parsed.cards.length)}
|
||||
{#if parsed.cards.length > 0}
|
||||
<span class="text-[var(--color-muted)]">
|
||||
<span class="text-[hsl(var(--color-muted-foreground))]">
|
||||
{t('import.preview_breakdown', {
|
||||
basic: typeBreakdown.basic,
|
||||
basic_reverse: typeBreakdown.basicReverse,
|
||||
|
|
@ -139,7 +139,7 @@
|
|||
{/if}
|
||||
</li>
|
||||
{#if parsed.mediaByFilename.size > 0}
|
||||
<li class="text-[var(--color-muted)]">
|
||||
<li class="text-[hsl(var(--color-muted-foreground))]">
|
||||
{t('import.preview_media', { n: parsed.mediaByFilename.size })}
|
||||
</li>
|
||||
{/if}
|
||||
|
|
@ -148,7 +148,7 @@
|
|||
{/if}
|
||||
</ul>
|
||||
{#if parsed.warnings.length > 0}
|
||||
<details class="text-xs text-[var(--color-muted)]">
|
||||
<details class="text-xs text-[hsl(var(--color-muted-foreground))]">
|
||||
<summary class="cursor-pointer">{t('import.preview_warnings', { n: parsed.warnings.length })}</summary>
|
||||
<ul class="mt-1 list-disc pl-4">
|
||||
{#each parsed.warnings.slice(0, 10) as w (w)}<li>{w}</li>{/each}
|
||||
|
|
@ -157,13 +157,13 @@
|
|||
{/if}
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<button
|
||||
class="rounded px-3 py-1.5 text-sm text-[var(--color-muted)] hover:text-[var(--color-fg)]"
|
||||
class="rounded px-3 py-1.5 text-sm text-[hsl(var(--color-muted-foreground))] hover:text-[hsl(var(--color-foreground))]"
|
||||
onclick={reset}
|
||||
>
|
||||
{t('import.cancel')}
|
||||
</button>
|
||||
<button
|
||||
class="rounded bg-[var(--color-primary)] px-4 py-1.5 text-sm text-[var(--color-primary-fg)] hover:opacity-90"
|
||||
class="rounded bg-[hsl(var(--color-primary))] px-4 py-1.5 text-sm text-[hsl(var(--color-primary-foreground))] hover:opacity-90"
|
||||
onclick={confirmImport}
|
||||
>
|
||||
{t('import.import_now')}
|
||||
|
|
@ -171,7 +171,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{:else if stage === 'importing'}
|
||||
<div class="py-6 text-center text-sm text-[var(--color-muted)]" aria-live="polite">
|
||||
<div class="py-6 text-center text-sm text-[hsl(var(--color-muted-foreground))]" aria-live="polite">
|
||||
{#if progress.stage === 'media'}
|
||||
{t('import.stage_media', { current: progress.current, total: progress.total })}
|
||||
{:else if progress.stage === 'decks'}
|
||||
|
|
@ -182,32 +182,32 @@
|
|||
{t('import.stage_done')}
|
||||
{/if}
|
||||
<div
|
||||
class="mx-auto mt-3 h-1 w-48 overflow-hidden rounded-full bg-[var(--color-border)]/40"
|
||||
class="mx-auto mt-3 h-1 w-48 overflow-hidden rounded-full bg-[hsl(var(--color-border))]/40"
|
||||
role="progressbar"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax={progress.total}
|
||||
aria-valuenow={progress.current}
|
||||
>
|
||||
<div
|
||||
class="h-full bg-[var(--color-primary)] transition-all"
|
||||
class="h-full bg-[hsl(var(--color-primary))] transition-all"
|
||||
style="width: {progress.total === 0 ? 0 : (progress.current / progress.total) * 100}%"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if stage === 'done' && result}
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="text-[var(--color-success,#16a34a)]">
|
||||
<div class="text-[hsl(var(--color-success))]">
|
||||
{result.decksCreated === 1
|
||||
? t('import.done_summary_one', { cards: result.cardsCreated })
|
||||
: t('import.done_summary', { cards: result.cardsCreated, decks: result.decksCreated })}
|
||||
</div>
|
||||
{#if result.cardsSkippedDuplicate > 0}
|
||||
<div class="text-[var(--color-muted)]">
|
||||
<div class="text-[hsl(var(--color-muted-foreground))]">
|
||||
{t('import.done_dupes', { n: result.cardsSkippedDuplicate })}
|
||||
</div>
|
||||
{/if}
|
||||
{#if result.mediaUploaded > 0 || result.mediaFailed > 0}
|
||||
<div class="text-[var(--color-muted)]">
|
||||
<div class="text-[hsl(var(--color-muted-foreground))]">
|
||||
{t('import.done_media', {
|
||||
uploaded: result.mediaUploaded,
|
||||
failed: result.mediaFailed,
|
||||
|
|
@ -215,7 +215,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
{#if result.failed > 0}
|
||||
<details class="text-[var(--color-danger)]">
|
||||
<details class="text-[hsl(var(--color-error))]">
|
||||
<summary class="cursor-pointer">{t('import.done_failures', { n: result.failed })}</summary>
|
||||
<ul class="mt-1 list-disc pl-4 text-xs">
|
||||
{#each result.failures.slice(0, 20) as msg (msg)}<li>{msg}</li>{/each}
|
||||
|
|
@ -223,7 +223,7 @@
|
|||
</details>
|
||||
{/if}
|
||||
<button
|
||||
class="rounded px-3 py-1.5 text-sm text-[var(--color-muted)] hover:text-[var(--color-fg)]"
|
||||
class="rounded px-3 py-1.5 text-sm text-[hsl(var(--color-muted-foreground))] hover:text-[hsl(var(--color-foreground))]"
|
||||
onclick={reset}
|
||||
>
|
||||
{t('import.done_more')}
|
||||
|
|
@ -231,9 +231,9 @@
|
|||
</div>
|
||||
{:else if stage === 'error'}
|
||||
<div class="space-y-2 text-sm" role="alert">
|
||||
<div class="text-[var(--color-danger)]">{t('import.error_label', { msg: error ?? '?' })}</div>
|
||||
<div class="text-[hsl(var(--color-error))]">{t('import.error_label', { msg: error ?? '?' })}</div>
|
||||
<button
|
||||
class="rounded px-3 py-1.5 text-sm text-[var(--color-muted)] hover:text-[var(--color-fg)]"
|
||||
class="rounded px-3 py-1.5 text-sm text-[hsl(var(--color-muted-foreground))] hover:text-[hsl(var(--color-foreground))]"
|
||||
onclick={reset}
|
||||
>
|
||||
{t('import.retry')}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,54 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
import { devUser } from '$lib/auth/dev-stub.svelte.ts';
|
||||
import { i18n, t } from '$lib/i18n/index.svelte.ts';
|
||||
import { PillTabGroup } from '@mana/shared-ui';
|
||||
|
||||
const langOptions = [
|
||||
{ id: 'de', label: 'DE', title: 'Deutsch' },
|
||||
{ id: 'en', label: 'EN', title: 'English' },
|
||||
];
|
||||
|
||||
const navOptions = $derived([
|
||||
{ id: 'decks', label: t('nav.decks') },
|
||||
{ id: 'study', label: t('nav.study') },
|
||||
{ id: 'explore', label: t('nav.explore') },
|
||||
{ id: 'import', label: t('nav.import') },
|
||||
{ id: 'stats', label: t('nav.stats') },
|
||||
]);
|
||||
|
||||
const activeNav = $derived.by(() => {
|
||||
const path = page.url.pathname;
|
||||
if (path.startsWith('/decks')) return 'decks';
|
||||
if (path.startsWith('/study')) return 'study';
|
||||
// /explore zeigt aktiv für Marketplace-Surfaces:
|
||||
// /explore selbst, /d/<slug>, /u/<slug>, /me/{published,subscribed,forks}.
|
||||
if (
|
||||
path.startsWith('/explore') ||
|
||||
path.startsWith('/d/') ||
|
||||
path.startsWith('/u/') ||
|
||||
path.startsWith('/me/')
|
||||
) {
|
||||
return 'explore';
|
||||
}
|
||||
if (path.startsWith('/import')) return 'import';
|
||||
if (path.startsWith('/stats')) return 'stats';
|
||||
return '';
|
||||
});
|
||||
|
||||
function navTo(id: string) {
|
||||
goto('/' + id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<header
|
||||
class="sticky top-0 z-40 w-full border-b bg-[var(--color-card)] border-[var(--color-border)]"
|
||||
class="sticky top-0 z-40 w-full border-b bg-[hsl(var(--color-card))] border-[hsl(var(--color-border))]"
|
||||
>
|
||||
<div class="mx-auto flex max-w-6xl items-center justify-between px-4 py-3">
|
||||
<a href="/" class="flex items-center gap-2 font-semibold">
|
||||
<span
|
||||
class="inline-flex h-7 w-7 items-center justify-center rounded bg-[var(--color-primary)] text-[var(--color-primary-fg)] text-sm"
|
||||
class="inline-flex h-7 w-7 items-center justify-center rounded bg-[hsl(var(--color-primary))] text-[hsl(var(--color-primary-foreground))] text-sm"
|
||||
aria-hidden="true"
|
||||
>
|
||||
C
|
||||
|
|
@ -18,57 +56,27 @@
|
|||
<span>{t('app.name')}</span>
|
||||
</a>
|
||||
|
||||
<nav class="flex items-center gap-6 text-sm" aria-label={t('common.main_nav')}>
|
||||
<a
|
||||
href="/decks"
|
||||
class="hover:text-[var(--color-primary)]"
|
||||
class:font-medium={page.url.pathname.startsWith('/decks')}>{t('nav.decks')}</a
|
||||
>
|
||||
<a
|
||||
href="/study"
|
||||
class="hover:text-[var(--color-primary)]"
|
||||
class:font-medium={page.url.pathname.startsWith('/study')}>{t('nav.study')}</a
|
||||
>
|
||||
<a
|
||||
href="/import"
|
||||
class="hover:text-[var(--color-primary)]"
|
||||
class:font-medium={page.url.pathname.startsWith('/import')}>{t('nav.import')}</a
|
||||
>
|
||||
<a
|
||||
href="/stats"
|
||||
class="hover:text-[var(--color-primary)]"
|
||||
class:font-medium={page.url.pathname.startsWith('/stats')}>{t('nav.stats')}</a
|
||||
>
|
||||
<nav aria-label={t('common.main_nav')}>
|
||||
<PillTabGroup
|
||||
options={navOptions}
|
||||
value={activeNav}
|
||||
onChange={navTo}
|
||||
primaryColor="hsl(var(--color-primary))"
|
||||
/>
|
||||
</nav>
|
||||
|
||||
<div class="flex items-center gap-3 text-sm">
|
||||
<div
|
||||
class="flex overflow-hidden rounded border border-[var(--color-border)] text-xs"
|
||||
role="group"
|
||||
aria-label={t('common.language_switcher')}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="px-2 py-1 transition-colors"
|
||||
class:bg-primary-active={i18n.current === 'de'}
|
||||
style:background-color={i18n.current === 'de' ? 'var(--color-primary)' : 'transparent'}
|
||||
style:color={i18n.current === 'de' ? 'var(--color-primary-fg)' : 'var(--color-muted)'}
|
||||
aria-pressed={i18n.current === 'de'}
|
||||
onclick={() => i18n.set('de')}>DE</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="px-2 py-1 transition-colors"
|
||||
style:background-color={i18n.current === 'en' ? 'var(--color-primary)' : 'transparent'}
|
||||
style:color={i18n.current === 'en' ? 'var(--color-primary-fg)' : 'var(--color-muted)'}
|
||||
aria-pressed={i18n.current === 'en'}
|
||||
onclick={() => i18n.set('en')}>EN</button
|
||||
>
|
||||
</div>
|
||||
<PillTabGroup
|
||||
options={langOptions}
|
||||
value={i18n.current}
|
||||
onChange={(id: string) => i18n.set(id as 'de' | 'en')}
|
||||
sectionLabel={t('common.language_switcher')}
|
||||
primaryColor="hsl(var(--color-primary))"
|
||||
/>
|
||||
{#if devUser.id}
|
||||
<a
|
||||
href="/account"
|
||||
class="truncate max-w-[200px] text-[var(--color-muted)] hover:text-[var(--color-fg)]"
|
||||
class="truncate max-w-[200px] text-[hsl(var(--color-muted-foreground))] hover:text-[hsl(var(--color-foreground))]"
|
||||
title={devUser.user?.email ?? devUser.id}
|
||||
>
|
||||
{devUser.user?.email ?? devUser.user?.name ?? devUser.id}
|
||||
|
|
@ -76,7 +84,7 @@
|
|||
{:else}
|
||||
<a
|
||||
href="/"
|
||||
class="rounded bg-[var(--color-primary)] px-3 py-1 text-[var(--color-primary-fg)]"
|
||||
class="rounded bg-[hsl(var(--color-primary))] px-3 py-1 text-[hsl(var(--color-primary-foreground))]"
|
||||
>
|
||||
Login
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@
|
|||
/>
|
||||
</label>
|
||||
{#if uploading}
|
||||
<p class="text-xs text-[var(--color-muted)]">{t('image_occlusion.uploading')}</p>
|
||||
<p class="text-xs text-[hsl(var(--color-muted-foreground))]">{t('image_occlusion.uploading')}</p>
|
||||
{/if}
|
||||
|
||||
{#if imageRef}
|
||||
|
|
@ -186,24 +186,24 @@
|
|||
</svg>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-[var(--color-muted)]">{t('image_occlusion.draw_hint')}</p>
|
||||
<p class="text-xs text-[hsl(var(--color-muted-foreground))]">{t('image_occlusion.draw_hint')}</p>
|
||||
|
||||
{#if masks.length > 0}
|
||||
<ul class="space-y-2 text-sm">
|
||||
{#each masks as m, i (m.id)}
|
||||
<li class="flex items-center gap-3 rounded border border-[var(--color-border)] px-3 py-2">
|
||||
<span class="text-xs text-[var(--color-muted)] tabular-nums">{i + 1}</span>
|
||||
<li class="flex items-center gap-3 rounded border border-[hsl(var(--color-border))] px-3 py-2">
|
||||
<span class="text-xs text-[hsl(var(--color-muted-foreground))] tabular-nums">{i + 1}</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('image_occlusion.label_placeholder')}
|
||||
value={m.label ?? ''}
|
||||
oninput={(e) => setLabel(m.id, (e.currentTarget as HTMLInputElement).value)}
|
||||
class="flex-1 rounded border bg-[var(--color-card)] border-[var(--color-border)] px-2 py-1 text-sm"
|
||||
class="flex-1 rounded border bg-[hsl(var(--color-card))] border-[hsl(var(--color-border))] px-2 py-1 text-sm"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => deleteMask(m.id)}
|
||||
class="text-xs text-[var(--color-muted)] hover:text-[var(--color-danger)]"
|
||||
class="text-xs text-[hsl(var(--color-muted-foreground))] hover:text-[hsl(var(--color-error))]"
|
||||
aria-label={t('image_occlusion.delete_mask')}
|
||||
>
|
||||
×
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@
|
|||
{#if stats && stats.deck && stats.cardCount > 0}
|
||||
<a
|
||||
href="/decks/{stats.deck.id}"
|
||||
class="block rounded-lg border border-[var(--color-primary)]/40 bg-[var(--color-primary)]/10 px-4 py-3 text-sm hover:bg-[var(--color-primary)]/15"
|
||||
class="block rounded-lg border border-[hsl(var(--color-primary))]/40 bg-[hsl(var(--color-primary))]/10 px-4 py-3 text-sm hover:bg-[hsl(var(--color-primary))]/15"
|
||||
>
|
||||
<span class="font-medium">{t('inbox_banner.label')}</span>
|
||||
<span class="text-[var(--color-muted)]" aria-hidden="true">·</span>
|
||||
<span class="text-[hsl(var(--color-muted-foreground))]" aria-hidden="true">·</span>
|
||||
<span>{tn('inbox_banner.count', stats.cardCount)}</span>
|
||||
<span class="ml-1 text-[var(--color-muted)]">{t('inbox_banner.cta')}</span>
|
||||
<span class="ml-1 text-[hsl(var(--color-muted-foreground))]">{t('inbox_banner.cta')}</span>
|
||||
</a>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -14,15 +14,15 @@
|
|||
>
|
||||
{#each toasts.items as t (t.id)}
|
||||
<div
|
||||
class="flex items-start gap-3 rounded-lg px-4 py-3 shadow-lg max-w-sm border bg-[var(--color-card)] border-[var(--color-border)]"
|
||||
class:text-[var(--color-success)]={t.kind === 'success'}
|
||||
class:text-[var(--color-warning)]={t.kind === 'warning'}
|
||||
class:text-[var(--color-danger)]={t.kind === 'error'}
|
||||
class="flex items-start gap-3 rounded-lg px-4 py-3 shadow-lg max-w-sm border bg-[hsl(var(--color-card))] border-[hsl(var(--color-border))]"
|
||||
class:text-[hsl(var(--color-success))]={t.kind === 'success'}
|
||||
class:text-[hsl(var(--color-warning))]={t.kind === 'warning'}
|
||||
class:text-[hsl(var(--color-error))]={t.kind === 'error'}
|
||||
role={t.kind === 'error' ? 'alert' : 'status'}
|
||||
>
|
||||
<span class="flex-1 text-sm">{t.message}</span>
|
||||
<button
|
||||
class="text-[var(--color-muted)] hover:text-[var(--color-fg)] text-lg leading-none"
|
||||
class="text-[hsl(var(--color-muted-foreground))] hover:text-[hsl(var(--color-foreground))] text-lg leading-none"
|
||||
onclick={() => toasts.dismiss(t.id)}
|
||||
aria-label={closeLabel}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const de: TranslationNode = {
|
|||
nav: {
|
||||
decks: 'Decks',
|
||||
study: 'Lernen',
|
||||
explore: 'Library',
|
||||
import: 'Import',
|
||||
stats: 'Statistik',
|
||||
login_dev: 'Login (dev)',
|
||||
|
|
@ -32,6 +33,8 @@ export const de: TranslationNode = {
|
|||
error: 'Fehler: {msg}',
|
||||
card_count: '{n} Karten',
|
||||
card_count_one: '1 Karte',
|
||||
card_count_more: '{n} weitere Karten im Stapel',
|
||||
card_count_more_one: '1 weitere Karte im Stapel',
|
||||
due_count: '{n} fällig',
|
||||
delete_confirm:
|
||||
'Deck "{name}" wirklich löschen? Alle Karten + Review-Daten gehen verloren.',
|
||||
|
|
@ -48,6 +51,11 @@ export const de: TranslationNode = {
|
|||
card_delete_aria: 'Karte löschen',
|
||||
card_delete_label: 'Löschen',
|
||||
card_delete_confirm: 'Karte wirklich löschen? Reviews werden mit gelöscht.',
|
||||
fan_aria: 'Aufgefächerte Karten von Stapel "{name}"',
|
||||
card_open: 'Karte öffnen — {type}',
|
||||
},
|
||||
deck_stack: {
|
||||
aria_label: 'Stapel "{name}" — {cards} Karten, {due} fällig',
|
||||
},
|
||||
deck_new: {
|
||||
title: 'Neues Deck',
|
||||
|
|
@ -126,6 +134,7 @@ export const de: TranslationNode = {
|
|||
grade_hint: '1=Wieder · 2=Schwer · 3=Gut · 4=Leicht',
|
||||
loading: 'Lade…',
|
||||
error: 'Fehler: {msg}',
|
||||
manage_link: 'Karten verwalten →',
|
||||
},
|
||||
import: {
|
||||
title: 'Importieren',
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export const en: TranslationNode = {
|
|||
nav: {
|
||||
decks: 'Decks',
|
||||
study: 'Study',
|
||||
explore: 'Library',
|
||||
import: 'Import',
|
||||
stats: 'Stats',
|
||||
login_dev: 'Login (dev)',
|
||||
|
|
@ -29,6 +30,8 @@ export const en: TranslationNode = {
|
|||
error: 'Error: {msg}',
|
||||
card_count: '{n} cards',
|
||||
card_count_one: '1 card',
|
||||
card_count_more: '{n} more cards in the stack',
|
||||
card_count_more_one: '1 more card in the stack',
|
||||
due_count: '{n} due',
|
||||
delete_confirm:
|
||||
'Really delete deck "{name}"? All cards + review data will be lost.',
|
||||
|
|
@ -45,6 +48,11 @@ export const en: TranslationNode = {
|
|||
card_delete_aria: 'Delete card',
|
||||
card_delete_label: 'Delete',
|
||||
card_delete_confirm: 'Really delete card? Reviews will be deleted with it.',
|
||||
fan_aria: 'Fanned cards from stack "{name}"',
|
||||
card_open: 'Open card — {type}',
|
||||
},
|
||||
deck_stack: {
|
||||
aria_label: 'Stack "{name}" — {cards} cards, {due} due',
|
||||
},
|
||||
deck_new: {
|
||||
title: 'New deck',
|
||||
|
|
@ -123,6 +131,7 @@ export const en: TranslationNode = {
|
|||
grade_hint: '1=Again · 2=Hard · 3=Good · 4=Easy',
|
||||
loading: 'Loading…',
|
||||
error: 'Error: {msg}',
|
||||
manage_link: 'Manage cards →',
|
||||
},
|
||||
import: {
|
||||
title: 'Import',
|
||||
|
|
|
|||
|
|
@ -31,12 +31,12 @@
|
|||
|
||||
<div class="mx-auto max-w-2xl py-12 text-center">
|
||||
<h1 class="text-3xl font-semibold">{t('app.name')}</h1>
|
||||
<p class="mt-2 text-[var(--color-muted)]">{t('landing.welcome')}</p>
|
||||
<p class="mt-1 text-sm text-[var(--color-muted)]">{t('landing.intro')}</p>
|
||||
<p class="mt-2 text-[hsl(var(--color-muted-foreground))]">{t('landing.welcome')}</p>
|
||||
<p class="mt-1 text-sm text-[hsl(var(--color-muted-foreground))]">{t('landing.intro')}</p>
|
||||
|
||||
{#if !devUser.id}
|
||||
<form
|
||||
class="mt-8 rounded-lg border bg-[var(--color-card)] border-[var(--color-border)] p-6 text-left space-y-3"
|
||||
class="mt-8 rounded-lg border bg-[hsl(var(--color-card))] border-[hsl(var(--color-border))] p-6 text-left space-y-3"
|
||||
onsubmit={onSubmit}
|
||||
>
|
||||
<h2 class="text-lg font-medium">Anmelden</h2>
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
bind:value={email}
|
||||
required
|
||||
autocomplete="email"
|
||||
class="mt-1 block w-full rounded border bg-[var(--color-bg)] border-[var(--color-border)] px-3 py-2 text-sm"
|
||||
class="mt-1 block w-full rounded border bg-[hsl(var(--color-background))] border-[hsl(var(--color-border))] px-3 py-2 text-sm"
|
||||
/>
|
||||
</label>
|
||||
<label class="block text-sm">
|
||||
|
|
@ -57,20 +57,20 @@
|
|||
bind:value={password}
|
||||
required
|
||||
autocomplete="current-password"
|
||||
class="mt-1 block w-full rounded border bg-[var(--color-bg)] border-[var(--color-border)] px-3 py-2 text-sm"
|
||||
class="mt-1 block w-full rounded border bg-[hsl(var(--color-background))] border-[hsl(var(--color-border))] px-3 py-2 text-sm"
|
||||
/>
|
||||
</label>
|
||||
{#if error}
|
||||
<p class="text-sm text-[var(--color-danger)]" role="alert">{error}</p>
|
||||
<p class="text-sm text-[hsl(var(--color-error))]" role="alert">{error}</p>
|
||||
{/if}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={busy || !email || !password}
|
||||
class="rounded bg-[var(--color-primary)] px-4 py-2 text-sm text-[var(--color-primary-fg)] disabled:opacity-50"
|
||||
class="rounded bg-[hsl(var(--color-primary))] px-4 py-2 text-sm text-[hsl(var(--color-primary-foreground))] disabled:opacity-50"
|
||||
>
|
||||
{busy ? '…' : 'Login'}
|
||||
</button>
|
||||
<p class="text-xs text-[var(--color-muted)]">
|
||||
<p class="text-xs text-[hsl(var(--color-muted-foreground))]">
|
||||
Mana-Account auf <a
|
||||
href="https://auth.mana.how"
|
||||
target="_blank"
|
||||
|
|
|
|||
|
|
@ -68,35 +68,35 @@
|
|||
<div class="mx-auto max-w-2xl px-4 py-8">
|
||||
<h1 class="text-2xl font-semibold">{t('account.title')}</h1>
|
||||
|
||||
<section class="mt-6 rounded-lg border border-[var(--color-border)] bg-[var(--color-card)] p-4">
|
||||
<section class="mt-6 rounded-lg border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4">
|
||||
{#if devUser.user}
|
||||
<div class="space-y-2 text-sm">
|
||||
<div>
|
||||
<div class="text-[var(--color-muted)]">E-Mail</div>
|
||||
<div class="text-[hsl(var(--color-muted-foreground))]">E-Mail</div>
|
||||
<div class="mt-1 font-medium">{devUser.user.email}</div>
|
||||
</div>
|
||||
{#if devUser.user.name}
|
||||
<div>
|
||||
<div class="text-[var(--color-muted)]">Name</div>
|
||||
<div class="text-[hsl(var(--color-muted-foreground))]">Name</div>
|
||||
<div class="mt-1">{devUser.user.name}</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<div class="text-[var(--color-muted)]">Tier</div>
|
||||
<div class="text-[hsl(var(--color-muted-foreground))]">Tier</div>
|
||||
<div class="mt-1">
|
||||
<span
|
||||
class="inline-flex rounded bg-[var(--color-border)]/40 px-2 py-0.5 text-xs"
|
||||
class="inline-flex rounded bg-[hsl(var(--color-border))]/40 px-2 py-0.5 text-xs"
|
||||
>{devUser.user.tier}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[var(--color-muted)]">{t('account.user_id_label')}</div>
|
||||
<code class="mt-1 block break-all text-xs text-[var(--color-muted)]">{devUser.user.id}</code>
|
||||
<div class="text-[hsl(var(--color-muted-foreground))]">{t('account.user_id_label')}</div>
|
||||
<code class="mt-1 block break-all text-xs text-[hsl(var(--color-muted-foreground))]">{devUser.user.id}</code>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-sm">
|
||||
<div class="text-[var(--color-muted)]">{t('account.user_id_label')} (Stub)</div>
|
||||
<div class="text-[hsl(var(--color-muted-foreground))]">{t('account.user_id_label')} (Stub)</div>
|
||||
<code class="mt-1 block break-all text-sm">{devUser.id ?? '—'}</code>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -104,34 +104,34 @@
|
|||
<button
|
||||
type="button"
|
||||
onclick={logout}
|
||||
class="rounded border border-[var(--color-border)] px-3 py-1.5 text-sm hover:bg-[var(--color-border)]/40"
|
||||
class="rounded border border-[hsl(var(--color-border))] px-3 py-1.5 text-sm hover:bg-[hsl(var(--color-border))]/40"
|
||||
>
|
||||
{t('account.logout')}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-6 rounded-lg border border-[var(--color-border)] bg-[var(--color-card)] p-4">
|
||||
<section class="mt-6 rounded-lg border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4">
|
||||
<h2 class="text-lg font-medium">{t('account.export_title')}</h2>
|
||||
<p class="mt-1 text-sm text-[var(--color-muted)]">{t('account.export_intro')}</p>
|
||||
<p class="mt-1 text-sm text-[hsl(var(--color-muted-foreground))]">{t('account.export_intro')}</p>
|
||||
<button
|
||||
type="button"
|
||||
onclick={onExport}
|
||||
disabled={exporting}
|
||||
class="mt-3 rounded bg-[var(--color-primary)] px-4 py-1.5 text-sm text-[var(--color-primary-fg)] disabled:opacity-50"
|
||||
class="mt-3 rounded bg-[hsl(var(--color-primary))] px-4 py-1.5 text-sm text-[hsl(var(--color-primary-foreground))] disabled:opacity-50"
|
||||
>
|
||||
{exporting ? t('account.export_loading') : t('account.export_button')}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="mt-6 rounded-lg border border-[var(--color-danger)]/30 bg-[var(--color-card)] p-4">
|
||||
<h2 class="text-lg font-medium text-[var(--color-danger)]">{t('account.delete_title')}</h2>
|
||||
<p class="mt-1 text-sm text-[var(--color-muted)]">{t('account.delete_intro')}</p>
|
||||
<section class="mt-6 rounded-lg border border-[hsl(var(--color-error))]/30 bg-[hsl(var(--color-card))] p-4">
|
||||
<h2 class="text-lg font-medium text-[hsl(var(--color-error))]">{t('account.delete_title')}</h2>
|
||||
<p class="mt-1 text-sm text-[hsl(var(--color-muted-foreground))]">{t('account.delete_intro')}</p>
|
||||
<button
|
||||
type="button"
|
||||
onclick={onDelete}
|
||||
disabled={deleting}
|
||||
class="mt-3 rounded border border-[var(--color-danger)] px-4 py-1.5 text-sm text-[var(--color-danger)] hover:bg-[var(--color-danger)]/10 disabled:opacity-50"
|
||||
class="mt-3 rounded border border-[hsl(var(--color-error))] px-4 py-1.5 text-sm text-[hsl(var(--color-error))] hover:bg-[hsl(var(--color-error))]/10 disabled:opacity-50"
|
||||
>
|
||||
{deleting ? t('account.delete_loading') : t('account.delete_button')}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -122,21 +122,21 @@
|
|||
|
||||
<div class="mx-auto max-w-3xl px-4 py-6">
|
||||
{#if loading}
|
||||
<p class="text-[var(--color-muted)]">{t('decks.loading')}</p>
|
||||
<p class="text-[hsl(var(--color-muted-foreground))]">{t('decks.loading')}</p>
|
||||
{:else if error}
|
||||
<p class="text-[var(--color-danger)]">{t('decks.error', { msg: error })}</p>
|
||||
<p class="text-[hsl(var(--color-error))]">{t('decks.error', { msg: error })}</p>
|
||||
{:else if card}
|
||||
<a
|
||||
href="/decks/{card.deck_id}"
|
||||
class="text-sm text-[var(--color-muted)] hover:text-[var(--color-fg)]">{t('card_edit.back')}</a
|
||||
class="text-sm text-[hsl(var(--color-muted-foreground))] hover:text-[hsl(var(--color-foreground))]">{t('card_edit.back')}</a
|
||||
>
|
||||
<div class="mt-2 flex items-baseline justify-between gap-3">
|
||||
<h1 class="text-2xl font-semibold">{t('card_edit.title')}</h1>
|
||||
<span class="rounded bg-[var(--color-border)] px-2 py-0.5 text-xs text-[var(--color-muted)]">
|
||||
<span class="rounded bg-[hsl(var(--color-border))] px-2 py-0.5 text-xs text-[hsl(var(--color-muted-foreground))]">
|
||||
{cardType}
|
||||
</span>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-[var(--color-muted)]">{t('card_edit.type_locked_help')}</p>
|
||||
<p class="mt-1 text-xs text-[hsl(var(--color-muted-foreground))]">{t('card_edit.type_locked_help')}</p>
|
||||
|
||||
<form class="mt-6 space-y-5" onsubmit={onSubmit}>
|
||||
{#if cardType === 'image-occlusion'}
|
||||
|
|
@ -150,14 +150,14 @@
|
|||
required
|
||||
rows="6"
|
||||
placeholder={t('card_new.cloze_text_placeholder')}
|
||||
class="mt-1 block w-full rounded border bg-[var(--color-card)] border-[var(--color-border)] px-3 py-2 font-mono text-sm"
|
||||
class="mt-1 block w-full rounded border bg-[hsl(var(--color-card))] border-[hsl(var(--color-border))] px-3 py-2 font-mono text-sm"
|
||||
></textarea>
|
||||
</label>
|
||||
<p class="mt-1 text-xs text-[var(--color-muted)]">{t('card_new.cloze_help')}</p>
|
||||
<p class="mt-1 text-xs text-[hsl(var(--color-muted-foreground))]">{t('card_new.cloze_help')}</p>
|
||||
{#if text.trim() && clusterIds.length === 0}
|
||||
<p class="mt-1 text-xs text-[var(--color-danger)]">{t('card_new.cloze_no_clusters')}</p>
|
||||
<p class="mt-1 text-xs text-[hsl(var(--color-error))]">{t('card_new.cloze_no_clusters')}</p>
|
||||
{:else if clusterIds.length > 0}
|
||||
<p class="mt-1 text-xs text-[var(--color-muted)]">
|
||||
<p class="mt-1 text-xs text-[hsl(var(--color-muted-foreground))]">
|
||||
{t('card_new.cloze_clusters_detected', {
|
||||
n: clusterIds.length,
|
||||
ids: clusterIds.join(', c'),
|
||||
|
|
@ -167,8 +167,8 @@
|
|||
</div>
|
||||
|
||||
{#if clozePreviewHtml}
|
||||
<div class="rounded border border-[var(--color-border)] bg-[var(--color-card)] p-3 text-sm">
|
||||
<div class="mb-1 text-xs uppercase text-[var(--color-muted)]">
|
||||
<div class="rounded border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-3 text-sm">
|
||||
<div class="mb-1 text-xs uppercase text-[hsl(var(--color-muted-foreground))]">
|
||||
{t('card_new.cloze_preview_label', { first: clusterIds[0] ?? 1 })}
|
||||
</div>
|
||||
<div class="prose prose-sm max-w-none">{@html clozePreviewHtml}</div>
|
||||
|
|
@ -181,7 +181,7 @@
|
|||
bind:value={extra}
|
||||
rows="3"
|
||||
placeholder={t('card_new.cloze_extra_placeholder')}
|
||||
class="mt-1 block w-full rounded border bg-[var(--color-card)] border-[var(--color-border)] px-3 py-2 font-mono text-sm"
|
||||
class="mt-1 block w-full rounded border bg-[hsl(var(--color-card))] border-[hsl(var(--color-border))] px-3 py-2 font-mono text-sm"
|
||||
></textarea>
|
||||
</label>
|
||||
{:else}
|
||||
|
|
@ -193,12 +193,12 @@
|
|||
bind:value={front}
|
||||
required
|
||||
rows="8"
|
||||
class="mt-1 block w-full rounded border bg-[var(--color-card)] border-[var(--color-border)] px-3 py-2 font-mono text-sm"
|
||||
class="mt-1 block w-full rounded border bg-[hsl(var(--color-card))] border-[hsl(var(--color-border))] px-3 py-2 font-mono text-sm"
|
||||
></textarea>
|
||||
</label>
|
||||
{#if front.trim()}
|
||||
<div class="mt-2 rounded border border-[var(--color-border)] bg-[var(--color-card)] p-3 text-sm">
|
||||
<div class="mb-1 text-xs uppercase text-[var(--color-muted)]">{t('card_new.preview_label')}</div>
|
||||
<div class="mt-2 rounded border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-3 text-sm">
|
||||
<div class="mb-1 text-xs uppercase text-[hsl(var(--color-muted-foreground))]">{t('card_new.preview_label')}</div>
|
||||
<div class="prose prose-sm max-w-none">{@html frontHtml}</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -210,12 +210,12 @@
|
|||
bind:value={back}
|
||||
required
|
||||
rows="8"
|
||||
class="mt-1 block w-full rounded border bg-[var(--color-card)] border-[var(--color-border)] px-3 py-2 font-mono text-sm"
|
||||
class="mt-1 block w-full rounded border bg-[hsl(var(--color-card))] border-[hsl(var(--color-border))] px-3 py-2 font-mono text-sm"
|
||||
></textarea>
|
||||
</label>
|
||||
{#if back.trim()}
|
||||
<div class="mt-2 rounded border border-[var(--color-border)] bg-[var(--color-card)] p-3 text-sm">
|
||||
<div class="mb-1 text-xs uppercase text-[var(--color-muted)]">{t('card_new.preview_label')}</div>
|
||||
<div class="mt-2 rounded border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-3 text-sm">
|
||||
<div class="mb-1 text-xs uppercase text-[hsl(var(--color-muted-foreground))]">{t('card_new.preview_label')}</div>
|
||||
<div class="prose prose-sm max-w-none">{@html backHtml}</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -228,19 +228,19 @@
|
|||
<button
|
||||
type="submit"
|
||||
disabled={!canSave}
|
||||
class="rounded bg-[var(--color-primary)] px-4 py-2 text-sm text-[var(--color-primary-fg)] disabled:opacity-50"
|
||||
class="rounded bg-[hsl(var(--color-primary))] px-4 py-2 text-sm text-[hsl(var(--color-primary-foreground))] disabled:opacity-50"
|
||||
>
|
||||
{saving ? t('card_edit.saving') : t('card_edit.save')}
|
||||
</button>
|
||||
<a
|
||||
href="/decks/{card.deck_id}"
|
||||
class="text-sm text-[var(--color-muted)] hover:text-[var(--color-fg)]">{t('card_edit.cancel')}</a
|
||||
class="text-sm text-[hsl(var(--color-muted-foreground))] hover:text-[hsl(var(--color-foreground))]">{t('card_edit.cancel')}</a
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onclick={onDelete}
|
||||
class="text-sm text-[var(--color-muted)] hover:text-[var(--color-danger)]"
|
||||
class="text-sm text-[hsl(var(--color-muted-foreground))] hover:text-[hsl(var(--color-error))]"
|
||||
>
|
||||
{t('card_edit.delete')}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<a href={deckId ? `/decks/${deckId}` : '/decks'} class="text-sm text-[var(--color-muted)] hover:text-[var(--color-fg)]"
|
||||
<a href={deckId ? `/decks/${deckId}` : '/decks'} class="text-sm text-[hsl(var(--color-muted-foreground))] hover:text-[hsl(var(--color-foreground))]"
|
||||
>{t('card_new.back')}</a
|
||||
>
|
||||
<h1 class="mt-2 text-2xl font-semibold">{t('card_new.title')}</h1>
|
||||
|
|
@ -119,7 +119,7 @@
|
|||
<select
|
||||
bind:value={deckId}
|
||||
required
|
||||
class="mt-1 block w-full rounded border bg-[var(--color-card)] border-[var(--color-border)] px-3 py-2 text-sm"
|
||||
class="mt-1 block w-full rounded border bg-[hsl(var(--color-card))] border-[hsl(var(--color-border))] px-3 py-2 text-sm"
|
||||
>
|
||||
{#each decks as d}
|
||||
<option value={d.id}>{d.name}</option>
|
||||
|
|
@ -131,7 +131,7 @@
|
|||
<span class="text-sm font-medium">{t('card_new.type_label')}</span>
|
||||
<select
|
||||
bind:value={cardType}
|
||||
class="mt-1 block w-full rounded border bg-[var(--color-card)] border-[var(--color-border)] px-3 py-2 text-sm"
|
||||
class="mt-1 block w-full rounded border bg-[hsl(var(--color-card))] border-[hsl(var(--color-border))] px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="basic">{t('card_new.type_basic')}</option>
|
||||
<option value="basic-reverse">{t('card_new.type_basic_reverse')}</option>
|
||||
|
|
@ -152,14 +152,14 @@
|
|||
required
|
||||
rows="6"
|
||||
placeholder={t('card_new.cloze_text_placeholder')}
|
||||
class="mt-1 block w-full rounded border bg-[var(--color-card)] border-[var(--color-border)] px-3 py-2 font-mono text-sm"
|
||||
class="mt-1 block w-full rounded border bg-[hsl(var(--color-card))] border-[hsl(var(--color-border))] px-3 py-2 font-mono text-sm"
|
||||
></textarea>
|
||||
</label>
|
||||
<p class="mt-1 text-xs text-[var(--color-muted)]">{t('card_new.cloze_help')}</p>
|
||||
<p class="mt-1 text-xs text-[hsl(var(--color-muted-foreground))]">{t('card_new.cloze_help')}</p>
|
||||
{#if text.trim() && clusterIds.length === 0}
|
||||
<p class="mt-1 text-xs text-[var(--color-danger)]">{t('card_new.cloze_no_clusters')}</p>
|
||||
<p class="mt-1 text-xs text-[hsl(var(--color-error))]">{t('card_new.cloze_no_clusters')}</p>
|
||||
{:else if clusterIds.length > 0}
|
||||
<p class="mt-1 text-xs text-[var(--color-muted)]">
|
||||
<p class="mt-1 text-xs text-[hsl(var(--color-muted-foreground))]">
|
||||
{t('card_new.cloze_clusters_detected', {
|
||||
n: clusterIds.length,
|
||||
ids: clusterIds.join(', c'),
|
||||
|
|
@ -169,8 +169,8 @@
|
|||
</div>
|
||||
|
||||
{#if clozePreviewHtml}
|
||||
<div class="rounded border border-[var(--color-border)] bg-[var(--color-card)] p-3 text-sm">
|
||||
<div class="mb-1 text-xs uppercase text-[var(--color-muted)]">
|
||||
<div class="rounded border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-3 text-sm">
|
||||
<div class="mb-1 text-xs uppercase text-[hsl(var(--color-muted-foreground))]">
|
||||
{t('card_new.cloze_preview_label', { first: clusterIds[0] ?? 1 })}
|
||||
</div>
|
||||
<div class="prose prose-sm max-w-none">{@html clozePreviewHtml}</div>
|
||||
|
|
@ -183,7 +183,7 @@
|
|||
bind:value={extra}
|
||||
rows="3"
|
||||
placeholder={t('card_new.cloze_extra_placeholder')}
|
||||
class="mt-1 block w-full rounded border bg-[var(--color-card)] border-[var(--color-border)] px-3 py-2 font-mono text-sm"
|
||||
class="mt-1 block w-full rounded border bg-[hsl(var(--color-card))] border-[hsl(var(--color-border))] px-3 py-2 font-mono text-sm"
|
||||
></textarea>
|
||||
</label>
|
||||
{:else}
|
||||
|
|
@ -196,12 +196,12 @@
|
|||
required
|
||||
rows="8"
|
||||
placeholder={t('card_new.front_placeholder')}
|
||||
class="mt-1 block w-full rounded border bg-[var(--color-card)] border-[var(--color-border)] px-3 py-2 font-mono text-sm"
|
||||
class="mt-1 block w-full rounded border bg-[hsl(var(--color-card))] border-[hsl(var(--color-border))] px-3 py-2 font-mono text-sm"
|
||||
></textarea>
|
||||
</label>
|
||||
{#if front.trim()}
|
||||
<div class="mt-2 rounded border border-[var(--color-border)] bg-[var(--color-card)] p-3 text-sm">
|
||||
<div class="mb-1 text-xs uppercase text-[var(--color-muted)]">{t('card_new.preview_label')}</div>
|
||||
<div class="mt-2 rounded border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-3 text-sm">
|
||||
<div class="mb-1 text-xs uppercase text-[hsl(var(--color-muted-foreground))]">{t('card_new.preview_label')}</div>
|
||||
<div class="prose prose-sm max-w-none">{@html frontHtml}</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -215,12 +215,12 @@
|
|||
required
|
||||
rows="8"
|
||||
placeholder={t('card_new.back_placeholder')}
|
||||
class="mt-1 block w-full rounded border bg-[var(--color-card)] border-[var(--color-border)] px-3 py-2 font-mono text-sm"
|
||||
class="mt-1 block w-full rounded border bg-[hsl(var(--color-card))] border-[hsl(var(--color-border))] px-3 py-2 font-mono text-sm"
|
||||
></textarea>
|
||||
</label>
|
||||
{#if back.trim()}
|
||||
<div class="mt-2 rounded border border-[var(--color-border)] bg-[var(--color-card)] p-3 text-sm">
|
||||
<div class="mb-1 text-xs uppercase text-[var(--color-muted)]">{t('card_new.preview_label')}</div>
|
||||
<div class="mt-2 rounded border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-3 text-sm">
|
||||
<div class="mb-1 text-xs uppercase text-[hsl(var(--color-muted-foreground))]">{t('card_new.preview_label')}</div>
|
||||
<div class="prose prose-sm max-w-none">{@html backHtml}</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -232,13 +232,13 @@
|
|||
<button
|
||||
type="submit"
|
||||
disabled={!canSave}
|
||||
class="rounded bg-[var(--color-primary)] px-4 py-2 text-sm text-[var(--color-primary-fg)] disabled:opacity-50"
|
||||
class="rounded bg-[hsl(var(--color-primary))] px-4 py-2 text-sm text-[hsl(var(--color-primary-foreground))] disabled:opacity-50"
|
||||
>
|
||||
{saving ? t('card_new.creating') : t('card_new.create')}
|
||||
</button>
|
||||
<a
|
||||
href={deckId ? `/decks/${deckId}` : '/decks'}
|
||||
class="text-sm text-[var(--color-muted)] hover:text-[var(--color-fg)]">{t('card_new.cancel')}</a
|
||||
class="text-sm text-[hsl(var(--color-muted-foreground))] hover:text-[hsl(var(--color-foreground))]">{t('card_new.cancel')}</a
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -50,11 +50,11 @@
|
|||
</svelte:head>
|
||||
|
||||
<div class="mx-auto max-w-2xl px-4 py-8">
|
||||
<a href="/decks" class="text-sm text-[var(--color-muted)] hover:text-[var(--color-fg)]">
|
||||
<a href="/decks" class="text-sm text-[hsl(var(--color-muted-foreground))] hover:text-[hsl(var(--color-foreground))]">
|
||||
← {t('nav.decks')}
|
||||
</a>
|
||||
<h1 class="mt-2 text-2xl font-semibold">✨ Deck mit KI erstellen</h1>
|
||||
<p class="mt-2 text-sm text-[var(--color-muted)]">
|
||||
<p class="mt-2 text-sm text-[hsl(var(--color-muted-foreground))]">
|
||||
Beschreibe ein Thema, und mana-llm baut ein Deck mit Karteikarten daraus. Du kannst die Karten
|
||||
danach jederzeit editieren oder ergänzen.
|
||||
</p>
|
||||
|
|
@ -68,9 +68,9 @@
|
|||
rows="4"
|
||||
maxlength="500"
|
||||
placeholder="z.B. Deutsche Hunderassen mit ihren wichtigsten Charaktermerkmalen"
|
||||
class="mt-1 block w-full rounded border bg-[var(--color-card)] border-[var(--color-border)] px-3 py-2 text-sm"
|
||||
class="mt-1 block w-full rounded border bg-[hsl(var(--color-card))] border-[hsl(var(--color-border))] px-3 py-2 text-sm"
|
||||
></textarea>
|
||||
<p class="mt-1 text-xs text-[var(--color-muted)]">
|
||||
<p class="mt-1 text-xs text-[hsl(var(--color-muted-foreground))]">
|
||||
Klar formulierte, abgegrenzte Themen funktionieren besser als zu breite („alles über
|
||||
Geschichte" → eher: „französische Revolution: Schlüsselereignisse 1789-1799").
|
||||
</p>
|
||||
|
|
@ -84,16 +84,16 @@
|
|||
bind:value={count}
|
||||
min="3"
|
||||
max="40"
|
||||
class="mt-1 block w-full rounded border bg-[var(--color-card)] border-[var(--color-border)] px-3 py-2 text-sm"
|
||||
class="mt-1 block w-full rounded border bg-[hsl(var(--color-card))] border-[hsl(var(--color-border))] px-3 py-2 text-sm"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-[var(--color-muted)]">3–40 (Server-Cap).</p>
|
||||
<p class="mt-1 text-xs text-[hsl(var(--color-muted-foreground))]">3–40 (Server-Cap).</p>
|
||||
</label>
|
||||
|
||||
<label class="block">
|
||||
<span class="text-sm font-medium">Sprache</span>
|
||||
<select
|
||||
bind:value={language}
|
||||
class="mt-1 block w-full rounded border bg-[var(--color-card)] border-[var(--color-border)] px-3 py-2 text-sm"
|
||||
class="mt-1 block w-full rounded border bg-[hsl(var(--color-card))] border-[hsl(var(--color-border))] px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="en">English</option>
|
||||
|
|
@ -102,7 +102,7 @@
|
|||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="rounded border border-[var(--color-danger)]/40 bg-[var(--color-danger)]/10 p-3 text-sm text-[var(--color-danger)]" role="alert">
|
||||
<div class="rounded border border-[hsl(var(--color-error))]/40 bg-[hsl(var(--color-error))]/10 p-3 text-sm text-[hsl(var(--color-error))]" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -111,30 +111,30 @@
|
|||
<button
|
||||
type="submit"
|
||||
disabled={busy || !prompt.trim()}
|
||||
class="rounded bg-[var(--color-primary)] px-4 py-2 text-sm text-[var(--color-primary-fg)] disabled:opacity-50"
|
||||
class="rounded bg-[hsl(var(--color-primary))] px-4 py-2 text-sm text-[hsl(var(--color-primary-foreground))] disabled:opacity-50"
|
||||
>
|
||||
{busy ? '✨ Generiere…' : '✨ Deck generieren'}
|
||||
</button>
|
||||
<a href="/decks" class="text-sm text-[var(--color-muted)] hover:text-[var(--color-fg)]"
|
||||
<a href="/decks" class="text-sm text-[hsl(var(--color-muted-foreground))] hover:text-[hsl(var(--color-foreground))]"
|
||||
>{t('deck_new.cancel')}</a
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if busy}
|
||||
<p class="text-xs text-[var(--color-muted)]" aria-live="polite">
|
||||
<p class="text-xs text-[hsl(var(--color-muted-foreground))]" aria-live="polite">
|
||||
mana-llm denkt nach. Bei {count} Karten typischerweise 10–60 Sekunden.
|
||||
</p>
|
||||
{/if}
|
||||
</form>
|
||||
|
||||
<aside class="mt-10 rounded-lg border border-dashed border-[var(--color-border)] p-4">
|
||||
<div class="text-xs font-medium text-[var(--color-fg)]">Beispiele zum Klicken:</div>
|
||||
<aside class="mt-10 rounded-lg border border-dashed border-[hsl(var(--color-border))] p-4">
|
||||
<div class="text-xs font-medium text-[hsl(var(--color-foreground))]">Beispiele zum Klicken:</div>
|
||||
<ul class="mt-2 space-y-1.5">
|
||||
{#each examples as ex (ex)}
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="text-left text-xs text-[var(--color-muted)] hover:text-[var(--color-primary)]"
|
||||
class="text-left text-xs text-[hsl(var(--color-muted-foreground))] hover:text-[hsl(var(--color-primary))]"
|
||||
onclick={() => (prompt = ex)}
|
||||
>
|
||||
→ {ex}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
</script>
|
||||
|
||||
<div class="mx-auto max-w-xl">
|
||||
<a href="/decks" class="text-sm text-[var(--color-muted)] hover:text-[var(--color-fg)]"
|
||||
<a href="/decks" class="text-sm text-[hsl(var(--color-muted-foreground))] hover:text-[hsl(var(--color-foreground))]"
|
||||
>{t('deck_detail.back')}</a
|
||||
>
|
||||
<h1 class="mt-2 text-2xl font-semibold">{t('deck_new.title')}</h1>
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
bind:value={name}
|
||||
required
|
||||
maxlength="200"
|
||||
class="mt-1 block w-full rounded border bg-[var(--color-card)] border-[var(--color-border)] px-3 py-2 text-sm"
|
||||
class="mt-1 block w-full rounded border bg-[hsl(var(--color-card))] border-[hsl(var(--color-border))] px-3 py-2 text-sm"
|
||||
/>
|
||||
</label>
|
||||
|
||||
|
|
@ -51,7 +51,7 @@
|
|||
bind:value={description}
|
||||
maxlength="2000"
|
||||
rows="3"
|
||||
class="mt-1 block w-full rounded border bg-[var(--color-card)] border-[var(--color-border)] px-3 py-2 text-sm"
|
||||
class="mt-1 block w-full rounded border bg-[hsl(var(--color-card))] border-[hsl(var(--color-border))] px-3 py-2 text-sm"
|
||||
></textarea>
|
||||
</label>
|
||||
|
||||
|
|
@ -60,7 +60,7 @@
|
|||
<input
|
||||
type="color"
|
||||
bind:value={color}
|
||||
class="mt-1 h-10 w-20 rounded border border-[var(--color-border)] bg-transparent"
|
||||
class="mt-1 h-10 w-20 rounded border border-[hsl(var(--color-border))] bg-transparent"
|
||||
/>
|
||||
</label>
|
||||
|
||||
|
|
@ -68,11 +68,11 @@
|
|||
<button
|
||||
type="submit"
|
||||
disabled={saving || !name.trim()}
|
||||
class="rounded bg-[var(--color-primary)] px-4 py-2 text-sm text-[var(--color-primary-fg)] disabled:opacity-50"
|
||||
class="rounded bg-[hsl(var(--color-primary))] px-4 py-2 text-sm text-[hsl(var(--color-primary-foreground))] disabled:opacity-50"
|
||||
>
|
||||
{saving ? t('deck_new.creating') : t('deck_new.create')}
|
||||
</button>
|
||||
<a href="/decks" class="text-sm text-[var(--color-muted)] hover:text-[var(--color-fg)]"
|
||||
<a href="/decks" class="text-sm text-[hsl(var(--color-muted-foreground))] hover:text-[hsl(var(--color-foreground))]"
|
||||
>{t('deck_new.cancel')}</a
|
||||
>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,21 +18,21 @@
|
|||
|
||||
<div class="mx-auto max-w-2xl px-4 py-8">
|
||||
<h1 class="text-2xl font-semibold">{t('import.title')}</h1>
|
||||
<p class="mt-2 text-sm text-[var(--color-muted)]">{t('import.intro')}</p>
|
||||
<p class="mt-2 text-sm text-[hsl(var(--color-muted-foreground))]">{t('import.intro')}</p>
|
||||
|
||||
<div class="mt-6">
|
||||
<AnkiImport />
|
||||
</div>
|
||||
|
||||
<aside class="mt-8 rounded-lg border border-dashed border-[var(--color-border)] p-4 text-xs text-[var(--color-muted)]">
|
||||
<div class="mb-1 font-medium text-[var(--color-fg)]">{t('import.what_works_title')}</div>
|
||||
<aside class="mt-8 rounded-lg border border-dashed border-[hsl(var(--color-border))] p-4 text-xs text-[hsl(var(--color-muted-foreground))]">
|
||||
<div class="mb-1 font-medium text-[hsl(var(--color-foreground))]">{t('import.what_works_title')}</div>
|
||||
<ul class="list-disc pl-4">
|
||||
<li>{t('import.what_works_decks')}</li>
|
||||
<li>{t('import.what_works_basic')}</li>
|
||||
<li>{t('import.what_works_cloze')}</li>
|
||||
<li>{t('import.what_works_media')}</li>
|
||||
</ul>
|
||||
<div class="mt-2 mb-1 font-medium text-[var(--color-fg)]">{t('import.what_skipped_title')}</div>
|
||||
<div class="mt-2 mb-1 font-medium text-[hsl(var(--color-foreground))]">{t('import.what_skipped_title')}</div>
|
||||
<ul class="list-disc pl-4">
|
||||
<li>{t('import.what_skipped_media')}</li>
|
||||
<li>{t('import.what_skipped_history')}</li>
|
||||
|
|
|
|||
|
|
@ -54,37 +54,37 @@
|
|||
<h1 class="text-2xl font-semibold">{t('stats.title')}</h1>
|
||||
|
||||
{#if loading}
|
||||
<p class="mt-6 text-[var(--color-muted)]">{t('stats.loading')}</p>
|
||||
<p class="mt-6 text-[hsl(var(--color-muted-foreground))]">{t('stats.loading')}</p>
|
||||
{:else if error}
|
||||
<p class="mt-6 text-[var(--color-danger)]">{t('stats.error', { msg: error })}</p>
|
||||
<p class="mt-6 text-[hsl(var(--color-error))]">{t('stats.error', { msg: error })}</p>
|
||||
{:else if stats}
|
||||
<p class="mt-1 text-xs text-[var(--color-muted)]">
|
||||
<p class="mt-1 text-xs text-[hsl(var(--color-muted-foreground))]">
|
||||
{t('stats.generated_at', { date: fullDate(stats.generated_at) })}
|
||||
</p>
|
||||
|
||||
<section class="mt-6 grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<div class="rounded-lg border border-[var(--color-border)] bg-[var(--color-card)] p-4">
|
||||
<div class="text-xs text-[var(--color-muted)]">{t('stats.decks')}</div>
|
||||
<div class="rounded-lg border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4">
|
||||
<div class="text-xs text-[hsl(var(--color-muted-foreground))]">{t('stats.decks')}</div>
|
||||
<div class="mt-1 text-2xl font-semibold">{stats.total_decks}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-[var(--color-border)] bg-[var(--color-card)] p-4">
|
||||
<div class="text-xs text-[var(--color-muted)]">{t('stats.cards')}</div>
|
||||
<div class="rounded-lg border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4">
|
||||
<div class="text-xs text-[hsl(var(--color-muted-foreground))]">{t('stats.cards')}</div>
|
||||
<div class="mt-1 text-2xl font-semibold">{stats.total_cards}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-[var(--color-border)] bg-[var(--color-card)] p-4">
|
||||
<div class="text-xs text-[var(--color-muted)]">{t('stats.reviews')}</div>
|
||||
<div class="rounded-lg border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4">
|
||||
<div class="text-xs text-[hsl(var(--color-muted-foreground))]">{t('stats.reviews')}</div>
|
||||
<div class="mt-1 text-2xl font-semibold">{stats.total_reviews}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-[var(--color-border)] bg-[var(--color-card)] p-4">
|
||||
<div class="text-xs text-[var(--color-muted)]">{t('stats.due_now')}</div>
|
||||
<div class="rounded-lg border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4">
|
||||
<div class="text-xs text-[hsl(var(--color-muted-foreground))]">{t('stats.due_now')}</div>
|
||||
<div class="mt-1 text-2xl font-semibold">{stats.due_now}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-6 rounded-lg border border-[var(--color-border)] bg-[var(--color-card)] p-4">
|
||||
<section class="mt-6 rounded-lg border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4">
|
||||
<div class="flex items-baseline justify-between">
|
||||
<h2 class="text-lg font-medium">{t('stats.days_title')}</h2>
|
||||
<span class="text-xs text-[var(--color-muted)]">
|
||||
<span class="text-xs text-[hsl(var(--color-muted-foreground))]">
|
||||
{tn('stats.streak', stats.streak_days, { total: reviewedTotal7 })}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -97,34 +97,34 @@
|
|||
>
|
||||
<div class="text-xs tabular-nums">{d.n || ''}</div>
|
||||
<div
|
||||
class="w-full rounded-t bg-[var(--color-primary)]/80"
|
||||
class="w-full rounded-t bg-[hsl(var(--color-primary))]/80"
|
||||
style="height: {(d.n / peakDay) * 100}%; min-height: {d.n > 0 ? '4px' : '0'};"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
<div class="text-xs text-[var(--color-muted)]">{dayLabel(d.day)}</div>
|
||||
<div class="text-xs text-[hsl(var(--color-muted-foreground))]">{dayLabel(d.day)}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-6 rounded-lg border border-[var(--color-border)] bg-[var(--color-card)] p-4">
|
||||
<section class="mt-6 rounded-lg border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-4">
|
||||
<h2 class="text-lg font-medium">{t('stats.fsrs_title')}</h2>
|
||||
<p class="mt-1 text-xs text-[var(--color-muted)]">{t('stats.fsrs_intro')}</p>
|
||||
<p class="mt-1 text-xs text-[hsl(var(--color-muted-foreground))]">{t('stats.fsrs_intro')}</p>
|
||||
<dl class="mt-3 grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<div>
|
||||
<dt class="text-xs text-[var(--color-muted)]">{t('stats.fsrs_new')}</dt>
|
||||
<dt class="text-xs text-[hsl(var(--color-muted-foreground))]">{t('stats.fsrs_new')}</dt>
|
||||
<dd class="text-xl font-semibold">{stats.state_counts.new}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs text-[var(--color-muted)]">{t('stats.fsrs_learning')}</dt>
|
||||
<dt class="text-xs text-[hsl(var(--color-muted-foreground))]">{t('stats.fsrs_learning')}</dt>
|
||||
<dd class="text-xl font-semibold">{stats.state_counts.learning}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs text-[var(--color-muted)]">{t('stats.fsrs_review')}</dt>
|
||||
<dt class="text-xs text-[hsl(var(--color-muted-foreground))]">{t('stats.fsrs_review')}</dt>
|
||||
<dd class="text-xl font-semibold">{stats.state_counts.review}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs text-[var(--color-muted)]">{t('stats.fsrs_relearning')}</dt>
|
||||
<dt class="text-xs text-[hsl(var(--color-muted-foreground))]">{t('stats.fsrs_relearning')}</dt>
|
||||
<dd class="text-xl font-semibold">{stats.state_counts.relearning}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
|
|
|||
|
|
@ -44,12 +44,12 @@
|
|||
</div>
|
||||
|
||||
{#if loading}
|
||||
<p class="mt-8 text-[var(--color-muted)]">{t('study_session.loading')}</p>
|
||||
<p class="mt-8 text-[hsl(var(--color-muted-foreground))]">{t('study_session.loading')}</p>
|
||||
{:else}
|
||||
<ul class="mt-6 space-y-2">
|
||||
{#each items as it (it.deck.id)}
|
||||
<li
|
||||
class="flex items-center justify-between rounded-lg border border-[var(--color-border)] bg-[var(--color-card)] px-4 py-3"
|
||||
class="flex items-center justify-between rounded-lg border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] px-4 py-3"
|
||||
>
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
{#if it.deck.color}
|
||||
|
|
@ -60,19 +60,19 @@
|
|||
></span>
|
||||
{/if}
|
||||
<span class="truncate font-medium">{it.deck.name}</span>
|
||||
<span class="text-sm text-[var(--color-muted)]">
|
||||
<span class="text-sm text-[hsl(var(--color-muted-foreground))]">
|
||||
{t('study.due_count', { n: it.due })}
|
||||
</span>
|
||||
</div>
|
||||
{#if it.due > 0}
|
||||
<a
|
||||
href="/study/{it.deck.id}"
|
||||
class="rounded bg-[var(--color-primary)] px-3 py-1.5 text-sm text-[var(--color-primary-fg)]"
|
||||
class="rounded bg-[hsl(var(--color-primary))] px-3 py-1.5 text-sm text-[hsl(var(--color-primary-foreground))]"
|
||||
>
|
||||
{t('study.study_now')}
|
||||
</a>
|
||||
{:else}
|
||||
<span class="text-sm text-[var(--color-muted)]" aria-label={t('study.none_due')}>—</span>
|
||||
<span class="text-sm text-[hsl(var(--color-muted-foreground))]" aria-label={t('study.none_due')}>—</span>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
|
|
|
|||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
|
|
@ -75,6 +75,9 @@ importers:
|
|||
'@mana/shared-ui':
|
||||
specifier: ^0.1.1
|
||||
version: 0.1.1(svelte@5.55.5)(vite@5.4.21(@types/node@22.19.18)(lightningcss@1.32.0))
|
||||
'@mana/themes':
|
||||
specifier: ^0.1.0
|
||||
version: 0.1.0
|
||||
dompurify:
|
||||
specifier: ^3.4.2
|
||||
version: 3.4.2
|
||||
|
|
@ -605,6 +608,9 @@ packages:
|
|||
peerDependencies:
|
||||
svelte: ^5.0.0
|
||||
|
||||
'@mana/themes@0.1.0':
|
||||
resolution: {integrity: sha512-YI3UU4Y1s+V1DRy5SDzkuA76Z/dSaf5VKf6zm5IofEhiQUn96/XoKwythRGemKiJz+kqnO7Kv9BJNCODU4y/Vg==}
|
||||
|
||||
'@nodable/entities@2.1.0':
|
||||
resolution: {integrity: sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==}
|
||||
|
||||
|
|
@ -2229,6 +2235,8 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- vite
|
||||
|
||||
'@mana/themes@0.1.0': {}
|
||||
|
||||
'@nodable/entities@2.1.0': {}
|
||||
|
||||
'@petamoriken/float16@3.9.3': {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue