Calendar: show the live end time while dragging (create, resize, move), not only the start #418

Closed
opened 2026-09-29 09:09:51 +00:00 by kayg · 9 comments
Owner

Request (owner, 2026-09-29)

"When I am dragging something like this, I love how the starting point shows up (08:30). I would also like the opposite: the end time showing up as I keep dragging!"
The screenshot shows the Calendar TimeGrid drag-to-create ghost block. Its top label reads "08:30 …".

Build

While the User drags in the TimeGrid (drag to create, resize from the bottom edge, resize from the top edge, or move), the ghost shows live times:

  • Top label: the start time, as today.
  • End label: the end time, pinned to the bottom edge of the ghost and inside it. It updates on every snap step as the pointer moves. It uses the same type role, tabular numerals and accent ink as the start label.
  • Short ghost: when the ghost is too short for two labels (below about two label heights), merge them into one top label, "08:30 – 10:15". Never let the labels overlap or clip.
  • Remove the trailing "…" from the drag label. It reads as truncation, not as "still dragging".
  • Respect the User's 12/24-hour time format preference (#… formatPreferences) and the grid snap step.
  • Move drag: both labels update.
  • Resize from the top edge: the start label moves with the pointer, and the end label stays fixed.
  • Screen readers: announce the final range once, on drop (live region, polite). Do not announce every snap step.
  • Reduced motion: labels change without animation.
  • Touch: the same labels during long-press dragging on phones.

Reuse the existing ghost and label code in packages/ui/src/components/calendar/TimeGrid.svelte (and whatever renders the "08:30" today). Do not add a second label component.

Proof

  • Unit tests for the label text at the boundaries: short ghost, crossing midnight at the day's end, and 12-hour vs 24-hour.
  • A production-build e2e: drag to create from 08:30 to 10:15, and assert that both labels read correctly at intermediate pointer positions.
  • A frame strip or screen capture at 390, 820 and 1440 px, light and dark.
    Gates as in the preamble, plus bun run check and bun run test in apps/web.
## Request (owner, 2026-09-29) "When I am dragging something like this, I love how the starting point shows up (08:30). I would also like the opposite: the end time showing up as I keep dragging!" The screenshot shows the Calendar TimeGrid drag-to-create ghost block. Its top label reads "08:30 …". ## Build While the User drags in the TimeGrid (drag to create, resize from the bottom edge, resize from the top edge, or move), the ghost shows live times: - **Top label:** the start time, as today. - **End label:** the end time, pinned to the bottom edge of the ghost and inside it. It updates on every snap step as the pointer moves. It uses the same type role, tabular numerals and accent ink as the start label. - **Short ghost:** when the ghost is too short for two labels (below about two label heights), merge them into one top label, "08:30 – 10:15". Never let the labels overlap or clip. - Remove the trailing "…" from the drag label. It reads as truncation, not as "still dragging". - Respect the User's 12/24-hour time format preference (#… formatPreferences) and the grid snap step. - **Move drag:** both labels update. - **Resize from the top edge:** the start label moves with the pointer, and the end label stays fixed. - **Screen readers:** announce the final range once, on drop (live region, polite). Do not announce every snap step. - **Reduced motion:** labels change without animation. - **Touch:** the same labels during long-press dragging on phones. Reuse the existing ghost and label code in `packages/ui/src/components/calendar/TimeGrid.svelte` (and whatever renders the "08:30" today). Do not add a second label component. ## Proof - Unit tests for the label text at the boundaries: short ghost, crossing midnight at the day's end, and 12-hour vs 24-hour. - A production-build e2e: drag to create from 08:30 to 10:15, and assert that both labels read correctly at intermediate pointer positions. - A frame strip or screen capture at 390, 820 and 1440 px, light and dark. Gates as in the preamble, plus `bun run check` and `bun run test` in `apps/web`.
Author
Owner

Started in job/drag-end-418 from dev at 99ee1fc9bb. I am tracing the existing TimeGrid ghost, time-format helper, and calendar e2e setup before implementing live end labels.

Started in job/drag-end-418 from dev at 99ee1fc9bbb0b3dd1f2d5b35e15529d1d0b3633c. I am tracing the existing TimeGrid ghost, time-format helper, and calendar e2e setup before implementing live end labels.
Author
Owner

Finding: GridColumn currently renders the create range on one line, so the end is not pinned to the lower edge. TimeGrid only detects a bottom resize edge, and touch pointer movement is ignored for create and block drags. I am extending these existing paths; the polite announcement will happen only on pointer release.

Finding: GridColumn currently renders the create range on one line, so the end is not pinned to the lower edge. TimeGrid only detects a bottom resize edge, and touch pointer movement is ignored for create and block drags. I am extending these existing paths; the polite announcement will happen only on pointer release.
Author
Owner

Finding: GridColumn currently renders the create range on one .b-meta line, so the end is not pinned to the lower edge. TimeGrid only detects a bottom resize edge, and touch pointer movement is ignored for create and block drags. I am extending these existing paths; the polite announcement will happen only on pointer release.

Finding: GridColumn currently renders the create range on one .b-meta line, so the end is not pinned to the lower edge. TimeGrid only detects a bottom resize edge, and touch pointer movement is ignored for create and block drags. I am extending these existing paths; the polite announcement will happen only on pointer release.
Author
Owner

Decision where DESIGN is silent: a touch becomes a drag after 480 ms without movement, so shorter touches keep native scrolling. A CSS container query combines labels below 42 px (about two Caption lines); the ghost keeps one Caption line of minimum height. An end at the grid’s 24:00 boundary reads as 00:00 on the next day. The shared Intl clock formatter keeps 12-hour periods and 24-hour padding from the User’s preference.

Decision where DESIGN is silent: a touch becomes a drag after 480 ms without movement, so shorter touches keep native scrolling. A CSS container query combines labels below 42 px (about two Caption lines); the ghost keeps one Caption line of minimum height. An end at the grid’s 24:00 boundary reads as 00:00 on the next day. The shared Intl clock formatter keeps 12-hour periods and 24-hour padding from the User’s preference.
Author
Owner

Finding: the production calendar e2e reached the future-event case after the new create-range assertions passed, then failed because it referenced an undefined body variable at apps/web/e2e/calendar.mjs:568. I am fixing the selector to measure the rendered .tg-body region.

Finding: the production calendar e2e reached the future-event case after the new create-range assertions passed, then failed because it referenced an undefined body variable at apps/web/e2e/calendar.mjs:568. I am fixing the selector to measure the rendered .tg-body region.
Author
Owner

Finding: the production e2e passed the live create range and continued into app-password setup, where the assertion for the server URL matched five rendered locations. I am scoping it to the server URL code element so the check remains precise with the current setup cards.

Finding: the production e2e passed the live create range and continued into app-password setup, where the assertion for the server URL matched five rendered locations. I am scoping it to the server URL code element so the check remains precise with the current setup cards.
Author
Owner

Finding: the broad calendar production e2e passes the drag-create labels, move labels, and both resize-label checks, then times out at the existing Agenda #^agenda-deck selection wait. The focused drag e2e passes, including phone touch and the six requested screenshots. The Agenda deep-link behavior is outside this TimeGrid change and remains a known gap.

Finding: the broad calendar production e2e passes the drag-create labels, move labels, and both resize-label checks, then times out at the existing Agenda #^agenda-deck selection wait. The focused drag e2e passes, including phone touch and the six requested screenshots. The Agenda deep-link behavior is outside this TimeGrid change and remains a known gap.
Author
Owner

Final report — drag-end-418

Branch: job/drag-end-418
Head SHA: bf91c99160f0cd6d8992fe41be2d35c1fbdc223b

Built

  • Live start and end labels for create, move, and both resize directions. Short ranges use one combined label. The label text follows the saved 12/24-hour format and the grid snap.
  • A polite range announcement on drop, with no per-snap announcements; touch long press uses the same labels.
  • Rendered-scale pointer-to-time conversion so snaps stay aligned when the UI scale changes.

Files

  • packages/ui/src/components/calendar/model.ts
  • packages/ui/src/components/calendar/GridColumn.svelte
  • packages/ui/src/components/calendar/TimeGrid.svelte
  • apps/web/src/routes/calendar/[view]/[date]/+page.svelte
  • apps/web/src/lib/calendar/TimeGrid.svelte.test.ts
  • apps/web/e2e/calendar.mjs
  • apps/web/src/routes/settings/parts/AccountRow.svelte (carried through the local dev merge)

Decisions

  • Use one shared formatter for visible and announced ranges, with 24:00 displayed as next-day 00:00.
  • Combine labels at a 42 px ghost-height threshold, and keep a one-caption minimum ghost height.
  • Arm touch dragging after 480 ms so an early touch movement remains native scrolling.
  • Use the rendered day-column height for pointer math because pointer coordinates and DOM rectangles use rendered coordinates.

Gates

bun run check:

$ node scripts/check-type-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json
Text sizes use shared role tokens.
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/drag-end-418/apps/web
Getting Svelte diagnostics...

svelte-check found 0 errors and 0 warnings

bun run test:

 Test Files  125 passed (125)
      Tests  803 passed (803)
   Start at  14:17:09
   Duration  134.57s (transform 53%, environment 17%, import 15%, tests 11%, setup 4%)

Environment  |component| jsdom was created 39 times · 111.71s total, 26% of tracked time
             create it once per worker with pool: 'vmThreads' (keeps per-file isolation) or isolate: false (shares it across files)
             learn more: https://vitest.dev/guide/improving-performance#caching-between-reruns

Production build output:

✓ built in 48.66s
✓ built in 68ms
✓ built in 1m 34s
> Using @sveltejs/adapter-static

Focused production e2e output:

calendar drag e2e: live labels and touch drag passed

cargo fmt --check, node --check e2e/calendar.mjs, and git diff --check exited 0 with no output. Rust clippy/tests were not run because no Rust crate changed. The server binary for the production e2e built successfully:

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 121m 13s

Known gap

The broader calendar.mjs production flow passes the drag-create, move, and both resize-label checks, then times out at its Agenda #^agenda-deck selection wait. The focused production drag flow passes.

Screenshots

Cleanup completed: cargo clean removed 6940 files (4.3 GiB), and apps/web/build and the local target/tmp logs were removed.

## Final report — drag-end-418 **Branch:** `job/drag-end-418` **Head SHA:** `bf91c99160f0cd6d8992fe41be2d35c1fbdc223b` ### Built - Live start and end labels for create, move, and both resize directions. Short ranges use one combined label. The label text follows the saved 12/24-hour format and the grid snap. - A polite range announcement on drop, with no per-snap announcements; touch long press uses the same labels. - Rendered-scale pointer-to-time conversion so snaps stay aligned when the UI scale changes. ### Files - `packages/ui/src/components/calendar/model.ts` - `packages/ui/src/components/calendar/GridColumn.svelte` - `packages/ui/src/components/calendar/TimeGrid.svelte` - `apps/web/src/routes/calendar/[view]/[date]/+page.svelte` - `apps/web/src/lib/calendar/TimeGrid.svelte.test.ts` - `apps/web/e2e/calendar.mjs` - `apps/web/src/routes/settings/parts/AccountRow.svelte` (carried through the local `dev` merge) ### Decisions - Use one shared formatter for visible and announced ranges, with `24:00` displayed as next-day `00:00`. - Combine labels at a 42 px ghost-height threshold, and keep a one-caption minimum ghost height. - Arm touch dragging after 480 ms so an early touch movement remains native scrolling. - Use the rendered day-column height for pointer math because pointer coordinates and DOM rectangles use rendered coordinates. ### Gates `bun run check`: ```text $ node scripts/check-type-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json Text sizes use shared role tokens. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/drag-end-418/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` `bun run test`: ```text Test Files 125 passed (125) Tests 803 passed (803) Start at 14:17:09 Duration 134.57s (transform 53%, environment 17%, import 15%, tests 11%, setup 4%) Environment |component| jsdom was created 39 times · 111.71s total, 26% of tracked time create it once per worker with pool: 'vmThreads' (keeps per-file isolation) or isolate: false (shares it across files) learn more: https://vitest.dev/guide/improving-performance#caching-between-reruns ``` Production build output: ```text ✓ built in 48.66s ✓ built in 68ms ✓ built in 1m 34s > Using @sveltejs/adapter-static ``` Focused production e2e output: ```text calendar drag e2e: live labels and touch drag passed ``` `cargo fmt --check`, `node --check e2e/calendar.mjs`, and `git diff --check` exited 0 with no output. Rust clippy/tests were not run because no Rust crate changed. The server binary for the production e2e built successfully: ```text Finished `dev` profile [unoptimized + debuginfo] target(s) in 121m 13s ``` ### Known gap The broader `calendar.mjs` production flow passes the drag-create, move, and both resize-label checks, then times out at its Agenda `#^agenda-deck` selection wait. The focused production drag flow passes. ### Screenshots - [390 px, light](https://git.kayg.org/attachments/b4e48338-7a2a-452a-8dd6-f344eae029a5) · [390 px, dark](https://git.kayg.org/attachments/65ebe2bc-3b37-4059-968e-17e0477ef0b9) - [820 px, light](https://git.kayg.org/attachments/9c0a4c8b-684a-417b-84f5-e77413cda286) · [820 px, dark](https://git.kayg.org/attachments/b989bd8b-0e24-4730-bdc6-9b0b1d9ef419) - [1440 px, light](https://git.kayg.org/attachments/318e0e99-7efe-42fa-9a0a-ddd4e81b1690) · [1440 px, dark](https://git.kayg.org/attachments/c7fa9cb0-25c2-4f03-9245-b2b3890f7e6a) Cleanup completed: `cargo clean` removed 6940 files (4.3 GiB), and `apps/web/build` and the local `target/tmp` logs were removed.
Author
Owner

Merged into dev and pushed (43c1377c); orchestrator visual review passed. Deploy to calternal.cloud follows once the #356/#393 Apple GO/NO-GO clears.

Merged into dev and pushed (43c1377c); orchestrator visual review passed. Deploy to calternal.cloud follows once the #356/#393 Apple GO/NO-GO clears.
kayg closed this issue 2026-09-29 14:58:06 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#418
No description provided.