+
diff --git a/components/DeckCard.tsx b/components/DeckCard.tsx
index f00e7bc..ca9f85e 100644
--- a/components/DeckCard.tsx
+++ b/components/DeckCard.tsx
@@ -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 (
+ {/* Desktop: the whole card is a link; interactive children sit above it. */}
+
+
{/* header ------------------------------------------------------- */}
{renaming ? (
-
+
Deck title
@@ -144,16 +152,28 @@ export function DeckCard({ deck, onMove }: { deck: Deck; onMove?: (folderId: str
{truncate(deck.title, 90)}
)}
-
- {formatDate(deck.updatedAt)}
- ·
+
+ {/* Phones keep the original date · activities · runs line. */}
+
+ {formatDate(deck.updatedAt)}
+ ·
+
{deck.activities.length} activities
- {deck.attempts > 0 ? (
- <>
- ·
- {deck.attempts} {deck.attempts === 1 ? "run" : "runs"}
- >
- ) : null}
+
+ {deck.attempts > 0 ? (
+ <>
+ ·
+ {deck.attempts} {deck.attempts === 1 ? "run" : "runs"}
+ >
+ ) : null}
+
+ {/* Desktop always shows the same two fields. */}
+
+ ·
+ {deck.attempts > 0
+ ? `${deck.attempts} ${deck.attempts === 1 ? "run" : "runs"}`
+ : "Not played yet"}
+
@@ -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)]",
)}
>
- ⋯
+
⋯
+
@@ -184,9 +205,14 @@ export function DeckCard({ deck, onMove }: { deck: Deck; onMove?: (folderId: str
) : null}
{deck.bestScore !== undefined ? (
-
- Best {deck.bestScore}%
-
+ <>
+
+ Best {deck.bestScore}%
+
+
+ Best {deck.bestScore}%
+
+ >
) : null}
@@ -219,18 +245,24 @@ export function DeckCard({ deck, onMove }: { deck: Deck; onMove?: (folderId: str
) : null}
{/* single action ------------------------------------------------ */}
-
-
+
{/* actions panel ------------------------------------------------ */}
{menuOpen ? (
setRenaming(true)} disabled={Boolean(busy)}>
@@ -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 (
-
+
{children}
);
diff --git a/components/LibraryScreen.tsx b/components/LibraryScreen.tsx
index 0a387ff..536dbd5 100644
--- a/components/LibraryScreen.tsx
+++ b/components/LibraryScreen.tsx
@@ -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 (
@@ -114,10 +117,23 @@ export function LibraryScreen() {
-
importRef.current?.click()}>
+ importRef.current?.click()}
+ >
Import JSON
+ 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"
+ >
+
+ Import JSON
+
{/* group header -------------------------------------- */}
-
+
{isEditing && editingFolder ? (
@@ -288,6 +304,13 @@ export function LibraryScreen() {
) : null}
+ {/* One quiet label replaces the redundant single-group heading. */}
+ {!hasFolders ? (
+
+ All decks
+
+ ) : null}
+
{confirmDelete === group.id ? (
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."}
) : (
-
+
{group.decks.map((deck) => (
void }) {
+function NavContent({
+ collapsed = false,
+ onNavigate,
+ desktop = false,
+}: {
+ collapsed?: boolean;
+ onNavigate?: () => void;
+ desktop?: boolean;
+}) {
const decks = useSyncExternalStore(deckStore.subscribe, deckStore.getSnapshot, deckStore.getServerSnapshot);
const folders = useSyncExternalStore(
folderStore.subscribe,
@@ -76,6 +84,21 @@ function NavContent({ collapsed = false, onNavigate }: { collapsed?: boolean; on
const count = (value: number) =>
!collapsed && value > 0 ? {value} : null;
+ /** One shared view row (All decks / Unorganized) for both nav layouts. */
+ const viewRow = (id: string, label: string, icon: ReactNode, trailing: ReactNode) => (
+ select(id)}
+ aria-pressed={selected === id}
+ title={label}
+ className={cn("nav-row", collapsed && "justify-center px-2")}
+ >
+ {icon}
+ {!collapsed ? {label} : null}
+ {trailing}
+
+ );
+
return (
{/* brand ---------------------------------------------------------- */}
@@ -122,7 +145,8 @@ function NavContent({ collapsed = false, onNavigate }: { collapsed?: boolean; on
{/* folders --------------------------------------------------------- */}
- {!collapsed ? (
+ {/* Mobile keeps the original layout: one Folders label above the list. */}
+ {!desktop && !collapsed ? (
Folders
@@ -139,7 +163,10 @@ function NavContent({ collapsed = false, onNavigate }: { collapsed?: boolean; on
) : null}
-
+
{collapsed ? (
) : null}
- select(ALL_FOLDERS)}
- aria-pressed={selected === ALL_FOLDERS}
- title="All decks"
- className={cn("nav-row", collapsed && "justify-center px-2")}
- >
-
- {!collapsed ? All decks : null}
- {count(decks.length)}
-
+ {/* Desktop: All decks + Unorganized are views, so they sit above the
+ Folders label; mobile keeps them inside the folder list. */}
+ {desktop ? (
+
+ {viewRow(
+ ALL_FOLDERS,
+ "All decks",
+ ,
+ count(decks.length),
+ )}
+ {viewRow(
+ UNORGANIZED_ID,
+ "Unorganized",
+ ,
+ null,
+ )}
+
+ ) : (
+ <>
+ {viewRow(
+ ALL_FOLDERS,
+ "All decks",
+ ,
+ count(decks.length),
+ )}
+ {viewRow(
+ UNORGANIZED_ID,
+ "Unorganized",
+ ,
+ count(counts[UNORGANIZED_ID] ?? 0),
+ )}
+ >
+ )}
- select(UNORGANIZED_ID)}
- aria-pressed={selected === UNORGANIZED_ID}
- title="Unorganized"
- className={cn("nav-row", collapsed && "justify-center px-2")}
- >
-
- {!collapsed ? Unorganized : null}
- {count(counts[UNORGANIZED_ID] ?? 0)}
-
+ {/* Desktop: the Folders label sits between the views and the folders. */}
+ {desktop && !collapsed ? (
+
+
+ Folders
+
+
setCreating(true)}
+ aria-label="New folder"
+ title="New folder"
+ className="grid size-6 place-items-center rounded-md text-[var(--text-subtle)] transition hover:bg-[var(--hover)] hover:text-[var(--text)]"
+ >
+
+
+
+ ) : null}
{sortedFolders.map((folder) => (
{!collapsed ? {folder.name} : null}
- {count(counts[folder.id] ?? 0)}
+ {desktop ? null : count(counts[folder.id] ?? 0)}
))}
@@ -311,7 +366,7 @@ export function Sidebar() {
isCollapsed ? "w-[68px]" : "w-[260px]",
)}
>
-
+
);
}