chore: auto-commit (57 archivos)
- frontend/functions/core/format_datetime_short.md - frontend/functions/core/format_datetime_short.test.ts - frontend/functions/core/format_datetime_short.ts - frontend/functions/core/format_duration.md - frontend/functions/core/format_duration.test.ts - frontend/functions/core/format_duration.ts - frontend/functions/core/month_grid.md - frontend/functions/core/month_grid.test.ts - frontend/functions/core/month_grid.ts - frontend/functions/core/string_hash_palette.md - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { type FC, useEffect, useRef } from "react";
|
||||
import { Popover, type PopoverProps } from "@mantine/core";
|
||||
import data from "@emoji-mart/data";
|
||||
import { Picker } from "emoji-mart";
|
||||
|
||||
export interface StickerPickerProps {
|
||||
/** Whether the picker popover is open. */
|
||||
opened: boolean;
|
||||
/** Called when the picker should close (click outside, Escape, or after selection). */
|
||||
onClose: () => void;
|
||||
/** Called with the selected emoji string (native unicode preferred, fallback to shortcode). */
|
||||
onSelect: (emoji: string) => void;
|
||||
/** Anchor element that opens the popover. */
|
||||
target: React.ReactNode;
|
||||
/** Color theme for the emoji-mart Picker. Defaults to "dark". */
|
||||
theme?: "dark" | "light" | "auto";
|
||||
/** Popover placement relative to the anchor. Defaults to "bottom-start". */
|
||||
position?: PopoverProps["position"];
|
||||
}
|
||||
|
||||
/**
|
||||
* StickerPicker — emoji picker wrapped in a Mantine Popover.
|
||||
*
|
||||
* Renders an emoji-mart Picker inside a transparent Popover dropdown.
|
||||
* The Picker instance is created once on mount and cleaned up on unmount
|
||||
* so re-renders caused by parent state changes do not recreate it.
|
||||
* onSelect and theme changes are reflected via refs without remounting.
|
||||
*/
|
||||
export const StickerPicker: FC<StickerPickerProps> = ({
|
||||
opened,
|
||||
onClose,
|
||||
onSelect,
|
||||
target,
|
||||
theme = "dark",
|
||||
position = "bottom-start",
|
||||
}) => {
|
||||
return (
|
||||
<Popover
|
||||
opened={opened}
|
||||
onChange={(o) => {
|
||||
if (!o) onClose();
|
||||
}}
|
||||
onDismiss={onClose}
|
||||
position={position}
|
||||
withArrow
|
||||
shadow="md"
|
||||
withinPortal
|
||||
closeOnClickOutside
|
||||
closeOnEscape
|
||||
trapFocus={false}
|
||||
>
|
||||
<Popover.Target>{target}</Popover.Target>
|
||||
<Popover.Dropdown p={0} style={{ background: "transparent", border: "none" }}>
|
||||
<PickerInner
|
||||
theme={theme}
|
||||
onSelect={(emoji) => {
|
||||
onSelect(emoji);
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal: mounts emoji-mart Picker once, updates callbacks via refs.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface PickerInnerProps {
|
||||
onSelect: (emoji: string) => void;
|
||||
theme: "dark" | "light" | "auto";
|
||||
}
|
||||
|
||||
function PickerInner({ onSelect, theme }: PickerInnerProps) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const instanceRef = useRef<unknown>(null);
|
||||
// Keep latest callback in ref so the Picker closure never goes stale.
|
||||
const onSelectRef = useRef(onSelect);
|
||||
onSelectRef.current = onSelect;
|
||||
const themeRef = useRef(theme);
|
||||
themeRef.current = theme;
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
instanceRef.current = new Picker({
|
||||
data,
|
||||
onEmojiSelect: (e: { native?: string; shortcodes?: string }) => {
|
||||
const cb = onSelectRef.current;
|
||||
if (e.native) cb(e.native);
|
||||
else if (e.shortcodes) cb(e.shortcodes);
|
||||
},
|
||||
theme: themeRef.current,
|
||||
previewPosition: "none",
|
||||
skinTonePosition: "search",
|
||||
autoFocus: true,
|
||||
maxFrequentRows: 2,
|
||||
ref,
|
||||
});
|
||||
return () => {
|
||||
if (ref.current) ref.current.innerHTML = "";
|
||||
instanceRef.current = null;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return <div ref={ref} />;
|
||||
}
|
||||
Reference in New Issue
Block a user