fix(editor): kill empty band + first-frame preview + polish
Browse filesLayout repair:
- Empty band above the editor topbar was caused by .editor-topbar
being position:sticky;top:72px while inside .editor-shell.nle
(overflow:hidden, no scroll context). The 72px offset rendered as
a 72px gap. Override to position:relative;top:auto in NLE mode.
- Compact AppHeader (56px) when in editor view; hide subtitle line.
Reclaim ~50px of vertical real estate for the timeline.
Preview stability:
- Add playsInline + preload=metadata + muted to the preview <video>.
- video.load() + currentTime=0.05 on clip change forces the first
frame to render so the canvas isn't blank before user interaction.
Trim handles:
- Widen drag handles from 10px to 14px and bump z-index to 4.
- Switch to amber/black for higher contrast against the indigo clip.
- Hover scales the handle 1.06x for clearer affordance.
AI Assistant copy:
- Subtitles were redundant ("ทำใหม่" / "—"). Replace with platform-
specific guidance ("Best for Reels & Shorts", "Best for TikTok
storytelling") so each tile communicates a distinct intent.
- Rename Tighten/Emphasize labels to explicit durations (30s/60s).
- Add aiDeleteClip + four ai*Sub keys across all 5 languages.
Inspector cleanup:
- Drop the duplicate inner "Inspector" h4 (panel head already labels it).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- frontend/src/App.jsx +65 -23
- frontend/src/styles.css +57 -16
|
@@ -282,9 +282,14 @@ const en = {
|
|
| 282 |
mediaBin: "Clips",
|
| 283 |
aiAssistant: "AI Assistant",
|
| 284 |
aiReason: "AI hasn't explained yet — try regenerating.",
|
| 285 |
-
aiTighten: "Tighten
|
| 286 |
-
aiEmphasize: "
|
| 287 |
-
aiRedoAll: "Regenerate
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
dragToTrim: "Drag edges to trim · drag body to move",
|
| 289 |
dragToPosition: "Drag caption to reposition",
|
| 290 |
};
|
|
@@ -451,9 +456,14 @@ const translations = {
|
|
| 451 |
mediaBin: "คลิปทั้งหมด",
|
| 452 |
aiAssistant: "ผู้ช่วย AI",
|
| 453 |
aiReason: "AI ยังไม่ได้อธิบาย ลองสร้างใหม่ดูสิ",
|
| 454 |
-
aiTighten: "ตัด
|
| 455 |
-
aiEmphasize: "เน
|
| 456 |
aiRedoAll: "สร้างคลิปนี้ใหม่",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 457 |
dragToTrim: "ลากขอบเพื่อ trim · ลากกลางเพื่อย้าย",
|
| 458 |
dragToPosition: "ลากข้อความเพื่อย้ายตำแหน่ง",
|
| 459 |
},
|
|
@@ -618,9 +628,14 @@ const translations = {
|
|
| 618 |
mediaBin: "クリップ一覧",
|
| 619 |
aiAssistant: "AIアシスタント",
|
| 620 |
aiReason: "AIの説明はまだありません。再生成してみてください。",
|
| 621 |
-
aiTighten: "短
|
| 622 |
-
aiEmphasize: "
|
| 623 |
aiRedoAll: "このクリップを再生成",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 624 |
dragToTrim: "端をドラッグでトリム · 中央をドラッグで移動",
|
| 625 |
dragToPosition: "字幕をドラッグして移動",
|
| 626 |
},
|
|
@@ -784,9 +799,14 @@ const translations = {
|
|
| 784 |
mediaBin: "片段列表",
|
| 785 |
aiAssistant: "AI 助手",
|
| 786 |
aiReason: "AI 还没解释,试试重新生成。",
|
| 787 |
-
aiTighten: "
|
| 788 |
-
aiEmphasize: "
|
| 789 |
aiRedoAll: "重新生成此片段",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 790 |
dragToTrim: "拖动边缘修剪 · 拖动中央移动",
|
| 791 |
dragToPosition: "拖动字幕移动位置",
|
| 792 |
},
|
|
@@ -951,9 +971,14 @@ const translations = {
|
|
| 951 |
mediaBin: "클립 목록",
|
| 952 |
aiAssistant: "AI 어시스턴트",
|
| 953 |
aiReason: "AI가 아직 설명하지 않았습니다. 다시 만들어 보세요.",
|
| 954 |
-
aiTighten: "
|
| 955 |
-
aiEmphasize: "
|
| 956 |
aiRedoAll: "이 클립 다시 만들기",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 957 |
dragToTrim: "끝을 드래그해 트림 · 가운데를 드래그해 이동",
|
| 958 |
dragToPosition: "자막을 드래그해 이동",
|
| 959 |
},
|
|
@@ -1137,6 +1162,7 @@ function App() {
|
|
| 1137 |
theme={theme}
|
| 1138 |
setTheme={setTheme}
|
| 1139 |
t={t}
|
|
|
|
| 1140 |
/>
|
| 1141 |
|
| 1142 |
{view === "editor" && editorClipId && editorClip ? (
|
|
@@ -1254,20 +1280,20 @@ function Dashboard({
|
|
| 1254 |
// ============================================================
|
| 1255 |
// App Header
|
| 1256 |
// ============================================================
|
| 1257 |
-
function AppHeader({ job, health, language, setLanguage, theme, setTheme, t }) {
|
| 1258 |
const status = job?.status || "idle";
|
| 1259 |
const modeLabel = health ? (health.demo_mode ? t("demoMode") : t("productionMode")) : "API";
|
| 1260 |
const modeClass = health ? (health.demo_mode ? "demo" : "prod") : "";
|
| 1261 |
|
| 1262 |
return (
|
| 1263 |
-
<header className=
|
| 1264 |
<div className="brand-block">
|
| 1265 |
<div className="brand-mark">
|
| 1266 |
<Scissors size={20} />
|
| 1267 |
</div>
|
| 1268 |
<div>
|
| 1269 |
<h1>ElevenClip.AI</h1>
|
| 1270 |
-
<p>{t("appSubtitle")}</p>
|
| 1271 |
</div>
|
| 1272 |
</div>
|
| 1273 |
|
|
@@ -1750,7 +1776,7 @@ function ClipEditorPage({
|
|
| 1750 |
1
|
| 1751 |
);
|
| 1752 |
|
| 1753 |
-
// Sync playhead with video element
|
| 1754 |
useEffect(() => {
|
| 1755 |
const video = videoRef.current;
|
| 1756 |
if (!video) return;
|
|
@@ -1763,15 +1789,26 @@ function ClipEditorPage({
|
|
| 1763 |
function onPause() {
|
| 1764 |
setIsPlaying(false);
|
| 1765 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1766 |
video.addEventListener("timeupdate", onTimeUpdate);
|
| 1767 |
video.addEventListener("play", onPlay);
|
| 1768 |
video.addEventListener("pause", onPause);
|
|
|
|
|
|
|
| 1769 |
return () => {
|
| 1770 |
video.removeEventListener("timeupdate", onTimeUpdate);
|
| 1771 |
video.removeEventListener("play", onPlay);
|
| 1772 |
video.removeEventListener("pause", onPause);
|
|
|
|
| 1773 |
};
|
| 1774 |
-
}, [clip.id, clip.start_seconds]);
|
| 1775 |
|
| 1776 |
// Reset playhead when clip changes
|
| 1777 |
useEffect(() => {
|
|
@@ -2026,7 +2063,13 @@ function PreviewStage({
|
|
| 2026 |
<div className="preview-stage" ref={stageRef}>
|
| 2027 |
<div className="preview-stage-canvas">
|
| 2028 |
{clip.video_url ? (
|
| 2029 |
-
<video
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2030 |
) : (
|
| 2031 |
<Film size={56} style={{ color: "var(--text-soft)" }} />
|
| 2032 |
)}
|
|
@@ -2361,7 +2404,7 @@ function AIAssistantPanel({ clip, t, onRegenerate, onTighten, onFitLength, onDel
|
|
| 2361 |
</span>
|
| 2362 |
<span className="ai-action-text">
|
| 2363 |
<strong>{t("aiRedoAll")}</strong>
|
| 2364 |
-
<small>{t("
|
| 2365 |
</span>
|
| 2366 |
</button>
|
| 2367 |
<button type="button" className="ai-action" onClick={onTighten}>
|
|
@@ -2370,7 +2413,7 @@ function AIAssistantPanel({ clip, t, onRegenerate, onTighten, onFitLength, onDel
|
|
| 2370 |
</span>
|
| 2371 |
<span className="ai-action-text">
|
| 2372 |
<strong>{t("aiTighten")}</strong>
|
| 2373 |
-
<small>
|
| 2374 |
</span>
|
| 2375 |
</button>
|
| 2376 |
<button
|
|
@@ -2383,7 +2426,7 @@ function AIAssistantPanel({ clip, t, onRegenerate, onTighten, onFitLength, onDel
|
|
| 2383 |
</span>
|
| 2384 |
<span className="ai-action-text">
|
| 2385 |
<strong>{t("aiEmphasize")}</strong>
|
| 2386 |
-
<small>
|
| 2387 |
</span>
|
| 2388 |
</button>
|
| 2389 |
<button
|
|
@@ -2398,8 +2441,8 @@ function AIAssistantPanel({ clip, t, onRegenerate, onTighten, onFitLength, onDel
|
|
| 2398 |
<Trash2 size={14} />
|
| 2399 |
</span>
|
| 2400 |
<span className="ai-action-text">
|
| 2401 |
-
<strong>{t("
|
| 2402 |
-
<small>
|
| 2403 |
</span>
|
| 2404 |
</button>
|
| 2405 |
</div>
|
|
@@ -2433,7 +2476,6 @@ function EditorInspector({
|
|
| 2433 |
<div className="nle-panel-body">
|
| 2434 |
<div className="inspector-stack">
|
| 2435 |
<section>
|
| 2436 |
-
<h4>{t("inspector")}</h4>
|
| 2437 |
<dl className="inspector-meta">
|
| 2438 |
<div>
|
| 2439 |
<dt>{t("score")}</dt>
|
|
|
|
| 282 |
mediaBin: "Clips",
|
| 283 |
aiAssistant: "AI Assistant",
|
| 284 |
aiReason: "AI hasn't explained yet — try regenerating.",
|
| 285 |
+
aiTighten: "Tighten to 30s",
|
| 286 |
+
aiEmphasize: "Extend to 60s",
|
| 287 |
+
aiRedoAll: "Regenerate clip",
|
| 288 |
+
aiDeleteClip: "Remove clip",
|
| 289 |
+
aiActionRedoSub: "Let AI pick a new moment",
|
| 290 |
+
aiActionTightenSub: "Best for Reels & Shorts",
|
| 291 |
+
aiActionEmphasizeSub: "Best for TikTok storytelling",
|
| 292 |
+
aiActionDeleteSub: "Drop from this batch",
|
| 293 |
dragToTrim: "Drag edges to trim · drag body to move",
|
| 294 |
dragToPosition: "Drag caption to reposition",
|
| 295 |
};
|
|
|
|
| 456 |
mediaBin: "คลิปทั้งหมด",
|
| 457 |
aiAssistant: "ผู้ช่วย AI",
|
| 458 |
aiReason: "AI ยังไม่ได้อธิบาย ลองสร้างใหม่ดูสิ",
|
| 459 |
+
aiTighten: "ตัดเหลือ 30 วิ",
|
| 460 |
+
aiEmphasize: "ขยายเป็น 60 วิ",
|
| 461 |
aiRedoAll: "สร้างคลิปนี้ใหม่",
|
| 462 |
+
aiDeleteClip: "ลบคลิปนี้",
|
| 463 |
+
aiActionRedoSub: "ให้ AI หาช่วงใหม่",
|
| 464 |
+
aiActionTightenSub: "เหมาะกับ Reels และ Shorts",
|
| 465 |
+
aiActionEmphasizeSub: "เหมาะกับ TikTok แบบเล่าเรื่อง",
|
| 466 |
+
aiActionDeleteSub: "เอาออกจากชุดนี้",
|
| 467 |
dragToTrim: "ลากขอบเพื่อ trim · ลากกลางเพื่อย้าย",
|
| 468 |
dragToPosition: "ลากข้อความเพื่อย้ายตำแหน่ง",
|
| 469 |
},
|
|
|
|
| 628 |
mediaBin: "クリップ一覧",
|
| 629 |
aiAssistant: "AIアシスタント",
|
| 630 |
aiReason: "AIの説明はまだありません。再生成してみてください。",
|
| 631 |
+
aiTighten: "30秒に短縮",
|
| 632 |
+
aiEmphasize: "60秒に延長",
|
| 633 |
aiRedoAll: "このクリップを再生成",
|
| 634 |
+
aiDeleteClip: "クリップを削除",
|
| 635 |
+
aiActionRedoSub: "AIに別の場面を選ばせる",
|
| 636 |
+
aiActionTightenSub: "Reels・Shortsに最適",
|
| 637 |
+
aiActionEmphasizeSub: "TikTokのストーリーテリングに最適",
|
| 638 |
+
aiActionDeleteSub: "このバッチから外す",
|
| 639 |
dragToTrim: "端をドラッグでトリム · 中央をドラッグで移動",
|
| 640 |
dragToPosition: "字幕をドラッグして移動",
|
| 641 |
},
|
|
|
|
| 799 |
mediaBin: "片段列表",
|
| 800 |
aiAssistant: "AI 助手",
|
| 801 |
aiReason: "AI 还没解释,试试重新生成。",
|
| 802 |
+
aiTighten: "压缩到 30 秒",
|
| 803 |
+
aiEmphasize: "延长到 60 秒",
|
| 804 |
aiRedoAll: "重新生成此片段",
|
| 805 |
+
aiDeleteClip: "删除此片段",
|
| 806 |
+
aiActionRedoSub: "让 AI 找新的精彩瞬间",
|
| 807 |
+
aiActionTightenSub: "适合 Reels 和 Shorts",
|
| 808 |
+
aiActionEmphasizeSub: "适合 TikTok 故事化内容",
|
| 809 |
+
aiActionDeleteSub: "从本批次移除",
|
| 810 |
dragToTrim: "拖动边缘修剪 · 拖动中央移动",
|
| 811 |
dragToPosition: "拖动字幕移动位置",
|
| 812 |
},
|
|
|
|
| 971 |
mediaBin: "클립 목록",
|
| 972 |
aiAssistant: "AI 어시스턴트",
|
| 973 |
aiReason: "AI가 아직 설명하지 않았습니다. 다시 만들어 보세요.",
|
| 974 |
+
aiTighten: "30초로 줄이기",
|
| 975 |
+
aiEmphasize: "60초로 늘리기",
|
| 976 |
aiRedoAll: "이 클립 다시 만들기",
|
| 977 |
+
aiDeleteClip: "클립 삭제",
|
| 978 |
+
aiActionRedoSub: "AI가 다른 장면을 찾도록",
|
| 979 |
+
aiActionTightenSub: "Reels와 Shorts에 적합",
|
| 980 |
+
aiActionEmphasizeSub: "TikTok 스토리텔링에 적합",
|
| 981 |
+
aiActionDeleteSub: "이번 배치에서 제외",
|
| 982 |
dragToTrim: "끝을 드래그해 트림 · 가운데를 드래그해 이동",
|
| 983 |
dragToPosition: "자막을 드래그해 이동",
|
| 984 |
},
|
|
|
|
| 1162 |
theme={theme}
|
| 1163 |
setTheme={setTheme}
|
| 1164 |
t={t}
|
| 1165 |
+
compact={view === "editor"}
|
| 1166 |
/>
|
| 1167 |
|
| 1168 |
{view === "editor" && editorClipId && editorClip ? (
|
|
|
|
| 1280 |
// ============================================================
|
| 1281 |
// App Header
|
| 1282 |
// ============================================================
|
| 1283 |
+
function AppHeader({ job, health, language, setLanguage, theme, setTheme, t, compact }) {
|
| 1284 |
const status = job?.status || "idle";
|
| 1285 |
const modeLabel = health ? (health.demo_mode ? t("demoMode") : t("productionMode")) : "API";
|
| 1286 |
const modeClass = health ? (health.demo_mode ? "demo" : "prod") : "";
|
| 1287 |
|
| 1288 |
return (
|
| 1289 |
+
<header className={`app-header ${compact ? "compact" : ""}`}>
|
| 1290 |
<div className="brand-block">
|
| 1291 |
<div className="brand-mark">
|
| 1292 |
<Scissors size={20} />
|
| 1293 |
</div>
|
| 1294 |
<div>
|
| 1295 |
<h1>ElevenClip.AI</h1>
|
| 1296 |
+
{!compact && <p>{t("appSubtitle")}</p>}
|
| 1297 |
</div>
|
| 1298 |
</div>
|
| 1299 |
|
|
|
|
| 1776 |
1
|
| 1777 |
);
|
| 1778 |
|
| 1779 |
+
// Sync playhead with video element + force first-frame render
|
| 1780 |
useEffect(() => {
|
| 1781 |
const video = videoRef.current;
|
| 1782 |
if (!video) return;
|
|
|
|
| 1789 |
function onPause() {
|
| 1790 |
setIsPlaying(false);
|
| 1791 |
}
|
| 1792 |
+
function onLoadedMetadata() {
|
| 1793 |
+
// Seek 0.05s in to force first frame to render in <video>
|
| 1794 |
+
try {
|
| 1795 |
+
video.currentTime = 0.05;
|
| 1796 |
+
} catch {
|
| 1797 |
+
/* ignore */
|
| 1798 |
+
}
|
| 1799 |
+
}
|
| 1800 |
video.addEventListener("timeupdate", onTimeUpdate);
|
| 1801 |
video.addEventListener("play", onPlay);
|
| 1802 |
video.addEventListener("pause", onPause);
|
| 1803 |
+
video.addEventListener("loadedmetadata", onLoadedMetadata);
|
| 1804 |
+
video.load();
|
| 1805 |
return () => {
|
| 1806 |
video.removeEventListener("timeupdate", onTimeUpdate);
|
| 1807 |
video.removeEventListener("play", onPlay);
|
| 1808 |
video.removeEventListener("pause", onPause);
|
| 1809 |
+
video.removeEventListener("loadedmetadata", onLoadedMetadata);
|
| 1810 |
};
|
| 1811 |
+
}, [clip.id, clip.start_seconds, clip.video_url]);
|
| 1812 |
|
| 1813 |
// Reset playhead when clip changes
|
| 1814 |
useEffect(() => {
|
|
|
|
| 2063 |
<div className="preview-stage" ref={stageRef}>
|
| 2064 |
<div className="preview-stage-canvas">
|
| 2065 |
{clip.video_url ? (
|
| 2066 |
+
<video
|
| 2067 |
+
ref={videoRef}
|
| 2068 |
+
src={`${API_BASE}${clip.video_url}`}
|
| 2069 |
+
playsInline
|
| 2070 |
+
preload="metadata"
|
| 2071 |
+
muted
|
| 2072 |
+
/>
|
| 2073 |
) : (
|
| 2074 |
<Film size={56} style={{ color: "var(--text-soft)" }} />
|
| 2075 |
)}
|
|
|
|
| 2404 |
</span>
|
| 2405 |
<span className="ai-action-text">
|
| 2406 |
<strong>{t("aiRedoAll")}</strong>
|
| 2407 |
+
<small>{t("aiActionRedoSub")}</small>
|
| 2408 |
</span>
|
| 2409 |
</button>
|
| 2410 |
<button type="button" className="ai-action" onClick={onTighten}>
|
|
|
|
| 2413 |
</span>
|
| 2414 |
<span className="ai-action-text">
|
| 2415 |
<strong>{t("aiTighten")}</strong>
|
| 2416 |
+
<small>{t("aiActionTightenSub")}</small>
|
| 2417 |
</span>
|
| 2418 |
</button>
|
| 2419 |
<button
|
|
|
|
| 2426 |
</span>
|
| 2427 |
<span className="ai-action-text">
|
| 2428 |
<strong>{t("aiEmphasize")}</strong>
|
| 2429 |
+
<small>{t("aiActionEmphasizeSub")}</small>
|
| 2430 |
</span>
|
| 2431 |
</button>
|
| 2432 |
<button
|
|
|
|
| 2441 |
<Trash2 size={14} />
|
| 2442 |
</span>
|
| 2443 |
<span className="ai-action-text">
|
| 2444 |
+
<strong>{t("aiDeleteClip")}</strong>
|
| 2445 |
+
<small>{t("aiActionDeleteSub")}</small>
|
| 2446 |
</span>
|
| 2447 |
</button>
|
| 2448 |
</div>
|
|
|
|
| 2476 |
<div className="nle-panel-body">
|
| 2477 |
<div className="inspector-stack">
|
| 2478 |
<section>
|
|
|
|
| 2479 |
<dl className="inspector-meta">
|
| 2480 |
<div>
|
| 2481 |
<dt>{t("score")}</dt>
|
|
@@ -183,6 +183,22 @@ button:disabled {
|
|
| 183 |
-webkit-backdrop-filter: blur(20px);
|
| 184 |
}
|
| 185 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
:root[data-theme="light"] .app-header {
|
| 187 |
background: rgba(255, 255, 255, 0.88);
|
| 188 |
}
|
|
@@ -1128,11 +1144,28 @@ textarea:focus,
|
|
| 1128 |
NLE 4-panel editor (Premiere-style)
|
| 1129 |
============================================================ */
|
| 1130 |
.editor-shell.nle {
|
| 1131 |
-
height: calc(100vh -
|
| 1132 |
-
min-height: calc(100vh -
|
| 1133 |
overflow: hidden;
|
| 1134 |
}
|
| 1135 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1136 |
.editor-grid-nle {
|
| 1137 |
display: grid;
|
| 1138 |
grid-template-columns: 240px minmax(0, 1fr) 340px;
|
|
@@ -1558,41 +1591,49 @@ textarea:focus,
|
|
| 1558 |
|
| 1559 |
.timeline-handle {
|
| 1560 |
position: absolute;
|
| 1561 |
-
top:
|
| 1562 |
-
bottom:
|
| 1563 |
-
width:
|
| 1564 |
-
background: var(--
|
| 1565 |
cursor: ew-resize;
|
| 1566 |
-
z-index:
|
| 1567 |
display: flex;
|
| 1568 |
align-items: center;
|
| 1569 |
justify-content: center;
|
| 1570 |
-
opacity: 0.
|
| 1571 |
-
transition: opacity 140ms ease, background 140ms ease;
|
|
|
|
|
|
|
| 1572 |
}
|
| 1573 |
|
| 1574 |
.timeline-handle::before {
|
| 1575 |
content: "";
|
| 1576 |
-
width:
|
| 1577 |
-
height:
|
| 1578 |
-
background:
|
| 1579 |
-
border-radius:
|
|
|
|
| 1580 |
}
|
| 1581 |
|
| 1582 |
.timeline-handle:hover,
|
| 1583 |
.timeline-handle.dragging {
|
| 1584 |
opacity: 1;
|
| 1585 |
-
background:
|
|
|
|
| 1586 |
}
|
| 1587 |
|
| 1588 |
.timeline-handle.left {
|
| 1589 |
left: 0;
|
| 1590 |
-
border-radius:
|
| 1591 |
}
|
| 1592 |
|
| 1593 |
.timeline-handle.right {
|
| 1594 |
right: 0;
|
| 1595 |
-
border-radius: 0
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1596 |
}
|
| 1597 |
|
| 1598 |
.timeline-caption-block {
|
|
|
|
| 183 |
-webkit-backdrop-filter: blur(20px);
|
| 184 |
}
|
| 185 |
|
| 186 |
+
.app-header.compact {
|
| 187 |
+
min-height: 56px;
|
| 188 |
+
height: 56px;
|
| 189 |
+
padding: 0 clamp(12px, 3vw, 32px);
|
| 190 |
+
gap: 14px;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
.app-header.compact .brand-mark {
|
| 194 |
+
width: 32px;
|
| 195 |
+
height: 32px;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.app-header.compact h1 {
|
| 199 |
+
font-size: 0.95rem;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
:root[data-theme="light"] .app-header {
|
| 203 |
background: rgba(255, 255, 255, 0.88);
|
| 204 |
}
|
|
|
|
| 1144 |
NLE 4-panel editor (Premiere-style)
|
| 1145 |
============================================================ */
|
| 1146 |
.editor-shell.nle {
|
| 1147 |
+
height: calc(100vh - 56px);
|
| 1148 |
+
min-height: calc(100vh - 56px);
|
| 1149 |
overflow: hidden;
|
| 1150 |
}
|
| 1151 |
|
| 1152 |
+
.editor-shell.nle .editor-topbar {
|
| 1153 |
+
position: relative;
|
| 1154 |
+
top: auto;
|
| 1155 |
+
z-index: auto;
|
| 1156 |
+
padding: 10px clamp(14px, 2vw, 28px);
|
| 1157 |
+
flex: 0 0 auto;
|
| 1158 |
+
}
|
| 1159 |
+
|
| 1160 |
+
.editor-shell.nle .editor-topbar-info h2 {
|
| 1161 |
+
font-size: 0.86rem;
|
| 1162 |
+
}
|
| 1163 |
+
|
| 1164 |
+
.editor-shell.nle .editor-topbar-info p {
|
| 1165 |
+
font-size: 0.72rem;
|
| 1166 |
+
margin-top: 2px;
|
| 1167 |
+
}
|
| 1168 |
+
|
| 1169 |
.editor-grid-nle {
|
| 1170 |
display: grid;
|
| 1171 |
grid-template-columns: 240px minmax(0, 1fr) 340px;
|
|
|
|
| 1591 |
|
| 1592 |
.timeline-handle {
|
| 1593 |
position: absolute;
|
| 1594 |
+
top: -2px;
|
| 1595 |
+
bottom: -2px;
|
| 1596 |
+
width: 14px;
|
| 1597 |
+
background: var(--accent);
|
| 1598 |
cursor: ew-resize;
|
| 1599 |
+
z-index: 4;
|
| 1600 |
display: flex;
|
| 1601 |
align-items: center;
|
| 1602 |
justify-content: center;
|
| 1603 |
+
opacity: 0.95;
|
| 1604 |
+
transition: opacity 140ms ease, background 140ms ease, transform 140ms ease;
|
| 1605 |
+
border: 1px solid rgba(0, 0, 0, 0.35);
|
| 1606 |
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
|
| 1607 |
}
|
| 1608 |
|
| 1609 |
.timeline-handle::before {
|
| 1610 |
content: "";
|
| 1611 |
+
width: 3px;
|
| 1612 |
+
height: 18px;
|
| 1613 |
+
background: #1a1300;
|
| 1614 |
+
border-radius: 1.5px;
|
| 1615 |
+
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.25);
|
| 1616 |
}
|
| 1617 |
|
| 1618 |
.timeline-handle:hover,
|
| 1619 |
.timeline-handle.dragging {
|
| 1620 |
opacity: 1;
|
| 1621 |
+
background: #fbbf24;
|
| 1622 |
+
transform: scaleY(1.06);
|
| 1623 |
}
|
| 1624 |
|
| 1625 |
.timeline-handle.left {
|
| 1626 |
left: 0;
|
| 1627 |
+
border-radius: 4px 0 0 4px;
|
| 1628 |
}
|
| 1629 |
|
| 1630 |
.timeline-handle.right {
|
| 1631 |
right: 0;
|
| 1632 |
+
border-radius: 0 4px 4px 0;
|
| 1633 |
+
}
|
| 1634 |
+
|
| 1635 |
+
.timeline-clip {
|
| 1636 |
+
padding: 0 22px;
|
| 1637 |
}
|
| 1638 |
|
| 1639 |
.timeline-caption-block {
|