refactor(ui): make an event row a single open target

The row already opened the event; the tick sat on top of it, so the two
smallest targets on the page did different things a few pixels apart.
"Done" was never the only thing a reader wants to say about an event
either — status, effort and notes all live in the detail sheet.

The tick becomes a chevron: decorative, aria-hidden, and leaning towards
where the row is about to take you on hover. The full-bleed button is
the only control, so there is no second stop for anyone tabbing or using
a screen reader. Undo on a revealed ignored row stays a real button —
it is a real action with nowhere better to sit.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-15 21:16:25 +02:00
co-authored by Claude Opus 5
parent b085087b05
commit b1df7b2581
3 changed files with 50 additions and 31 deletions
-2
View File
@@ -228,7 +228,6 @@ export function App() {
status={prog.progress[row.event.id]?.status} status={prog.progress[row.event.id]?.status}
effort={prog.progress[row.event.id]?.effort} effort={prog.progress[row.event.id]?.effort}
ignored={ignored.marks[row.event.id] !== undefined} ignored={ignored.marks[row.event.id] !== undefined}
onToggle={toggle}
onRestore={(id) => ignored.toggle(id)} onRestore={(id) => ignored.toggle(id)}
onOpen={setOpenId} onOpen={setOpenId}
/> />
@@ -246,7 +245,6 @@ export function App() {
status={prog.progress[row.event.id]?.status} status={prog.progress[row.event.id]?.status}
effort={prog.progress[row.event.id]?.effort} effort={prog.progress[row.event.id]?.effort}
ignored={ignored.marks[row.event.id] !== undefined} ignored={ignored.marks[row.event.id] !== undefined}
onToggle={toggle}
onRestore={(id) => ignored.toggle(id)} onRestore={(id) => ignored.toggle(id)}
onOpen={setOpenId} onOpen={setOpenId}
/> />
+31 -18
View File
@@ -21,7 +21,6 @@ interface EventRowProps {
effort?: Effort | undefined; effort?: Effort | undefined;
/** Only ever true when the reader has chosen to reveal ignored events. */ /** Only ever true when the reader has chosen to reveal ignored events. */
ignored?: boolean | undefined; ignored?: boolean | undefined;
onToggle: (id: string) => void;
onRestore?: ((id: string) => void) | undefined; onRestore?: ((id: string) => void) | undefined;
onOpen: (id: string) => void; onOpen: (id: string) => void;
} }
@@ -32,7 +31,6 @@ export function EventRow({
status, status,
effort, effort,
ignored = false, ignored = false,
onToggle,
onRestore, onRestore,
onOpen, onOpen,
}: EventRowProps) { }: EventRowProps) {
@@ -156,9 +154,15 @@ export function EventRow({
</div> </div>
</div> </div>
{/* Sits above the row target so ticking done never opens the sheet. {/* The trailing control used to tick an event done from the list. It
On a revealed ignored row this becomes the undo, which is the most does not any more: "done" was never the only thing a reader wants
direct place to put it. */} to say about an event, and a tick they can hit by accident on the
way to opening it is a bad trade. The row opens the sheet, where
status, effort and notes all live; this is just the affordance
saying so.
The one exception is a revealed ignored row, where undo is a real
action with nowhere better to sit. */}
{ignored && onRestore !== undefined ? ( {ignored && onRestore !== undefined ? (
<button <button
type="button" type="button"
@@ -178,20 +182,17 @@ export function EventRow({
</svg> </svg>
</button> </button>
) : ( ) : (
<button // Decorative: the full-bleed button behind the row is already the
type="button" // control, and a second one with the same effect would just be an
onClick={() => onToggle(event.id)} // extra stop for anyone tabbing or using a screen reader.
aria-pressed={completed} <span
aria-label={ aria-hidden
completed ? `Mark ${event.title} not done` : `Mark ${event.title} done` className={`row-check row-open grid size-7 shrink-0 place-items-center self-center rounded-md ${
} completed ? "text-near" : "text-faint"
className={`row-check pointer-events-auto relative z-20 grid size-7 shrink-0 cursor-pointer place-items-center self-center rounded-md border ${
completed
? "border-transparent bg-near/20 text-near"
: "border-hairline text-faint"
}`} }`}
> >
<svg viewBox="0 0 16 16" className="size-3.5" aria-hidden> {completed ? (
<svg viewBox="0 0 16 16" className="size-3.5">
<path <path
d="M2.5 8.5l3.5 3.5 7.5-8" d="M2.5 8.5l3.5 3.5 7.5-8"
fill="none" fill="none"
@@ -201,7 +202,19 @@ export function EventRow({
strokeLinejoin="round" strokeLinejoin="round"
/> />
</svg> </svg>
</button> ) : (
<svg viewBox="0 0 16 16" className="size-3.5">
<path
d="M6 3l5 5-5 5"
fill="none"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
</span>
)} )}
</div> </div>
</li> </li>
+8
View File
@@ -176,6 +176,13 @@ body {
border-color: var(--color-faint); border-color: var(--color-faint);
color: var(--color-muted); color: var(--color-muted);
} }
/* The chevron leans towards where the row is about to take you. It is the
only thing left on the right of a row, so it has to carry "this opens"
on its own. */
.event-row:hover .row-open {
transform: translateX(2px);
color: var(--color-ink);
}
.row-check:hover { .row-check:hover {
transform: scale(1.08); transform: scale(1.08);
border-color: var(--color-muted); border-color: var(--color-muted);
@@ -212,6 +219,7 @@ body {
} }
.row-check:hover, .row-check:hover,
.row-check:active, .row-check:active,
.event-row:hover .row-open,
.event-row:hover .row-rail { .event-row:hover .row-rail {
transform: none; transform: none;
} }