JakgritB commited on
Commit
f585352
·
1 Parent(s): 6eb98ab

fix(frontend): polish locale controls and editor layout

Browse files
Files changed (2) hide show
  1. frontend/src/App.jsx +207 -5
  2. frontend/src/styles.css +97 -3
frontend/src/App.jsx CHANGED
@@ -101,6 +101,12 @@ const en = {
101
  progressNote: "Progress is updated by backend steps. Rendering shows clip-by-clip status.",
102
  currentStep: "Current step",
103
  timing: "Timing",
 
 
 
 
 
 
104
  transcript: "Transcript",
105
  transcriptEmpty: "Transcript will appear after transcription finishes.",
106
  clips: "Clips",
@@ -126,12 +132,27 @@ const en = {
126
  clipRange: "Clip range",
127
  subtitleCues: "Subtitle cues",
128
  subtitleCueHelp: "Short cues read better on TikTok, Reels, and Shorts.",
 
 
 
 
 
 
 
 
 
 
 
 
129
  inspector: "Inspector",
130
  title: "Title",
131
  status: "Status",
132
  notApproved: "Not approved",
133
  model: "Model",
134
  source: "Source",
 
 
 
135
  settings: "Settings",
136
  stepInput: "Input",
137
  stepTranscription: "Transcription",
@@ -210,6 +231,12 @@ const translations = {
210
  progressNote: "แถบนี้อัปเดตตามขั้นตอน backend และตอน render จะแสดงทีละคลิป",
211
  currentStep: "ขั้นตอนปัจจุบัน",
212
  timing: "เวลา",
 
 
 
 
 
 
213
  transcript: "ถอดเสียง",
214
  transcriptEmpty: "ข้อความถอดเสียงจะแสดงหลังประมวลผลเสร็จ",
215
  clips: "คลิป",
@@ -235,12 +262,27 @@ const translations = {
235
  clipRange: "ช่วงคลิป",
236
  subtitleCues: "จังหวะซับ",
237
  subtitleCueHelp: "ซับสั้น ๆ อ่านง่ายกว่าสำหรับ TikTok, Reels และ Shorts",
 
 
 
 
 
 
 
 
 
 
 
 
238
  inspector: "รายละเอียด",
239
  title: "ชื่อคลิป",
240
  status: "สถานะ",
241
  notApproved: "ยังไม่อนุมัติ",
242
  model: "โมเดล",
243
  source: "แหล่งที่มา",
 
 
 
244
  settings: "ตั้งค่า",
245
  stepInput: "รับวิดีโอ",
246
  stepTranscription: "ถอดเสียง",
@@ -317,6 +359,12 @@ const translations = {
317
  progressNote: "進捗はバックエンドの各工程で更新され、レンダリング中はクリップ単位で表示されます。",
318
  currentStep: "現在の工程",
319
  timing: "処理時間",
 
 
 
 
 
 
320
  transcript: "文字起こし",
321
  transcriptEmpty: "文字起こしが完了するとここに表示されます。",
322
  clips: "クリップ",
@@ -336,12 +384,27 @@ const translations = {
336
  subtitles: "字幕",
337
  subtitleCues: "字幕キュー",
338
  subtitleCueHelp: "短い字幕キューのほうがTikTok、Reels、Shortsで読みやすくなります。",
 
 
 
 
 
 
 
 
 
 
 
 
339
  inspector: "詳細",
340
  title: "タイトル",
341
  status: "状態",
342
  notApproved: "未承認",
343
  model: "モデル",
344
  source: "ソース",
 
 
 
345
  settings: "設定",
346
  stepInput: "入力",
347
  stepTranscription: "文字起こし",
@@ -423,6 +486,12 @@ const translations = {
423
  progressNote: "进度会按后端步骤更新,渲染阶段会显示每个剪辑的状态。",
424
  currentStep: "当前步骤",
425
  timing: "耗时",
 
 
 
 
 
 
426
  transcript: "转录文本",
427
  transcriptEmpty: "转录完成后会显示在这里。",
428
  clips: "剪辑",
@@ -442,12 +511,27 @@ const translations = {
442
  subtitles: "字幕",
443
  subtitleCues: "字幕片段",
444
  subtitleCueHelp: "短字幕片段更适合 TikTok、Reels 和 Shorts。",
 
 
 
 
 
 
 
 
 
 
 
 
445
  inspector: "检查器",
446
  title: "标题",
447
  status: "状态",
448
  notApproved: "未批准",
449
  model: "模型",
450
  source: "来源",
 
 
 
451
  settings: "设置",
452
  stepInput: "输入",
453
  stepTranscription: "转录",
@@ -530,6 +614,12 @@ const translations = {
530
  progressNote: "진행률은 백엔드 단계별로 갱신되며, 렌더링 중에는 클립별 상태가 표시됩니다.",
531
  currentStep: "현재 단계",
532
  timing: "소요 시간",
 
 
 
 
 
 
533
  transcript: "전사",
534
  transcriptEmpty: "전사가 끝나면 여기에 표시됩니다.",
535
  clips: "클립",
@@ -549,12 +639,27 @@ const translations = {
549
  subtitles: "자막",
550
  subtitleCues: "자막 큐",
551
  subtitleCueHelp: "짧은 자막 큐가 TikTok, Reels, Shorts에서 더 읽기 쉽습니다.",
 
 
 
 
 
 
 
 
 
 
 
 
552
  inspector: "검사 패널",
553
  title: "제목",
554
  status: "상태",
555
  notApproved: "미승인",
556
  model: "모델",
557
  source: "소스",
 
 
 
558
  settings: "설정",
559
  stepInput: "입력",
560
  stepTranscription: "전사",
@@ -1031,7 +1136,7 @@ function ProgressPanel({ job, t }) {
1031
  <div className="timing-grid">
1032
  {Object.entries(job.timings).map(([name, value]) => (
1033
  <div key={name}>
1034
- <span>{name.replaceAll("_", " ")}</span>
1035
  <strong>{value}s</strong>
1036
  </div>
1037
  ))}
@@ -1177,12 +1282,42 @@ function ClipEditorPage({ clip, job, t, onBack, onPatch, onDelete, onApprove, on
1177
  const duration = Math.max(1, clip.end_seconds - clip.start_seconds);
1178
  const cues = getSubtitleCues(clip, duration);
1179
  const metadataModel = clip.metadata?.model || "unknown";
 
 
 
 
 
 
 
 
1180
 
1181
  function patchCue(index, text) {
1182
  const next = cues.map((cue, cueIndex) => (cueIndex === index ? { ...cue, text } : cue));
1183
  onPatch(clip.id, { subtitle_text: next.map((cue) => cue.text).join(" ") });
1184
  }
1185
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1186
  return (
1187
  <div className="editor-shell">
1188
  <div className="editor-topbar">
@@ -1212,23 +1347,86 @@ function ClipEditorPage({ clip, job, t, onBack, onPatch, onDelete, onApprove, on
1212
  </div>
1213
 
1214
  <div className="range-editor">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1215
  <div className="timeline-visual">
1216
  <div className="timeline-fill" />
1217
- <div className="timeline-window" style={{ left: "18%", width: "46%" }} />
1218
  {Array.from({ length: 9 }).map((_, index) => (
1219
  <span key={index} style={{ left: `${index * 12.5}%` }} />
1220
  ))}
1221
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1222
  <div className="timeline">
1223
  <NumberField
1224
  label={t("start")}
1225
  value={Number(clip.start_seconds).toFixed(1)}
1226
- onChange={(value) => onPatch(clip.id, { start_seconds: Number(value) })}
1227
  />
1228
  <NumberField
1229
  label={t("end")}
1230
  value={Number(clip.end_seconds).toFixed(1)}
1231
- onChange={(value) => onPatch(clip.id, { end_seconds: Number(value) })}
1232
  />
1233
  <strong>{duration.toFixed(1)}s</strong>
1234
  </div>
@@ -1291,7 +1489,7 @@ function ClipEditorPage({ clip, job, t, onBack, onPatch, onDelete, onApprove, on
1291
  </div>
1292
  <div>
1293
  <dt>{t("source")}</dt>
1294
- <dd>{job?.source?.kind || "video"}</dd>
1295
  </div>
1296
  </dl>
1297
 
@@ -1468,6 +1666,10 @@ function roundTime(value) {
1468
  return Math.round(value * 10) / 10;
1469
  }
1470
 
 
 
 
 
1471
  function formatTime(value) {
1472
  const safeValue = Number.isFinite(Number(value)) ? Number(value) : 0;
1473
  const minutes = Math.floor(safeValue / 60);
 
101
  progressNote: "Progress is updated by backend steps. Rendering shows clip-by-clip status.",
102
  currentStep: "Current step",
103
  timing: "Timing",
104
+ timing_input: "Input",
105
+ timing_transcription: "Transcription",
106
+ timing_highlight_detection: "Highlight detection",
107
+ timing_multimodal_analysis: "Multimodal analysis",
108
+ timing_clip_generation: "Clip generation",
109
+ timing_total: "Total",
110
  transcript: "Transcript",
111
  transcriptEmpty: "Transcript will appear after transcription finishes.",
112
  clips: "Clips",
 
132
  clipRange: "Clip range",
133
  subtitleCues: "Subtitle cues",
134
  subtitleCueHelp: "Short cues read better on TikTok, Reels, and Shorts.",
135
+ editorTools: "Editing tools",
136
+ rangeStart: "Range start",
137
+ rangeEnd: "Range end",
138
+ trimStartBack: "Start -0.5s",
139
+ trimStartForward: "Start +0.5s",
140
+ trimEndBack: "End -0.5s",
141
+ trimEndForward: "End +0.5s",
142
+ moveClipLeft: "Move -1s",
143
+ moveClipRight: "Move +1s",
144
+ setClipLength30: "30s cut",
145
+ setClipLength60: "60s cut",
146
+ setClipLength90: "90s cut",
147
  inspector: "Inspector",
148
  title: "Title",
149
  status: "Status",
150
  notApproved: "Not approved",
151
  model: "Model",
152
  source: "Source",
153
+ source_upload: "Upload",
154
+ source_youtube: "YouTube",
155
+ source_video: "Video",
156
  settings: "Settings",
157
  stepInput: "Input",
158
  stepTranscription: "Transcription",
 
231
  progressNote: "แถบนี้อัปเดตตามขั้นตอน backend และตอน render จะแสดงทีละคลิป",
232
  currentStep: "ขั้นตอนปัจจุบัน",
233
  timing: "เวลา",
234
+ timing_input: "รับวิดีโอ",
235
+ timing_transcription: "ถอดเสียง",
236
+ timing_highlight_detection: "หาไฮไลต์",
237
+ timing_multimodal_analysis: "วิเคราะห์ภาพ",
238
+ timing_clip_generation: "สร้างคลิป",
239
+ timing_total: "รวม",
240
  transcript: "ถอดเสียง",
241
  transcriptEmpty: "ข้อความถอดเสียงจะแสดงหลังประมวลผลเสร็จ",
242
  clips: "คลิป",
 
262
  clipRange: "ช่วงคลิป",
263
  subtitleCues: "จังหวะซับ",
264
  subtitleCueHelp: "ซับสั้น ๆ อ่านง่ายกว่าสำหรับ TikTok, Reels และ Shorts",
265
+ editorTools: "เครื่องมือตัดต่อ",
266
+ rangeStart: "ตำแหน่งเริ่ม",
267
+ rangeEnd: "ตำแหน่งจบ",
268
+ trimStartBack: "เริ่ม -0.5 วิ",
269
+ trimStartForward: "เริ่ม +0.5 วิ",
270
+ trimEndBack: "จบ -0.5 วิ",
271
+ trimEndForward: "จบ +0.5 วิ",
272
+ moveClipLeft: "เลื่อนคลิป -1 วิ",
273
+ moveClipRight: "เลื่อนคลิป +1 วิ",
274
+ setClipLength30: "ตัด 30 วิ",
275
+ setClipLength60: "ตัด 60 วิ",
276
+ setClipLength90: "ตัด 90 วิ",
277
  inspector: "รายละเอียด",
278
  title: "ชื่อคลิป",
279
  status: "สถานะ",
280
  notApproved: "ยังไม่อนุมัติ",
281
  model: "โมเดล",
282
  source: "แหล่งที่มา",
283
+ source_upload: "อัปโหลด",
284
+ source_youtube: "YouTube",
285
+ source_video: "วิดีโอ",
286
  settings: "ตั้งค่า",
287
  stepInput: "รับวิดีโอ",
288
  stepTranscription: "ถอดเสียง",
 
359
  progressNote: "進捗はバックエンドの各工程で更新され、レンダリング中はクリップ単位で表示されます。",
360
  currentStep: "現在の工程",
361
  timing: "処理時間",
362
+ timing_input: "入力",
363
+ timing_transcription: "文字起こし",
364
+ timing_highlight_detection: "ハイライト検出",
365
+ timing_multimodal_analysis: "マルチモーダル分析",
366
+ timing_clip_generation: "クリップ生成",
367
+ timing_total: "合計",
368
  transcript: "文字起こし",
369
  transcriptEmpty: "文字起こしが完了するとここに表示されます。",
370
  clips: "クリップ",
 
384
  subtitles: "字幕",
385
  subtitleCues: "字幕キュー",
386
  subtitleCueHelp: "短い字幕キューのほうがTikTok、Reels、Shortsで読みやすくなります。",
387
+ editorTools: "編集ツール",
388
+ rangeStart: "開始位置",
389
+ rangeEnd: "終了位置",
390
+ trimStartBack: "開始 -0.5秒",
391
+ trimStartForward: "開始 +0.5秒",
392
+ trimEndBack: "終了 -0.5秒",
393
+ trimEndForward: "終了 +0.5秒",
394
+ moveClipLeft: "クリップ -1秒",
395
+ moveClipRight: "クリップ +1秒",
396
+ setClipLength30: "30秒カット",
397
+ setClipLength60: "60秒カット",
398
+ setClipLength90: "90秒カット",
399
  inspector: "詳細",
400
  title: "タイトル",
401
  status: "状態",
402
  notApproved: "未承認",
403
  model: "モデル",
404
  source: "ソース",
405
+ source_upload: "アップロード",
406
+ source_youtube: "YouTube",
407
+ source_video: "動画",
408
  settings: "設定",
409
  stepInput: "入力",
410
  stepTranscription: "文字起こし",
 
486
  progressNote: "进度会按后端步骤更新,渲染阶段会显示每个剪辑的状态。",
487
  currentStep: "当前步骤",
488
  timing: "耗时",
489
+ timing_input: "输入",
490
+ timing_transcription: "转录",
491
+ timing_highlight_detection: "高光检测",
492
+ timing_multimodal_analysis: "多模态分析",
493
+ timing_clip_generation: "剪辑生成",
494
+ timing_total: "总计",
495
  transcript: "转录文本",
496
  transcriptEmpty: "转录完成后会显示在这里。",
497
  clips: "剪辑",
 
511
  subtitles: "字幕",
512
  subtitleCues: "字幕片段",
513
  subtitleCueHelp: "短字幕片段更适合 TikTok、Reels 和 Shorts。",
514
+ editorTools: "编辑工具",
515
+ rangeStart: "开始位置",
516
+ rangeEnd: "结束位置",
517
+ trimStartBack: "开始 -0.5 秒",
518
+ trimStartForward: "开始 +0.5 秒",
519
+ trimEndBack: "结束 -0.5 秒",
520
+ trimEndForward: "结束 +0.5 秒",
521
+ moveClipLeft: "剪辑 -1 秒",
522
+ moveClipRight: "剪辑 +1 秒",
523
+ setClipLength30: "30 秒剪辑",
524
+ setClipLength60: "60 秒剪辑",
525
+ setClipLength90: "90 秒剪辑",
526
  inspector: "检查器",
527
  title: "标题",
528
  status: "状态",
529
  notApproved: "未批准",
530
  model: "模型",
531
  source: "来源",
532
+ source_upload: "上传",
533
+ source_youtube: "YouTube",
534
+ source_video: "视频",
535
  settings: "设置",
536
  stepInput: "输入",
537
  stepTranscription: "转录",
 
614
  progressNote: "진행률은 백엔드 단계별로 갱신되며, 렌더링 중에는 클립별 상태가 표시됩니다.",
615
  currentStep: "현재 단계",
616
  timing: "소요 시간",
617
+ timing_input: "입력",
618
+ timing_transcription: "전사",
619
+ timing_highlight_detection: "하이라이트 감지",
620
+ timing_multimodal_analysis: "멀티모달 분석",
621
+ timing_clip_generation: "클립 생성",
622
+ timing_total: "합계",
623
  transcript: "전사",
624
  transcriptEmpty: "전사가 끝나면 여기에 표시됩니다.",
625
  clips: "클립",
 
639
  subtitles: "자막",
640
  subtitleCues: "자막 큐",
641
  subtitleCueHelp: "짧은 자막 큐가 TikTok, Reels, Shorts에서 더 읽기 쉽습니다.",
642
+ editorTools: "편집 도구",
643
+ rangeStart: "시작 위치",
644
+ rangeEnd: "종료 위치",
645
+ trimStartBack: "시작 -0.5초",
646
+ trimStartForward: "시작 +0.5초",
647
+ trimEndBack: "종료 -0.5초",
648
+ trimEndForward: "종료 +0.5초",
649
+ moveClipLeft: "클립 -1초",
650
+ moveClipRight: "클립 +1초",
651
+ setClipLength30: "30초 컷",
652
+ setClipLength60: "60초 컷",
653
+ setClipLength90: "90초 컷",
654
  inspector: "검사 패널",
655
  title: "제목",
656
  status: "상태",
657
  notApproved: "미승인",
658
  model: "모델",
659
  source: "소스",
660
+ source_upload: "업로드",
661
+ source_youtube: "YouTube",
662
+ source_video: "비디오",
663
  settings: "설정",
664
  stepInput: "입력",
665
  stepTranscription: "전사",
 
1136
  <div className="timing-grid">
1137
  {Object.entries(job.timings).map(([name, value]) => (
1138
  <div key={name}>
1139
+ <span>{t(`timing_${name}`)}</span>
1140
  <strong>{value}s</strong>
1141
  </div>
1142
  ))}
 
1282
  const duration = Math.max(1, clip.end_seconds - clip.start_seconds);
1283
  const cues = getSubtitleCues(clip, duration);
1284
  const metadataModel = clip.metadata?.model || "unknown";
1285
+ const sourceKind = job?.source?.kind || "video";
1286
+ const timelineDuration = Math.max(
1287
+ clip.end_seconds,
1288
+ ...(job?.transcript || []).map((segment) => Number(segment.end_seconds || 0)),
1289
+ 1
1290
+ );
1291
+ const rangeLeft = clamp((clip.start_seconds / timelineDuration) * 100, 0, 100);
1292
+ const rangeWidth = clamp(((clip.end_seconds - clip.start_seconds) / timelineDuration) * 100, 1, 100);
1293
 
1294
  function patchCue(index, text) {
1295
  const next = cues.map((cue, cueIndex) => (cueIndex === index ? { ...cue, text } : cue));
1296
  onPatch(clip.id, { subtitle_text: next.map((cue) => cue.text).join(" ") });
1297
  }
1298
 
1299
+ function updateStart(value) {
1300
+ const start = clamp(Number(value), 0, Math.max(0, clip.end_seconds - 1));
1301
+ onPatch(clip.id, { start_seconds: roundTime(start) });
1302
+ }
1303
+
1304
+ function updateEnd(value) {
1305
+ const end = clamp(Number(value), clip.start_seconds + 1, timelineDuration);
1306
+ onPatch(clip.id, { end_seconds: roundTime(end) });
1307
+ }
1308
+
1309
+ function moveClip(delta) {
1310
+ const safeDelta = clamp(delta, -clip.start_seconds, timelineDuration - clip.end_seconds);
1311
+ onPatch(clip.id, {
1312
+ start_seconds: roundTime(clip.start_seconds + safeDelta),
1313
+ end_seconds: roundTime(clip.end_seconds + safeDelta),
1314
+ });
1315
+ }
1316
+
1317
+ function setClipLength(seconds) {
1318
+ onPatch(clip.id, { end_seconds: roundTime(clamp(clip.start_seconds + seconds, clip.start_seconds + 1, timelineDuration)) });
1319
+ }
1320
+
1321
  return (
1322
  <div className="editor-shell">
1323
  <div className="editor-topbar">
 
1347
  </div>
1348
 
1349
  <div className="range-editor">
1350
+ <div className="panel-heading compact">
1351
+ <div>
1352
+ <h2>{t("clipRange")}</h2>
1353
+ <p>
1354
+ {formatTime(clip.start_seconds)} - {formatTime(clip.end_seconds)}
1355
+ </p>
1356
+ </div>
1357
+ <Scissors size={18} />
1358
+ </div>
1359
+ <div className="editor-toolbox">
1360
+ <span>{t("editorTools")}</span>
1361
+ <button type="button" onClick={() => updateStart(clip.start_seconds - 0.5)}>
1362
+ {t("trimStartBack")}
1363
+ </button>
1364
+ <button type="button" onClick={() => updateStart(clip.start_seconds + 0.5)}>
1365
+ {t("trimStartForward")}
1366
+ </button>
1367
+ <button type="button" onClick={() => updateEnd(clip.end_seconds - 0.5)}>
1368
+ {t("trimEndBack")}
1369
+ </button>
1370
+ <button type="button" onClick={() => updateEnd(clip.end_seconds + 0.5)}>
1371
+ {t("trimEndForward")}
1372
+ </button>
1373
+ <button type="button" onClick={() => moveClip(-1)}>
1374
+ {t("moveClipLeft")}
1375
+ </button>
1376
+ <button type="button" onClick={() => moveClip(1)}>
1377
+ {t("moveClipRight")}
1378
+ </button>
1379
+ <button type="button" onClick={() => setClipLength(30)}>
1380
+ {t("setClipLength30")}
1381
+ </button>
1382
+ <button type="button" onClick={() => setClipLength(60)}>
1383
+ {t("setClipLength60")}
1384
+ </button>
1385
+ <button type="button" onClick={() => setClipLength(90)}>
1386
+ {t("setClipLength90")}
1387
+ </button>
1388
+ </div>
1389
  <div className="timeline-visual">
1390
  <div className="timeline-fill" />
1391
+ <div className="timeline-window" style={{ left: `${rangeLeft}%`, width: `${rangeWidth}%` }} />
1392
  {Array.from({ length: 9 }).map((_, index) => (
1393
  <span key={index} style={{ left: `${index * 12.5}%` }} />
1394
  ))}
1395
  </div>
1396
+ <div className="range-sliders">
1397
+ <label>
1398
+ <span>{t("rangeStart")}</span>
1399
+ <input
1400
+ type="range"
1401
+ min="0"
1402
+ max={timelineDuration}
1403
+ step="0.5"
1404
+ value={clip.start_seconds}
1405
+ onChange={(event) => updateStart(event.target.value)}
1406
+ />
1407
+ </label>
1408
+ <label>
1409
+ <span>{t("rangeEnd")}</span>
1410
+ <input
1411
+ type="range"
1412
+ min="1"
1413
+ max={timelineDuration}
1414
+ step="0.5"
1415
+ value={clip.end_seconds}
1416
+ onChange={(event) => updateEnd(event.target.value)}
1417
+ />
1418
+ </label>
1419
+ </div>
1420
  <div className="timeline">
1421
  <NumberField
1422
  label={t("start")}
1423
  value={Number(clip.start_seconds).toFixed(1)}
1424
+ onChange={updateStart}
1425
  />
1426
  <NumberField
1427
  label={t("end")}
1428
  value={Number(clip.end_seconds).toFixed(1)}
1429
+ onChange={updateEnd}
1430
  />
1431
  <strong>{duration.toFixed(1)}s</strong>
1432
  </div>
 
1489
  </div>
1490
  <div>
1491
  <dt>{t("source")}</dt>
1492
+ <dd>{t(`source_${sourceKind}`)}</dd>
1493
  </div>
1494
  </dl>
1495
 
 
1666
  return Math.round(value * 10) / 10;
1667
  }
1668
 
1669
+ function clamp(value, min, max) {
1670
+ return Math.min(Math.max(value, min), max);
1671
+ }
1672
+
1673
  function formatTime(value) {
1674
  const safeValue = Number.isFinite(Number(value)) ? Number(value) : 0;
1675
  const minutes = Math.floor(safeValue / 60);
frontend/src/styles.css CHANGED
@@ -218,15 +218,30 @@ button:disabled {
218
 
219
  .toolbar-select {
220
  padding: 0 8px;
 
221
  }
222
 
223
  .toolbar-select select {
 
224
  border: 0;
225
- background: transparent;
226
  color: var(--text);
 
227
  outline: none;
228
  }
229
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  .icon-button {
231
  width: 36px;
232
  padding: 0;
@@ -234,8 +249,9 @@ button:disabled {
234
 
235
  .workspace-grid {
236
  display: grid;
237
- grid-template-columns: minmax(300px, 360px) minmax(360px, 1fr) minmax(340px, 460px);
238
  gap: 18px;
 
239
  padding: 20px clamp(16px, 4vw, 44px) 44px;
240
  }
241
 
@@ -266,6 +282,12 @@ button:disabled {
266
  .input-panel {
267
  position: sticky;
268
  top: 96px;
 
 
 
 
 
 
269
  }
270
 
271
  .panel-heading {
@@ -533,7 +555,9 @@ textarea:focus,
533
 
534
  .clip-grid {
535
  display: grid;
 
536
  gap: 14px;
 
537
  }
538
 
539
  .clip-card {
@@ -546,7 +570,7 @@ textarea:focus,
546
  .clip-video {
547
  display: grid;
548
  aspect-ratio: 9 / 16;
549
- max-height: 390px;
550
  place-items: center;
551
  background: #050b16;
552
  color: white;
@@ -722,6 +746,26 @@ textarea:focus,
722
  border: 2px solid var(--primary);
723
  border-radius: 999px;
724
  background: color-mix(in srgb, var(--primary) 24%, transparent);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
725
  }
726
 
727
  .timeline-visual span {
@@ -739,6 +783,54 @@ textarea:focus,
739
  gap: 10px;
740
  }
741
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
742
  .timeline label {
743
  display: grid;
744
  gap: 5px;
@@ -868,6 +960,7 @@ textarea:focus,
868
 
869
  .input-panel {
870
  position: static;
 
871
  }
872
 
873
  .clip-grid {
@@ -885,6 +978,7 @@ textarea:focus,
885
 
886
  @media (max-width: 620px) {
887
  .form-grid-two,
 
888
  .timeline,
889
  .cue-row,
890
  .transcript-row {
 
218
 
219
  .toolbar-select {
220
  padding: 0 8px;
221
+ background: var(--surface);
222
  }
223
 
224
  .toolbar-select select {
225
+ min-width: 52px;
226
  border: 0;
227
+ background: var(--surface);
228
  color: var(--text);
229
+ -webkit-text-fill-color: var(--text);
230
  outline: none;
231
  }
232
 
233
+ .toolbar-select select option,
234
+ .text-input option {
235
+ background: #ffffff;
236
+ color: #111827;
237
+ }
238
+
239
+ :root[data-theme="dark"] .toolbar-select select option,
240
+ :root[data-theme="dark"] .text-input option {
241
+ background: #111827;
242
+ color: #f8fafc;
243
+ }
244
+
245
  .icon-button {
246
  width: 36px;
247
  padding: 0;
 
249
 
250
  .workspace-grid {
251
  display: grid;
252
+ grid-template-columns: minmax(320px, 400px) minmax(0, 1fr);
253
  gap: 18px;
254
+ align-items: start;
255
  padding: 20px clamp(16px, 4vw, 44px) 44px;
256
  }
257
 
 
282
  .input-panel {
283
  position: sticky;
284
  top: 96px;
285
+ max-height: calc(100vh - 120px);
286
+ overflow: auto;
287
+ }
288
+
289
+ .results-column {
290
+ grid-column: 1 / -1;
291
  }
292
 
293
  .panel-heading {
 
555
 
556
  .clip-grid {
557
  display: grid;
558
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
559
  gap: 14px;
560
+ align-items: start;
561
  }
562
 
563
  .clip-card {
 
570
  .clip-video {
571
  display: grid;
572
  aspect-ratio: 9 / 16;
573
+ max-height: 340px;
574
  place-items: center;
575
  background: #050b16;
576
  color: white;
 
746
  border: 2px solid var(--primary);
747
  border-radius: 999px;
748
  background: color-mix(in srgb, var(--primary) 24%, transparent);
749
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--primary) 35%, transparent);
750
+ }
751
+
752
+ .timeline-window::before,
753
+ .timeline-window::after {
754
+ position: absolute;
755
+ top: 4px;
756
+ width: 2px;
757
+ height: 14px;
758
+ border-radius: 999px;
759
+ background: var(--primary-strong);
760
+ content: "";
761
+ }
762
+
763
+ .timeline-window::before {
764
+ left: 8px;
765
+ }
766
+
767
+ .timeline-window::after {
768
+ right: 8px;
769
  }
770
 
771
  .timeline-visual span {
 
783
  gap: 10px;
784
  }
785
 
786
+ .editor-toolbox {
787
+ display: flex;
788
+ flex-wrap: wrap;
789
+ gap: 8px;
790
+ align-items: center;
791
+ }
792
+
793
+ .editor-toolbox span {
794
+ margin-right: 2px;
795
+ color: var(--text);
796
+ font-size: 0.82rem;
797
+ font-weight: 850;
798
+ }
799
+
800
+ .editor-toolbox button {
801
+ min-height: 34px;
802
+ border: 1px solid var(--border);
803
+ border-radius: 7px;
804
+ background: var(--surface);
805
+ color: var(--text);
806
+ padding: 0 10px;
807
+ font-size: 0.82rem;
808
+ font-weight: 750;
809
+ }
810
+
811
+ .editor-toolbox button:hover {
812
+ border-color: var(--primary);
813
+ }
814
+
815
+ .range-sliders {
816
+ display: grid;
817
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
818
+ gap: 12px;
819
+ }
820
+
821
+ .range-sliders label {
822
+ display: grid;
823
+ gap: 6px;
824
+ color: var(--text-muted);
825
+ font-size: 0.76rem;
826
+ font-weight: 800;
827
+ }
828
+
829
+ .range-sliders input[type="range"] {
830
+ width: 100%;
831
+ accent-color: var(--primary);
832
+ }
833
+
834
  .timeline label {
835
  display: grid;
836
  gap: 5px;
 
960
 
961
  .input-panel {
962
  position: static;
963
+ max-height: none;
964
  }
965
 
966
  .clip-grid {
 
978
 
979
  @media (max-width: 620px) {
980
  .form-grid-two,
981
+ .range-sliders,
982
  .timeline,
983
  .cue-row,
984
  .transcript-row {