Skip to content

Commit 185e15a

Browse files
committed
feat(chat): make emoji picker theme-aware and add border
- Inject stylesheet into picker shadow root in dark mode so background, search bar, category nav, and dividers use design tokens - Pass theme from store (light/dark) to picker; guard injection to avoid duplicate sheets when toggling theme - Add border and shadow to picker host (same as Popover/context menu) - Keep em-emoji host transparent for inline emoji in messages Made-with: Cursor
1 parent da9ac59 commit 185e15a

File tree

3 files changed

+130
-28
lines changed

3 files changed

+130
-28
lines changed
Lines changed: 88 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,103 @@
11
import data from '@emoji-mart/data/sets/14/native.json'
22
import EmojiPicker from '@emoji-mart/react'
3-
import { useChatStore } from '@stores'
3+
import { getEmojiMartTheme, useChatStore, useThemeStore } from '@stores'
4+
import { useEffect, useRef } from 'react'
45

56
import { useEmojiPanelContext } from './context/EmojiPanelContext'
67

8+
const EMOJI_PICKER_DARK_STYLES = `
9+
:host {
10+
background-color: var(--color-base-100) !important;
11+
color: var(--color-base-content) !important;
12+
}
13+
section, #root, [id="root"], .root {
14+
background-color: var(--color-base-100) !important;
15+
color: var(--color-base-content) !important;
16+
}
17+
input, [role="searchbox"], [type="search"], [class*="search"] input {
18+
background-color: var(--color-base-200) !important;
19+
color: var(--color-base-content) !important;
20+
border-color: var(--color-base-300) !important;
21+
}
22+
input::placeholder {
23+
color: var(--color-base-content);
24+
opacity: 0.6;
25+
}
26+
nav, [role="navigation"], [class*="nav"], [class*="category"] {
27+
background-color: var(--color-base-100) !important;
28+
border-color: var(--color-base-300) !important;
29+
color: var(--color-base-content) !important;
30+
}
31+
button, [role="button"], [class*="category"] svg, [class*="tab"] {
32+
color: var(--color-base-content) !important;
33+
}
34+
[class*="active"], [aria-selected="true"] {
35+
color: var(--color-primary) !important;
36+
}
37+
hr, [class*="divider"], [class*="separator"] {
38+
border-color: var(--color-base-300) !important;
39+
}
40+
`
41+
42+
const THEMED_ATTR = 'data-docsplus-themed'
43+
44+
function useEmojiPickerTheme(ref: React.RefObject<HTMLDivElement | null>, isDark: boolean) {
45+
useEffect(() => {
46+
if (!isDark || !ref.current) return
47+
const inject = () => {
48+
const el = ref.current?.querySelector?.('em-emoji-picker') as
49+
| (HTMLElement & { shadowRoot?: ShadowRoot })
50+
| null
51+
if (!el?.shadowRoot || el.hasAttribute(THEMED_ATTR)) return false
52+
try {
53+
const sheet = new CSSStyleSheet()
54+
sheet.replaceSync(EMOJI_PICKER_DARK_STYLES)
55+
el.shadowRoot.adoptedStyleSheets = [...el.shadowRoot.adoptedStyleSheets, sheet]
56+
el.setAttribute(THEMED_ATTR, 'true')
57+
return true
58+
} catch {
59+
return false
60+
}
61+
}
62+
if (inject()) return
63+
const t = setTimeout(() => inject(), 100)
64+
return () => clearTimeout(t)
65+
}, [isDark, ref])
66+
}
67+
768
type Props = {
869
emojiSelectHandler: (emoji: any) => void
970
}
1071
export const Picker = ({ emojiSelectHandler }: Props) => {
1172
const { variant } = useEmojiPanelContext()
1273
const { closeEmojiPicker, emojiPicker } = useChatStore()
74+
const resolvedTheme = useThemeStore((s) => s.resolvedTheme)
75+
const isDark = resolvedTheme !== 'docsplus'
76+
const wrapperRef = useRef<HTMLDivElement>(null)
77+
78+
useEmojiPickerTheme(wrapperRef, isDark)
1379

1480
return (
15-
<EmojiPicker
16-
data={data}
17-
dynamicWidth={variant === 'mobile' ? true : false}
18-
navPosition="bottom"
19-
previewPosition="none"
20-
searchPosition="sticky"
21-
skinTonePosition="search"
22-
{...(variant === 'mobile' && {
23-
emojiSize: 34,
24-
emojiButtonSize: 42
25-
})}
26-
emojiVersion="14"
27-
set="native"
28-
theme="light"
29-
onClickOutside={() => {
30-
if (emojiPicker.isOpen) closeEmojiPicker()
31-
}}
32-
onEmojiSelect={emojiSelectHandler}
33-
/>
81+
<div ref={wrapperRef}>
82+
<EmojiPicker
83+
data={data}
84+
dynamicWidth={variant === 'mobile' ? true : false}
85+
navPosition="bottom"
86+
previewPosition="none"
87+
searchPosition="sticky"
88+
skinTonePosition="search"
89+
{...(variant === 'mobile' && {
90+
emojiSize: 34,
91+
emojiButtonSize: 42
92+
})}
93+
emojiVersion="14"
94+
set="native"
95+
theme={getEmojiMartTheme(resolvedTheme)}
96+
onClickOutside={() => {
97+
if (emojiPicker.isOpen) closeEmojiPicker()
98+
}}
99+
onEmojiSelect={emojiSelectHandler}
100+
/>
101+
</div>
34102
)
35103
}

packages/webapp/src/stores/themeStore.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ export function resolveTheme(preference: ThemePreference): ResolvedTheme {
4141
return 'docsplus'
4242
}
4343

44+
/** Map resolved theme to emoji-mart theme (Picker). Single place for this contract. */
45+
export function getEmojiMartTheme(resolved: ResolvedTheme): 'light' | 'dark' {
46+
return resolved === 'docsplus' ? 'light' : 'dark'
47+
}
48+
4449
/** Apply theme to the DOM (instant swap — see Theme_Light_Dark.md §6.6) */
4550
function applyTheme(theme: ResolvedTheme) {
4651
if (typeof document === 'undefined') return

packages/webapp/src/styles/globals.scss

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -370,10 +370,6 @@ html:not(.m_mobile) {
370370
box-shadow: 0 0px 6px color-mix(in oklch, var(--color-base-content) 20%, transparent);
371371
}
372372
.media-resize-clamp {
373-
// width: 10px;
374-
// height: 10px;
375-
// background-color: #1a73e8;
376-
// border: 1px solid #fff;
377373
position: relative;
378374

379375
&::after {
@@ -595,7 +591,6 @@ html:not(.m_mobile) {
595591
}
596592

597593
&--active {
598-
// border: 1.5px solid #1a73e8;
599594
display: block;
600595
.media-resize-clamp {
601596
display: block;
@@ -1274,9 +1269,6 @@ ol {
12741269
/* Context menu active state for message cards */
12751270
.msg_card.context-menu-active {
12761271
background-color: color-mix(in oklch, var(--color-primary) 8%, transparent) !important;
1277-
// border-left: 3px solid #3b82f6 !important;
1278-
// transform: translateX(2px) !important;
1279-
// transition: all 0.2s ease-in-out !important;
12801272
}
12811273

12821274
.overflow-anchor-auto {
@@ -1427,3 +1419,40 @@ ol {
14271419
.scrollbar-dark::-webkit-scrollbar-thumb:hover {
14281420
background: color-mix(in oklch, var(--color-neutral) 70%, transparent);
14291421
}
1422+
1423+
/* em-emoji-picker (emoji-mart) — border and shadow for separation (same UX as Popover/context menu). */
1424+
/* stylelint-disable-next-line selector-type-no-unknown -- em-emoji-picker is a custom element */
1425+
em-emoji-picker {
1426+
border: 1px solid var(--color-base-300);
1427+
border-radius: var(--radius-field);
1428+
box-shadow: 0 2px 4px color-mix(in oklch, var(--color-base-content) 10%, transparent);
1429+
}
1430+
1431+
/* em-emoji-picker (emoji-mart) — force our theme tokens in dark mode.
1432+
Built-in "dark" theme can render light; set CSS vars on host so shadow DOM inherits. */
1433+
/* stylelint-disable-next-line selector-type-no-unknown -- em-emoji-picker is a custom element */
1434+
[data-theme='docsplus-dark'] em-emoji-picker,
1435+
[data-theme='docsplus-dark-hc'] em-emoji-picker {
1436+
background-color: var(--color-base-100) !important;
1437+
--background: var(--color-base-100);
1438+
--color: var(--color-base-content);
1439+
--border-color: var(--color-base-300);
1440+
--input-background: var(--color-base-200);
1441+
--input-border-color: var(--color-base-300);
1442+
--input-placeholder-color: var(--color-base-content);
1443+
--category-icon-color: var(--color-base-content);
1444+
--category-icon-active-color: var(--color-primary);
1445+
--hover-background: var(--color-base-200);
1446+
--focus-background: var(--color-base-200);
1447+
--epr-bg-color: var(--color-base-100);
1448+
--epr-text-color: var(--color-base-content);
1449+
--epr-picker-border-color: var(--color-base-300);
1450+
--epr-category-icon-active-color: var(--color-primary);
1451+
}
1452+
1453+
/* em-emoji (emoji-mart web component) — theme-aware.
1454+
Host has no opaque background so parent/base surface shows in dark mode. */
1455+
/* stylelint-disable-next-line selector-type-no-unknown -- em-emoji is a custom element */
1456+
em-emoji {
1457+
background: transparent !important;
1458+
}

0 commit comments

Comments
 (0)