fuzzyworld

word-waltz / commit

fix styling

2f59f746015ab317da3c9238ce8ed27b1aaff108 · Pavel Safronov · 2026-03-26 22:48:45 -0700 · parents d3fd1d8a92fa

fix styling
 src/components/WordleBoard.ts | 84 +++++++++++++++++++++++++++++--------------
 src/index.html                |  2 +-
 src/styles/main.css           | 32 +++++++++++++++--
 3 files changed, 88 insertions(+), 30 deletions(-)

diff --git a/src/components/WordleBoard.ts b/src/components/WordleBoard.ts
index 604c412..35ae393 100644
--- a/src/components/WordleBoard.ts
+++ b/src/components/WordleBoard.ts
@@ -145,6 +145,37 @@ export function mountWordleBoard(container: HTMLElement, props: WordleBoardProps
     });
   }
 
+  // How long one tile flip takes, and the stagger between tiles.
+  const FLIP_MS = 500;
+  const STAGGER_MS = 100;
+
+  // Reveal a row with staggered flips. Returns a promise that resolves when
+  // the last tile finishes so callers can sequence work afterward.
+  function revealRow(
+    rowTiles: HTMLElement[],
+    guess: string,
+    states: TileState[],
+  ): Promise<void> {
+    return new Promise((resolve) => {
+      rowTiles.forEach((tile, i) => {
+        const delay = i * STAGGER_MS;
+        // Start flip
+        setTimeout(() => {
+          tile.classList.add('wordle-board__tile--flipping');
+          // At the halfway point, swap in the color
+          setTimeout(() => {
+            tile.className = `wordle-board__tile wordle-board__tile--${states[i]} wordle-board__tile--flipping`;
+          }, FLIP_MS / 2);
+          // When animation ends, remove the flipping class
+          tile.addEventListener('animationend', () => {
+            tile.classList.remove('wordle-board__tile--flipping');
+            if (i === rowTiles.length - 1) resolve();
+          }, { once: true });
+        }, delay);
+      });
+    });
+  }
+
   function submitGuess(): void {
     if (currentInput.length !== WORD_LEN) {
       flash('Not enough letters');
@@ -157,37 +188,38 @@ export function mountWordleBoard(container: HTMLElement, props: WordleBoardProps
     }
 
     const states = evaluate(guess, secret);
+    const row = tiles[currentRow];
 
-    // Color tiles
-    tiles[currentRow].forEach((tile, i) => {
-      tile.textContent = guess[i];
-      tile.className = `wordle-board__tile wordle-board__tile--${states[i]}`;
-    });
-
-    // Merge letter states for keyboard
-    states.forEach((s, i) => {
-      if (s === 'correct' || s === 'present' || s === 'absent') {
-        letterStates[guess[i]] = mergeLetterState(letterStates[guess[i]], s);
-      }
-    });
-    updateKeyboard(keyboardEl, letterStates);
-
+    // Lock input during animation
+    done = true;
+    currentInput = '';
     guesses.push(guess);
     currentRow++;
-    currentInput = '';
 
     const solved = states.every((s) => s === 'correct');
-    if (solved) {
-      done = true;
-      giveUpBtn.hidden = true;
-      message.textContent = PRAISE[Math.min(guesses.length - 1, PRAISE.length - 1)];
-      scheduleRoundEnd(true);
-    } else if (currentRow >= MAX_GUESSES) {
-      done = true;
-      giveUpBtn.hidden = true;
-      message.textContent = `The word was ${secret}`;
-      scheduleRoundEnd(false);
-    }
+
+    revealRow(row, guess, states).then(() => {
+      // Merge letter states for keyboard only after all tiles have flipped
+      states.forEach((s, i) => {
+        if (s === 'correct' || s === 'present' || s === 'absent') {
+          letterStates[guess[i]] = mergeLetterState(letterStates[guess[i]], s);
+        }
+      });
+      updateKeyboard(keyboardEl, letterStates);
+
+      if (solved) {
+        giveUpBtn.hidden = true;
+        message.textContent = PRAISE[Math.min(guesses.length - 1, PRAISE.length - 1)];
+        scheduleRoundEnd(true);
+      } else if (currentRow >= MAX_GUESSES) {
+        giveUpBtn.hidden = true;
+        message.textContent = `The word was ${secret}`;
+        scheduleRoundEnd(false);
+      } else {
+        // Allow input again for the next row
+        done = false;
+      }
+    });
   }
 
   function flash(text: string): void {
diff --git a/src/index.html b/src/index.html
index 342cbba..807df7a 100644
--- a/src/index.html
+++ b/src/index.html
@@ -3,7 +3,7 @@
 
 <head>
     <meta charset="UTF-8" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
     <title>Word Waltz</title>
     <link rel="icon" type="image/x-icon" href="/favicon.ico" />
     <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
diff --git a/src/styles/main.css b/src/styles/main.css
index e1fd9ff..f5e3aac 100644
--- a/src/styles/main.css
+++ b/src/styles/main.css
@@ -10,8 +10,8 @@
     --color-text: #1a1a1a;
     --color-muted: #6b6b6b;
 
-    --color-correct: #538d4e;
-    --color-present: #b59f3b;
+    --color-correct: #6aaa64;
+    --color-present: #e5a428;
     --color-absent: #787c7e;
     --color-tile-bg: #ffffff;
     --color-tile-text: #1a1a1a;
@@ -435,14 +435,39 @@ header {
     font-family: var(--font-mono);
     background: var(--color-tile-bg);
     color: var(--color-tile-text);
-    transition: border-color 0.1s;
     user-select: none;
+    /* flip animation */
+    perspective: 250px;
+    transition: border-color 0.1s;
 }
 
 .wordle-board__tile--filled {
     border-color: #999;
 }
 
+/* Flip keyframe: rotate around X, colour applied at 50% */
+@keyframes tile-flip {
+    0% {
+        transform: rotateX(0deg);
+    }
+
+    49% {
+        transform: rotateX(-90deg);
+    }
+
+    50% {
+        transform: rotateX(-90deg);
+    }
+
+    100% {
+        transform: rotateX(0deg);
+    }
+}
+
+.wordle-board__tile--flipping {
+    animation: tile-flip 0.5s ease-in-out forwards;
+}
+
 .wordle-board__tile--correct {
     background: var(--color-correct);
     border-color: var(--color-correct);
@@ -493,6 +518,7 @@ header {
     align-items: center;
     gap: 8px;
     width: 100%;
+    padding-bottom: env(safe-area-inset-bottom, 0px);
     --key-w: clamp(26px, calc(10vw - 6.9px), 43px);
     --key-w-wide: clamp(40px, calc(15vw - 10px), 65px);
 }