Selection toolbar fill swatch lies about transparent fill (and offers no way to reset to transparent) #143

Open
opened 2026-05-05 09:39:45 +00:00 by AhmedHanafy725 · 3 comments
Member

Bug

When a Shape object has fill = 'transparent' (the rect shows only its stroke, no interior color), the selection toolbar's Fill color swatch is rendered as a solid #2b3035 (dark gray). This is misleading on two levels: it suggests the shape has a dark gray fill when it actually has none, and once the user picks any palette color the shape is now filled with no way to set it back to transparent (the palette has no transparent entry).

Reproduction

  1. Create a Shape (Rect) on the whiteboard. New shapes default to fill: 'transparent'.
  2. Click to select the shape — the selection toolbar appears above it.
  3. Observe the Fill color swatch (third trigger from the left, after the shape-type dropdown and the Stroke color swatch). It is rendered as a solid dark color, not as a 'no fill' indicator.
  4. Click the Fill swatch and pick a palette color. The shape now has that fill.
  5. Try to set the fill back to transparent. There is no transparent / 'no fill' option in the popover, so it is impossible from the toolbar.

Root cause

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/selection_toolbar.js:857 substitutes a fake color before passing to the swatch:
    var fill = (bg && bg.fill() !== 'transparent') ? bg.fill() : '#2b3035';
    
    The transparent state is hidden from the swatch instead of represented.
  • _buildColorTrigger at line 524 has no special rendering branch for 'transparent' — it just sets swatch.style.background = initial || '#000', which paints the swatch with whatever string was passed.
  • DEFAULT_COLOR_PALETTE (line 26) has no transparent entry, so even if the swatch and trigger handled transparent correctly, the popover still does not let the user pick it.

Fix scope

  1. In _buildColorTrigger, when initial === 'transparent' (or any value that resolves to transparent) render the swatch with a clear 'no fill' visual — typically a checkered background plus a thin diagonal red strike, the same convention Figma/Miro/Excalidraw use. Keep the same indicator logic in activeColorSetter so swapping the value back to transparent re-renders the indicator.
  2. In _buildColorPopover, add a leading 'no fill' / transparent option that returns 'transparent' to the onPick callback. Render it with the same checkered + strike indicator so it is recognizable.
  3. In _renderShape, drop the fake-color substitution at line 857 and pass bg.fill() (which may be 'transparent') straight through to _colorTriggerWithTitle.
  4. Audit other call sites that pass a fill-like value through _colorTriggerWithTitle and decide per-site whether transparent is a valid option there. At minimum: shape Fill (line 886) and frame Background (line 1018) should accept transparent. Sticky Note background, Document background, Text color, Stroke color, etc. should not — keep them as-is.

Affected files

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/selection_toolbar.js — the only file in scope.

Acceptance criteria

  • A newly created Shape (default fill: 'transparent') shows a 'no fill' swatch in the selection toolbar (checkered + strike), not a solid dark color.
  • Opening the Fill popover for a shape exposes a 'no fill' / transparent option at the top of the swatch grid, with the same indicator visual.
  • Picking 'no fill' on a filled shape sets bg.fill('transparent') and the swatch re-renders to the 'no fill' indicator.
  • Picking any palette color on a transparent shape sets the fill normally.
  • Stroke color / Text color / Sticky color triggers are unchanged.
  • No regression in serialization or sync — bg.fill() returns whatever was last picked, including 'transparent'.
## Bug When a Shape object has `fill = 'transparent'` (the rect shows only its stroke, no interior color), the selection toolbar's Fill color swatch is rendered as a solid `#2b3035` (dark gray). This is misleading on two levels: it suggests the shape has a dark gray fill when it actually has none, and once the user picks any palette color the shape is now filled with no way to set it back to transparent (the palette has no transparent entry). ## Reproduction 1. Create a Shape (Rect) on the whiteboard. New shapes default to `fill: 'transparent'`. 2. Click to select the shape — the selection toolbar appears above it. 3. Observe the Fill color swatch (third trigger from the left, after the shape-type dropdown and the Stroke color swatch). It is rendered as a solid dark color, not as a 'no fill' indicator. 4. Click the Fill swatch and pick a palette color. The shape now has that fill. 5. Try to set the fill back to transparent. There is no transparent / 'no fill' option in the popover, so it is impossible from the toolbar. ## Root cause - `crates/hero_whiteboard_ui/static/web/js/whiteboard/selection_toolbar.js:857` substitutes a fake color before passing to the swatch: ```js var fill = (bg && bg.fill() !== 'transparent') ? bg.fill() : '#2b3035'; ``` The transparent state is hidden from the swatch instead of represented. - `_buildColorTrigger` at line 524 has no special rendering branch for `'transparent'` — it just sets `swatch.style.background = initial || '#000'`, which paints the swatch with whatever string was passed. - `DEFAULT_COLOR_PALETTE` (line 26) has no transparent entry, so even if the swatch and trigger handled transparent correctly, the popover still does not let the user pick it. ## Fix scope 1. In `_buildColorTrigger`, when `initial === 'transparent'` (or any value that resolves to transparent) render the swatch with a clear 'no fill' visual — typically a checkered background plus a thin diagonal red strike, the same convention Figma/Miro/Excalidraw use. Keep the same indicator logic in `activeColorSetter` so swapping the value back to transparent re-renders the indicator. 2. In `_buildColorPopover`, add a leading 'no fill' / transparent option that returns `'transparent'` to the `onPick` callback. Render it with the same checkered + strike indicator so it is recognizable. 3. In `_renderShape`, drop the fake-color substitution at line 857 and pass `bg.fill()` (which may be `'transparent'`) straight through to `_colorTriggerWithTitle`. 4. Audit other call sites that pass a `fill`-like value through `_colorTriggerWithTitle` and decide per-site whether transparent is a valid option there. At minimum: shape Fill (line 886) and frame Background (line 1018) should accept transparent. Sticky Note background, Document background, Text color, Stroke color, etc. should not — keep them as-is. ## Affected files - `crates/hero_whiteboard_ui/static/web/js/whiteboard/selection_toolbar.js` — the only file in scope. ## Acceptance criteria - [ ] A newly created Shape (default `fill: 'transparent'`) shows a 'no fill' swatch in the selection toolbar (checkered + strike), not a solid dark color. - [ ] Opening the Fill popover for a shape exposes a 'no fill' / transparent option at the top of the swatch grid, with the same indicator visual. - [ ] Picking 'no fill' on a filled shape sets `bg.fill('transparent')` and the swatch re-renders to the 'no fill' indicator. - [ ] Picking any palette color on a transparent shape sets the fill normally. - [ ] Stroke color / Text color / Sticky color triggers are unchanged. - [ ] No regression in serialization or sync — `bg.fill()` returns whatever was last picked, including `'transparent'`.
Author
Member

Implementation Spec for Issue #143

Objective

Make the Fill color swatch in the selection toolbar honestly represent a transparent fill (using the standard checker + diagonal-strike "no fill" indicator) and give users a way to set fill back to transparent via a "No fill" entry at the top of the Fill popover. The new option must be opt-in per call site so Stroke / Text / Sticky color triggers are unchanged.

Requirements

  • When a swatch's value is 'transparent' and the call site has opted in, render the "no fill" visual instead of background: transparent.
  • When the call site has opted in, prepend a "No fill" entry to the popover that picks 'transparent'.
  • The activeColorSetter indirection must use the same indicator helper, so programmatic re-drives keep the swatch correct.
  • Only Shape Fill (_renderShape line 886) and Document Background (_renderDocument) opt in. Sticky color, Text color, Stroke color, Border color, Drawing stroke, Document text remain unchanged.
  • Drop the '#2b3035' lie at _renderShape line 857 and pass bg.fill() straight through.
  • No serialization / sync changes — bg.fill('transparent') already round-trips.

Files to Modify/Create

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/selection_toolbar.js — add _applySwatchVisual helper, thread an allowTransparent option through _colorTriggerWithTitle -> _buildColorTrigger -> _buildColorPopover, fix _renderShape fill handling, opt the two relevant call sites in.
  • crates/hero_whiteboard_ui/static/web/css/selection_toolbar.css (or wherever the toolbar CSS lives) — add .is-no-fill styling reusable by both the trigger swatch and a popover swatch.

Implementation Plan

Step 1: Add the "no fill" indicator visual (CSS)

Files: toolbar CSS file

  • Add a class .is-no-fill (scoped under both .wb-pt-color-trigger .swatch and .wb-pt-color-swatch) that paints a small checkered background using two linear-gradient layers + a diagonal red strike using a third linear-gradient. Keep the existing 1px border.
  • Sizes use percentages so a single rule works at both the 18x18 trigger and 24x24 popover sizes.
    Dependencies: none.

Step 2: Add _applySwatchVisual helper in selection_toolbar.js

Files: selection_toolbar.js

  • New private function _applySwatchVisual(swatchEl, value):
    • If value === 'transparent': swatchEl.classList.add('is-no-fill'), clear swatchEl.style.background.
    • Else: remove the class, set swatchEl.style.background = value || '#000'.
      Dependencies: Step 1.

Step 3: Thread allowTransparent through trigger / popover builders

Files: selection_toolbar.js

  • Extend _buildColorPopover(currentValue, palette, onPick, allowTransparent): when allowTransparent, prepend a wb-pt-color-swatch is-no-fill button (title=No fill, aria-label=No fill); marked is-selected when currentValue === 'transparent'; click -> onPick('transparent') then close.
  • Extend _buildColorTrigger(initial, onPick, palette, allowTransparent): replace swatch.style.background = initial || '#000' with _applySwatchVisual(swatch, initial). Same in the click handler and in activeColorSetter. Pass allowTransparent to _buildColorPopover.
  • Extend _colorTriggerWithTitle(initial, onPick, palette, tooltip, allowTransparent) and pass through. Default false so all existing call sites are byte-identical.
    Dependencies: Step 2.

Step 4: Fix _renderShape Fill picker

Files: selection_toolbar.js

  • Line 857: var fill = bg ? bg.fill() : 'transparent'; (no fake substitution).
  • Line 886: append true to the Fill _colorTriggerWithTitle call.
  • Stroke (877) and Text (904) untouched.
    Dependencies: Step 3.

Step 5: Opt Document Background in

Files: selection_toolbar.js

  • _renderDocument Background-color call: append true as the fifth argument.
  • Border color and Document text color untouched.
  • All other _colorTriggerWithTitle call sites (sticky, text, drawing stroke, kanban, etc.) keep the four-arg form.
    Dependencies: Step 3.

Step 6: Manual verification

  • New shape (default fill: 'transparent'): Fill swatch renders "no fill" indicator.
  • Fill popover first cell is "No fill"; the 18 palette colors unchanged.
  • Pick palette color -> swatch turns solid, shape fills.
  • Pick "No fill" on a filled shape -> bg.fill('transparent') set, swatch updates via activeColorSetter path.
  • Document Background trigger behaves the same.
  • Stroke / Text / Sticky / Border / Drawing stroke triggers show no "No fill" entry.
  • Reload -> fills round-trip identically.

Acceptance Criteria

  • Default-fill shape's Fill swatch renders as the no-fill indicator (checker + diagonal red strike), not #2b3035.
  • Fill popover for shapes shows "No fill" as the first entry.
  • Document Background popover shows "No fill" as the first entry.
  • Picking "No fill" on a filled shape sets bg.fill('transparent') and the trigger swatch re-renders via _applySwatchVisual.
  • Picking a palette color on a transparent shape sets the fill normally and the trigger swatch shows the solid color.
  • Stroke, Text, Sticky, Border, Drawing stroke triggers and popovers are byte-identical to before.
  • No regression in serialization or sync.
  • activeColorSetter programmatic updates correctly toggle between solid and no-fill visuals.

Notes

  • DEFAULT_COLOR_PALETTE stays untouched — the "No fill" cell is injected at popover-build time only when allowTransparent is true.
  • Per-call-site opt-in via the allowTransparent boolean keeps the Stroke/Text/Sticky paths untouched and makes future additions a one-flag change.
  • "No fill" is prepended (not appended) to match Figma/Miro/Excalidraw conventions.
  • The native <input type="color"> row in the popover stays. It cannot represent transparency, so picking from it always commits a solid color — that is the expected fallback.
  • CSS gradient sizing uses percentages so the rule looks right at both the 18x18 trigger and 24x24 popover sizes.
## Implementation Spec for Issue #143 ### Objective Make the Fill color swatch in the selection toolbar honestly represent a `transparent` fill (using the standard checker + diagonal-strike "no fill" indicator) and give users a way to set fill back to transparent via a "No fill" entry at the top of the Fill popover. The new option must be opt-in per call site so Stroke / Text / Sticky color triggers are unchanged. ### Requirements - When a swatch's value is `'transparent'` and the call site has opted in, render the "no fill" visual instead of `background: transparent`. - When the call site has opted in, prepend a "No fill" entry to the popover that picks `'transparent'`. - The `activeColorSetter` indirection must use the same indicator helper, so programmatic re-drives keep the swatch correct. - Only Shape Fill (`_renderShape` line 886) and Document Background (`_renderDocument`) opt in. Sticky color, Text color, Stroke color, Border color, Drawing stroke, Document text remain unchanged. - Drop the `'#2b3035'` lie at `_renderShape` line 857 and pass `bg.fill()` straight through. - No serialization / sync changes — `bg.fill('transparent')` already round-trips. ### Files to Modify/Create - `crates/hero_whiteboard_ui/static/web/js/whiteboard/selection_toolbar.js` — add `_applySwatchVisual` helper, thread an `allowTransparent` option through `_colorTriggerWithTitle` -> `_buildColorTrigger` -> `_buildColorPopover`, fix `_renderShape` fill handling, opt the two relevant call sites in. - `crates/hero_whiteboard_ui/static/web/css/selection_toolbar.css` (or wherever the toolbar CSS lives) — add `.is-no-fill` styling reusable by both the trigger swatch and a popover swatch. ### Implementation Plan #### Step 1: Add the "no fill" indicator visual (CSS) Files: toolbar CSS file - Add a class `.is-no-fill` (scoped under both `.wb-pt-color-trigger .swatch` and `.wb-pt-color-swatch`) that paints a small checkered background using two `linear-gradient` layers + a diagonal red strike using a third `linear-gradient`. Keep the existing 1px border. - Sizes use percentages so a single rule works at both the 18x18 trigger and 24x24 popover sizes. Dependencies: none. #### Step 2: Add `_applySwatchVisual` helper in `selection_toolbar.js` Files: `selection_toolbar.js` - New private function `_applySwatchVisual(swatchEl, value)`: - If `value === 'transparent'`: `swatchEl.classList.add('is-no-fill')`, clear `swatchEl.style.background`. - Else: remove the class, set `swatchEl.style.background = value || '#000'`. Dependencies: Step 1. #### Step 3: Thread `allowTransparent` through trigger / popover builders Files: `selection_toolbar.js` - Extend `_buildColorPopover(currentValue, palette, onPick, allowTransparent)`: when `allowTransparent`, prepend a `wb-pt-color-swatch is-no-fill` button (`title=No fill`, `aria-label=No fill`); marked `is-selected` when `currentValue === 'transparent'`; click -> `onPick('transparent')` then close. - Extend `_buildColorTrigger(initial, onPick, palette, allowTransparent)`: replace `swatch.style.background = initial || '#000'` with `_applySwatchVisual(swatch, initial)`. Same in the click handler and in `activeColorSetter`. Pass `allowTransparent` to `_buildColorPopover`. - Extend `_colorTriggerWithTitle(initial, onPick, palette, tooltip, allowTransparent)` and pass through. Default `false` so all existing call sites are byte-identical. Dependencies: Step 2. #### Step 4: Fix `_renderShape` Fill picker Files: `selection_toolbar.js` - Line 857: `var fill = bg ? bg.fill() : 'transparent';` (no fake substitution). - Line 886: append `true` to the Fill `_colorTriggerWithTitle` call. - Stroke (877) and Text (904) untouched. Dependencies: Step 3. #### Step 5: Opt Document Background in Files: `selection_toolbar.js` - `_renderDocument` Background-color call: append `true` as the fifth argument. - Border color and Document text color untouched. - All other `_colorTriggerWithTitle` call sites (sticky, text, drawing stroke, kanban, etc.) keep the four-arg form. Dependencies: Step 3. #### Step 6: Manual verification - New shape (default `fill: 'transparent'`): Fill swatch renders "no fill" indicator. - Fill popover first cell is "No fill"; the 18 palette colors unchanged. - Pick palette color -> swatch turns solid, shape fills. - Pick "No fill" on a filled shape -> `bg.fill('transparent')` set, swatch updates via `activeColorSetter` path. - Document Background trigger behaves the same. - Stroke / Text / Sticky / Border / Drawing stroke triggers show no "No fill" entry. - Reload -> fills round-trip identically. ### Acceptance Criteria - [ ] Default-fill shape's Fill swatch renders as the no-fill indicator (checker + diagonal red strike), not `#2b3035`. - [ ] Fill popover for shapes shows "No fill" as the first entry. - [ ] Document Background popover shows "No fill" as the first entry. - [ ] Picking "No fill" on a filled shape sets `bg.fill('transparent')` and the trigger swatch re-renders via `_applySwatchVisual`. - [ ] Picking a palette color on a transparent shape sets the fill normally and the trigger swatch shows the solid color. - [ ] Stroke, Text, Sticky, Border, Drawing stroke triggers and popovers are byte-identical to before. - [ ] No regression in serialization or sync. - [ ] `activeColorSetter` programmatic updates correctly toggle between solid and no-fill visuals. ### Notes - `DEFAULT_COLOR_PALETTE` stays untouched — the "No fill" cell is injected at popover-build time only when `allowTransparent` is true. - Per-call-site opt-in via the `allowTransparent` boolean keeps the Stroke/Text/Sticky paths untouched and makes future additions a one-flag change. - "No fill" is prepended (not appended) to match Figma/Miro/Excalidraw conventions. - The native `<input type="color">` row in the popover stays. It cannot represent transparency, so picking from it always commits a solid color — that is the expected fallback. - CSS gradient sizing uses percentages so the rule looks right at both the 18x18 trigger and 24x24 popover sizes.
Author
Member

Test Results

  • cargo test --workspace --lib — PASS
  • cargo fmt --check — PASS
  • cargo clippy --workspace --all-targets -- -D warnings — PASS

Notes

This fix is JS+CSS only (crates/hero_whiteboard_ui/static/web/js/whiteboard/selection_toolbar.js and crates/hero_whiteboard_ui/static/web/css/selection_toolbar.css). The Rust workspace tests do not exercise this code path; they confirm the workspace still builds, formats, and lints cleanly. Manual UI verification is required for the user-facing behavior (no-fill indicator on transparent shapes; "No fill" entry in the Fill and Document Background popovers; other color triggers unaffected).

## Test Results - `cargo test --workspace --lib` — PASS - `cargo fmt --check` — PASS - `cargo clippy --workspace --all-targets -- -D warnings` — PASS ### Notes This fix is JS+CSS only (`crates/hero_whiteboard_ui/static/web/js/whiteboard/selection_toolbar.js` and `crates/hero_whiteboard_ui/static/web/css/selection_toolbar.css`). The Rust workspace tests do not exercise this code path; they confirm the workspace still builds, formats, and lints cleanly. Manual UI verification is required for the user-facing behavior (no-fill indicator on transparent shapes; "No fill" entry in the Fill and Document Background popovers; other color triggers unaffected).
Author
Member

Implementation Summary

Fix for the misleading Fill swatch on transparent shapes and the inability to set fill back to transparent.

Changes

  • crates/hero_whiteboard_ui/static/web/css/selection_toolbar.css — added .is-no-fill rule (scoped under both .wb-pt-color-trigger .swatch and .wb-pt-color-swatch) that paints a checkered background plus a diagonal red strike, sized in percentages so it scales for both the 18x18 trigger swatch and the 24x24 popover swatch.
  • crates/hero_whiteboard_ui/static/web/js/whiteboard/selection_toolbar.js:
    • New _applySwatchVisual(swatchEl, value) helper — single source of truth for color-string vs no-fill rendering. Used everywhere the trigger swatch updates (initial render, popover pick, activeColorSetter).
    • Added an allowTransparent flag threaded through _colorTriggerWithTitle -> _buildColorTrigger -> _buildColorPopover. When set, the popover prepends a "No fill" cell that picks 'transparent'. Default is falsy so all existing 4-arg call sites are byte-identical.
    • _renderShape: dropped the '#2b3035' substitution at line 857 — fill value now flows from bg.fill() straight through. Fill picker call site opts in with allowTransparent: true.
    • _renderDocument: Background-color picker opts in. Border color and Document text color untouched.
    • All other color triggers (Stroke, Text, Sticky, Drawing stroke, Kanban, Mindmap, Document border, Document text) keep the 4-arg form and remain byte-identical.

DEFAULT_COLOR_PALETTE is unchanged — the "No fill" cell is injected per call site at popover-build time, not added to the palette.

Test Results

  • cargo test --workspace --lib — PASS
  • cargo fmt --check — PASS
  • cargo clippy --workspace --all-targets -- -D warnings — PASS

Manual Verification Required

The fix is JS+CSS only. Please verify:

  1. Create a Shape (Rect). The Fill swatch in the selection toolbar shows a checker pattern with a diagonal red strike (no longer a solid dark gray).
  2. Open the Fill popover — first cell is the "No fill" indicator. The remaining 18 palette colors are unchanged.
  3. Pick a palette color — the swatch turns solid; the shape fills.
  4. Pick "No fill" on a filled shape — the shape goes transparent and the trigger swatch flips back to the no-fill indicator.
  5. Open the Document Background picker — the same "No fill" entry is at the top.
  6. Stroke / Text / Sticky / Border / Drawing-stroke triggers do NOT show "No fill".
  7. Reload the page — fills round-trip identically.
## Implementation Summary Fix for the misleading Fill swatch on transparent shapes and the inability to set fill back to transparent. ### Changes - **`crates/hero_whiteboard_ui/static/web/css/selection_toolbar.css`** — added `.is-no-fill` rule (scoped under both `.wb-pt-color-trigger .swatch` and `.wb-pt-color-swatch`) that paints a checkered background plus a diagonal red strike, sized in percentages so it scales for both the 18x18 trigger swatch and the 24x24 popover swatch. - **`crates/hero_whiteboard_ui/static/web/js/whiteboard/selection_toolbar.js`**: - New `_applySwatchVisual(swatchEl, value)` helper — single source of truth for color-string vs no-fill rendering. Used everywhere the trigger swatch updates (initial render, popover pick, `activeColorSetter`). - Added an `allowTransparent` flag threaded through `_colorTriggerWithTitle` -> `_buildColorTrigger` -> `_buildColorPopover`. When set, the popover prepends a "No fill" cell that picks `'transparent'`. Default is falsy so all existing 4-arg call sites are byte-identical. - `_renderShape`: dropped the `'#2b3035'` substitution at line 857 — fill value now flows from `bg.fill()` straight through. Fill picker call site opts in with `allowTransparent: true`. - `_renderDocument`: Background-color picker opts in. Border color and Document text color untouched. - All other color triggers (Stroke, Text, Sticky, Drawing stroke, Kanban, Mindmap, Document border, Document text) keep the 4-arg form and remain byte-identical. `DEFAULT_COLOR_PALETTE` is unchanged — the "No fill" cell is injected per call site at popover-build time, not added to the palette. ### Test Results - `cargo test --workspace --lib` — PASS - `cargo fmt --check` — PASS - `cargo clippy --workspace --all-targets -- -D warnings` — PASS ### Manual Verification Required The fix is JS+CSS only. Please verify: 1. Create a Shape (Rect). The Fill swatch in the selection toolbar shows a checker pattern with a diagonal red strike (no longer a solid dark gray). 2. Open the Fill popover — first cell is the "No fill" indicator. The remaining 18 palette colors are unchanged. 3. Pick a palette color — the swatch turns solid; the shape fills. 4. Pick "No fill" on a filled shape — the shape goes transparent and the trigger swatch flips back to the no-fill indicator. 5. Open the Document Background picker — the same "No fill" entry is at the top. 6. Stroke / Text / Sticky / Border / Drawing-stroke triggers do NOT show "No fill". 7. Reload the page — fills round-trip identically.
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#143
No description provided.