From 698fe9d14a79c9f37b9389dff354ad806e7efdae Mon Sep 17 00:00:00 2001 From: adam Date: Tue, 29 Sep 2026 21:36:27 +0000 Subject: [PATCH] polish the website - show Wordle's praise (Genius ... Phew) for wins on the board and stats pages instead of repeating the score - history: month-style calendar with dates and results, next to the summary numbers, replacing the small heatmap strip - recent form as two rows of a week each - fuller identicons at small sizes - put "The word was" above the answer tiles, wrap the terminal demo on phones and hide keyboard hints on touch screens - say where followed players show up (the home page leaderboard) Co-Authored-By: Claude Opus 5.5 --- www/html/wordle/board.js | 6 ++-- www/html/wordle/history.js | 31 +++++++++++-------- www/html/wordle/stats.js | 4 +-- www/html/wordle/wordle.css | 62 ++++++++++++++++++++++++-------------- www/html/wordle/wordle.js | 8 ++++- 5 files changed, 70 insertions(+), 41 deletions(-) diff --git a/www/html/wordle/board.js b/www/html/wordle/board.js index 7c46fdb..63df518 100644 --- a/www/html/wordle/board.js +++ b/www/html/wordle/board.js @@ -1,7 +1,7 @@ import { h, fill, icon, tile, bigBoard, emptyState, skeleton, toast, copyText, confetti, mountShell, profileHeader, parseBoardName, boardPage, historyPage, getBoard, getLastBoard, findGame, today, - formatDay, resultText, shareText, + formatDay, resultText, shareText, verdict, } from "./wordle.js?v=3"; mountShell(); @@ -57,12 +57,12 @@ async function show(user, day) { const answerEl = board.won || !board.answer ? null : h("div", { class: "answer-row", "data-letters": "hidden" }, h("span", {}, "The word was"), - [...board.answer].map(letter => tile(letter, "correct"))); + h("div", { class: "answer-tiles" }, [...board.answer].map(letter => tile(letter, "correct")))); fill(stage, h("div", { class: "headline" }, h("span", { class: board.won ? "badge win" : "badge loss", style: { "font-size": "0.95rem", padding: "6px 14px" } }, - board.won ? `Solved in ${resultText(board)}` : `Missed it - ${resultText(board)}`)), + `${verdict(board)} · ${resultText(board)}`)), boardEl, answerEl, revealButton(boardEl, answerEl), diff --git a/www/html/wordle/history.js b/www/html/wordle/history.js index 8bd62d4..451b43b 100644 --- a/www/html/wordle/history.js +++ b/www/html/wordle/history.js @@ -1,6 +1,6 @@ import { h, icon, miniBoard, emptyState, skeleton, mountShell, profileHeader, normalizeUser, boardPage, - getHistory, today, formatDay, relativeDay, weekdayIndex, resultText, + getHistory, today, formatDay, relativeDay, weekdayIndex, dayOfMonth, resultText, } from "./wordle.js?v=3"; mountShell(); @@ -19,12 +19,12 @@ if (!user) { async function show(user) { const header = profileHeader(user, "history", { meta: "Loading games…" }); - const summary = h("div", { class: "stat-grid" }, Array.from({ length: 4 }, () => skeleton("104px"))); + const summary = h("div", { class: "stat-grid stat-grid-2" }, Array.from({ length: 4 }, () => skeleton("104px"))); const calendar = h("div", { class: "card" }, skeleton("150px")); const games = h("div", { class: "game-grid" }, Array.from({ length: 6 }, () => skeleton("190px"))); const more = h("button", { class: "btn", type: "button", hidden: true }, icon("calendar"), `Load ${PAGE_DAYS} more days`); main.append(header, h("div", { class: "stack", style: { "margin-top": "28px" } }, - summary, calendar, + h("div", { class: "grid-2" }, calendar, summary), h("div", { class: "card-head", style: { "margin-bottom": "0" } }, h("h2", {}, "Games"), h("span", { class: "sub" }, "Newest first")), games, h("div", { style: { "text-align": "center" } }, more))); @@ -49,7 +49,7 @@ async function show(user) { h("div", { class: "card-head" }, h("h2", {}, "Calendar"), h("span", { class: "sub" }, `${formatDay(start, "medium")} - ${formatDay(end, "medium")} (UTC)`)), - heatmap(user, boards, start, end)); + calendarGrid(user, boards, start, end)); games.replaceChildren(...(boards.length ? boards.map(board => gameCard(user, board)) @@ -75,15 +75,19 @@ function stat(value, label) { return h("div", { class: "stat" }, h("div", { class: "value" }, String(value)), h("div", { class: "label" }, label)); } -// GitHub-style grid: a column per week, Monday at the top -function heatmap(user, boards, start, end) { +// Month-style calendar, a row per week starting on Monday. Played days +// show the result and link to their board. +function calendarGrid(user, boards, start, end) { const byDay = new Map(boards.map(board => [board.day, board])); - const cells = Array.from({ length: weekdayIndex(start) }, () => h("span", { class: "heat none" })); + const cells = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].map(name => h("span", { class: "cal-head" }, name)); + for (let i = 0; i < weekdayIndex(start); i++) cells.push(h("span")); + for (let day = start; day <= end; day++) { const board = byDay.get(day); - const cls = day === end ? "heat today" : "heat"; + const date = h("span", { class: "date" }, dayOfMonth(day) === 1 ? formatDay(day, "month") : String(dayOfMonth(day))); + const cls = day === end ? "cal-day today" : "cal-day"; if (!board) { - cells.push(h("span", { class: cls, title: `${formatDay(day, "short")}: not played` })); + cells.push(h("span", { class: cls, title: `${formatDay(day, "short")}: not played` }, date)); continue; } const level = !board.won ? "loss" : board.guesses <= 2 ? "1" : board.guesses <= 4 ? "2" : "3"; @@ -93,14 +97,15 @@ function heatmap(user, boards, start, end) { "data-level": level, title: `${formatDay(day, "short")}: ${resultText(board)}`, "aria-label": `${formatDay(day, "short")}: ${resultText(board)}`, - })); + }, date, board.won ? String(board.guesses) : "✗")); } - const key = (level, label) => [h("span", { class: "heat", "data-level": level }), label]; + + const key = (level, label) => [h("span", { class: "cal-key", "data-level": level }), label]; return h("div", {}, - h("div", { class: "heatmap-wrap" }, h("div", { class: "heatmap" }, cells)), + h("div", { class: "cal" }, cells), h("div", { class: "legend" }, key("1", "1-2 guesses"), key("2", "3-4"), key("3", "5-6"), key("loss", "missed"), - [h("span", { class: "heat" }), "not played"])); + [h("span", { class: "cal-key" }), "not played"])); } function gameCard(user, board) { diff --git a/www/html/wordle/stats.js b/www/html/wordle/stats.js index 80aa310..5d99587 100644 --- a/www/html/wordle/stats.js +++ b/www/html/wordle/stats.js @@ -1,7 +1,7 @@ import { h, icon, miniBoard, emptyState, skeleton, toast, copyText, countUp, countdown, mountShell, profileHeader, normalizeUser, statsPage, boardPage, getStats, getHistory, today, formatDay, - relativeDay, resultText, describeBoard, + relativeDay, resultText, describeBoard, verdict, } from "./wordle.js?v=3"; mountShell(); @@ -102,7 +102,7 @@ function todayCard(user, board, day) { ? [ h("a", { href: boardPage(user, day), title: "Open today's board" }, miniBoard(board, { large: true })), h("div", { class: "text" }, - h("div", { class: "big" }, board.won ? `Solved ${resultText(board)}` : "Missed it"), + h("div", { class: "big" }, `${verdict(board)} ${resultText(board)}`), h("p", { style: { margin: "6px 0 14px", color: "var(--muted)" } }, describeBoard(board)), h("a", { class: "btn btn-sm", href: boardPage(user, day) }, icon("grid"), "View board"))] : [ diff --git a/www/html/wordle/wordle.css b/www/html/wordle/wordle.css index b1c33d1..a964afc 100644 --- a/www/html/wordle/wordle.css +++ b/www/html/wordle/wordle.css @@ -379,11 +379,12 @@ main.container { padding-top: 32px; padding-bottom: 64px; } flex: none; } .avatar span { border-radius: 2px; } +.avatar span[data-state="none"] { background: color-mix(in srgb, var(--text) 5%, transparent); } .avatar span[data-state="correct"] { background: var(--correct); } .avatar span[data-state="present"] { background: var(--present); } .avatar-lg { --size: 84px; gap: 3px; } .chip .avatar { --size: 26px; gap: 1px; padding: 3px; border-radius: 7px; } -.lb-row .avatar { --size: 36px; } +.lb-row .avatar { --size: 38px; gap: 1px; padding: 4px; } .avatar-lg span { border-radius: 3px; } /* ---------- profile header (stats, history, board pages) ---------- */ @@ -399,7 +400,6 @@ main.container { padding-top: 32px; padding-bottom: 64px; } .tabs { width: 100%; } .tabs a { flex: 1; justify-content: center; padding: 8px 6px; font-size: 0.88rem; } } -.terminal-body .line { white-space: pre-wrap; } .tabs a:hover { color: var(--text); text-decoration: none; } .tabs a[aria-current="page"] { background: var(--surface-3); color: var(--text); } @@ -443,12 +443,11 @@ main.container { padding-top: 32px; padding-bottom: 64px; } .dist-row.miss .dist-bar { background: color-mix(in srgb, var(--danger) 70%, var(--absent)); } @keyframes grow { from { transform: scaleX(0); } } -.form-strip { display: flex; gap: 6px; flex-wrap: wrap; } +.form-strip { display: grid; grid-template-columns: repeat(7, minmax(0, 40px)); gap: 6px; } .form-day { display: grid; place-items: center; - width: 34px; - height: 34px; + aspect-ratio: 1; border-radius: 8px; font: 800 0.9rem/1 var(--mono); background: var(--surface-2); @@ -467,22 +466,39 @@ a.form-day:hover { text-decoration: none; transform: translateY(-2px); } /* ---------- history ---------- */ -.heatmap-wrap { overflow-x: auto; padding-bottom: 4px; } -.heatmap { display: grid; grid-auto-flow: column; grid-template-rows: repeat(7, 16px); grid-auto-columns: 16px; gap: 4px; width: max-content; } -.heat { - border-radius: 4px; - background: var(--surface-3); - border: 1px solid transparent; +.stat-grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } + +.cal { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 6px; } +.cal-head { text-align: center; font: 700 0.7rem/1 var(--mono); color: var(--faint); text-transform: uppercase; letter-spacing: 0.08em; padding-bottom: 4px; } +.cal-day { + position: relative; + display: grid; + place-items: center; + aspect-ratio: 1; + border-radius: 10px; + background: var(--surface-2); + border: 1px solid var(--border); + color: #fff; + font: 800 1.05rem/1 var(--mono); + transition: transform 0.15s ease; } -.heat.none { background: transparent; } -.heat[data-level="1"] { background: color-mix(in srgb, var(--correct) 100%, transparent); } -.heat[data-level="2"] { background: color-mix(in srgb, var(--correct) 75%, var(--surface-3)); } -.heat[data-level="3"] { background: color-mix(in srgb, var(--correct) 50%, var(--surface-3)); } -.heat[data-level="loss"] { background: color-mix(in srgb, var(--danger) 70%, var(--absent)); } -.heat.today { border-color: var(--accent); } -a.heat:hover { outline: 2px solid var(--text); outline-offset: 1px; } -.legend { display: flex; align-items: center; gap: 6px; color: var(--muted); font-size: 0.8rem; margin-top: 10px; flex-wrap: wrap; } -.legend .heat { width: 12px; height: 12px; display: inline-block; } +.cal-day .date { position: absolute; top: 5px; left: 7px; font-size: 0.64rem; font-weight: 600; color: var(--faint); } +.cal-day[data-level] { border-color: transparent; } +.cal-day[data-level] .date { color: rgba(255, 255, 255, 0.8); } +.cal-day.today { outline: 2px solid var(--accent); outline-offset: 2px; } +a.cal-day:hover { transform: translateY(-2px); text-decoration: none; } +@media (max-width: 480px) { + .cal { gap: 4px; } + .cal-day { border-radius: 7px; font-size: 0.85rem; } + .cal-day .date { top: 3px; left: 4px; font-size: 0.55rem; } +} +.cal-key { width: 12px; height: 12px; border-radius: 3px; display: inline-block; background: var(--surface-3); } +[data-level="1"] { background: var(--correct); } +[data-level="2"] { background: color-mix(in srgb, var(--correct) 72%, var(--surface-3)); } +[data-level="3"] { background: color-mix(in srgb, var(--correct) 48%, var(--surface-3)); } +[data-level="loss"] { background: color-mix(in srgb, var(--danger) 70%, var(--absent)); } +.legend { display: flex; align-items: center; gap: 6px; color: var(--muted); font-size: 0.8rem; margin-top: 14px; flex-wrap: wrap; } +.legend .cal-key:not(:first-child) { margin-left: 6px; } .game-grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); } .game-card { @@ -504,13 +520,15 @@ a.heat:hover { outline: 2px solid var(--text); outline-offset: 1px; } .board-stage { display: grid; gap: 22px; justify-items: center; text-align: center; } .board-stage .headline { font: 800 1.1rem/1.2 var(--mono); } -.answer-row { display: flex; gap: 6px; justify-content: center; align-items: center; color: var(--muted); } +.answer-row { display: grid; gap: 8px; justify-items: center; color: var(--muted); font-size: 0.9rem; } +.answer-tiles { display: flex; gap: 6px; } .answer-row .tile { width: 38px; font-size: 1.1rem; } .hold-btn { touch-action: none; min-width: 230px; } .hold-btn .icon { transition: transform 0.2s ease; } .hold-btn[aria-pressed="true"] .icon { transform: scale(1.2); } .nav-row { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; } .kbd-hint { color: var(--faint); font-size: 0.82rem; } +@media (hover: none) { .kbd-hint { display: none; } } /* no keyboard on touch screens */ .kbd-hint kbd { padding: 1px 6px; border-radius: 5px; border: 1px solid var(--border-strong); background: var(--surface-2); color: var(--muted); } /* ---------- home page ---------- */ @@ -562,7 +580,7 @@ a.heat:hover { outline: 2px solid var(--text); outline-offset: 1px; } .terminal-bar i:nth-child(3) { background: #28c840; } .terminal-bar span { margin-left: 8px; font-size: 0.8rem; color: #6c8a76; } .terminal-body { padding: 18px 20px 22px; min-height: 380px; font-size: 0.92rem; } -.terminal-body .line { min-height: 1.5em; white-space: pre; overflow: hidden; } +.terminal-body .line { min-height: 1.5em; white-space: pre-wrap; } .terminal-body .prompt { color: #51ff7a; } .terminal-body .dim { color: #5f7a68; } .caret { display: inline-block; width: 0.6em; height: 1.1em; vertical-align: text-bottom; background: #51ff7a; animation: blink 1s steps(1) infinite; } diff --git a/www/html/wordle/wordle.js b/www/html/wordle/wordle.js index 87e9bf8..e0134f9 100644 --- a/www/html/wordle/wordle.js +++ b/www/html/wordle/wordle.js @@ -107,6 +107,7 @@ export function formatDay(day, style = "long") { long: { weekday: "long", day: "numeric", month: "long", year: "numeric" }, medium: { day: "numeric", month: "short", year: "numeric" }, short: { weekday: "short", day: "numeric", month: "short" }, + month: { day: "numeric", month: "short" }, }; return new Date(day * DAY_MS).toLocaleDateString(undefined, { timeZone: "UTC", ...styles[style] }); } @@ -119,6 +120,7 @@ export function relativeDay(day) { } export const weekdayIndex = day => (new Date(day * DAY_MS).getUTCDay() + 6) % 7; // Monday = 0 +export const dayOfMonth = day => new Date(day * DAY_MS).getUTCDate(); export function formatCountdown(ms) { const total = Math.max(0, Math.floor(ms / 1000)); @@ -329,6 +331,10 @@ export function score(guess, answer) { export const resultText = board => `${board.won ? board.guesses : "X"}/6`; +// What the real Wordle says for a win in 1-6 guesses +const PRAISE = ["Genius", "Magnificent", "Impressive", "Splendid", "Great", "Phew"]; +export const verdict = board => (board.won ? PRAISE[board.guesses - 1] : "Missed it"); + export function describeBoard(board) { return board.won ? `Solved in ${board.guesses} of 6 guesses` : "Didn't find the word"; } @@ -637,7 +643,7 @@ export function followButton(user) { const on = !isFollowing(user); setFollowing(user, on); render(); - toast(on ? `Added ${user} to your leaderboard` : `Removed ${user} from your leaderboard`); + toast(on ? `Added ${user} to your leaderboard on the home page` : `Removed ${user} from your leaderboard`); }); render(); return button;