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 <noreply@anthropic.com>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-09-29 21:36:27 +00:00
1 parent 710c9c5b79
commit 698fe9d14a
5 files changed
+70 -41

No files matched your search

+3 -3
View File
@@ -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),
+18 -13
View File
@@ -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) {
+2 -2
View File
@@ -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"))]
: [
+40 -22
View File
@@ -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; }
+7 -1
View File
@@ -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;