Navy theme from the app icon, frosted glass, and the new mark in-app
The icon's artwork is now the source of truth for the palette: - frost-* is a navy-tinted neutral scale and accent-* is the icon's snowflake blue; light page #f4f7fd / sidebar #e9eff9 / border #dce4f1, dark page #0e1424 / sidebar #0a0f1c / surfaces #171f33. - Surfaces are frosted glass: .card, .glass-panel, .list-group and .sidebar-surface are translucent with backdrop-blur/saturate, and the page wrapper no longer paints an opaque --page, so the fixed navy/blue backdrop reads through the sidebar, cards, chrome and sheets. - The primary CTA is the icon's blue (#1d61ab in light, #59a8f7 in dark with ink text). - The gate stage, fields and glows are navy/blue; viewport theme-color and the manifest background/theme colour are #0e1424. - The old monochrome snowflake tile is gone from the brand: sidebar, header, key gate and empty state now show the real app icon (the install sheet already did). Decorative snowflakes stay. - --surface-strong is finally defined (it was referenced but never declared, so those inputs had no fill). - Icon URLs carry ?v=2 so browser favicon caches refetch. Updated the contrast audit (21 pairs) and the deck-chrome token assertions to the new theme. Verified: contrast 21/21, deck chrome 15/15, key gate 49/49, install sheet 64/64, final ux 12/12, retry 14/14, zoom 6/6, security 8/8, pwa 16/16, deck fixes 15/15.
This commit is contained in:
parent
2b8341c189
commit
979f3ddc39
10 changed files with 181 additions and 158 deletions
39
README.md
39
README.md
|
|
@ -112,8 +112,10 @@ no database, no data leaving your machine except the requests you send to Ollama
|
||||||
list with per-folder counts, Settings/Dark mode pinned to the bottom). It **collapses** to a 68px
|
list with per-folder counts, Settings/Dark mode pinned to the bottom). It **collapses** to a 68px
|
||||||
icon rail (remembered in `localStorage`) and becomes a slide-in **drawer** behind a hamburger on
|
icon rail (remembered in `localStorage`) and becomes a slide-in **drawer** behind a hamburger on
|
||||||
phones; mobile also keeps the frosted header + iOS tab bar
|
phones; mobile also keeps the frosted header + iOS tab bar
|
||||||
- **One neutral palette, both themes**: light page `#ffffff` / sidebar `#f9f9f9` / border `#e5e5e5`;
|
- **One navy palette pulled from the app icon, both themes**: light page `#f4f7fd` / sidebar
|
||||||
dark page `#212121` / sidebar `#171717` / surfaces `#2f2f2f` / border `rgba(255,255,255,.1)`.
|
`#e9eff9` / border `#dce4f1`; dark page `#0e1424` / sidebar `#0a0f1c` / surfaces `#171f33` /
|
||||||
|
border `rgba(255,255,255,.12)`. Surfaces are frosted glass (translucent + `backdrop-blur`) over
|
||||||
|
the icon's navy backdrop.
|
||||||
Text tiers are documented AA ratios (see below), and the cards fill the row with
|
Text tiers are documented AA ratios (see below), and the cards fill the row with
|
||||||
`repeat(auto-fill, minmax(280px, 1fr))` — no stranded cards, no dead space
|
`repeat(auto-fill, minmax(280px, 1fr))` — no stranded cards, no dead space
|
||||||
- Deck cards: title → muted metadata → neutral tags (difficulty carries the only colour:
|
- Deck cards: title → muted metadata → neutral tags (difficulty carries the only colour:
|
||||||
|
|
@ -725,9 +727,10 @@ Things I chose while building, with the reasoning:
|
||||||
have no `folderId`), deleting a folder re-homes its decks instead of deleting them, and folder
|
have no `folderId`), deleting a folder re-homes its decks instead of deleting them, and folder
|
||||||
names travel in exports so an imported deck lands where it came from. 13 browser checks cover
|
names travel in exports so an imported deck lands where it came from. 13 browser checks cover
|
||||||
create → freeze → move → rename → delete → reload → import.
|
create → freeze → move → rename → delete → reload → import.
|
||||||
18. **The visual language is glassmorphism, with restraint.** One accent colour drives the buttons
|
18. **The visual language is glassmorphism, with restraint.** The icon's blue is the one accent
|
||||||
and highlights; the backdrop is a soft aurora wash; surfaces are frosted and translucent. Colour
|
colour driving buttons and highlights; the backdrop is a navy wash; surfaces — sidebar, cards,
|
||||||
appears in activity tints and semantic feedback only — pretty, but still readable and quiet.
|
chrome, sheets — are frosted and translucent. Colour appears in activity tints and semantic
|
||||||
|
feedback only — pretty, but still readable and quiet.
|
||||||
19. **Custom CSS lives in `@layer components`, and glass uses Tailwind's backdrop utilities.**
|
19. **Custom CSS lives in `@layer components`, and glass uses Tailwind's backdrop utilities.**
|
||||||
Two build-level traps, both found by looking at real screenshots: unlayered rules beat Tailwind
|
Two build-level traps, both found by looking at real screenshots: unlayered rules beat Tailwind
|
||||||
utilities (which silently broke `fixed` on the feed), and a hand-written `backdrop-filter` gets
|
utilities (which silently broke `fixed` on the feed), and a hand-written `backdrop-filter` gets
|
||||||
|
|
@ -738,19 +741,19 @@ Things I chose while building, with the reasoning:
|
||||||
a fixture deck into `localStorage` rather than relying on app code.
|
a fixture deck into `localStorage` rather than relying on app code.
|
||||||
21. **Dark mode is token-driven, and contrast is audited.** Text colours come from
|
21. **Dark mode is token-driven, and contrast is audited.** Text colours come from
|
||||||
`--text` / `--text-muted` / `--text-subtle` (never hardcoded), and a `contrast.mjs` audit checks
|
`--text` / `--text-muted` / `--text-subtle` (never hardcoded), and a `contrast.mjs` audit checks
|
||||||
every pairing at WCAG AA — currently **20/20 pairs pass** (e.g. dark secondary `#b4b4b4` on
|
every pairing at WCAG AA — currently **21/21 pairs pass** (e.g. dark secondary `#b7c3dc` on
|
||||||
`#212121` = 7.8:1). Difficulty colours were darkened until they cleared 4.5:1 on their pills.
|
`#0e1424` = 10.4:1, white on the light blue CTA `#1d61ab` = 6.3:1). Difficulty colours were
|
||||||
|
darkened until they cleared 4.5:1 on their pills.
|
||||||
This was a real bug: an earlier edit silently failed, so `.dark` never overrode `--text`, leaving
|
This was a real bug: an earlier edit silently failed, so `.dark` never overrode `--text`, leaving
|
||||||
near-black text on a dark page.
|
near-black text on a dark page.
|
||||||
22. **The dashboard follows a neutral, ChatGPT-style system.** Near-white/near-black surfaces,
|
22. **The dashboard follows the icon's navy system.** Frosted navy/white surfaces, hairline borders,
|
||||||
hairline borders, pill CTAs, 18px radii and restrained shadows; the frost survives in the chrome
|
pill CTAs, 18px radii and restrained shadows; the sidebar, cards and chrome are translucent
|
||||||
(header, tab bar, sheets) rather than in every card, which keeps text crisp and the layout
|
glass over the backdrop. Deck cards got real hierarchy: title → muted metadata → tags → summary
|
||||||
premium. Deck cards got real hierarchy: title → muted metadata → tags → summary → prominent
|
→ secondary Study CTA, with secondary actions behind a quiet menu.
|
||||||
Study CTA, with secondary actions behind a quiet menu.
|
23. **One fixed backdrop layer instead of per-page blobs.** The decorative wash used to be
|
||||||
23. **One fixed backdrop layer instead of per-page blobs.** The decorative aurora used to be
|
|
||||||
rendered inside the content column, so on wide screens it was clipped into a visible hard edge.
|
rendered inside the content column, so on wide screens it was clipped into a visible hard edge.
|
||||||
It now lives in a single `position: fixed` layer (`.app-backdrop`, `z-index: -10`) with very low
|
It now lives in a single `position: fixed` layer (`.app-backdrop`, `z-index: -10`) in the
|
||||||
opacities, and `body`'s colour matches its bottom so mobile overscroll shows no seam.
|
icon's navy and blue, and `body`'s colour matches its bottom so mobile overscroll shows no seam.
|
||||||
24. **The UI was deliberately made quieter.** One accent colour (`accent-*`), a desaturated
|
24. **The UI was deliberately made quieter.** One accent colour (`accent-*`), a desaturated
|
||||||
`frost-*` scale, hairline borders and flat surfaces replaced the saturated gradients and the
|
`frost-*` scale, hairline borders and flat surfaces replaced the saturated gradients and the
|
||||||
seven-colour activity badges. Semantic colours (green/amber/red) survive only where they carry
|
seven-colour activity badges. Semantic colours (green/amber/red) survive only where they carry
|
||||||
|
|
@ -784,6 +787,12 @@ Things I chose while building, with the reasoning:
|
||||||
`user-scalable=no` for an app-like feel, and a mobile-only rule keeps every input and textarea at
|
`user-scalable=no` for an app-like feel, and a mobile-only rule keeps every input and textarea at
|
||||||
16px so iOS Safari's focus auto-zoom never fires (iOS ignores `user-scalable=no` for that).
|
16px so iOS Safari's focus auto-zoom never fires (iOS ignores `user-scalable=no` for that).
|
||||||
Desktop keeps the designed 12–14px field sizes.
|
Desktop keeps the designed 12–14px field sizes.
|
||||||
|
31. **The app icon drives the palette.** The navy rounded-square artwork in
|
||||||
|
`assets/app-icon-source.jpeg` is the source of truth: `frost-*` is navy-tinted, the accent is
|
||||||
|
the snowflake blue, the backdrop is the same navy wash, the primary CTA is the icon blue (deep
|
||||||
|
in light mode, bright in dark), and every icon/splash/favicon is rendered from it by
|
||||||
|
`scripts/make-icons.mjs`. The sidebar, header, key gate, empty state and install sheet all show
|
||||||
|
the real artwork instead of the old monochrome snowflake tile.
|
||||||
14. **`gpt-oss:120b` does all content work — but it cannot read images** (`this model does not
|
14. **`gpt-oss:120b` does all content work — but it cannot read images** (`this model does not
|
||||||
support image input`), so `VISION_MODEL` has to stay a multimodal model (`gemma4:31b-cloud`).
|
support image input`), so `VISION_MODEL` has to stay a multimodal model (`gemma4:31b-cloud`).
|
||||||
Text-only inputs — pasted text, PDFs with a text layer, DOCX/PPTX/ODT — never touch the vision
|
Text-only inputs — pasted text, PDFs with a text layer, DOCX/PPTX/ODT — never touch the vision
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export const metadata = {
|
||||||
export default function CreatePage() {
|
export default function CreatePage() {
|
||||||
return (
|
return (
|
||||||
<ApiKeyGate>
|
<ApiKeyGate>
|
||||||
<div className="flex min-h-dvh bg-[var(--page)]">
|
<div className="flex min-h-dvh">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<SidebarOffset />
|
<SidebarOffset />
|
||||||
<MobileSidebar />
|
<MobileSidebar />
|
||||||
|
|
|
||||||
216
app/globals.css
216
app/globals.css
|
|
@ -4,30 +4,30 @@
|
||||||
@custom-variant dark (&:where(.dark, .dark *));
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
/* Neutral scale (a whisper of cool) — the ChatGPT-ish workhorse. ------- */
|
/* Navy-tinted neutral scale — sampled from the app icon. ---------------- */
|
||||||
--color-frost-50: #f8f8f9;
|
--color-frost-50: #f5f8fd;
|
||||||
--color-frost-100: #f1f1f3;
|
--color-frost-100: #eaf0fa;
|
||||||
--color-frost-200: #e4e4e7;
|
--color-frost-200: #d9e3f3;
|
||||||
--color-frost-300: #cfcfd4;
|
--color-frost-300: #c2cfe4;
|
||||||
--color-frost-400: #a3a3aa;
|
--color-frost-400: #92a3c0;
|
||||||
--color-frost-500: #7a7a82;
|
--color-frost-500: #6a7c9b;
|
||||||
--color-frost-600: #5d5d64;
|
--color-frost-600: #4e5f7d;
|
||||||
--color-frost-700: #47474d;
|
--color-frost-700: #3c4b66;
|
||||||
--color-frost-800: #34343a;
|
--color-frost-800: #2b3752;
|
||||||
--color-frost-900: #26262a;
|
--color-frost-900: #1d2740;
|
||||||
--color-frost-950: #171719;
|
--color-frost-950: #0e1526;
|
||||||
|
|
||||||
/* Accent: one confident blue for buttons and highlights. -------------- */
|
/* Accent: the icon's snowflake blue. ------------------------------------ */
|
||||||
--color-accent-50: #eef7fd;
|
--color-accent-50: #eff7ff;
|
||||||
--color-accent-100: #d9edfa;
|
--color-accent-100: #dbecff;
|
||||||
--color-accent-200: #b6dcf4;
|
--color-accent-200: #bcdcff;
|
||||||
--color-accent-300: #83c3ea;
|
--color-accent-300: #8fc6ff;
|
||||||
--color-accent-400: #4aa4dc;
|
--color-accent-400: #59a8f7;
|
||||||
--color-accent-500: #2387c4;
|
--color-accent-500: #3f95ea;
|
||||||
--color-accent-600: #186ca6;
|
--color-accent-600: #2478d2;
|
||||||
--color-accent-700: #175687;
|
--color-accent-700: #1d61ab;
|
||||||
--color-accent-800: #18496f;
|
--color-accent-800: #1c518b;
|
||||||
--color-accent-900: #183e5d;
|
--color-accent-900: #1b4473;
|
||||||
|
|
||||||
/* Aurora hues used only for the frosted backdrop wash. ----------------- */
|
/* Aurora hues used only for the frosted backdrop wash. ----------------- */
|
||||||
--color-aurora-mint: #9fe3d0;
|
--color-aurora-mint: #9fe3d0;
|
||||||
|
|
@ -81,74 +81,76 @@
|
||||||
--------------------------------------------------------------------------- */
|
--------------------------------------------------------------------------- */
|
||||||
:root {
|
:root {
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
--page: #ffffff;
|
--page: #f4f7fd;
|
||||||
--sidebar: #f9f9f9;
|
--sidebar: #e9eff9;
|
||||||
--surface: #ffffff;
|
--surface: #ffffff;
|
||||||
--surface-raised: #ffffff;
|
--surface-raised: #ffffff;
|
||||||
--card-hover-bg: #fafafa;
|
--surface-strong: #edf2fa;
|
||||||
--surface-sunken: rgb(13 13 13 / 0.05);
|
--card-hover-bg: #f7faff;
|
||||||
--hairline: #e5e5e5;
|
--surface-sunken: rgb(14 21 40 / 0.05);
|
||||||
--card-border: #e2e2e2;
|
--hairline: #dce4f1;
|
||||||
--sidebar-divider: #e2e2e2;
|
--card-border: #d7e0ef;
|
||||||
--hairline-soft: rgb(13 13 13 / 0.07);
|
--sidebar-divider: #d7e0ef;
|
||||||
--glass-tint: rgb(255 255 255 / 0.6);
|
--hairline-soft: rgb(14 21 40 / 0.07);
|
||||||
--text: #0d0d0d; /* 17.6:1 on #ffffff */
|
--glass-tint: rgb(255 255 255 / 0.55);
|
||||||
--text-muted: #5d5d5d; /* 7.4:1 */
|
--text: #0e1526; /* 16.9:1 on #f4f7fd */
|
||||||
--text-subtle: #6b6b6b; /* 5.4:1 */
|
--text-muted: #4d5c78; /* 6.3:1 */
|
||||||
--hover: rgb(13 13 13 / 0.05);
|
--text-subtle: #5a6a86; /* 5.1:1 */
|
||||||
--sidebar-top: #fbfbfc;
|
--hover: rgb(14 21 40 / 0.05);
|
||||||
--sidebar-bottom: #f6f6f7;
|
--sidebar-top: #eef3fb;
|
||||||
--sidebar-edge: 0 1px 0 var(--hairline), 6px 0 18px -12px rgb(13 13 13 / 0.38);
|
--sidebar-bottom: #e5edf9;
|
||||||
/* API-key gate: near-white stage with soft blank glows, tinted input. */
|
--sidebar-edge: 0 1px 0 var(--sidebar-divider), 6px 0 18px -12px rgb(14 21 40 / 0.35);
|
||||||
--gate-base: #f6f6f8;
|
/* API-key gate: cool near-white stage with soft blue glows, tinted input. */
|
||||||
--gate-glow-a: rgb(84 102 130 / 0.22);
|
--gate-base: #eef3fb;
|
||||||
--gate-glow-b: rgb(124 138 160 / 0.18);
|
--gate-glow-a: rgb(90 150 240 / 0.25);
|
||||||
|
--gate-glow-b: rgb(140 150 240 / 0.2);
|
||||||
--gate-card: #ffffff;
|
--gate-card: #ffffff;
|
||||||
--gate-card-border: #e7e7e9;
|
--gate-card-border: #dde5f2;
|
||||||
--gate-callout-bg: #f2f2f5;
|
--gate-callout-bg: #eef3fa;
|
||||||
--gate-callout-border: #cfcfd6;
|
--gate-callout-border: #c9d5e8;
|
||||||
--field: #f3f3f4;
|
--field: #eef2f9;
|
||||||
--field-border: #e2e2e4;
|
--field-border: #dbe3f0;
|
||||||
--shadow-gate: 0 28px 70px -36px rgb(13 13 13 / 0.3), 0 1px 2px rgb(13 13 13 / 0.05);
|
--shadow-gate: 0 28px 70px -36px rgb(14 21 40 / 0.3), 0 1px 2px rgb(14 21 40 / 0.05);
|
||||||
/* Install sheet: light variant (dark is the default, see .dark). */
|
/* Install sheet: light variant (dark is the default, see .dark). */
|
||||||
--install-sheet: #ffffff;
|
--install-sheet: #ffffff;
|
||||||
--install-card: #f7f7f8;
|
--install-card: #f7f7f8;
|
||||||
--install-chip: #f2f2f2;
|
--install-chip: #f2f2f2;
|
||||||
--install-border: #e0e0e0;
|
--install-border: #e0e0e0;
|
||||||
--shadow-card: 0 1px 2px rgb(13 13 13 / 0.06), 0 1px 3px rgb(13 13 13 / 0.04);
|
--shadow-card: 0 1px 2px rgb(14 21 40 / 0.06), 0 1px 3px rgb(14 21 40 / 0.04);
|
||||||
--card-shadow: 0 1px 2px rgb(13 13 13 / 0.05), 0 5px 14px -6px rgb(13 13 13 / 0.12);
|
--card-shadow: 0 1px 2px rgb(14 21 40 / 0.05), 0 5px 14px -6px rgb(14 21 40 / 0.12);
|
||||||
--shadow-card-hover: 0 12px 30px -12px rgb(13 13 13 / 0.2), 0 2px 6px rgb(13 13 13 / 0.07);
|
--shadow-card-hover: 0 12px 30px -12px rgb(14 21 40 / 0.2), 0 2px 6px rgb(14 21 40 / 0.07);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
--page: #212121;
|
--page: #0e1424;
|
||||||
--sidebar: #171717;
|
--sidebar: #0a0f1c;
|
||||||
--surface: #2f2f2f;
|
--surface: #171f33;
|
||||||
--surface-raised: #383838;
|
--surface-raised: #1e2740;
|
||||||
--card-hover-bg: #383838;
|
--surface-strong: #202a42;
|
||||||
--surface-sunken: rgb(255 255 255 / 0.08);
|
--card-hover-bg: #1e2740;
|
||||||
--hairline: rgb(255 255 255 / 0.14);
|
--surface-sunken: rgb(255 255 255 / 0.07);
|
||||||
--card-border: var(--hairline);
|
--hairline: rgb(255 255 255 / 0.12);
|
||||||
--sidebar-divider: var(--hairline);
|
--card-border: rgb(255 255 255 / 0.12);
|
||||||
|
--sidebar-divider: rgb(255 255 255 / 0.1);
|
||||||
--hairline-soft: rgb(255 255 255 / 0.08);
|
--hairline-soft: rgb(255 255 255 / 0.08);
|
||||||
--glass-tint: rgb(255 255 255 / 0.08);
|
--glass-tint: rgb(255 255 255 / 0.06);
|
||||||
--text: #ececec; /* 12.9:1 on #212121 */
|
--text: #e9eefb; /* 15.4:1 on #0e1424 */
|
||||||
--text-muted: #b4b4b4; /* 7.6:1 */
|
--text-muted: #b7c3dc; /* 10.4:1 */
|
||||||
--text-subtle: #a0a0a0; /* 6.0:1 */
|
--text-subtle: #94a4c2; /* 7.3:1 */
|
||||||
--hover: rgb(255 255 255 / 0.07);
|
--hover: rgb(255 255 255 / 0.07);
|
||||||
--sidebar-top: #181818;
|
--sidebar-top: #0b1120;
|
||||||
--sidebar-bottom: #141414;
|
--sidebar-bottom: #080d18;
|
||||||
--sidebar-edge: 0 1px 0 var(--hairline), 6px 0 16px -14px rgb(0 0 0 / 0.75);
|
--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. */
|
/* API-key gate: navy stage with the icon's blue glows, raised input. */
|
||||||
--gate-base: #0f0f0f;
|
--gate-base: #0a0f1c;
|
||||||
--gate-glow-a: rgb(255 255 255 / 0.17);
|
--gate-glow-a: rgb(70 140 235 / 0.2);
|
||||||
--gate-glow-b: rgb(255 255 255 / 0.12);
|
--gate-glow-b: rgb(120 120 235 / 0.14);
|
||||||
--gate-card: #1a1a1a;
|
--gate-card: #131a2c;
|
||||||
--gate-card-border: rgb(255 255 255 / 0.08);
|
--gate-card-border: rgb(255 255 255 / 0.1);
|
||||||
--gate-callout-bg: rgb(255 255 255 / 0.07);
|
--gate-callout-bg: rgb(255 255 255 / 0.06);
|
||||||
--gate-callout-border: rgb(255 255 255 / 0.22);
|
--gate-callout-border: rgb(255 255 255 / 0.2);
|
||||||
--field: #242425;
|
--field: #1b2438;
|
||||||
--field-border: rgb(255 255 255 / 0.1);
|
--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-gate: 0 30px 80px -40px rgb(0 0 0 / 0.85), 0 1px 2px rgb(0 0 0 / 0.4);
|
||||||
/* Install sheet: layered dark surfaces (default theme for the sheet). */
|
/* Install sheet: layered dark surfaces (default theme for the sheet). */
|
||||||
|
|
@ -161,22 +163,22 @@
|
||||||
--shadow-card-hover: 0 10px 28px -16px rgb(0 0 0 / 0.55), 0 1px 2px rgb(0 0 0 / 0.3);
|
--shadow-card-hover: 0 10px 28px -16px rgb(0 0 0 / 0.55), 0 1px 2px rgb(0 0 0 / 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Backdrop: a whisper of cool over a neutral base (never a coloured surface). */
|
/* Backdrop: the icon's navy base and blue wash, seen through frosted glass. */
|
||||||
:root {
|
:root {
|
||||||
--bd-sky: rgb(150 200 240 / 0.07);
|
--bd-sky: rgb(110 175 245 / 0.2);
|
||||||
--bd-lilac: rgb(188 178 236 / 0.05);
|
--bd-lilac: rgb(145 145 240 / 0.14);
|
||||||
--bd-mint: rgb(150 220 202 / 0.05);
|
--bd-mint: rgb(110 210 230 / 0.12);
|
||||||
--bd-top: #fdfdfd;
|
--bd-top: #f8fafe;
|
||||||
--bd-mid: #fafafa;
|
--bd-mid: #f2f6fd;
|
||||||
--bd-bottom: #f7f7f7;
|
--bd-bottom: #ecf1fb;
|
||||||
}
|
}
|
||||||
.dark {
|
.dark {
|
||||||
--bd-sky: rgb(74 164 220 / 0.05);
|
--bd-sky: rgb(60 130 230 / 0.14);
|
||||||
--bd-lilac: rgb(160 140 235 / 0.04);
|
--bd-lilac: rgb(110 115 235 / 0.1);
|
||||||
--bd-mint: rgb(88 201 173 / 0.04);
|
--bd-mint: rgb(70 180 215 / 0.08);
|
||||||
--bd-top: #232323;
|
--bd-top: #111a2e;
|
||||||
--bd-mid: #212121;
|
--bd-mid: #0e1424;
|
||||||
--bd-bottom: #1e1e1e;
|
--bd-bottom: #0a101d;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
|
|
@ -280,14 +282,14 @@ body {
|
||||||
}
|
}
|
||||||
.dark .app-backdrop::before,
|
.dark .app-backdrop::before,
|
||||||
.dark .app-backdrop::after {
|
.dark .app-backdrop::after {
|
||||||
opacity: 0.16;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
/* Glass primitives ------------------------------------------------------ */
|
/* Glass primitives ------------------------------------------------------ */
|
||||||
.glass-panel {
|
.glass-panel {
|
||||||
@apply backdrop-blur-xl;
|
@apply backdrop-blur-xl backdrop-saturate-150;
|
||||||
background: var(--surface);
|
background: color-mix(in oklab, var(--surface) 82%, transparent);
|
||||||
border: 1px solid var(--hairline);
|
border: 1px solid var(--hairline);
|
||||||
box-shadow: var(--shadow-card);
|
box-shadow: var(--shadow-card);
|
||||||
}
|
}
|
||||||
|
|
@ -323,7 +325,8 @@ body {
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
border-radius: 1.125rem; /* 18px */
|
border-radius: 1.125rem; /* 18px */
|
||||||
background: var(--surface);
|
@apply backdrop-blur-xl backdrop-saturate-150;
|
||||||
|
background: color-mix(in oklab, var(--surface) 78%, transparent);
|
||||||
border: 1px solid var(--card-border);
|
border: 1px solid var(--card-border);
|
||||||
box-shadow: var(--card-shadow);
|
box-shadow: var(--card-shadow);
|
||||||
}
|
}
|
||||||
|
|
@ -351,7 +354,7 @@ body {
|
||||||
.list-group {
|
.list-group {
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
@apply backdrop-blur-lg backdrop-saturate-150;
|
@apply backdrop-blur-lg backdrop-saturate-150;
|
||||||
background: var(--surface);
|
background: color-mix(in oklab, var(--surface) 82%, transparent);
|
||||||
border: 1px solid var(--hairline);
|
border: 1px solid var(--hairline);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
@ -389,7 +392,7 @@ body {
|
||||||
}
|
}
|
||||||
.dark .segmented > button[aria-pressed="true"],
|
.dark .segmented > button[aria-pressed="true"],
|
||||||
.dark .segmented > button[aria-checked="true"] {
|
.dark .segmented > button[aria-checked="true"] {
|
||||||
background: #383838;
|
background: var(--surface-raised);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -420,9 +423,14 @@ body {
|
||||||
.dark .tint-peach { background: linear-gradient(140deg, rgb(230 160 118 / 0.26), rgb(230 160 118 / 0.1)); color: #fadfc9 }
|
.dark .tint-peach { background: linear-gradient(140deg, rgb(230 160 118 / 0.26), rgb(230 160 118 / 0.1)); color: #fadfc9 }
|
||||||
.dark .tint-rose { background: linear-gradient(140deg, rgb(230 130 150 / 0.26), rgb(230 130 150 / 0.1)); color: #fbd5dd }
|
.dark .tint-rose { background: linear-gradient(140deg, rgb(230 130 150 / 0.26), rgb(230 130 150 / 0.1)); color: #fbd5dd }
|
||||||
|
|
||||||
/* Sidebar surface: a touch darker than the page, with a real edge. */
|
/* Sidebar surface: frosted navy glass with a real edge. */
|
||||||
.sidebar-surface {
|
.sidebar-surface {
|
||||||
background: linear-gradient(180deg, var(--sidebar-top), var(--sidebar-bottom));
|
@apply backdrop-blur-xl backdrop-saturate-150;
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
color-mix(in oklab, var(--sidebar-top) 84%, transparent),
|
||||||
|
color-mix(in oklab, var(--sidebar-bottom) 84%, transparent)
|
||||||
|
);
|
||||||
border-right: 1px solid var(--sidebar-divider);
|
border-right: 1px solid var(--sidebar-divider);
|
||||||
box-shadow: var(--sidebar-edge);
|
box-shadow: var(--sidebar-edge);
|
||||||
}
|
}
|
||||||
|
|
@ -450,27 +458,27 @@ body {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Shared CTA language: near-black pill in light mode, white pill in dark. */
|
/* Shared CTA language: the icon's blue, with a deeper fill in light mode. */
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: #0d0d0d;
|
background: #1d61ab;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
box-shadow: 0 1px 2px rgb(13 13 13 / 0.18);
|
box-shadow: 0 1px 2px rgb(14 21 40 / 0.22);
|
||||||
transition: background-color 160ms ease, transform 160ms ease, box-shadow 160ms ease;
|
transition: background-color 160ms ease, transform 160ms ease, box-shadow 160ms ease;
|
||||||
}
|
}
|
||||||
.btn-primary:hover { background: #2f2f2f; }
|
.btn-primary:hover { background: #2478d2; }
|
||||||
.btn-primary:active { transform: scale(0.985); }
|
.btn-primary:active { transform: scale(0.985); }
|
||||||
.dark .btn-primary {
|
.dark .btn-primary {
|
||||||
background: #ffffff;
|
background: #59a8f7;
|
||||||
color: #0d0d0d;
|
color: #081020;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
.dark .btn-primary:hover { background: #e5e5e5; }
|
.dark .btn-primary:hover { background: #7bbaff; }
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,15 @@ export const metadata: Metadata = {
|
||||||
},
|
},
|
||||||
icons: {
|
icons: {
|
||||||
icon: [
|
icon: [
|
||||||
{ url: "/icons/favicon-32.png", sizes: "32x32", type: "image/png" },
|
// ?v=2 busts browser favicon caches after the icon redesign.
|
||||||
{ url: "/icons/icon-192.png", sizes: "192x192", type: "image/png" },
|
{ url: "/icons/favicon-32.png?v=2", sizes: "32x32", type: "image/png" },
|
||||||
{ url: "/icons/icon-512.png", sizes: "512x512", type: "image/png" },
|
{ url: "/icons/icon-192.png?v=2", sizes: "192x192", type: "image/png" },
|
||||||
|
{ url: "/icons/icon-512.png?v=2", sizes: "512x512", type: "image/png" },
|
||||||
],
|
],
|
||||||
apple: [
|
apple: [
|
||||||
{ url: "/icons/apple-touch-icon.png", sizes: "180x180" },
|
{ url: "/icons/apple-touch-icon.png?v=2", sizes: "180x180" },
|
||||||
{ url: "/icons/apple-touch-icon-167.png", sizes: "167x167" },
|
{ url: "/icons/apple-touch-icon-167.png?v=2", sizes: "167x167" },
|
||||||
{ url: "/icons/apple-touch-icon-152.png", sizes: "152x152" },
|
{ url: "/icons/apple-touch-icon-152.png?v=2", sizes: "152x152" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
other: {
|
other: {
|
||||||
|
|
@ -48,8 +49,8 @@ export const viewport: Viewport = {
|
||||||
userScalable: false,
|
userScalable: false,
|
||||||
viewportFit: "cover",
|
viewportFit: "cover",
|
||||||
themeColor: [
|
themeColor: [
|
||||||
{ media: "(prefers-color-scheme: light)", color: "#f8fafc" },
|
{ media: "(prefers-color-scheme: light)", color: "#f4f7fd" },
|
||||||
{ media: "(prefers-color-scheme: dark)", color: "#0a0f13" },
|
{ media: "(prefers-color-scheme: dark)", color: "#0e1424" },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ export default function manifest(): MetadataRoute.Manifest {
|
||||||
scope: "/",
|
scope: "/",
|
||||||
display: "standalone",
|
display: "standalone",
|
||||||
orientation: "portrait",
|
orientation: "portrait",
|
||||||
background_color: "#f8fafc",
|
background_color: "#0e1424",
|
||||||
theme_color: "#f8fafc",
|
theme_color: "#0e1424",
|
||||||
categories: ["education", "productivity"],
|
categories: ["education", "productivity"],
|
||||||
icons: [
|
icons: [
|
||||||
{ src: "/icons/icon-192.png", sizes: "192x192", type: "image/png", purpose: "any" },
|
{ src: "/icons/icon-192.png", sizes: "192x192", type: "image/png", purpose: "any" },
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { TabBar } from "@/components/TabBar";
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
return (
|
return (
|
||||||
<ApiKeyGate>
|
<ApiKeyGate>
|
||||||
<div className="flex min-h-dvh bg-[var(--page)]">
|
<div className="flex min-h-dvh">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<SidebarOffset />
|
<SidebarOffset />
|
||||||
<MobileSidebar />
|
<MobileSidebar />
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import {
|
||||||
EyeOffIcon,
|
EyeOffIcon,
|
||||||
MoonIcon,
|
MoonIcon,
|
||||||
QuestionIcon,
|
QuestionIcon,
|
||||||
SnowflakeIcon,
|
|
||||||
SunIcon,
|
SunIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from "@/components/icons";
|
} from "@/components/icons";
|
||||||
|
|
@ -233,12 +232,13 @@ function GateScreen() {
|
||||||
transition={{ duration: 0.45, ease: [0.22, 1, 0.36, 1] }}
|
transition={{ duration: 0.45, ease: [0.22, 1, 0.36, 1] }}
|
||||||
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"
|
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
|
{/* eslint-disable-next-line @next/next/no-img-element -- static app icon, no optimisation needed */}
|
||||||
|
<img
|
||||||
|
src="/icons/icon-192.png"
|
||||||
|
alt=""
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className="mx-auto grid size-14 place-items-center rounded-[16px] bg-[var(--text)] text-[var(--page)] dark:border dark:border-[var(--field-border)] dark:bg-[var(--field)] dark:text-[var(--text)]"
|
className="mx-auto size-14 rounded-[16px] shadow-[var(--shadow-card)]"
|
||||||
>
|
/>
|
||||||
<SnowflakeIcon className="size-7" />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<h1 className="mt-6 text-[24px] 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
|
Connect your Ollama key
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { GearIcon, MenuIcon, SnowflakeIcon } from "@/components/icons";
|
import { GearIcon, MenuIcon } from "@/components/icons";
|
||||||
import { SettingsDialog } from "@/components/SettingsDialog";
|
import { SettingsDialog } from "@/components/SettingsDialog";
|
||||||
import { ThemeToggle } from "@/components/ThemeToggle";
|
import { ThemeToggle } from "@/components/ThemeToggle";
|
||||||
import { sidebarOpen } from "@/lib/nav";
|
import { sidebarOpen } from "@/lib/nav";
|
||||||
|
|
@ -47,12 +47,13 @@ export function AppHeader({ className, compact = false }: { className?: string;
|
||||||
<MenuIcon className="size-5" />
|
<MenuIcon className="size-5" />
|
||||||
</button>
|
</button>
|
||||||
<Link href="/" className="group flex items-center gap-2.5 rounded-xl px-1 py-1" aria-label="Freezmo home">
|
<Link href="/" className="group flex items-center gap-2.5 rounded-xl px-1 py-1" aria-label="Freezmo home">
|
||||||
<span
|
{/* eslint-disable-next-line @next/next/no-img-element -- static app icon, no optimisation needed */}
|
||||||
|
<img
|
||||||
|
src="/icons/icon-192.png"
|
||||||
|
alt=""
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className="grid size-8 place-items-center rounded-[0.6rem] bg-[var(--text)] text-[var(--page)] transition-transform group-hover:scale-[1.04]"
|
className="size-8 rounded-[0.6rem] transition-transform group-hover:scale-[1.04]"
|
||||||
>
|
/>
|
||||||
<SnowflakeIcon className="size-[18px]" />
|
|
||||||
</span>
|
|
||||||
<span className="flex items-baseline gap-2">
|
<span className="flex items-baseline gap-2">
|
||||||
<span className="text-[15px] font-semibold tracking-[-0.01em] text-[var(--text)]">
|
<span className="text-[15px] font-semibold tracking-[-0.01em] text-[var(--text)]">
|
||||||
Freezmo
|
Freezmo
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import {
|
||||||
LayersIcon,
|
LayersIcon,
|
||||||
MoonIcon,
|
MoonIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
SnowflakeIcon,
|
|
||||||
SunIcon,
|
SunIcon,
|
||||||
} from "@/components/icons";
|
} from "@/components/icons";
|
||||||
import { useTheme } from "@/components/ThemeToggle";
|
import { useTheme } from "@/components/ThemeToggle";
|
||||||
|
|
@ -110,12 +109,13 @@ function NavContent({
|
||||||
aria-label="Freezmo home"
|
aria-label="Freezmo home"
|
||||||
title="Freezmo"
|
title="Freezmo"
|
||||||
>
|
>
|
||||||
<span
|
{/* eslint-disable-next-line @next/next/no-img-element -- static app icon, no optimisation needed */}
|
||||||
|
<img
|
||||||
|
src="/icons/icon-192.png"
|
||||||
|
alt=""
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className="grid size-7 shrink-0 place-items-center rounded-lg bg-[var(--text)] text-[var(--page)]"
|
className="size-7 shrink-0 rounded-lg shadow-[0_1px_3px_rgb(14_21_40/0.35)]"
|
||||||
>
|
/>
|
||||||
<SnowflakeIcon className="size-4" />
|
|
||||||
</span>
|
|
||||||
{!collapsed ? (
|
{!collapsed ? (
|
||||||
<span className="text-[15px] font-semibold tracking-[-0.01em] text-[var(--text)]">Freezmo</span>
|
<span className="text-[15px] font-semibold tracking-[-0.01em] text-[var(--text)]">Freezmo</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import { forwardRef } from "react";
|
import { forwardRef } from "react";
|
||||||
|
|
||||||
import { SnowflakeIcon } from "@/components/icons";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type ButtonVariant = "primary" | "secondary" | "ghost" | "danger" | "success";
|
type ButtonVariant = "primary" | "secondary" | "ghost" | "danger" | "success";
|
||||||
|
|
@ -135,12 +134,17 @@ export function EmptyState({
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="animate-rise flex flex-col items-center px-6 py-6 text-center sm:py-24">
|
<div className="animate-rise flex flex-col items-center px-6 py-6 text-center sm:py-24">
|
||||||
|
{icon ? (
|
||||||
<span
|
<span
|
||||||
className="mb-4 grid size-12 place-items-center rounded-2xl border border-[var(--hairline)] bg-[var(--surface-strong)] text-[var(--text-subtle)]"
|
className="mb-4 grid size-12 place-items-center rounded-2xl border border-[var(--hairline)] bg-[var(--surface-strong)] text-[var(--text-subtle)]"
|
||||||
aria-hidden
|
aria-hidden
|
||||||
>
|
>
|
||||||
{icon ?? <SnowflakeIcon className="size-6" />}
|
{icon}
|
||||||
</span>
|
</span>
|
||||||
|
) : (
|
||||||
|
/* eslint-disable-next-line @next/next/no-img-element -- static app icon, no optimisation needed */
|
||||||
|
<img src="/icons/icon-192.png" alt="" aria-hidden className="mb-4 size-12 rounded-2xl" />
|
||||||
|
)}
|
||||||
<h2 className="text-[17px] font-semibold tracking-[-0.01em] text-[var(--text)]">{title}</h2>
|
<h2 className="text-[17px] font-semibold tracking-[-0.01em] text-[var(--text)]">{title}</h2>
|
||||||
{children ? (
|
{children ? (
|
||||||
<p className="mt-2 max-w-md text-sm leading-relaxed text-[var(--text-muted)]">{children}</p>
|
<p className="mt-2 max-w-md text-sm leading-relaxed text-[var(--text-muted)]">{children}</p>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue