Wizard polish: step headlines, gated Next, real summary, portal action bar
Mobile only (<=639px); desktop markup untouched. Bug fix: the Back/Next bar was fixed-positioned inside the screen entrance animation's transformed wrapper, so it painted under the content and jumped to the viewport bottom about a second in. It is now portalled to <body> and only fades in, pinned to the bottom from the first frame. The other fixed/sticky elements (tab bar, drawer, sheets, install sheet) were checked and live outside the animated wrappers. - The step name is now the large headline with a one-line description; the repeated page title is dropped on phones. Thin progress bar + 1/3. - Step 1: formats/limits hint, one big dashed Add photos or files area (plus icon), Camera and Paste text instead as secondaries, and a 'nothing added yet' placeholder that becomes the thumbnail/chip list. - Next is disabled until a photo, file or pasted text exists; step 3 can never be reached empty. - Step 1 shows Cancel (back to the Library) instead of a dead Back. - Step 3 is a real summary: thumbnail/chip strip plus Files, Difficulty, Activities and Folder rows, each with an Edit link to its step; Freeze is the single primary action. - Step 2 got a visible Difficulty label and a helper line under every group, defaults still preselected. - Bottom chrome: flat full-width Back/Next bar above the safe area, and the tab bar hides inside both wizards. The lower-right orb is hidden on phones so it can never glow behind the bar. - Make my homework matches: headline, hint, big add area, placeholder, Cancel, gated Next, flat bar. Verified: new wizard suite 80/80 (bar position sampled at 5 frames during the entrance from tab bar/drawer/empty state; 360 + 390, dark + light, EN + CS, reduced motion on/off; gating, summary Edit links, hidden tab bar and orb), declutter 116/116, regression 74/74, mobile 52/52, animation 8/8, i18n 25/25. Lint + build clean. SW freezmo-v22.
This commit is contained in:
parent
374752c59e
commit
e66141e9a0
9 changed files with 338 additions and 132 deletions
|
|
@ -1,10 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { CameraIcon, FolderIcon, PlayIcon, PlusIcon, SparklesIcon } from "@/components/icons";
|
||||
import { Button } from "@/components/ui";
|
||||
import { WizardActionBar } from "@/components/WizardActionBar";
|
||||
import {
|
||||
DOCUMENT_ACCEPT_ATTR,
|
||||
documentsToText,
|
||||
|
|
@ -66,6 +68,7 @@ export function HomeworkInput({
|
|||
onDelete: (id: string) => void;
|
||||
}) {
|
||||
const { t, plural } = useI18n();
|
||||
const router = useRouter();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const cameraInputRef = useRef<HTMLInputElement>(null);
|
||||
const [step, setStep] = useState(1);
|
||||
|
|
@ -206,37 +209,41 @@ export function HomeworkInput({
|
|||
: fileSummary || t("homework.source.need");
|
||||
const showPaste = pasteOpen || text.trim().length > 0;
|
||||
|
||||
return (
|
||||
<div className="relative pb-24">
|
||||
<section className="space-y-1.5">
|
||||
<h1 className="text-[24px] font-semibold leading-tight tracking-[-0.02em] text-[var(--text)]">
|
||||
{t("homeworkHeroTitle")}
|
||||
</h1>
|
||||
<p className="text-[14px] text-frost-600 dark:text-frost-300">{t("homework.subtitle")}</p>
|
||||
</section>
|
||||
const stepDesc =
|
||||
step === 1
|
||||
? t("upload.phone.notesDesc", { images: 8, documents: 6 })
|
||||
: t("homework.phone.modeDesc");
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{errorCard}
|
||||
|
||||
<div className="mt-5 space-y-2">
|
||||
<div className="flex items-center justify-between gap-3 text-[12px] font-semibold text-[var(--text-subtle)]">
|
||||
<h2 className="min-w-0 truncate">{stepLabel}</h2>
|
||||
<span className="shrink-0 tabular-nums">{t("common.stepOf", { n: step, total: 2 })}</span>
|
||||
</div>
|
||||
{/* progress: thin bar + “1 / 2” */}
|
||||
<div className="mt-1 flex items-center gap-3">
|
||||
<div
|
||||
role="progressbar"
|
||||
aria-label={stepLabel}
|
||||
aria-valuemin={1}
|
||||
aria-valuemax={2}
|
||||
aria-valuenow={step}
|
||||
className="h-1 overflow-hidden rounded-full bg-[var(--surface-sunken)]"
|
||||
className="h-1 flex-1 overflow-hidden rounded-full bg-[var(--surface-sunken)]"
|
||||
>
|
||||
<div
|
||||
className="grad-ice h-full rounded-full transition-[width] duration-300 motion-reduce:transition-none"
|
||||
style={{ width: `${(step / 2) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="shrink-0 text-[11px] font-semibold tabular-nums text-[var(--text-subtle)]">
|
||||
{t("common.stepOf", { n: step, total: 2 })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* the step name is the headline; the page title lives in the chrome */}
|
||||
<h1 className="mt-4 text-[26px] font-semibold leading-tight tracking-[-0.02em] text-[var(--text)]">
|
||||
{stepLabel}
|
||||
</h1>
|
||||
<p className="mt-1 text-[14px] text-frost-600 dark:text-frost-300">{stepDesc}</p>
|
||||
|
||||
<motion.div
|
||||
key={step}
|
||||
initial="initial"
|
||||
|
|
@ -246,15 +253,21 @@ export function HomeworkInput({
|
|||
>
|
||||
{step === 1 ? (
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
className="touch-target"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="press flex min-h-32 w-full flex-col items-center justify-center gap-2 rounded-3xl border-2 border-dashed border-[var(--glass-border)] bg-[var(--surface-strong)] px-4 py-6 text-center transition hover:border-accent-400"
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className="grid size-12 place-items-center rounded-2xl bg-[var(--surface-sunken)] text-accent-600 dark:text-accent-200"
|
||||
>
|
||||
<FolderIcon className="size-4" /> {t("homework.source.add")}
|
||||
</Button>
|
||||
<PlusIcon className="size-6" />
|
||||
</span>
|
||||
<span className="text-[15px] font-semibold text-[var(--text)]">{t("homework.source.add")}</span>
|
||||
</button>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
|
|
@ -263,16 +276,16 @@ export function HomeworkInput({
|
|||
>
|
||||
<CameraIcon className="size-4" /> {t("upload.camera")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
className="touch-target"
|
||||
aria-expanded={showPaste}
|
||||
onClick={() => setPasteOpen((value) => !value)}
|
||||
>
|
||||
{t("upload.pasteInstead")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPasteOpen((value) => !value)}
|
||||
aria-expanded={showPaste}
|
||||
className="text-[13px] font-semibold text-accent-700 dark:text-accent-200"
|
||||
>
|
||||
{t("upload.pasteInstead")}
|
||||
</button>
|
||||
{showPaste ? (
|
||||
<textarea
|
||||
id="homework-assignment-phone"
|
||||
|
|
@ -345,6 +358,12 @@ export function HomeworkInput({
|
|||
</ul>
|
||||
) : null}
|
||||
|
||||
{intake.images.length === 0 && intake.documents.length === 0 ? (
|
||||
<p className="rounded-2xl border border-dashed border-[var(--hairline)] px-4 py-3 text-center text-[12.5px] text-[var(--text-subtle)]">
|
||||
{t("upload.nothingAdded")}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{intake.documents
|
||||
.filter((entry) => entry.status === "error")
|
||||
.map((entry) => (
|
||||
|
|
@ -391,9 +410,19 @@ export function HomeworkInput({
|
|||
|
||||
<HomeworkHistory records={history} onOpen={onOpen} onDelete={onDelete} />
|
||||
|
||||
{/* Fixed step bar, floating just above the tab bar. */}
|
||||
<div className="fixed inset-x-0 bottom-[calc(env(safe-area-inset-bottom,0px)+4.6rem)] z-20 px-4">
|
||||
<div className="glass-panel mx-auto flex max-w-3xl items-center gap-2 p-2">
|
||||
{/* The action bar is portalled to <body> — never inside the animated
|
||||
content wrapper, where a transform would break position: fixed. */}
|
||||
<WizardActionBar>
|
||||
{step === 1 ? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
className="touch-target flex-1"
|
||||
onClick={() => router.push("/")}
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
|
|
@ -402,25 +431,24 @@ export function HomeworkInput({
|
|||
setStepDir(-1);
|
||||
setStep(1);
|
||||
}}
|
||||
disabled={step === 1}
|
||||
>
|
||||
{t("common.back")}
|
||||
</Button>
|
||||
{step === 1 ? (
|
||||
<Button
|
||||
size="lg"
|
||||
className="touch-target flex-[1.5]"
|
||||
onClick={() => {
|
||||
setStepDir(1);
|
||||
setStep(2);
|
||||
}}
|
||||
disabled={!canSubmit}
|
||||
>
|
||||
{t("common.next")}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{step === 1 ? (
|
||||
<Button
|
||||
size="lg"
|
||||
className="touch-target flex-[1.5]"
|
||||
onClick={() => {
|
||||
setStepDir(1);
|
||||
setStep(2);
|
||||
}}
|
||||
disabled={!canSubmit}
|
||||
>
|
||||
{t("common.next")}
|
||||
</Button>
|
||||
) : null}
|
||||
</WizardActionBar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue