Resize glitches on calendar widget (aspect ratio + odd sizes) #18

Open
opened 2026-04-20 15:23:57 +00:00 by mahmoud · 4 comments
Owner

Calendar widget loses aspect ratio during resize drag (self-corrects on release), and renders broken at odd sizes.

Calendar widget loses aspect ratio during resize drag (self-corrects on release), and renders broken at odd sizes.
Member

Implementation Spec for Issue #18

Objective

Make the calendar widget render correctly both while the resize is in progress (no aspect-ratio distortion of children) and at any aspect ratio allowed by the transformer (no row/column overlap at short heights or narrow widths).

Root-cause analysis

  • Distortion during drag: the Konva transformer applies non-uniform scaleX/scaleY to the calendar Group during a resize drag. Every child (bg, header, date text, grid lines) is visually stretched/squished until transformend, where WhiteboardObjects.applyTransform resets scale to 1 and WhiteboardCalendar.redraw rebuilds the layout at the new width/height. The "self-corrects on release" effect the user reported is exactly this deferred redraw.
  • Broken rendering at odd sizes: with keepRatio: false in the transformer and Math.max(240, …) / Math.max(200, …) as the only clamp in objects.js:1041-1042, the month view (calendar.js:234) computes cellH = (contentH - 20) / 6. When the user stretches the calendar very wide and short, contentH becomes small enough that cellH is smaller than the 11 px date text, so all 5-6 week rows collapse on top of each other. The minimum clamp is also not view-aware: month needs a tall-enough box to show 6 weeks; day view needs even more for 14 hour rows; week view wants width.

Requirements

  • During an active calendar resize the widget must redraw live at every transformer tick, so the user sees the final layout as they drag instead of a stretched preview that snaps on release.
  • The min-size clamp for calendars must be view-aware so the active view always has enough room to render without row/column overlap.
  • Transformer must still allow free (non-uniform) resizing above those minimums; holding Shift still toggles keep-ratio as today.
  • No changes to the persistence format, the _calState shape, undo/redo, nav arrows, or multi-user sync.
  • The live-redraw hook must be scoped to calendars specifically; do not introduce expensive per-tick redraws for other object types.

Files to Modify

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js — add a transformer.on('transform', ...) handler that, for each calendar node currently being transformed, live-applies scaled dimensions to the bg and calls WhiteboardCalendar.redraw(node) so the user sees correct layout during the drag.
  • crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js — replace the hardcoded Math.max(240, …) / Math.max(200, …) in the calendar branch of applyTransform with view-aware minimums read from a helper in calendar.js. Keep the same code path for transformend so both live redraw (Step 1) and final redraw use the same clamp.
  • crates/hero_whiteboard_ui/static/web/js/whiteboard/calendar.js — expose a getMinSize(viewMode) helper and harden renderMonthView so cellH never drops below the text height (defensive floor so a manual bg.width/height write from elsewhere cannot reintroduce the overlap).

Implementation Plan

Step 1: Live redraw during calendar transform

Files: crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js, crates/hero_whiteboard_ui/static/web/js/whiteboard/calendar.js

  • Add a transform event listener on the transformer (next to the existing transformstart / transformend handlers) that iterates the transformer's nodes and, for each one named object calendar:
    • Read scaleX / scaleY on the node.
    • Find .bg, compute newW = bg.width() * scaleX, newH = bg.height() * scaleY, clamp to the view-aware minimum (see Step 2).
    • Call bg.width(newW) / bg.height(newH), reset node.scaleX(1) / node.scaleY(1) so the next transform event starts from the clamped box instead of compounding the scale.
    • Call WhiteboardCalendar.redraw(node) so children re-lay-out at the live dimensions.
    • Call transformer.forceUpdate() at the end of the iteration so the outline tracks the clamped box.
  • In calendar.js, ensure redraw is cheap enough to call ~60 Hz. renderCalendar already does group.destroyChildren() + a small number of Konva node creations; this is acceptable. Add a name: 'bg' to the bg creation if not already present (it is — line 76). No changes needed here for this step beyond defining getMinSize (Step 2).

Dependencies: Step 2 for the helper, but they can be landed in the same pass; order the edits as Step 2 first so getMinSize is available when Step 1 references it.

Step 2: View-aware minimum size clamp

Files: crates/hero_whiteboard_ui/static/web/js/whiteboard/calendar.js, crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js

  • In calendar.js, add a getMinSize(viewMode) function returning { minW, minH }:
    • month: { minW: 280, minH: 260 } — 7 day columns + 6 week rows need at least this to stay readable.
    • week: { minW: 320, minH: 220 } — 7 day columns + 12 hour rows.
    • day: { minW: 200, minH: 280 } — single-column day view with 14 hour rows.
    • Default (unknown): { minW: 240, minH: 200 }.
  • Export getMinSize from the module return.
  • In objects.js, replace the calendar branch of applyTransform:
    } else if (type === 'calendar') {
        var viewMode = node._calState ? node._calState.viewMode : 'month';
        var min = WhiteboardCalendar.getMinSize(viewMode);
        if (bg) {
            bg.width(Math.max(min.minW, Math.round(bg.width() * scaleX)));
            bg.height(Math.max(min.minH, Math.round(bg.height() * scaleY)));
        }
        node.scaleX(1);
        node.scaleY(1);
        if (WhiteboardCalendar && WhiteboardCalendar.redraw) {
            WhiteboardCalendar.redraw(node);
        }
    }
    
  • Reuse the same helper from the Step 1 transform handler so live-drag and release both clamp to the same numbers.

Dependencies: none (lands before Step 1 to satisfy its reference).

Step 3: Defensive floor on month-view row height

Files: crates/hero_whiteboard_ui/static/web/js/whiteboard/calendar.js

  • In renderMonthView (calendar.js:234), clamp cellH to a minimum of about 14 px (the 11 px font size + breathing room) so a programmatic bg.height set — through persisted state loaded from the server, a legacy board, or any code path that bypasses applyTransform — cannot reintroduce the overlapping-rows bug seen in the attached video.
    var cellH = Math.max(14, (contentH - 20) / 6);
    
  • Apply the same defensive floor on hourH in renderWeekView and renderDayView (Math.max(10, ...)), so time rows can't collapse either.
  • No behavioural change at normal sizes (the formula already produces values far above 14 at the view-aware minimums from Step 2); this is strictly a safety net.

Dependencies: none.

Acceptance Criteria

  • Dragging any transformer anchor on a calendar shows the grid, header, and date labels laid out correctly at each frame — no horizontally/vertically stretched text, no squished cells, no "snap" on release.
  • Calendar cannot be resized below the view-aware minimum (month: 280×260, week: 320×220, day: 200×280). The outline stops at the min box and the boundBoxFunc in the transformer is not triggered to abort the drag.
  • Switching the view mode in the properties panel then resizing uses the new view's minimums immediately.
  • At the minimum size the month grid shows all 6 possible week rows without overlap; the week and day views show their hour rows without overlap.
  • Reloading a board with a persisted calendar under the old minimums does not crash and renders with the defensive Math.max(14, …) floor engaged (no overlapping text).
  • No regression in: nav arrows, Prev/Today/Next buttons in the properties panel, double-click view cycling, undo/redo, multi-user sync, drag to reposition the calendar, transformer resize of every other object type.

Notes

  • Why live redraw rather than a transformer keepRatio: true override? Because the reported bug is that users want the calendar to take different aspect ratios — it is the preview during drag that misleads, not the final shape. Live redraw keeps the existing free-resize flexibility.
  • The transform event fires very frequently. The calendar redraw rebuilds a few dozen Konva nodes; on modern hardware this is <1 ms and comfortably 60 Hz-capable. If profiling shows it stuttering on low-end hardware, add a simple rAF throttle inside the handler.
  • Step 3 is the only step that touches rendering math; Steps 1 and 2 only touch the resize control flow. This keeps the blast radius of the change small.
## Implementation Spec for Issue #18 ### Objective Make the calendar widget render correctly both while the resize is in progress (no aspect-ratio distortion of children) and at any aspect ratio allowed by the transformer (no row/column overlap at short heights or narrow widths). ### Root-cause analysis - **Distortion during drag**: the Konva transformer applies non-uniform `scaleX`/`scaleY` to the calendar Group during a resize drag. Every child (bg, header, date text, grid lines) is visually stretched/squished until `transformend`, where `WhiteboardObjects.applyTransform` resets `scale` to 1 and `WhiteboardCalendar.redraw` rebuilds the layout at the new width/height. The "self-corrects on release" effect the user reported is exactly this deferred redraw. - **Broken rendering at odd sizes**: with `keepRatio: false` in the transformer and `Math.max(240, …) / Math.max(200, …)` as the only clamp in [objects.js:1041-1042](crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js#L1041), the month view ([calendar.js:234](crates/hero_whiteboard_ui/static/web/js/whiteboard/calendar.js#L234)) computes `cellH = (contentH - 20) / 6`. When the user stretches the calendar very wide and short, `contentH` becomes small enough that `cellH` is smaller than the 11 px date text, so all 5-6 week rows collapse on top of each other. The minimum clamp is also not view-aware: month needs a tall-enough box to show 6 weeks; day view needs even more for 14 hour rows; week view wants width. ### Requirements - During an active calendar resize the widget must redraw live at every transformer tick, so the user sees the final layout as they drag instead of a stretched preview that snaps on release. - The min-size clamp for calendars must be view-aware so the active view always has enough room to render without row/column overlap. - Transformer must still allow free (non-uniform) resizing above those minimums; holding Shift still toggles keep-ratio as today. - No changes to the persistence format, the `_calState` shape, undo/redo, nav arrows, or multi-user sync. - The live-redraw hook must be scoped to calendars specifically; do not introduce expensive per-tick redraws for other object types. ### Files to Modify - `crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js` — add a `transformer.on('transform', ...)` handler that, for each calendar node currently being transformed, live-applies scaled dimensions to the bg and calls `WhiteboardCalendar.redraw(node)` so the user sees correct layout during the drag. - `crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js` — replace the hardcoded `Math.max(240, …) / Math.max(200, …)` in the `calendar` branch of `applyTransform` with view-aware minimums read from a helper in `calendar.js`. Keep the same code path for `transformend` so both live redraw (Step 1) and final redraw use the same clamp. - `crates/hero_whiteboard_ui/static/web/js/whiteboard/calendar.js` — expose a `getMinSize(viewMode)` helper and harden `renderMonthView` so `cellH` never drops below the text height (defensive floor so a manual bg.width/height write from elsewhere cannot reintroduce the overlap). ### Implementation Plan #### Step 1: Live redraw during calendar transform Files: `crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js`, `crates/hero_whiteboard_ui/static/web/js/whiteboard/calendar.js` - Add a `transform` event listener on the transformer (next to the existing `transformstart` / `transformend` handlers) that iterates the transformer's nodes and, for each one named `object calendar`: - Read `scaleX` / `scaleY` on the node. - Find `.bg`, compute `newW = bg.width() * scaleX`, `newH = bg.height() * scaleY`, clamp to the view-aware minimum (see Step 2). - Call `bg.width(newW)` / `bg.height(newH)`, reset `node.scaleX(1)` / `node.scaleY(1)` so the next `transform` event starts from the clamped box instead of compounding the scale. - Call `WhiteboardCalendar.redraw(node)` so children re-lay-out at the live dimensions. - Call `transformer.forceUpdate()` at the end of the iteration so the outline tracks the clamped box. - In `calendar.js`, ensure `redraw` is cheap enough to call ~60 Hz. `renderCalendar` already does `group.destroyChildren()` + a small number of Konva node creations; this is acceptable. Add a `name: 'bg'` to the bg creation if not already present (it is — line 76). No changes needed here for this step beyond defining `getMinSize` (Step 2). Dependencies: Step 2 for the helper, but they can be landed in the same pass; order the edits as Step 2 first so `getMinSize` is available when Step 1 references it. #### Step 2: View-aware minimum size clamp Files: `crates/hero_whiteboard_ui/static/web/js/whiteboard/calendar.js`, `crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js` - In `calendar.js`, add a `getMinSize(viewMode)` function returning `{ minW, minH }`: - `month`: `{ minW: 280, minH: 260 }` — 7 day columns + 6 week rows need at least this to stay readable. - `week`: `{ minW: 320, minH: 220 }` — 7 day columns + 12 hour rows. - `day`: `{ minW: 200, minH: 280 }` — single-column day view with 14 hour rows. - Default (unknown): `{ minW: 240, minH: 200 }`. - Export `getMinSize` from the module return. - In `objects.js`, replace the `calendar` branch of `applyTransform`: ```js } else if (type === 'calendar') { var viewMode = node._calState ? node._calState.viewMode : 'month'; var min = WhiteboardCalendar.getMinSize(viewMode); if (bg) { bg.width(Math.max(min.minW, Math.round(bg.width() * scaleX))); bg.height(Math.max(min.minH, Math.round(bg.height() * scaleY))); } node.scaleX(1); node.scaleY(1); if (WhiteboardCalendar && WhiteboardCalendar.redraw) { WhiteboardCalendar.redraw(node); } } ``` - Reuse the same helper from the Step 1 transform handler so live-drag and release both clamp to the same numbers. Dependencies: none (lands before Step 1 to satisfy its reference). #### Step 3: Defensive floor on month-view row height Files: `crates/hero_whiteboard_ui/static/web/js/whiteboard/calendar.js` - In `renderMonthView` ([calendar.js:234](crates/hero_whiteboard_ui/static/web/js/whiteboard/calendar.js#L234)), clamp `cellH` to a minimum of about `14` px (the 11 px font size + breathing room) so a programmatic `bg.height` set — through persisted state loaded from the server, a legacy board, or any code path that bypasses `applyTransform` — cannot reintroduce the overlapping-rows bug seen in the attached video. ```js var cellH = Math.max(14, (contentH - 20) / 6); ``` - Apply the same defensive floor on `hourH` in `renderWeekView` and `renderDayView` (`Math.max(10, ...)`), so time rows can't collapse either. - No behavioural change at normal sizes (the formula already produces values far above 14 at the view-aware minimums from Step 2); this is strictly a safety net. Dependencies: none. ### Acceptance Criteria - [ ] Dragging any transformer anchor on a calendar shows the grid, header, and date labels laid out correctly at each frame — no horizontally/vertically stretched text, no squished cells, no "snap" on release. - [ ] Calendar cannot be resized below the view-aware minimum (month: 280×260, week: 320×220, day: 200×280). The outline stops at the min box and the `boundBoxFunc` in the transformer is not triggered to abort the drag. - [ ] Switching the view mode in the properties panel then resizing uses the new view's minimums immediately. - [ ] At the minimum size the month grid shows all 6 possible week rows without overlap; the week and day views show their hour rows without overlap. - [ ] Reloading a board with a persisted calendar under the old minimums does not crash and renders with the defensive `Math.max(14, …)` floor engaged (no overlapping text). - [ ] No regression in: nav arrows, Prev/Today/Next buttons in the properties panel, double-click view cycling, undo/redo, multi-user sync, drag to reposition the calendar, transformer resize of every other object type. ### Notes - Why live redraw rather than a transformer `keepRatio: true` override? Because the reported bug is that users *want* the calendar to take different aspect ratios — it is the *preview* during drag that misleads, not the final shape. Live redraw keeps the existing free-resize flexibility. - The `transform` event fires very frequently. The calendar `redraw` rebuilds a few dozen Konva nodes; on modern hardware this is <1 ms and comfortably 60 Hz-capable. If profiling shows it stuttering on low-end hardware, add a simple rAF throttle inside the handler. - Step 3 is the only step that touches rendering math; Steps 1 and 2 only touch the resize control flow. This keeps the blast radius of the change small.
Member

Test Results

  • cargo check --workspace: pass
  • cargo clippy -- -D warnings: pass
  • cargo fmt --check: pass
  • cargo test --workspace --lib: 0 passed / 0 failed (0)

Note: changes are entirely in vanilla JS (frontend); UI behavior must be verified manually in the browser.

## Test Results - `cargo check --workspace`: pass - `cargo clippy -- -D warnings`: pass - `cargo fmt --check`: pass - `cargo test --workspace --lib`: 0 passed / 0 failed (0) Note: changes are entirely in vanilla JS (frontend); UI behavior must be verified manually in the browser.
Member

Implementation Summary

All three steps from the approved spec are implemented.

Changes

crates/hero_whiteboard_ui/static/web/js/whiteboard/calendar.js

  • Added getMinSize(viewMode) returning per-view minimums: month 280x260, week 320x220, day 200x280 (default 240x200). Exported from the module.
  • Defensive floors to prevent row/column collapse even if a legacy or out-of-band write produces an undersized bg: cellH = Math.max(14, (contentH - 20) / 6) in the month view; hourH = Math.max(10, ...) in the week and day views.

crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js

  • applyTransform calendar branch now clamps to the view-aware minimum returned by WhiteboardCalendar.getMinSize(node._calState.viewMode) instead of the hardcoded 240x200.

crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js

  • Added a transformer.on('transform', ...) handler that fires continuously during a resize drag. For calendar nodes it applies the current scale to the bg, clamps to the view-aware minimum, resets the group scale to 1, and calls WhiteboardCalendar.redraw(node) so children re-lay-out on every tick. transformer.forceUpdate() after so the outline hugs the clamped box. Scoped to calendars only; other object types are unaffected.

Test Results

  • cargo check --workspace: pass
  • cargo clippy --workspace -- -D warnings: pass
  • cargo fmt --check: pass
  • cargo test --workspace --lib: 0 passed / 0 failed (no lib tests in the workspace)

Changes are entirely vanilla JS (frontend). UI behavior needs manual verification in the browser.

Manual verification checklist

  • Dragging any transformer anchor on a calendar redraws the grid live; no stretched cells, no snap-on-release.
  • Calendar cannot be resized below the view-aware minimum (month 280x260, week 320x220, day 200x280).
  • Switching view mode in the properties panel then resizing uses the new view's minimums immediately.
  • At the minimum size the month grid shows all six possible week rows without overlap; week and day views show their hour rows without overlap.
  • Nav arrows, Prev/Today/Next buttons, double-click view cycling, undo/redo, and multi-user sync still work.
  • No regression in resizing of other object types (sticky, document, kanban, mindmap, webframe, shape, image, frame, text).
## Implementation Summary All three steps from the approved spec are implemented. ### Changes **`crates/hero_whiteboard_ui/static/web/js/whiteboard/calendar.js`** - Added `getMinSize(viewMode)` returning per-view minimums: month 280x260, week 320x220, day 200x280 (default 240x200). Exported from the module. - Defensive floors to prevent row/column collapse even if a legacy or out-of-band write produces an undersized bg: `cellH = Math.max(14, (contentH - 20) / 6)` in the month view; `hourH = Math.max(10, ...)` in the week and day views. **`crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js`** - `applyTransform` calendar branch now clamps to the view-aware minimum returned by `WhiteboardCalendar.getMinSize(node._calState.viewMode)` instead of the hardcoded 240x200. **`crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js`** - Added a `transformer.on('transform', ...)` handler that fires continuously during a resize drag. For calendar nodes it applies the current scale to the bg, clamps to the view-aware minimum, resets the group scale to 1, and calls `WhiteboardCalendar.redraw(node)` so children re-lay-out on every tick. `transformer.forceUpdate()` after so the outline hugs the clamped box. Scoped to calendars only; other object types are unaffected. ### Test Results - `cargo check --workspace`: pass - `cargo clippy --workspace -- -D warnings`: pass - `cargo fmt --check`: pass - `cargo test --workspace --lib`: 0 passed / 0 failed (no lib tests in the workspace) Changes are entirely vanilla JS (frontend). UI behavior needs manual verification in the browser. ### Manual verification checklist - [ ] Dragging any transformer anchor on a calendar redraws the grid live; no stretched cells, no snap-on-release. - [ ] Calendar cannot be resized below the view-aware minimum (month 280x260, week 320x220, day 200x280). - [ ] Switching view mode in the properties panel then resizing uses the new view's minimums immediately. - [ ] At the minimum size the month grid shows all six possible week rows without overlap; week and day views show their hour rows without overlap. - [ ] Nav arrows, Prev/Today/Next buttons, double-click view cycling, undo/redo, and multi-user sync still work. - [ ] No regression in resizing of other object types (sticky, document, kanban, mindmap, webframe, shape, image, frame, text).
Member

Pull request opened: #24

This PR implements the changes discussed in this issue.

Pull request opened: https://forge.ourworld.tf/lhumina_code/hero_whiteboard/pulls/24 This PR implements the changes discussed in this issue.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
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
lhumina_code/hero_whiteboard#18
No description provided.