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
|
|
@ -499,6 +499,7 @@ body {
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.375rem;
|
||||
min-height: 2.75rem;
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.42rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
|
|
@ -727,8 +728,7 @@ body {
|
|||
@media (max-width: 639px) {
|
||||
.touch-target { min-height: 44px; min-width: 44px; }
|
||||
.nav-row { min-height: 44px; }
|
||||
.segmented > button,
|
||||
.segmented > a { min-height: 44px; }
|
||||
.segmented > button { min-height: 44px; }
|
||||
.folder-pill { min-height: 44px; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ const en: Record<string, string> = {
|
|||
"homework.history.openTitle": "Open “{title}”",
|
||||
"homework.history.deleteConfirm": "Delete this result?",
|
||||
"homework.emptyTitle": "No homework yet",
|
||||
"homework.emptyBody": "Solved assignments and step-by-step guides you save show up here.",
|
||||
"homework.emptyBody": "Solved assignments and step-by-step guides show up here.",
|
||||
"homework.open": "Open",
|
||||
"homework.steps.one": "{n} step",
|
||||
"homework.steps.few": "{n} steps",
|
||||
|
|
@ -234,10 +234,8 @@ const en: Record<string, string> = {
|
|||
"tabHomework": "Homework",
|
||||
|
||||
/* quick actions (library empty state + /create switcher) ----------- */
|
||||
"emptyStateCreateDeck": "Study Deck",
|
||||
"emptyStateHomework": "Homework Solver",
|
||||
"actionStudyDeck": "Study Deck",
|
||||
"actionHomework": "Homework Solver",
|
||||
"emptyStateCreateDeck": "Create study deck",
|
||||
"emptyStateHomework": "Solve homework",
|
||||
|
||||
/* library --------------------------------------------------------- */
|
||||
"library.title": "Your icebox",
|
||||
|
|
@ -250,9 +248,7 @@ const en: Record<string, string> = {
|
|||
"library.deckMoved": "Deck moved.",
|
||||
"library.deckMovedUnorganized": "Deck moved to Unorganized.",
|
||||
"library.noDecks": "No decks yet",
|
||||
"library.emptyBody": "Freeze a page of notes into a feed of flashcards and quizzes.",
|
||||
"library.emptyHint": "Using your own Ollama models —",
|
||||
"library.emptySettings": "check the connection in Settings",
|
||||
"library.emptyBody": "Freeze a page of notes into flashcards and quizzes.",
|
||||
"library.renameFolderAria": "Folder name",
|
||||
"library.nothingUnfiled": "Nothing unfiled right now.",
|
||||
"library.emptyFolder": "Empty folder — freeze a deck into it or move one over from Unorganized.",
|
||||
|
|
@ -817,7 +813,7 @@ const cs: Record<string, string> = {
|
|||
"homework.history.openTitle": "Otevřít „{title}“",
|
||||
"homework.history.deleteConfirm": "Smazat tento výsledek?",
|
||||
"homework.emptyTitle": "Zatím žádné úkoly",
|
||||
"homework.emptyBody": "Uložená řešení úkolů a návody krok za krokem se objeví tady.",
|
||||
"homework.emptyBody": "Vyřešená zadání a návody krok za krokem se objeví tady.",
|
||||
"homework.open": "Otevřít",
|
||||
"homework.steps.one": "{n} krok",
|
||||
"homework.steps.few": "{n} kroky",
|
||||
|
|
@ -828,10 +824,8 @@ const cs: Record<string, string> = {
|
|||
"tabHomework": "Úkoly",
|
||||
|
||||
/* quick actions (library empty state + /create switcher) ----------- */
|
||||
"emptyStateCreateDeck": "Studijní balíček",
|
||||
"emptyStateHomework": "Řešitel úkolů",
|
||||
"actionStudyDeck": "Studijní balíček",
|
||||
"actionHomework": "Řešitel úkolů",
|
||||
"emptyStateCreateDeck": "Vytvořit studijní balíček",
|
||||
"emptyStateHomework": "Vyřešit úkol",
|
||||
|
||||
/* library --------------------------------------------------------- */
|
||||
"library.title": "Tvoje mraznička",
|
||||
|
|
@ -844,9 +838,7 @@ const cs: Record<string, string> = {
|
|||
"library.deckMoved": "Sada přesunuta.",
|
||||
"library.deckMovedUnorganized": "Sada přesunuta do Nezařazených.",
|
||||
"library.noDecks": "Zatím žádné sady",
|
||||
"library.emptyBody": "Zmraz stránku poznámek do feedu kartiček a kvízů.",
|
||||
"library.emptyHint": "S tvými vlastními modely Ollama —",
|
||||
"library.emptySettings": "zkontroluj připojení v Nastavení",
|
||||
"library.emptyBody": "Zmraz stránku poznámek do kartiček a kvízů.",
|
||||
"library.renameFolderAria": "Název složky",
|
||||
"library.nothingUnfiled": "Teď tu nic nezařazeného není.",
|
||||
"library.emptyFolder": "Prázdná složka — zmraz do ní sadu nebo ji sem přesuň z Nezařazených.",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
*
|
||||
* Bumping CACHE_VERSION is how a new deploy invalidates old assets.
|
||||
*/
|
||||
const CACHE_VERSION = "freezmo-v10";
|
||||
const CACHE_VERSION = "freezmo-v11";
|
||||
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
||||
const ASSET_CACHE = `${CACHE_VERSION}-assets`;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue