Desktop library pass: alignment, card weight, sidebar and theme
Layout (desktop / lg+ only): - 40px top padding above the page title. - Deck grid uses minmax(320px, 1fr), so two cards fill the content width and share a right edge with the header (same max-width container). - Group headings only render once real folders exist; with none, a single quiet "All decks" label replaces "Unorganized". - Import JSON becomes a plain text button (mobile keeps the pill). Deck cards (desktop): - The whole card is a link; Study is a compact secondary button instead of a full-width solid pill. - The ... control is a 32px ghost button with a real ellipsis icon, aligned to the title's centre. - Meta line always shows the same fields (activities · runs / Not played yet); the score is a separate quiet "Best 53%" stat. - 12px rhythm between title/meta/tags/summary, 16px before the actions. - Hover: lighter surface, stronger border, soft shadow and a 2px lift in light mode (pointer devices only, so touch has no sticky hover). Sidebar (desktop rail): - All decks + Unorganized are views above the FOLDERS label; only user-created folders sit under it. - Count only on All decks. The mobile drawer is unchanged. Theme: - Dark: sidebar is a step darker (#181818 to #141414 vs #212121 page). - Light: cards get a slightly stronger border (#e2e2e2) and a soft shadow; the sidebar divider and edge shadow are stronger too. - Medium difficulty darkened to #7a5300 (6.2:1 on the light chip). Mobile is untouched: layout changes are behind lg: (or the desktop prop); theme tweaks are shared tokens. Verified: key-gate 49/49, install sheet 64/64, retry 14/14, zoom 6/6, deck chrome 15/15, deck fixes 15/15, final ux 12/12, contrast 20/20, security 8/8, pwa 16/16.
This commit is contained in:
parent
190664c1a5
commit
3aae6f3199
5 changed files with 196 additions and 67 deletions
|
|
@ -9,7 +9,7 @@ import { createFolder as makeFolder, folderName, listFolders } from "@/lib/folde
|
|||
import { UNORGANIZED_ID, type Deck } from "@/lib/schemas";
|
||||
import { appendActivities, deleteDeck, downloadDeck, renameDeck, upsertDeck } from "@/lib/storage";
|
||||
import { cn, formatDate, truncate } from "@/lib/utils";
|
||||
import { DownloadIcon, FolderIcon, PlayIcon, RefreshIcon } from "@/components/icons";
|
||||
import { DownloadIcon, EllipsisIcon, FolderIcon, PlayIcon, RefreshIcon } from "@/components/icons";
|
||||
import { Button, ProgressBar } from "@/components/ui";
|
||||
|
||||
type Busy = null | "more" | "regenerate";
|
||||
|
|
@ -109,11 +109,19 @@ export function DeckCard({ deck, onMove }: { deck: Deck; onMove?: (folderId: str
|
|||
|
||||
return (
|
||||
<li className="card card-hover group relative flex flex-col p-5">
|
||||
{/* Desktop: the whole card is a link; interactive children sit above it. */}
|
||||
<Link
|
||||
href={`/deck/${deck.id}`}
|
||||
aria-hidden="true"
|
||||
tabIndex={-1}
|
||||
className="absolute inset-0 hidden rounded-[1.125rem] lg:block"
|
||||
/>
|
||||
|
||||
{/* header ------------------------------------------------------- */}
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
{renaming ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative z-10 flex items-center gap-2">
|
||||
<label className="sr-only" htmlFor={`rename-${deck.id}`}>
|
||||
Deck title
|
||||
</label>
|
||||
|
|
@ -144,16 +152,28 @@ export function DeckCard({ deck, onMove }: { deck: Deck; onMove?: (folderId: str
|
|||
{truncate(deck.title, 90)}
|
||||
</h3>
|
||||
)}
|
||||
<p className="mt-1 text-[12.5px] text-[var(--text-subtle)]">
|
||||
{formatDate(deck.updatedAt)}
|
||||
<span aria-hidden> · </span>
|
||||
<p className="mt-1 text-[12.5px] text-[var(--text-subtle)] lg:mt-3">
|
||||
{/* Phones keep the original date · activities · runs line. */}
|
||||
<span className="lg:hidden">
|
||||
{formatDate(deck.updatedAt)}
|
||||
<span aria-hidden> · </span>
|
||||
</span>
|
||||
{deck.activities.length} activities
|
||||
{deck.attempts > 0 ? (
|
||||
<>
|
||||
<span aria-hidden> · </span>
|
||||
{deck.attempts} {deck.attempts === 1 ? "run" : "runs"}
|
||||
</>
|
||||
) : null}
|
||||
<span className="lg:hidden">
|
||||
{deck.attempts > 0 ? (
|
||||
<>
|
||||
<span aria-hidden> · </span>
|
||||
{deck.attempts} {deck.attempts === 1 ? "run" : "runs"}
|
||||
</>
|
||||
) : null}
|
||||
</span>
|
||||
{/* Desktop always shows the same two fields. */}
|
||||
<span className="hidden lg:inline">
|
||||
<span aria-hidden> · </span>
|
||||
{deck.attempts > 0
|
||||
? `${deck.attempts} ${deck.attempts === 1 ? "run" : "runs"}`
|
||||
: "Not played yet"}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -163,11 +183,12 @@ export function DeckCard({ deck, onMove }: { deck: Deck; onMove?: (folderId: str
|
|||
aria-expanded={menuOpen}
|
||||
onClick={() => setMenuOpen((value) => !value)}
|
||||
className={cn(
|
||||
"grid size-8 shrink-0 place-items-center rounded-full text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)]",
|
||||
"relative z-10 grid size-8 shrink-0 place-items-center rounded-full text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)] lg:-mt-1.5",
|
||||
menuOpen && "bg-[var(--hover)] text-[var(--text)]",
|
||||
)}
|
||||
>
|
||||
⋯
|
||||
<span aria-hidden className="lg:hidden">⋯</span>
|
||||
<EllipsisIcon className="hidden size-4 lg:block" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -184,9 +205,14 @@ export function DeckCard({ deck, onMove }: { deck: Deck; onMove?: (folderId: str
|
|||
</Tag>
|
||||
) : null}
|
||||
{deck.bestScore !== undefined ? (
|
||||
<Tag>
|
||||
Best <span className="text-emerald-700 dark:text-emerald-300">{deck.bestScore}%</span>
|
||||
</Tag>
|
||||
<>
|
||||
<Tag className="lg:hidden">
|
||||
Best <span className="text-emerald-700 dark:text-emerald-300">{deck.bestScore}%</span>
|
||||
</Tag>
|
||||
<span className="hidden text-[12px] font-medium tabular-nums text-[var(--text-muted)] lg:ml-1 lg:inline">
|
||||
Best {deck.bestScore}%
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
|
|
@ -219,18 +245,24 @@ export function DeckCard({ deck, onMove }: { deck: Deck; onMove?: (folderId: str
|
|||
) : null}
|
||||
|
||||
{/* single action ------------------------------------------------ */}
|
||||
<div className="mt-5">
|
||||
<Link href={`/deck/${deck.id}`} className="btn-primary h-10 w-full px-4 text-sm">
|
||||
<div className="mt-5 lg:mt-4">
|
||||
<Link href={`/deck/${deck.id}`} className="btn-primary h-10 w-full px-4 text-sm lg:hidden">
|
||||
<PlayIcon className="size-4" />
|
||||
Study
|
||||
</Link>
|
||||
<div className="relative z-10 hidden lg:flex lg:justify-end">
|
||||
<Link href={`/deck/${deck.id}`} className="btn-secondary h-9 px-4 text-[13px]">
|
||||
<PlayIcon className="size-4" />
|
||||
Study
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* actions panel ------------------------------------------------ */}
|
||||
{menuOpen ? (
|
||||
<div
|
||||
data-testid="deck-actions"
|
||||
className="animate-rise mt-3 space-y-1 border-t border-[var(--hairline)] pt-3"
|
||||
className="animate-rise relative z-10 mt-3 space-y-1 border-t border-[var(--hairline)] pt-3"
|
||||
>
|
||||
<button type="button" className={menuRow} onClick={() => setRenaming(true)} disabled={Boolean(busy)}>
|
||||
<span aria-hidden className="w-4 text-center text-[var(--text-subtle)]">
|
||||
|
|
@ -398,9 +430,14 @@ function difficultyClass(difficulty: Deck["difficulty"]): string {
|
|||
}
|
||||
|
||||
/** Neutral pill; colour is only ever used on the text inside it. */
|
||||
function Tag({ children }: { children: React.ReactNode }) {
|
||||
function Tag({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-[var(--surface-sunken)] px-2.5 py-1 text-[12px] font-medium leading-none text-[var(--text-muted)]">
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 rounded-full bg-[var(--surface-sunken)] px-2.5 py-1 text-[12px] font-medium leading-none text-[var(--text-muted)]",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue