# Subtitle Preset Grid Redesign Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Redesign preset preview cards to match uploaded video aspect ratio with modern visual refresh and style characteristics display **Architecture:** Fetch video metadata to calculate aspect ratio, pass to preset cards via props. Update StylePreview for dynamic sizing. Add loading skeleton state. Use Catppuccin Mocha color palette matching the project theme. **Tech Stack:** React, TypeScript, SCSS Modules, TanStack Query, openapi-react-query **Design Spec:** `docs/superpowers/specs/2026-04-06-subtitle-preset-grid-redesign.md` --- ## File Structure | File | Purpose | |------|---------| | `src/features/project/CaptionSettingsStep/useVideoMetadata.ts` | New hook to fetch video metadata and calculate aspect ratio | | `src/features/project/CaptionSettingsStep/PresetGrid.tsx` | Modified - adds aspect ratio fetching, loading state, passes ratio to cards | | `src/features/project/CaptionSettingsStep/PresetGrid.module.scss` | Modified - grid layout, responsive styles | | `src/features/project/CaptionSettingsStep/PresetCard.tsx` | Modified - adds style characteristics display, checkmark indicator, updated styling | | `src/features/project/CaptionSettingsStep/PresetCard.module.scss` | Modified - new card design with Catppuccin Mocha colors | | `src/features/project/CaptionSettingsStep/StylePreview.tsx` | Modified - accepts aspectRatio prop for dynamic sizing | | `src/features/project/CaptionSettingsStep/StylePreview.module.scss` | Modified - dynamic aspect-ratio container | | `src/features/project/CaptionSettingsStep/PresetCardSkeleton.tsx` | New - skeleton loading component for preset cards | | `src/features/project/CaptionSettingsStep/PresetCardSkeleton.module.scss` | New - skeleton styles with shimmer animation | --- ## Task 1: Create useVideoMetadata Hook **Files:** - Create: `src/features/project/CaptionSettingsStep/useVideoMetadata.ts` **Context:** This hook fetches video metadata from the API and calculates the aspect ratio. It uses the existing `api` from `@shared/api` which is openapi-react-query. - [ ] **Step 1: Write the hook implementation** ```typescript import { useMemo } from "react" import api from "@shared/api" interface UseVideoMetadataResult { aspectRatio: number isLoading: boolean isError: boolean } const DEFAULT_ASPECT_RATIO = 16 / 9 export function useVideoMetadata(fileId: string | null): UseVideoMetadataResult { const { data: mediaFile, isLoading, isError } = api.useQuery( "get", "/api/media/mediafiles/{media_file_id}/", { params: { path: { media_file_id: fileId ?? "", }, }, }, { enabled: !!fileId, } ) const aspectRatio = useMemo(() => { if (!mediaFile?.width || !mediaFile?.height) { return DEFAULT_ASPECT_RATIO } return mediaFile.width / mediaFile.height }, [mediaFile]) return { aspectRatio, isLoading, isError, } } ``` - [ ] **Step 2: Commit** ```bash git add src/features/project/CaptionSettingsStep/useVideoMetadata.ts git commit -m "feat: add useVideoMetadata hook for aspect ratio calculation" ``` --- ## Task 2: Create PresetCardSkeleton Component **Files:** - Create: `src/features/project/CaptionSettingsStep/PresetCardSkeleton.tsx` - Create: `src/features/project/CaptionSettingsStep/PresetCardSkeleton.module.scss` - [ ] **Step 1: Write the SCSS module** ```scss // PresetCardSkeleton.module.scss .skeletonCard { border-radius: 12px; overflow: hidden; background: var(--bg-default); border: 1px solid var(--border-subtle); display: flex; flex-direction: column; } .skeletonPreview { aspect-ratio: 16 / 9; background: var(--bg-surface); position: relative; overflow: hidden; &::after { content: ""; position: absolute; inset: 0; background: linear-gradient( 90deg, transparent 0%, rgba(203, 166, 247, 0.08) 50%, transparent 100% ); animation: shimmer 1.5s infinite; } } @keyframes shimmer { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } .skeletonFooter { padding: 14px 16px; background: linear-gradient(to top, var(--bg-surface), var(--bg-default)); border-top: 1px solid var(--border-subtle); display: flex; flex-direction: column; gap: 10px; } .skeletonLine { height: 14px; background: var(--bg-hover); border-radius: 4px; width: 60%; position: relative; overflow: hidden; &::after { content: ""; position: absolute; inset: 0; background: linear-gradient( 90deg, transparent 0%, rgba(203, 166, 247, 0.06) 50%, transparent 100% ); animation: shimmer 1.5s infinite; } } .skeletonLineShort { composes: skeletonLine; width: 40%; height: 10px; } ``` - [ ] **Step 2: Write the component** ```typescript // PresetCardSkeleton.tsx import type { FunctionComponent } from "react" import type { JSX } from "react" import styles from "./PresetCardSkeleton.module.scss" interface IPresetCardSkeletonProps { aspectRatio?: number } export const PresetCardSkeleton: FunctionComponent = ({ aspectRatio = 16 / 9, }): JSX.Element => { return (
) } ``` - [ ] **Step 3: Add barrel export** Add to `src/features/project/CaptionSettingsStep/index.ts`: ```typescript export { PresetCardSkeleton } from "./PresetCardSkeleton" ``` - [ ] **Step 4: Commit** ```bash git add src/features/project/CaptionSettingsStep/PresetCardSkeleton.tsx git add src/features/project/CaptionSettingsStep/PresetCardSkeleton.module.scss git add src/features/project/CaptionSettingsStep/index.ts git commit -m "feat: add PresetCardSkeleton component with shimmer animation" ``` --- ## Task 3: Update StylePreview for Dynamic Aspect Ratio **Files:** - Modify: `src/features/project/CaptionSettingsStep/StylePreview.tsx` - Modify: `src/features/project/CaptionSettingsStep/StylePreview.module.scss` - [ ] **Step 1: Read existing StylePreview files** ```bash cat src/features/project/CaptionSettingsStep/StylePreview.tsx cat src/features/project/CaptionSettingsStep/StylePreview.module.scss ``` - [ ] **Step 2: Update StylePreview.module.scss** Add or modify the preview container to accept dynamic aspect-ratio: ```scss // Add to existing StylePreview.module.scss .previewContainer { position: relative; width: 100%; overflow: hidden; background: #0c0a1a; display: flex; align-items: center; justify-content: center; } ``` - [ ] **Step 3: Update StylePreview.tsx** Add `aspectRatio` prop and apply it to the container: ```typescript // Add to existing imports import type { CSSProperties } from "react" // Update interface to include aspectRatio interface IStylePreviewProps { preset: CaptionPresetRead aspectRatio?: number className?: string } // In component, apply aspect ratio export const StylePreview: FunctionComponent = ({ preset, aspectRatio = 9 / 16, // Default to vertical (original behavior) className, }): JSX.Element => { // ... existing logic ... const containerStyle: CSSProperties = { aspectRatio: String(aspectRatio), } return (
{/* ... existing preview content ... */}
) } ``` - [ ] **Step 4: Commit** ```bash git add src/features/project/CaptionSettingsStep/StylePreview.tsx git add src/features/project/CaptionSettingsStep/StylePreview.module.scss git commit -m "feat: add aspectRatio prop to StylePreview for dynamic sizing" ``` --- ## Task 4: Update PresetCard with New Design **Files:** - Modify: `src/features/project/CaptionSettingsStep/PresetCard.tsx` - Modify: `src/features/project/CaptionSettingsStep/PresetCard.module.scss` - [ ] **Step 1: Read existing PresetCard files** ```bash cat src/features/project/CaptionSettingsStep/PresetCard.tsx cat src/features/project/CaptionSettingsStep/PresetCard.module.scss ``` - [ ] **Step 2: Rewrite PresetCard.module.scss with new design** ```scss // PresetCard.module.scss .presetCard { position: relative; border-radius: 12px; overflow: hidden; background: var(--bg-default); border: 1px solid var(--border-subtle); cursor: pointer; transition: all 0.2s cubic-bezier(0.2, 0.8, 0.2, 1); display: flex; flex-direction: column; &:hover { border-color: var(--purple-400); transform: translateY(-2px); box-shadow: var(--shadow-md); } } .selected { border-color: var(--purple-400); box-shadow: 0 0 0 1px var(--purple-400), 0 0 20px rgba(203, 166, 247, 0.25), var(--shadow-lg); &::before { content: ""; position: absolute; inset: -1px; border-radius: 12px; padding: 1px; background: linear-gradient(135deg, var(--purple-400), var(--purple-600)); -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); -webkit-mask-composite: xor; mask-composite: exclude; pointer-events: none; } } .previewArea { position: relative; overflow: hidden; } .selectedIndicator { position: absolute; top: 8px; right: 8px; width: 20px; height: 20px; background: var(--purple-400); border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 8px rgba(203, 166, 247, 0.4); svg { width: 12px; height: 12px; color: var(--bg-canvas); } } .cardFooter { padding: 14px 16px; background: linear-gradient(to top, var(--bg-surface), var(--bg-default)); border-top: 1px solid var(--border-subtle); } .presetName { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 6px; display: flex; align-items: center; gap: 8px; } .systemBadge { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; padding: 2px 8px; background: var(--purple-100); color: var(--purple-400); border-radius: 4px; } .styleChars { font-size: 12px; color: var(--text-tertiary); display: flex; align-items: center; gap: 8px; } .colorDot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1); } .divider { color: var(--border-default); } ``` - [ ] **Step 3: Update PresetCard.tsx with style characteristics** ```typescript // Add helper to extract style characteristics function getStyleCharacteristics(preset: CaptionPresetRead): { fontFamily: string accentColor: string | null accentName: string | null } { const style = preset.style_config const fontFamily = style?.text?.font_family ?? "Inter" // Extract accent color from highlight or text color const highlightColor = style?.highlight?.color const textColor = style?.text?.color // Simple color name mapping (expand as needed) const colorMap: Record = { "#FFD700": "Золотой", "#00ffff": "Неоновый", "#ffffff": "Белый", "#ff006e": "Розовый", "#cba6f7": "Пурпурный", "#f38ba8": "Розовый", "#a6e3a1": "Зеленый", "#f9e2af": "Желтый", "#89dceb": "Голубой", } const accentColor = highlightColor || textColor const accentName = accentColor ? (colorMap[accentColor] ?? null) : null return { fontFamily, accentColor, accentName, } } // Update component to render characteristics export const PresetCard: FunctionComponent = ({ preset, isSelected, aspectRatio, onSelect, onEdit, onDelete, }): JSX.Element => { const { fontFamily, accentColor, accentName } = getStyleCharacteristics(preset) return (
{isSelected && (
)}
{preset.name} {preset.is_system && Системный}
{fontFamily} {accentColor && accentName && ( <> · {accentName} )}
{/* Context menu for edit/delete - preserve existing */}
) } ``` - [ ] **Step 4: Commit** ```bash git add src/features/project/CaptionSettingsStep/PresetCard.tsx git add src/features/project/CaptionSettingsStep/PresetCard.module.scss git commit -m "feat: redesign PresetCard with style characteristics and checkmark indicator" ``` --- ## Task 5: Update PresetGrid with Aspect Ratio and Loading State **Files:** - Modify: `src/features/project/CaptionSettingsStep/PresetGrid.tsx` - Modify: `src/features/project/CaptionSettingsStep/PresetGrid.module.scss` - [ ] **Step 1: Read existing PresetGrid files** ```bash cat src/features/project/CaptionSettingsStep/PresetGrid.tsx cat src/features/project/CaptionSettingsStep/PresetGrid.module.scss ``` - [ ] **Step 2: Update PresetGrid.module.scss** ```scss // PresetGrid.module.scss .presetGrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; @media (max-width: 768px) { grid-template-columns: repeat(2, 1fr); gap: 12px; } } // Optional: Add fade-in animation for cards @keyframes fadeInUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .cardWrapper { animation: fadeInUp 0.3s ease forwards; // Staggered animation delay @for $i from 1 through 10 { &:nth-child(#{$i}) { animation-delay: #{$i * 50}ms; } } } ``` - [ ] **Step 3: Update PresetGrid.tsx** ```typescript // Add imports import { useVideoMetadata } from "./useVideoMetadata" import { PresetCardSkeleton } from "./PresetCardSkeleton" import { useWizard } from "../WizardContext" // In component export const PresetGrid: FunctionComponent = ({ presets, selectedPresetId, onSelect, onEdit, onDelete, onCreate, }): JSX.Element => { const { primaryFileId } = useWizard() const { aspectRatio, isLoading: isLoadingMetadata } = useVideoMetadata(primaryFileId) if (isLoadingMetadata) { return (
{Array.from({ length: 4 }).map((_, i) => ( ))}
) } return (
{presets.map((preset, index) => (
onSelect(preset.id)} onEdit={() => onEdit(preset.id)} onDelete={() => onDelete(preset.id)} />
))} {/* Create new card - preserve existing */}
) } ``` - [ ] **Step 4: Commit** ```bash git add src/features/project/CaptionSettingsStep/PresetGrid.tsx git add src/features/project/CaptionSettingsStep/PresetGrid.module.scss git commit -m "feat: add aspect ratio and loading state to PresetGrid" ``` --- ## Task 6: Type Check and Verify - [ ] **Step 1: Run type check** ```bash cd cofee_frontend && bunx tsc --noEmit ``` Expected: No TypeScript errors - [ ] **Step 2: Run lint check** ```bash cd cofee_frontend && bun run lint 2>/dev/null || echo "Lint not configured, using type check only" ``` - [ ] **Step 3: Final commit** ```bash git add . git commit -m "feat: complete subtitle preset grid redesign with dynamic aspect ratio" ``` --- ## Verification Checklist - [ ] Preset cards display with correct aspect ratio based on uploaded video - [ ] Loading state shows skeleton cards with shimmer animation - [ ] Style characteristics (font, color) visible on card footers - [ ] Selected card shows checkmark indicator and purple glow border - [ ] Grid is responsive (2 columns on mobile, more on desktop) - [ ] Hover effects work smoothly - [ ] Falls back to 16:9 when no video is available - [ ] All existing functionality preserved (select, edit, delete, create)