Kanban: card text cut off when longer than one line #52

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

Problem

Card text longer than one line does not display — cards use wrap: 'none', ellipsis: true, which truncates to a single line with ellipsis. Additionally, the inline editor is a single-line <input>, so multi-line content cannot even be entered via the UI.

Evidence

  • renderCard(): cardText is wrap: 'none', ellipsis: true.
  • editInline(): always uses document.createElement('input') with type='text'.

Fix

  • Change card text to wrap: wrap: 'word', drop ellipsis: true.
  • Grow the card slot height dynamically per-card based on measured text height: after creating the Konva.Text, read cardText.height() and set the card slot height accordingly; propagate through the card slot Y offsets so cards stack without overlapping.
  • Replace the single-line input in editInline with a <textarea> when the target is a card text node (keep single-line input for titles/column names). Shift+Enter inserts a newline, Enter or blur commits; Escape cancels.
## Problem Card text longer than one line does not display — cards use `wrap: 'none', ellipsis: true`, which truncates to a single line with ellipsis. Additionally, the inline editor is a single-line `<input>`, so multi-line content cannot even be entered via the UI. ## Evidence - `renderCard()`: cardText is `wrap: 'none', ellipsis: true`. - `editInline()`: always uses `document.createElement('input')` with `type='text'`. ## Fix - Change card text to wrap: `wrap: 'word'`, drop `ellipsis: true`. - Grow the card slot height dynamically per-card based on measured text height: after creating the Konva.Text, read `cardText.height()` and set the card slot height accordingly; propagate through the card slot Y offsets so cards stack without overlapping. - Replace the single-line input in `editInline` with a `<textarea>` when the target is a card text node (keep single-line input for titles/column names). Shift+Enter inserts a newline, Enter or blur commits; Escape cancels.
Author
Member

Implementation Spec for Issue #52

Objective

Fix kanban card text rendering so multi-line content displays correctly and allow multi-line entry in the inline editor. Card text must word-wrap instead of truncating with an ellipsis, the card slot height must grow per-card to accommodate wrapped text, and double-click/Edit must open a multi-line textarea (Shift+Enter = newline, Enter/blur = commit, Escape = cancel). Base cardHeight in state (controlled by slider + transformer resize) continues to act as the minimum slot height; wrapped text can push a given card taller without persisting that overflow into state.

Requirements

  1. Card text uses wrap: 'word', ellipsis: false (drop the ellipsis: true from renderCard). Text width stays at colW - 50 (preserves room for the 3-dots menu).
  2. Each card's rendered height = max(baseCardHeight, measuredWrappedTextHeight + verticalPadding).
  3. Per-column card stacking uses accumulated per-card heights (not uniform cardH). This flows through:
    • card Y placement in renderColumn / renderCard,
    • "+ Add card" button Y,
    • column background height (via renderKanban totalH),
    • cardGroup dragend drop-slot-index search.
  4. editInline gains a multiline parameter. When true, renders a <textarea> that grows vertically, supports Shift+Enter for newlines, commits on Enter and on blur, cancels on Escape. Single-line path for titles/column names is unchanged.
  5. Card card.text may contain \n characters. No schema change: Konva.Text renders explicit \n; JSON round-trips \n losslessly.
  6. PR-compatibility:
    • PR #62: card click still draws its own 2px #007AFF stroke inline without re-rendering (preserves Konva dblclick identity).
    • PR #63: transformer live-redraw still writes st.cardHeight from sy. Unchanged; the value becomes the base only.
    • PR #65 (open): showCardMenu already receives cardText; multi-line editInline(..., true) must be invoked from both the dblclick handler and the menu Edit item.
  7. Font scaling (PR #51) is untouched: textFs still derives from the base cardH.

Files to Modify/Create

Modify only:

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/kanban.js

No changes to sync.js, tools.js, properties.js, shortcuts.js, or CSS.

Implementation Plan

Step 1 — Introduce height-measurement helper and constants

Files: kanban.js

  • Add a constant near cardGap: var cardTextPadV = 16; (8px padding top + 8px padding bottom).
  • Add helper measureCardTextHeight(text, fontSize, width) that constructs a temporary Konva.Text({ text, fontSize, width, wrap: 'word' }), reads .height(), destroys the temp node, returns the height.
  • Add module-level var _colLayout = new WeakMap(); to hold per-column {heights, offsets} between measurement and render.

Dependencies: none.

Step 2 — Two-pass measurement in renderKanban

Files: kanban.js, function renderKanban

  • Before creating bg, iterate columns and compute each column's {heights, offsets}:
    • textFs = clampFont(cardH * 0.25, 9, 20) (same formula as renderCard).
    • heights[i] = max(cardH, measureCardTextHeight(card.text || 'Untitled', textFs, colW - 50) + cardTextPadV).
    • offsets[i] = sum(heights[0..i-1] + cardGap).
    • Store via _colLayout.set(col, { heights, offsets }).
  • Compute per-column stack total: stackH = heights.length ? offsets[last] + heights[last] : 0.
  • totalH = headerH + 8 + max(maxStack, cardH) + 28 + 50.

Dependencies: Step 1.

Step 3 — renderColumn reads pre-computed layout

Files: kanban.js, function renderColumn

  • Read var layout = _colLayout.get(col) || { heights: [], offsets: [] };.
  • Rewrite card loop: col.cards.forEach(function(card, cardIdx) { renderCard(group, col, colIdx, card, cardIdx, cx, cy, columns, colW, cardH, layout.offsets[cardIdx], layout.heights[cardIdx]); });
  • Replace addY calc with the stack-end from the layout: var stackEnd = layout.heights.length ? layout.offsets[last] + layout.heights[last] + cardGap : 0; var addY = cy + 28 + stackEnd;

Dependencies: Step 2.

Step 4 — Adapt renderCard signature to receive Y and height

Files: kanban.js, function renderCard

  • New signature: renderCard(group, col, colIdx, card, cardIdx, cx, cy, columns, colW, baseCardH, cardYLocal, actualCardH).
  • var cardY = cy + 28 + cardYLocal;
  • Font computations use baseCardH unchanged: textFs, menuFs.
  • textY = 8 unconditionally (top-align; wrap-enabled text makes center layout impractical).
  • menuY computed against a top-anchored band: var menuBandH = Math.min(actualCardH, 40); var menuY = Math.round((menuBandH - menuFs) / 2);
  • cardRect uses height: actualCardH.
  • cardText: wrap: 'word', drop ellipsis: true. Width stays colW - 50.
  • menuHit: y: 2, height: actualCardH - 4.

Dependencies: Steps 2, 3.

Step 5 — Fix cardGroup.on('dragend') slot calculation

Files: kanban.js

  • After determining targetColIdx: var layout = _colLayout.get(cols[targetColIdx]);.
  • Iterate: for (j = 0; j < targetCards.length; j++) { var midY = headerH + 28 + layout.offsets[j] + layout.heights[j] / 2; if (local.y < midY) { insertIdx = j; break; } }.
  • Defensive fallback to uniform formula if layout missing (shouldn't happen mid-render).

Dependencies: Steps 2, 3.

Step 6 — Add multiline param to editInline, wire up card callers

Files: kanban.js

  • Change signature: editInline(group, textNode, onDone, initialValue, multiline).
  • When !multiline: existing <input type="text"> path unchanged.
  • When multiline:
    • Create <textarea class="konva-text-edit">.
    • Styles: same as input branch plus resize:none; line-height:1.3; overflow:hidden; white-space:pre-wrap;.
    • Initial height: max(textNode.height() * stageScale, textNode.fontSize() * stageScale + 12).
    • Auto-grow on input event: this.style.height = 'auto'; this.style.height = this.scrollHeight + 'px';.
    • Keydown: plain EnterpreventDefault() + blur() (commit); Shift+Enter → default (newline) + trigger auto-grow; Escape → restore initial + blur. e.stopPropagation() always.
  • Both card edit sites pass multiline: true:
    • cardRect.on('dblclick dbltap') handler.
    • showCardMenu Edit item.
  • Title and column-title edits keep the existing calls without the flag.

Dependencies: none (independent of Steps 1–5).

Acceptance Criteria

  • A card whose text is longer than one line wraps across multiple lines inside the card (no ellipsis).
  • The card grows vertically to fit; shorter cards keep the base height.
  • Cards stacked below a tall card are pushed down; no overlap.
  • "+ Add card" button sits just below the last (variable-height) card.
  • Column background is tall enough to contain the tallest stack + Add button.
  • Dragging a card to another column drops it at the slot under the pointer.
  • Dragging within a column reorders around tall cards correctly.
  • Double-click opens a multi-line textarea; Shift+Enter inserts newline; Enter commits; Escape cancels.
  • 3-dots menu Edit opens the same textarea.
  • Multi-line text persists across reload.
  • Board title and column title edits still use a single-line input.
  • cardHeight slider still scales the base slot height.
  • Transformer-resize still scales the base cardHeight; wrapped text reflows on transformend.
  • Click-selection (PR #62) still works, and a quick follow-up dblclick still opens edit mode.
  • Global Delete/Backspace/Enter shortcuts do not fire while typing in the textarea.

Notes

(a) Variable slot heights. state.cardHeight = base/minimum slot height; per-card actual = max(base, measuredText + cardTextPadV). Only base is persisted; measurements re-derive each render.

(b) Stack layout. Two-pass renderKanban: measure all columns → compute totalH from max stack → render columns reading the stored layout. heights/offsets live in a module-level WeakMap<col, layout>, never serialized.

(c) Drag-drop. cardGroup.on('dragend') reads the target column's layout from the WeakMap and uses per-card midpoints. Defensive fallback to uniform formula if absent.

(d) menuBtn positioning on tall cards. 3-dots stays anchored near the TOP, centered within a 40px band (or actualCardH if smaller). Prevents the button from drifting into the middle of wrapped text. Tradeoff: for very tall cards the menu is not vertically centered — but the alternative collides with text.

(e) editInline multiline. Flag is true only for card text. Textarea auto-grows on input; plain Enter commits (preventDefault to avoid a stray newline before blur); Shift+Enter inserts a newline; Escape cancels. Existing .konva-text-edit CSS is reused.

(f) Font and resize interaction. textFs uses the base cardH — PR #51 behavior intact. Per-card actual heights absorb wrap overflow beyond the base. Transformer live-redraw (tools.js) writes the base cardHeight; downstream renderKanban re-measures and re-stacks.

(g) Cross-PR compatibility.

  • PR #62: untouched. cardRect.on('click tap') still mutates stroke / strokeWidth directly without a full render.
  • PR #63: untouched. Writer treats cardHeight as the scalar; the new reader-side interpretation ("base") is transparent.
  • PR #65 (open): showCardMenu already receives cardText; Edit call gains multiline: true.

Scope discipline. All changes in kanban.js. No schema change. No new module. No reshuffle of responsibilities beyond threading per-card Y/height through renderColumnrenderCard and centralizing measurement in renderKanban.

## Implementation Spec for Issue #52 ### Objective Fix kanban card text rendering so multi-line content displays correctly and allow multi-line entry in the inline editor. Card text must word-wrap instead of truncating with an ellipsis, the card slot height must grow per-card to accommodate wrapped text, and double-click/Edit must open a multi-line textarea (Shift+Enter = newline, Enter/blur = commit, Escape = cancel). Base `cardHeight` in state (controlled by slider + transformer resize) continues to act as the minimum slot height; wrapped text can push a given card taller without persisting that overflow into state. ### Requirements 1. Card text uses `wrap: 'word'`, `ellipsis: false` (drop the `ellipsis: true` from `renderCard`). Text width stays at `colW - 50` (preserves room for the 3-dots menu). 2. Each card's rendered height = `max(baseCardHeight, measuredWrappedTextHeight + verticalPadding)`. 3. Per-column card stacking uses accumulated per-card heights (not uniform `cardH`). This flows through: - card Y placement in `renderColumn` / `renderCard`, - "+ Add card" button Y, - column background height (via `renderKanban` totalH), - cardGroup `dragend` drop-slot-index search. 4. `editInline` gains a `multiline` parameter. When `true`, renders a `<textarea>` that grows vertically, supports `Shift+Enter` for newlines, commits on `Enter` and on blur, cancels on `Escape`. Single-line path for titles/column names is unchanged. 5. Card `card.text` may contain `\n` characters. No schema change: Konva.Text renders explicit `\n`; JSON round-trips `\n` losslessly. 6. PR-compatibility: - PR #62: card click still draws its own 2px `#007AFF` stroke inline without re-rendering (preserves Konva dblclick identity). - PR #63: transformer live-redraw still writes `st.cardHeight` from `sy`. Unchanged; the value becomes the base only. - PR #65 (open): `showCardMenu` already receives `cardText`; multi-line `editInline(..., true)` must be invoked from both the dblclick handler and the menu Edit item. 7. Font scaling (PR #51) is untouched: `textFs` still derives from the base `cardH`. ### Files to Modify/Create Modify only: - `crates/hero_whiteboard_ui/static/web/js/whiteboard/kanban.js` No changes to sync.js, tools.js, properties.js, shortcuts.js, or CSS. ### Implementation Plan #### Step 1 — Introduce height-measurement helper and constants Files: `kanban.js` - Add a constant near `cardGap`: `var cardTextPadV = 16;` (8px padding top + 8px padding bottom). - Add helper `measureCardTextHeight(text, fontSize, width)` that constructs a temporary `Konva.Text({ text, fontSize, width, wrap: 'word' })`, reads `.height()`, destroys the temp node, returns the height. - Add module-level `var _colLayout = new WeakMap();` to hold per-column `{heights, offsets}` between measurement and render. Dependencies: none. #### Step 2 — Two-pass measurement in `renderKanban` Files: `kanban.js`, function `renderKanban` - Before creating `bg`, iterate columns and compute each column's `{heights, offsets}`: - `textFs = clampFont(cardH * 0.25, 9, 20)` (same formula as `renderCard`). - `heights[i] = max(cardH, measureCardTextHeight(card.text || 'Untitled', textFs, colW - 50) + cardTextPadV)`. - `offsets[i] = sum(heights[0..i-1] + cardGap)`. - Store via `_colLayout.set(col, { heights, offsets })`. - Compute per-column stack total: `stackH = heights.length ? offsets[last] + heights[last] : 0`. - `totalH = headerH + 8 + max(maxStack, cardH) + 28 + 50`. Dependencies: Step 1. #### Step 3 — `renderColumn` reads pre-computed layout Files: `kanban.js`, function `renderColumn` - Read `var layout = _colLayout.get(col) || { heights: [], offsets: [] };`. - Rewrite card loop: `col.cards.forEach(function(card, cardIdx) { renderCard(group, col, colIdx, card, cardIdx, cx, cy, columns, colW, cardH, layout.offsets[cardIdx], layout.heights[cardIdx]); });` - Replace `addY` calc with the stack-end from the layout: `var stackEnd = layout.heights.length ? layout.offsets[last] + layout.heights[last] + cardGap : 0; var addY = cy + 28 + stackEnd;` Dependencies: Step 2. #### Step 4 — Adapt `renderCard` signature to receive Y and height Files: `kanban.js`, function `renderCard` - New signature: `renderCard(group, col, colIdx, card, cardIdx, cx, cy, columns, colW, baseCardH, cardYLocal, actualCardH)`. - `var cardY = cy + 28 + cardYLocal;` - Font computations use `baseCardH` unchanged: `textFs`, `menuFs`. - `textY = 8` unconditionally (top-align; wrap-enabled text makes center layout impractical). - `menuY` computed against a top-anchored band: `var menuBandH = Math.min(actualCardH, 40); var menuY = Math.round((menuBandH - menuFs) / 2);` - `cardRect` uses `height: actualCardH`. - `cardText`: `wrap: 'word'`, drop `ellipsis: true`. Width stays `colW - 50`. - `menuHit`: `y: 2, height: actualCardH - 4`. Dependencies: Steps 2, 3. #### Step 5 — Fix `cardGroup.on('dragend')` slot calculation Files: `kanban.js` - After determining `targetColIdx`: `var layout = _colLayout.get(cols[targetColIdx]);`. - Iterate: `for (j = 0; j < targetCards.length; j++) { var midY = headerH + 28 + layout.offsets[j] + layout.heights[j] / 2; if (local.y < midY) { insertIdx = j; break; } }`. - Defensive fallback to uniform formula if layout missing (shouldn't happen mid-render). Dependencies: Steps 2, 3. #### Step 6 — Add `multiline` param to `editInline`, wire up card callers Files: `kanban.js` - Change signature: `editInline(group, textNode, onDone, initialValue, multiline)`. - When `!multiline`: existing `<input type="text">` path unchanged. - When `multiline`: - Create `<textarea class="konva-text-edit">`. - Styles: same as input branch plus `resize:none; line-height:1.3; overflow:hidden; white-space:pre-wrap;`. - Initial height: `max(textNode.height() * stageScale, textNode.fontSize() * stageScale + 12)`. - Auto-grow on `input` event: `this.style.height = 'auto'; this.style.height = this.scrollHeight + 'px';`. - Keydown: plain `Enter` → `preventDefault()` + `blur()` (commit); `Shift+Enter` → default (newline) + trigger auto-grow; `Escape` → restore initial + blur. `e.stopPropagation()` always. - Both card edit sites pass `multiline: true`: - `cardRect.on('dblclick dbltap')` handler. - `showCardMenu` Edit item. - Title and column-title edits keep the existing calls without the flag. Dependencies: none (independent of Steps 1–5). ### Acceptance Criteria - [ ] A card whose text is longer than one line wraps across multiple lines inside the card (no ellipsis). - [ ] The card grows vertically to fit; shorter cards keep the base height. - [ ] Cards stacked below a tall card are pushed down; no overlap. - [ ] "+ Add card" button sits just below the last (variable-height) card. - [ ] Column background is tall enough to contain the tallest stack + Add button. - [ ] Dragging a card to another column drops it at the slot under the pointer. - [ ] Dragging within a column reorders around tall cards correctly. - [ ] Double-click opens a multi-line textarea; Shift+Enter inserts newline; Enter commits; Escape cancels. - [ ] 3-dots menu Edit opens the same textarea. - [ ] Multi-line text persists across reload. - [ ] Board title and column title edits still use a single-line input. - [ ] cardHeight slider still scales the base slot height. - [ ] Transformer-resize still scales the base `cardHeight`; wrapped text reflows on `transformend`. - [ ] Click-selection (PR #62) still works, and a quick follow-up dblclick still opens edit mode. - [ ] Global Delete/Backspace/Enter shortcuts do not fire while typing in the textarea. ### Notes **(a) Variable slot heights.** `state.cardHeight` = base/minimum slot height; per-card actual = `max(base, measuredText + cardTextPadV)`. Only base is persisted; measurements re-derive each render. **(b) Stack layout.** Two-pass `renderKanban`: measure all columns → compute `totalH` from max stack → render columns reading the stored layout. `heights`/`offsets` live in a module-level `WeakMap<col, layout>`, never serialized. **(c) Drag-drop.** `cardGroup.on('dragend')` reads the target column's layout from the WeakMap and uses per-card midpoints. Defensive fallback to uniform formula if absent. **(d) menuBtn positioning on tall cards.** 3-dots stays anchored near the TOP, centered within a 40px band (or `actualCardH` if smaller). Prevents the button from drifting into the middle of wrapped text. Tradeoff: for very tall cards the menu is not vertically centered — but the alternative collides with text. **(e) editInline multiline.** Flag is `true` only for card text. Textarea auto-grows on input; plain Enter commits (preventDefault to avoid a stray newline before blur); Shift+Enter inserts a newline; Escape cancels. Existing `.konva-text-edit` CSS is reused. **(f) Font and resize interaction.** `textFs` uses the base `cardH` — PR #51 behavior intact. Per-card actual heights absorb wrap overflow beyond the base. Transformer live-redraw (tools.js) writes the base `cardHeight`; downstream `renderKanban` re-measures and re-stacks. **(g) Cross-PR compatibility.** - PR #62: untouched. `cardRect.on('click tap')` still mutates `stroke` / `strokeWidth` directly without a full render. - PR #63: untouched. Writer treats `cardHeight` as the scalar; the new reader-side interpretation ("base") is transparent. - PR #65 (open): `showCardMenu` already receives `cardText`; Edit call gains `multiline: true`. **Scope discipline.** All changes in `kanban.js`. No schema change. No new module. No reshuffle of responsibilities beyond threading per-card Y/height through `renderColumn` → `renderCard` and centralizing measurement in `renderKanban`.
Author
Member

Test Results

JavaScript-only change in kanban.js; 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

No Rust tests exercise the JavaScript whiteboard modules, so the suite confirms regression safety on the Rust side. Manual verification against a running UI is required for the wrap/stack/drag/edit criteria listed in the spec's Acceptance Criteria.

## Test Results JavaScript-only change in `kanban.js`; 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 | No Rust tests exercise the JavaScript whiteboard modules, so the suite confirms regression safety on the Rust side. Manual verification against a running UI is required for the wrap/stack/drag/edit criteria listed in the spec's Acceptance Criteria.
Author
Member

Implementation Summary

Kanban cards now wrap long text, grow to fit, and accept multi-line input.

Files changed

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/kanban.js (+93 / -25)

Behavior changes

  • cardText uses wrap: 'word', ellipsis removed. Long card text wraps across multiple lines.
  • Each card's rendered height is max(baseCardHeight, measuredWrappedTextHeight + cardTextPadV). Base cardHeight in state remains the slider/transformer-controlled minimum; wrapped overflow is never persisted.
  • renderKanban does a two-pass measurement: for every column it computes {heights[], offsets[]} and stores the layout in a module-level WeakMap<col, layout>. Column totalH is derived from the tallest column stack. renderColumn and renderCard read the layout back through WeakMap.get.
  • renderCard now receives cardYLocal and actualCardH from its caller; cardRect.height and menuHit.height use the per-card actual height.
  • cardGroup.on('dragend') computes drop-slot midpoints as headerH + 28 + offsets[j] + heights[j]/2 from the target column's layout, so reordering works correctly for variable-height cards. Defensive fallback to the uniform formula if the WeakMap lookup misses.
  • menuBtn is top-anchored inside a 40-px band so the 3-dots stay at the visible top-right of tall cards rather than drifting into the middle of wrapped text.
  • editInline gained a multiline parameter. When true it renders a <textarea> with line-height, auto-grow on input, Shift+Enter newlines, Enter/blur commit, Escape cancel. Single-line <input> path for board-title and column-title edits is preserved unchanged. The dblclick handler on cardRect and the menu Edit item both pass multiline: true.

Preserved

  • Font scaling (PR #51): textFs / menuFs still derive from base cardH.
  • Click-selection (PR #62): cardRect.on('click tap') still draws its own 2px #007AFF stroke inline without re-rendering, so Konva dblclick-identity is preserved.
  • Transformer live-redraw (PR #63): still writes state.cardHeight from the live sy; downstream renderKanban re-measures and re-stacks.
  • Edit fix (PR #65 open): showCardMenu still receives cardText as a direct reference; the Edit call now passes multiline: true.

Out of scope

  • No schema/sync changes. card.text remains a plain string; \n round-trips through JSON.
  • No changes to shortcuts.js, tools.js, properties.js, or CSS. The whiteboard's global key handler already short-circuits on INPUT/TEXTAREA targets, so Delete/Backspace/Enter shortcuts don't fire while typing.
  • Persistence of wrapped-text "actual height" is deliberately avoided — it's re-derived each render from text + font + width.

Test results

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

Manual QA required: wrap rendering, stacking under tall cards, drag-drop into variable-height columns, textarea editing (Enter commit, Shift+Enter newline, Escape cancel), persistence across reload, and all preserved behaviors (click-select, drag-as-a-unit, font scaling, transformer resize, menu Edit).

## Implementation Summary Kanban cards now wrap long text, grow to fit, and accept multi-line input. ### Files changed - `crates/hero_whiteboard_ui/static/web/js/whiteboard/kanban.js` (+93 / -25) ### Behavior changes - `cardText` uses `wrap: 'word'`, `ellipsis` removed. Long card text wraps across multiple lines. - Each card's rendered height is `max(baseCardHeight, measuredWrappedTextHeight + cardTextPadV)`. Base `cardHeight` in state remains the slider/transformer-controlled minimum; wrapped overflow is never persisted. - `renderKanban` does a two-pass measurement: for every column it computes `{heights[], offsets[]}` and stores the layout in a module-level `WeakMap<col, layout>`. Column `totalH` is derived from the tallest column stack. `renderColumn` and `renderCard` read the layout back through `WeakMap.get`. - `renderCard` now receives `cardYLocal` and `actualCardH` from its caller; `cardRect.height` and `menuHit.height` use the per-card actual height. - `cardGroup.on('dragend')` computes drop-slot midpoints as `headerH + 28 + offsets[j] + heights[j]/2` from the target column's layout, so reordering works correctly for variable-height cards. Defensive fallback to the uniform formula if the WeakMap lookup misses. - `menuBtn` is top-anchored inside a 40-px band so the 3-dots stay at the visible top-right of tall cards rather than drifting into the middle of wrapped text. - `editInline` gained a `multiline` parameter. When `true` it renders a `<textarea>` with line-height, auto-grow on `input`, `Shift+Enter` newlines, `Enter`/`blur` commit, `Escape` cancel. Single-line `<input>` path for board-title and column-title edits is preserved unchanged. The dblclick handler on `cardRect` and the menu Edit item both pass `multiline: true`. ### Preserved - Font scaling (PR #51): `textFs` / `menuFs` still derive from base `cardH`. - Click-selection (PR #62): `cardRect.on('click tap')` still draws its own 2px `#007AFF` stroke inline without re-rendering, so Konva dblclick-identity is preserved. - Transformer live-redraw (PR #63): still writes `state.cardHeight` from the live `sy`; downstream `renderKanban` re-measures and re-stacks. - Edit fix (PR #65 open): `showCardMenu` still receives `cardText` as a direct reference; the Edit call now passes `multiline: true`. ### Out of scope - No schema/sync changes. `card.text` remains a plain string; `\n` round-trips through JSON. - No changes to `shortcuts.js`, `tools.js`, `properties.js`, or CSS. The whiteboard's global key handler already short-circuits on `INPUT`/`TEXTAREA` targets, so Delete/Backspace/Enter shortcuts don't fire while typing. - Persistence of wrapped-text "actual height" is deliberately avoided — it's re-derived each render from text + font + width. ### Test results - `cargo check --workspace`: pass - `cargo test --workspace --lib`: pass - `cargo clippy --workspace -- -D warnings`: pass - `cargo fmt --check`: pass Manual QA required: wrap rendering, stacking under tall cards, drag-drop into variable-height columns, textarea editing (Enter commit, Shift+Enter newline, Escape cancel), persistence across reload, and all preserved behaviors (click-select, drag-as-a-unit, font scaling, transformer resize, menu Edit).
Author
Member

Pull request opened: #66

This PR implements the changes discussed in this issue.

Pull request opened: https://forge.ourworld.tf/lhumina_code/hero_whiteboard/pulls/66 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#52
No description provided.