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:
@@ -71,6 +71,6 @@ button { font-family: inherit; cursor: pointer; }
|
||||
the page's scroll). The .playing-word class is toggled by /play and always
|
||||
removed on navigation via effect cleanup. Mobile only — desktop is unaffected. */
|
||||
@media (max-width: 720px) {
|
||||
html.playing-word, html.playing-word body { overflow: hidden; }
|
||||
html.playing-word footer.site { display: none; }
|
||||
html.playing-game, html.playing-game body { overflow: hidden; }
|
||||
html.playing-game footer.site { display: none; }
|
||||
}
|
||||
|
||||
@@ -147,18 +147,20 @@
|
||||
{:else if !words.length}
|
||||
<p class="muted">Could not load today’s word search.</p>
|
||||
{:else}
|
||||
<p class="theme"><span class="lbl">Today’s theme</span>{theme}</p>
|
||||
<p class="theme">{theme}</p>
|
||||
|
||||
<div class="grid" class:ok={okFlash} class:done={status === 'done'} bind:this={gridEl} style="--n:{n}"
|
||||
role="application" aria-label="Word search grid — drag across letters to find words"
|
||||
onpointerdown={down} onpointermove={move} onpointerup={up} onpointercancel={up}>
|
||||
{#each grid as rowStr, r (r)}
|
||||
{#each rowStr.split('') as ch, c (c)}
|
||||
{@const key = r + ',' + c}
|
||||
<div class="cell" class:sel={selSet.has(key)}
|
||||
style={cellColor.has(key) && !selSet.has(key) ? `background:${PALETTE[cellColor.get(key)]};color:#2a2f36` : ''}>{ch}</div>
|
||||
<div class="gridwrap">
|
||||
<div class="grid" class:ok={okFlash} class:done={status === 'done'} bind:this={gridEl} style="--n:{n}"
|
||||
role="application" aria-label="Word search grid — drag across letters to find words"
|
||||
onpointerdown={down} onpointermove={move} onpointerup={up} onpointercancel={up}>
|
||||
{#each grid as rowStr, r (r)}
|
||||
{#each rowStr.split('') as ch, c (c)}
|
||||
{@const key = r + ',' + c}
|
||||
<div class="cell" class:sel={selSet.has(key)}
|
||||
style={cellColor.has(key) && !selSet.has(key) ? `background:${PALETTE[cellColor.get(key)]};color:#2a2f36` : ''}>{ch}</div>
|
||||
{/each}
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="palette">
|
||||
@@ -189,9 +191,8 @@
|
||||
<style>
|
||||
.wordsearch { max-width: 520px; margin: 0 auto; opacity: 0; transform: translateY(6px); }
|
||||
.wordsearch.ready { opacity: 1; transform: none; transition: opacity 0.3s ease, transform 0.3s ease; }
|
||||
.theme { text-align: center; font-family: var(--serif); font-size: 1.3rem; color: var(--accent-deep); margin: 0 0 14px; }
|
||||
.theme .lbl { display: block; text-transform: uppercase; letter-spacing: 0.1em; font-size: 0.64rem;
|
||||
font-family: var(--label); color: var(--muted); margin-bottom: 2px; }
|
||||
.theme { text-align: center; font-family: var(--serif); font-size: 1.4rem; color: var(--accent-deep); margin: 0 0 12px; }
|
||||
.gridwrap { display: flex; justify-content: center; }
|
||||
.grid {
|
||||
/* Cells are ~32px on a wide screen (board grows with the grid) and shrink via
|
||||
1fr to fit a narrow phone — capped by max-width so it can never overflow. */
|
||||
@@ -224,4 +225,21 @@
|
||||
padding: 11px 26px; font: inherit; font-weight: 600; cursor: pointer; }
|
||||
.share:hover { background: var(--accent-deep); }
|
||||
.muted { color: var(--muted); text-align: center; }
|
||||
|
||||
/* Mobile: a focused game screen (matches Daily Word). The grid sizes to the
|
||||
largest square that fits the space left between the theme and the palette,
|
||||
so the whole puzzle is on screen with no page scroll. */
|
||||
@media (max-width: 720px) {
|
||||
.wordsearch { display: flex; flex-direction: column; height: 100%; max-width: 100%; }
|
||||
.theme { flex-shrink: 0; font-size: 1.2rem; margin: 4px 0 8px; }
|
||||
.gridwrap { flex: 1; min-height: 0; container-type: size; align-items: center; padding: 2px 0; }
|
||||
.grid { width: min(100%, 100cqh, calc(var(--n) * 40px)); max-width: none; margin: 0; }
|
||||
.palette { flex-shrink: 0; margin: 10px auto 0; padding: 9px 12px 10px; max-width: 100%;
|
||||
width: 100%; box-sizing: border-box; }
|
||||
.plabel { margin-bottom: 7px; }
|
||||
.words { gap: 6px; }
|
||||
.words li { font-size: 0.78rem; padding: 3px 9px; }
|
||||
.hint { flex-shrink: 0; padding: 9px 0 calc(env(safe-area-inset-bottom) + 6px); }
|
||||
.result { flex-shrink: 0; padding-bottom: calc(env(safe-area-inset-bottom) + 6px); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>Today’s 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 {
|
||||
|
||||
Reference in New Issue
Block a user