Keep the library header steady across tabs; simplify the empty states
Header & action bar
- Import JSON no longer disappears on the Homework tab: it sits directly
under the "Your icebox" header/subtitle, above the tab switcher, on every
tab — so switching pools no longer shifts the layout or hides actions. It
is one secondary button at all sizes now, not a quiet link when empty.
- The decks-only "Homework Solver" header action is gone; the homework tab's
empty-state CTA and the drawer cover starting a new solve.
Empty states
- Dropped the "Using your own Ollama models — check the connection in
Settings." line (and its two dictionary keys) everywhere.
- Both tabs share one structure: ice icon -> heading -> short description ->
primary CTA button, starting at the same height:
Study Decks: "No decks yet" -> "Freeze a page of notes into flashcards
and quizzes." -> "Create study deck"
Homework: "No homework yet" -> "Solved assignments and step-by-step
guides show up here." -> "Solve homework"
- CTA labels read as actions on the standardized nouns; removed the now
unused actionStudyDeck/actionHomework keys.
Tabs
- Link segments are 44px tall at every breakpoint (min-height in the base
.segmented > a rule).
Service worker: freezmo-v11.
Verified: library-consistency suite 49/49 (Import visible with a fixed
position on both tabs, no Ollama text, empty states at the same y, primary
ice CTA buttons >=44px, deep links, deletion, redirects, desktop, 360px, axe
en+cs light+dark), mobile 51/51, i18n 27/27, desktop regression 74/74.
Lint + build clean.
This commit is contained in:
parent
8c44f47881
commit
9d6ef90f02
4 changed files with 55 additions and 139 deletions
|
|
@ -6,13 +6,12 @@ import { useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|||
import { DeckCard } from "@/components/DeckCard";
|
||||
import { FolderBar } from "@/components/FolderBar";
|
||||
import { HomeworkCard } from "@/components/HomeworkCard";
|
||||
import { FolderIcon, SnowflakeIcon, SparklesIcon, UploadIcon } from "@/components/icons";
|
||||
import { FolderIcon, UploadIcon } from "@/components/icons";
|
||||
import { Button, EmptyState } from "@/components/ui";
|
||||
import { folderStore, renameFolder } from "@/lib/folders";
|
||||
import { deleteHomework, homeworkStore, sortHomework } from "@/lib/homework";
|
||||
import { useI18n } from "@/lib/i18n";
|
||||
import { ALL_FOLDERS, folderSelection, useSelectedFolder } from "@/lib/nav";
|
||||
import { openSettings } from "@/lib/ui-events";
|
||||
import { UNORGANIZED_ID, type Deck } from "@/lib/schemas";
|
||||
import { deckStore, deleteFolder, importDeck, sortDecks, upsertDeck } from "@/lib/storage";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
|
@ -118,89 +117,47 @@ export function LibraryScreen({ initialTab = "decks" }: { initialTab?: "decks" |
|
|||
<div className="relative">
|
||||
{/* page header ---------------------------------------------------- */}
|
||||
<section className="mb-6 space-y-5">
|
||||
<div className="flex flex-wrap items-end justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-[24px] font-semibold leading-tight tracking-[-0.02em] text-[var(--text)] sm:text-[26px]">
|
||||
{t("library.title")}
|
||||
</h1>
|
||||
<p className="mt-1.5 text-[13.5px] text-[var(--text-subtle)]">
|
||||
{t("library.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-[24px] font-semibold leading-tight tracking-[-0.02em] text-[var(--text)] sm:text-[26px]">
|
||||
{t("library.title")}
|
||||
</h1>
|
||||
<p className="mt-1.5 text-[13.5px] text-[var(--text-subtle)]">
|
||||
{t("library.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{/* Context action: Import for decks (a quiet link while the pool
|
||||
is empty), start-a-new-solve for homework. */}
|
||||
{tab === "decks" ? (
|
||||
isEmpty ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => importRef.current?.click()}
|
||||
className="inline-flex items-center gap-1.5 text-[13px] font-medium text-[var(--text-muted)] transition hover:text-[var(--text)] hover:underline"
|
||||
>
|
||||
<UploadIcon className="size-4" />
|
||||
{t("library.import")}
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="md"
|
||||
className="touch-target lg:hidden"
|
||||
onClick={() => importRef.current?.click()}
|
||||
>
|
||||
<UploadIcon className="size-4" />
|
||||
{t("library.import")}
|
||||
</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" />
|
||||
{t("library.import")}
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
) : homework.length > 0 ? (
|
||||
<>
|
||||
<Link
|
||||
href="/homework?mode=homework"
|
||||
className="btn-secondary touch-target h-10 px-4 text-sm lg:hidden"
|
||||
>
|
||||
<SparklesIcon className="size-4" />
|
||||
{t("actionHomework")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/homework?mode=homework"
|
||||
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"
|
||||
>
|
||||
<SparklesIcon className="size-4" />
|
||||
{t("actionHomework")}
|
||||
</Link>
|
||||
</>
|
||||
) : null}
|
||||
<input
|
||||
ref={importRef}
|
||||
type="file"
|
||||
accept="application/json,.json"
|
||||
className="sr-only"
|
||||
onChange={(event) => {
|
||||
void handleImport(event.target.files?.[0]);
|
||||
event.target.value = "";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* Import sits above the tabs on every tab, so the header never
|
||||
shifts when you switch pools. */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="md"
|
||||
className="touch-target"
|
||||
onClick={() => importRef.current?.click()}
|
||||
>
|
||||
<UploadIcon className="size-4" />
|
||||
{t("library.import")}
|
||||
</Button>
|
||||
<input
|
||||
ref={importRef}
|
||||
type="file"
|
||||
accept="application/json,.json"
|
||||
className="sr-only"
|
||||
onChange={(event) => {
|
||||
void handleImport(event.target.files?.[0]);
|
||||
event.target.value = "";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Two pools, never mixed: study decks and saved homework. */}
|
||||
<div className="segmented w-full sm:max-w-sm">
|
||||
<Link href="/" aria-current={tab === "decks" ? "page" : undefined}>
|
||||
{t("tabDecks")}
|
||||
</Link>
|
||||
<Link href="/?tab=homework" aria-current={tab === "homework" ? "page" : undefined}>
|
||||
{t("tabHomework")}
|
||||
</Link>
|
||||
</div>
|
||||
{/* Two pools, never mixed: study decks and saved homework. */}
|
||||
<div className="segmented w-full sm:max-w-sm">
|
||||
<Link href="/" aria-current={tab === "decks" ? "page" : undefined}>
|
||||
{t("tabDecks")}
|
||||
</Link>
|
||||
<Link href="/?tab=homework" aria-current={tab === "homework" ? "page" : undefined}>
|
||||
{t("tabHomework")}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Secondary stats folded into the header as quiet text on desktop,
|
||||
|
|
@ -263,34 +220,12 @@ export function LibraryScreen({ initialTab = "decks" }: { initialTab?: "decks" |
|
|||
<EmptyState
|
||||
title={t("library.noDecks")}
|
||||
action={
|
||||
<Link
|
||||
href="/create?mode=deck"
|
||||
className="card card-hover touch-target flex w-[min(24rem,calc(100vw_-_3rem))] 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 href="/create?mode=deck" className="btn-primary touch-target h-11 px-5 text-sm">
|
||||
{t("emptyStateCreateDeck")}
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
{t("library.emptyBody")}
|
||||
<span className="mt-3 block text-[12.5px] text-[var(--text-subtle)]">
|
||||
{t("library.emptyHint")}{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={openSettings}
|
||||
className="font-medium text-[var(--text-muted)] underline underline-offset-2 hover:text-[var(--text)]"
|
||||
>
|
||||
{t("library.emptySettings")}
|
||||
</button>
|
||||
.
|
||||
</span>
|
||||
</EmptyState>
|
||||
) : null}
|
||||
|
||||
|
|
@ -424,19 +359,8 @@ export function LibraryScreen({ initialTab = "decks" }: { initialTab?: "decks" |
|
|||
<EmptyState
|
||||
title={t("homework.emptyTitle")}
|
||||
action={
|
||||
<Link
|
||||
href="/homework?mode=homework"
|
||||
className="card card-hover touch-target flex w-[min(24rem,calc(100vw_-_3rem))] 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]"
|
||||
>
|
||||
<SparklesIcon className="size-5" />
|
||||
</span>
|
||||
<span className="min-w-0 text-[14px] font-semibold leading-snug text-[var(--text)]">
|
||||
{t("emptyStateHomework")}
|
||||
</span>
|
||||
<Link href="/homework?mode=homework" className="btn-primary touch-target h-11 px-5 text-sm">
|
||||
{t("emptyStateHomework")}
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue