Gate polish round two: glows, disabled state, help link, modal

- Disabled Continue is now the primary style at 45% opacity with
  cursor: not-allowed; full opacity while validating and once text is
  entered.
- Move "How do I get a key?" below the Continue button (small ? icon,
  muted, underline on hover) and tighten input+button spacing.
- Shorten the subtitle to "One step to freeze your first deck." and
  balance it.
- Replace the faint gradient with two real blurred blobs (top-right and
  bottom-left, 120px blur, pointer-events: none, behind the card).
- Dark: page #0f0f0f, card #1a1a1a, 8% white border.
- Phone: 24px heading, 15px subtitle, wider card padding, 40px corner
  buttons with safe-area-aware offsets, robust 100dvh centering.
- Modal: 480px wide with text-pretty steps, stronger tinted free-tier
  callout, handle removed from the phone sheet.
- Tests: gate suite now 49 checks incl. disabled opacity, glow pseudo,
  card separation and help-link placement; new state screenshots for
  invalid/offline/validating in both themes.
This commit is contained in:
Martin Dvořák 2026-10-07 06:09:13 +00:00
commit 969f7a82a0
2 changed files with 62 additions and 39 deletions

View file

@ -69,7 +69,7 @@ function CornerButton({
onClick={onClick}
aria-label={label}
title={title}
className="grid size-9 place-items-center rounded-full border border-[var(--field-border)] bg-[var(--gate-card)] text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)]"
className="grid size-10 place-items-center rounded-full border border-[var(--field-border)] bg-[var(--gate-card)] text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)]"
>
{children}
</button>
@ -123,8 +123,8 @@ function GateScreen() {
<div
className="fixed z-20 flex items-center gap-2"
style={{
top: "max(env(safe-area-inset-top), 1rem)",
right: "max(env(safe-area-inset-right), 1rem)",
top: "max(env(safe-area-inset-top), 1.25rem)",
right: "max(env(safe-area-inset-right), 1.25rem)",
}}
>
<CornerButton
@ -153,12 +153,12 @@ function GateScreen() {
</div>
{/* card ----------------------------------------------------------- */}
<div className="relative z-10 flex min-h-dvh items-center justify-center px-5 py-10">
<div className="relative z-10 flex min-h-[100dvh] items-center justify-center px-5 py-10">
<motion.div
initial={reduceMotion ? false : { opacity: 0, y: 14 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.45, ease: [0.22, 1, 0.36, 1] }}
className="w-full max-w-[420px] rounded-[24px] border border-[var(--gate-card-border)] bg-[var(--gate-card)] p-6 text-center shadow-[var(--shadow-gate)] sm:p-8"
className="my-auto w-full max-w-[420px] rounded-[24px] border border-[var(--gate-card-border)] bg-[var(--gate-card)] p-6 px-7 text-center shadow-[var(--shadow-gate)] sm:p-8"
>
<span
aria-hidden
@ -167,11 +167,11 @@ function GateScreen() {
<SnowflakeIcon className="size-7" />
</span>
<h1 className="mt-6 text-[26px] font-medium leading-tight tracking-[-0.01em] text-[var(--text)] sm:text-[28px]">
<h1 className="mt-6 text-[24px] font-medium leading-tight tracking-[-0.01em] text-[var(--text)] sm:text-[28px]">
Connect your Ollama key
</h1>
<p className="mx-auto mt-1.5 max-w-[300px] text-[13.5px] leading-relaxed text-[var(--text-subtle)]">
One step and you&rsquo;re ready to freeze your first deck.
<p className="mx-auto mt-1.5 max-w-[300px] text-balance text-[15px] leading-relaxed text-[var(--text-subtle)]">
One step to freeze your first deck.
</p>
<form className="mt-6" onSubmit={submit} noValidate>
@ -223,20 +223,13 @@ function GateScreen() {
</p>
) : null}
<div className="mt-2.5 text-center">
<button
type="button"
onClick={() => setHelpOpen(true)}
className="text-[12.5px] font-medium text-[var(--text-muted)] underline-offset-2 transition hover:text-[var(--text)] hover:underline"
>
How do I get a key?
</button>
</div>
<button
type="submit"
disabled={!draft.trim() || busy}
className="btn-primary mt-3.5 h-12 w-full rounded-[13px] text-[14.5px] font-medium disabled:cursor-not-allowed disabled:bg-[rgb(13_13_13_/_0.45)] dark:disabled:bg-[rgb(255_255_255_/_0.45)]"
className={cn(
"btn-primary mt-3 h-12 w-full rounded-[13px] text-[14.5px] font-medium",
!draft.trim() && "cursor-not-allowed opacity-45",
)}
>
{busy ? (
<>
@ -248,6 +241,17 @@ function GateScreen() {
</button>
</form>
<div className="mt-4 text-center">
<button
type="button"
onClick={() => setHelpOpen(true)}
className="inline-flex items-center gap-1.5 text-[12.5px] font-medium text-[var(--text-muted)] transition hover:text-[var(--text)] hover:underline"
>
<QuestionIcon className="size-3.5" />
How do I get a key?
</button>
</div>
<p className="mx-auto mt-4 max-w-[320px] text-balance text-[12px] leading-relaxed text-[var(--text-subtle)]">
Your key stays in this browser. Our server never stores it.
</p>
@ -285,10 +289,8 @@ function HelpDialog({ open, onClose }: { open: boolean; onClose: () => void }) {
exit={reduceMotion ? { opacity: 0 } : { y: 24, opacity: 0 }}
transition={{ type: "spring", stiffness: 340, damping: 32 }}
onClick={(event) => event.stopPropagation()}
className="thin-scroll max-h-[90dvh] w-full max-w-md overflow-y-auto rounded-t-[24px] border border-[var(--gate-card-border)] bg-[var(--gate-card)] p-6 pb-safe shadow-[var(--shadow-gate)] sm:rounded-[24px] sm:pb-6"
className="thin-scroll max-h-[90dvh] w-full max-w-[480px] overflow-y-auto rounded-t-[24px] border border-[var(--gate-card-border)] bg-[var(--gate-card)] p-6 pb-safe shadow-[var(--shadow-gate)] sm:rounded-[24px] sm:pb-6"
>
<div className="sheet-grabber sm:hidden" aria-hidden />
<div className="flex items-center justify-between gap-4">
<h2
id="gate-help-title"
@ -306,7 +308,7 @@ function HelpDialog({ open, onClose }: { open: boolean; onClose: () => void }) {
</button>
</div>
<div className="mt-4 flex items-start gap-2.5 rounded-xl border border-[var(--gate-callout-border)] bg-[var(--surface-sunken)] px-3.5 py-3">
<div className="mt-4 flex items-start gap-2.5 rounded-xl border border-[var(--gate-callout-border)] bg-[var(--gate-callout-bg)] px-3.5 py-3">
<CheckCircleIcon className="mt-px size-[18px] shrink-0 text-[var(--text-muted)]" />
<p className="text-[12.5px] leading-relaxed text-[var(--text-muted)]">
You only need the{" "}
@ -375,7 +377,7 @@ function Step({ index, children }: { index: number; children: React.ReactNode })
>
{index}
</span>
<span className="text-[13px] leading-6 text-[var(--text-muted)]">{children}</span>
<span className="text-pretty text-[13px] leading-6 text-[var(--text-muted)]">{children}</span>
</li>
);
}