Files
upbeatBytes/frontend/src/lib/components/BottomNav.svelte
T
thejayman77 215a5c4d64 Play hub + Daily Word game (Phase 1 of the games feature)
A calm /play space — "after the brief, a small thing to enjoy." Framework-ready
for more games (Word Search next; zen/coloring later).

* Daily Word (5 letters / 6 guesses) + Long Word (6 / 7) — same Wordle mechanic,
  Upbeat Bytes flavor (no "Wordle" in the UI). Hopeful answers; after solving, a
  one-line "why this word matters."
* LLM proposes, code disposes: answers are picked deterministically by date-seed
  from a hand-curated hopeful pool that's pre-validated ⊆ the guess dictionary
  (always typeable), avoiding recent repeats; the LLM only adds the optional
  "why" (with fallback). daily_puzzles(date, game, variant, payload) stores them
  so everyone gets the same daily; the cycle pre-generates with the "why".
* Bundled guess dictionaries (words-5/6.json, ~12.6k/22.4k) for client-side guess
  validation — never the LLM. Answer lightly obfuscated (base64) in the payload.
* Private, gentle stats (played/solved/streak, guess distribution); spoiler-free
  emoji-grid share. No leaderboard, no timer, no streak-loss drama.
* Play in the bottom nav (replacing Browse, still on the lane rail) + the header.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 16:06:20 -04:00

55 lines
2.8 KiB
Svelte

<script>
// Mobile-only primary navigation. Highlights = the brief, Latest = the
// chronological feed, Browse = mood/topic discovery, You = account.
import Avatar from './Avatar.svelte';
let { active = 'today', onToday, onLatest, onPlay, onYou, user = null } = $props();
</script>
<nav class="bottomnav" aria-label="Primary">
<button class:active={active === 'today'} onclick={onToday}>
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4.2" fill="none" stroke="currentColor" stroke-width="1.8" /><path d="M12 3v2M12 19v2M3 12h2M19 12h2M5.6 5.6l1.4 1.4M17 17l1.4 1.4M18.4 5.6L17 7M7 17l-1.4 1.4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" /></svg>
<span>Highlights</span>
</button>
<button class:active={active === 'latest'} onclick={onLatest}>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h10" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" /></svg>
<span>Latest</span>
</button>
<button class:active={active === 'play'} onclick={onPlay}>
<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="4" width="7" height="7" rx="2" fill="none" stroke="currentColor" stroke-width="1.8"/><rect x="13" y="4" width="7" height="7" rx="2" fill="none" stroke="currentColor" stroke-width="1.8"/><rect x="4" y="13" width="7" height="7" rx="2" fill="none" stroke="currentColor" stroke-width="1.8"/><rect x="13" y="13" width="7" height="7" rx="2" fill="none" stroke="currentColor" stroke-width="1.8"/></svg>
<span>Play</span>
</button>
<button class:active={active === 'you'} onclick={onYou}>
{#if user}
<Avatar {user} size={23} />
{:else}
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="8.5" r="3.6" fill="none" stroke="currentColor" stroke-width="1.8" /><path d="M5 20c0-3.6 3.1-5.5 7-5.5s7 1.9 7 5.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" /></svg>
{/if}
<span>You</span>
</button>
</nav>
<style>
.bottomnav { display: none; }
@media (max-width: 720px) {
.bottomnav {
display: flex;
position: fixed;
left: 0; right: 0; bottom: 0;
z-index: 30;
background: var(--surface);
border-top: 1px solid var(--line);
padding: 6px 8px calc(6px + env(safe-area-inset-bottom));
box-shadow: 0 -2px 14px rgba(40, 38, 28, 0.05);
}
.bottomnav button {
flex: 1;
display: flex; flex-direction: column; align-items: center; gap: 3px;
background: none; border: none; padding: 6px 0; cursor: pointer;
color: var(--muted); font-size: 0.7rem; letter-spacing: 0.02em;
}
.bottomnav button svg { width: 23px; height: 23px; }
.bottomnav button.active { color: var(--accent-deep); }
.bottomnav button.active svg { stroke: var(--accent); }
}
</style>