Build the SvelteKit frontend: calm home with mood modes

- New frontend/ SvelteKit static SPA (Svelte 5), served by FastAPI from
  frontend/build (falls back to the legacy page if unbuilt).
- Calm design system: cream/sage palette, serif headlines, generous space,
  no urgency colors, gentle motion (respects prefers-reduced-motion).
- Home screen: mood-mode nav (Today/Wonder/People Helping/Solutions/Light
  Only/Grounded), the daily brief as a hero + remaining four, browsable mood
  lanes, an explicit calm end-state, inline Not today / Less like this / Hide
  affordances, and device-local Calm Filters mirroring goodnews/filters.py.
- Backend: moods.py + GET /api/moods (single source of truth for the modes);
  FilterPrefs gains max_cortisol/max_ragebait ceilings (for Light Only).
- Push categorical filters (include/mute topics+flavors, ceilings) into SQL in
  queries.feed so low-ranked-but-matching items (e.g. discovery for Wonder)
  are not truncated by ranking; only avoid-terms stay a Python pass.
- PWA manifest + icon (installable; offline deferred per plan).
- Multi-stage Dockerfile builds the site then serves it from the API.
- Tests: queries.feed categorical filters (63 total). README updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-05-30 22:27:46 +00:00
parent 1e190c5e88
commit 5601022cf7
24 changed files with 2262 additions and 16 deletions
+54
View File
@@ -0,0 +1,54 @@
/* goodNews calm design system.
Warm but crisp: cream surfaces, sage accent, a serif voice for headlines,
strong readable contrast, generous space. No urgency colors (no red). */
:root {
--bg: #faf6ee;
--surface: #fffdf8;
--ink: #21281f;
--muted: #616c60;
--sage: #2f7d5b;
--sage-deep: #235e44;
--sage-soft: #e6efe6;
--line: #e7e1d4;
--gold: #a9802f;
--serif: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif;
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--maxw: 880px;
--radius: 16px;
--shadow: 0 1px 2px rgba(40, 38, 28, 0.04), 0 10px 30px rgba(40, 38, 28, 0.055);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: var(--sans);
font-size: 17px;
line-height: 1.62;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
h1, h2, h3 {
font-family: var(--serif);
font-weight: 600;
line-height: 1.18;
letter-spacing: -0.012em;
margin: 0;
}
a { color: inherit; text-decoration: none; }
img { max-width: 100%; display: block; }
button { font-family: inherit; cursor: pointer; }
::selection { background: var(--sage-soft); }
.container { max-width: var(--maxw); margin: 0 auto; padding: 0 20px; }
.muted { color: var(--muted); }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(238px, 1fr)); gap: 18px; }
/* a soft, slow fade for content as it arrives — calm, not flashy */
@keyframes rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.rise { animation: rise 0.5s ease both; }
@media (prefers-reduced-motion: reduce) { .rise { animation: none; } html { scroll-behavior: auto; } }