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:
Martin Dvořák 2026-10-07 22:37:29 +00:00
commit 3aae6f3199
5 changed files with 196 additions and 67 deletions

View file

@ -98,6 +98,9 @@ export function LibraryScreen() {
};
const isEmpty = sorted.length === 0 && folderList.length === 0;
// Group headings only earn their keep once real folders exist; with none,
// desktop shows a single quiet “All decks” label instead of “Unorganized”.
const hasFolders = folderList.length > 0;
return (
<div className="relative">
@ -114,10 +117,23 @@ export function LibraryScreen() {
</div>
<div className="flex flex-wrap items-center gap-2">
<Button variant="secondary" size="md" onClick={() => importRef.current?.click()}>
<Button
variant="secondary"
size="md"
className="lg:hidden"
onClick={() => importRef.current?.click()}
>
<UploadIcon className="size-4" />
Import JSON
</Button>
<button
type="button"
onClick={() => importRef.current?.click()}
className="hidden items-center gap-1.5 text-[13px] font-medium text-[var(--text-muted)] transition hover:text-[var(--text)] hover:underline lg:inline-flex"
>
<UploadIcon className="size-4" />
Import JSON
</button>
<input
ref={importRef}
type="file"
@ -211,7 +227,7 @@ export function LibraryScreen() {
return (
<section key={group.id} aria-label={group.name}>
{/* group header -------------------------------------- */}
<div className="mb-4 flex items-center gap-2">
<div className={cn("mb-4 flex items-center gap-2", !hasFolders && "lg:hidden")}>
<FolderIcon className="size-4 shrink-0 text-[var(--text-subtle)]" />
{isEditing && editingFolder ? (
@ -288,6 +304,13 @@ export function LibraryScreen() {
) : null}
</div>
{/* One quiet label replaces the redundant single-group heading. */}
{!hasFolders ? (
<p className="mb-4 hidden text-[12px] font-medium text-[var(--text-subtle)] lg:block">
All decks
</p>
) : null}
{confirmDelete === group.id ? (
<p className="mb-3 text-[12.5px] text-[var(--text-muted)]">
The {group.decks.length} deck{group.decks.length === 1 ? "" : "s"} inside will move to
@ -302,7 +325,7 @@ export function LibraryScreen() {
: "Empty folder — freeze a deck into it or move one over from Unorganized."}
</div>
) : (
<ul className="grid list-none grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-4">
<ul className="grid list-none grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-4 lg:grid-cols-[repeat(auto-fill,minmax(320px,1fr))]">
{group.decks.map((deck) => (
<DeckCard
key={deck.id}