Emoji support: picker tool to place emojis on the board (Miro-style) #43

Open
opened 2026-04-22 08:41:07 +00:00 by AhmedHanafy725 · 2 comments
Member

Add Miro-style emoji support to the whiteboard. Users should be able to pick an emoji from a picker and place it on the canvas as a standalone object (draggable, resizable, rotatable, persisted).

Scope

  • New toolbar button (smiley icon) between existing tools (e.g. next to Text).
  • Clicking the button opens an emoji picker popover anchored to the button.
  • Picker has:
    • A search input at the top (filters by keyword/name).
    • Category tabs/rows: Smileys, People, Animals, Food, Activities, Travel, Objects, Symbols, Flags.
    • A grid of emojis (~6-8 cols). Clicking one selects it.
  • After selecting, the cursor enters "place emoji" mode; clicking the canvas drops the emoji at that point and switches back to the select tool.
  • Emojis are rendered as Konva Text nodes inside a Group named object emoji, with a default font size so they read well on the board.
  • All normal object semantics apply: drag, resize (uniform — clamped floor), rotate, Delete key, eraser, undo/redo, multi-user sync.
  • Persistence: reuse the existing objects table — store as { type: 'emoji', data: { char: '\ud83d\ude04' }, width, height, ... }. No schema migration needed; data is already a JSON blob.
  • Keyboard shortcut (e.g. : or ctrl/cmd+. like Figma) to open the picker.
  • Escape closes the picker without placing anything.

Non-goals (for this issue)

  • Emoji reactions on other objects (separate feature).
  • Emoji autocomplete inside text / sticky / document editors (separate feature).
  • Custom emoji uploads / Slack-style :shortcode: expansion.
  • External emoji library dependency — ship a curated static list (~300 emojis across categories) baked into the picker module to stay vanilla-JS.

Acceptance

  • Clicking the toolbar emoji button opens the picker.
  • Search filters the grid live.
  • Clicking a grid cell then clicking the canvas places the emoji at the click position.
  • Placed emoji persists across reload.
  • Emoji is draggable, resizable (uniform scale), rotatable, deletable, eraseable, selectable via rubber-band.
  • Keyboard : opens the picker when no text input is focused.
  • Escape closes the picker.
Add Miro-style emoji support to the whiteboard. Users should be able to pick an emoji from a picker and place it on the canvas as a standalone object (draggable, resizable, rotatable, persisted). ## Scope - New toolbar button (smiley icon) between existing tools (e.g. next to Text). - Clicking the button opens an emoji picker popover anchored to the button. - Picker has: - A search input at the top (filters by keyword/name). - Category tabs/rows: Smileys, People, Animals, Food, Activities, Travel, Objects, Symbols, Flags. - A grid of emojis (`~6-8 cols`). Clicking one selects it. - After selecting, the cursor enters "place emoji" mode; clicking the canvas drops the emoji at that point and switches back to the select tool. - Emojis are rendered as Konva Text nodes inside a Group named `object emoji`, with a default font size so they read well on the board. - All normal object semantics apply: drag, resize (uniform — clamped floor), rotate, Delete key, eraser, undo/redo, multi-user sync. - Persistence: reuse the existing `objects` table — store as `{ type: 'emoji', data: { char: '\ud83d\ude04' }, width, height, ... }`. No schema migration needed; `data` is already a JSON blob. - Keyboard shortcut (e.g. `:` or `ctrl/cmd+.` like Figma) to open the picker. - Escape closes the picker without placing anything. ## Non-goals (for this issue) - Emoji reactions on other objects (separate feature). - Emoji autocomplete inside text / sticky / document editors (separate feature). - Custom emoji uploads / Slack-style `:shortcode:` expansion. - External emoji library dependency — ship a curated static list (~300 emojis across categories) baked into the picker module to stay vanilla-JS. ## Acceptance - Clicking the toolbar emoji button opens the picker. - Search filters the grid live. - Clicking a grid cell then clicking the canvas places the emoji at the click position. - Placed emoji persists across reload. - Emoji is draggable, resizable (uniform scale), rotatable, deletable, eraseable, selectable via rubber-band. - Keyboard `:` opens the picker when no text input is focused. - Escape closes the picker.
Author
Member

Implementation Spec for Issue #43

Objective

Add a Miro-style emoji picker tool that lets the user pick an emoji from a curated 300-emoji / 9-category grid and click-to-place it on the canvas as a first-class emoji object (drag, resize, rotate, delete, erase, undo/redo, multi-user sync, persist across reload).

Requirements

  • New toolbar button data-tool="emoji" (bi-emoji-smile icon) inserted after the eraser divider on the left toolbar.
  • Clicking the button opens an emoji-picker popover (separate from the normal subtoolbar because the grid needs ~340 px width).
  • Popover: search input + 9 category tabs + 8-column emoji grid + ~300 emojis.
  • Clicking a grid cell arms an emoji; clicking the canvas drops the emoji at that point and returns to the select tool (Miro parity).
  • Emoji renders as a Konva.Group name="object emoji" containing a Konva.Text at fontSize: 48 plus an invisible .bg hit rect so the generic transformer / rubber-band / eraser / applyTransform paths work automatically.
  • Normal object semantics: drag, uniform resize (fontSize scales), rotate, Delete key, eraser, undo/redo, multi-user sync, rubber-band select, copy/paste/duplicate, persist across reload.
  • Keyboard shortcut : opens the picker (when no INPUT/TEXTAREA/contentEditable has focus). Escape closes.
  • No external library; ~300 emojis across 9 categories baked into a new emoji_picker.js module.
  • No server changes — reuse the existing objects table with type='emoji' and data={char, fontSize}.

Files to modify / create

New

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

Modify

  • crates/hero_whiteboard_ui/templates/web/board.html — toolbar button + popover markup + script include.
  • crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js'emoji' branch in onMouseDown + setEmojiChar / getEmojiChar helpers.
  • crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.jscreateEmoji(); 'emoji' case in applyTransform.
  • crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.js'emoji' branches in serializeForServer + applySyncUpdate.
  • crates/hero_whiteboard_ui/static/web/js/whiteboard/app.js'emoji' case in createObjectFromData; call WhiteboardEmojiPicker.init() once.
  • crates/hero_whiteboard_ui/static/web/js/whiteboard/shortcuts.js':' shortcut; Escape closes picker.
  • crates/hero_whiteboard_ui/static/web/js/whiteboard/toolbar.js — when the emoji tool is clicked, open the picker (and close it when switching to any other tool).
  • crates/hero_whiteboard_ui/static/web/css/whiteboard.css.wb-emoji-picker styles (popover + tabs + grid + search).

Implementation Plan

Step 1 — Create emoji_picker.js

Files: crates/hero_whiteboard_ui/static/web/js/whiteboard/emoji_picker.js

  • New IIFE WhiteboardEmojiPicker exposing { init, open, close, toggle, isOpen }.
  • CATEGORIES: ordered array of 9 {id, label, icon}smileys, animals, food, activities, travel, objects, symbols, flags, hearts. Hardcode ~33 emojis per category (~300 total) as Unicode chars.
  • Small KEYWORDS map ({char: 'comma,separated,keywords'}) for the most common emojis to make search meaningful. If an emoji has no keyword, it is reachable only via its category tab — acceptable.
  • open(): builds the DOM lazily, positions the popover next to the emoji tool button (reuse btn.getBoundingClientRect()), shows it, focuses the search input, sets the toolbar to the emoji tool.
  • close(): hides popover, clears selectedEmoji, disarms via WhiteboardTools.setEmojiChar(null).
  • render(): rebuilds the tab strip (active class on current tab) and the grid (if searchQuery non-empty, match across all categories via the keyword map + direct char match; else show the active category).
  • Cell click: sets selectedEmoji, calls WhiteboardTools.setEmojiChar(char), highlights the cell. Popover stays open so the user can place multiple copies.
  • Outside-click: a document.addEventListener('mousedown', ...) installed while open closes the popover if the target is not inside it and not the toolbar button.
    Dependencies: none.

Step 2 — Toolbar button + popover container in board.html

Files: crates/hero_whiteboard_ui/templates/web/board.html

  • Insert <button class="tool-btn" data-tool="emoji" title="Emoji (:)"><i class="bi bi-emoji-smile"></i></button> after the eraser divider.
  • Before <!-- Whiteboard Canvas -->, add the empty popover container <div class="wb-emoji-picker" id="emoji-picker" style="display:none;"> <input id="emoji-search"> <div id="emoji-tabs"></div> <div id="emoji-grid"></div> </div>.
  • Add <script src="{{ base_path }}/js/whiteboard/emoji_picker.js"></script> before sync.js.
    Dependencies: Step 1.

Step 3 — Toolbar integration in toolbar.js + app.js

Files: toolbar.js, app.js

  • In WhiteboardToolbar.init()'s data-tool click handler, after calling WhiteboardTools.setTool(tool):
    if (tool === 'emoji' && typeof WhiteboardEmojiPicker !== 'undefined') {
        WhiteboardEmojiPicker.open();
    } else if (typeof WhiteboardEmojiPicker !== 'undefined' && WhiteboardEmojiPicker.isOpen()) {
        WhiteboardEmojiPicker.close();
    }
    
  • In app.js init(), after WhiteboardToolbar.init(), call WhiteboardEmojiPicker.init() once.
    Dependencies: Steps 1, 2.

Step 4 — createEmoji + applyTransform case in objects.js

Files: crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js

  • Add createEmoji(x, y, opts): creates a Konva.Group named object emoji with id opts.id || nextTempId(), containing a Konva.Text named emoji-text (font family 'Apple Color Emoji','Segoe UI Emoji','Noto Color Emoji','Segoe UI Symbol',sans-serif, fontSize 48 by default) and an invisible .bg Konva.Rect sized to match the text. If opts.width/height present (reload), scale fontSize accordingly so the text fills the saved box.
  • Stash _emojiChar and _emojiFontSize on the group for serialization.
  • Register in objects map as { group, type: 'emoji', char }. Push history + onCreate sync.
  • In applyTransform, add an else if (type === 'emoji') branch: take s = min(scaleX, scaleY) for uniform resize, set fontSize = max(8, round(_emojiFontSize * s)), resize .bg to match the new text width/height, reset group scales to 1.
  • Export createEmoji.
    Dependencies: none (independent of Steps 1–3).

Step 5 — tools.jsemoji branch + helpers

Files: crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js

  • Add var emojiChar = null; in the module-scope vars at the top.
  • In onMouseDown, add else if (currentTool === 'emoji') { ... }: if emojiChar is null return; else call WhiteboardObjects.createEmoji(pos.x, pos.y, { char: emojiChar }), then setTool('select'), WhiteboardToolbar.setActive('select'), close the picker, reset emojiChar = null.
  • Export setEmojiChar(c) and getEmojiChar().
    Dependencies: Step 4.

Step 6 — shortcuts.js: + Escape

Files: crates/hero_whiteboard_ui/static/web/js/whiteboard/shortcuts.js

  • In the main keydown handler, after the INPUT/TEXTAREA/contentEditable guard, add a case for e.key === ':' that calls WhiteboardEmojiPicker.open() and preventDefault().
  • In case 'escape', at the top, if WhiteboardEmojiPicker.isOpen(), call close() and break (don't fall through to the existing deselect logic).
    Dependencies: Steps 1, 5.

Step 7 — sync.js — serialize + applySyncUpdate

Files: crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.js

  • In serializeForServer, add else if (type === 'emoji') { data.char = node._emojiChar; data.fontSize = node._emojiFontSize; }.
  • In applySyncUpdate, add a branch that re-applies char and fontSize to the Konva Text child and resizes the .bg rect accordingly.
    Dependencies: Step 4.

Step 8 — app.jscreateObjectFromData case

Files: crates/hero_whiteboard_ui/static/web/js/whiteboard/app.js

  • In the type switch, add case 'emoji': WhiteboardObjects.createEmoji(obj.x || 0, obj.y || 0, { id: objId, char: data?.char || '😀', fontSize: data?.fontSize || 48, width: obj.width, height: obj.height }); break;.
    Dependencies: Step 4.

Step 9 — CSS for the picker

Files: crates/hero_whiteboard_ui/static/web/css/whiteboard.css

  • Append styles for .wb-emoji-picker (positioned popover, width 340, max-height 420, padded, bordered, shadowed).
  • .emoji-tabs / .emoji-tab / .emoji-tab.active for the row of 9 category buttons.
  • .emoji-grid (8-col grid, scrollable) + .emoji-cell (transparent button, font-size ~20) + .emoji-cell:hover, .emoji-cell.selected.
  • #emoji-search input styling to match theme vars.
    Dependencies: Step 2.

Acceptance Criteria

  • New toolbar button with bi-emoji-smile icon and tooltip "Emoji (:)" is visible next to the eraser.
  • Clicking the toolbar button opens the picker popover.
  • Popover shows 9 category tabs, a search input, and ~300 emojis across categories.
  • Pressing : outside any text input opens the picker.
  • Typing in the search box filters the grid; clearing it returns to the active category's list.
  • Clicking a grid cell then clicking the canvas drops the emoji at the click position, tool returns to select, picker closes.
  • Dropped emojis can be selected, dragged, uniformly resized, rotated, and deleted with Delete/Backspace.
  • Eraser removes emojis; rubber-band selects them.
  • Ctrl+Z undoes creation; Ctrl+Y / Ctrl+Shift+Z redoes.
  • Multi-user: emoji create/move/resize/delete propagates in real time.
  • After reload, emojis re-appear at saved position, size, rotation with the correct character.
  • Escape closes the picker; typing : inside a sticky/text/document editor does NOT open it.
  • No new JS errors in console under normal use.
  • Zero server-side changes.

Notes

  • Uniform resize is enforced in applyTransform by taking min(scaleX, scaleY) for the new fontSize — keeps the glyph square without per-type transformer config.
  • Font stack Apple Color Emoji / Segoe UI Emoji / Noto Color Emoji / Segoe UI Symbol / sans-serif covers macOS, Windows, and Linux distros that ship a color-emoji font.
  • Search is substring-match against a small hand-written keyword map. Emojis without a keyword entry are still reachable via their category tab.
  • No server-side changes: the existing objects table already stores data as a JSON blob; {char, fontSize} fits natively.
  • clipboard.js, history.js, and the generic transformer/rubber-band/eraser paths already work with any name='object ...' group, so Ctrl+C/V/D, undo/redo, and multi-select all come along for free.
## Implementation Spec for Issue #43 ### Objective Add a Miro-style emoji picker tool that lets the user pick an emoji from a curated 300-emoji / 9-category grid and click-to-place it on the canvas as a first-class `emoji` object (drag, resize, rotate, delete, erase, undo/redo, multi-user sync, persist across reload). ### Requirements - New toolbar button `data-tool="emoji"` (bi-emoji-smile icon) inserted after the eraser divider on the left toolbar. - Clicking the button opens an emoji-picker popover (separate from the normal subtoolbar because the grid needs ~340 px width). - Popover: search input + 9 category tabs + 8-column emoji grid + ~300 emojis. - Clicking a grid cell arms an emoji; clicking the canvas drops the emoji at that point and returns to the select tool (Miro parity). - Emoji renders as a `Konva.Group name="object emoji"` containing a `Konva.Text` at `fontSize: 48` plus an invisible `.bg` hit rect so the generic transformer / rubber-band / eraser / applyTransform paths work automatically. - Normal object semantics: drag, uniform resize (fontSize scales), rotate, Delete key, eraser, undo/redo, multi-user sync, rubber-band select, copy/paste/duplicate, persist across reload. - Keyboard shortcut `:` opens the picker (when no INPUT/TEXTAREA/contentEditable has focus). Escape closes. - No external library; ~300 emojis across 9 categories baked into a new `emoji_picker.js` module. - No server changes — reuse the existing `objects` table with `type='emoji'` and `data={char, fontSize}`. ### Files to modify / create **New** - `crates/hero_whiteboard_ui/static/web/js/whiteboard/emoji_picker.js` **Modify** - `crates/hero_whiteboard_ui/templates/web/board.html` — toolbar button + popover markup + script include. - `crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js` — `'emoji'` branch in `onMouseDown` + `setEmojiChar` / `getEmojiChar` helpers. - `crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js` — `createEmoji()`; `'emoji'` case in `applyTransform`. - `crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.js` — `'emoji'` branches in `serializeForServer` + `applySyncUpdate`. - `crates/hero_whiteboard_ui/static/web/js/whiteboard/app.js` — `'emoji'` case in `createObjectFromData`; call `WhiteboardEmojiPicker.init()` once. - `crates/hero_whiteboard_ui/static/web/js/whiteboard/shortcuts.js` — `':'` shortcut; Escape closes picker. - `crates/hero_whiteboard_ui/static/web/js/whiteboard/toolbar.js` — when the `emoji` tool is clicked, open the picker (and close it when switching to any other tool). - `crates/hero_whiteboard_ui/static/web/css/whiteboard.css` — `.wb-emoji-picker` styles (popover + tabs + grid + search). ### Implementation Plan #### Step 1 — Create `emoji_picker.js` Files: `crates/hero_whiteboard_ui/static/web/js/whiteboard/emoji_picker.js` - New IIFE `WhiteboardEmojiPicker` exposing `{ init, open, close, toggle, isOpen }`. - `CATEGORIES`: ordered array of 9 `{id, label, icon}` — `smileys`, `animals`, `food`, `activities`, `travel`, `objects`, `symbols`, `flags`, `hearts`. Hardcode ~33 emojis per category (~300 total) as Unicode chars. - Small `KEYWORDS` map (`{char: 'comma,separated,keywords'}`) for the most common emojis to make search meaningful. If an emoji has no keyword, it is reachable only via its category tab — acceptable. - `open()`: builds the DOM lazily, positions the popover next to the emoji tool button (reuse `btn.getBoundingClientRect()`), shows it, focuses the search input, sets the toolbar to the `emoji` tool. - `close()`: hides popover, clears `selectedEmoji`, disarms via `WhiteboardTools.setEmojiChar(null)`. - `render()`: rebuilds the tab strip (active class on current tab) and the grid (if `searchQuery` non-empty, match across all categories via the keyword map + direct char match; else show the active category). - Cell click: sets `selectedEmoji`, calls `WhiteboardTools.setEmojiChar(char)`, highlights the cell. Popover stays open so the user can place multiple copies. - Outside-click: a `document.addEventListener('mousedown', ...)` installed while open closes the popover if the target is not inside it and not the toolbar button. Dependencies: none. #### Step 2 — Toolbar button + popover container in `board.html` Files: `crates/hero_whiteboard_ui/templates/web/board.html` - Insert `<button class="tool-btn" data-tool="emoji" title="Emoji (:)"><i class="bi bi-emoji-smile"></i></button>` after the eraser divider. - Before `<!-- Whiteboard Canvas -->`, add the empty popover container `<div class="wb-emoji-picker" id="emoji-picker" style="display:none;"> <input id="emoji-search"> <div id="emoji-tabs"></div> <div id="emoji-grid"></div> </div>`. - Add `<script src="{{ base_path }}/js/whiteboard/emoji_picker.js"></script>` before `sync.js`. Dependencies: Step 1. #### Step 3 — Toolbar integration in `toolbar.js` + `app.js` Files: `toolbar.js`, `app.js` - In `WhiteboardToolbar.init()`'s `data-tool` click handler, after calling `WhiteboardTools.setTool(tool)`: ```js if (tool === 'emoji' && typeof WhiteboardEmojiPicker !== 'undefined') { WhiteboardEmojiPicker.open(); } else if (typeof WhiteboardEmojiPicker !== 'undefined' && WhiteboardEmojiPicker.isOpen()) { WhiteboardEmojiPicker.close(); } ``` - In `app.js` `init()`, after `WhiteboardToolbar.init()`, call `WhiteboardEmojiPicker.init()` once. Dependencies: Steps 1, 2. #### Step 4 — `createEmoji` + `applyTransform` case in `objects.js` Files: `crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js` - Add `createEmoji(x, y, opts)`: creates a `Konva.Group` named `object emoji` with id `opts.id || nextTempId()`, containing a `Konva.Text` named `emoji-text` (font family `'Apple Color Emoji','Segoe UI Emoji','Noto Color Emoji','Segoe UI Symbol',sans-serif`, fontSize 48 by default) and an invisible `.bg` `Konva.Rect` sized to match the text. If `opts.width/height` present (reload), scale fontSize accordingly so the text fills the saved box. - Stash `_emojiChar` and `_emojiFontSize` on the group for serialization. - Register in `objects` map as `{ group, type: 'emoji', char }`. Push history + onCreate sync. - In `applyTransform`, add an `else if (type === 'emoji')` branch: take `s = min(scaleX, scaleY)` for uniform resize, set `fontSize = max(8, round(_emojiFontSize * s))`, resize `.bg` to match the new text width/height, reset group scales to 1. - Export `createEmoji`. Dependencies: none (independent of Steps 1–3). #### Step 5 — `tools.js` — `emoji` branch + helpers Files: `crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.js` - Add `var emojiChar = null;` in the module-scope vars at the top. - In `onMouseDown`, add `else if (currentTool === 'emoji') { ... }`: if `emojiChar` is null return; else call `WhiteboardObjects.createEmoji(pos.x, pos.y, { char: emojiChar })`, then `setTool('select')`, `WhiteboardToolbar.setActive('select')`, close the picker, reset `emojiChar = null`. - Export `setEmojiChar(c)` and `getEmojiChar()`. Dependencies: Step 4. #### Step 6 — `shortcuts.js` — `:` + Escape Files: `crates/hero_whiteboard_ui/static/web/js/whiteboard/shortcuts.js` - In the main keydown handler, after the INPUT/TEXTAREA/contentEditable guard, add a case for `e.key === ':'` that calls `WhiteboardEmojiPicker.open()` and `preventDefault()`. - In `case 'escape'`, at the top, if `WhiteboardEmojiPicker.isOpen()`, call `close()` and `break` (don't fall through to the existing deselect logic). Dependencies: Steps 1, 5. #### Step 7 — `sync.js` — serialize + applySyncUpdate Files: `crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.js` - In `serializeForServer`, add `else if (type === 'emoji') { data.char = node._emojiChar; data.fontSize = node._emojiFontSize; }`. - In `applySyncUpdate`, add a branch that re-applies `char` and `fontSize` to the Konva Text child and resizes the `.bg` rect accordingly. Dependencies: Step 4. #### Step 8 — `app.js` — `createObjectFromData` case Files: `crates/hero_whiteboard_ui/static/web/js/whiteboard/app.js` - In the type switch, add `case 'emoji': WhiteboardObjects.createEmoji(obj.x || 0, obj.y || 0, { id: objId, char: data?.char || '😀', fontSize: data?.fontSize || 48, width: obj.width, height: obj.height }); break;`. Dependencies: Step 4. #### Step 9 — CSS for the picker Files: `crates/hero_whiteboard_ui/static/web/css/whiteboard.css` - Append styles for `.wb-emoji-picker` (positioned popover, width 340, max-height 420, padded, bordered, shadowed). - `.emoji-tabs` / `.emoji-tab` / `.emoji-tab.active` for the row of 9 category buttons. - `.emoji-grid` (8-col grid, scrollable) + `.emoji-cell` (transparent button, font-size ~20) + `.emoji-cell:hover`, `.emoji-cell.selected`. - `#emoji-search` input styling to match theme vars. Dependencies: Step 2. ### Acceptance Criteria - [ ] New toolbar button with bi-emoji-smile icon and tooltip "Emoji (:)" is visible next to the eraser. - [ ] Clicking the toolbar button opens the picker popover. - [ ] Popover shows 9 category tabs, a search input, and ~300 emojis across categories. - [ ] Pressing `:` outside any text input opens the picker. - [ ] Typing in the search box filters the grid; clearing it returns to the active category's list. - [ ] Clicking a grid cell then clicking the canvas drops the emoji at the click position, tool returns to select, picker closes. - [ ] Dropped emojis can be selected, dragged, uniformly resized, rotated, and deleted with Delete/Backspace. - [ ] Eraser removes emojis; rubber-band selects them. - [ ] Ctrl+Z undoes creation; Ctrl+Y / Ctrl+Shift+Z redoes. - [ ] Multi-user: emoji create/move/resize/delete propagates in real time. - [ ] After reload, emojis re-appear at saved position, size, rotation with the correct character. - [ ] Escape closes the picker; typing `:` inside a sticky/text/document editor does NOT open it. - [ ] No new JS errors in console under normal use. - [ ] Zero server-side changes. ### Notes - Uniform resize is enforced in `applyTransform` by taking `min(scaleX, scaleY)` for the new fontSize — keeps the glyph square without per-type transformer config. - Font stack `Apple Color Emoji / Segoe UI Emoji / Noto Color Emoji / Segoe UI Symbol / sans-serif` covers macOS, Windows, and Linux distros that ship a color-emoji font. - Search is substring-match against a small hand-written keyword map. Emojis without a keyword entry are still reachable via their category tab. - No server-side changes: the existing `objects` table already stores `data` as a JSON blob; `{char, fontSize}` fits natively. - `clipboard.js`, `history.js`, and the generic transformer/rubber-band/eraser paths already work with any `name='object ...'` group, so Ctrl+C/V/D, undo/redo, and multi-select all come along for free.
Author
Member

Pull request opened: #44

CI

  • cargo check --workspace: pass
  • cargo clippy --workspace -- -D warnings: pass
  • cargo fmt --check: pass
Pull request opened: https://forge.ourworld.tf/lhumina_code/hero_whiteboard/pulls/44 ### CI - `cargo check --workspace`: pass - `cargo clippy --workspace -- -D warnings`: pass - `cargo fmt --check`: pass
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#43
No description provided.