Daily Word: give the keyboard + board some JUICE
The keyboard read as a timid little afterthought. Now it's a confident game board: * Bigger, bolder keys (taller, larger font) with a tactile press — a soft bottom edge + shadow that compresses on tap (translateY). Enter is a solid accent key with its own depth; feedback keys keep matching depth. * Board tiles a touch larger to fill the screen better. * Real game feedback animations: tiles POP as you type, the row REVEALS with a staggered bounce when you submit, and shakes on an invalid word. Respects prefers-reduced-motion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
let status = $state('playing'); // 'playing' | 'won' | 'lost'
|
||||
let loading = $state(true);
|
||||
let submitting = $state(false);
|
||||
let shake = $state(false);
|
||||
let message = $state('');
|
||||
let ready = $state(false); // animate-in once loaded
|
||||
|
||||
@@ -85,10 +86,12 @@
|
||||
if (/^[a-z]$/.test(k) && current.length < length) current += k;
|
||||
}
|
||||
|
||||
function triggerShake() { shake = true; setTimeout(() => (shake = false), 420); }
|
||||
|
||||
async function submit() {
|
||||
if (submitting) return;
|
||||
if (current.length < length) return flash('Not enough letters');
|
||||
if (dict && !dict.has(current)) return flash('Not in word list');
|
||||
if (current.length < length) { flash('Not enough letters'); triggerShake(); return; }
|
||||
if (dict && !dict.has(current)) { flash('Not in word list'); triggerShake(); return; }
|
||||
submitting = true;
|
||||
try {
|
||||
const res = await postJSON('/api/puzzle/word/guess', { variant, guess: current, n: guesses.length + 1 });
|
||||
@@ -158,10 +161,11 @@
|
||||
{#each Array(maxGuesses) as _, r (r)}
|
||||
{@const g = guesses[r]}
|
||||
{@const cs = cols[r] || null}
|
||||
<div class="row">
|
||||
<div class="row" class:shake={shake && r === guesses.length}>
|
||||
{#each Array(length) as _, c (c)}
|
||||
{@const ch = g ? g[c] : (r === guesses.length ? current[c] : '')}
|
||||
<div class="tile {cs ? cs[c] : ''}" class:filled={!!ch}>{(ch || '').toUpperCase()}</div>
|
||||
<div class="tile {cs ? cs[c] : ''}" class:filled={!!ch}
|
||||
style={cs ? `animation-delay:${c * 0.08}s` : ''}>{(ch || '').toUpperCase()}</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
@@ -217,6 +221,16 @@
|
||||
.tile.correct { background: #4a9d6e; border-color: #4a9d6e; color: #fff; }
|
||||
.tile.present { background: #d8b24a; border-color: #d8b24a; color: #fff; }
|
||||
.tile.absent { background: #9aa6b2; border-color: #9aa6b2; color: #fff; }
|
||||
|
||||
/* Juice: a tile pops as you type; the row reveals with a staggered bounce when
|
||||
you submit; the row shakes on an invalid word. */
|
||||
.tile.filled:not(.correct):not(.present):not(.absent) { animation: pop 0.13s ease; }
|
||||
.tile.correct, .tile.present, .tile.absent { animation: reveal 0.34s ease both; }
|
||||
.row.shake { animation: shake 0.4s ease; }
|
||||
@keyframes pop { 0% { transform: scale(1); } 45% { transform: scale(1.09); } 100% { transform: scale(1); } }
|
||||
@keyframes reveal { 0% { transform: scale(0.5); opacity: 0.3; } 55% { transform: scale(1.12); } 100% { transform: scale(1); opacity: 1; } }
|
||||
@keyframes shake { 0%, 100% { transform: translateX(0); } 20% { transform: translateX(-7px); } 40% { transform: translateX(7px); } 60% { transform: translateX(-5px); } 80% { transform: translateX(5px); } }
|
||||
@media (prefers-reduced-motion: reduce) { .tile, .row { animation: none !important; } }
|
||||
.flash {
|
||||
text-align: center; background: var(--ink); color: #fff; border-radius: 8px;
|
||||
padding: 7px 14px; width: fit-content; margin: 0 auto 12px; font-size: 0.86rem;
|
||||
@@ -224,22 +238,24 @@
|
||||
/* Keyboard fills its area like a real phone game: a left block of square,
|
||||
alphabetical letter keys + a right column with Backspace (top) / Enter
|
||||
(bottom) tucked in. Flat, warm, on-brand keys — off-white, hairline border. */
|
||||
.keyboard { display: flex; align-items: stretch; gap: 6px; margin: 10px auto 0; max-width: 440px; }
|
||||
.letters { flex: 1; display: grid; grid-template-columns: repeat(9, 1fr); gap: 5px; }
|
||||
.controls { display: grid; grid-template-rows: 1fr 1fr; gap: 5px; width: 50px; flex-shrink: 0; }
|
||||
.keyboard { display: flex; align-items: stretch; gap: 7px; margin: 12px auto 0; max-width: 460px; }
|
||||
.letters { flex: 1; display: grid; grid-template-columns: repeat(9, 1fr); gap: 6px; }
|
||||
.controls { display: grid; grid-template-rows: 1fr 1fr; gap: 6px; width: 58px; flex-shrink: 0; }
|
||||
.key {
|
||||
border: 1px solid var(--line); border-radius: 11px; background: var(--surface); color: var(--ink);
|
||||
font-family: var(--label); font-weight: 600; font-size: 0.95rem; cursor: pointer; text-transform: uppercase;
|
||||
padding: 0; transition: background 0.12s ease, transform 0.05s ease;
|
||||
border: 1px solid var(--line); border-radius: 12px; background: var(--surface); color: var(--ink);
|
||||
font-family: var(--label); font-weight: 700; font-size: 1.18rem; cursor: pointer; text-transform: uppercase;
|
||||
padding: 0; box-shadow: 0 2px 0 rgba(120, 108, 84, 0.22), 0 3px 6px rgba(60, 50, 30, 0.07);
|
||||
transition: transform 0.06s ease, box-shadow 0.06s ease, background 0.12s ease, filter 0.12s ease;
|
||||
}
|
||||
.key:active { transform: translateY(1px); background: var(--bg); }
|
||||
.letters .key { aspect-ratio: 1; min-width: 0; }
|
||||
.controls .key { height: 100%; font-size: 0.62rem; letter-spacing: 0.03em; }
|
||||
.controls .back { font-size: 1.2rem; }
|
||||
.controls .enter { background: var(--accent-soft); border-color: transparent; color: var(--accent-deep); }
|
||||
.key.correct { background: #4a9d6e; border-color: #4a9d6e; color: #fff; }
|
||||
.key.present { background: #d8b24a; border-color: #d8b24a; color: #fff; }
|
||||
.key.absent { background: #9aa6b2; border-color: #9aa6b2; color: #fff; }
|
||||
.key:active { transform: translateY(2px); box-shadow: 0 0 0 rgba(0, 0, 0, 0); background: var(--bg); }
|
||||
.letters .key { min-width: 0; height: 52px; }
|
||||
.controls .key { height: 100%; font-size: 0.66rem; letter-spacing: 0.04em; }
|
||||
.controls .back { font-size: 1.35rem; }
|
||||
.controls .enter { background: var(--accent); border-color: var(--accent); color: #fff;
|
||||
box-shadow: 0 2px 0 var(--accent-deep), 0 3px 6px rgba(0, 131, 173, 0.18); }
|
||||
.key.correct { background: #4a9d6e; border-color: #4a9d6e; color: #fff; box-shadow: 0 2px 0 #3a7d56; }
|
||||
.key.present { background: #d8b24a; border-color: #d8b24a; color: #fff; box-shadow: 0 2px 0 #b8943a; }
|
||||
.key.absent { background: #9aa6b2; border-color: #9aa6b2; color: #fff; box-shadow: 0 2px 0 #7e8a96; }
|
||||
.key:hover { filter: brightness(0.98); }
|
||||
.muted { color: var(--muted); text-align: center; }
|
||||
|
||||
@@ -251,13 +267,14 @@
|
||||
.play-area { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column;
|
||||
justify-content: flex-start; padding: 8px 0 4px; }
|
||||
.board {
|
||||
--tile: min(58px, calc((100vw - 56px) / var(--cols)), calc((100dvh - 290px) / var(--rows)));
|
||||
gap: 4px; width: fit-content; margin: 0 auto 10px;
|
||||
--tile: min(64px, calc((100vw - 56px) / var(--cols)), calc((100dvh - 300px) / var(--rows)));
|
||||
gap: 5px; width: fit-content; margin: 0 auto 10px;
|
||||
}
|
||||
.row { grid-template-columns: repeat(var(--cols), var(--tile)); gap: 4px; }
|
||||
.row { grid-template-columns: repeat(var(--cols), var(--tile)); gap: 5px; }
|
||||
.tile { width: var(--tile); height: var(--tile); aspect-ratio: auto; font-size: calc(var(--tile) * 0.46); }
|
||||
.keyboard { flex-shrink: 0; gap: 5px; margin: 8px auto calc(env(safe-area-inset-bottom) + 4px); }
|
||||
.controls { width: 46px; }
|
||||
.keyboard { flex-shrink: 0; gap: 6px; margin: 10px auto calc(env(safe-area-inset-bottom) + 6px); }
|
||||
.letters .key { height: 50px; }
|
||||
.controls { width: 50px; }
|
||||
}
|
||||
.result { text-align: center; }
|
||||
.rmark { font-family: var(--serif); font-style: italic; color: var(--accent-deep); font-size: 1.2rem; margin: 0 0 10px; }
|
||||
|
||||
Reference in New Issue
Block a user