diff --git a/app/globals.css b/app/globals.css index fa6f070..9f098c0 100644 --- a/app/globals.css +++ b/app/globals.css @@ -103,12 +103,15 @@ --sidebar-bottom: #f6f6f7; --sidebar-edge: 0 1px 0 var(--hairline), 6px 0 16px -14px rgb(13 13 13 / 0.28); /* API-key gate: near-white stage with soft blank glows, tinted input. */ - --gate-base: #f7f7f8; - --gate-glow-a: rgb(64 74 90 / 0.1); - --gate-glow-b: rgb(96 108 124 / 0.07); + --gate-base: #f6f6f8; + --gate-glow-a: rgb(88 106 132 / 0.16); + --gate-glow-b: rgb(126 140 160 / 0.12); + --gate-card: #ffffff; + --gate-card-border: #e7e7e9; + --gate-callout-border: #d9d9dc; --field: #f3f3f4; --field-border: #e2e2e4; - --shadow-gate: 0 28px 70px -36px rgb(13 13 13 / 0.35), 0 1px 2px rgb(13 13 13 / 0.05); + --shadow-gate: 0 28px 70px -36px rgb(13 13 13 / 0.3), 0 1px 2px rgb(13 13 13 / 0.05); --shadow-card: 0 1px 2px rgb(13 13 13 / 0.06), 0 1px 3px rgb(13 13 13 / 0.04); --shadow-card-hover: 0 10px 28px -14px rgb(13 13 13 / 0.22), 0 1px 2px rgb(13 13 13 / 0.06); } @@ -131,12 +134,15 @@ --sidebar-bottom: #151515; --sidebar-edge: 0 1px 0 var(--hairline), 6px 0 16px -14px rgb(0 0 0 / 0.75); /* API-key gate: near-black stage, monochrome glows, raised input. */ - --gate-base: #0d0d0e; - --gate-glow-a: rgb(255 255 255 / 0.075); - --gate-glow-b: rgb(255 255 255 / 0.05); - --field: #383838; - --field-border: rgb(255 255 255 / 0.12); - --shadow-gate: 0 30px 80px -40px rgb(0 0 0 / 0.9), 0 1px 2px rgb(0 0 0 / 0.4); + --gate-base: #0c0c0d; + --gate-glow-a: rgb(255 255 255 / 0.14); + --gate-glow-b: rgb(255 255 255 / 0.09); + --gate-card: #171718; + --gate-card-border: rgb(255 255 255 / 0.08); + --gate-callout-border: rgb(255 255 255 / 0.2); + --field: #242425; + --field-border: rgb(255 255 255 / 0.1); + --shadow-gate: 0 30px 80px -40px rgb(0 0 0 / 0.85), 0 1px 2px rgb(0 0 0 / 0.4); --shadow-card: 0 1px 2px rgb(0 0 0 / 0.25); --shadow-card-hover: 0 10px 28px -16px rgb(0 0 0 / 0.55), 0 1px 2px rgb(0 0 0 / 0.3); } @@ -191,8 +197,8 @@ body { softest "blurred glow" available without extra layers. */ .gate-bg { background: - radial-gradient(64rem 46rem at 112% -12%, var(--gate-glow-a), transparent 68%), - radial-gradient(58rem 42rem at -12% 112%, var(--gate-glow-b), transparent 70%), + radial-gradient(72rem 54rem at 108% -14%, var(--gate-glow-a), transparent 70%), + radial-gradient(64rem 48rem at -14% 112%, var(--gate-glow-b), transparent 72%), var(--gate-base); } diff --git a/components/ApiKeyGate.tsx b/components/ApiKeyGate.tsx index 4c33c40..b07fdd5 100644 --- a/components/ApiKeyGate.tsx +++ b/components/ApiKeyGate.tsx @@ -3,7 +3,18 @@ import { AnimatePresence, motion, useReducedMotion } from "framer-motion"; import { useEffect, useRef, useState, useSyncExternalStore } from "react"; -import { EyeIcon, EyeOffIcon, QuestionIcon, SnowflakeIcon, XIcon } from "@/components/icons"; +import { useTheme } from "@/components/ThemeToggle"; +import { + CheckCircleIcon, + ExternalLinkIcon, + EyeIcon, + EyeOffIcon, + MoonIcon, + QuestionIcon, + SnowflakeIcon, + SunIcon, + XIcon, +} from "@/components/icons"; import { Spinner } from "@/components/ui"; import { verifyOllamaKey } from "@/lib/client-api"; import { saveSettings, settingsStore } from "@/lib/settings"; @@ -40,8 +51,34 @@ export function ApiKeyGate({ children }: { children: React.ReactNode }) { return ; } +/** Ghost icon button used for the two top-right controls. */ +function CornerButton({ + label, + title, + onClick, + children, +}: { + label: string; + title: string; + onClick: () => void; + children: React.ReactNode; +}) { + return ( + + ); +} + function GateScreen() { const reduceMotion = useReducedMotion(); + const { theme, toggle } = useTheme(); const [draft, setDraft] = useState(""); const [reveal, setReveal] = useState(false); const [busy, setBusy] = useState(false); @@ -82,42 +119,50 @@ function GateScreen() { return (
- {/* help -------------------------------------------------------- */} + {/* corner controls: theme + help ---------------------------------- */}
- - - How do I get a key? - + {theme === "dark" ? : } + + +
+ setHelpOpen(true)} + > + + + + How do I get a key? + +
- {/* card -------------------------------------------------------- */} + {/* card ----------------------------------------------------------- */}
@@ -125,7 +170,7 @@ function GateScreen() {

Connect your Ollama key

-

+

One step and you’re ready to freeze your first deck.

@@ -150,7 +195,7 @@ function GateScreen() { disabled={busy} aria-invalid={error?.tone === "invalid" || undefined} aria-describedby={error ? "gate-api-key-error" : undefined} - className="h-12 w-full rounded-[13px] border border-[var(--field-border)] bg-[var(--field)] pl-3.5 pr-12 text-[14px] text-[var(--text)] outline-none transition placeholder:text-[var(--text-subtle)] focus:border-[var(--text-subtle)] disabled:opacity-60" + className="h-12 w-full rounded-[13px] border border-[var(--field-border)] bg-[var(--field)] pl-3.5 pr-12 text-left text-[14px] text-[var(--text)] outline-none transition placeholder:text-[var(--text-subtle)] focus:border-[var(--text-subtle)] disabled:opacity-60" /> +
+ -

- Your key stays in this browser. Freezmo’s server never stores it. +

+ Your key stays in this browser. Our server never stores it.

@@ -230,11 +285,11 @@ 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(--hairline)] bg-[var(--surface)] p-6 pb-safe shadow-[var(--shadow-gate)] sm:rounded-[24px] sm:pb-6" + 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" >
-
+

void }) { type="button" onClick={onClose} aria-label="Close" - className="grid size-8 shrink-0 place-items-center rounded-[10px] text-[var(--text-subtle)] transition hover:bg-[var(--hover)] hover:text-[var(--text)]" + className="grid size-9 shrink-0 place-items-center rounded-[10px] text-[var(--text-subtle)] transition hover:bg-[var(--hover)] hover:text-[var(--text)]" >

-

- You only need the free cloud tier. No payment and no credit card required. -

+
+ +

+ You only need the{" "} + free cloud tier. No + payment and no credit card required. +

+
    Go to{" "} - - ollama.com - {" "} - and sign in, or create a free account. + ollama.com and sign in, or + create a free account. - Open Settings → API keys at{" "} - - ollama.com/settings/keys - - . + Open Settings → API keys at{" "} + ollama.com/settings/keys. - Click “Create new key”, give it - any name, and copy it. + Click{" "} + “Create new key”, + give it any name, and copy it. Paste it into Freezmo and press Continue.
@@ -306,16 +352,30 @@ function HelpDialog({ open, onClose }: { open: boolean; onClose: () => void }) { ); } +function ExternalLink({ href, children }: { href: string; children: React.ReactNode }) { + return ( + + {children} + + + ); +} + function Step({ index, children }: { index: number; children: React.ReactNode }) { return ( -
  • +
  • {index} - {children} + {children}
  • ); } diff --git a/components/icons.tsx b/components/icons.tsx index f0f4a52..19fd7ed 100644 --- a/components/icons.tsx +++ b/components/icons.tsx @@ -275,3 +275,13 @@ export function XIcon({ className }: IconProps) { ); } + +export function ExternalLinkIcon({ className }: IconProps) { + return ( + + + + + + ); +}