Word Search mobile: focused viewport, theme placement, unique-per-size words

Per field feedback.
* Each day is now THREE distinct puzzles: the three sizes draw DISJOINT word
  slices from a date-shuffled pool (small/med/large = 6/9/13, sum 28 unique).
  Curated fallback themes expanded to 30 words each; LLM proposals accepted only
  if they supply >= 28 unique words, else fall back. No more repeats across sizes.
* Word Search is now a focused game screen on mobile (same as Daily Word): body
  scroll locked + footer hidden (generalized .playing-game), and the grid sizes
  to the largest square that fits between the theme and the palette (container
  query) — the whole puzzle is on screen, no page scroll.
* Theme placement: full "Today's theme · <name>" on the size-selection screen;
  just the theme name on the puzzle itself, saving vertical space for Large.
* cosy → cozy. 🇺🇸

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-11 09:15:06 -04:00
parent 1dda91fd96
commit 52a8bc5326
5 changed files with 101 additions and 57 deletions
+15 -6
View File
@@ -101,24 +101,27 @@
}
});
// Daily Word on mobile = a focused viewport: lock scroll + hide footer. Cleanup
// Any puzzle on mobile = a focused viewport: lock scroll + hide footer. Cleanup
// ALWAYS removes the class (re-run or unmount), so leaving /play can't strand it.
$effect(() => {
if (typeof document === 'undefined') return;
if (view === 'play' && game === 'word') {
document.documentElement.classList.add('playing-word');
return () => document.documentElement.classList.remove('playing-word');
if (view === 'play') {
document.documentElement.classList.add('playing-game');
return () => document.documentElement.classList.remove('playing-game');
}
});
const WS_OPTS = [
['small', 'Small', 'cosy · 8×8'],
['small', 'Small', 'cozy · 8×8'],
['med', 'Medium', 'balanced · 11×11'],
['large', 'Large', 'a longer sit · 14×14'],
];
let wsTheme = $state('');
onMount(async () => {
try { date = (await getJSON('/api/puzzle/word?variant=5')).date; } catch { /* offline */ }
try { wsTheme = (await getJSON('/api/puzzle/wordsearch?variant=med')).theme; } catch { /* offline */ }
refreshStatus();
});
// Refresh hub/selection statuses whenever we land on a screen (incl. Back).
@@ -140,7 +143,7 @@
</div>
</header>
<main class="container page" class:gameview={view === 'play' && game === 'word'}>
<main class="container page" class:gameview={view === 'play'}>
{#if view === 'hub'}
<h1>Play</h1>
<p class="sub">A small calm thing after the brief. One of each a day — no rush, no score to beat but your own.</p>
@@ -165,6 +168,9 @@
{:else if view === 'select'}
<h1 class="seltitle">{game === 'word' ? 'Daily Word' : 'Word Search'}</h1>
{#if game === 'wordsearch' && wsTheme}
<p class="wstheme"><span>Todays theme</span>{wsTheme}</p>
{/if}
<p class="sub">{game === 'word' ? 'Pick your length.' : 'Pick your size.'}</p>
<div class="opts">
{#if game === 'word'}
@@ -206,6 +212,9 @@
h1 { font-size: clamp(2rem, 5vw, 2.6rem); margin: 6px 0 6px; }
.seltitle { font-size: clamp(1.7rem, 4.5vw, 2.2rem); }
.sub { color: var(--muted); margin: 0 0 24px; max-width: 540px; }
.wstheme { font-family: var(--serif); font-size: 1.6rem; color: var(--accent-deep); margin: 4px 0 18px; }
.wstheme span { display: block; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.66rem;
font-family: var(--label); color: var(--muted); margin-bottom: 3px; }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 16px; }
.gamecard {