Skip to content

feat(composer): ":" emoji suggestions with plain-text items - #917

Draft
pasqualevitiello wants to merge 2 commits into
mainfrom
feat/composer-emoji-suggestions
Draft

pasqualevitiello wants to merge 2 commits into
mainfrom
feat/composer-emoji-suggestions

Conversation

@pasqualevitiello

@pasqualevitiello pasqualevitiello commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

What

Slack-style emoji suggestions for the Composer. Typing : plus at least two letters (:thu) opens a popover of matching emoji. Picking one (arrows + Enter, or click) replaces :thu with the character, 👍. Each row shows the emoji (20px), then :shortcode: in muted text.

Light Dark
Emoji popover, light Emoji popover, dark

Recording of typing Ship it :thu → Enter → 👍 → send: typing-thu.mp4 (raw: download).

Mechanism: B, a small core addition

ComposerItemValue.type gains "text". Selecting such an item replaces the query with the item's text (or label) as a plain text node, with the same trailing-space and caret handling as tokens. It's a few lines in insertItem (lib/composer-document.ts) and a type change. ComposerPart is unchanged, so the draft never contains anything except text for an emoji.

Why not A (an emoji token with text: "👍" drawn by renderToken):

  • The draft would hold { type: "mention", trigger: ":" } parts. Anything that reads mentions (notifications, Cal Video) would have to filter emoji out, which contradicts "plain text, not a chip".
  • Without a renderToken branch, the default ComposerToken draws :thumbsup in primary text. Every consumer would need that branch, the chat demo included.
  • A token is a contenteditable="false" island: word-wise caret movement and selection skip it, IME composition next to it is fragile, and a pasted 👍 (plain text) would behave differently from a picked one.

With B, backspace, copy/paste, undo (one history step back to :thu), caret position and screen readers all treat the emoji like a typed character.

Lighter route for the trigger rules (no core change)

The two-character minimum, ranking (shortcode prefix → keyword prefix → substring) and the 8-result limit live in user land. The demo ranks inside onQueryChange and passes the result with filter: null:

const [emoji, setEmoji] = useState<ComposerItemValue[]>([])
<Composer
  onQueryChange={(query) => setEmoji(query?.character === ":" ? searchEmoji(query.text) : [])}
  triggers={[{ character: ":", items: emoji, filter: null }]}
/>

Trade-off: every consumer writes this state + callback, and the ranked list arrives one commit after the query changes. I measured it in the browser frame by frame while typing and backspacing (:thu → :thumbsu → :thum): the popup stayed open every frame, with no close/reopen or fade. If more consumers need it, a follow-up could let items be a function of the query.

The existing core rules already cover the rest: : opens only at the start or after whitespace (so 10:30 and https:// don't open it), a space right after : closes it, and so does Escape.

Data

No dataset dependency. apps/ui/registry/default/chat-demo/chat-demo-emoji.ts holds 137 curated { emoji, shortcode, keywords } entries (including 👍 ❤️ 😂 😮 👏 🎉) plus searchEmoji. It sits next to the chat demo and is registered as a second file of the chat-demo block and of the new p-composer-12 particle. shadcn rewrites @/registry/default/chat-demo/… to @/components/chat-demo/…, which matches the file's target.

Changes

  • lib/composer-document.ts (registry + synced packages/ui): type: "text" items.
  • p-composer-12 particle + "Emoji" section under "Mentions and actions" in composer.mdx; the ComposerItemValue docs now mention "text".
  • Chat showcase (chat-demo-composer.tsx): : trigger next to @.
  • apps/ui/public/r/*.json regenerated with bun run registry:build. The build also rewrote ~30 unrelated p-*.json files because the registry had drifted from earlier commits. I left those out to keep this PR focused.

Tests

  • apps/ui/tests/chat/chat-demo-emoji.test.ts: two-character minimum, ranking order, 8-result limit, case/space handling, item shape, common reactions present, unique shortcodes.
  • packages/ui/test/components/composer.dom.tsx (happy-dom + real React render): opens after : + 2 characters; stays closed in at 10:30, https://thumbs and : th; Escape closes; ↑/↓ wrap; Enter inserts nice 👍 as a single text part, getComposerText returns it, the caret lands after the space, and no token element exists; undo restores :tada.
  • Adds @happy-dom/global-registrator as a packages/ui dev dependency. The DOM test runs in its own bun test process from the root test script, because button.test.tsx mocks @base-ui/react/use-render and merge-props process-wide, which breaks a real render. It's named .dom.tsx so the main run doesn't pick it up.
  • bun run test, apps/ui typecheck (app + tests) and biome all pass. I also checked in the browser: docs demo (light/dark), chat showcase (Congrats :ta → 🎉, then @ma still suggests Maya).

Follow-ups (out of scope)

  • Typing the closing colon of an exact shortcode (:thumbsup:) to insert it directly. Not tiny: it needs an input-time hook in the core, because rewriting the controlled value would reset undo history and the caret.
  • Cal Video chat (calcom/cal), skin tones, categories, a full grid, recently used.

🤖 Generated with Claude Code


Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.

- Add `type: "text"` to ComposerItemValue: selecting such an item replaces
  the query with the item's `text` (or `label`) as ordinary characters
  instead of inserting a token, so backspace, copy/paste, undo, caret
  placement and screen readers treat it like typed text
- Add the p-composer-12 particle and an "Emoji" docs section: typing
  ":thu" suggests 👍 from a curated list of 137 emoji ranked in
  onQueryChange (shortcode prefix, keyword prefix, substring; at most 8,
  at least 2 characters); each row shows the emoji on a blurred glow
- Add the ":" trigger to the chat demo composer
- Test the ranking, and the query, selection, keyboard and undo
  behavior in a happy-dom test that runs in its own bun process

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
coss-com Ready Ready Preview Nov 4, 2026 12:55pm UTC
coss-com-ui Ready Ready Preview Nov 4, 2026 12:55pm UTC
coss-examples-calcom Ready Ready Preview Nov 4, 2026 12:55pm UTC
1 Skipped Deployment
Project Deployment Actions Updated
coss-com-origin Skipped Skipped Nov 4, 2026 12:55pm UTC

Request Review

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was successfully deployed

3 active and 1 inactive deployments
Preview – coss-com-ui — 6a096cfd Deployed Nov 4, 2026 by vercel[bot]
Preview – coss-examples-calcom — 6a096cfd Deployed Nov 4, 2026 by vercel[bot]
Preview – coss-com — 6a096cfd Deployed Nov 4, 2026 by vercel[bot]
Preview – coss-com-origin — 6a096cfd Deployed Nov 4, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant