# 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<IPresetCardSkeletonProps> = ({
  aspectRatio = 16 / 9,
}): JSX.Element => {
  return (
    <div className={styles.skeletonCard}>
      <div
        className={styles.skeletonPreview}
        style={{ aspectRatio }}
      />
      <div className={styles.skeletonFooter}>
        <div className={styles.skeletonLine} />
        <div className={styles.skeletonLineShort} />
      </div>
    </div>
  )
}
```

- [ ] **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<IStylePreviewProps> = ({
  preset,
  aspectRatio = 9 / 16, // Default to vertical (original behavior)
  className,
}): JSX.Element => {
  // ... existing logic ...

  const containerStyle: CSSProperties = {
    aspectRatio: String(aspectRatio),
  }

  return (
    <div
      className={cs(styles.previewContainer, className)}
      style={containerStyle}
    >
      {/* ... existing preview content ... */}
    </div>
  )
}
```

- [ ] **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<string, string> = {
    "#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<IPresetCardProps> = ({
  preset,
  isSelected,
  aspectRatio,
  onSelect,
  onEdit,
  onDelete,
}): JSX.Element => {
  const { fontFamily, accentColor, accentName } = getStyleCharacteristics(preset)

  return (
    <div
      className={cs(styles.presetCard, { [styles.selected]: isSelected })}
      onClick={onSelect}
    >
      <div className={styles.previewArea}>
        <StylePreview preset={preset} aspectRatio={aspectRatio} />
        {isSelected && (
          <div className={styles.selectedIndicator}>
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
              <polyline points="20 6 9 17 4 12" />
            </svg>
          </div>
        )}
      </div>
      <div className={styles.cardFooter}>
        <div className={styles.presetName}>
          {preset.name}
          {preset.is_system && <span className={styles.systemBadge}>Системный</span>}
        </div>
        <div className={styles.styleChars}>
          {fontFamily}
          {accentColor && accentName && (
            <>
              <span className={styles.divider}>·</span>
              <span
                className={styles.colorDot}
                style={{ background: accentColor }}
              />
              <span style={{ color: accentColor }}>{accentName}</span>
            </>
          )}
        </div>
      </div>
      {/* Context menu for edit/delete - preserve existing */}
    </div>
  )
}
```

- [ ] **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<IPresetGridProps> = ({
  presets,
  selectedPresetId,
  onSelect,
  onEdit,
  onDelete,
  onCreate,
}): JSX.Element => {
  const { primaryFileId } = useWizard()
  const { aspectRatio, isLoading: isLoadingMetadata } = useVideoMetadata(primaryFileId)

  if (isLoadingMetadata) {
    return (
      <div className={styles.presetGrid}>
        {Array.from({ length: 4 }).map((_, i) => (
          <PresetCardSkeleton key={i} aspectRatio={aspectRatio} />
        ))}
      </div>
    )
  }

  return (
    <div className={styles.presetGrid}>
      {presets.map((preset, index) => (
        <div
          key={preset.id}
          className={styles.cardWrapper}
          style={{ animationDelay: `${index * 50}ms` }}
        >
          <PresetCard
            preset={preset}
            isSelected={preset.id === selectedPresetId}
            aspectRatio={aspectRatio}
            onSelect={() => onSelect(preset.id)}
            onEdit={() => onEdit(preset.id)}
            onDelete={() => onDelete(preset.id)}
          />
        </div>
      ))}
      {/* Create new card - preserve existing */}
    </div>
  )
}
```

- [ ] **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)
