Polish sweep: title clipping, drag handles, dialog UX, per-timer chime, and reorder/archive fixes#77
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)
h1painted its fill only within a tightline-heightline box, shearing the tail off the “y”. Added a touch of vertical padding so glyphs render in full.clip-pathnow leaves only the padded outer ring as the handle.requestSubmit, so required-field validation still applies), which feels more fluid.scrollbar-gutter: stableso revealing the archive or overflowing the viewport no longer pops a scrollbar that shifts the centered board sideways.moveActiveWidgetreorders against the visible (non-archived) neighbor.DragOverlayportal 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.)
compare-title.pngcompare-card-header.pngcompare-timer-dialog.pngcompare-settings.pngafter-archive-drag.png