ui-nyla/src/components/FolderTree/actions/FileActionBottomSheet.tsx
2026-04-21 23:49:50 +02:00

83 lines
2.2 KiB
TypeScript

/**
* FileActionBottomSheet — Long-Press Action-Sheet für Mobile.
*
* Slide-Up von unten, 48 px Touch-Targets, ESC + Backdrop schließen.
*/
import React, { useEffect } from 'react';
import {
type FileAction,
type FileActionContext,
type FileActionTarget,
resolveActionLabel,
} from './types';
import { runAction } from './registry';
import styles from './FileActionBottomSheet.module.css';
interface Props {
open: boolean;
actions: FileAction[];
target: FileActionTarget;
ctx: FileActionContext;
onClose: () => void;
title?: string;
confirm?: (title: string, body: string) => boolean | Promise<boolean>;
}
export const FileActionBottomSheet: React.FC<Props> = ({
open,
actions,
target,
ctx,
onClose,
title,
confirm,
}) => {
useEffect(() => {
if (!open) return;
const _onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', _onKey);
return () => window.removeEventListener('keydown', _onKey);
}, [open, onClose]);
if (!open) return null;
const _handleClick = async (action: FileAction) => {
onClose();
await runAction(action, target, ctx, confirm);
};
return (
<>
<div className={styles.backdrop} onClick={onClose} />
<div className={styles.sheet} role="dialog" aria-modal="true" aria-label={title}>
<div className={styles.handle} aria-hidden="true" />
{title && <div className={styles.title}>{title}</div>}
{actions.length === 0 ? (
<div className={styles.empty}></div>
) : (
actions.map((a) => {
const Icon = a.icon;
const cls = a.danger ? `${styles.item} ${styles.danger}` : styles.item;
return (
<button
key={a.id}
type="button"
className={cls}
onClick={() => _handleClick(a)}
style={a.iconColor ? { color: a.iconColor } : undefined}
>
<span className={styles.icon}>
<Icon size={17} />
</span>
<span className={styles.label}>{resolveActionLabel(a, target)}</span>
</button>
);
})
)}
</div>
</>
);
};