Files
wehub-resource-sync c48612c494
CI / E2E Tests (push) Has been cancelled
CI / Lint, Typecheck & Unit Tests (push) Has been cancelled
Docs Build / Build docs site (push) Has been cancelled
Publish @openmaic packages / Build, validate & publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:23 +08:00

97 lines
3.8 KiB
TypeScript

'use client';
import { AnimatePresence, motion } from 'motion/react';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { useI18n } from '@/lib/hooks/use-i18n';
import { useSettingsStore } from '@/lib/store/settings';
import { CHROME_EASE } from '@/lib/edit/transitions';
import type { InsertPaletteItem } from '@/lib/edit/scene-editor-surface';
import { cn } from '@/lib/utils';
import { InsertButton } from './InsertButton';
interface Props {
readonly items: readonly InsertPaletteItem[];
}
/**
* Persistent floating insert toolbar — sits centered above the slide
* canvas, ~12px from the top of the studio frame. Replaces the inline
* insert slot in CommandBar so the global stage controls (back, undo
* /redo, title, settings, Pro, Download) aren't visually mixed with
* content-insertion affordances ("text box / image / shape ..." live
* with the content, not with stage controls).
*
* Collapses to a small chevron handle at the same anchor position;
* the collapsed flag persists in `settings.editInsertToolbarCollapsed`.
*/
export function FloatingInsertToolbar({ items }: Props) {
const { t } = useI18n();
const collapsed = useSettingsStore((s) => s.editInsertToolbarCollapsed);
const setCollapsed = useSettingsStore((s) => s.setEditInsertToolbarCollapsed);
if (items.length === 0) return null;
return (
<div className="pointer-events-none absolute top-3 left-1/2 z-30 -translate-x-1/2">
<AnimatePresence initial={false} mode="wait">
{collapsed ? (
<motion.button
key="collapsed"
type="button"
onClick={() => setCollapsed(false)}
aria-label={t('edit.insert.expandToolbar')}
title={t('edit.insert.expandToolbar')}
initial={{ opacity: 0, y: -6 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -6 }}
transition={{ duration: 0.18, ease: CHROME_EASE }}
className={cn(
'pointer-events-auto inline-flex h-7 w-9 items-center justify-center rounded-b-lg rounded-t-none',
'bg-white/90 dark:bg-zinc-900/90 backdrop-blur-md',
'ring-1 ring-zinc-200/80 dark:ring-zinc-700/80 border-t-0',
'shadow-sm text-zinc-500 dark:text-zinc-400',
'hover:text-violet-600 dark:hover:text-violet-300',
'transition-colors',
)}
>
<ChevronDown className="h-3.5 w-3.5" />
</motion.button>
) : (
<motion.div
key="expanded"
initial={{ opacity: 0, y: -6 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -6 }}
transition={{ duration: 0.2, ease: CHROME_EASE }}
className={cn(
'pointer-events-auto flex items-center gap-1 px-1.5 py-1',
'bg-white/90 dark:bg-zinc-900/90 backdrop-blur-md',
'ring-1 ring-zinc-200/80 dark:ring-zinc-700/80',
'rounded-2xl shadow-md',
)}
>
{items.map((item) => (
<InsertButton key={item.id} item={item} />
))}
<span className="mx-1 h-5 w-px bg-zinc-200 dark:bg-zinc-700" />
<button
type="button"
onClick={() => setCollapsed(true)}
aria-label={t('edit.insert.collapseToolbar')}
title={t('edit.insert.collapseToolbar')}
className={cn(
'inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-lg',
'text-zinc-400 hover:text-zinc-700 dark:text-zinc-500 dark:hover:text-zinc-200',
'hover:bg-zinc-100 dark:hover:bg-zinc-800',
'transition-colors',
)}
>
<ChevronUp className="h-3.5 w-3.5" />
</button>
</motion.div>
)}
</AnimatePresence>
</div>
);
}