Emoji support: picker tool to place emojis on the board (Miro-style) #43
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_whiteboard#43
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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
~6-8 cols). Clicking one selects it.object emoji, with a default font size so they read well on the board.objectstable — store as{ type: 'emoji', data: { char: '\ud83d\ude04' }, width, height, ... }. No schema migration needed;datais already a JSON blob.:orctrl/cmd+.like Figma) to open the picker.Non-goals (for this issue)
:shortcode:expansion.Acceptance
:opens the picker when no text input is focused.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
emojiobject (drag, resize, rotate, delete, erase, undo/redo, multi-user sync, persist across reload).Requirements
data-tool="emoji"(bi-emoji-smile icon) inserted after the eraser divider on the left toolbar.Konva.Group name="object emoji"containing aKonva.TextatfontSize: 48plus an invisible.bghit rect so the generic transformer / rubber-band / eraser / applyTransform paths work automatically.:opens the picker (when no INPUT/TEXTAREA/contentEditable has focus). Escape closes.emoji_picker.jsmodule.objectstable withtype='emoji'anddata={char, fontSize}.Files to modify / create
New
crates/hero_whiteboard_ui/static/web/js/whiteboard/emoji_picker.jsModify
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 inonMouseDown+setEmojiChar/getEmojiCharhelpers.crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js—createEmoji();'emoji'case inapplyTransform.crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.js—'emoji'branches inserializeForServer+applySyncUpdate.crates/hero_whiteboard_ui/static/web/js/whiteboard/app.js—'emoji'case increateObjectFromData; callWhiteboardEmojiPicker.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 theemojitool 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-pickerstyles (popover + tabs + grid + search).Implementation Plan
Step 1 — Create
emoji_picker.jsFiles:
crates/hero_whiteboard_ui/static/web/js/whiteboard/emoji_picker.jsWhiteboardEmojiPickerexposing{ 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.KEYWORDSmap ({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 (reusebtn.getBoundingClientRect()), shows it, focuses the search input, sets the toolbar to theemojitool.close(): hides popover, clearsselectedEmoji, disarms viaWhiteboardTools.setEmojiChar(null).render(): rebuilds the tab strip (active class on current tab) and the grid (ifsearchQuerynon-empty, match across all categories via the keyword map + direct char match; else show the active category).selectedEmoji, callsWhiteboardTools.setEmojiChar(char), highlights the cell. Popover stays open so the user can place multiple copies.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.htmlFiles:
crates/hero_whiteboard_ui/templates/web/board.html<button class="tool-btn" data-tool="emoji" title="Emoji (:)"><i class="bi bi-emoji-smile"></i></button>after the eraser divider.<!-- 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>.<script src="{{ base_path }}/js/whiteboard/emoji_picker.js"></script>beforesync.js.Dependencies: Step 1.
Step 3 — Toolbar integration in
toolbar.js+app.jsFiles:
toolbar.js,app.jsWhiteboardToolbar.init()'sdata-toolclick handler, after callingWhiteboardTools.setTool(tool):app.jsinit(), afterWhiteboardToolbar.init(), callWhiteboardEmojiPicker.init()once.Dependencies: Steps 1, 2.
Step 4 —
createEmoji+applyTransformcase inobjects.jsFiles:
crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.jscreateEmoji(x, y, opts): creates aKonva.Groupnamedobject emojiwith idopts.id || nextTempId(), containing aKonva.Textnamedemoji-text(font family'Apple Color Emoji','Segoe UI Emoji','Noto Color Emoji','Segoe UI Symbol',sans-serif, fontSize 48 by default) and an invisible.bgKonva.Rectsized to match the text. Ifopts.width/heightpresent (reload), scale fontSize accordingly so the text fills the saved box._emojiCharand_emojiFontSizeon the group for serialization.objectsmap as{ group, type: 'emoji', char }. Push history + onCreate sync.applyTransform, add anelse if (type === 'emoji')branch: takes = min(scaleX, scaleY)for uniform resize, setfontSize = max(8, round(_emojiFontSize * s)), resize.bgto match the new text width/height, reset group scales to 1.createEmoji.Dependencies: none (independent of Steps 1–3).
Step 5 —
tools.js—emojibranch + helpersFiles:
crates/hero_whiteboard_ui/static/web/js/whiteboard/tools.jsvar emojiChar = null;in the module-scope vars at the top.onMouseDown, addelse if (currentTool === 'emoji') { ... }: ifemojiCharis null return; else callWhiteboardObjects.createEmoji(pos.x, pos.y, { char: emojiChar }), thensetTool('select'),WhiteboardToolbar.setActive('select'), close the picker, resetemojiChar = null.setEmojiChar(c)andgetEmojiChar().Dependencies: Step 4.
Step 6 —
shortcuts.js—:+ EscapeFiles:
crates/hero_whiteboard_ui/static/web/js/whiteboard/shortcuts.jse.key === ':'that callsWhiteboardEmojiPicker.open()andpreventDefault().case 'escape', at the top, ifWhiteboardEmojiPicker.isOpen(), callclose()andbreak(don't fall through to the existing deselect logic).Dependencies: Steps 1, 5.
Step 7 —
sync.js— serialize + applySyncUpdateFiles:
crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.jsserializeForServer, addelse if (type === 'emoji') { data.char = node._emojiChar; data.fontSize = node._emojiFontSize; }.applySyncUpdate, add a branch that re-appliescharandfontSizeto the Konva Text child and resizes the.bgrect accordingly.Dependencies: Step 4.
Step 8 —
app.js—createObjectFromDatacaseFiles:
crates/hero_whiteboard_ui/static/web/js/whiteboard/app.jscase '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.wb-emoji-picker(positioned popover, width 340, max-height 420, padded, bordered, shadowed)..emoji-tabs/.emoji-tab/.emoji-tab.activefor 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-searchinput styling to match theme vars.Dependencies: Step 2.
Acceptance Criteria
:outside any text input opens the picker.:inside a sticky/text/document editor does NOT open it.Notes
applyTransformby takingmin(scaleX, scaleY)for the new fontSize — keeps the glyph square without per-type transformer config.Apple Color Emoji / Segoe UI Emoji / Noto Color Emoji / Segoe UI Symbol / sans-serifcovers macOS, Windows, and Linux distros that ship a color-emoji font.objectstable already storesdataas a JSON blob;{char, fontSize}fits natively.clipboard.js,history.js, and the generic transformer/rubber-band/eraser paths already work with anyname='object ...'group, so Ctrl+C/V/D, undo/redo, and multi-select all come along for free.Pull request opened: #44
CI
cargo check --workspace: passcargo clippy --workspace -- -D warnings: passcargo fmt --check: pass