Paket A: Vollständige Emoji-Auswahl für Notiz-Icons mit Lazy-Loading und Mobile Bottom-Sheet
This commit is contained in:
1 parent
a354f3d008
commit
633bd6f1ef
5 files changed
+282
-132
No files matched your search
Generated
+22
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prismjs": "^1.26.6",
|
"@types/prismjs": "^1.26.6",
|
||||||
|
"emoji-picker-react": "^4.12.0",
|
||||||
"framer-motion": "^11.15.0",
|
"framer-motion": "^11.15.0",
|
||||||
"lucide-react": "^0.468.0",
|
"lucide-react": "^0.468.0",
|
||||||
"prismjs": "^1.30.0",
|
"prismjs": "^1.30.0",
|
||||||
@@ -2101,6 +2102,21 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/emoji-picker-react": {
|
||||||
|
"version": "4.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.12.0.tgz",
|
||||||
|
"integrity": "sha512-q2c8UcZH0eRIMj41bj0k1akTjk69tsu+E7EzkW7giN66iltF6H9LQvQvw6ugscsxdC+1lmt3WZpQkkY65J95tg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"flairup": "1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/entities": {
|
"node_modules/entities": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||||
@@ -2302,6 +2318,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/flairup": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/flairup/-/flairup-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/form-data": {
|
"node_modules/form-data": {
|
||||||
"version": "4.0.6",
|
"version": "4.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prismjs": "^1.26.6",
|
"@types/prismjs": "^1.26.6",
|
||||||
|
"emoji-picker-react": "4.12.0",
|
||||||
"framer-motion": "^11.15.0",
|
"framer-motion": "^11.15.0",
|
||||||
"lucide-react": "^0.468.0",
|
"lucide-react": "^0.468.0",
|
||||||
"prismjs": "^1.30.0",
|
"prismjs": "^1.30.0",
|
||||||
|
|||||||
@@ -44,63 +44,7 @@ export interface DocumentEditorProps {
|
|||||||
onSaveStatusChange?: (status: DocSaveStatus, errorMsg?: string) => void;
|
onSaveStatusChange?: (status: DocSaveStatus, errorMsg?: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EmojiItem {
|
import { EmojiPickerModal } from './EmojiPickerModal';
|
||||||
emoji: string;
|
|
||||||
category: string;
|
|
||||||
keywords: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EMOJI_CATALOG: EmojiItem[] = [
|
|
||||||
// Notizen & Dokumente
|
|
||||||
{ emoji: '📝', category: 'Notizen', keywords: 'notiz schreiben memo text note' },
|
|
||||||
{ emoji: '📄', category: 'Notizen', keywords: 'dokument seite page doc' },
|
|
||||||
{ emoji: '📋', category: 'Notizen', keywords: 'clipboard klemmbrett liste' },
|
|
||||||
{ emoji: '📂', category: 'Notizen', keywords: 'ordner mappe folder datei' },
|
|
||||||
{ emoji: '📁', category: 'Notizen', keywords: 'ordner folder directory' },
|
|
||||||
{ emoji: '📌', category: 'Notizen', keywords: 'pin anheften bookmark wichtig' },
|
|
||||||
{ emoji: '📍', category: 'Notizen', keywords: 'standort ort pin location' },
|
|
||||||
{ emoji: '🏷️', category: 'Notizen', keywords: 'tag etikett label markierung' },
|
|
||||||
{ emoji: '📚', category: 'Notizen', keywords: 'buch bibliothek lesen studium' },
|
|
||||||
{ emoji: '📖', category: 'Notizen', keywords: 'buch open book wissen' },
|
|
||||||
|
|
||||||
// Ideen & Fokus
|
|
||||||
{ emoji: '🧠', category: 'Ideen', keywords: 'gehirn brain denken wissen thought mind' },
|
|
||||||
{ emoji: '💡', category: 'Ideen', keywords: 'idee inspiration lampe glühbirne idea' },
|
|
||||||
{ emoji: '🎯', category: 'Ideen', keywords: 'ziel target fokus mission okr' },
|
|
||||||
{ emoji: '🚀', category: 'Ideen', keywords: 'rakete start launch projekt rocket' },
|
|
||||||
{ emoji: '⚡', category: 'Ideen', keywords: 'blitz energie schnell quick power' },
|
|
||||||
{ emoji: '🔥', category: 'Ideen', keywords: 'feuer flame priorität hot wichtig' },
|
|
||||||
{ emoji: '⭐', category: 'Ideen', keywords: 'stern favoriten star top' },
|
|
||||||
{ emoji: '✨', category: 'Ideen', keywords: 'funkeln magie neu neuigkeit sparkles' },
|
|
||||||
{ emoji: '🧩', category: 'Ideen', keywords: 'puzzle teil lösung problem' },
|
|
||||||
{ emoji: '🔬', category: 'Ideen', keywords: 'mikroskop forschung analyse test' },
|
|
||||||
|
|
||||||
// Tech & Dev
|
|
||||||
{ emoji: '💻', category: 'Tech', keywords: 'laptop computer dev code programming' },
|
|
||||||
{ emoji: '🖥️', category: 'Tech', keywords: 'desktop monitor bildschirm workstation' },
|
|
||||||
{ emoji: '🛠️', category: 'Tech', keywords: 'werkzeug tools optionen settings einstellungen' },
|
|
||||||
{ emoji: '⚙️', category: 'Tech', keywords: 'zahnrad config settings system' },
|
|
||||||
{ emoji: '📊', category: 'Tech', keywords: 'diagramm chart daten statistik dashboard' },
|
|
||||||
{ emoji: '📈', category: 'Tech', keywords: 'wachstum trend kurve business' },
|
|
||||||
{ emoji: '🔒', category: 'Tech', keywords: 'schloss sicherheit geheim privat secure' },
|
|
||||||
{ emoji: '🔑', category: 'Tech', keywords: 'schlüssel key passwort access' },
|
|
||||||
{ emoji: '🌐', category: 'Tech', keywords: 'web internet network welt globus' },
|
|
||||||
{ emoji: '📦', category: 'Tech', keywords: 'paket box release lieferung package' },
|
|
||||||
|
|
||||||
// Alltag & Organisation
|
|
||||||
{ emoji: '💬', category: 'Alltag', keywords: 'chat nachricht kommentar call sprechen' },
|
|
||||||
{ emoji: '✉️', category: 'Alltag', keywords: 'mail email brief post nachricht' },
|
|
||||||
{ emoji: '📅', category: 'Alltag', keywords: 'kalender datum termin plan event' },
|
|
||||||
{ emoji: '⏰', category: 'Alltag', keywords: 'wecker uhr zeit deadline alarm' },
|
|
||||||
{ emoji: '🛒', category: 'Alltag', keywords: 'warenkorb einkauf shopping liste' },
|
|
||||||
{ emoji: '☕', category: 'Alltag', keywords: 'kaffee pause bistro morning' },
|
|
||||||
{ emoji: '🏠', category: 'Alltag', keywords: 'haus heim home wohnung' },
|
|
||||||
{ emoji: '🏢', category: 'Alltag', keywords: 'büro firma arbeit office work' },
|
|
||||||
{ emoji: '✈️', category: 'Alltag', keywords: 'flugzeug reise urlaub trip travel' },
|
|
||||||
{ emoji: '🎨', category: 'Alltag', keywords: 'kunst farbe design creative' },
|
|
||||||
{ emoji: '🏆', category: 'Alltag', keywords: 'pokal erfolg win trophy award' },
|
|
||||||
{ emoji: '✅', category: 'Alltag', keywords: 'haken check done erledigt' },
|
|
||||||
];
|
|
||||||
|
|
||||||
function ensureNonEmptyBlocks(docId: string, blocks?: DocumentBlock[]): DocumentBlock[] {
|
function ensureNonEmptyBlocks(docId: string, blocks?: DocumentBlock[]): DocumentBlock[] {
|
||||||
if (!blocks || blocks.length === 0) {
|
if (!blocks || blocks.length === 0) {
|
||||||
@@ -250,21 +194,12 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
|||||||
const [loading, setLoading] = useState<boolean>(!initialDoc);
|
const [loading, setLoading] = useState<boolean>(!initialDoc);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [showEmojiPicker, setShowEmojiPicker] = useState<boolean>(false);
|
const [showEmojiPicker, setShowEmojiPicker] = useState<boolean>(false);
|
||||||
const [emojiSearch, setEmojiSearch] = useState<string>('');
|
|
||||||
const [zoomedBlockId, setZoomedBlockId] = useState<string | null>(null);
|
const [zoomedBlockId, setZoomedBlockId] = useState<string | null>(null);
|
||||||
|
|
||||||
// Sync engine (operation log): stores every change locally and syncs it with the server
|
// Sync engine (operation log): stores every change locally and syncs it with the server
|
||||||
const engineRef = useRef<DocSyncEngine | null>(null);
|
const engineRef = useRef<DocSyncEngine | null>(null);
|
||||||
const [engineState, setEngineState] = useState<EngineState | null>(null);
|
const [engineState, setEngineState] = useState<EngineState | null>(null);
|
||||||
|
|
||||||
const filteredEmojis = useMemo(() => {
|
|
||||||
const q = emojiSearch.trim().toLowerCase();
|
|
||||||
if (!q) return EMOJI_CATALOG;
|
|
||||||
return EMOJI_CATALOG.filter(
|
|
||||||
(item) => item.keywords.includes(q) || item.emoji.includes(q) || item.category.toLowerCase().includes(q)
|
|
||||||
);
|
|
||||||
}, [emojiSearch]);
|
|
||||||
|
|
||||||
|
|
||||||
const titleInputRef = useRef<HTMLTextAreaElement | HTMLInputElement | null>(null);
|
const titleInputRef = useRef<HTMLTextAreaElement | HTMLInputElement | null>(null);
|
||||||
const outlinerTreeRef = useRef<OutlinerTreeHandle | null>(null);
|
const outlinerTreeRef = useRef<OutlinerTreeHandle | null>(null);
|
||||||
@@ -1044,72 +979,11 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
|
|||||||
{doc?.icon || '📝'}
|
{doc?.icon || '📝'}
|
||||||
</button>
|
</button>
|
||||||
{showEmojiPicker && canEdit && (
|
{showEmojiPicker && canEdit && (
|
||||||
<div
|
<EmojiPickerModal
|
||||||
className="absolute left-0 mt-1 p-2 panel-surface shadow-lg border border-border rounded-card z-30 w-72 space-y-2"
|
currentIcon={doc?.icon}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onSelectEmoji={handleSelectIcon}
|
||||||
>
|
onClose={() => setShowEmojiPicker(false)}
|
||||||
<div className="relative">
|
/>
|
||||||
<Search className="w-3.5 h-3.5 text-faint absolute left-2 top-1/2 -translate-y-1/2" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
autoFocus
|
|
||||||
value={emojiSearch}
|
|
||||||
onChange={(e) => setEmojiSearch(e.target.value)}
|
|
||||||
placeholder="Icon suchen oder Emoji eingeben…"
|
|
||||||
className="input-field w-full pl-7 pr-2 py-1 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{doc?.icon && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
handleSelectIcon('');
|
|
||||||
setShowEmojiPicker(false);
|
|
||||||
setEmojiSearch('');
|
|
||||||
}}
|
|
||||||
className="w-full text-left px-2 py-1 text-status-warn/90 hover:bg-status-warn/10 rounded text-xs font-medium flex items-center gap-1.5 cursor-pointer"
|
|
||||||
>
|
|
||||||
<Trash2 className="w-3 h-3" />
|
|
||||||
<span>Icon entfernen</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{emojiSearch.trim() && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
handleSelectIcon(emojiSearch.trim());
|
|
||||||
setShowEmojiPicker(false);
|
|
||||||
setEmojiSearch('');
|
|
||||||
}}
|
|
||||||
className="w-full text-left px-2 py-1 bg-accent/10 hover:bg-accent/20 text-accent rounded text-xs font-medium flex items-center justify-between cursor-pointer"
|
|
||||||
>
|
|
||||||
<span>Eingabe als Icon nutzen</span>
|
|
||||||
<span className="text-base">{emojiSearch.trim()}</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<div className="max-h-48 overflow-y-auto grid grid-cols-6 gap-1 pr-1">
|
|
||||||
{filteredEmojis.map((item) => (
|
|
||||||
<button
|
|
||||||
key={item.emoji}
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
handleSelectIcon(item.emoji);
|
|
||||||
setShowEmojiPicker(false);
|
|
||||||
setEmojiSearch('');
|
|
||||||
}}
|
|
||||||
className="p-1.5 rounded-btn hover:bg-panel-elevated text-lg flex items-center justify-center cursor-pointer transition-colors"
|
|
||||||
title={item.keywords}
|
|
||||||
>
|
|
||||||
{item.emoji}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
{filteredEmojis.length === 0 && (
|
|
||||||
<div className="col-span-6 text-center text-xs text-faint py-3">
|
|
||||||
Kein passendes Standard-Icon
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
import React, { Suspense, useEffect, useRef, useState } from 'react';
|
||||||
|
import { X, Trash2, Loader2, Clock } from 'lucide-react';
|
||||||
|
import type { EmojiClickData } from 'emoji-picker-react';
|
||||||
|
import { EmojiStyle, Theme, SuggestionMode } from 'emoji-picker-react';
|
||||||
|
|
||||||
|
// Lazy-load emoji-picker-react so the main bundle size stays small
|
||||||
|
const EmojiPicker = React.lazy(() => import('emoji-picker-react'));
|
||||||
|
|
||||||
|
const RECENT_EMOJIS_STORAGE_KEY = 'mindsift_recent_emojis';
|
||||||
|
const MAX_RECENT_EMOJIS = 10;
|
||||||
|
|
||||||
|
function getStoredRecentEmojis(): string[] {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(RECENT_EMOJIS_STORAGE_KEY);
|
||||||
|
if (!raw) return ['📝', '💡', '🚀', '⭐', '🔥', '🎯', '📚', '💻'];
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||||
|
return parsed.slice(0, MAX_RECENT_EMOJIS);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// fallback
|
||||||
|
}
|
||||||
|
return ['📝', '💡', '🚀', '⭐', '🔥', '🎯', '📚', '💻'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveRecentEmoji(emoji: string): void {
|
||||||
|
try {
|
||||||
|
const current = getStoredRecentEmojis();
|
||||||
|
const updated = [emoji, ...current.filter((e) => e !== emoji)].slice(0, MAX_RECENT_EMOJIS);
|
||||||
|
localStorage.setItem(RECENT_EMOJIS_STORAGE_KEY, JSON.stringify(updated));
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EmojiPickerModalProps {
|
||||||
|
currentIcon?: string;
|
||||||
|
onSelectEmoji: (emoji: string) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmojiPickerModal: React.FC<EmojiPickerModalProps> = ({
|
||||||
|
currentIcon,
|
||||||
|
onSelectEmoji,
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
|
const [recentEmojis, setRecentEmojis] = useState<string[]>(getStoredRecentEmojis);
|
||||||
|
const [isDark, setIsDark] = useState<boolean>(() =>
|
||||||
|
typeof document !== 'undefined'
|
||||||
|
? document.documentElement.classList.contains('dark')
|
||||||
|
: true
|
||||||
|
);
|
||||||
|
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Sync theme changes
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
setIsDark(document.documentElement.classList.contains('dark'));
|
||||||
|
});
|
||||||
|
observer.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['class', 'data-theme'],
|
||||||
|
});
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Close on Escape or click outside (desktop popover)
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
if (
|
||||||
|
containerRef.current &&
|
||||||
|
!containerRef.current.contains(e.target as Node)
|
||||||
|
) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
const handlePick = (emoji: string) => {
|
||||||
|
saveRecentEmoji(emoji);
|
||||||
|
setRecentEmojis(getStoredRecentEmojis());
|
||||||
|
onSelectEmoji(emoji);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePickerEmojiClick = (emojiData: EmojiClickData) => {
|
||||||
|
handlePick(emojiData.emoji);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Mobile Backdrop (< 640px) */}
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/60 z-40 sm:hidden backdrop-blur-xs"
|
||||||
|
onClick={onClose}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Main Container: Mobile Bottom-Sheet (<640px) & Desktop Popover (>=640px) */}
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="
|
||||||
|
fixed inset-x-0 bottom-0 z-50 rounded-t-2xl max-h-[85vh] bg-panel border-t border-border shadow-2xl flex flex-col p-3 space-y-3
|
||||||
|
sm:absolute sm:inset-auto sm:left-0 sm:top-full sm:mt-1 sm:z-40 sm:w-[360px] sm:max-h-[520px] sm:rounded-card sm:border sm:border-border sm:shadow-xl
|
||||||
|
"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
// Inject MindSift CSS variables into emoji-picker-react for seamless theming
|
||||||
|
'--epr-bg-color': 'var(--bg-panel)',
|
||||||
|
'--epr-category-label-bg-color': 'var(--bg-panel)',
|
||||||
|
'--epr-picker-border-color': 'var(--border-color)',
|
||||||
|
'--epr-text-color': 'var(--text-main)',
|
||||||
|
'--epr-search-input-bg-color': 'var(--bg-app)',
|
||||||
|
'--epr-search-input-bg-color-active': 'var(--bg-panel-elevated)',
|
||||||
|
'--epr-hover-bg-color': 'var(--bg-panel-elevated)',
|
||||||
|
'--epr-focus-bg-color': 'var(--bg-panel-elevated)',
|
||||||
|
'--epr-highlight-color': 'var(--accent-primary)',
|
||||||
|
'--epr-category-icon-active-color': 'var(--accent-primary)',
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* Mobile handle indicator */}
|
||||||
|
<div className="w-10 h-1 bg-border rounded-full mx-auto sm:hidden shrink-0" />
|
||||||
|
|
||||||
|
{/* Top Header: Title, Actions & Close */}
|
||||||
|
<div className="flex items-center justify-between gap-2 shrink-0 border-b border-border pb-2">
|
||||||
|
<div className="flex items-center gap-1.5 text-xs font-semibold text-main">
|
||||||
|
<span>Icon auswählen</span>
|
||||||
|
{currentIcon && (
|
||||||
|
<span className="text-base leading-none ml-1">{currentIcon}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{currentIcon && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onSelectEmoji('')}
|
||||||
|
className="h-11 sm:h-7 px-3 sm:px-2 rounded text-xs font-medium text-status-err hover:bg-status-err/10 flex items-center gap-1 transition-colors cursor-pointer"
|
||||||
|
title="Icon entfernen"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3.5 h-3.5" />
|
||||||
|
<span className="hidden sm:inline">Entfernen</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="w-11 h-11 sm:w-7 sm:h-7 rounded flex items-center justify-center text-muted hover:text-main hover:bg-panel-elevated transition-colors cursor-pointer"
|
||||||
|
title="Schließen"
|
||||||
|
aria-label="Schließen"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4 sm:w-3.5 sm:h-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Recent Emojis Bar */}
|
||||||
|
{recentEmojis.length > 0 && (
|
||||||
|
<div className="shrink-0 space-y-1.5">
|
||||||
|
<div className="flex items-center gap-1 text-[11px] text-muted">
|
||||||
|
<Clock className="w-3 h-3 text-accent" />
|
||||||
|
<span>Zuletzt verwendet:</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5 overflow-x-auto pb-1 no-scrollbar">
|
||||||
|
{recentEmojis.map((emoji) => (
|
||||||
|
<button
|
||||||
|
key={emoji}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handlePick(emoji)}
|
||||||
|
className="min-w-[44px] min-h-[44px] sm:min-w-[32px] sm:min-h-[32px] rounded-btn bg-panel-elevated hover:bg-canvas border border-border flex items-center justify-center text-xl sm:text-base cursor-pointer transition-transform active:scale-95 shrink-0"
|
||||||
|
>
|
||||||
|
{emoji}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Full Emoji Catalog via emoji-picker-react */}
|
||||||
|
<div className="flex-1 min-h-[300px] overflow-hidden rounded mindsift-emoji-picker">
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div className="h-64 flex flex-col items-center justify-center gap-2 text-xs text-muted">
|
||||||
|
<Loader2 className="w-5 h-5 animate-spin text-accent" />
|
||||||
|
<span>Emoji-Katalog wird geladen…</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<EmojiPicker
|
||||||
|
onEmojiClick={handlePickerEmojiClick}
|
||||||
|
autoFocusSearch={true}
|
||||||
|
theme={isDark ? Theme.DARK : Theme.LIGHT}
|
||||||
|
emojiStyle={EmojiStyle.NATIVE}
|
||||||
|
suggestedEmojisMode={SuggestionMode.RECENT}
|
||||||
|
searchPlaceholder="Emoji suchen…"
|
||||||
|
searchPlaceHolder="Emoji suchen…"
|
||||||
|
width="100%"
|
||||||
|
height="350px"
|
||||||
|
previewConfig={{
|
||||||
|
showPreview: false,
|
||||||
|
}}
|
||||||
|
skinTonesDisabled={false}
|
||||||
|
lazyLoadEmojis={true}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -444,3 +444,30 @@ body {
|
|||||||
color: #f97316;
|
color: #f97316;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
EMOJI PICKER (emoji-picker-react Integration & Mobile Touch Tuning)
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.mindsift-emoji-picker .EmojiPickerReact {
|
||||||
|
border: none !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 639px) {
|
||||||
|
/* Prevent iOS auto-zoom on input focus */
|
||||||
|
.mindsift-emoji-picker .EmojiPickerReact input.epr-search-input {
|
||||||
|
font-size: 1rem !important;
|
||||||
|
min-height: 44px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Min 44px touch targets on mobile */
|
||||||
|
.mindsift-emoji-picker .EmojiPickerReact button.epr-emoji {
|
||||||
|
min-width: 44px !important;
|
||||||
|
min-height: 44px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mindsift-emoji-picker .EmojiPickerReact .epr-category-nav button {
|
||||||
|
min-height: 44px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user