@plasius/video 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1024 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ import { aiVideoStageFlow } from "./stages.js";
3
+ import { aiVideoGenerationTokens } from "./tokens.js";
4
+ import type { AIVideoGenerationScreenModel, AIVideoImageVariant, AIVideoGenerationStage } from "./types.js";
5
+
6
+ const stageLabel: Record<AIVideoGenerationStage, string> = {
7
+ idle: "Prompt",
8
+ generatingImages: "Images",
9
+ imageSelection: "Selection",
10
+ generatingVideo: "Video",
11
+ playback: "Playback",
12
+ voiceover: "Voiceover",
13
+ export: "Export",
14
+ };
15
+
16
+ function clampProgress(value: number | undefined): number {
17
+ if (typeof value !== "number" || Number.isNaN(value)) {
18
+ return 0;
19
+ }
20
+
21
+ if (value < 0) {
22
+ return 0;
23
+ }
24
+
25
+ if (value > 100) {
26
+ return 100;
27
+ }
28
+
29
+ return Math.round(value);
30
+ }
31
+
32
+ function gradientForCard(index: number): string {
33
+ const gradients = [
34
+ "linear-gradient(140deg, #20305a 0%, #6c5ce7 45%, #00d4ff 100%)",
35
+ "linear-gradient(140deg, #2b1f3f 0%, #6c5ce7 48%, #3dd9f5 100%)",
36
+ "linear-gradient(140deg, #283645 0%, #4f89c2 46%, #9be3ff 100%)",
37
+ "linear-gradient(140deg, #2f2921 0%, #8a5f3a 48%, #ffd39f 100%)",
38
+ ];
39
+
40
+ return gradients[index % gradients.length];
41
+ }
42
+
43
+ export interface AIVideoGenerationScreenCallbacks {
44
+ onHistory?: () => void;
45
+ onSettings?: () => void;
46
+ onExport?: () => void;
47
+ onAccount?: () => void;
48
+ onGenerate?: () => void;
49
+ onUploadImage?: () => void;
50
+ onAdvanced?: () => void;
51
+ onSelectImage?: (variant: AIVideoImageVariant) => void;
52
+ onRefineImage?: (variant: AIVideoImageVariant) => void;
53
+ onSaveImage?: (variant: AIVideoImageVariant) => void;
54
+ onUseForVideo?: (variant: AIVideoImageVariant) => void;
55
+ onAddVoiceover?: () => void;
56
+ onRegenerateVideo?: () => void;
57
+ onDownloadVideo?: () => void;
58
+ }
59
+
60
+ export interface AIVideoGenerationScreenProps extends AIVideoGenerationScreenCallbacks {
61
+ model: AIVideoGenerationScreenModel;
62
+ className?: string;
63
+ style?: CSSProperties;
64
+ showContextPanel?: boolean;
65
+ reduceMotion?: boolean;
66
+ }
67
+
68
+ function renderImageCard(
69
+ variant: AIVideoImageVariant,
70
+ index: number,
71
+ callbacks: AIVideoGenerationScreenCallbacks,
72
+ ): ReactNode {
73
+ const isSelected = Boolean(variant.isSelected);
74
+
75
+ return (
76
+ <article
77
+ className={`plv-image-card${isSelected ? " is-selected" : ""}`}
78
+ key={variant.id}
79
+ aria-label={variant.alt ?? variant.label}
80
+ >
81
+ <button
82
+ type="button"
83
+ className="plv-image-select-hitbox"
84
+ aria-pressed={isSelected}
85
+ onClick={() => callbacks.onSelectImage?.(variant)}
86
+ >
87
+ {variant.src ? (
88
+ <img src={variant.src} alt={variant.alt ?? variant.label} className="plv-image-media" />
89
+ ) : (
90
+ <div className="plv-image-media" style={{ background: gradientForCard(index) }} />
91
+ )}
92
+ <div className="plv-image-caption">
93
+ <span>{variant.label}</span>
94
+ {isSelected ? <span className="plv-chip">Selected</span> : null}
95
+ </div>
96
+ </button>
97
+
98
+ <div className="plv-image-overlay-controls" role="group" aria-label={`${variant.label} controls`}>
99
+ <button type="button" onClick={() => callbacks.onRefineImage?.(variant)}>
100
+ Refine
101
+ </button>
102
+ <button type="button" onClick={() => callbacks.onSaveImage?.(variant)}>
103
+ Save
104
+ </button>
105
+ <button type="button" onClick={() => callbacks.onUseForVideo?.(variant)}>
106
+ Use for Video
107
+ </button>
108
+ </div>
109
+
110
+ {isSelected ? (
111
+ <span className="plv-selected-checkmark" aria-hidden="true">
112
+
113
+ </span>
114
+ ) : null}
115
+ </article>
116
+ );
117
+ }
118
+
119
+ function renderSkeletonGrid(count: number): ReactNode {
120
+ const items = Array.from({ length: count }).map((_, index) => (
121
+ <div className="plv-skeleton-card" key={`skeleton-${index}`} />
122
+ ));
123
+
124
+ return <div className="plv-image-grid">{items}</div>;
125
+ }
126
+
127
+ function renderStageCanvas(
128
+ model: AIVideoGenerationScreenModel,
129
+ callbacks: AIVideoGenerationScreenCallbacks,
130
+ ): ReactNode {
131
+ const progress = clampProgress(model.generationProgress);
132
+
133
+ if (model.stage === "generatingImages") {
134
+ return (
135
+ <>
136
+ <h2>Generating Image Variants</h2>
137
+ <p className="plv-muted">{model.statusText}</p>
138
+ {renderSkeletonGrid(model.imageVariants.length > 0 ? model.imageVariants.length : 8)}
139
+ </>
140
+ );
141
+ }
142
+
143
+ if (model.stage === "imageSelection") {
144
+ return (
145
+ <>
146
+ <h2>Course Setting Image Grid</h2>
147
+ <p className="plv-muted">Select a visual anchor before video generation.</p>
148
+ <div className="plv-image-grid">
149
+ {model.imageVariants.map((variant, index) => renderImageCard(variant, index, callbacks))}
150
+ </div>
151
+ </>
152
+ );
153
+ }
154
+
155
+ if (model.stage === "generatingVideo") {
156
+ return (
157
+ <>
158
+ <h2>Video Generation Phase</h2>
159
+ <div className="plv-split-panel">
160
+ <div className="plv-preview-box">
161
+ <div className="plv-preview-title">Image Preview</div>
162
+ <div className="plv-preview-media" />
163
+ </div>
164
+ <div className="plv-motion-panel">
165
+ <div className="plv-preview-title">Motion Summary</div>
166
+ <label>
167
+ Camera motion
168
+ <textarea readOnly value={model.motionDraft.cameraMotion} />
169
+ </label>
170
+ <label>
171
+ Environmental motion
172
+ <textarea readOnly value={model.motionDraft.environmentalMotion} />
173
+ </label>
174
+ <label>
175
+ Subject motion
176
+ <textarea readOnly value={model.motionDraft.subjectMotion} />
177
+ </label>
178
+ </div>
179
+ </div>
180
+ <div className="plv-progress-panel">
181
+ <div className="plv-progress-topline">
182
+ <span>Generating Video...</span>
183
+ <span>{progress}%</span>
184
+ </div>
185
+ <div className="plv-progress-track" role="progressbar" aria-valuemin={0} aria-valuemax={100} aria-valuenow={progress}>
186
+ <div className="plv-progress-fill" style={{ width: `${progress}%` }} />
187
+ </div>
188
+ <div className="plv-waveform" aria-hidden="true" />
189
+ </div>
190
+ </>
191
+ );
192
+ }
193
+
194
+ if (model.stage === "playback" || model.stage === "voiceover" || model.stage === "export") {
195
+ return (
196
+ <>
197
+ <h2>Video Playback</h2>
198
+ <div className="plv-player-shell">
199
+ <div className="plv-player-frame">
200
+ <div className="plv-player-glow" />
201
+ <div className="plv-player-screen">16:9 Preview Frame</div>
202
+ </div>
203
+ <div className="plv-player-controls">
204
+ <button type="button">Play / Pause</button>
205
+ <button type="button">Timeline</button>
206
+ <button type="button">Volume</button>
207
+ <button type="button" onClick={callbacks.onDownloadVideo}>
208
+ Download
209
+ </button>
210
+ <button type="button" onClick={callbacks.onRegenerateVideo}>
211
+ Regenerate
212
+ </button>
213
+ <button type="button" onClick={callbacks.onAddVoiceover}>
214
+ Add Voiceover
215
+ </button>
216
+ </div>
217
+ </div>
218
+
219
+ {model.stage === "voiceover" ? (
220
+ <section className="plv-voiceover-panel" aria-label="Voiceover panel">
221
+ <div className="plv-preview-title">Extracted Speech</div>
222
+ <textarea readOnly value={model.voiceSettings.script} />
223
+ <div className="plv-voice-grid">
224
+ <div>
225
+ <div className="plv-field-label">Voice</div>
226
+ <div className="plv-pill-group">
227
+ {model.voicePresets.map((voice) => (
228
+ <button
229
+ type="button"
230
+ key={voice.id}
231
+ className={voice.id === model.voiceSettings.voiceId ? "is-active" : undefined}
232
+ >
233
+ {voice.label}
234
+ </button>
235
+ ))}
236
+ </div>
237
+ </div>
238
+ <div>
239
+ <div className="plv-field-label">Speed: {model.voiceSettings.speed.toFixed(1)}x</div>
240
+ <div className="plv-slider-track" />
241
+ </div>
242
+ <div>
243
+ <div className="plv-field-label">Emotion: {model.voiceSettings.emotion}</div>
244
+ <div className="plv-slider-track" />
245
+ </div>
246
+ </div>
247
+ <div className="plv-waveform is-voiceover" aria-hidden="true" />
248
+ </section>
249
+ ) : null}
250
+
251
+ {model.stage === "export" ? (
252
+ <div className="plv-export-modal" role="dialog" aria-label="Export modal">
253
+ <h3>Export</h3>
254
+ <p>Finalize codec, quality, and voice mixdown profile.</p>
255
+ <button type="button" onClick={callbacks.onExport}>
256
+ Confirm Export
257
+ </button>
258
+ </div>
259
+ ) : null}
260
+ </>
261
+ );
262
+ }
263
+
264
+ return (
265
+ <>
266
+ <h2>Prompt Entry</h2>
267
+ <p className="plv-muted">Start with a cinematic prompt, then generate course-setting images.</p>
268
+ <div className="plv-idle-canvas">
269
+ {model.uploadedImageName ? (
270
+ <>
271
+ <div className="plv-upload-chip">Upload Image: {model.uploadedImageName}</div>
272
+ <div className="plv-required-chip">Add Motion Instructions (Required)</div>
273
+ </>
274
+ ) : (
275
+ <div className="plv-upload-chip">Upload an image or write a prompt to begin.</div>
276
+ )}
277
+ </div>
278
+ </>
279
+ );
280
+ }
281
+
282
+ export function AIVideoGenerationScreen({
283
+ model,
284
+ className,
285
+ style,
286
+ showContextPanel = true,
287
+ reduceMotion = false,
288
+ ...callbacks
289
+ }: AIVideoGenerationScreenProps) {
290
+ const rootClassName = [
291
+ "plv-video-screen",
292
+ reduceMotion ? "plv-reduce-motion" : "",
293
+ className ?? "",
294
+ ]
295
+ .filter(Boolean)
296
+ .join(" ");
297
+
298
+ const requiresMotionPrompt = Boolean(model.uploadedImageName) && !model.motionPrompt?.trim();
299
+
300
+ return (
301
+ <section className={rootClassName} style={style}>
302
+ <style>{AI_VIDEO_GENERATION_SCREEN_STYLES}</style>
303
+
304
+ <header className="plv-header">
305
+ <div className="plv-logo">PLASIUS</div>
306
+ <div className="plv-project-title">{model.projectName}</div>
307
+ <div className="plv-header-actions">
308
+ <button type="button" onClick={callbacks.onHistory}>
309
+ History
310
+ </button>
311
+ <button type="button" onClick={callbacks.onSettings}>
312
+ Settings
313
+ </button>
314
+ <button type="button" onClick={callbacks.onExport}>
315
+ Export
316
+ </button>
317
+ <button type="button" onClick={callbacks.onAccount}>
318
+ Account
319
+ </button>
320
+ </div>
321
+ </header>
322
+
323
+ <main className="plv-main-canvas">{renderStageCanvas(model, callbacks)}</main>
324
+
325
+ {showContextPanel ? (
326
+ <aside className="plv-context-panel">
327
+ <div className="plv-context-title">Context Panel</div>
328
+ <div className="plv-chip-row" role="list" aria-label="Prompt versions">
329
+ {model.promptVersions.map((version) => (
330
+ <button
331
+ type="button"
332
+ key={version.id}
333
+ role="listitem"
334
+ className={version.isActive ? "is-active" : undefined}
335
+ >
336
+ {version.label}
337
+ </button>
338
+ ))}
339
+ </div>
340
+ <div className="plv-metadata-grid">
341
+ <div>
342
+ <span>Stage</span>
343
+ <strong>{stageLabel[model.stage]}</strong>
344
+ </div>
345
+ <div>
346
+ <span>Status</span>
347
+ <strong>{model.statusText}</strong>
348
+ </div>
349
+ <div>
350
+ <span>Prompt</span>
351
+ <strong>{model.prompt}</strong>
352
+ </div>
353
+ </div>
354
+ <div className="plv-stage-row">
355
+ {aiVideoStageFlow.map((stage) => (
356
+ <span key={stage.stage} className={stage.stage === model.stage ? "is-active" : undefined}>
357
+ {stage.label}
358
+ </span>
359
+ ))}
360
+ </div>
361
+ </aside>
362
+ ) : null}
363
+
364
+ <footer className="plv-prompt-area">
365
+ <label className="plv-sr-only" htmlFor="plv-prompt-textarea">
366
+ Prompt input
367
+ </label>
368
+ <textarea
369
+ id="plv-prompt-textarea"
370
+ value={model.prompt}
371
+ readOnly
372
+ placeholder={model.promptPlaceholder}
373
+ aria-label="Prompt input"
374
+ />
375
+
376
+ <div className="plv-prompt-actions">
377
+ <button type="button" onClick={callbacks.onUploadImage}>
378
+ Upload Image
379
+ </button>
380
+ <button type="button" onClick={callbacks.onAdvanced}>
381
+ Advanced
382
+ </button>
383
+ <button type="button" className="is-generate" disabled={!model.canGenerate} onClick={callbacks.onGenerate}>
384
+ Generate
385
+ </button>
386
+ </div>
387
+
388
+ {requiresMotionPrompt ? (
389
+ <div className="plv-required-chip">Upload Image complete. Motion prompt is required before generating.</div>
390
+ ) : null}
391
+ </footer>
392
+ </section>
393
+ );
394
+ }
395
+
396
+ export const AI_VIDEO_GENERATION_SCREEN_STYLES = `
397
+ .plv-video-screen {
398
+ --plv-background: ${aiVideoGenerationTokens.color.background};
399
+ --plv-surface: ${aiVideoGenerationTokens.color.surface};
400
+ --plv-border: ${aiVideoGenerationTokens.color.borderSubtle};
401
+ --plv-text: ${aiVideoGenerationTokens.color.textPrimary};
402
+ --plv-text-muted: ${aiVideoGenerationTokens.color.textSecondary};
403
+ --plv-accent-primary: ${aiVideoGenerationTokens.color.accentPrimary};
404
+ --plv-accent-secondary: ${aiVideoGenerationTokens.color.accentSecondary};
405
+ --plv-success: ${aiVideoGenerationTokens.color.success};
406
+ --plv-warning: ${aiVideoGenerationTokens.color.warning};
407
+ --plv-error: ${aiVideoGenerationTokens.color.error};
408
+ --plv-prompt-placeholder: ${aiVideoGenerationTokens.color.placeholderText};
409
+ --plv-header-height: ${aiVideoGenerationTokens.layout.headerHeightPx}px;
410
+ --plv-transition: ${aiVideoGenerationTokens.animation.standardMs}ms ${aiVideoGenerationTokens.animation.easing};
411
+ font-family: ${aiVideoGenerationTokens.typography.bodyFontFamily};
412
+ color: var(--plv-text);
413
+ background:
414
+ radial-gradient(1200px 520px at 20% -10%, rgba(108, 92, 231, 0.22), transparent 62%),
415
+ radial-gradient(1000px 540px at 80% -20%, rgba(0, 212, 255, 0.16), transparent 60%),
416
+ var(--plv-background);
417
+ border-radius: ${aiVideoGenerationTokens.radius.panelPx}px;
418
+ border: 1px solid var(--plv-border);
419
+ overflow: hidden;
420
+ display: grid;
421
+ grid-template-rows: var(--plv-header-height) minmax(360px, 1fr) auto auto;
422
+ min-height: 820px;
423
+ }
424
+
425
+ .plv-header {
426
+ display: flex;
427
+ align-items: center;
428
+ gap: 14px;
429
+ padding: 0 16px;
430
+ border-bottom: 1px solid var(--plv-border);
431
+ background: #0f1117;
432
+ }
433
+
434
+ .plv-logo {
435
+ font-weight: 800;
436
+ letter-spacing: 0.08em;
437
+ }
438
+
439
+ .plv-project-title {
440
+ font-size: 15px;
441
+ color: var(--plv-text-muted);
442
+ }
443
+
444
+ .plv-header-actions {
445
+ margin-left: auto;
446
+ display: flex;
447
+ gap: 8px;
448
+ }
449
+
450
+ .plv-header-actions button,
451
+ .plv-player-controls button,
452
+ .plv-prompt-actions button,
453
+ .plv-chip-row button,
454
+ .plv-pill-group button,
455
+ .plv-image-overlay-controls button {
456
+ border: 1px solid var(--plv-border);
457
+ background: rgba(255, 255, 255, 0.02);
458
+ color: var(--plv-text);
459
+ border-radius: 10px;
460
+ padding: 8px 12px;
461
+ font-size: 13px;
462
+ transition: all var(--plv-transition);
463
+ }
464
+
465
+ .plv-header-actions button:hover,
466
+ .plv-player-controls button:hover,
467
+ .plv-prompt-actions button:hover,
468
+ .plv-chip-row button:hover,
469
+ .plv-pill-group button:hover,
470
+ .plv-image-overlay-controls button:hover {
471
+ border-color: rgba(0, 212, 255, 0.45);
472
+ box-shadow: 0 0 0 1px rgba(0, 212, 255, 0.22);
473
+ }
474
+
475
+ .plv-main-canvas {
476
+ padding: 18px;
477
+ }
478
+
479
+ .plv-main-canvas h2 {
480
+ margin: 0 0 8px;
481
+ font-family: ${aiVideoGenerationTokens.typography.headingFontFamily};
482
+ font-size: ${aiVideoGenerationTokens.typography.h2Px}px;
483
+ }
484
+
485
+ .plv-muted {
486
+ margin: 0 0 14px;
487
+ color: var(--plv-text-muted);
488
+ font-size: ${aiVideoGenerationTokens.typography.smallPx}px;
489
+ }
490
+
491
+ .plv-image-grid {
492
+ display: grid;
493
+ gap: ${aiVideoGenerationTokens.layout.gridGapPx}px;
494
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
495
+ }
496
+
497
+ .plv-image-card,
498
+ .plv-skeleton-card {
499
+ aspect-ratio: ${aiVideoGenerationTokens.layout.cardAspectRatio};
500
+ border-radius: ${aiVideoGenerationTokens.radius.cardPx}px;
501
+ position: relative;
502
+ overflow: hidden;
503
+ }
504
+
505
+ .plv-image-card {
506
+ border: 1px solid var(--plv-border);
507
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
508
+ transition: transform var(--plv-transition), box-shadow var(--plv-transition), border-color var(--plv-transition);
509
+ }
510
+
511
+ .plv-image-card:hover {
512
+ transform: scale(1.02);
513
+ border-color: rgba(0, 212, 255, 0.55);
514
+ box-shadow: 0 14px 36px rgba(0, 0, 0, 0.48);
515
+ }
516
+
517
+ .plv-image-card.is-selected {
518
+ border: 2px solid var(--plv-accent-secondary);
519
+ box-shadow: 0 0 0 1px rgba(108, 92, 231, 0.35), 0 16px 32px rgba(0, 0, 0, 0.55);
520
+ }
521
+
522
+ .plv-image-select-hitbox {
523
+ border: 0;
524
+ margin: 0;
525
+ width: 100%;
526
+ height: 100%;
527
+ padding: 0;
528
+ background: none;
529
+ display: flex;
530
+ flex-direction: column;
531
+ justify-content: flex-end;
532
+ cursor: pointer;
533
+ }
534
+
535
+ .plv-image-media {
536
+ position: absolute;
537
+ inset: 0;
538
+ }
539
+
540
+ .plv-image-caption {
541
+ z-index: 1;
542
+ display: flex;
543
+ justify-content: space-between;
544
+ align-items: center;
545
+ gap: 8px;
546
+ padding: 10px;
547
+ background: linear-gradient(180deg, transparent 0%, rgba(15, 17, 23, 0.84) 60%);
548
+ }
549
+
550
+ .plv-chip {
551
+ font-size: 11px;
552
+ border-radius: 999px;
553
+ padding: 2px 8px;
554
+ background: rgba(0, 212, 255, 0.14);
555
+ border: 1px solid rgba(0, 212, 255, 0.36);
556
+ }
557
+
558
+ .plv-selected-checkmark {
559
+ position: absolute;
560
+ top: 10px;
561
+ right: 10px;
562
+ width: 24px;
563
+ height: 24px;
564
+ display: grid;
565
+ place-items: center;
566
+ border-radius: 999px;
567
+ color: #00121a;
568
+ background: #7ff3ff;
569
+ font-weight: 900;
570
+ }
571
+
572
+ .plv-image-overlay-controls {
573
+ position: absolute;
574
+ top: 10px;
575
+ left: 10px;
576
+ display: flex;
577
+ gap: 8px;
578
+ opacity: 0;
579
+ transform: translateY(-6px);
580
+ transition: opacity var(--plv-transition), transform var(--plv-transition);
581
+ }
582
+
583
+ .plv-image-card:hover .plv-image-overlay-controls {
584
+ opacity: 1;
585
+ transform: translateY(0);
586
+ }
587
+
588
+ .plv-skeleton-card {
589
+ background: linear-gradient(
590
+ 100deg,
591
+ rgba(255, 255, 255, 0.03) 20%,
592
+ rgba(255, 255, 255, 0.14) 40%,
593
+ rgba(255, 255, 255, 0.03) 60%
594
+ );
595
+ background-size: 200% 100%;
596
+ animation: plv-shimmer 1.25s linear infinite;
597
+ }
598
+
599
+ .plv-split-panel {
600
+ display: grid;
601
+ grid-template-columns: 1.1fr 0.9fr;
602
+ gap: 16px;
603
+ }
604
+
605
+ .plv-preview-box,
606
+ .plv-motion-panel,
607
+ .plv-progress-panel,
608
+ .plv-player-shell,
609
+ .plv-voiceover-panel,
610
+ .plv-idle-canvas,
611
+ .plv-context-panel,
612
+ .plv-prompt-area {
613
+ background: var(--plv-surface);
614
+ border: 1px solid var(--plv-border);
615
+ border-radius: ${aiVideoGenerationTokens.radius.panelPx}px;
616
+ }
617
+
618
+ .plv-preview-box,
619
+ .plv-motion-panel {
620
+ padding: 14px;
621
+ }
622
+
623
+ .plv-preview-title {
624
+ font-size: ${aiVideoGenerationTokens.typography.smallPx}px;
625
+ text-transform: uppercase;
626
+ letter-spacing: 0.08em;
627
+ color: var(--plv-text-muted);
628
+ margin-bottom: 10px;
629
+ }
630
+
631
+ .plv-preview-media {
632
+ width: 100%;
633
+ aspect-ratio: ${aiVideoGenerationTokens.layout.cardAspectRatio};
634
+ border-radius: ${aiVideoGenerationTokens.radius.cardPx}px;
635
+ background: linear-gradient(135deg, #2d3e67 0%, #6c5ce7 45%, #00d4ff 100%);
636
+ }
637
+
638
+ .plv-motion-panel label {
639
+ display: block;
640
+ font-size: ${aiVideoGenerationTokens.typography.smallPx}px;
641
+ color: var(--plv-text-muted);
642
+ margin-bottom: 10px;
643
+ }
644
+
645
+ .plv-motion-panel textarea,
646
+ .plv-voiceover-panel textarea {
647
+ width: 100%;
648
+ margin-top: 6px;
649
+ border: 1px solid var(--plv-border);
650
+ border-radius: 10px;
651
+ min-height: 62px;
652
+ resize: none;
653
+ background: rgba(255, 255, 255, 0.02);
654
+ color: var(--plv-text);
655
+ padding: 10px;
656
+ font-family: ${aiVideoGenerationTokens.typography.bodyFontFamily};
657
+ }
658
+
659
+ .plv-progress-panel {
660
+ margin-top: 16px;
661
+ padding: 12px 14px;
662
+ }
663
+
664
+ .plv-progress-topline {
665
+ display: flex;
666
+ justify-content: space-between;
667
+ font-size: ${aiVideoGenerationTokens.typography.smallPx}px;
668
+ margin-bottom: 8px;
669
+ }
670
+
671
+ .plv-progress-track {
672
+ height: 8px;
673
+ border-radius: 999px;
674
+ background: rgba(255, 255, 255, 0.08);
675
+ overflow: hidden;
676
+ }
677
+
678
+ .plv-progress-fill {
679
+ height: 100%;
680
+ background: linear-gradient(135deg, var(--plv-accent-primary), var(--plv-accent-secondary));
681
+ transition: width var(--plv-transition);
682
+ }
683
+
684
+ .plv-waveform {
685
+ margin-top: 10px;
686
+ height: 32px;
687
+ border-radius: 10px;
688
+ background:
689
+ repeating-linear-gradient(
690
+ 90deg,
691
+ rgba(108, 92, 231, 0.24) 0,
692
+ rgba(108, 92, 231, 0.24) 4px,
693
+ rgba(0, 212, 255, 0.28) 4px,
694
+ rgba(0, 212, 255, 0.28) 8px
695
+ );
696
+ animation: plv-wave 1.2s linear infinite;
697
+ }
698
+
699
+ .plv-waveform.is-voiceover {
700
+ height: 46px;
701
+ }
702
+
703
+ .plv-player-shell {
704
+ padding: 14px;
705
+ }
706
+
707
+ .plv-player-frame {
708
+ position: relative;
709
+ aspect-ratio: ${aiVideoGenerationTokens.layout.cardAspectRatio};
710
+ background: #020202;
711
+ border-radius: ${aiVideoGenerationTokens.radius.cardPx}px;
712
+ overflow: hidden;
713
+ }
714
+
715
+ .plv-player-glow {
716
+ position: absolute;
717
+ inset: -20%;
718
+ background: radial-gradient(circle at center, rgba(0, 212, 255, 0.26), transparent 60%);
719
+ }
720
+
721
+ .plv-player-screen {
722
+ position: absolute;
723
+ inset: 0;
724
+ display: grid;
725
+ place-items: center;
726
+ color: rgba(230, 234, 242, 0.74);
727
+ font-size: 15px;
728
+ }
729
+
730
+ .plv-player-controls {
731
+ margin-top: 12px;
732
+ display: grid;
733
+ grid-template-columns: repeat(6, minmax(0, 1fr));
734
+ gap: 8px;
735
+ }
736
+
737
+ .plv-voiceover-panel {
738
+ margin-top: 14px;
739
+ padding: 14px;
740
+ animation: plv-slide-up var(--plv-transition);
741
+ }
742
+
743
+ .plv-voice-grid {
744
+ margin-top: 10px;
745
+ display: grid;
746
+ gap: 10px;
747
+ }
748
+
749
+ .plv-pill-group {
750
+ display: flex;
751
+ flex-wrap: wrap;
752
+ gap: 8px;
753
+ }
754
+
755
+ .plv-pill-group .is-active,
756
+ .plv-chip-row .is-active {
757
+ border-color: rgba(0, 212, 255, 0.6);
758
+ box-shadow: 0 0 0 1px rgba(0, 212, 255, 0.24);
759
+ }
760
+
761
+ .plv-field-label {
762
+ font-size: ${aiVideoGenerationTokens.typography.smallPx}px;
763
+ color: var(--plv-text-muted);
764
+ margin-bottom: 6px;
765
+ }
766
+
767
+ .plv-slider-track {
768
+ height: 8px;
769
+ border-radius: 999px;
770
+ background: linear-gradient(135deg, rgba(108, 92, 231, 0.7), rgba(0, 212, 255, 0.7));
771
+ }
772
+
773
+ .plv-export-modal {
774
+ margin-top: 14px;
775
+ padding: 14px;
776
+ border-radius: ${aiVideoGenerationTokens.radius.panelPx}px;
777
+ border: 1px solid rgba(255, 176, 32, 0.4);
778
+ background: rgba(255, 176, 32, 0.08);
779
+ }
780
+
781
+ .plv-export-modal h3 {
782
+ margin: 0 0 6px;
783
+ font-size: 18px;
784
+ }
785
+
786
+ .plv-export-modal p {
787
+ margin: 0 0 10px;
788
+ color: var(--plv-text-muted);
789
+ font-size: ${aiVideoGenerationTokens.typography.smallPx}px;
790
+ }
791
+
792
+ .plv-export-modal button {
793
+ border: 0;
794
+ border-radius: 10px;
795
+ background: linear-gradient(135deg, #ffb020, #ffd386);
796
+ color: #161a23;
797
+ font-weight: 700;
798
+ padding: 8px 12px;
799
+ }
800
+
801
+ .plv-idle-canvas {
802
+ min-height: 180px;
803
+ display: grid;
804
+ place-items: center;
805
+ gap: 10px;
806
+ padding: 16px;
807
+ }
808
+
809
+ .plv-upload-chip,
810
+ .plv-required-chip {
811
+ border-radius: 999px;
812
+ padding: 8px 12px;
813
+ font-size: ${aiVideoGenerationTokens.typography.smallPx}px;
814
+ }
815
+
816
+ .plv-upload-chip {
817
+ background: rgba(255, 255, 255, 0.04);
818
+ border: 1px solid var(--plv-border);
819
+ }
820
+
821
+ .plv-required-chip {
822
+ background: rgba(255, 176, 32, 0.12);
823
+ border: 1px solid rgba(255, 176, 32, 0.4);
824
+ color: #ffd18a;
825
+ }
826
+
827
+ .plv-context-panel {
828
+ margin: 0 18px 16px;
829
+ padding: 14px;
830
+ }
831
+
832
+ .plv-context-title {
833
+ margin-bottom: 10px;
834
+ font-size: ${aiVideoGenerationTokens.typography.smallPx}px;
835
+ text-transform: uppercase;
836
+ letter-spacing: 0.08em;
837
+ color: var(--plv-text-muted);
838
+ }
839
+
840
+ .plv-chip-row {
841
+ display: flex;
842
+ gap: 8px;
843
+ flex-wrap: wrap;
844
+ }
845
+
846
+ .plv-metadata-grid {
847
+ margin-top: 12px;
848
+ display: grid;
849
+ gap: 8px;
850
+ grid-template-columns: repeat(3, minmax(0, 1fr));
851
+ }
852
+
853
+ .plv-metadata-grid div {
854
+ display: grid;
855
+ gap: 4px;
856
+ }
857
+
858
+ .plv-metadata-grid span {
859
+ font-size: ${aiVideoGenerationTokens.typography.smallPx}px;
860
+ color: var(--plv-text-muted);
861
+ }
862
+
863
+ .plv-metadata-grid strong {
864
+ font-size: 14px;
865
+ line-height: 1.35;
866
+ }
867
+
868
+ .plv-stage-row {
869
+ margin-top: 10px;
870
+ display: flex;
871
+ gap: 8px;
872
+ flex-wrap: wrap;
873
+ }
874
+
875
+ .plv-stage-row span {
876
+ border-radius: 999px;
877
+ padding: 5px 10px;
878
+ border: 1px solid var(--plv-border);
879
+ color: var(--plv-text-muted);
880
+ font-size: 12px;
881
+ }
882
+
883
+ .plv-stage-row .is-active {
884
+ color: var(--plv-text);
885
+ border-color: rgba(0, 212, 255, 0.55);
886
+ }
887
+
888
+ .plv-prompt-area {
889
+ margin: 0 18px 18px;
890
+ padding: 14px;
891
+ }
892
+
893
+ .plv-prompt-area textarea {
894
+ width: 100%;
895
+ min-height: ${aiVideoGenerationTokens.layout.promptBarMinHeightPx}px;
896
+ border: 1px solid var(--plv-border);
897
+ border-radius: ${aiVideoGenerationTokens.radius.promptPx}px;
898
+ background: rgba(255, 255, 255, 0.02);
899
+ color: var(--plv-text);
900
+ font-family: ${aiVideoGenerationTokens.typography.bodyFontFamily};
901
+ font-size: 17px;
902
+ line-height: 1.45;
903
+ padding: 12px;
904
+ resize: vertical;
905
+ }
906
+
907
+ .plv-prompt-area textarea::placeholder {
908
+ color: var(--plv-prompt-placeholder);
909
+ }
910
+
911
+ .plv-prompt-actions {
912
+ margin-top: 10px;
913
+ display: flex;
914
+ gap: 8px;
915
+ align-items: center;
916
+ }
917
+
918
+ .plv-prompt-actions .is-generate {
919
+ margin-left: auto;
920
+ border: 0;
921
+ background: linear-gradient(135deg, #6c5ce7, #00d4ff);
922
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08), 0 12px 28px rgba(0, 212, 255, 0.22);
923
+ }
924
+
925
+ .plv-prompt-actions .is-generate:hover {
926
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08), 0 14px 34px rgba(0, 212, 255, 0.34);
927
+ }
928
+
929
+ .plv-prompt-actions .is-generate:disabled {
930
+ opacity: 0.4;
931
+ cursor: not-allowed;
932
+ }
933
+
934
+ .plv-sr-only {
935
+ position: absolute;
936
+ width: 1px;
937
+ height: 1px;
938
+ margin: -1px;
939
+ padding: 0;
940
+ overflow: hidden;
941
+ clip: rect(0, 0, 0, 0);
942
+ border: 0;
943
+ }
944
+
945
+ .plv-reduce-motion * {
946
+ animation: none !important;
947
+ transition: none !important;
948
+ }
949
+
950
+ @media (max-width: 1024px) {
951
+ .plv-video-screen {
952
+ min-height: 920px;
953
+ }
954
+
955
+ .plv-split-panel {
956
+ grid-template-columns: 1fr;
957
+ }
958
+
959
+ .plv-player-controls {
960
+ grid-template-columns: repeat(2, minmax(0, 1fr));
961
+ }
962
+
963
+ .plv-metadata-grid {
964
+ grid-template-columns: 1fr;
965
+ }
966
+ }
967
+
968
+ @media (max-width: 720px) {
969
+ .plv-header {
970
+ flex-wrap: wrap;
971
+ height: auto;
972
+ min-height: var(--plv-header-height);
973
+ padding-top: 10px;
974
+ padding-bottom: 10px;
975
+ }
976
+
977
+ .plv-header-actions {
978
+ width: 100%;
979
+ justify-content: space-between;
980
+ }
981
+
982
+ .plv-image-grid {
983
+ grid-template-columns: 1fr;
984
+ }
985
+
986
+ .plv-prompt-actions {
987
+ flex-wrap: wrap;
988
+ }
989
+
990
+ .plv-prompt-actions .is-generate {
991
+ margin-left: 0;
992
+ width: 100%;
993
+ }
994
+ }
995
+
996
+ @keyframes plv-shimmer {
997
+ from {
998
+ background-position: 120% 0;
999
+ }
1000
+ to {
1001
+ background-position: -120% 0;
1002
+ }
1003
+ }
1004
+
1005
+ @keyframes plv-wave {
1006
+ from {
1007
+ background-position: 0 0;
1008
+ }
1009
+ to {
1010
+ background-position: 64px 0;
1011
+ }
1012
+ }
1013
+
1014
+ @keyframes plv-slide-up {
1015
+ from {
1016
+ opacity: 0;
1017
+ transform: translateY(10px);
1018
+ }
1019
+ to {
1020
+ opacity: 1;
1021
+ transform: translateY(0);
1022
+ }
1023
+ }
1024
+ `;