fix: let the settings panel use the width of the page

Looked at on a 1440px screen it was the one block on the page not using
the column it sits in. The rows were held to max-w-3xl inside an 1150px
shell, but the section's own rule spans the shell — so every hairline
inside the panel stopped a third of the column short of the one above it,
between a two-column checklist and a three-column footer that both go
full width. The measure was there to keep a name and its state reading as
one line, but that pairing is what "running now" already does with a
title and its countdown directly above, so full width is the house
pattern rather than a stretch. Prose inside a group keeps its measure.

The game list was the worse half: eighteen rows of four controls at
max-w-md is a thousand pixels of ribbon down the left with two thirds of
the screen empty beside it. It takes two columns past lg once there are
nine or more games, flowing down then across so the numbers still read in
a straight line and an arrow still swaps a row with its neighbour. Below
nine, and on a phone, it is unchanged.

The last group also loses its bottom rule, which was a second hairline a
few pixels above the footer's own.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-20 07:24:38 +02:00
co-authored by Claude Opus 5
parent abed3f86fa
commit 35b3ab8e89
4 changed files with 57 additions and 12 deletions
+10
View File
@@ -937,6 +937,16 @@ to an open page). Four things hold it up:
Native `<details>` for the reason the reorder arrows are ordinary buttons: keyboard and screen Native `<details>` for the reason the reorder arrows are ordinary buttons: keyboard and screen
reader reach it without a second implementation. `summary` is not an `a`, `button` or `[tabindex]`, reader reach it without a second implementation. `summary` is not an `a`, `button` or `[tabindex]`,
so it needs its own `:focus-visible` rule in `styles.css` — the shared one does not reach it. so it needs its own `:focus-visible` rule in `styles.css` — the shared one does not reach it.
- **The panel is one column of groups, and it uses the page's width.** Those are two claims and only
the first survived a desktop read: the rows were held to `max-w-3xl` inside a shell a third wider,
so every rule inside the panel stopped short of the panel's own, and it was the one block on a wide
page not using the width — between a two-column checklist and a three-column footer that both do.
A name at one end of a row and its state at the other is what "running now" already does with a
title and its countdown, so full width is the house pattern rather than a stretch. Prose inside a
group keeps its own measure, and the game list — the one thing tall enough to need it — takes **two
columns of its own past `lg` once there are nine or more games**, flowing down then across so the
numbers still read in a straight line and an arrow still swaps a row with its neighbour. Eighteen
rows in one column was a 1,000px ribbon with two thirds of the width empty beside it.
- **A game's dailies stay together, and grouping never re-sorts inside a group.** `dailyGroups` - **A game's dailies stay together, and grouping never re-sorts inside a group.** `dailyGroups`
(`Dailies.tsx`) emits a game's standing chore followed by that game's repeating events, in the (`Dailies.tsx`) emits a game's standing chore followed by that game's repeating events, in the
order those arrived; it replaced `[...chores, ...repeating]`, which put a game's chore and its own order those arrived; it replaced `[...chores, ...repeating]`, which put a game's chore and its own
+9
View File
@@ -217,6 +217,15 @@ points at: the empty states name both the switch and the group it is in, which i
a checkbox in a wall was. And **the summary states are derived, never stored** — they are a reading a checkbox in a wall was. And **the summary states are derived, never stored** — they are a reading
of `prefs`, so they cannot disagree with the controls inside. of `prefs`, so they cannot disagree with the controls inside.
**The panel uses the page's width**, like every other block on it. The rows were held to a narrower
measure on the argument that a name and its state stop reading as one line across a wide screen, but
the panel's own rule spans the shell, so the rules inside it stopped a third of the column short and
the panel became the one thing on a desktop page not using the width — sitting between a two-column
checklist and a three-column footer that both do. A title at one end of a full-width row and its
countdown at the other is what "running now" does directly above it. Prose inside a group keeps a
readable measure, and the game list takes two columns of its own on a wide screen once there are nine
or more games, because eighteen rows in one column is a thousand pixels of ribbon down the left.
**F4a — Focus one game at a time.** **F4a — Focus one game at a time.**
Switching games on and off says *which games the reader plays*, and is set once. It is the wrong Switching games on and off says *which games the reader plays*, and is set once. It is the wrong
tool for the thing a player of four games actually does while reading: clear one game, move to the tool for the thing a player of four games actually does while reading: clear one game, move to the
+29 -12
View File
@@ -61,11 +61,11 @@ const SPLITS: Array<{ id: boolean; label: string; hint: string }> = [
* report of how the app is configured, and makes opening one a deliberate act * report of how the app is configured, and makes opening one a deliberate act
* rather than the price of reading it. * rather than the price of reading it.
* *
* One column rather than the two this replaced. The two-column split was there * One column of groups rather than the two this replaced: the split was there
* because the panel was tall and a wide screen had room to halve it; with the * because the panel was tall, and with the groups closed the whole thing is
* groups closed the whole thing is shorter than the header above it, so the * shorter than the header above it. That is a claim about the *groups*, not
* argument is gone, and full-width rows give the summary state somewhere to sit * about the panel's width — the rows run the full column, and the one list long
* on the right of the name it belongs to. * enough to need it takes two columns of its own inside its group.
*/ */
export function Controls({ export function Controls({
games, games,
@@ -104,11 +104,16 @@ export function Controls({
you only have to open the one you came to change. you only have to open the one you came to change.
</p> </p>
{/* Held to a readable measure rather than the container's full width. Each {/* Full width, like every other row on this page. This was held to
row pairs a name on the left with its state on the right, and across a `max-w-3xl` on the argument that a name and its state stop reading as
wide screen those two ends stop reading as one line — which is the whole one line across a wide screen — but the section's own rule spans the
point of putting the state there. */} shell, so every rule inside it stopped short of that by a third of the
<div className="mt-3.5 max-w-3xl border-t border-hairline"> column, and the panel became the one block on a desktop page not using
the width, between a two-column checklist and a three-column footer
that both do. The pairing itself is what the page already does: a
deadline and its countdown sit at opposite ends of a full-width row
directly above this. Prose inside a group keeps its own measure. */}
<div className="mt-3.5 border-t border-hairline">
<Group name="Games" state={gamesState(games.length, shown, prefs.gameOrder !== undefined)}> <Group name="Games" state={gamesState(games.length, shown, prefs.gameOrder !== undefined)}>
<GameOrder <GameOrder
games={games} games={games}
@@ -499,7 +504,19 @@ function GameOrder({
)} )}
</div> </div>
<ul className="mt-2 max-w-md"> {/* Two columns once there are enough games to make one tall, and once the
screen is wide enough to hold them — eighteen rows was a 1,000px ribbon
down the left of a desktop page with two thirds of the width empty
beside it. It flows *down* then across, not across then down, so the
numbers still read 123 in a straight line and an arrow still swaps a
row with the one it is next to. Below nine games it stays one column:
splitting a list short enough to take in at a glance only strands a
couple of rows in a second column. */}
<ul
className={`mt-2 max-w-md ${
games.length >= 9 ? "lg:max-w-4xl lg:columns-2 lg:gap-x-10" : ""
}`}
>
{games.map((id, i) => { {games.map((id, i) => {
const game = gameMeta(id); const game = gameMeta(id);
const on = !hidden.includes(id); const on = !hidden.includes(id);
@@ -517,7 +534,7 @@ function GameOrder({
setDragging(null); setDragging(null);
}} }}
onDragEnd={() => setDragging(null)} onDragEnd={() => setDragging(null)}
className={`flex items-center gap-2 rounded-lg py-1 ${ className={`flex break-inside-avoid items-center gap-2 rounded-lg py-1 ${
dragging === i ? "opacity-40" : "" dragging === i ? "opacity-40" : ""
}`} }`}
> >
+9
View File
@@ -448,6 +448,15 @@ body {
border-radius: 3px; border-radius: 3px;
} }
/*
* The last group closes the list, so its rule would be a second hairline a few
* pixels above the footer's own — two parallel lines with nothing between them,
* which reads as a mistake rather than as a boundary.
*/
.settings-group:last-child {
border-bottom-width: 0;
}
.settings-chevron { .settings-chevron {
transition: transform 150ms ease; transition: transform 150ms ease;
} }