Skip to content

Polish sweep: title clipping, drag handles, dialog UX, per-timer chime, and reorder/archive fixes#77

Merged
danielchalmers merged 9 commits into
mainfrom
claude/elastic-chatelet-380dcb
Jun 26, 2026
Merged

Polish sweep: title clipping, drag handles, dialog UX, per-timer chime, and reorder/archive fixes#77
danielchalmers merged 9 commits into
mainfrom
claude/elastic-chatelet-380dcb

Conversation

@danielchalmers

Copy link
Copy Markdown
Owner

A round of polish fixes. Each item is its own commit so they can be reviewed (and merge-committed) independently. Every change is covered by unit and/or e2e tests, and the full suite, both browser builds, and Playwright e2e all pass locally.

Changes (one commit each)

  1. Keep the Dayboard title's descender from being clipped — the gradient-clipped h1 painted its fill only within a tight line-height line box, shearing the tail off the “y”. Added a touch of vertical padding so glyphs render in full.
  2. Drop the colored dot before each widget title — removed the accent dot (and its styles); the card's tint already conveys the color. Title is back to a plain heading.
  3. Limit the drag handle to the card's edge — the drag frame covered the whole card behind the content, so the blank space between header and body was a drag surface. A donut clip-path now leaves only the padded outer ring as the handle.
  4. Save the edit dialog when its backdrop is clicked — clicking outside the add/edit dialog did nothing; it now commits the edit (via the form's requestSubmit, so required-field validation still applies), which feels more fluid.
  5. Close and cancel the edit dialog on Escape — Escape did nothing because the dialog adopted its draft in an effect, so its focusable section didn't exist on the first open render and the modal-focus hook (Escape, focus-move, focus-trap) never attached. The draft is now synced during render; Escape closes and discards, and focus is correctly moved/trapped.
  6. Reserve the scrollbar gutterscrollbar-gutter: stable so revealing the archive or overflowing the viewport no longer pops a scrollbar that shifts the centered board sideways.
  7. Fix the menu's Move up/down when widgets are archived — Move up/down reordered against the raw array; a widget added after archiving one sat past the hidden archived entry, so swapping with its array-neighbor changed nothing visible. New moveActiveWidget reorders against the visible (non-archived) neighbor.
  8. Make the timer chime per-widget and opt-in — replaced the single global chime toggle with a per-timer “Chime when it ends” switch in each timer's dialog, so you can have specific timers that chime while the rest stay silent.
  9. Stop dragged cards snapping back at the archive zone — the archive/restore drop zones sit outside the sortable list, so dragging onto one reset the active card's transform and it jumped back to its slot. The lifted card now renders in a DragOverlay portal that tracks the cursor onto the zones; the in-list card stays as a dimmed placeholder.

Testing

  • npm run verify (typecheck + Vitest + Chrome build) — green (115 unit tests).
  • npm run build:edge — green.
  • npm run e2e — green (44 Playwright tests), including new coverage for the edge-only handle, backdrop-save, Escape-cancel, stable gutter, archived Move up/down, per-timer chime, and the archive drag tracking the cursor.

Screenshots

Before/after comparison images for the visual changes are attached below. (Uploading separately — the generated PNGs aren't committed.)

  • Clipped “y” fixedcompare-title.png
  • Colored dot removedcompare-card-header.png
  • Per-timer chime togglecompare-timer-dialog.png
  • Global chime removed from Optionscompare-settings.png
  • Drag-to-archive follows the cursorafter-archive-drag.png

The page title clips the heading's gradient to the text with
-webkit-background-clip: text, which paints the fill only within the line
box. With the tight line-height: 1.1 the box ended right at the baseline,
so the tail of the "y" in "Dayboard" was sheared off at the bottom.

Add a small vertical padding (a touch above, a bit more below) so the
glyphs — descenders included — render in full without noticeably shifting
the header layout.
The small accent dot in front of the card title added visual noise without
carrying information the card's own tint doesn't already convey. Remove the
dot element and its styles, and simplify the title back to a plain text
heading (the flex layout existed only to align the dot).

The widget's color preset is still reflected by the card's border, gradient,
and controls, so identity reads clearly without the extra mark.
The drag frame stretched across the whole card behind the content, so the
empty space between the header and body — and any gap inside the card — was a
drag surface. Grabbing the visually blank middle of a card to move it felt
wrong.

Cut a donut-shaped hole through the frame with clip-path so only the card's
padded outer ring catches drag pointer events. The body content was already
lifted above the frame, so this leaves just the border as the handle; the
interior no longer starts a drag (and no longer lights the card on hover).

Add an e2e test that presses in the gap between the header and body and
confirms no drag begins.
Clicking outside the add/edit dialog previously did nothing, so dismissing it
that way was impossible and any in-progress edit lingered. Make a backdrop
click commit the edit instead — the same path as pressing Save or Enter — so
closing the dialog by clicking away feels fluid and keeps the work.

It routes through the form's requestSubmit(), so native required-field
validation still applies: an invalid field keeps the dialog open rather than
saving a half-filled widget.
Pressing Escape in the add/edit dialog did nothing: the modal-focus hook that
handles Escape (and focus-move and the focus trap) was never wired up for this
dialog. The dialog adopted its editable draft in an effect, so on the first
open render the draft was still null and the component returned null. The focus
hook ran against a section that did not exist yet and bailed; its deps never
changed afterward, so it never re-attached once the section mounted.

Adopt the incoming item during render instead, so the dialog's section exists
on the first open render and the focus hook attaches. Escape now closes the
dialog and discards the in-progress edit, and focus correctly moves into and is
trapped within the dialog.
Revealing the archived section or adding enough widgets to overflow the
viewport made a vertical scrollbar appear, which narrowed the page and nudged
the centered board sideways — a small but distracting jump.

Set scrollbar-gutter: stable on the root so the gutter is always reserved. The
page keeps its width and position whether or not the scrollbar is showing.
Move up/down reordered against the raw widgets array. Archived widgets are
kept in that array (just hidden), and they are not guaranteed to sit after the
active ones — adding a widget after archiving one leaves it past the archived
entry. In that case the array neighbor of an active widget was the hidden
archived widget, so swapping with it changed nothing visible: Move up/down
appeared to do nothing.

Replace moveWidget with moveActiveWidget, which reorders a widget relative to
its visible (non-archived) neighbor, stepping over any interleaved archived
widgets. Cover the regression with a unit test and an e2e test that archives a
widget, adds another, and moves it up.
The chime was a single global toggle, so it was all-or-nothing across every
timer. Move it onto the timer itself: each timer has an optional `chime` flag,
set by a "Chime when it ends" switch in that timer's dialog, so you can have a
few timers that chime while the rest stay silent.

Remove the global chimeOnTimerEnd setting and its Options row, and drop the
timerChime prop that threaded the global value down through the board to each
timer; TimerBody now reads the flag from its own widget. The flag defaults off,
keeping sound opt-in, and survives the timer settling at zero. Settings
normalization no longer carries the old field (it is simply ignored on read).
While dragging a card toward the "Drag here to archive" zone, it jumped back to
its original slot the moment the cursor reached the zone. The sortable items
translate to follow the pointer, but the archive/restore drop zones sit outside
the sortable list, so once the pointer was over one the strategy reset the
active card's transform to zero — leaving it parked in its slot while the cursor
was elsewhere.

Render the lifted card in a DragOverlay portal that tracks the cursor
regardless of what it is over, and leave the in-list card behind as a dimmed
placeholder. The overlay now follows the pointer all the way onto the archive
and restore zones. Drop animation is disabled so archiving (which removes the
card) doesn't animate toward a slot that no longer exists.
@danielchalmers danielchalmers merged commit 22963d4 into main Jun 26, 2026
5 checks passed
@danielchalmers danielchalmers deleted the claude/elastic-chatelet-380dcb branch June 26, 2026 18:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant