Floating toolbar: edit multiple same-type items at once, with group actions shown alongside #209

Closed
opened 2026-05-21 08:34:16 +00:00 by AhmedHanafy725 · 2 comments

Summary

When the user selects multiple items of the same type (e.g. several sticky notes, several text blocks, several shapes), the floating selection toolbar should render the per-type editing controls (color, font, stroke width, etc.) and apply each change to every selected item at once. The existing multi-select group actions (Lock / Group / Ungroup) should remain visible alongside the per-type controls so the user gets both.

Scope

  • Multi-select with 2+ items and all of them sharing the same type (e.g. all 'sticky'): show the same per-type controls that appear today for a single item, but every change applies to all selected items in one history step.
  • Group / Lock controls that already render for multi-select continue to show side-by-side with the per-type controls. Visual order: group/lock actions first (or in a dedicated cluster on one side), then the per-type editors.
  • Multi-select with mixed types: behave as today (only group/lock cluster). No per-type editor is shown.
  • Multi-select with all comments: behave as today (comment-specific actions only).
  • Single selection: unchanged.

Editing semantics

  • A change to any per-type control applies to every selected item of that type. The change is captured as one history entry (batch) so a single Undo reverts the whole batch.
  • Inputs that read a starting value from one node (color picker, font select, stroke width slider, etc.) seed from the first selected node. Where the selected nodes disagree, the input shows an "indeterminate" state (placeholder, blank, or a subtle indicator) so the user knows the change will overwrite all of them.
  • Live broadcast (WebSocket) and persistence (RPC) fire per item via the existing single-item code paths; the existing debounce/batch coalesces them into one round-trip.

Acceptance Criteria

  • Selecting 2+ sticky notes shows the sticky editor controls in the floating toolbar plus the existing multi-select group/lock cluster.
  • Changing the sticky color in the toolbar changes the color of every selected sticky and is a single Undo step.
  • Same for other per-type properties (font, stroke width, fill, etc.) on every type that has a single-item editor today (sticky, text, shape, drawing, frame, document, calendar, kanban, mind map, web frame).
  • Selecting items of mixed types (e.g. a sticky + a shape) shows only the group/lock cluster, as today.
  • Selecting all comments shows only comment actions, as today.
  • When the initial values disagree across the selection, the control shows an indeterminate / mixed state.
  • No regression to single-selection toolbar rendering, undo/redo, sync, persistence.

Notes

The floating toolbar lives in crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js. Today it dispatches via update(nodes) and renders single-item controls in _renderForNode(node) (around line 230+), with _renderMultiSelection() handling the multi-select group/lock cluster. The fix should reuse the per-type renderers rather than duplicate them: extend each per-type renderer (or wrap it in a small helper) so its property handlers apply to every selected item of that type when invoked from the multi-select branch. Indeterminate state should be a tiny convention (e.g. a data-mixed class or empty value) rather than separate "mixed" widgets per type. Frontend assets are embedded via rust-embed; rebuild the admin crate to pick up JS changes.

## Summary When the user selects multiple items of the **same type** (e.g. several sticky notes, several text blocks, several shapes), the floating selection toolbar should render the per-type editing controls (color, font, stroke width, etc.) and apply each change to **every** selected item at once. The existing multi-select group actions (Lock / Group / Ungroup) should remain visible alongside the per-type controls so the user gets both. ## Scope - Multi-select with 2+ items and all of them sharing the same `type` (e.g. all `'sticky'`): show the same per-type controls that appear today for a single item, but every change applies to all selected items in one history step. - Group / Lock controls that already render for multi-select continue to show side-by-side with the per-type controls. Visual order: group/lock actions first (or in a dedicated cluster on one side), then the per-type editors. - Multi-select with mixed types: behave as today (only group/lock cluster). No per-type editor is shown. - Multi-select with all comments: behave as today (comment-specific actions only). - Single selection: unchanged. ## Editing semantics - A change to any per-type control applies to **every** selected item of that type. The change is captured as one history entry (batch) so a single Undo reverts the whole batch. - Inputs that read a starting value from one node (color picker, font select, stroke width slider, etc.) seed from the first selected node. Where the selected nodes disagree, the input shows an "indeterminate" state (placeholder, blank, or a subtle indicator) so the user knows the change will overwrite all of them. - Live broadcast (WebSocket) and persistence (RPC) fire per item via the existing single-item code paths; the existing debounce/batch coalesces them into one round-trip. ## Acceptance Criteria - [ ] Selecting 2+ sticky notes shows the sticky editor controls in the floating toolbar **plus** the existing multi-select group/lock cluster. - [ ] Changing the sticky color in the toolbar changes the color of every selected sticky and is a single Undo step. - [ ] Same for other per-type properties (font, stroke width, fill, etc.) on every type that has a single-item editor today (sticky, text, shape, drawing, frame, document, calendar, kanban, mind map, web frame). - [ ] Selecting items of mixed types (e.g. a sticky + a shape) shows only the group/lock cluster, as today. - [ ] Selecting all comments shows only comment actions, as today. - [ ] When the initial values disagree across the selection, the control shows an indeterminate / mixed state. - [ ] No regression to single-selection toolbar rendering, undo/redo, sync, persistence. ## Notes The floating toolbar lives in `crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js`. Today it dispatches via `update(nodes)` and renders single-item controls in `_renderForNode(node)` (around line 230+), with `_renderMultiSelection()` handling the multi-select group/lock cluster. The fix should reuse the per-type renderers rather than duplicate them: extend each per-type renderer (or wrap it in a small helper) so its property handlers apply to every selected item of that type when invoked from the multi-select branch. Indeterminate state should be a tiny convention (e.g. a `data-mixed` class or empty value) rather than separate "mixed" widgets per type. Frontend assets are embedded via rust-embed; rebuild the admin crate to pick up JS changes.
Author
Owner

Implementation Spec for Issue #209

Objective

When 2+ items of the same non-comment type are selected, the floating selection toolbar renders the per-type editor controls (color, font size, stroke width, alignment, etc.) alongside the existing multi-select group/lock cluster. Every change applies to all selected items as a single undo step. Mixed-type and all-comment selections behave as today.

Requirements

  1. Branch detection in update(nodes): a new homogeneous-multi path triggers when cachedNodes.length >= 2 && !allComments && _distinctTypes(cachedNodes) === 1 AND the shared type is in V1_MULTI_TYPES = { sticky, text, shape, drawing }.
  2. The existing _renderMultiSelection() (Lock + Group/Ungroup) renders first (before the per-type controls) so users see both clusters.
  3. The per-type renderer renders once, but every mutation it performs is applied to every selected node, wrapped in WhiteboardHistory.batch(...) for one-step undo.
  4. Locked items in the selection are skipped by mutations.
  5. Inputs seed their value from the representative (cachedNodes[0]). Indeterminate/mixed visualization is out of scope for v1 (deferred to a follow-up).
  6. Inherently per-instance UI is hidden in multi mode: the "Edit text" pencil button (sticky/text/shape), frame title editor, move-up/move-down. Frame is intentionally NOT in V1_MULTI_TYPES because no per-type controls survive multi mode for it.
  7. WhiteboardSync.onUpdate fires for every mutated node; the existing debounce/batch coalesces.
  8. refresh() (re-render after handler-triggered mutations like align) must preserve the multi-target context. Use a _lastTargets stash + restore around _renderForNode.

Files to Modify/Create

  • Modify: crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js — single file change.
  • No changes to objects.js, history.js, templates.

Implementation Plan

Step 1: Add multi-target state + _applyToTargets helper

Files: selection_toolbar.js

  • Add var _targetNodes = null; var _lastTargets = null; and var V1_MULTI_TYPES = { sticky: true, text: true, shape: true, drawing: true }; near other module-level vars.
  • Add _applyToTargets(mutateFn) after persistToolbarMutation:
    function _applyToTargets(mutateFn) {
        var targets = (_targetNodes && _targetNodes.length) ? _targetNodes : (cachedNode ? [cachedNode] : []);
        if (targets.length === 0) return;
        WhiteboardHistory.batch(function () {
            targets.forEach(function (n) {
                if (!n || !n.id) return;
                var id = n.id();
                if (!id) return;
                if (typeof WhiteboardObjects !== 'undefined' && WhiteboardObjects.isLocked && WhiteboardObjects.isLocked(id)) return;
                WhiteboardHistory.snapshotBefore(id);
                mutateFn(n);
                WhiteboardHistory.commitUpdate(id);
            });
        });
    }
    
  • Add _filterUnlocked(nodes) near _allLocked returning the subset whose ids aren't locked.
    Dependencies: none

Step 2: Refactor in-scope per-type renderers to route mutations through _applyToTargets

Files: selection_toolbar.js

  • For each handler in _renderSticky (1228-1272), _renderText (1274-1313), _renderShape (1315-1380), _renderDrawing (1382-1406), replace the existing snapshotBefore(node.id()); /*mutate*/ commitUpdate(node.id()); (Pattern A) or persistToolbarMutation(node) (Pattern B) pattern with _applyToTargets(function(n) { /*mutate against n; for Pattern B end with WhiteboardSync.onUpdate(n)*/ });.
  • The applicator owns snapshotBefore/commitUpdate/batch; mutator bodies still own canvas redraws and Pattern B's WhiteboardSync.onUpdate(n) (Pattern A's WhiteboardObjects.rerenderXxx(n) already covers sync).
  • Single selection is unaffected: _targetNodes === null → applicator falls back to [cachedNode].
    Dependencies: Step 1

Step 3: Gate per-instance UI by multi mode

Files: selection_toolbar.js

  • At the top of each in-scope renderer, add var isMulti = !!(_targetNodes && _targetNodes.length > 1);.
  • Wrap the trailing "Edit text" pencil button in sticky/text/shape with if (!isMulti) { ... }.
  • Frame is not in V1 scope so no change to its renderer needed.
    Dependencies: Step 2 (same hunks)

Step 4: Dispatcher branch for homogeneous multi-select

Files: selection_toolbar.js update(nodes) (~line 250)

  • Replace the existing else-branch's body with:
    } else {
        cachedNode = null;
        if (!allComments && cachedNodes.length >= 2) {
            _renderMultiSelection();
            if (_distinctTypes(cachedNodes) === 1) {
                var rep = cachedNodes[0];
                var repId = rep && rep.id && rep.id();
                var od = repId && typeof WhiteboardObjects !== 'undefined' ? WhiteboardObjects.getObject(repId) : null;
                if (od && V1_MULTI_TYPES[od.type]) {
                    var editable = _filterUnlocked(cachedNodes);
                    if (editable.length >= 2) {
                        _targetNodes = editable;
                        _lastTargets = editable.slice();
                        try {
                            cachedNode = rep;
                            _renderForNode(rep);
                        } finally {
                            _targetNodes = null;
                        }
                    }
                }
            }
            _showPropsGroup(true);
        } else {
            _showPropsGroup(false);
        }
    }
    
  • In refresh() (~line 397), if _lastTargets && _lastTargets.length >= 2, restore _targetNodes = _lastTargets; before re-rendering and clear after.
  • At the top of update(nodes) (after cachedNodes rebuild) and in hide(), reset _targetNodes = null; _lastTargets = null;.
    Dependencies: Steps 1-3

Step 5: Build + manual verify

  • touch crates/hero_whiteboard_admin/src/assets.rs && cargo build --release -p hero_whiteboard_admin.
  • Manual scenarios in Acceptance Criteria.

Acceptance Criteria

  • Selecting 2+ sticky notes shows: Lock, Group, sticky color, font size, text color, alignment. NO pencil button.
  • Editing any per-type control updates all selected items.
  • One Ctrl+Z reverts the whole multi-edit batch.
  • Same for 2+ text, 2+ shapes, 2+ drawings (each with the type's canonical controls).
  • Locked items in the selection are not mutated.
  • Mixed-type multi-select still shows only Lock + Group/Ungroup.
  • All-comments multi-select still shows only comment actions.
  • Single selection toolbar is unchanged (pencil "Edit text", frame title editor, move-up/down all still render).
  • refresh() after a multi-edit handler keeps the multi-target context (no degradation to single).
  • WhiteboardSync.onUpdate fires for every mutated node so peers see the same change set.

Notes

  • Indeterminate/mixed visualization deferred to v2. Today's controls seed from a scalar; seeding from cachedNodes[0] is acceptable for v1. Follow-up: add _isUniform(cachedNodes, getter) + wb-input-mixed CSS class on triggers when values disagree.
  • Why frame is out of V1_MULTI_TYPES: frame's only per-type controls are the title editor and move-up/down — all per-instance. Including frame would only add code paths without user-visible benefit (Lock/Group already cover multi-frame via the unchanged outer branch).
  • Why _applyToTargets over a shared mutator swap: existing renderers don't go through a shared per-node mutator — they're inline closures. Routing each handler through the applicator is the smallest correct refactor and is the canonical extension point if more types are added later.
  • refresh() re-entry: the _lastTargets stash is the minimal correct change; clearing it on every update()/hide() prevents leaking across selection changes.
## Implementation Spec for Issue #209 ### Objective When 2+ items of the **same non-comment type** are selected, the floating selection toolbar renders the per-type editor controls (color, font size, stroke width, alignment, etc.) alongside the existing multi-select group/lock cluster. Every change applies to all selected items as a single undo step. Mixed-type and all-comment selections behave as today. ### Requirements 1. Branch detection in `update(nodes)`: a new homogeneous-multi path triggers when `cachedNodes.length >= 2 && !allComments && _distinctTypes(cachedNodes) === 1` AND the shared type is in `V1_MULTI_TYPES = { sticky, text, shape, drawing }`. 2. The existing `_renderMultiSelection()` (Lock + Group/Ungroup) renders first (before the per-type controls) so users see both clusters. 3. The per-type renderer renders once, but every mutation it performs is applied to every selected node, wrapped in `WhiteboardHistory.batch(...)` for one-step undo. 4. Locked items in the selection are skipped by mutations. 5. Inputs seed their value from the representative (`cachedNodes[0]`). Indeterminate/mixed visualization is out of scope for v1 (deferred to a follow-up). 6. Inherently per-instance UI is hidden in multi mode: the "Edit text" pencil button (sticky/text/shape), frame title editor, move-up/move-down. Frame is intentionally NOT in `V1_MULTI_TYPES` because no per-type controls survive multi mode for it. 7. `WhiteboardSync.onUpdate` fires for every mutated node; the existing debounce/batch coalesces. 8. `refresh()` (re-render after handler-triggered mutations like align) must preserve the multi-target context. Use a `_lastTargets` stash + restore around `_renderForNode`. ### Files to Modify/Create - Modify: `crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js` — single file change. - No changes to `objects.js`, `history.js`, templates. ### Implementation Plan #### Step 1: Add multi-target state + `_applyToTargets` helper Files: `selection_toolbar.js` - Add `var _targetNodes = null; var _lastTargets = null;` and `var V1_MULTI_TYPES = { sticky: true, text: true, shape: true, drawing: true };` near other module-level vars. - Add `_applyToTargets(mutateFn)` after `persistToolbarMutation`: ```js function _applyToTargets(mutateFn) { var targets = (_targetNodes && _targetNodes.length) ? _targetNodes : (cachedNode ? [cachedNode] : []); if (targets.length === 0) return; WhiteboardHistory.batch(function () { targets.forEach(function (n) { if (!n || !n.id) return; var id = n.id(); if (!id) return; if (typeof WhiteboardObjects !== 'undefined' && WhiteboardObjects.isLocked && WhiteboardObjects.isLocked(id)) return; WhiteboardHistory.snapshotBefore(id); mutateFn(n); WhiteboardHistory.commitUpdate(id); }); }); } ``` - Add `_filterUnlocked(nodes)` near `_allLocked` returning the subset whose ids aren't locked. Dependencies: none #### Step 2: Refactor in-scope per-type renderers to route mutations through `_applyToTargets` Files: `selection_toolbar.js` - For each handler in `_renderSticky` (1228-1272), `_renderText` (1274-1313), `_renderShape` (1315-1380), `_renderDrawing` (1382-1406), replace the existing `snapshotBefore(node.id()); /*mutate*/ commitUpdate(node.id());` (Pattern A) or `persistToolbarMutation(node)` (Pattern B) pattern with `_applyToTargets(function(n) { /*mutate against n; for Pattern B end with WhiteboardSync.onUpdate(n)*/ });`. - The applicator owns `snapshotBefore`/`commitUpdate`/`batch`; mutator bodies still own canvas redraws and Pattern B's `WhiteboardSync.onUpdate(n)` (Pattern A's `WhiteboardObjects.rerenderXxx(n)` already covers sync). - Single selection is unaffected: `_targetNodes === null` → applicator falls back to `[cachedNode]`. Dependencies: Step 1 #### Step 3: Gate per-instance UI by multi mode Files: `selection_toolbar.js` - At the top of each in-scope renderer, add `var isMulti = !!(_targetNodes && _targetNodes.length > 1);`. - Wrap the trailing "Edit text" pencil button in sticky/text/shape with `if (!isMulti) { ... }`. - Frame is not in V1 scope so no change to its renderer needed. Dependencies: Step 2 (same hunks) #### Step 4: Dispatcher branch for homogeneous multi-select Files: `selection_toolbar.js` `update(nodes)` (~line 250) - Replace the existing else-branch's body with: ```js } else { cachedNode = null; if (!allComments && cachedNodes.length >= 2) { _renderMultiSelection(); if (_distinctTypes(cachedNodes) === 1) { var rep = cachedNodes[0]; var repId = rep && rep.id && rep.id(); var od = repId && typeof WhiteboardObjects !== 'undefined' ? WhiteboardObjects.getObject(repId) : null; if (od && V1_MULTI_TYPES[od.type]) { var editable = _filterUnlocked(cachedNodes); if (editable.length >= 2) { _targetNodes = editable; _lastTargets = editable.slice(); try { cachedNode = rep; _renderForNode(rep); } finally { _targetNodes = null; } } } } _showPropsGroup(true); } else { _showPropsGroup(false); } } ``` - In `refresh()` (~line 397), if `_lastTargets && _lastTargets.length >= 2`, restore `_targetNodes = _lastTargets;` before re-rendering and clear after. - At the top of `update(nodes)` (after `cachedNodes` rebuild) and in `hide()`, reset `_targetNodes = null; _lastTargets = null;`. Dependencies: Steps 1-3 #### Step 5: Build + manual verify - `touch crates/hero_whiteboard_admin/src/assets.rs && cargo build --release -p hero_whiteboard_admin`. - Manual scenarios in Acceptance Criteria. ### Acceptance Criteria - [ ] Selecting 2+ sticky notes shows: Lock, Group, sticky color, font size, text color, alignment. NO pencil button. - [ ] Editing any per-type control updates all selected items. - [ ] One Ctrl+Z reverts the whole multi-edit batch. - [ ] Same for 2+ text, 2+ shapes, 2+ drawings (each with the type's canonical controls). - [ ] Locked items in the selection are not mutated. - [ ] Mixed-type multi-select still shows only Lock + Group/Ungroup. - [ ] All-comments multi-select still shows only comment actions. - [ ] Single selection toolbar is unchanged (pencil "Edit text", frame title editor, move-up/down all still render). - [ ] `refresh()` after a multi-edit handler keeps the multi-target context (no degradation to single). - [ ] `WhiteboardSync.onUpdate` fires for every mutated node so peers see the same change set. ### Notes - **Indeterminate/mixed visualization deferred to v2.** Today's controls seed from a scalar; seeding from `cachedNodes[0]` is acceptable for v1. Follow-up: add `_isUniform(cachedNodes, getter)` + `wb-input-mixed` CSS class on triggers when values disagree. - **Why frame is out of V1_MULTI_TYPES:** frame's only per-type controls are the title editor and move-up/down — all per-instance. Including frame would only add code paths without user-visible benefit (Lock/Group already cover multi-frame via the unchanged outer branch). - **Why `_applyToTargets` over a shared mutator swap:** existing renderers don't go through a shared per-node mutator — they're inline closures. Routing each handler through the applicator is the smallest correct refactor and is the canonical extension point if more types are added later. - **refresh() re-entry:** the `_lastTargets` stash is the minimal correct change; clearing it on every `update()`/`hide()` prevents leaking across selection changes.
Author
Owner

Implementation Summary

The floating selection toolbar now supports homogeneous multi-select editing for sticky / text / shape / drawing, with the existing Lock and Group/Ungroup cluster rendered alongside.

Changes

  • crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js:
    • Added module-level _targetNodes (live targets while rendering) and _lastTargets (stashed for refresh()), and a V1_MULTI_TYPES = { sticky, text, shape, drawing } table.
    • New _filterUnlocked(nodes) helper and _applyToTargets(mutateFn) helper. The applicator wraps a WhiteboardHistory.batch(...) and, for each target, runs snapshotBefore -> mutateFn(n) -> commitUpdate; locked nodes are skipped.
    • Refactored every mutation handler in _renderSticky / _renderText / _renderShape / _renderDrawing to route through _applyToTargets(function(n) { ... }). Pattern A handlers end with WhiteboardObjects.rerenderXxx(n) (sync via re-render); Pattern B handlers end with batchDraw() + WhiteboardSync.onUpdate(n) (the applicator now owns history bracketing, replacing the previous persistToolbarMutation).
    • Added an isMulti flag in those renderers to suppress the per-instance "Edit text" pencil button when 2+ items are selected.
    • update(nodes) clears _targetNodes / _lastTargets at entry, then in the multi-select branch (when _distinctTypes(cachedNodes) === 1 and the shared type is in V1_MULTI_TYPES) populates them with the unlocked subset and calls _renderForNode(rep) — so the existing _renderMultiSelection() group cluster renders first, then the per-type controls.
    • refresh() stashes/restores _targetNodes from _lastTargets so handler-triggered re-renders keep multi context. hide() clears both.
    • Out-of-scope renderers (frame, document, calendar, kanban, mindmap, group, webframe, comment) untouched.

Behavior

  • 2+ sticky notes: toolbar shows Lock + Group + sticky color + font size + text color + alignment. Changing any applies to all selected; one Ctrl+Z reverts the whole batch. Pencil "Edit text" is hidden.
  • 2+ text / shape / drawing: same idea with the type's canonical controls.
  • Mixed-type or all-comment multi-select: unchanged (Lock + Group/Ungroup only, or comment actions).
  • Single selection: unchanged (pencil and per-instance controls still render).
  • Locked items in the multi-selection are skipped by mutations.
  • WhiteboardSync.onUpdate fires for every mutated node so peers see the same change set.

Test results

  • cargo test --workspace --lib: compiled cleanly, no failures (change is JS-only; regression guard).
  • node --check selection_toolbar.js: OK. File encoding clean (UTF-8, zero control bytes).
  • Rebuilt and redeployed; served selection_toolbar.js contains the new helpers and refactored handlers (20 references to _applyToTargets / V1_MULTI_TYPES etc.).

Notes

  • Indeterminate / mixed-value visualization is deferred to a follow-up. Today's controls seed from the representative (cachedNodes[0]). A follow-up should add an _isUniform(cachedNodes, getter) check and a wb-input-mixed CSS class on triggers when values disagree.
  • Frame is intentionally not in V1_MULTI_TYPES: its only per-type controls (title editor, slide order buttons) are per-instance, so including it would add code paths without user-visible benefit.
  • Pure client-side change. No Rust changes, no schema, no RPC.
## Implementation Summary The floating selection toolbar now supports homogeneous multi-select editing for sticky / text / shape / drawing, with the existing Lock and Group/Ungroup cluster rendered alongside. ### Changes - `crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js`: - Added module-level `_targetNodes` (live targets while rendering) and `_lastTargets` (stashed for `refresh()`), and a `V1_MULTI_TYPES = { sticky, text, shape, drawing }` table. - New `_filterUnlocked(nodes)` helper and `_applyToTargets(mutateFn)` helper. The applicator wraps a `WhiteboardHistory.batch(...)` and, for each target, runs `snapshotBefore` -> `mutateFn(n)` -> `commitUpdate`; locked nodes are skipped. - Refactored every mutation handler in `_renderSticky` / `_renderText` / `_renderShape` / `_renderDrawing` to route through `_applyToTargets(function(n) { ... })`. Pattern A handlers end with `WhiteboardObjects.rerenderXxx(n)` (sync via re-render); Pattern B handlers end with `batchDraw()` + `WhiteboardSync.onUpdate(n)` (the applicator now owns history bracketing, replacing the previous `persistToolbarMutation`). - Added an `isMulti` flag in those renderers to suppress the per-instance "Edit text" pencil button when 2+ items are selected. - `update(nodes)` clears `_targetNodes` / `_lastTargets` at entry, then in the multi-select branch (when `_distinctTypes(cachedNodes) === 1` and the shared type is in `V1_MULTI_TYPES`) populates them with the unlocked subset and calls `_renderForNode(rep)` — so the existing `_renderMultiSelection()` group cluster renders first, then the per-type controls. - `refresh()` stashes/restores `_targetNodes` from `_lastTargets` so handler-triggered re-renders keep multi context. `hide()` clears both. - Out-of-scope renderers (frame, document, calendar, kanban, mindmap, group, webframe, comment) untouched. ### Behavior - 2+ sticky notes: toolbar shows Lock + Group + sticky color + font size + text color + alignment. Changing any applies to all selected; one Ctrl+Z reverts the whole batch. Pencil "Edit text" is hidden. - 2+ text / shape / drawing: same idea with the type's canonical controls. - Mixed-type or all-comment multi-select: unchanged (Lock + Group/Ungroup only, or comment actions). - Single selection: unchanged (pencil and per-instance controls still render). - Locked items in the multi-selection are skipped by mutations. - `WhiteboardSync.onUpdate` fires for every mutated node so peers see the same change set. ### Test results - `cargo test --workspace --lib`: compiled cleanly, no failures (change is JS-only; regression guard). - `node --check selection_toolbar.js`: OK. File encoding clean (UTF-8, zero control bytes). - Rebuilt and redeployed; served `selection_toolbar.js` contains the new helpers and refactored handlers (20 references to `_applyToTargets` / `V1_MULTI_TYPES` etc.). ### Notes - Indeterminate / mixed-value visualization is deferred to a follow-up. Today's controls seed from the representative (`cachedNodes[0]`). A follow-up should add an `_isUniform(cachedNodes, getter)` check and a `wb-input-mixed` CSS class on triggers when values disagree. - Frame is intentionally not in `V1_MULTI_TYPES`: its only per-type controls (title editor, slide order buttons) are per-instance, so including it would add code paths without user-visible benefit. - Pure client-side change. No Rust changes, no schema, no RPC.
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#209
No description provided.