Redesign the quick-action CTAs: two ways in (study deck vs. homework)

Library empty state ("Your icebox")
- Replace the single "Make your first deck" button with two quick-action
  cards: "Create interactive test" (primary, ice-gradient icon + subtle
  accent border) and "Help me with my homework" (glass card, sparkles
  icon). Stacked on phones, side by side from 640px up; both >=44px tall.

/create
- Replace the "Got homework instead of notes?" banner with a two-card mode
  switcher above "1. Add your notes": "Study Deck & Quizzes" (the current
  view — aria-current, accent tint, ice icon) and "Homework Solver" (a
  shortcut to /homework). Icon-on-top tiles on phones, inline rows from
  640px up; glass tokens + backdrop blur in both themes.

Strings
- New keys in both dictionaries: emptyStateCreateDeck, emptyStateHomework,
  actionStudyDeck, actionHomework. Drop the now-unused homework.entry and
  library.makeFirst keys.

Drive-by AA fix (light mode, /create only)
- The "New folder" dashed button and the difficulty blurbs move from
  frost-500 to frost-600 (4.0:1 -> ~6:1). Dark mode is unchanged.

Service worker: freezmo-v8.

Verified: redesign suite 32/32 (stacked/side-by-side layouts, 360/390/1280,
dark+light, en+cs, >=44px targets, both cards navigate, no overflow, axe
contrast), mobile 49/49, i18n 27/27, desktop regression 73/73. Lint + build
clean.
This commit is contained in:
Martin Dvořák 2026-10-08 19:19:20 +00:00
commit 22af888ec0
4 changed files with 81 additions and 28 deletions

View file

@ -5,7 +5,7 @@ import { useMemo, useRef, useState, useSyncExternalStore } from "react";
import { DeckCard } from "@/components/DeckCard";
import { FolderBar } from "@/components/FolderBar";
import { FolderIcon, UploadIcon } from "@/components/icons";
import { FolderIcon, SnowflakeIcon, SparklesIcon, UploadIcon } from "@/components/icons";
import { Button, EmptyState } from "@/components/ui";
import { folderStore, renameFolder } from "@/lib/folders";
import { useI18n } from "@/lib/i18n";
@ -224,9 +224,38 @@ export function LibraryScreen() {
<EmptyState
title={t("library.noDecks")}
action={
<Link href="/create" className="btn-primary touch-target h-10 px-4 text-sm">
{t("library.makeFirst")}
</Link>
/* Two ways in: build a study deck from notes, or hand over an
assignment. Stacked on phones, side by side from 640px up. */
<div className="grid w-[min(24rem,calc(100vw_-_3rem))] gap-3 sm:w-[36rem] sm:grid-cols-2">
<Link
href="/create"
className="card card-hover touch-target flex items-center gap-3 border-accent-400/60 p-3.5 text-left sm:p-4"
>
<span
aria-hidden
className="grad-ice grid size-11 shrink-0 place-items-center rounded-2xl text-[#06111f]"
>
<SnowflakeIcon className="size-5" />
</span>
<span className="min-w-0 text-[14px] font-semibold leading-snug text-[var(--text)]">
{t("emptyStateCreateDeck")}
</span>
</Link>
<Link
href="/homework"
className="card card-hover touch-target flex items-center gap-3 p-3.5 text-left sm:p-4"
>
<span
aria-hidden
className="grid size-11 shrink-0 place-items-center rounded-2xl bg-[var(--surface-sunken)] text-[var(--text-muted)]"
>
<SparklesIcon className="size-5" />
</span>
<span className="min-w-0 text-[14px] font-semibold leading-snug text-[var(--text)]">
{t("emptyStateHomework")}
</span>
</Link>
</div>
}
>
{t("library.emptyBody")}

View file

@ -33,7 +33,7 @@ import { UNORGANIZED_ID, type Difficulty } from "@/lib/schemas";
import { upsertDeck } from "@/lib/storage";
import { openSettings } from "@/lib/ui-events";
import { cn, uid } from "@/lib/utils";
import { CameraIcon, ChevronRightIcon, FolderIcon, IceCubeIcon, PlusIcon, SnowflakeIcon, SparklesIcon, SunIcon } from "@/components/icons";
import { CameraIcon, FolderIcon, IceCubeIcon, PlusIcon, SnowflakeIcon, SparklesIcon, SunIcon } from "@/components/icons";
import { Button, ProgressBar } from "@/components/ui";
type Phase = "idle" | "preparing" | "loading" | "error";
@ -321,23 +321,39 @@ export function UploadScreen() {
</p>
</section>
{/* The other thing you can bring here: an assignment. */}
<Link
href="/homework"
className="card card-hover flex items-center gap-3 p-4 text-left"
>
<span
aria-hidden
className="grid size-10 shrink-0 place-items-center rounded-xl bg-[var(--surface-sunken)] text-[var(--text-muted)]"
{/* Mode switcher: this page builds a study deck from notes; the other
card hands the same intake over to the homework flow. */}
<div className="grid grid-cols-2 gap-2.5 sm:gap-3">
<Link
href="/create"
aria-current="page"
className="card touch-target flex flex-col items-start gap-2 rounded-2xl border-accent-400/60 bg-accent-50/60 p-3 ring-2 ring-accent-400/30 sm:flex-row sm:items-center sm:gap-3 sm:p-3.5 dark:bg-accent-500/15"
>
<SparklesIcon className="size-5" />
</span>
<span className="min-w-0 flex-1">
<span className="block text-[14px] font-semibold text-[var(--text)]">{t("homework.entry")}</span>
<span className="block text-[12.5px] text-[var(--text-subtle)]">{t("homework.nav")} →</span>
</span>
<ChevronRightIcon className="size-4 shrink-0 text-[var(--text-subtle)]" />
</Link>
<span
aria-hidden
className="grad-ice grid size-9 shrink-0 place-items-center rounded-xl text-[#06111f]"
>
<SnowflakeIcon className="size-5" />
</span>
<span className="min-w-0 text-[13px] font-semibold leading-snug text-[var(--text)] sm:text-[13.5px]">
{t("actionStudyDeck")}
</span>
</Link>
<Link
href="/homework"
className="card card-hover touch-target flex flex-col items-start gap-2 rounded-2xl p-3 sm:flex-row sm:items-center sm:gap-3 sm:p-3.5"
>
<span
aria-hidden
className="grid size-9 shrink-0 place-items-center rounded-xl bg-[var(--surface-sunken)] text-[var(--text-muted)]"
>
<SparklesIcon className="size-5" />
</span>
<span className="min-w-0 text-[13px] font-semibold leading-snug text-[var(--text)] sm:text-[13.5px]">
{t("actionHomework")}
</span>
</Link>
</div>
{error ? (
@ -676,7 +692,7 @@ export function UploadScreen() {
<button
type="button"
onClick={() => setCreatingFolder(true)}
className="inline-flex items-center gap-1.5 rounded-xl border border-dashed border-[var(--hairline)] px-3 py-1.5 text-sm font-semibold text-frost-500 transition-colors hover:text-accent-700 dark:text-frost-400 dark:hover:text-accent-200"
className="inline-flex items-center gap-1.5 rounded-xl border border-dashed border-[var(--hairline)] px-3 py-1.5 text-sm font-semibold text-frost-600 transition-colors hover:text-accent-700 dark:text-frost-400 dark:hover:text-accent-200"
>
<PlusIcon className="size-4" /> {t("common.newFolder")}
</button>
@ -705,7 +721,7 @@ export function UploadScreen() {
{option.icon}
</span>
<span className="text-sm font-bold text-[var(--text)]">{t(option.labelKey)}</span>
<span className="hidden text-[11px] text-frost-500 sm:block dark:text-frost-400">{t(option.blurbKey)}</span>
<span className="hidden text-[11px] text-frost-600 sm:block dark:text-frost-400">{t(option.blurbKey)}</span>
</button>
);
})}