feat: emoji picker in create dialogs; due date and priority right under a deepened task; no flicker on kanban drop
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m29s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-11-01 14:20:34 +01:00
1 parent fa02e8dee6
commit 7860609201
13 files changed
+143 -74

No files matched your search

@@ -1,4 +1,5 @@
import React, { Suspense, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { X, Trash2, Loader2 } from 'lucide-react';
import type { EmojiClickData } from 'emoji-picker-react';
import { Categories, EmojiStyle, Theme, SuggestionMode } from 'emoji-picker-react';
@@ -25,12 +26,16 @@ interface EmojiPickerModalProps {
currentIcon?: string;
onSelectEmoji: (emoji: string) => void;
onClose: () => void;
// Inside a dialog (which clips and transforms its content): the picker is put on the page instead and
// opens below this button on desktop
anchor?: DOMRect | null;
}
export const EmojiPickerModal: React.FC<EmojiPickerModalProps> = ({
currentIcon,
onSelectEmoji,
onClose,
anchor,
}) => {
const { inset, visibleHeight } = useKeyboardInset();
const isPhone = typeof window !== 'undefined' && window.innerWidth < 640;
@@ -61,6 +66,7 @@ export const EmojiPickerModal: React.FC<EmojiPickerModalProps> = ({
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation(); // only the picker closes, not the dialog it was opened from
onClose();
}
};
@@ -74,10 +80,10 @@ export const EmojiPickerModal: React.FC<EmojiPickerModalProps> = ({
}
};
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keydown', handleKeyDown, true);
document.addEventListener('mousedown', handleClickOutside);
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keydown', handleKeyDown, true);
document.removeEventListener('mousedown', handleClickOutside);
};
}, [onClose]);
@@ -90,11 +96,18 @@ export const EmojiPickerModal: React.FC<EmojiPickerModalProps> = ({
handlePick(emojiData.emoji);
};
return (
const anchorStyle = anchor
? ({
'--pick-left': `${Math.max(12, Math.min(anchor.left, window.innerWidth - 372))}px`,
'--pick-top': `${Math.min(anchor.bottom + 4, window.innerHeight - 532)}px`,
} as React.CSSProperties)
: undefined;
const picker = (
<>
{/* Mobile Backdrop (< 640px) */}
<div
className="fixed inset-0 bg-black/60 z-40 sm:hidden backdrop-blur-xs"
className={`fixed inset-0 bg-black/60 sm:hidden backdrop-blur-xs ${anchor ? 'z-[60]' : 'z-40'}`}
onClick={onClose}
aria-hidden="true"
/>
@@ -102,12 +115,17 @@ export const EmojiPickerModal: React.FC<EmojiPickerModalProps> = ({
{/* Main Container: Mobile Bottom-Sheet (<640px) & Desktop Popover (>=640px) */}
<div
ref={containerRef}
className="
fixed inset-x-0 bottom-[var(--kb-inset)] z-50 rounded-t-2xl max-h-[var(--vv-max)] 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
"
className={`
fixed inset-x-0 bottom-[var(--kb-inset)] rounded-t-2xl max-h-[var(--vv-max)] bg-panel border-t border-border shadow-2xl flex flex-col p-3 space-y-3
sm:inset-auto sm:w-[360px] sm:max-h-[520px] sm:rounded-card sm:border sm:border-border sm:shadow-xl ${
anchor
? 'z-[61] sm:fixed sm:left-[var(--pick-left)] sm:top-[max(12px,var(--pick-top))]'
: 'z-50 sm:absolute sm:left-0 sm:top-full sm:mt-1 sm:z-40'
}
`}
style={
{
...anchorStyle,
// 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)',
@@ -193,4 +211,5 @@ export const EmojiPickerModal: React.FC<EmojiPickerModalProps> = ({
</div>
</>
);
return anchor ? createPortal(picker, document.body) : picker;
};
@@ -57,6 +57,7 @@ import { JournalCalendar } from './JournalCalendar';
import { isJournalEnabled, readWorkspaceSession, todayISO, writeWorkspaceSession } from '../../lib/journal';
import { MergeDialog } from './MergeDialog';
import { CountBadge } from '../../components/CountBadge';
import { EmojiIconButton } from '../../components/EmojiIconButton';
import { extractHashtags } from '../../lib/hashtags';
interface WorkspaceViewProps {
@@ -2968,14 +2969,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
}}
>
<div className="flex items-center gap-1.5">
<input
type="text"
value={newNoteIcon}
onChange={(e) => setNewNoteIcon(e.target.value)}
aria-label="Emoji"
title="Icon (Emoji)"
className="w-12 sm:w-10 h-11 sm:h-8 text-center input-field text-base sm:text-sm"
/>
<EmojiIconButton value={newNoteIcon} onChange={setNewNoteIcon} fallback="📝" />
<input
type="text"
autoFocus