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
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useMemo, useState, useSyncExternalStore } from "react";
|
||||
import { useMemo, useState, useSyncExternalStore, type ReactNode } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
import {
|
||||
|
|
@ -26,7 +26,15 @@ import { cn } from "@/lib/utils";
|
|||
const COLLAPSE_KEY = "freezmo.sidebarCollapsed";
|
||||
|
||||
/** Shared nav content for the desktop rail and the mobile drawer. */
|
||||
function NavContent({ collapsed = false, onNavigate }: { collapsed?: boolean; onNavigate?: () => 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 ? <span className="text-[12px] tabular-nums opacity-60">{value}</span> : null;
|
||||
|
||||
/** One shared view row (All decks / Unorganized) for both nav layouts. */
|
||||
const viewRow = (id: string, label: string, icon: ReactNode, trailing: ReactNode) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => select(id)}
|
||||
aria-pressed={selected === id}
|
||||
title={label}
|
||||
className={cn("nav-row", collapsed && "justify-center px-2")}
|
||||
>
|
||||
{icon}
|
||||
{!collapsed ? <span className="flex-1 truncate text-left">{label}</span> : null}
|
||||
{trailing}
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
{/* brand ---------------------------------------------------------- */}
|
||||
|
|
@ -122,7 +145,8 @@ function NavContent({ collapsed = false, onNavigate }: { collapsed?: boolean; on
|
|||
</Link>
|
||||
|
||||
{/* folders --------------------------------------------------------- */}
|
||||
{!collapsed ? (
|
||||
{/* Mobile keeps the original layout: one Folders label above the list. */}
|
||||
{!desktop && !collapsed ? (
|
||||
<div className="flex items-center justify-between px-2 pb-1">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wider text-[var(--text-subtle)]">
|
||||
Folders
|
||||
|
|
@ -139,7 +163,10 @@ function NavContent({ collapsed = false, onNavigate }: { collapsed?: boolean; on
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
<nav aria-label="Folders" className="thin-scroll min-h-0 flex-1 space-y-0.5 overflow-y-auto">
|
||||
<nav
|
||||
aria-label={desktop ? "Library" : "Folders"}
|
||||
className="thin-scroll min-h-0 flex-1 space-y-0.5 overflow-y-auto"
|
||||
>
|
||||
{collapsed ? (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -152,29 +179,57 @@ function NavContent({ collapsed = false, onNavigate }: { collapsed?: boolean; on
|
|||
</button>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => select(ALL_FOLDERS)}
|
||||
aria-pressed={selected === ALL_FOLDERS}
|
||||
title="All decks"
|
||||
className={cn("nav-row", collapsed && "justify-center px-2")}
|
||||
>
|
||||
<LayersIcon className="size-4 shrink-0 opacity-70" />
|
||||
{!collapsed ? <span className="flex-1 truncate text-left">All decks</span> : null}
|
||||
{count(decks.length)}
|
||||
</button>
|
||||
{/* Desktop: All decks + Unorganized are views, so they sit above the
|
||||
Folders label; mobile keeps them inside the folder list. */}
|
||||
{desktop ? (
|
||||
<div className={cn("space-y-0.5", !collapsed && "mb-1")}>
|
||||
{viewRow(
|
||||
ALL_FOLDERS,
|
||||
"All decks",
|
||||
<LayersIcon className="size-4 shrink-0 opacity-70" />,
|
||||
count(decks.length),
|
||||
)}
|
||||
{viewRow(
|
||||
UNORGANIZED_ID,
|
||||
"Unorganized",
|
||||
<FolderIcon className="size-4 shrink-0 opacity-70" />,
|
||||
null,
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{viewRow(
|
||||
ALL_FOLDERS,
|
||||
"All decks",
|
||||
<LayersIcon className="size-4 shrink-0 opacity-70" />,
|
||||
count(decks.length),
|
||||
)}
|
||||
{viewRow(
|
||||
UNORGANIZED_ID,
|
||||
"Unorganized",
|
||||
<FolderIcon className="size-4 shrink-0 opacity-70" />,
|
||||
count(counts[UNORGANIZED_ID] ?? 0),
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => select(UNORGANIZED_ID)}
|
||||
aria-pressed={selected === UNORGANIZED_ID}
|
||||
title="Unorganized"
|
||||
className={cn("nav-row", collapsed && "justify-center px-2")}
|
||||
>
|
||||
<FolderIcon className="size-4 shrink-0 opacity-70" />
|
||||
{!collapsed ? <span className="flex-1 truncate text-left">Unorganized</span> : null}
|
||||
{count(counts[UNORGANIZED_ID] ?? 0)}
|
||||
</button>
|
||||
{/* Desktop: the Folders label sits between the views and the folders. */}
|
||||
{desktop && !collapsed ? (
|
||||
<div className="flex items-center justify-between px-2 pb-1 pt-1">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wider text-[var(--text-subtle)]">
|
||||
Folders
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => 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)]"
|
||||
>
|
||||
<PlusIcon className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{sortedFolders.map((folder) => (
|
||||
<button
|
||||
|
|
@ -187,7 +242,7 @@ function NavContent({ collapsed = false, onNavigate }: { collapsed?: boolean; on
|
|||
>
|
||||
<FolderIcon className="size-4 shrink-0 opacity-70" />
|
||||
{!collapsed ? <span className="flex-1 truncate text-left">{folder.name}</span> : null}
|
||||
{count(counts[folder.id] ?? 0)}
|
||||
{desktop ? null : count(counts[folder.id] ?? 0)}
|
||||
</button>
|
||||
))}
|
||||
|
||||
|
|
@ -311,7 +366,7 @@ export function Sidebar() {
|
|||
isCollapsed ? "w-[68px]" : "w-[260px]",
|
||||
)}
|
||||
>
|
||||
<NavContent collapsed={isCollapsed} />
|
||||
<NavContent collapsed={isCollapsed} desktop />
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue