Kanban: resize minimum is too tight — cannot shrink further #50

Open
opened 2026-04-22 11:26:10 +00:00 by AhmedHanafy725 · 4 comments
Member

Problem

When resizing a kanban via the transformer handles, the board stops shrinking before it looks small enough. The property-panel sliders also have conservative minimums.

Evidence

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/properties.js: prop-kanban-col-width slider min="140", prop-kanban-card-height slider min="30".
  • In the resize path (tools.js applyTransform for kanban type), any clamp is set against these same bounds.

Fix

  • Lower the clamps. Suggested new minimums: colWidth min 100, cardHeight min 22. Update both the sliders in properties.js and the clamp in applyTransform (tools.js) so transformer-resize and slider match.
  • Verify the '+ Add card' text still fits inside a 100-wide column (it renders center-aligned with padding 6, so colW - 12 = 88px of text width — sufficient for '+ Add card').
## Problem When resizing a kanban via the transformer handles, the board stops shrinking before it looks small enough. The property-panel sliders also have conservative minimums. ## Evidence - `crates/hero_whiteboard_ui/static/web/js/whiteboard/properties.js`: `prop-kanban-col-width` slider `min="140"`, `prop-kanban-card-height` slider `min="30"`. - In the resize path (tools.js `applyTransform` for kanban type), any clamp is set against these same bounds. ## Fix - Lower the clamps. Suggested new minimums: `colWidth min 100`, `cardHeight min 22`. Update both the sliders in `properties.js` and the clamp in `applyTransform` (tools.js) so transformer-resize and slider match. - Verify the '+ Add card' text still fits inside a 100-wide column (it renders center-aligned with padding 6, so `colW - 12 = 88`px of text width — sufficient for '+ Add card').
Author
Member

Implementation Spec for Issue #50

Objective

Fix two bugs in kanban resize behavior:

  1. Minimum clamps are too high. Both the transformer-resize path and the property-panel sliders refuse to shrink the kanban below a size that still feels large on screen. Lower both to colWidth min 100 and cardHeight min 22.

  2. Snapback on release. When the user resizes via the transformer handles and releases the mouse, the kanban visibly jumps to a different size than what was shown during the drag. Root cause: the kanban has fixed constants in its layout (padding, header, card-gap, "+ Column" button slot, footer) that are uniformly scaled by Konva during drag but revert to their unscaled constant values on release, while only colWidth/cardHeight are scaled into state by applyTransform. Fix by doing a live redraw during the transform event (mirroring the existing calendar pattern) so on-release dimensions equal on-drag dimensions.

Requirements

  • Transformer-resize of a kanban does not visibly jump on mouse release (pixel-perfect continuity between the last transform frame and the post-transformend render, within normal rounding).
  • Kanban can be shrunk to colWidth = 100 and cardHeight = 22 via both the transformer and the sliders.
  • Slider minimums match the applyTransform clamp (100 for column width, 22 for card height).
  • The + Add card button remains legible inside a 100-wide column.
  • No other object type changes behavior.
  • No refactoring, no new options/config, no history plumbing beyond what's already present.

Files to Modify/Create

Modify (no new files):

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/properties.js — lower slider min attributes.
  • crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js — update clamps in the kanban branch of applyTransform.
  • crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js — extend the existing transform live-redraw handler to also cover kanban, eliminating the snapback.

Implementation Plan

Step 1 — Lower slider minimums in properties panel

  • Files: properties.js
  • Subtasks:
    • Around line 445: change min="140" to min="100" on the prop-kanban-col-width range input.
    • Around line 453: change min="30" to min="22" on the prop-kanban-card-height range input.
    • Do not touch max values or defaults. Do not touch the input handlers (around lines 872-897); they already parseInt and assign directly, no bounds logic lives there.
  • Dependencies: none.

Step 2 — Update clamp in applyTransform (kanban branch)

  • Files: objects.js
  • Subtasks:
    • Around line 1029: change Math.max(80, ...) to Math.max(100, ...) for colWidth.
    • Around line 1030: change Math.max(24, ...) to Math.max(22, ...) for cardHeight.
    • Keep Math.round(...). Do not switch to Math.floor or Math.ceil.
  • Dependencies: none.

Step 3 — Add kanban to the live-redraw transform handler

  • Files: tools.js
  • Subtasks:
    • Extend the existing transformer.on('transform', ...) handler (around lines 115-138) which currently live-redraws calendar nodes.
    • For each node where node.hasName('kanban') is true and WhiteboardKanban is defined and (scaleX !== 1 || scaleY !== 1):
      1. Read sx = node.scaleX(), sy = node.scaleY().
      2. Read current st = node._kanbanState. If missing, skip.
      3. Compute newColW = Math.max(100, Math.round((st.colWidth || 200) * sx)).
      4. Compute newCardH = Math.max(22, Math.round((st.cardHeight || 44) * sy)).
      5. Write st.colWidth = newColW; st.cardHeight = newCardH;.
      6. Reset node.scaleX(1); node.scaleY(1);.
      7. Call WhiteboardKanban.redraw(node).
      8. Set the existing anyLiveRedrawn flag (already present; drives transformer.forceUpdate() at the bottom).
    • Keep the calendar branch untouched so both types coexist.
    • applyTransform in objects.js already has an early exit when scale is ~1, so once live redraw has reset scale to 1, applyTransform becomes a no-op for kanban — no double-apply.
  • Dependencies: Steps 1 and 2 (so the clamp value 100/22 used in the live redraw matches the clamp used in applyTransform and the slider minimums).

Acceptance Criteria

  • Slider prop-kanban-col-width min attribute is 100 in properties.js.
  • Slider prop-kanban-card-height min attribute is 22 in properties.js.
  • applyTransform kanban branch clamp is Math.max(100, ...) on colWidth and Math.max(22, ...) on cardHeight (objects.js).
  • In tools.js, the transform event handler live-redraws kanban nodes (scales colWidth/cardHeight into state, resets group scale to 1, calls WhiteboardKanban.redraw), matching the existing calendar pattern.
  • Manual: select a kanban, drag a transformer corner/edge handle, release. The final rendered kanban size matches what was visible immediately before release (no visible jump). Verified for shrink and enlarge, and for X, Y, and corner drags.
  • Manual: sliders can reach 100 (col width) and 22 (card height); the board re-renders correctly at those minima.
  • Manual: + Add card text remains visible and centered inside a 100-wide column.
  • Manual: no regression on other object types during transform and transformend.
  • Persistence: after a resize-via-transformer, reloading the page shows the same kanban dimensions (confirms WhiteboardSync.onUpdate still fires from transformend — live redraw must not suppress it).

Notes

Snapback root cause (beyond what the issue body diagnosed).

During a transform drag, Konva applies the group's scaleX/scaleY uniformly to every child. The kanban group's rendered width and height are computed in kanban.js renderKanban as:

  • totalW = columns.length * (colW + padding) + padding + (colW/2 + padding)(N + 0.5)*colW + (N + 2)*padding (N = column count, padding = 8).
  • totalH = headerH + 8 + max(K,1)*(cardH + cardGap) + 5094 + 6K + K*cardH (K = max cards per column, headerH = 36, cardGap = 6).

Both formulas have a scale-dependent term ((N+0.5)*colW, K*cardH) and a scale-independent constant term ((N+2)*padding, 94 + 6K).

While the user drags, Konva scales both terms uniformly: visible width = scaleX * totalW_old. On release, applyTransform only multiplies colW/cardH by scale, rounds, writes into state, resets group scale to 1, and calls renderKanban. renderKanban recomputes totalW with the new colW but the padding/header constants remain unscaled. The mismatch is approximately (constant_term) * (scaleX - 1):

  • Enlarging (scaleX > 1): on-release render is smaller than the drag preview by (N+2)*padding*(scaleX - 1) px (and analogously on Y).
  • Shrinking (scaleX < 1): on-release render is larger than the drag preview by the same factor.

This is what the user perceived as "it decreases a bit" on release after enlarging. Math.round contributes at most ±0.5 px per axis — not the source.

Fix: live redraw. By converting scale to state on every transform tick and resetting the group's scale to 1, the next tick starts from the newly-rendered box whose constants are already in place. The live-drawn dimensions therefore track the state dimensions exactly, so the transformend render equals the last transform render — no visible jump.

Why not instead scale the padding/header in applyTransform? Those constants are style, not data: scaling the 8-px card padding or the 36-px header text height per-resize would bake visual drift into persisted state and compound across resizes. The live-redraw approach keeps padding/headerH/cardGap as constants and the authoritative state stays two scalars (colWidth, cardHeight).

Why Math.round not Math.floor. The issue body speculated about Math.floor dropping fractional pixels. The code actually uses Math.round, which rounds to nearest — its bias is ≤0.5 px per axis per resize. The dominant snapback term is the fixed constants, not rounding. Leave Math.round as-is.

Why the two sides need matching clamps. If the slider min were 100 but applyTransform clamped at 80, a transformer-drag could leave the kanban below the slider's reachable floor and the slider would render "pinned" at 100 while state showed 80 — visually inconsistent. Using 100/22 on both sides keeps them in sync. The existing 80/24 clamp in applyTransform is intentionally moved to 100/22 for this reason (22 is a lowering from 24, 100 is a raising from 80 — both land at the spec's target values, matching the slider minimums).

Scope. No changes to the transformer's boundBoxFunc (that guard is on the visual transformer box, not kanban units, and with 100-wide columns the visual min is far above that floor). No changes to defaults, column colors, history snapshots, sync, or any other code path.

## Implementation Spec for Issue #50 ### Objective Fix two bugs in kanban resize behavior: 1. **Minimum clamps are too high.** Both the transformer-resize path and the property-panel sliders refuse to shrink the kanban below a size that still feels large on screen. Lower both to `colWidth min 100` and `cardHeight min 22`. 2. **Snapback on release.** When the user resizes via the transformer handles and releases the mouse, the kanban visibly jumps to a different size than what was shown during the drag. Root cause: the kanban has fixed constants in its layout (padding, header, card-gap, "+ Column" button slot, footer) that are uniformly scaled by Konva during drag but revert to their unscaled constant values on release, while only `colWidth`/`cardHeight` are scaled into state by `applyTransform`. Fix by doing a live redraw during the transform event (mirroring the existing calendar pattern) so on-release dimensions equal on-drag dimensions. ### Requirements - Transformer-resize of a kanban does not visibly jump on mouse release (pixel-perfect continuity between the last `transform` frame and the post-`transformend` render, within normal rounding). - Kanban can be shrunk to `colWidth = 100` and `cardHeight = 22` via both the transformer and the sliders. - Slider minimums match the `applyTransform` clamp (`100` for column width, `22` for card height). - The `+ Add card` button remains legible inside a 100-wide column. - No other object type changes behavior. - No refactoring, no new options/config, no history plumbing beyond what's already present. ### Files to Modify/Create Modify (no new files): - `crates/hero_whiteboard_ui/static/web/js/whiteboard/properties.js` — lower slider `min` attributes. - `crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js` — update clamps in the kanban branch of `applyTransform`. - `crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js` — extend the existing `transform` live-redraw handler to also cover kanban, eliminating the snapback. ### Implementation Plan **Step 1 — Lower slider minimums in properties panel** - Files: `properties.js` - Subtasks: - Around line 445: change `min="140"` to `min="100"` on the `prop-kanban-col-width` range input. - Around line 453: change `min="30"` to `min="22"` on the `prop-kanban-card-height` range input. - Do not touch `max` values or defaults. Do not touch the input handlers (around lines 872-897); they already `parseInt` and assign directly, no bounds logic lives there. - Dependencies: none. **Step 2 — Update clamp in `applyTransform` (kanban branch)** - Files: `objects.js` - Subtasks: - Around line 1029: change `Math.max(80, ...)` to `Math.max(100, ...)` for `colWidth`. - Around line 1030: change `Math.max(24, ...)` to `Math.max(22, ...)` for `cardHeight`. - Keep `Math.round(...)`. Do not switch to `Math.floor` or `Math.ceil`. - Dependencies: none. **Step 3 — Add kanban to the live-redraw `transform` handler** - Files: `tools.js` - Subtasks: - Extend the existing `transformer.on('transform', ...)` handler (around lines 115-138) which currently live-redraws calendar nodes. - For each node where `node.hasName('kanban')` is true and `WhiteboardKanban` is defined and `(scaleX !== 1 || scaleY !== 1)`: 1. Read `sx = node.scaleX()`, `sy = node.scaleY()`. 2. Read current `st = node._kanbanState`. If missing, skip. 3. Compute `newColW = Math.max(100, Math.round((st.colWidth || 200) * sx))`. 4. Compute `newCardH = Math.max(22, Math.round((st.cardHeight || 44) * sy))`. 5. Write `st.colWidth = newColW; st.cardHeight = newCardH;`. 6. Reset `node.scaleX(1); node.scaleY(1);`. 7. Call `WhiteboardKanban.redraw(node)`. 8. Set the existing `anyLiveRedrawn` flag (already present; drives `transformer.forceUpdate()` at the bottom). - Keep the calendar branch untouched so both types coexist. - `applyTransform` in `objects.js` already has an early exit when scale is ~1, so once live redraw has reset scale to 1, `applyTransform` becomes a no-op for kanban — no double-apply. - Dependencies: Steps 1 and 2 (so the clamp value `100`/`22` used in the live redraw matches the clamp used in `applyTransform` and the slider minimums). ### Acceptance Criteria - [ ] Slider `prop-kanban-col-width` `min` attribute is `100` in `properties.js`. - [ ] Slider `prop-kanban-card-height` `min` attribute is `22` in `properties.js`. - [ ] `applyTransform` kanban branch clamp is `Math.max(100, ...)` on `colWidth` and `Math.max(22, ...)` on `cardHeight` (`objects.js`). - [ ] In `tools.js`, the `transform` event handler live-redraws kanban nodes (scales `colWidth`/`cardHeight` into state, resets group scale to 1, calls `WhiteboardKanban.redraw`), matching the existing calendar pattern. - [ ] Manual: select a kanban, drag a transformer corner/edge handle, release. The final rendered kanban size matches what was visible immediately before release (no visible jump). Verified for shrink and enlarge, and for X, Y, and corner drags. - [ ] Manual: sliders can reach `100` (col width) and `22` (card height); the board re-renders correctly at those minima. - [ ] Manual: `+ Add card` text remains visible and centered inside a 100-wide column. - [ ] Manual: no regression on other object types during transform and transformend. - [ ] Persistence: after a resize-via-transformer, reloading the page shows the same kanban dimensions (confirms `WhiteboardSync.onUpdate` still fires from `transformend` — live redraw must not suppress it). ### Notes **Snapback root cause (beyond what the issue body diagnosed).** During a transform drag, Konva applies the group's `scaleX`/`scaleY` uniformly to every child. The kanban group's rendered width and height are computed in `kanban.js` `renderKanban` as: - `totalW = columns.length * (colW + padding) + padding + (colW/2 + padding)` → `(N + 0.5)*colW + (N + 2)*padding` (N = column count, padding = 8). - `totalH = headerH + 8 + max(K,1)*(cardH + cardGap) + 50` → `94 + 6K + K*cardH` (K = max cards per column, headerH = 36, cardGap = 6). Both formulas have a **scale-dependent term** (`(N+0.5)*colW`, `K*cardH`) and a **scale-independent constant term** (`(N+2)*padding`, `94 + 6K`). While the user drags, Konva scales both terms uniformly: visible width = `scaleX * totalW_old`. On release, `applyTransform` only multiplies `colW`/`cardH` by scale, rounds, writes into state, resets group scale to 1, and calls `renderKanban`. `renderKanban` recomputes `totalW` with the new `colW` but the padding/header constants remain unscaled. The mismatch is approximately `(constant_term) * (scaleX - 1)`: - Enlarging (scaleX > 1): on-release render is **smaller** than the drag preview by `(N+2)*padding*(scaleX - 1)` px (and analogously on Y). - Shrinking (scaleX < 1): on-release render is **larger** than the drag preview by the same factor. This is what the user perceived as "it decreases a bit" on release after enlarging. `Math.round` contributes at most ±0.5 px per axis — not the source. **Fix: live redraw.** By converting scale to state on every `transform` tick and resetting the group's scale to 1, the next tick starts from the newly-rendered box whose constants are already in place. The live-drawn dimensions therefore track the state dimensions exactly, so the `transformend` render equals the last `transform` render — no visible jump. **Why not instead scale the padding/header in `applyTransform`?** Those constants are style, not data: scaling the 8-px card padding or the 36-px header text height per-resize would bake visual drift into persisted state and compound across resizes. The live-redraw approach keeps `padding`/`headerH`/`cardGap` as constants and the authoritative state stays two scalars (`colWidth`, `cardHeight`). **Why `Math.round` not `Math.floor`.** The issue body speculated about `Math.floor` dropping fractional pixels. The code actually uses `Math.round`, which rounds to nearest — its bias is ≤0.5 px per axis per resize. The dominant snapback term is the fixed constants, not rounding. Leave `Math.round` as-is. **Why the two sides need matching clamps.** If the slider `min` were `100` but `applyTransform` clamped at `80`, a transformer-drag could leave the kanban below the slider's reachable floor and the slider would render "pinned" at 100 while state showed 80 — visually inconsistent. Using `100`/`22` on both sides keeps them in sync. The existing `80`/`24` clamp in `applyTransform` is intentionally moved to `100`/`22` for this reason (`22` is a lowering from `24`, `100` is a raising from `80` — both land at the spec's target values, matching the slider minimums). **Scope.** No changes to the transformer's `boundBoxFunc` (that guard is on the visual transformer box, not kanban units, and with 100-wide columns the visual min is far above that floor). No changes to defaults, column colors, history snapshots, sync, or any other code path.
Author
Member

Test Results

JavaScript-only change; Rust workspace validated for regressions.

Check Result
cargo check --workspace pass
cargo test --workspace --lib pass (0 executed)
cargo clippy --workspace -- -D warnings pass
cargo fmt --check pass

The repository has no Rust tests exercising the JavaScript whiteboard modules, so the suite confirms regression safety for the Rust side. Manual verification against a running UI is required to confirm the snapback fix and the new minima; steps are listed in the spec's Acceptance Criteria.

## Test Results JavaScript-only change; Rust workspace validated for regressions. | Check | Result | |---|---| | `cargo check --workspace` | pass | | `cargo test --workspace --lib` | pass (0 executed) | | `cargo clippy --workspace -- -D warnings` | pass | | `cargo fmt --check` | pass | The repository has no Rust tests exercising the JavaScript whiteboard modules, so the suite confirms regression safety for the Rust side. Manual verification against a running UI is required to confirm the snapback fix and the new minima; steps are listed in the spec's Acceptance Criteria.
Author
Member

Implementation Summary

Two kanban resize bugs fixed:

  1. Shrink minimums lowered. Both property-panel sliders and the transformer clamp now agree on colWidth = 100 and cardHeight = 22, so the board shrinks further and the slider/transformer paths stay consistent.
  2. Resize snapback eliminated. The live-redraw handler that previously existed only for calendar now also covers kanban: on every transform tick, the current Konva scale is written into colWidth/cardHeight state, the group scale is reset to 1, and WhiteboardKanban.redraw re-renders with the new state. The transformend render therefore matches the last transform render instead of shrinking by the unscaled-padding delta.

Files changed

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/properties.js — slider min values 140→100 and 30→22.
  • crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.jsapplyTransform kanban clamp 80→100, 24→22.
  • crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.jstransform live-redraw extended to kanban; the existing calendar branch is untouched.

Why the snapback existed

kanban.js renderKanban computes totalW = (N+0.5)*colW + (N+2)*padding and totalH = 94 + 6K + K*cardH — both contain a scale-dependent term and a scale-independent constant (padding, headerH, cardGap, the + Column slot, and the footer). While the user drags, Konva scales every child uniformly, so the on-screen box grows or shrinks by scale * totalW_old. On release, the old implementation scaled only colW/cardH into state, reset scale to 1, and redrew — but the constant terms were no longer pre-scaled, so the rendered size differed from the drag preview by approximately constants * (scale − 1). The live-redraw fix repeats that scale-to-state conversion during the drag itself, so the final render is the same render that was already visible.

Test results

  • cargo check --workspace: pass
  • cargo test --workspace --lib: pass
  • cargo clippy --workspace -- -D warnings: pass
  • cargo fmt --check: pass

Notes / out of scope

  • No changes to the transformer's boundBoxFunc, kanban defaults, history/undo, sync, or any other object type.
  • Math.round (not Math.floor) is preserved in the clamp — its ±0.5 px bias is dwarfed by the former constant-term delta and was not the cause of the perceived shrink.
  • Manual QA against a running UI should confirm: (a) no visible jump on release for shrink/enlarge and corner/edge handles, (b) sliders reach the new minima, (c) + Add card text remains legible in a 100-wide column, (d) no regression on other object types.
## Implementation Summary Two kanban resize bugs fixed: 1. **Shrink minimums lowered.** Both property-panel sliders and the transformer clamp now agree on `colWidth = 100` and `cardHeight = 22`, so the board shrinks further and the slider/transformer paths stay consistent. 2. **Resize snapback eliminated.** The live-redraw handler that previously existed only for calendar now also covers kanban: on every `transform` tick, the current Konva scale is written into `colWidth`/`cardHeight` state, the group scale is reset to 1, and `WhiteboardKanban.redraw` re-renders with the new state. The `transformend` render therefore matches the last `transform` render instead of shrinking by the unscaled-padding delta. ### Files changed - `crates/hero_whiteboard_ui/static/web/js/whiteboard/properties.js` — slider `min` values 140→100 and 30→22. - `crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js` — `applyTransform` kanban clamp 80→100, 24→22. - `crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js` — `transform` live-redraw extended to kanban; the existing calendar branch is untouched. ### Why the snapback existed `kanban.js` `renderKanban` computes `totalW = (N+0.5)*colW + (N+2)*padding` and `totalH = 94 + 6K + K*cardH` — both contain a scale-dependent term and a scale-independent constant (`padding`, `headerH`, `cardGap`, the `+ Column` slot, and the footer). While the user drags, Konva scales every child uniformly, so the on-screen box grows or shrinks by `scale * totalW_old`. On release, the old implementation scaled only `colW`/`cardH` into state, reset scale to 1, and redrew — but the constant terms were no longer pre-scaled, so the rendered size differed from the drag preview by approximately `constants * (scale − 1)`. The live-redraw fix repeats that scale-to-state conversion during the drag itself, so the final render is the same render that was already visible. ### Test results - `cargo check --workspace`: pass - `cargo test --workspace --lib`: pass - `cargo clippy --workspace -- -D warnings`: pass - `cargo fmt --check`: pass ### Notes / out of scope - No changes to the transformer's `boundBoxFunc`, kanban defaults, history/undo, sync, or any other object type. - `Math.round` (not `Math.floor`) is preserved in the clamp — its ±0.5 px bias is dwarfed by the former constant-term delta and was not the cause of the perceived shrink. - Manual QA against a running UI should confirm: (a) no visible jump on release for shrink/enlarge and corner/edge handles, (b) sliders reach the new minima, (c) `+ Add card` text remains legible in a 100-wide column, (d) no regression on other object types.
Author
Member

Pull request opened: #63

This PR implements the changes discussed in this issue.

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