Files
MindSift/frontend/src/components/FormSheet.tsx
T
sandrodachandClaude Opus 5.5 c1adb10b5e
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m36s
fix(ui): bottom sheets on phones are square at the bottom like the other sheets
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:58:35 +01:00

96 lines
3.9 KiB
TypeScript

import React, { useEffect } from 'react';
import { X } from 'lucide-react';
import { useKeyboardInset } from '../lib/useKeyboardInset';
interface FormSheetProps {
title: string;
icon?: React.ReactNode;
onClose: () => void;
onSubmit: (e: React.FormEvent) => void;
children: React.ReactNode; // the fields
submitLabel?: string;
danger?: boolean; // red submit button for destructive actions
busy?: boolean;
// Desktop only: open next to the click instead of centred
anchor?: { x: number; y: number } | null;
}
// Small create/rename dialogs. Phone: bottom sheet directly above the keyboard, full width, large
// fields, square at the bottom with only a top border (like the other sheets: it docks to the keyboard
// even with a rounded theme, whose panel-surface would round all corners). Desktop: centred dialog (or next to the click). Escape and the backdrop close it.
export const FormSheet: React.FC<FormSheetProps> = ({
title,
icon,
onClose,
onSubmit,
children,
submitLabel = 'Erstellen',
anchor,
danger = false,
busy = false,
}) => {
const { inset, visibleHeight } = useKeyboardInset();
useEffect(() => {
const onKey = (e: KeyboardEvent) => e.key === 'Escape' && onClose();
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [onClose]);
const desktopAnchor = anchor
? ({
'--sheet-left': `${anchor.x}px`,
'--sheet-top': `${anchor.y}px`,
'--sheet-x': `clamp(12px, calc(${anchor.x}px - 8.25rem), calc(100vw - 16.5rem - 12px))`,
'--sheet-y': `calc(${anchor.y}px + 8px)`,
} as React.CSSProperties)
: undefined;
return (
<div className="fixed inset-0 !m-0 z-50 modal-overlay" onClick={onClose}>
<form
onSubmit={onSubmit}
onClick={(e) => e.stopPropagation()}
style={
{
...desktopAnchor,
'--kb-inset': `${inset}px`,
'--vv-max': visibleHeight ? `${visibleHeight - 16}px` : '85vh',
} as React.CSSProperties
}
className={`fixed inset-x-0 bottom-[var(--kb-inset)] max-h-[var(--vv-max)] panel-surface rounded-t-2xl rounded-b-none border-x-0 border-b-0 border-t border-border shadow-panel p-4 space-y-3 overflow-y-auto
sm:inset-x-auto sm:bottom-auto sm:max-h-[85vh] sm:w-full sm:rounded-card sm:border ${
anchor
? 'sm:max-w-[16.5rem] sm:left-[var(--sheet-x)] sm:top-[var(--sheet-y)]'
: 'sm:max-w-sm sm:left-1/2 sm:top-1/2 sm:-translate-x-1/2 sm:-translate-y-1/2'
}`}
>
<div className="w-10 h-1 bg-border rounded-full mx-auto sm:hidden" aria-hidden="true" />
<div className="flex items-center justify-between gap-2">
<h3 className="text-sm font-semibold text-main flex items-center gap-1.5 min-w-0">
{icon}
<span className="truncate">{title}</span>
</h3>
<button type="button" onClick={onClose} className="w-11 h-11 sm:w-7 sm:h-7 -mr-2 sm:mr-0 flex items-center justify-center text-muted hover:text-main" title="Schließen">
<X className="w-4 h-4" />
</button>
</div>
{children}
<div className="flex gap-2 pt-1 sm:justify-end">
<button type="button" onClick={onClose} className="btn-action flex-1 sm:flex-none h-11 sm:h-8 px-3 text-sm sm:text-xs">
Abbrechen
</button>
<button
type="submit"
disabled={busy}
className={`${danger ? 'btn-danger' : 'btn-primary'} flex-1 sm:flex-none h-11 sm:h-8 px-4 text-sm sm:text-xs font-semibold disabled:opacity-50`}
>
{submitLabel}
</button>
</div>
{/* keeps the buttons clear of the home indicator when no keyboard is open */}
<div className="sm:hidden" style={{ height: inset ? 0 : 'env(safe-area-inset-bottom, 0px)' }} />
</form>
</div>
);
};