docs initial
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
# Codex Team Policy Fixes
|
||||
|
||||
Date: 2026-04-05
|
||||
|
||||
## Scope
|
||||
- Fix the `.codex/memories` path convention so the shared rule and per-agent instructions use the same agent IDs.
|
||||
- Tighten the team-first wording so non-trivial repo work consults the team by default.
|
||||
- Remove role skill assignments that depend on unavailable review infrastructure.
|
||||
|
||||
## Approved Approach
|
||||
Use a minimal patch:
|
||||
- Standardize memory paths on the actual Codex agent IDs, which use underscores.
|
||||
- Change the consultation policy from "before deep analysis" to "before any non-trivial repo task", while keeping a narrow exception for purely mechanical actions and explicit user opt-outs.
|
||||
- Replace non-executable `requesting-code-review` entries with executable installed skills only.
|
||||
|
||||
## Intended Changes
|
||||
### Memory paths
|
||||
- Update `.codex/agent-team.md` to state that memories live under `.codex/memories/<agent_id>/`.
|
||||
- Update every `.codex/agents/*.toml` file to reference underscore-based memory directories matching the agent names.
|
||||
- Update `.codex/memories/README.md` examples to use `<agent_id>` wording.
|
||||
|
||||
### Team-first policy
|
||||
- Update `AGENTS.md` and `.codex/agent-team.md` to require team consultation before any non-trivial repo task.
|
||||
- Keep a narrow local-only exception for purely mechanical actions that cannot materially change behavior, architecture, or risk.
|
||||
|
||||
### Skill map
|
||||
- Remove `requesting-code-review` from roles because the required `superpowers:code-reviewer` subagent is not available in this workspace.
|
||||
- Keep the map limited to executable skills already installed in the current environment.
|
||||
|
||||
## Success Criteria
|
||||
- Shared policy and per-agent instructions point to the same memory paths.
|
||||
- The root guidance no longer leaves "deep analysis" as the main threshold for consulting the team.
|
||||
- The skill map contains only practically usable role assignments for this environment.
|
||||
@@ -0,0 +1,625 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Subtitle Preset Grid Redesign Demo</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Lobster&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
/* Catppuccin Mocha - Matching the actual project colors */
|
||||
:root {
|
||||
/* Backgrounds */
|
||||
--bg-canvas: #11111b;
|
||||
--bg-default: #1e1e2e;
|
||||
--bg-surface: #313244;
|
||||
--bg-hover: #45475a;
|
||||
--bg-default-invert: #eff1f5;
|
||||
|
||||
/* Text */
|
||||
--text-primary: #cdd6f4;
|
||||
--text-secondary: #bac2de;
|
||||
--text-tertiary: #9399b2;
|
||||
|
||||
/* Borders */
|
||||
--border-default: #45475a;
|
||||
--border-subtle: #313244;
|
||||
|
||||
/* Purples (accent) */
|
||||
--purple-400: #cba6f7;
|
||||
--purple-500: #d9bcfa;
|
||||
--purple-600: #e4cffc;
|
||||
--purple-700: #eddfff;
|
||||
--purple-300: #6a5a93;
|
||||
--purple-200: #4b4168;
|
||||
--purple-100: #362f4c;
|
||||
--purple-50: #2b253b;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px rgba(17, 17, 27, 0.5);
|
||||
--shadow-md: 0 4px 6px -1px rgba(17, 17, 27, 0.58), 0 24px 48px -12px rgba(17, 17, 27, 0.52);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(17, 17, 27, 0.6), 0 40px 80px -20px rgba(17, 17, 27, 0.7);
|
||||
|
||||
/* Accent glow */
|
||||
--accent-shadow: rgba(203, 166, 247, 0.22);
|
||||
--accent-shadow-hover: rgba(203, 166, 247, 0.3);
|
||||
|
||||
/* Preview background */
|
||||
--preview-bg: #0c0a1a;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: var(--bg-canvas);
|
||||
color: var(--text-primary);
|
||||
padding: 40px 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at 50% 0%, rgba(203, 166, 247, 0.08) 0%, transparent 55%);
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 32px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.aspect-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.aspect-btn {
|
||||
padding: 8px 14px;
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-default);
|
||||
color: var(--text-secondary);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.aspect-btn:hover {
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--purple-400);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.aspect-btn.active {
|
||||
background: var(--purple-100);
|
||||
border-color: var(--purple-400);
|
||||
color: var(--purple-400);
|
||||
}
|
||||
|
||||
/* Grid */
|
||||
.preset-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
/* Preset Card */
|
||||
.preset-card {
|
||||
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;
|
||||
}
|
||||
|
||||
.preset-card:hover {
|
||||
border-color: var(--purple-400);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.preset-card.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);
|
||||
}
|
||||
|
||||
.preset-card.selected::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;
|
||||
}
|
||||
|
||||
/* Preview Area */
|
||||
.preview-area {
|
||||
position: relative;
|
||||
aspect-ratio: 16 / 9;
|
||||
background: var(--preview-bg);
|
||||
overflow: hidden;
|
||||
transition: aspect-ratio 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.preview-area.vertical {
|
||||
aspect-ratio: 9 / 16;
|
||||
}
|
||||
|
||||
.preview-area.square {
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.preview-area.instagram {
|
||||
aspect-ratio: 4 / 5;
|
||||
}
|
||||
|
||||
.preview-text {
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
font-size: 28px;
|
||||
line-height: 1.4;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.preview-text .highlight {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Card Footer */
|
||||
.card-footer {
|
||||
padding: 14px 16px;
|
||||
background: linear-gradient(to top, var(--bg-surface), var(--bg-default));
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.preset-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.system-badge {
|
||||
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;
|
||||
}
|
||||
|
||||
.style-chars {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.color-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Create New Card */
|
||||
.create-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
aspect-ratio: 16 / 9;
|
||||
background: transparent;
|
||||
border: 2px dashed var(--border-default);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: var(--text-tertiary);
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.create-card:hover {
|
||||
border-color: var(--purple-400);
|
||||
background: rgba(203, 166, 247, 0.05);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.create-card svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Skeleton Loading - Improved */
|
||||
.skeleton-card {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: var(--bg-default);
|
||||
border: 1px solid var(--border-subtle);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.skeleton-preview {
|
||||
aspect-ratio: 16 / 9;
|
||||
background: var(--bg-surface);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.skeleton-preview::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%); }
|
||||
}
|
||||
|
||||
.skeleton-footer {
|
||||
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;
|
||||
}
|
||||
|
||||
.skeleton-line {
|
||||
height: 14px;
|
||||
background: var(--bg-hover);
|
||||
border-radius: 4px;
|
||||
width: 60%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.skeleton-line.short {
|
||||
width: 40%;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.skeleton-line::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;
|
||||
}
|
||||
|
||||
/* Section Label */
|
||||
.section-label {
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Selected indicator */
|
||||
.selected-indicator {
|
||||
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);
|
||||
}
|
||||
|
||||
.selected-indicator svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
color: var(--bg-canvas);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.preset-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Выбор пресета субтитров</h1>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
<label>Аспектное соотношение видео</label>
|
||||
<div class="aspect-buttons">
|
||||
<button class="aspect-btn active" data-ratio="16:9">16:9 (Широкое)</button>
|
||||
<button class="aspect-btn" data-ratio="9:16">9:16 (Вертикальное)</button>
|
||||
<button class="aspect-btn" data-ratio="1:1">1:1 (Квадрат)</button>
|
||||
<button class="aspect-btn" data-ratio="4:5">4:5 (Instagram)</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section: Ready Presets -->
|
||||
<div class="section-label">Пример: готовые пресеты</div>
|
||||
<div class="preset-grid" id="presetGrid">
|
||||
<!-- Card 1: Классические -->
|
||||
<div class="preset-card selected" data-preset="classic">
|
||||
<div class="preview-area">
|
||||
<div class="preview-text" style="font-family: 'Lobster', cursive; color: #ffffff;">
|
||||
Пример <span class="highlight" style="color: #FFD700;">субтитров</span>
|
||||
</div>
|
||||
<div class="selected-indicator">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="preset-name">
|
||||
Классические
|
||||
<span class="system-badge">Системный</span>
|
||||
</div>
|
||||
<div class="style-chars">
|
||||
Lobster
|
||||
<span style="color: var(--border-default);">·</span>
|
||||
<span class="color-dot" style="background: #FFD700;"></span>
|
||||
<span style="color: #FFD700;">Золотой</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card 2: Неон -->
|
||||
<div class="preset-card" data-preset="neon">
|
||||
<div class="preview-area">
|
||||
<div class="preview-text" style="font-family: 'Inter', sans-serif; font-weight: 700; color: #ffffff; text-shadow: 0 0 10px #00ffff, 0 0 20px #00ffff;">
|
||||
Пример <span class="highlight" style="color: #00ffff;">субтитров</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="preset-name">
|
||||
Неон
|
||||
<span class="system-badge">Системный</span>
|
||||
</div>
|
||||
<div class="style-chars">
|
||||
Inter Bold
|
||||
<span style="color: var(--border-default);">·</span>
|
||||
<span class="color-dot" style="background: #00ffff; box-shadow: 0 0 6px #00ffff;"></span>
|
||||
<span style="color: #00ffff;">Неоновый</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card 3: Минимализм -->
|
||||
<div class="preset-card" data-preset="minimal">
|
||||
<div class="preview-area">
|
||||
<div class="preview-text" style="font-family: 'Inter', sans-serif; font-weight: 400; color: #e0e0e0; font-size: 24px;">
|
||||
Пример <span class="highlight" style="color: #ffffff; font-weight: 500;">субтитров</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="preset-name">
|
||||
Минимализм
|
||||
<span class="system-badge">Системный</span>
|
||||
</div>
|
||||
<div class="style-chars">
|
||||
Inter Regular
|
||||
<span style="color: var(--border-default);">·</span>
|
||||
<span class="color-dot" style="background: #ffffff;"></span>
|
||||
<span style="color: #ffffff;">Белый</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card 4: Жирный -->
|
||||
<div class="preset-card" data-preset="bold">
|
||||
<div class="preview-area">
|
||||
<div class="preview-text" style="font-family: 'Inter', sans-serif; font-weight: 900; color: #ffffff; -webkit-text-stroke: 2px #000000; font-size: 32px;">
|
||||
Пример <span class="highlight" style="color: #ff006e;">субтитров</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="preset-name">
|
||||
Жирный
|
||||
<span class="system-badge">Системный</span>
|
||||
</div>
|
||||
<div class="style-chars">
|
||||
Inter Black
|
||||
<span style="color: var(--border-default);">·</span>
|
||||
<span class="color-dot" style="background: #ff006e;"></span>
|
||||
<span style="color: #ff006e;">Розовый</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create New Card -->
|
||||
<div class="create-card">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
<span style="font-size: 14px; font-weight: 500;">Создать пресет</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section: Loading State -->
|
||||
<div class="section-label">Пример: состояние загрузки</div>
|
||||
<div class="preset-grid">
|
||||
<div class="skeleton-card">
|
||||
<div class="skeleton-preview"></div>
|
||||
<div class="skeleton-footer">
|
||||
<div class="skeleton-line"></div>
|
||||
<div class="skeleton-line short"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skeleton-card">
|
||||
<div class="skeleton-preview"></div>
|
||||
<div class="skeleton-footer">
|
||||
<div class="skeleton-line"></div>
|
||||
<div class="skeleton-line short"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skeleton-card">
|
||||
<div class="skeleton-preview"></div>
|
||||
<div class="skeleton-footer">
|
||||
<div class="skeleton-line"></div>
|
||||
<div class="skeleton-line short"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skeleton-card">
|
||||
<div class="skeleton-preview"></div>
|
||||
<div class="skeleton-footer">
|
||||
<div class="skeleton-line"></div>
|
||||
<div class="skeleton-line short"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Aspect ratio switching
|
||||
const aspectButtons = document.querySelectorAll('.aspect-btn');
|
||||
const previewAreas = document.querySelectorAll('.preview-area');
|
||||
const skeletonPreviews = document.querySelectorAll('.skeleton-preview');
|
||||
const createCard = document.querySelector('.create-card');
|
||||
|
||||
aspectButtons.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
// Update active button
|
||||
aspectButtons.forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
|
||||
const ratio = btn.dataset.ratio;
|
||||
|
||||
// Update preview areas
|
||||
previewAreas.forEach(preview => {
|
||||
preview.classList.remove('vertical', 'square', 'instagram');
|
||||
if (ratio === '9:16') preview.classList.add('vertical');
|
||||
if (ratio === '1:1') preview.classList.add('square');
|
||||
if (ratio === '4:5') preview.classList.add('instagram');
|
||||
});
|
||||
|
||||
// Update skeleton previews
|
||||
skeletonPreviews.forEach(preview => {
|
||||
if (ratio === '9:16') preview.style.aspectRatio = '9 / 16';
|
||||
else if (ratio === '1:1') preview.style.aspectRatio = '1 / 1';
|
||||
else if (ratio === '4:5') preview.style.aspectRatio = '4 / 5';
|
||||
else preview.style.aspectRatio = '16 / 9';
|
||||
});
|
||||
|
||||
// Update create card
|
||||
if (ratio === '9:16') createCard.style.aspectRatio = '9 / 16';
|
||||
else if (ratio === '1:1') createCard.style.aspectRatio = '1 / 1';
|
||||
else if (ratio === '4:5') createCard.style.aspectRatio = '4 / 5';
|
||||
else createCard.style.aspectRatio = '16 / 9';
|
||||
});
|
||||
});
|
||||
|
||||
// Card selection
|
||||
const presetCards = document.querySelectorAll('.preset-card');
|
||||
presetCards.forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
presetCards.forEach(c => {
|
||||
c.classList.remove('selected');
|
||||
const indicator = c.querySelector('.selected-indicator');
|
||||
if (indicator) indicator.remove();
|
||||
});
|
||||
card.classList.add('selected');
|
||||
|
||||
// Add checkmark indicator
|
||||
const preview = card.querySelector('.preview-area');
|
||||
if (!preview.querySelector('.selected-indicator')) {
|
||||
const indicator = document.createElement('div');
|
||||
indicator.className = 'selected-indicator';
|
||||
indicator.innerHTML = `
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg>
|
||||
`;
|
||||
preview.appendChild(indicator);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,212 @@
|
||||
# Subtitle Preset Grid Redesign - Design Document
|
||||
|
||||
**Date:** 2026-04-06
|
||||
**Scope:** Redesign preset preview cards in Caption Settings step to match uploaded video aspect ratio with modern visual refresh
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Redesign the subtitle preset selection grid to:
|
||||
1. Display preset previews with the **same aspect ratio as the uploaded video**
|
||||
2. Apply a **modern visual refresh** consistent with the app's design language
|
||||
3. Show **style characteristics** (font, colors) as subtle hints
|
||||
4. Maintain **responsive layout** across screen sizes
|
||||
|
||||
---
|
||||
|
||||
## Core Functionality
|
||||
|
||||
### Dynamic Aspect Ratio
|
||||
|
||||
**Data Flow:**
|
||||
1. Fetch video metadata via `GET /api/media/mediafiles/{media_file_id}/` using `primaryFileId` from WizardContext
|
||||
2. Extract `width` and `height` from `MediaFileRead` response
|
||||
3. Calculate aspect ratio: `width / height`
|
||||
4. Apply as CSS `aspect-ratio` to preset cards via inline style or CSS variable
|
||||
5. Handle loading state while fetching metadata
|
||||
6. Fallback to 16:9 if no video is uploaded or API error occurs
|
||||
|
||||
**Implementation Notes:**
|
||||
- Store aspect ratio in WizardContext alongside other video metadata
|
||||
- Update ratio when `primaryFileId` changes
|
||||
- Cards use container queries for responsive sizing
|
||||
|
||||
---
|
||||
|
||||
## Visual Design (5 Pillars Applied)
|
||||
|
||||
### 1. Typography with Character
|
||||
- Keep existing font system (consistent with app)
|
||||
- Style name: `font-weight: 500`, `font-size: 14px`
|
||||
- Characteristic labels: `font-size: 12px`, muted color (`--gray-10`)
|
||||
|
||||
### 2. Committed Color & Theme
|
||||
- Uses **Catppuccin Mocha** palette matching the project:
|
||||
- Canvas: `--bg-canvas: #11111b`
|
||||
- Cards: `--bg-default: #1e1e2e`
|
||||
- Surfaces: `--bg-surface: #313244`
|
||||
- Borders: `--border-default: #45475a`, `--border-subtle: #313244`
|
||||
- Text: `--text-primary: #cdd6f4`, `--text-secondary: #bac2de`, `--text-tertiary: #9399b2`
|
||||
- Selected state: purple accent (`--purple-400: #cba6f7`) with glow shadow
|
||||
- Card hover: border transitions to purple accent
|
||||
- System badge: purple-100 background with purple-400 text
|
||||
- Checkmark indicator on selected card (top-right corner)
|
||||
|
||||
### 3. Purposeful Motion
|
||||
- Cards fade in with staggered animation (50ms delay per card)
|
||||
- Smooth border-color transition on hover (150ms ease)
|
||||
- Selection change: immediate border color change
|
||||
- Loading skeleton: shimmer animation
|
||||
|
||||
### 4. Brave Spatial Composition
|
||||
- CSS Grid with `auto-fill` and `minmax(200px, 1fr)`
|
||||
- Consistent 16px gap between cards
|
||||
- Cards maintain video aspect ratio without stretching
|
||||
- Responsive: more columns on wide screens, fewer on narrow
|
||||
|
||||
### 5. Atmosphere & Depth
|
||||
- Card background: subtle gradient overlay for depth
|
||||
- Selected card: elevated with `box-shadow` + accent glow
|
||||
- Dark preview background (`#0c0a1a`) preserved from existing StylePreview
|
||||
- Rounded corners: `border-radius: 12px`
|
||||
|
||||
---
|
||||
|
||||
## Component Structure
|
||||
|
||||
### PresetCard
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────┐
|
||||
│ │
|
||||
│ [StylePreview Component] │ ← Dynamic aspect-ratio
|
||||
│ "Пример субтитров" │ based on video
|
||||
│ │
|
||||
├──────────────────────────────────────┤
|
||||
│ Style Name [Системный] │ ← Footer
|
||||
│ Lobster · Yellow accent │ ← Characteristics (subtle)
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Props:**
|
||||
- `preset: CaptionPresetRead`
|
||||
- `isSelected: boolean`
|
||||
- `aspectRatio: number` (width/height, e.g., 1.777 for 16:9)
|
||||
- `onSelect: () => void`
|
||||
- `onEdit: () => void`
|
||||
- `onDelete: () => void`
|
||||
|
||||
### StylePreview Updates
|
||||
|
||||
**New Props:**
|
||||
- `aspectRatio?: number` - overrides default 9/16
|
||||
|
||||
**Behavior:**
|
||||
- Uses passed `aspectRatio` for container sizing
|
||||
- Falls back to 9/16 if not provided
|
||||
- Maintains all existing text styling logic
|
||||
|
||||
### PresetGrid Updates
|
||||
|
||||
**New Behavior:**
|
||||
- Fetches video metadata via `useVideoMetadata()` hook
|
||||
- Passes `aspectRatio` to all PresetCard children
|
||||
- Shows skeleton loading state while fetching
|
||||
- Responsive grid layout
|
||||
|
||||
---
|
||||
|
||||
## Style Characteristics Display
|
||||
|
||||
Each card footer shows:
|
||||
- **Font family** (e.g., "Lobster", "Inter") - extracted from `preset.style_config.text.font_family`
|
||||
- **Accent color** - small color dot + name if distinct from default
|
||||
- Hidden on cards narrower than 180px (responsive)
|
||||
|
||||
**Format:**
|
||||
```
|
||||
{font_family} · {accent_color_name}
|
||||
```
|
||||
|
||||
Example: `Lobster · Желтый` or `Inter · Неоновый`
|
||||
|
||||
---
|
||||
|
||||
## Loading State
|
||||
|
||||
**Skeleton Card:**
|
||||
- Same aspect ratio as target (default 16:9 while loading)
|
||||
- Shimmer animation on preview area
|
||||
- Gray placeholder for text
|
||||
- 4-6 skeleton cards shown while loading
|
||||
|
||||
---
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
| Screen Width | Grid Columns | Card Min Width |
|
||||
|--------------|--------------|----------------|
|
||||
| < 480px | 2 | 140px |
|
||||
| 480-768px | 3 | 160px |
|
||||
| 768-1200px | 4 | 180px |
|
||||
| > 1200px | 5-6 | 200px |
|
||||
|
||||
---
|
||||
|
||||
## API Integration
|
||||
|
||||
### New Hook: `useVideoMetadata`
|
||||
|
||||
```typescript
|
||||
function useVideoMetadata(fileId: string | null) {
|
||||
return api.useQuery(
|
||||
"get",
|
||||
"/api/media/mediafiles/{media_file_id}/",
|
||||
{ params: { path: { media_file_id: fileId ?? "" } } },
|
||||
{ enabled: !!fileId }
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Aspect Ratio Calculation
|
||||
|
||||
```typescript
|
||||
const aspectRatio = useMemo(() => {
|
||||
if (!mediaFile?.width || !mediaFile?.height) return 16 / 9
|
||||
return mediaFile.width / mediaFile.height
|
||||
}, [mediaFile])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Edge Cases
|
||||
|
||||
1. **No video uploaded:** Fall back to 16:9 aspect ratio
|
||||
2. **Video metadata unavailable:** Show error toast, fall back to 16:9
|
||||
3. **Very wide video (>21:9):** Cap max card width to prevent overflow
|
||||
4. **Very tall video (9:16+):** Limit max height, allow scrolling if needed
|
||||
5. **No presets:** Show empty state with "Создать пресет" card only
|
||||
|
||||
---
|
||||
|
||||
## Files Modified
|
||||
|
||||
1. `src/features/project/CaptionSettingsStep/PresetGrid.tsx` - Grid logic, aspect ratio distribution
|
||||
2. `src/features/project/CaptionSettingsStep/PresetGrid.module.scss` - Grid styles, responsive layout
|
||||
3. `src/features/project/CaptionSettingsStep/StylePreview.tsx` - Accept aspect ratio prop
|
||||
4. `src/features/project/CaptionSettingsStep/StylePreview.module.scss` - Dynamic sizing
|
||||
5. `src/features/project/CaptionSettingsStep/useVideoMetadata.ts` - New hook (or inline in PresetGrid)
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Preset cards display with uploaded video's aspect ratio
|
||||
- [ ] Grid is responsive and works on mobile/desktop
|
||||
- [ ] Loading state shows skeleton cards
|
||||
- [ ] Style characteristics (font, color) visible on cards
|
||||
- [ ] Selected state clearly visible with accent border
|
||||
- [ ] Hover effects smooth and purposeful
|
||||
- [ ] Fallback to 16:9 when no video available
|
||||
- [ ] All existing functionality preserved (select, edit, delete, create)
|
||||
Reference in New Issue
Block a user