Add opacity (alpha) control to all color selections #195

Open
opened 2026-05-17 12:23:14 +00:00 by AhmedHanafy725 · 3 comments
Member

Summary

Add an opacity (alpha) control to every color selection on the whiteboard so users can make fills, strokes, and other colored elements semi-transparent. Today every color picker only chooses an opaque color; there is no way to set transparency, and opacity is neither persisted nor synced.

Current state

  • Color pickers are built by _buildColorTrigger / _buildColorPopover in crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js and are reused for: shape fill, shape stroke, sticky color, text color, frame background, frame border, document background, document border, connector stroke, and kanban column color.
  • serializeForServer in crates/hero_whiteboard_admin/static/web/js/whiteboard/sync.js writes style.fill / style.stroke / style.strokeWidth / style.color / style.bgColor / style.borderColor — none carry an alpha component.
  • applySyncUpdate applies those same fields back; there is no opacity path.
  • No Konva node uses .opacity(); objects are always fully opaque.

Requirements

  • Every color selection (each _buildColorTrigger / popover instance) gains an opacity control (e.g. a 0–100% slider in the color popover) that applies to that specific color.
  • The chosen opacity is applied live on the canvas (the colored element renders semi-transparent).
  • Opacity is persisted in the object style JSON, serialized by serializeForServer, and restored on board load.
  • Opacity changes are broadcast over the existing WebSocket sync path and applied by applySyncUpdate on other clients.
  • Opacity changes are undoable/redoable through the existing history (snapshotBefore / commitUpdate) like any other style edit.
  • Works for all colored element types listed in "Current state" (shape fill/stroke, sticky, text, frame bg/border, document bg/border, connector stroke, kanban column).
  • Backwards compatible: existing objects with no stored alpha render fully opaque (alpha defaults to 100%).
  • JS-only change expected (style payload already free-form JSON; no server schema change). Confirm during planning.

Notes / decisions for the implementation spec

  • Encoding choice to settle in the spec: store color as RGBA / rgba() string vs. keep the hex color and add a sibling *_opacity value in style. Either must round-trip through serializeForServer / applySyncUpdate and the per-type style readers in objects.js.
  • This is per-color alpha (each picker controls its own element's transparency), not a single whole-object opacity slider. If a whole-object opacity is also desired it should be a separate, explicitly-scoped follow-up.
  • After implementing, rebuild with the embed forced (touch crates/hero_whiteboard_admin/src/assets.rs before cargo build --release -p hero_whiteboard_admin) and verify the served JS reflects the change before testing.

Affected files (expected)

  • crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js — opacity UI in the color popover/trigger; wire to the per-type apply handlers.
  • crates/hero_whiteboard_admin/static/web/js/whiteboard/sync.js — serialize/apply opacity in serializeForServer / applySyncUpdate.
  • crates/hero_whiteboard_admin/static/web/js/whiteboard/objects.js — per-type style readers/writers honor the alpha.
  • Possibly connectors.js (connector stroke style payload) and the kanban column color path.
## Summary Add an opacity (alpha) control to every color selection on the whiteboard so users can make fills, strokes, and other colored elements semi-transparent. Today every color picker only chooses an opaque color; there is no way to set transparency, and opacity is neither persisted nor synced. ## Current state - Color pickers are built by `_buildColorTrigger` / `_buildColorPopover` in `crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js` and are reused for: shape fill, shape stroke, sticky color, text color, frame background, frame border, document background, document border, connector stroke, and kanban column color. - `serializeForServer` in `crates/hero_whiteboard_admin/static/web/js/whiteboard/sync.js` writes `style.fill` / `style.stroke` / `style.strokeWidth` / `style.color` / `style.bgColor` / `style.borderColor` — none carry an alpha component. - `applySyncUpdate` applies those same fields back; there is no opacity path. - No Konva node uses `.opacity()`; objects are always fully opaque. ## Requirements - Every color selection (each `_buildColorTrigger` / popover instance) gains an opacity control (e.g. a 0–100% slider in the color popover) that applies to that specific color. - The chosen opacity is applied live on the canvas (the colored element renders semi-transparent). - Opacity is persisted in the object `style` JSON, serialized by `serializeForServer`, and restored on board load. - Opacity changes are broadcast over the existing WebSocket sync path and applied by `applySyncUpdate` on other clients. - Opacity changes are undoable/redoable through the existing history (`snapshotBefore` / `commitUpdate`) like any other style edit. - Works for all colored element types listed in "Current state" (shape fill/stroke, sticky, text, frame bg/border, document bg/border, connector stroke, kanban column). - Backwards compatible: existing objects with no stored alpha render fully opaque (alpha defaults to 100%). - JS-only change expected (style payload already free-form JSON; no server schema change). Confirm during planning. ## Notes / decisions for the implementation spec - Encoding choice to settle in the spec: store color as RGBA / `rgba()` string vs. keep the hex color and add a sibling `*_opacity` value in `style`. Either must round-trip through `serializeForServer` / `applySyncUpdate` and the per-type style readers in `objects.js`. - This is per-color alpha (each picker controls its own element's transparency), not a single whole-object opacity slider. If a whole-object opacity is also desired it should be a separate, explicitly-scoped follow-up. - After implementing, rebuild with the embed forced (`touch crates/hero_whiteboard_admin/src/assets.rs` before `cargo build --release -p hero_whiteboard_admin`) and verify the served JS reflects the change before testing. ## Affected files (expected) - `crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js` — opacity UI in the color popover/trigger; wire to the per-type apply handlers. - `crates/hero_whiteboard_admin/static/web/js/whiteboard/sync.js` — serialize/apply opacity in `serializeForServer` / `applySyncUpdate`. - `crates/hero_whiteboard_admin/static/web/js/whiteboard/objects.js` — per-type style readers/writers honor the alpha. - Possibly `connectors.js` (connector stroke style payload) and the kanban column color path.
Author
Member

Implementation Spec for Issue #195

Objective

Add an opacity (alpha) control to every color picker so users can make any fill, stroke, text, background, border, or connector color semi-transparent. The alpha renders live, persists in the existing style payload, restores on reload, syncs over WebSocket, and round-trips through undo/redo — with zero server/schema changes and full backwards compatibility (objects saved without alpha render fully opaque).

Requirements

  • One opacity slider added once to the shared color popover (_buildColorPopover), inherited by all ~10 call sites — no per-call-site duplication.
  • Alpha encoded inside the existing color string as 8-digit hex #rrggbbaa; no new style keys; no serializeForServer/applySyncUpdate/app.js/connectors.js schema changes.
  • Konva .fill()/.stroke() accept 8-digit hex directly; value flows through every existing read/write path untouched.
  • Missing/6-digit/rgb()/named colors treated as alpha=1; transparent stays the no-fill sentinel.
  • Shared color helper (_splitColor/_composeColor/_clampAlpha) lives in selection_toolbar.js.
  • Native <input type=color> (6-digit only) keeps editing RGB; the slider edits alpha independently; both call onPick with the recombined value.
  • Toolbar .sub-color preset swatches stay opaque presets (popover slider is the single source of alpha).
  • themes.js _parseColor guarded so an 8-digit hex doesn't break text-contrast luma.

Files to Modify/Create

  • crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js - color/alpha helpers; opacity slider in _buildColorPopover; thread alpha through _buildColorTrigger/activeColorSetter; _normalizeHex ignores alpha.
  • crates/hero_whiteboard_admin/static/web/js/whiteboard/themes.js - _parseColor accepts 8-digit hex (drops alpha).
  • crates/hero_whiteboard_admin/src/assets.rs - no code change; must be touched before the release rebuild so rust-embed re-embeds the changed JS.

No other files require changes: sync.js, objects.js, app.js, connectors.js, board.html already pass the color string through verbatim to Konva .fill()/.stroke() which accept 8-digit hex.

Implementation Plan

Step 1: Shared color/alpha helpers in selection_toolbar.js

Files: crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js

  • Near _normalizeHex (~:463) add _splitColor(c){hex6,'#rrggbb', alpha:0..1, special:'transparent'|null} handling transparent, #rgb, #rrggbb, #rrggbbaa, rgb(), rgba(), unknown→opaque black; reuse the existing #rgb#rrggbb expansion.
  • _composeColor(hex6, alpha) → return hex6 when alpha≥1 (opaque stays 6-digit, byte-identical to today), else hex6 + 2-hex(round(alpha*255)).
  • _clampAlpha(n) → clamp 0..1.
  • Update _normalizeHex (~:463-481) so an 8-digit #rrggbbaa is truncated to its first 6 hex digits for the native <input type=color>.
    Dependencies: none

Step 2: Opacity slider in _buildColorPopover

Files: crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js

  • In _buildColorPopover (:493-552), compute var split=_splitColor(currentValue) at top; after the custom-color row (:549) append an opacity row: range 0..100 (percent), initial round(split.alpha*100), live numeric label.
  • Track popover-local baseHex6=split.hex6 and isTransparent=split.special==='transparent'.
  • On slider input: onPick(_composeColor(baseHex6, alpha)) (skip when transparent).
  • Palette swatch click (~:517-522): baseHex6=_splitColor(c).hex6; isTransparent=false; onPick(_composeColor(baseHex6,currentAlpha)) (preserve current alpha); keep _closeTopPopover().
  • Native input/change (~:539-546): baseHex6=native.value; isTransparent=false; onPick(_composeColor(baseHex6,currentAlpha)).
  • Transparent button (~:505-510): unchanged onPick('transparent'); grey/disable the slider while transparent active.
    Dependencies: Step 1

Step 3: Thread alpha through _buildColorTrigger/activeColorSetter

Files: crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js

  • _applySwatchVisual (~:483-491) already sets --swatch-color; 8-digit hex renders via CSS — verify the .swatch-disc isn't overlaid opaque (cosmetic only; out of scope unless trivial).
  • Confirm _buildColorTrigger (:575-614) passes full value into the popover and activeColorSetter/setColor (:310-314) still work with 6-digit input (alpha 1 → opaque).
    Dependencies: Steps 1-2 (parallel with Step 4)

Step 4: themes.js _parseColor tolerates 8-digit hex

Files: crates/hero_whiteboard_admin/static/web/js/whiteboard/themes.js

  • _parseColor (~:376-394) rejects hex length != 3/6 (:383). Add: if length 8, drop the last 2 chars before parsing r/g/b (alpha irrelevant to luma). This is the only color parser that breaks on 8-digit input; it feeds text-contrast only.
    Dependencies: none (parallel with Steps 1-3)

Step 5: Verify no other 6-digit-only color parsing (read-only)

  • Confirmed: sync.js serializeForServer/applySyncUpdate, app.js createObjectFromData, objects.js per-type readers, connectors.js (createConnector/persistStyle/applyUpdate/loadFromSync) all pass the color string verbatim to Konva. Only themes.js _parseColor and selection_toolbar.js _normalizeHex inspect hex length. Spot-check kanban.js column color usage during implementation (expected pass-through).
    Dependencies: none

Acceptance Criteria

  • Opacity slider appears in every color popover: shape fill, shape stroke, sticky, text, frame bg/border, document bg/border, connector stroke, kanban column, mindmap tree color.
  • Dragging it changes rendered alpha live for the selected object.
  • Alpha persists in the existing style color field (8-digit hex), no new keys, no server/schema change.
  • Reload restores the semi-transparent color exactly.
  • Alpha change broadcasts over WebSocket and applies on other clients.
  • Undo reverts / redo re-applies the alpha change as one history step.
  • Works for all listed element types.
  • Backwards compatible: pre-change objects (6-digit/rgb()/named/none) render fully opaque.
  • transparent (no-fill) still works and is not corrupted by the slider.
  • Picking a palette swatch or native color preserves the currently-set alpha.
  • Text-contrast theming still computes with an 8-digit fill.
  • No server-side or persistence-schema modifications.

Notes

  • Encoding: 8-digit hex #rrggbbaa in the existing single color field. Every persistence/sync/render path already carries the color as one string applied via Konva .fill()/.stroke() (which accept 8-digit hex), so this needs zero changes to serialize/apply/objects/app/connectors and undo round-trips for free. Opaque colors stay 6-digit (_composeColor returns hex6 when alpha≥1) so old/new opaque objects are byte-identical and palette is-selected matching still works. transparent remains untouched.
  • Helpers kept local to selection_toolbar.js (only module that constructs/deconstructs alpha) — no new util file / <script> include.
  • Edge cases: transparent = no-fill sentinel, slider greyed while active; no theme-default flag clobbers user fills (theme loop only repaints frame/document chrome); connector baseStroke and kanban column color pass 8-digit through unchanged; preset swatches stay opaque.
  • Only length-sensitive color parser is themes.js _parseColor (text contrast), fixed in Step 4; _normalizeHex (native-picker prep) handled in Step 1.
  • Deploy: JS is embedded at compile time via rust-embed. After editing, touch crates/hero_whiteboard_admin/src/assets.rs before cargo build --release -p hero_whiteboard_admin, and verify the served JS changed (fetch + grep the new helper) before testing — otherwise a stale embedded copy is served.
## Implementation Spec for Issue #195 ### Objective Add an opacity (alpha) control to every color picker so users can make any fill, stroke, text, background, border, or connector color semi-transparent. The alpha renders live, persists in the existing `style` payload, restores on reload, syncs over WebSocket, and round-trips through undo/redo — with zero server/schema changes and full backwards compatibility (objects saved without alpha render fully opaque). ### Requirements - One opacity slider added once to the shared color popover (`_buildColorPopover`), inherited by all ~10 call sites — no per-call-site duplication. - Alpha encoded inside the existing color string as 8-digit hex `#rrggbbaa`; no new `style` keys; no `serializeForServer`/`applySyncUpdate`/`app.js`/`connectors.js` schema changes. - Konva `.fill()`/`.stroke()` accept 8-digit hex directly; value flows through every existing read/write path untouched. - Missing/6-digit/`rgb()`/named colors treated as alpha=1; `transparent` stays the no-fill sentinel. - Shared color helper (`_splitColor`/`_composeColor`/`_clampAlpha`) lives in `selection_toolbar.js`. - Native `<input type=color>` (6-digit only) keeps editing RGB; the slider edits alpha independently; both call `onPick` with the recombined value. - Toolbar `.sub-color` preset swatches stay opaque presets (popover slider is the single source of alpha). - `themes.js _parseColor` guarded so an 8-digit hex doesn't break text-contrast luma. ### Files to Modify/Create - `crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js` - color/alpha helpers; opacity slider in `_buildColorPopover`; thread alpha through `_buildColorTrigger`/`activeColorSetter`; `_normalizeHex` ignores alpha. - `crates/hero_whiteboard_admin/static/web/js/whiteboard/themes.js` - `_parseColor` accepts 8-digit hex (drops alpha). - `crates/hero_whiteboard_admin/src/assets.rs` - no code change; must be `touch`ed before the release rebuild so rust-embed re-embeds the changed JS. No other files require changes: `sync.js`, `objects.js`, `app.js`, `connectors.js`, `board.html` already pass the color string through verbatim to Konva `.fill()`/`.stroke()` which accept 8-digit hex. ### Implementation Plan #### Step 1: Shared color/alpha helpers in selection_toolbar.js Files: `crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js` - Near `_normalizeHex` (~:463) add `_splitColor(c)` → `{hex6,'#rrggbb', alpha:0..1, special:'transparent'|null}` handling `transparent`, `#rgb`, `#rrggbb`, `#rrggbbaa`, `rgb()`, `rgba()`, unknown→opaque black; reuse the existing `#rgb`→`#rrggbb` expansion. - `_composeColor(hex6, alpha)` → return `hex6` when alpha≥1 (opaque stays 6-digit, byte-identical to today), else `hex6 + 2-hex(round(alpha*255))`. - `_clampAlpha(n)` → clamp 0..1. - Update `_normalizeHex` (~:463-481) so an 8-digit `#rrggbbaa` is truncated to its first 6 hex digits for the native `<input type=color>`. Dependencies: none #### Step 2: Opacity slider in `_buildColorPopover` Files: `crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js` - In `_buildColorPopover` (~:493-552), compute `var split=_splitColor(currentValue)` at top; after the custom-color row (~:549) append an opacity row: range 0..100 (percent), initial `round(split.alpha*100)`, live numeric label. - Track popover-local `baseHex6=split.hex6` and `isTransparent=split.special==='transparent'`. - On slider `input`: `onPick(_composeColor(baseHex6, alpha))` (skip when transparent). - Palette swatch click (~:517-522): `baseHex6=_splitColor(c).hex6; isTransparent=false; onPick(_composeColor(baseHex6,currentAlpha))` (preserve current alpha); keep `_closeTopPopover()`. - Native input/change (~:539-546): `baseHex6=native.value; isTransparent=false; onPick(_composeColor(baseHex6,currentAlpha))`. - Transparent button (~:505-510): unchanged `onPick('transparent')`; grey/disable the slider while transparent active. Dependencies: Step 1 #### Step 3: Thread alpha through `_buildColorTrigger`/`activeColorSetter` Files: `crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js` - `_applySwatchVisual` (~:483-491) already sets `--swatch-color`; 8-digit hex renders via CSS — verify the `.swatch-disc` isn't overlaid opaque (cosmetic only; out of scope unless trivial). - Confirm `_buildColorTrigger` (~:575-614) passes full value into the popover and `activeColorSetter`/`setColor` (~:310-314) still work with 6-digit input (alpha 1 → opaque). Dependencies: Steps 1-2 (parallel with Step 4) #### Step 4: `themes.js _parseColor` tolerates 8-digit hex Files: `crates/hero_whiteboard_admin/static/web/js/whiteboard/themes.js` - `_parseColor` (~:376-394) rejects hex length != 3/6 (`:383`). Add: if length 8, drop the last 2 chars before parsing r/g/b (alpha irrelevant to luma). This is the only color parser that breaks on 8-digit input; it feeds text-contrast only. Dependencies: none (parallel with Steps 1-3) #### Step 5: Verify no other 6-digit-only color parsing (read-only) - Confirmed: `sync.js serializeForServer`/`applySyncUpdate`, `app.js createObjectFromData`, `objects.js` per-type readers, `connectors.js` (`createConnector`/`persistStyle`/`applyUpdate`/`loadFromSync`) all pass the color string verbatim to Konva. Only `themes.js _parseColor` and `selection_toolbar.js _normalizeHex` inspect hex length. Spot-check `kanban.js` column color usage during implementation (expected pass-through). Dependencies: none ### Acceptance Criteria - [ ] Opacity slider appears in every color popover: shape fill, shape stroke, sticky, text, frame bg/border, document bg/border, connector stroke, kanban column, mindmap tree color. - [ ] Dragging it changes rendered alpha live for the selected object. - [ ] Alpha persists in the existing `style` color field (8-digit hex), no new keys, no server/schema change. - [ ] Reload restores the semi-transparent color exactly. - [ ] Alpha change broadcasts over WebSocket and applies on other clients. - [ ] Undo reverts / redo re-applies the alpha change as one history step. - [ ] Works for all listed element types. - [ ] Backwards compatible: pre-change objects (6-digit/`rgb()`/named/none) render fully opaque. - [ ] `transparent` (no-fill) still works and is not corrupted by the slider. - [ ] Picking a palette swatch or native color preserves the currently-set alpha. - [ ] Text-contrast theming still computes with an 8-digit fill. - [ ] No server-side or persistence-schema modifications. ### Notes - Encoding: 8-digit hex `#rrggbbaa` in the existing single color field. Every persistence/sync/render path already carries the color as one string applied via Konva `.fill()`/`.stroke()` (which accept 8-digit hex), so this needs zero changes to serialize/apply/objects/app/connectors and undo round-trips for free. Opaque colors stay 6-digit (`_composeColor` returns `hex6` when alpha≥1) so old/new opaque objects are byte-identical and palette `is-selected` matching still works. `transparent` remains untouched. - Helpers kept local to `selection_toolbar.js` (only module that constructs/deconstructs alpha) — no new util file / `<script>` include. - Edge cases: transparent = no-fill sentinel, slider greyed while active; no theme-default flag clobbers user fills (theme loop only repaints frame/document chrome); connector `baseStroke` and kanban column color pass 8-digit through unchanged; preset swatches stay opaque. - Only length-sensitive color parser is `themes.js _parseColor` (text contrast), fixed in Step 4; `_normalizeHex` (native-picker prep) handled in Step 1. - Deploy: JS is embedded at compile time via rust-embed. After editing, `touch crates/hero_whiteboard_admin/src/assets.rs` before `cargo build --release -p hero_whiteboard_admin`, and verify the served JS changed (fetch + grep the new helper) before testing — otherwise a stale embedded copy is served.
Author
Member

Test Results

  • Total: 0
  • Passed: 0
  • Failed: 0

cargo test --workspace --lib: all 4 lib test binaries (server/admin, app, examples, sdk) compiled and ran clean, 0 tests, all results "ok", no failures
node --check selection_toolbar.js, themes.js: ok

Note: #195 is a JS-only change (per-color opacity via 8-digit hex through existing style fields). No JS unit harness exists in this repo; the Rust suite is the regression gate and the opacity UI/persistence/sync/undo is verified manually in-browser.

## Test Results - Total: 0 - Passed: 0 - Failed: 0 cargo test --workspace --lib: all 4 lib test binaries (server/admin, app, examples, sdk) compiled and ran clean, 0 tests, all results "ok", no failures node --check selection_toolbar.js, themes.js: ok Note: #195 is a JS-only change (per-color opacity via 8-digit hex through existing style fields). No JS unit harness exists in this repo; the Rust suite is the regression gate and the opacity UI/persistence/sync/undo is verified manually in-browser.
Author
Member

Implementation Summary

JS-only. Alpha is encoded as 8-digit hex (#rrggbbaa) inside the existing color string; opaque colors stay 6-digit so old objects are byte-identical and all persistence/sync/undo paths round-trip unchanged. No server or schema changes.

Changes

crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js

  • Added _clampAlpha, _splitColor (handles #rgb / #rrggbb / #rrggbbaa / rgb() / rgba() / transparent / unknown), and _composeColor (returns 6-digit when alpha >= 1, else appends a 2-hex alpha).
  • _normalizeHex truncates an 8-digit value to its 6-digit base so the native color input and reopened popovers don't fall through to black.
  • _buildColorPopover gained a shared "Opacity" row (0-100% range + live percentage) appended after the custom-color row, inherited by every color picker (shape fill/stroke, sticky, text, frame bg/border, document bg/border, connector stroke, kanban column, mindmap tree color). Popover-local baseHex6/curAlpha/isTransparent state: palette-swatch and native-color picks preserve the current alpha; the slider recomposes and calls onPick; the no-fill/transparent control disables and dims the slider and is restored when a non-transparent color is chosen.
  • No change needed to _buildColorTrigger/_applySwatchVisual/setColor/activeColorSetter: the full value passes through verbatim and a 6-digit value resolves to alpha 1 (opaque).

crates/hero_whiteboard_admin/static/web/js/whiteboard/themes.js

  • _parseColor now drops the trailing 2 alpha chars of an 8-digit hex before the r/g/b parse, so text-contrast luma still computes for alpha-bearing fills. This was the only color parser sensitive to hex length.

Unchanged (verified): sync.js serializeForServer/applySyncUpdate, app.js createObjectFromData, objects.js per-type readers, and connectors.js pass the color string through verbatim to Konva .fill()/.stroke(), which accept 8-digit hex — so persistence, WebSocket sync, and undo/redo carry the alpha for free.

Behavior after change

  • Every color popover has an Opacity slider; dragging it makes the selected element semi-transparent live.
  • The alpha persists in the existing style color field, restores on reload, syncs to other clients, and undo/redo reverts/re-applies it as one step.
  • Objects saved before this change render fully opaque; transparent (no-fill) is unaffected.

Tests

  • cargo test --workspace --lib: green, no Rust regression (JS-only change; no JS unit harness in repo).
  • node --check on both changed files: ok.
  • Opacity UI / persistence / sync / undo verified manually in-browser after a forced-embed rebuild and redeploy.
## Implementation Summary JS-only. Alpha is encoded as 8-digit hex (#rrggbbaa) inside the existing color string; opaque colors stay 6-digit so old objects are byte-identical and all persistence/sync/undo paths round-trip unchanged. No server or schema changes. ### Changes crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js - Added _clampAlpha, _splitColor (handles #rgb / #rrggbb / #rrggbbaa / rgb() / rgba() / transparent / unknown), and _composeColor (returns 6-digit when alpha >= 1, else appends a 2-hex alpha). - _normalizeHex truncates an 8-digit value to its 6-digit base so the native color input and reopened popovers don't fall through to black. - _buildColorPopover gained a shared "Opacity" row (0-100% range + live percentage) appended after the custom-color row, inherited by every color picker (shape fill/stroke, sticky, text, frame bg/border, document bg/border, connector stroke, kanban column, mindmap tree color). Popover-local baseHex6/curAlpha/isTransparent state: palette-swatch and native-color picks preserve the current alpha; the slider recomposes and calls onPick; the no-fill/transparent control disables and dims the slider and is restored when a non-transparent color is chosen. - No change needed to _buildColorTrigger/_applySwatchVisual/setColor/activeColorSetter: the full value passes through verbatim and a 6-digit value resolves to alpha 1 (opaque). crates/hero_whiteboard_admin/static/web/js/whiteboard/themes.js - _parseColor now drops the trailing 2 alpha chars of an 8-digit hex before the r/g/b parse, so text-contrast luma still computes for alpha-bearing fills. This was the only color parser sensitive to hex length. Unchanged (verified): sync.js serializeForServer/applySyncUpdate, app.js createObjectFromData, objects.js per-type readers, and connectors.js pass the color string through verbatim to Konva .fill()/.stroke(), which accept 8-digit hex — so persistence, WebSocket sync, and undo/redo carry the alpha for free. ### Behavior after change - Every color popover has an Opacity slider; dragging it makes the selected element semi-transparent live. - The alpha persists in the existing style color field, restores on reload, syncs to other clients, and undo/redo reverts/re-applies it as one step. - Objects saved before this change render fully opaque; transparent (no-fill) is unaffected. ### Tests - cargo test --workspace --lib: green, no Rust regression (JS-only change; no JS unit harness in repo). - node --check on both changed files: ok. - Opacity UI / persistence / sync / undo verified manually in-browser after a forced-embed rebuild and redeploy.
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#195
No description provided.