fix(mobile): sheets above the keyboard, emoji picker, cleaner header
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m25s

- FormSheet: create dialogs (task list, shopping list, folder) as bottom
  sheet right above the on-screen keyboard (visualViewport), large fields;
  centred or anchored dialog on desktop
- Emoji picker: category icons keep their sprite size (no doubled icons,
  round highlight), one 'Zuletzt verwendet' (library category, German
  names), sheet and picker height follow the keyboard
- Mobile header no longer repeats the current section next to the logo

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-29 06:43:51 +01:00
1 parent 108d166990
commit eaabe469d5
10 files changed
+249 -217

No files matched your search

@@ -1,37 +1,25 @@
import React, { Suspense, useEffect, useRef, useState } from 'react';
import { X, Trash2, Loader2, Clock } from 'lucide-react';
import { X, Trash2, Loader2 } from 'lucide-react';
import type { EmojiClickData } from 'emoji-picker-react';
import { EmojiStyle, Theme, SuggestionMode } from 'emoji-picker-react';
import { Categories, EmojiStyle, Theme, SuggestionMode } from 'emoji-picker-react';
import { useKeyboardInset } from '../../lib/useKeyboardInset';
// 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
}
}
// The library's own "recently used" category (stored by it) is the only recent list; a second bar
// would cost height on phones. Category names in German.
const CATEGORIES = [
{ category: Categories.SUGGESTED, name: 'Zuletzt verwendet' },
{ category: Categories.SMILEYS_PEOPLE, name: 'Smileys & Personen' },
{ category: Categories.ANIMALS_NATURE, name: 'Tiere & Natur' },
{ category: Categories.FOOD_DRINK, name: 'Essen & Trinken' },
{ category: Categories.TRAVEL_PLACES, name: 'Reisen & Orte' },
{ category: Categories.ACTIVITIES, name: 'Aktivitäten' },
{ category: Categories.OBJECTS, name: 'Objekte' },
{ category: Categories.SYMBOLS, name: 'Symbole' },
{ category: Categories.FLAGS, name: 'Flaggen' },
];
interface EmojiPickerModalProps {
currentIcon?: string;
@@ -44,7 +32,10 @@ export const EmojiPickerModal: React.FC<EmojiPickerModalProps> = ({
onSelectEmoji,
onClose,
}) => {
const [recentEmojis, setRecentEmojis] = useState<string[]>(getStoredRecentEmojis);
const { inset, visibleHeight } = useKeyboardInset();
const isPhone = typeof window !== 'undefined' && window.innerWidth < 640;
// On phones the picker shrinks with the keyboard so search field and results stay visible
const pickerHeight = isPhone ? Math.max(200, Math.min(350, visibleHeight - 110)) : 350;
const [isDark, setIsDark] = useState<boolean>(() =>
typeof document !== 'undefined'
? document.documentElement.classList.contains('dark')
@@ -92,8 +83,6 @@ export const EmojiPickerModal: React.FC<EmojiPickerModalProps> = ({
}, [onClose]);
const handlePick = (emoji: string) => {
saveRecentEmoji(emoji);
setRecentEmojis(getStoredRecentEmojis());
onSelectEmoji(emoji);
};
@@ -114,7 +103,7 @@ export const EmojiPickerModal: React.FC<EmojiPickerModalProps> = ({
<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
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
"
style={
@@ -130,6 +119,8 @@ export const EmojiPickerModal: React.FC<EmojiPickerModalProps> = ({
'--epr-focus-bg-color': 'var(--bg-panel-elevated)',
'--epr-highlight-color': 'var(--accent-primary)',
'--epr-category-icon-active-color': 'var(--accent-primary)',
'--kb-inset': `${inset}px`,
'--vv-max': `${Math.round(visibleHeight * 0.9)}px`,
} as React.CSSProperties
}
>
@@ -170,30 +161,8 @@ export const EmojiPickerModal: React.FC<EmojiPickerModalProps> = ({
</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">
<div className="flex-1 min-h-0 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">
@@ -211,7 +180,8 @@ export const EmojiPickerModal: React.FC<EmojiPickerModalProps> = ({
searchPlaceholder="Emoji suchen…"
searchPlaceHolder="Emoji suchen…"
width="100%"
height="350px"
height={`${pickerHeight}px`}
categories={CATEGORIES as any}
previewConfig={{
showPreview: false,
}}
@@ -49,6 +49,7 @@ import {
RealtimeTransport,
} from '../../lib/api';
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
import { FormSheet } from '../../components/FormSheet';
import { sendDocOps } from '../../lib/docSync/oneoff';
import { useMobileActions } from '../../context/MobileActionContext';
import { DocumentEditor, DocSaveStatus } from './DocumentEditor';
@@ -340,7 +341,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
if (e) {
setNewFolderCoords({
x: Math.min(e.clientX, window.innerWidth - 270),
y: Math.min(e.clientY, window.innerHeight - 130),
y: Math.min(e.clientY, window.innerHeight - 200),
});
} else {
setNewFolderCoords({ x: 100, y: 150 });
@@ -2196,55 +2197,22 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{/* New Folder Popover (small & positioned near click, asks only for name & OK) */}
{showNewFolderModal && (
<div
className="fixed inset-0 z-50 bg-black/20"
onClick={() => setShowNewFolderModal(false)}
<FormSheet
title={newFolderParentPath ? `Unterordner in "${newFolderParentPath}"` : 'Neuer Ordner'}
icon={<FolderPlus className="w-4 h-4 text-accent" />}
onClose={() => setShowNewFolderModal(false)}
onSubmit={handleCreateFolderSubmit}
anchor={newFolderCoords}
>
<form
onSubmit={handleCreateFolderSubmit}
onClick={(e) => e.stopPropagation()}
style={
newFolderCoords
? { top: `${newFolderCoords.y}px`, left: `${newFolderCoords.x}px` }
: undefined
}
className="fixed w-64 bg-panel border border-border rounded-card p-3 shadow-lg space-y-2.5 z-50 text-xs"
>
<div className="font-semibold text-main text-xs flex items-center justify-between">
<span className="truncate">{newFolderParentPath ? `Unterordner in "${newFolderParentPath}"` : 'Neuer Ordner'}</span>
<button
type="button"
onClick={() => setShowNewFolderModal(false)}
className="text-faint hover:text-main cursor-pointer"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
<input
type="text"
autoFocus
value={newFolderName}
onChange={(e) => setNewFolderName(e.target.value)}
placeholder="Ordnername"
className="input-field w-full px-2 py-1 text-xs"
/>
<div className="flex justify-end gap-1.5 pt-1">
<button
type="button"
onClick={() => setShowNewFolderModal(false)}
className="btn-action px-2.5 py-1 text-xs"
>
Abbrechen
</button>
<button
type="submit"
className="btn-primary px-3 py-1 text-xs font-semibold"
>
OK
</button>
</div>
</form>
</div>
<input
type="text"
autoFocus
value={newFolderName}
onChange={(e) => setNewFolderName(e.target.value)}
placeholder="Ordnername"
className="input-field w-full h-11 sm:h-8 px-2.5 text-base sm:text-xs"
/>
</FormSheet>
)}
{/* Unified Sharing Modal (Strictly Read-Only for Docs and Folders) */}