- RegisterPage: scroll envelope, iOS-zoom-safe inputs, ≥44px tap targets, mobile-friendly invite chip + swatch row - TransferIndicator: mounted via MobileScreenHeader.rightActions across every settings/instance screen and inline in MobileChatScreen; touch-close listener; viewport-safe panel width - MessageInput popovers (emoji/GIF/mention) now route through new InputPopover wrapper — desktop popover, mobile bottom-sheet - EmojiPicker mobile branch: dynamicWidth + scoped CSS (.emoji-picker-wrapper--mobile) so the <em-emoji-picker> custom element fills the sheet; bigger touch targets (perLine 8, button 40, emoji 28); desktop unchanged - MessageInput trigger row: mobile-first sizing with md: desktop overrides (40×40 buttons + gap on mobile, 34×34 unchanged on desktop) - Spec updates: docs/systems/auth.md (RegisterPage responsive contract), docs/systems/uploads.md (TransferIndicator mobile surfaces)
64 lines
2.5 KiB
TypeScript
64 lines
2.5 KiB
TypeScript
import React, { useRef, useEffect } from 'react';
|
|
import Picker from '@emoji-mart/react';
|
|
import data from '@emoji-mart/data';
|
|
|
|
interface EmojiPickerProps {
|
|
onEmojiSelect: (emoji: { native: string }) => void;
|
|
/**
|
|
* Mobile rendering: stretch the picker to fill its container width
|
|
* (the parent bottom-sheet provides the viewport-wide bounds), use
|
|
* larger touch targets, and let the picker's own scroll area expand
|
|
* to consume the available height of the sheet.
|
|
*/
|
|
mobile?: boolean;
|
|
}
|
|
|
|
export function EmojiPicker({ onEmojiSelect, mobile = false }: EmojiPickerProps) {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Prevent keyboard events from bubbling out (e.g. Enter submitting the chat input)
|
|
useEffect(() => {
|
|
const el = containerRef.current;
|
|
if (!el) return;
|
|
const stop = (e: KeyboardEvent) => e.stopPropagation();
|
|
el.addEventListener('keydown', stop);
|
|
return () => el.removeEventListener('keydown', stop);
|
|
}, []);
|
|
|
|
// emoji-mart's Picker computes its own internal width from
|
|
// `perLine * emojiButtonSize` UNLESS `dynamicWidth` is set, in which case
|
|
// it stretches to its parent's width. On mobile we want full-viewport.
|
|
// The mobile sheet wraps this picker in `flex-1 min-h-0 flex flex-col`,
|
|
// so we make the wrapper fill that space and tell the picker to expand.
|
|
//
|
|
// The `<em-emoji-picker>` custom element has no intrinsic stretch behavior:
|
|
// even with `dynamicWidth: true` it ships with `display: flex` but no
|
|
// `width: 100%`, so it shrinks to its perLine*emojiButtonSize content width
|
|
// unless we force it to fill. The `emoji-picker-wrapper--mobile` modifier
|
|
// applies that override (see globals.css) — desktop keeps the legacy
|
|
// intrinsic-width sizing.
|
|
const wrapperClass = mobile
|
|
? 'emoji-picker-wrapper emoji-picker-wrapper--mobile flex-1 min-h-0 w-full overflow-hidden'
|
|
: 'emoji-picker-wrapper';
|
|
|
|
return (
|
|
<div ref={containerRef} className={wrapperClass}>
|
|
<Picker
|
|
data={data}
|
|
onEmojiSelect={onEmojiSelect}
|
|
theme="dark"
|
|
set="native"
|
|
skinTonePosition="search"
|
|
previewPosition="none"
|
|
navPosition="bottom"
|
|
perLine={mobile ? 8 : 10}
|
|
maxFrequentRows={2}
|
|
emojiSize={mobile ? 28 : 24}
|
|
emojiButtonSize={mobile ? 40 : 32}
|
|
dynamicWidth={mobile ? true : false}
|
|
categories={['frequent', 'people', 'nature', 'foods', 'activity', 'places', 'objects', 'symbols', 'flags']}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|