From 39e508075a88839b753952315637c70cee42e19b Mon Sep 17 00:00:00 2001 From: Till JS Date: Fri, 8 May 2026 02:48:40 +0200 Subject: [PATCH] =?UTF-8?q?feat(cards):=20CardFace=20v2=20=E2=80=94=203D-F?= =?UTF-8?q?lip=20+=20tap-anywhere=20reveal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CardFace now renders one card surface that flips on Y-axis when the user taps it. Both faces share a CSS-grid cell so the parent height is the max of front/back, no jumpy reflow on flip. - Tap-anywhere on the surface reveals (only while showBack is false). The /learn page keeps the keyboard space/enter shortcut via the existing handler; the standalone "Aufdecken" button is now type-in-only (where the input field on the front breaks the flip-mental-model). - prefers-reduced-motion: reduce collapses the rotateY into an instant cross-fade — same affordance, no vestibular trigger. - Added a subtle "Tippe auf die Karte oder drücke Leertaste" hint on the front face so the new affordance is discoverable. - Fixed the last Phase-A leftover: focus:border-indigo-400 in the type-in input → focus:border-app-accent. --- .../web/src/lib/components/CardFace.svelte | 147 +++++++++++++++--- .../src/routes/learn/[deckId]/+page.svelte | 5 +- 2 files changed, 130 insertions(+), 22 deletions(-) diff --git a/apps/cards/apps/web/src/lib/components/CardFace.svelte b/apps/cards/apps/web/src/lib/components/CardFace.svelte index 9e14a4161..19d09cf1f 100644 --- a/apps/cards/apps/web/src/lib/components/CardFace.svelte +++ b/apps/cards/apps/web/src/lib/components/CardFace.svelte @@ -3,6 +3,19 @@ * CardFace — renders one learnable unit (a single subIndex of a card) * for any Phase-1 card type. Stateless: the parent owns `showBack`, * `typedAnswer`, and any timing. + * + * Card-feel design (Phase A polish): + * - Single surface that physically flips on Y axis when revealed. + * Both faces share a CSS-grid cell so the parent height is the + * max of front/back, no jumpy reflow on flip. + * - Tap anywhere on the surface reveals (only while `showBack` is + * false). The /learn page keeps the keyboard space/enter shortcut. + * - `prefers-reduced-motion: reduce` collapses the rotateY into an + * instant cross-fade — same affordance, no vestibular trigger. + * + * Type-in cards skip the flip: the input field doesn't make sense on + * a flippable face, so we keep the historical "input + answer below" + * layout for that single card type. */ import { renderCloze, renderMarkdown, type Card } from '@mana/cards-core'; @@ -13,9 +26,10 @@ showBack: boolean; typedAnswer?: string; onTypedAnswer?: (value: string) => void; + onReveal?: () => void; } - let { card, subIndex, showBack, typedAnswer = '', onTypedAnswer }: Props = $props(); + let { card, subIndex, showBack, typedAnswer = '', onTypedAnswer, onReveal }: Props = $props(); const view = $derived.by(() => { switch (card.type) { @@ -54,34 +68,127 @@ const matched = $derived( isTypeIn && typedAnswer.trim().toLowerCase() === view.expected.trim().toLowerCase() ); + + function tryReveal() { + if (!showBack && !isTypeIn) onReveal?.(); + } -
-
- {@html view.prompt} -
+{#if isTypeIn} + +
+
+ {@html view.prompt} +
- {#if isTypeIn} onTypedAnswer?.((e.currentTarget as HTMLInputElement).value)} disabled={showBack} /> - {/if} - {#if showBack} -
+ {@html view.answer} +
+ {/if} +
+{:else} +
+
+
+ {@html view.prompt} + {#if !showBack} +

+ Tippe auf die Karte oder drücke Leertaste +

+ {/if} +
+ +
+ {@html view.answer} +
+ +
+{/if} + + diff --git a/apps/cards/apps/web/src/routes/learn/[deckId]/+page.svelte b/apps/cards/apps/web/src/routes/learn/[deckId]/+page.svelte index 8c3dc299d..6588788b0 100644 --- a/apps/cards/apps/web/src/routes/learn/[deckId]/+page.svelte +++ b/apps/cards/apps/web/src/routes/learn/[deckId]/+page.svelte @@ -146,6 +146,7 @@ {showBack} {typedAnswer} onTypedAnswer={(v) => (typedAnswer = v)} + onReveal={reveal} /> {#if canSuggest} @@ -171,14 +172,14 @@ {/if} {/if} - {#if !showBack} + {#if !showBack && current.card.type === 'type-in'} - {:else} + {:else if showBack}