"use client"; /** * Aspect-ratio tab + grouping primitives for video surfaces. * * `RatioTabs` + `RATIO_GRID_CLASS` are ADMIN-ONLY now (VideoBitesEditor, * VideoLibraryGrid) — the public grid was replaced by ``, * which consumes only `detectAspectRatio` + `RATIO_TO_CSS_ASPECT` from here. * * Why these live here in the lib (not the hub): the strip and the hub's * admin editors share the ratio primitives, and the lib is the SSoT. */ import { Tabs, TabsList, TabsTrigger, TabsContent, } from '../ui/tabs'; import type { VideoTeaser } from '../../types/video-processing'; /** * Vizard clip extraction aspect ratios. Mirrors `lib/types/aspect-ratio.ts` * in the hub — narrow on purpose. If the hub's `VizardAspectRatio` adds a * value, mirror it here. */ export type VizardAspectRatio = '9:16' | '16:9' | '1:1'; /** * Extended VideoTeaser with aspect_ratio metadata from Vizard. * The base `VideoTeaser` carries every other persisted bite field * (timing/confidence/viral_reason live there); this adds ONLY the * ratio. Import this when you need the ratio at compile time — * it is the canonical type display/aggregation code extends. */ export interface VideoTeaserWithRatio extends VideoTeaser { aspect_ratio?: VizardAspectRatio; } /** Ratio category used for grid layout and tab grouping. */ export type RatioCategory = 'portrait' | 'square' | 'landscape'; // Shared tab trigger class — matches ``'s pattern. const TAB_TRIGGER_CLASS = 'rounded-none border-b-2 border-transparent data-[state=active]:border-ods-accent data-[state=active]:bg-transparent data-[state=active]:shadow-none px-4 py-2 text-h6 text-ods-text-secondary data-[state=active]:text-ods-text-primary'; /** Grid class for each aspect ratio (admin editors — narrower columns). */ export const RATIO_GRID_CLASS: Record = { portrait: 'grid grid-cols-2 md:grid-cols-3 gap-4', square: 'grid grid-cols-2 md:grid-cols-3 gap-4', landscape: 'grid grid-cols-1 md:grid-cols-2 gap-4', }; /** CSS `aspect-ratio` value per category — sizing for strip cards + placeholders. */ export const RATIO_TO_CSS_ASPECT: Record = { portrait: '9 / 16', square: '1 / 1', landscape: '16 / 9', }; const RATIO_TAB_CONFIG: { key: RatioCategory; label: string }[] = [ { key: 'portrait', label: 'Portrait 9:16' }, { key: 'square', label: 'Square 1:1' }, { key: 'landscape', label: 'Landscape 16:9' }, ]; interface RatioTabsProps { groups: Record React.ReactNode }>; defaultTab?: RatioCategory; className?: string; } /** * RatioTabs — shared aspect-ratio tab wrapper. * * Only renders tabs that have content. `forceMount` + `data-[state=inactive]:hidden` * keeps inactive tabs in the DOM so switching back doesn't scroll-jump. */ export function RatioTabs({ groups, defaultTab, className = '', }: RatioTabsProps) { const activeTabs = RATIO_TAB_CONFIG.filter(t => groups[t.key].count > 0); // If only one tab has content, don't show tabs. if (activeTabs.length <= 1) { const active = activeTabs[0]; return active ? <>{groups[active.key].render()} : null; } const firstTab = defaultTab && groups[defaultTab].count > 0 ? defaultTab : activeTabs[0].key; return ( {activeTabs.map(t => ( {t.label} ({groups[t.key].count}) ))} {activeTabs.map(t => ( {groups[t.key].render()} ))} ); } /** * Detect aspect ratio from a Vizard ratio string, falling back to * inferring from width/height if the string is missing or unknown. * Default: portrait (`'9:16'`). */ export function detectAspectRatio( ratioString?: string, width?: number, height?: number, ): VizardAspectRatio { if (ratioString === '16:9') return '16:9'; if (ratioString === '1:1') return '1:1'; if (ratioString === '9:16') return '9:16'; if (width && height) { if (Math.abs(width - height) < Math.min(width, height) * 0.1) return '1:1'; if (width > height) return '16:9'; } return '9:16'; } /** Map a `VizardAspectRatio` to its `RatioCategory` for grouping. */ export function ratioToCategory(ratio: VizardAspectRatio): RatioCategory { if (ratio === '16:9') return 'landscape'; if (ratio === '1:1') return 'square'; return 'portrait'; } /** * Group items by aspect ratio into portrait / square / landscape buckets. * `hasMultiple` is true when 2+ buckets are non-empty (drives tab vs. flat * grid rendering downstream). */ export function groupByAspectRatio( items: T[], getAspectRatio: (item: T) => VizardAspectRatio, ): { portrait: T[]; square: T[]; landscape: T[]; hasMultiple: boolean; } { const portrait: T[] = []; const square: T[] = []; const landscape: T[] = []; for (const item of items) { const cat = ratioToCategory(getAspectRatio(item)); if (cat === 'landscape') landscape.push(item); else if (cat === 'square') square.push(item); else portrait.push(item); } const filled = [portrait, square, landscape].filter(a => a.length > 0).length; return { portrait, square, landscape, hasMultiple: filled > 1 }; }