From 7b6ee622350ad01291b7240823a3a20527ab68d5 Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Thu, 20 Aug 2026 05:26:02 +0200 Subject: [PATCH] feat: sort the first-run game picker alphabetically MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The picker took `available` in feed order — whichever game happened to hold the first event row — which means something everywhere else in the app and nothing on this screen, where the reader is scanning for the two or three names they already know. Sorted on the name printed on the button rather than the LaneId, since the id is not what a reader sees, and through localeCompare rather than `<`, because a code-point sort files hololive Dreams after every capitalised game instead of between Genshin and Honkai. Co-Authored-By: Claude Opus 5 (1M context) --- docs/PRD.md | 5 +++- src/client/components/Welcome.tsx | 27 ++++++++++++++++--- test/views.test.tsx | 44 +++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/docs/PRD.md b/docs/PRD.md index 447a324..17abc94 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -279,7 +279,10 @@ Before any events are shown, the reader picks which games they play, and how the them. A calendar full of games they don't play is worse than an empty one — it buries the thing they came for. Nothing is preselected and the button stays disabled until something is chosen; guessing on their behalf and hoping they -notice is worse than asking. The choice is stored as *hidden* games, the inverse — which is a storage +notice is worse than asking. **The games are listed alphabetically by name**, not in feed order: +everywhere else a lane's position carries meaning, but here the reader is scanning for the two or +three names they already know, and a list ordered by which game happened to hold the first event row +gives them nothing to scan by. The choice is stored as *hidden* games, the inverse — which is a storage shape, not a policy: what happens to a game added later is decided separately, below. **A game added later arrives switched off.** Adding a source is our decision, not the reader's, and diff --git a/src/client/components/Welcome.tsx b/src/client/components/Welcome.tsx index 78e8164..f78be83 100644 --- a/src/client/components/Welcome.tsx +++ b/src/client/components/Welcome.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { useGameMeta } from "../state/gameMeta.tsx"; import type { LaneId } from "../../shared/custom.ts"; import type { View } from "../state/usePrefs.ts"; @@ -37,6 +37,25 @@ export function Welcome({ prev.includes(id) ? prev.filter((g) => g !== id) : [...prev, id], ); + /** + * Alphabetical, by the name on the button. + * + * `available` arrives in feed order — whichever game happened to hold the + * first row — which is meaningful everywhere else in the app and meaningless + * here, where the reader is not reading the list but looking for the two or + * three names they already know. Sorted by `name` and not by `LaneId`, + * because the id is not what is printed: `hsr` is Honkai: Star Rail. And + * through `localeCompare`, because `<` orders by code point and would file + * hololive Dreams after every capitalised name on the screen. + */ + const ordered = useMemo( + () => + [...available].sort((a, b) => + gameMeta(a).name.localeCompare(gameMeta(b).name), + ), + [available, gameMeta], + ); + return (

@@ -55,7 +74,7 @@ export function Welcome({ pushes the view question and the way in off the bottom of the screen, on the one screen where both need to be seen. */}

- {available.map((id) => { + {ordered.map((id) => { const game = gameMeta(id); const on = chosen.includes(id); return ( @@ -111,7 +130,7 @@ export function Welcome({ gameMeta(id).hue)} + hues={ordered.slice(0, 3).map((id) => gameMeta(id).hue)} />
@@ -127,7 +146,7 @@ export function Welcome({