videowright 0.1.0
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.
- package/README.md +91 -0
- package/dist/cli/argv.d.ts +28 -0
- package/dist/cli/argv.d.ts.map +1 -0
- package/dist/cli/argv.js +115 -0
- package/dist/cli/argv.js.map +1 -0
- package/dist/cli/bin.d.ts +7 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +10 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/dev.d.ts +19 -0
- package/dist/cli/dev.d.ts.map +1 -0
- package/dist/cli/dev.js +104 -0
- package/dist/cli/dev.js.map +1 -0
- package/dist/cli/discover.d.ts +29 -0
- package/dist/cli/discover.d.ts.map +1 -0
- package/dist/cli/discover.js +104 -0
- package/dist/cli/discover.js.map +1 -0
- package/dist/cli/discover_project.d.ts +29 -0
- package/dist/cli/discover_project.d.ts.map +1 -0
- package/dist/cli/discover_project.js +108 -0
- package/dist/cli/discover_project.js.map +1 -0
- package/dist/cli/errors.d.ts +10 -0
- package/dist/cli/errors.d.ts.map +1 -0
- package/dist/cli/errors.js +13 -0
- package/dist/cli/errors.js.map +1 -0
- package/dist/cli/ffmpeg.d.ts +57 -0
- package/dist/cli/ffmpeg.d.ts.map +1 -0
- package/dist/cli/ffmpeg.js +122 -0
- package/dist/cli/ffmpeg.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +152 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/playwright_check.d.ts +44 -0
- package/dist/cli/playwright_check.d.ts.map +1 -0
- package/dist/cli/playwright_check.js +20 -0
- package/dist/cli/playwright_check.js.map +1 -0
- package/dist/cli/prompt.d.ts +13 -0
- package/dist/cli/prompt.d.ts.map +1 -0
- package/dist/cli/prompt.js +47 -0
- package/dist/cli/prompt.js.map +1 -0
- package/dist/cli/render.d.ts +60 -0
- package/dist/cli/render.d.ts.map +1 -0
- package/dist/cli/render.js +471 -0
- package/dist/cli/render.js.map +1 -0
- package/dist/cli/script_cmd.d.ts +26 -0
- package/dist/cli/script_cmd.d.ts.map +1 -0
- package/dist/cli/script_cmd.js +88 -0
- package/dist/cli/script_cmd.js.map +1 -0
- package/dist/cli/time_shim.d.ts +44 -0
- package/dist/cli/time_shim.d.ts.map +1 -0
- package/dist/cli/time_shim.js +390 -0
- package/dist/cli/time_shim.js.map +1 -0
- package/dist/cli/ts_loader.d.ts +28 -0
- package/dist/cli/ts_loader.d.ts.map +1 -0
- package/dist/cli/ts_loader.js +95 -0
- package/dist/cli/ts_loader.js.map +1 -0
- package/dist/cli/vite_helpers.d.ts +62 -0
- package/dist/cli/vite_helpers.d.ts.map +1 -0
- package/dist/cli/vite_helpers.js +273 -0
- package/dist/cli/vite_helpers.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/player/hash_router.d.ts +23 -0
- package/dist/player/hash_router.d.ts.map +1 -0
- package/dist/player/hash_router.js +49 -0
- package/dist/player/hash_router.js.map +1 -0
- package/dist/player/hud.d.ts +33 -0
- package/dist/player/hud.d.ts.map +1 -0
- package/dist/player/hud.js +357 -0
- package/dist/player/hud.js.map +1 -0
- package/dist/player/index.d.ts +123 -0
- package/dist/player/index.d.ts.map +1 -0
- package/dist/player/index.js +848 -0
- package/dist/player/index.js.map +1 -0
- package/dist/player/input.d.ts +14 -0
- package/dist/player/input.d.ts.map +1 -0
- package/dist/player/input.js +90 -0
- package/dist/player/input.js.map +1 -0
- package/dist/player/slot.d.ts +22 -0
- package/dist/player/slot.d.ts.map +1 -0
- package/dist/player/slot.js +43 -0
- package/dist/player/slot.js.map +1 -0
- package/dist/player/transitions/cut.d.ts +7 -0
- package/dist/player/transitions/cut.d.ts.map +1 -0
- package/dist/player/transitions/cut.js +9 -0
- package/dist/player/transitions/cut.js.map +1 -0
- package/dist/player/transitions/fade.d.ts +7 -0
- package/dist/player/transitions/fade.d.ts.map +1 -0
- package/dist/player/transitions/fade.js +18 -0
- package/dist/player/transitions/fade.js.map +1 -0
- package/dist/player/transitions/index.d.ts +4 -0
- package/dist/player/transitions/index.d.ts.map +1 -0
- package/dist/player/transitions/index.js +4 -0
- package/dist/player/transitions/index.js.map +1 -0
- package/dist/player/transitions/slide.d.ts +6 -0
- package/dist/player/transitions/slide.d.ts.map +1 -0
- package/dist/player/transitions/slide.js +35 -0
- package/dist/player/transitions/slide.js.map +1 -0
- package/dist/script/index.d.ts +2 -0
- package/dist/script/index.d.ts.map +1 -0
- package/dist/script/index.js +2 -0
- package/dist/script/index.js.map +1 -0
- package/dist/script/script.d.ts +10 -0
- package/dist/script/script.d.ts.map +1 -0
- package/dist/script/script.js +41 -0
- package/dist/script/script.js.map +1 -0
- package/dist/segment/SegmentRunner.d.ts +52 -0
- package/dist/segment/SegmentRunner.d.ts.map +1 -0
- package/dist/segment/SegmentRunner.js +187 -0
- package/dist/segment/SegmentRunner.js.map +1 -0
- package/dist/segment/defineConfig.d.ts +6 -0
- package/dist/segment/defineConfig.d.ts.map +1 -0
- package/dist/segment/defineConfig.js +7 -0
- package/dist/segment/defineConfig.js.map +1 -0
- package/dist/segment/defineSegment.d.ts +7 -0
- package/dist/segment/defineSegment.d.ts.map +1 -0
- package/dist/segment/defineSegment.js +25 -0
- package/dist/segment/defineSegment.js.map +1 -0
- package/dist/segment/index.d.ts +5 -0
- package/dist/segment/index.d.ts.map +1 -0
- package/dist/segment/index.js +4 -0
- package/dist/segment/index.js.map +1 -0
- package/dist/timeline/index.d.ts +73 -0
- package/dist/timeline/index.d.ts.map +1 -0
- package/dist/timeline/index.js +142 -0
- package/dist/timeline/index.js.map +1 -0
- package/dist/timeline/loadAudioTrack.d.ts +18 -0
- package/dist/timeline/loadAudioTrack.d.ts.map +1 -0
- package/dist/timeline/loadAudioTrack.js +44 -0
- package/dist/timeline/loadAudioTrack.js.map +1 -0
- package/dist/timeline/loadVoiceover.d.ts +18 -0
- package/dist/timeline/loadVoiceover.d.ts.map +1 -0
- package/dist/timeline/loadVoiceover.js +38 -0
- package/dist/timeline/loadVoiceover.js.map +1 -0
- package/dist/timeline/resolveTiming.d.ts +28 -0
- package/dist/timeline/resolveTiming.d.ts.map +1 -0
- package/dist/timeline/resolveTiming.js +63 -0
- package/dist/timeline/resolveTiming.js.map +1 -0
- package/dist/timeline/validateTiming.d.ts +29 -0
- package/dist/timeline/validateTiming.d.ts.map +1 -0
- package/dist/timeline/validateTiming.js +62 -0
- package/dist/timeline/validateTiming.js.map +1 -0
- package/dist/types.d.ts +216 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +47 -0
- package/skill/SKILL.md +64 -0
- package/skill/assets/hello_world/PLAN.md +31 -0
- package/skill/assets/hello_world/README.md +27 -0
- package/skill/assets/hello_world/audio/audio_plan.md +14 -0
- package/skill/assets/hello_world/segments/hello_intro.ts +69 -0
- package/skill/assets/hello_world/segments/hello_outro.ts +71 -0
- package/skill/assets/hello_world/timeline.ts +15 -0
- package/skill/assets/hello_world/voiceover_script/script.md +10 -0
- package/skill/assets/install/package.json +10 -0
- package/skill/assets/install/tsconfig.json +23 -0
- package/skill/assets/styles/editorial-mono/STYLE.md +124 -0
- package/skill/assets/styles/editorial-mono/brand.md +85 -0
- package/skill/assets/styles/editorial-mono/reference/animations.jsx +752 -0
- package/skill/assets/styles/editorial-mono/reference/scenes.html +563 -0
- package/skill/assets/styles/editorial-mono/sample/bullet.ts +101 -0
- package/skill/assets/styles/editorial-mono/sample/content.ts +104 -0
- package/skill/assets/styles/editorial-mono/sample/cta.ts +113 -0
- package/skill/assets/styles/editorial-mono/sample/feature.ts +111 -0
- package/skill/assets/styles/editorial-mono/sample/grid.ts +97 -0
- package/skill/assets/styles/editorial-mono/sample/kinetic.ts +96 -0
- package/skill/assets/styles/editorial-mono/sample/section.ts +101 -0
- package/skill/assets/styles/editorial-mono/sample/stat.ts +128 -0
- package/skill/assets/styles/editorial-mono/sample/title.ts +97 -0
- package/skill/assets/styles/editorial-mono/sample/ui-showcase.ts +159 -0
- package/skill/assets/styles/editorial-mono/tokens.css +44 -0
- package/skill/assets/styles/iso-diagram/STYLE.md +109 -0
- package/skill/assets/styles/iso-diagram/brand.md +32 -0
- package/skill/assets/styles/iso-diagram/reference/animations.jsx +673 -0
- package/skill/assets/styles/iso-diagram/reference/scenes.html +427 -0
- package/skill/assets/styles/iso-diagram/sample/bullet.ts +144 -0
- package/skill/assets/styles/iso-diagram/sample/content.ts +192 -0
- package/skill/assets/styles/iso-diagram/sample/cta.ts +162 -0
- package/skill/assets/styles/iso-diagram/sample/feature.ts +205 -0
- package/skill/assets/styles/iso-diagram/sample/grid.ts +181 -0
- package/skill/assets/styles/iso-diagram/sample/kinetic.ts +102 -0
- package/skill/assets/styles/iso-diagram/sample/section.ts +149 -0
- package/skill/assets/styles/iso-diagram/sample/stat.ts +164 -0
- package/skill/assets/styles/iso-diagram/sample/title.ts +173 -0
- package/skill/assets/styles/iso-diagram/sample/ui-showcase.ts +162 -0
- package/skill/assets/styles/iso-diagram/tokens.css +40 -0
- package/skill/assets/styles/motion-engineering/STYLE.md +106 -0
- package/skill/assets/styles/motion-engineering/brand.md +29 -0
- package/skill/assets/styles/motion-engineering/reference/animations.jsx +673 -0
- package/skill/assets/styles/motion-engineering/reference/scenes.html +513 -0
- package/skill/assets/styles/motion-engineering/sample/bullet.ts +176 -0
- package/skill/assets/styles/motion-engineering/sample/content.ts +228 -0
- package/skill/assets/styles/motion-engineering/sample/cta.ts +209 -0
- package/skill/assets/styles/motion-engineering/sample/feature.ts +299 -0
- package/skill/assets/styles/motion-engineering/sample/grid.ts +190 -0
- package/skill/assets/styles/motion-engineering/sample/kinetic.ts +159 -0
- package/skill/assets/styles/motion-engineering/sample/section.ts +196 -0
- package/skill/assets/styles/motion-engineering/sample/stat.ts +230 -0
- package/skill/assets/styles/motion-engineering/sample/title.ts +219 -0
- package/skill/assets/styles/motion-engineering/sample/ui-showcase.ts +267 -0
- package/skill/assets/styles/motion-engineering/tokens.css +40 -0
- package/skill/assets/styles/neon-terminal/STYLE.md +105 -0
- package/skill/assets/styles/neon-terminal/brand.md +27 -0
- package/skill/assets/styles/neon-terminal/reference/animations.jsx +673 -0
- package/skill/assets/styles/neon-terminal/reference/scenes.html +387 -0
- package/skill/assets/styles/neon-terminal/sample/bullet.ts +113 -0
- package/skill/assets/styles/neon-terminal/sample/content.ts +117 -0
- package/skill/assets/styles/neon-terminal/sample/cta.ts +131 -0
- package/skill/assets/styles/neon-terminal/sample/feature.ts +112 -0
- package/skill/assets/styles/neon-terminal/sample/grid.ts +128 -0
- package/skill/assets/styles/neon-terminal/sample/kinetic.ts +105 -0
- package/skill/assets/styles/neon-terminal/sample/section.ts +96 -0
- package/skill/assets/styles/neon-terminal/sample/stat.ts +123 -0
- package/skill/assets/styles/neon-terminal/sample/title.ts +122 -0
- package/skill/assets/styles/neon-terminal/sample/ui-showcase.ts +127 -0
- package/skill/assets/styles/neon-terminal/tokens.css +39 -0
- package/skill/assets/styles/risograph/STYLE.md +110 -0
- package/skill/assets/styles/risograph/brand.md +26 -0
- package/skill/assets/styles/risograph/reference/animations.jsx +673 -0
- package/skill/assets/styles/risograph/reference/scenes.html +403 -0
- package/skill/assets/styles/risograph/sample/bullet.ts +124 -0
- package/skill/assets/styles/risograph/sample/content.ts +135 -0
- package/skill/assets/styles/risograph/sample/cta.ts +149 -0
- package/skill/assets/styles/risograph/sample/feature.ts +152 -0
- package/skill/assets/styles/risograph/sample/grid.ts +123 -0
- package/skill/assets/styles/risograph/sample/kinetic.ts +125 -0
- package/skill/assets/styles/risograph/sample/section.ts +130 -0
- package/skill/assets/styles/risograph/sample/stat.ts +145 -0
- package/skill/assets/styles/risograph/sample/title.ts +132 -0
- package/skill/assets/styles/risograph/sample/ui-showcase.ts +147 -0
- package/skill/assets/styles/risograph/tokens.css +39 -0
- package/skill/assets/styles/swiss-console/STYLE.md +107 -0
- package/skill/assets/styles/swiss-console/brand.md +37 -0
- package/skill/assets/styles/swiss-console/reference/animations.jsx +673 -0
- package/skill/assets/styles/swiss-console/reference/scenes.html +420 -0
- package/skill/assets/styles/swiss-console/sample/bullet.ts +122 -0
- package/skill/assets/styles/swiss-console/sample/content.ts +137 -0
- package/skill/assets/styles/swiss-console/sample/cta.ts +109 -0
- package/skill/assets/styles/swiss-console/sample/feature.ts +163 -0
- package/skill/assets/styles/swiss-console/sample/grid.ts +145 -0
- package/skill/assets/styles/swiss-console/sample/kinetic.ts +117 -0
- package/skill/assets/styles/swiss-console/sample/section.ts +127 -0
- package/skill/assets/styles/swiss-console/sample/stat.ts +148 -0
- package/skill/assets/styles/swiss-console/sample/title.ts +148 -0
- package/skill/assets/styles/swiss-console/sample/ui-showcase.ts +198 -0
- package/skill/assets/styles/swiss-console/tokens.css +39 -0
- package/skill/install/INSTALL.md +400 -0
- package/skill/references/audio/audio_plan.md +199 -0
- package/skill/references/audio/build.md +208 -0
- package/skill/references/audio/cue_template.md +219 -0
- package/skill/references/audio/ffmpeg_cookbook.md +267 -0
- package/skill/references/audio/music/music.md +171 -0
- package/skill/references/audio/music/providers/elevenlabs.md +170 -0
- package/skill/references/audio/music/providers/manual.md +140 -0
- package/skill/references/audio/music/providers/openverse.md +265 -0
- package/skill/references/audio/sfx/providers/elevenlabs.md +152 -0
- package/skill/references/audio/sfx/providers/manual.md +117 -0
- package/skill/references/audio/sfx/providers/openverse.md +243 -0
- package/skill/references/audio/sfx/sfx.md +149 -0
- package/skill/references/audio/styles.md +102 -0
- package/skill/references/audio/sync.md +237 -0
- package/skill/references/audio/voiceover/animation_sync.md +142 -0
- package/skill/references/audio/voiceover/provider_script.md +153 -0
- package/skill/references/audio/voiceover/providers/elevenlabs.md +288 -0
- package/skill/references/audio/voiceover/providers/manual.md +100 -0
- package/skill/references/audio/voiceover/script_writing.md +100 -0
- package/skill/references/audio/voiceover/style_intake.md +56 -0
- package/skill/references/audio/voiceover/sync_algorithm.md +167 -0
- package/skill/references/audio/voiceover.md +296 -0
- package/skill/references/audio.md +135 -0
- package/skill/references/authoring_segment.md +446 -0
- package/skill/references/create_or_edit_video.md +232 -0
- package/skill/references/dev_server.md +157 -0
- package/skill/references/export.md +145 -0
- package/skill/references/new_video.md +117 -0
- package/skill/references/project_structure.md +144 -0
- package/skill/references/setup.md +109 -0
- package/skill/references/setup_new_style.md +158 -0
- package/skill/references/styles.md +154 -0
- package/skill/references/testing.md +115 -0
- package/skill/references/types.md +240 -0
- package/src/cli/entry/components/copy_button.ts +42 -0
- package/src/cli/entry/components/download_modal.ts +204 -0
- package/src/cli/entry/components/empty_state.ts +55 -0
- package/src/cli/entry/components/hide_hud_tab.ts +37 -0
- package/src/cli/entry/components/icons.ts +31 -0
- package/src/cli/entry/components/top_bar.ts +69 -0
- package/src/cli/entry/components/video_card.ts +57 -0
- package/src/cli/entry/dev_frame.ts +189 -0
- package/src/cli/entry/entry_index.ts +16 -0
- package/src/cli/entry/entry_video.ts +24 -0
- package/src/cli/entry/index.html +12 -0
- package/src/cli/entry/parse_slug.ts +14 -0
- package/src/cli/entry/render.html +17 -0
- package/src/cli/entry/render_entry.ts +121 -0
- package/src/cli/entry/styles/base.css +45 -0
- package/src/cli/entry/styles/components.css +605 -0
- package/src/cli/entry/styles/tokens.css +44 -0
- package/src/cli/entry/video.html +22 -0
- package/src/cli/entry/views/homepage.ts +66 -0
- package/src/cli/entry/views/video_view.ts +286 -0
- package/src/cli/entry/virtual.d.ts +8 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
## When this is loaded
|
|
4
|
+
|
|
5
|
+
You were routed here from the intent dispatch table because the user wants to write tests for their Videowright project.
|
|
6
|
+
|
|
7
|
+
## What is worth testing
|
|
8
|
+
|
|
9
|
+
Videowright projects are content-heavy. Not everything needs automated tests. Focus testing effort on things that break silently and are hard to catch by eye.
|
|
10
|
+
|
|
11
|
+
The examples below are suggestions -- pick what fits the project. Do not prescribe a rigid test suite. For context on the tools these tests exercise, see [dev_server.md](dev_server.md) (interactive preview) and [export.md](export.md) (render export pipeline and advances validation).
|
|
12
|
+
|
|
13
|
+
## Advances coherence
|
|
14
|
+
|
|
15
|
+
The most common source of export failures is a mismatch between a segment's `advances` array and its actual `waitForNext()`/`hold()` calls.
|
|
16
|
+
|
|
17
|
+
**What to test:** For each segment, verify that `advances.length` matches the expected number of triggerNext presses.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { expect, test } from 'vitest';
|
|
21
|
+
|
|
22
|
+
test('intro segment has correct advances count', async () => {
|
|
23
|
+
const mod = await import('../segments/intro/index.js');
|
|
24
|
+
const segment = mod.default;
|
|
25
|
+
// intro: hold(3000) -> 1 press to move past
|
|
26
|
+
expect(segment.advances).toHaveLength(1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('feature-list segment advances match beat count', async () => {
|
|
30
|
+
const mod = await import('../segments/feature-list/index.js');
|
|
31
|
+
const segment = mod.default;
|
|
32
|
+
// 3 waitForNext calls + 1 final press = 4 advances
|
|
33
|
+
expect(segment.advances).toHaveLength(4);
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This catches the case where someone adds a `waitForNext()` call but forgets to add an entry to `advances` (or vice versa). The export pipeline validates this at runtime, but catching it in tests is faster.
|
|
38
|
+
|
|
39
|
+
## Typecheck
|
|
40
|
+
|
|
41
|
+
Run the TypeScript compiler as part of CI to catch type errors in segments, timelines, and config:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx tsc --noEmit
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This verifies that:
|
|
48
|
+
|
|
49
|
+
- All segment specs match the `SegmentSpec` type.
|
|
50
|
+
- Timeline entries reference valid types.
|
|
51
|
+
- Config uses correct field types.
|
|
52
|
+
|
|
53
|
+
Type errors in segments are common after refactoring shared components or updating the lib version.
|
|
54
|
+
|
|
55
|
+
## Playwright screenshot tests
|
|
56
|
+
|
|
57
|
+
For visual-heavy segments, capture a screenshot at key beats and compare against a baseline:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { expect, test } from '@playwright/test';
|
|
61
|
+
|
|
62
|
+
test('intro segment renders correctly', async ({ page }) => {
|
|
63
|
+
await page.goto('http://localhost:5173/#/intro/0');
|
|
64
|
+
// Wait for segment to mount and settle
|
|
65
|
+
await page.waitForFunction('document.body.dataset.vwState === "playing"');
|
|
66
|
+
await expect(page).toHaveScreenshot('intro-beat-0.png');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('feature-cards shows all cards after advances', async ({ page }) => {
|
|
70
|
+
await page.goto('http://localhost:5173/#/feature-cards/0');
|
|
71
|
+
await page.waitForFunction('document.body.dataset.vwState === "playing"');
|
|
72
|
+
// Advance twice to reveal cards, waiting for player to settle after each press
|
|
73
|
+
await page.keyboard.press('Space');
|
|
74
|
+
await page.waitForFunction('document.body.dataset.vwState === "playing"');
|
|
75
|
+
await page.keyboard.press('Space');
|
|
76
|
+
await page.waitForFunction('document.body.dataset.vwState === "playing"');
|
|
77
|
+
await expect(page).toHaveScreenshot('feature-cards-revealed.png');
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
This requires a running dev server. In CI, start the dev server before tests:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npx videowright dev --port 5173 &
|
|
85
|
+
npx playwright test
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Screenshot tests are heavyweight — use them selectively for segments where visual correctness is important (branded content, data visualizations, precise layouts).
|
|
89
|
+
|
|
90
|
+
## Timeline structure
|
|
91
|
+
|
|
92
|
+
Verify that a timeline references valid segments and has expected properties:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { expect, test } from 'vitest';
|
|
96
|
+
|
|
97
|
+
test('demo timeline has expected segments', async () => {
|
|
98
|
+
const mod = await import('../videos/demo/timeline.js');
|
|
99
|
+
const timeline = mod.default;
|
|
100
|
+
|
|
101
|
+
expect(timeline.meta.title).toBeTruthy();
|
|
102
|
+
expect(timeline.segments.length).toBeGreaterThan(0);
|
|
103
|
+
|
|
104
|
+
const ids = timeline.segments.map(s => s.id);
|
|
105
|
+
expect(ids).toContain('intro');
|
|
106
|
+
expect(ids).toContain('outro');
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## What not to test
|
|
111
|
+
|
|
112
|
+
- **Visual aesthetics.** Whether a segment "looks good" is a design judgment, not a test assertion.
|
|
113
|
+
- **Voiceover text content.** VO copy changes frequently. Testing exact strings creates maintenance burden.
|
|
114
|
+
- **Transition behavior.** Built-in transitions are tested in the lib's own test suite. Custom transitions may warrant tests if they have complex logic.
|
|
115
|
+
- **The lib itself.** Videowright's internal tests cover the player, runner, CLI, and export pipeline. Consumer projects do not need to re-test lib behavior.
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Types
|
|
2
|
+
|
|
3
|
+
Quick reference for Videowright's public TypeScript types. All types are importable from `'videowright'`.
|
|
4
|
+
|
|
5
|
+
## Segment types
|
|
6
|
+
|
|
7
|
+
### `SegmentSpec`
|
|
8
|
+
|
|
9
|
+
The spec object passed to `defineSegment()`.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
interface SegmentSpec {
|
|
13
|
+
id: string;
|
|
14
|
+
advances: number[];
|
|
15
|
+
voiceover?: string;
|
|
16
|
+
notes?: string;
|
|
17
|
+
mount?(el: HTMLElement, ctx: PlayerContext): void | Promise<void>;
|
|
18
|
+
play(ctx: PlayerContext): Promise<void>;
|
|
19
|
+
unmount?(): void;
|
|
20
|
+
next?(): boolean;
|
|
21
|
+
prev?(): boolean;
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
See [authoring_segment.md](authoring_segment.md) for field details.
|
|
26
|
+
|
|
27
|
+
### `Segment`
|
|
28
|
+
|
|
29
|
+
The branded object returned by `defineSegment()`. Extends `SegmentSpec` with an internal brand symbol. Frozen and immutable.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
interface Segment extends SegmentSpec {
|
|
33
|
+
readonly [SEGMENT_BRAND]: true;
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### `PlayerContext`
|
|
38
|
+
|
|
39
|
+
Runtime context passed to `mount` and `play` lifecycle methods.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
interface PlayerContext {
|
|
43
|
+
waitForNext(): Promise<void>;
|
|
44
|
+
hold(ms: number): Promise<void>;
|
|
45
|
+
signal: AbortSignal;
|
|
46
|
+
mode: 'interactive' | 'render';
|
|
47
|
+
clock(): number;
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Timeline types
|
|
52
|
+
|
|
53
|
+
### `Timeline`
|
|
54
|
+
|
|
55
|
+
A complete timeline definition. Default export of a video's `timeline.ts`.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
interface Timeline {
|
|
59
|
+
meta: TimelineMeta;
|
|
60
|
+
segments: TimelineEntry[];
|
|
61
|
+
default_timing?: Timing;
|
|
62
|
+
default_audio_track?: AudioTrack;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
| Field | Required | Purpose |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `meta` | Yes | Timeline-level metadata. |
|
|
69
|
+
| `segments` | Yes | Ordered array of segment entries. |
|
|
70
|
+
| `default_timing` | No | Standalone timing overrides. Used when no audio track is active. |
|
|
71
|
+
| `default_audio_track` | No | Default audio track for this video. See [audio.md](audio.md). |
|
|
72
|
+
|
|
73
|
+
### `TimelineMeta`
|
|
74
|
+
|
|
75
|
+
Timeline-level metadata.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
interface TimelineMeta {
|
|
79
|
+
title: string;
|
|
80
|
+
style?: string;
|
|
81
|
+
aspectRatio?: string;
|
|
82
|
+
resolution?: [number, number];
|
|
83
|
+
fps?: number;
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
| Field | Required | Purpose |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `title` | Yes | Video title. |
|
|
90
|
+
| `style` | No | Style slug for this video. Overrides `config.defaultStyle`. Must match a folder in `styles/`. |
|
|
91
|
+
| `aspectRatio` | No | Aspect ratio string (e.g., `'16:9'`). Falls back to config default, then `'16:9'`. |
|
|
92
|
+
| `resolution` | No | `[width, height]` in pixels. Falls back to config default, then `[1920, 1080]`. |
|
|
93
|
+
| `fps` | No | Frames per second. Falls back to config default, then `60`. |
|
|
94
|
+
|
|
95
|
+
### `TimelineEntry`
|
|
96
|
+
|
|
97
|
+
A single entry in a timeline's segments array.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
interface TimelineEntry {
|
|
101
|
+
id: string;
|
|
102
|
+
transition?: string | { type: string; [k: string]: unknown };
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
| Field | Required | Purpose |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `id` | Yes | Segment id. Maps to `segments/<id>/index.ts`. |
|
|
109
|
+
| `transition` | No | Transition when entering this segment. String name (e.g., `'fade'`) or config object. |
|
|
110
|
+
|
|
111
|
+
Built-in transitions: `cut`, `fade`, `slideLeft`, `slideRight`, `slideUp`, `slideDown`.
|
|
112
|
+
|
|
113
|
+
## Timing and Voiceover types
|
|
114
|
+
|
|
115
|
+
### `Timing`
|
|
116
|
+
|
|
117
|
+
Per-segment advance schedule overrides. Used by voiceovers and as a standalone timing layer (`default_timing` on Timeline).
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
type Timing = {
|
|
121
|
+
perSegment: Partial<Record<string, number[]>>;
|
|
122
|
+
};
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Keys are segment ids; values are advance times in seconds (same units as `SegmentSpec.advances`). A Timing only needs to specify segments it wants to override -- unspecified segments fall back to their own `advances`.
|
|
126
|
+
|
|
127
|
+
### `AudioTrack`
|
|
128
|
+
|
|
129
|
+
A rendered audio track for a video. Stored at `videos/<video>/audio/tracks/<id>/track.ts`.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
type AudioTrack = {
|
|
133
|
+
audio_file: string;
|
|
134
|
+
length_s: number;
|
|
135
|
+
timing: Timing;
|
|
136
|
+
audio_plan_path?: string;
|
|
137
|
+
plan_snapshot_path?: string;
|
|
138
|
+
created_at?: string;
|
|
139
|
+
notes?: string;
|
|
140
|
+
};
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
| Field | Required | Purpose |
|
|
144
|
+
|---|---|---|
|
|
145
|
+
| `audio_file` | Yes | Audio file path, relative to the **video folder** (directory containing `timeline.ts`). E.g., `"./audio/tracks/v1/track.mp3"`. Rewritten to absolute by `loadAudioTrack()`. |
|
|
146
|
+
| `length_s` | Yes | Length of the rendered audio in seconds. |
|
|
147
|
+
| `timing` | Yes | Per-segment advance timing synced to this audio track. |
|
|
148
|
+
| `audio_plan_path` | No | Path to the audio plan that produced this track, relative to `track.ts`. |
|
|
149
|
+
| `plan_snapshot_path` | No | Path to the point-in-time plan snapshot, relative to `track.ts`. |
|
|
150
|
+
| `created_at` | No | ISO timestamp when this track was rendered. |
|
|
151
|
+
| `notes` | No | Freeform notes about this track. |
|
|
152
|
+
|
|
153
|
+
See [audio.md](audio.md) for the audio workflow.
|
|
154
|
+
|
|
155
|
+
### `Voiceover`
|
|
156
|
+
|
|
157
|
+
A single voiceover source file. Stored at `videos/<video>/audio/originals/voiceovers/<slug>/voiceover.ts`. Voiceovers are source data used to build audio tracks -- they are not directly referenced by `timeline.ts`.
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
type Voiceover = {
|
|
161
|
+
audio_file: string;
|
|
162
|
+
provider: "elevenlabs" | "manual";
|
|
163
|
+
provider_timing_file?: string;
|
|
164
|
+
timing: Timing;
|
|
165
|
+
notes?: string;
|
|
166
|
+
eleven_labs_voice_id?: string;
|
|
167
|
+
};
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
| Field | Required | Purpose |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| `audio_file` | Yes | Audio file path, relative to `voiceover.ts` directory. |
|
|
173
|
+
| `provider` | Yes | Provider that produced the audio. |
|
|
174
|
+
| `provider_timing_file` | No | Provider timing JSON path, relative to `voiceover.ts` directory. |
|
|
175
|
+
| `timing` | Yes | Per-segment advance timing synced to this audio. |
|
|
176
|
+
| `notes` | No | Freeform notes about this voiceover. |
|
|
177
|
+
| `eleven_labs_voice_id` | No | ElevenLabs voice ID for TTS generation. Defaults to Asher (`tMvyQtpCVQ0DkixuYm6J`) when omitted. Ignored when provider is `"manual"`. |
|
|
178
|
+
|
|
179
|
+
See [audio/voiceover.md](audio/voiceover.md) for the full voiceover flow and file conventions.
|
|
180
|
+
|
|
181
|
+
## Transition types
|
|
182
|
+
|
|
183
|
+
### `Transition`
|
|
184
|
+
|
|
185
|
+
A transition function that animates between two slot elements.
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
type Transition = (
|
|
189
|
+
outgoing: HTMLElement,
|
|
190
|
+
incoming: HTMLElement,
|
|
191
|
+
ctx: TransitionContext,
|
|
192
|
+
) => Promise<void>;
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### `TransitionContext`
|
|
196
|
+
|
|
197
|
+
Context passed to transition functions.
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
interface TransitionContext {
|
|
201
|
+
direction: 'forward' | 'backward';
|
|
202
|
+
duration?: number;
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Config types
|
|
207
|
+
|
|
208
|
+
### `Config`
|
|
209
|
+
|
|
210
|
+
Repo-wide configuration from `videowright.config.ts`. Authored via `defineConfig()`.
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
interface Config {
|
|
214
|
+
projectStructure: 'v1';
|
|
215
|
+
defaultStyle?: string;
|
|
216
|
+
defaults?: {
|
|
217
|
+
resolution?: [number, number];
|
|
218
|
+
fps?: number;
|
|
219
|
+
aspectRatio?: string;
|
|
220
|
+
};
|
|
221
|
+
transitions?: Record<string, () => Promise<{ default: Transition }>>;
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
| Field | Required | Purpose |
|
|
226
|
+
|---|---|---|
|
|
227
|
+
| `projectStructure` | Yes | Layout version marker. Always `'v1'`. |
|
|
228
|
+
| `defaultStyle` | After setup | Slug of the default style in `styles/<slug>/`. Required once setup completes. Empty or missing = setup gate open. |
|
|
229
|
+
| `defaults` | No | Default values for timeline meta fields (resolution, fps, aspectRatio). |
|
|
230
|
+
| `transitions` | No | Custom transition loaders keyed by name. Shadows built-ins if names collide. |
|
|
231
|
+
|
|
232
|
+
## Functions
|
|
233
|
+
|
|
234
|
+
### `defineSegment(spec: SegmentSpec): Segment`
|
|
235
|
+
|
|
236
|
+
Validates and brands a segment spec. Throws `TypeError` if `id`, `play`, or `advances` is missing or invalid.
|
|
237
|
+
|
|
238
|
+
### `defineConfig(config: Config): Config`
|
|
239
|
+
|
|
240
|
+
Identity function for typed config authoring. Provides autocomplete and type checking for `videowright.config.ts`.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy-to-clipboard icon button.
|
|
3
|
+
* Swaps to a check icon for 1.5s after a successful copy.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { iconCheck, iconCopy } from "./icons.js";
|
|
7
|
+
|
|
8
|
+
export function renderCopyButton(text: string): HTMLButtonElement {
|
|
9
|
+
const btn = document.createElement("button");
|
|
10
|
+
btn.className = "vw-copy-btn";
|
|
11
|
+
btn.type = "button";
|
|
12
|
+
btn.setAttribute("aria-label", "Copy to clipboard");
|
|
13
|
+
btn.innerHTML = iconCopy();
|
|
14
|
+
|
|
15
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
16
|
+
|
|
17
|
+
btn.addEventListener("click", (e) => {
|
|
18
|
+
e.stopPropagation();
|
|
19
|
+
e.preventDefault();
|
|
20
|
+
|
|
21
|
+
navigator.clipboard.writeText(text).then(
|
|
22
|
+
() => {
|
|
23
|
+
btn.innerHTML = iconCheck();
|
|
24
|
+
btn.classList.add("vw-copy-btn--copied");
|
|
25
|
+
btn.setAttribute("aria-label", "Copied");
|
|
26
|
+
|
|
27
|
+
if (timeout) clearTimeout(timeout);
|
|
28
|
+
timeout = setTimeout(() => {
|
|
29
|
+
btn.innerHTML = iconCopy();
|
|
30
|
+
btn.classList.remove("vw-copy-btn--copied");
|
|
31
|
+
btn.setAttribute("aria-label", "Copy to clipboard");
|
|
32
|
+
timeout = null;
|
|
33
|
+
}, 1500);
|
|
34
|
+
},
|
|
35
|
+
() => {
|
|
36
|
+
// Clipboard API not available or denied -- silent fail for dev tool
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return btn;
|
|
42
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Download modal: two-column layout offering "Export Video" (CLI command)
|
|
3
|
+
* and "Screen Record" (instructions). Accessible from both homepage card
|
|
4
|
+
* and video view top bar.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { renderCopyButton } from "./copy_button.js";
|
|
8
|
+
import { iconX } from "./icons.js";
|
|
9
|
+
|
|
10
|
+
export interface DownloadModalProps {
|
|
11
|
+
slug: string;
|
|
12
|
+
title: string;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Mount the download modal onto document.body.
|
|
18
|
+
* Returns a cleanup function that removes it.
|
|
19
|
+
*/
|
|
20
|
+
export function renderDownloadModal(props: DownloadModalProps): () => void {
|
|
21
|
+
// Dismiss any existing modal before opening a new one
|
|
22
|
+
const existing = document.querySelector(".vw-modal-backdrop");
|
|
23
|
+
if (existing) {
|
|
24
|
+
existing.remove();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const { slug, title, onClose } = props;
|
|
28
|
+
|
|
29
|
+
// Backdrop
|
|
30
|
+
const backdrop = document.createElement("div");
|
|
31
|
+
backdrop.className = "vw-modal-backdrop";
|
|
32
|
+
backdrop.setAttribute("role", "presentation");
|
|
33
|
+
|
|
34
|
+
// Modal dialog
|
|
35
|
+
const dialog = document.createElement("div");
|
|
36
|
+
dialog.className = "vw-modal";
|
|
37
|
+
dialog.setAttribute("role", "dialog");
|
|
38
|
+
dialog.setAttribute("aria-modal", "true");
|
|
39
|
+
dialog.setAttribute("aria-labelledby", "vw-modal-title");
|
|
40
|
+
|
|
41
|
+
// Header
|
|
42
|
+
const header = document.createElement("div");
|
|
43
|
+
header.className = "vw-modal__header";
|
|
44
|
+
|
|
45
|
+
const headerText = document.createElement("div");
|
|
46
|
+
headerText.className = "vw-modal__header-text";
|
|
47
|
+
|
|
48
|
+
const headerTitle = document.createElement("h2");
|
|
49
|
+
headerTitle.className = "vw-modal__title";
|
|
50
|
+
headerTitle.id = "vw-modal-title";
|
|
51
|
+
headerTitle.textContent = `Export ${title}`;
|
|
52
|
+
headerText.appendChild(headerTitle);
|
|
53
|
+
|
|
54
|
+
const headerSlug = document.createElement("div");
|
|
55
|
+
headerSlug.className = "vw-modal__slug";
|
|
56
|
+
headerSlug.textContent = slug;
|
|
57
|
+
headerText.appendChild(headerSlug);
|
|
58
|
+
|
|
59
|
+
header.appendChild(headerText);
|
|
60
|
+
|
|
61
|
+
const closeBtn = document.createElement("button");
|
|
62
|
+
closeBtn.className = "vw-modal__close";
|
|
63
|
+
closeBtn.type = "button";
|
|
64
|
+
closeBtn.setAttribute("aria-label", "Close");
|
|
65
|
+
closeBtn.innerHTML = iconX();
|
|
66
|
+
closeBtn.addEventListener("click", dismiss);
|
|
67
|
+
header.appendChild(closeBtn);
|
|
68
|
+
|
|
69
|
+
dialog.appendChild(header);
|
|
70
|
+
|
|
71
|
+
// Columns container
|
|
72
|
+
const columns = document.createElement("div");
|
|
73
|
+
columns.className = "vw-modal__columns";
|
|
74
|
+
|
|
75
|
+
// --- Export Video column ---
|
|
76
|
+
const exportCol = document.createElement("div");
|
|
77
|
+
exportCol.className = "vw-modal__column";
|
|
78
|
+
|
|
79
|
+
const exportLabel = document.createElement("div");
|
|
80
|
+
exportLabel.className = "vw-modal__column-label";
|
|
81
|
+
|
|
82
|
+
const exportLabelText = document.createElement("span");
|
|
83
|
+
exportLabelText.textContent = "EXPORT VIDEO";
|
|
84
|
+
exportLabel.appendChild(exportLabelText);
|
|
85
|
+
|
|
86
|
+
const badge = document.createElement("span");
|
|
87
|
+
badge.className = "vw-modal__badge";
|
|
88
|
+
badge.textContent = "Recommended";
|
|
89
|
+
exportLabel.appendChild(badge);
|
|
90
|
+
|
|
91
|
+
exportCol.appendChild(exportLabel);
|
|
92
|
+
|
|
93
|
+
const exportDesc = document.createElement("p");
|
|
94
|
+
exportDesc.className = "vw-modal__desc";
|
|
95
|
+
exportDesc.textContent = "Pixel-perfect MP4 export. Best quality.";
|
|
96
|
+
exportCol.appendChild(exportDesc);
|
|
97
|
+
|
|
98
|
+
// CLI command code block
|
|
99
|
+
const cliCommand = `npx videowright render ${slug}`;
|
|
100
|
+
const codeBlock = document.createElement("div");
|
|
101
|
+
codeBlock.className = "vw-modal__code";
|
|
102
|
+
|
|
103
|
+
const code = document.createElement("code");
|
|
104
|
+
code.textContent = cliCommand;
|
|
105
|
+
codeBlock.appendChild(code);
|
|
106
|
+
|
|
107
|
+
const copyBtn = renderCopyButton(cliCommand);
|
|
108
|
+
codeBlock.appendChild(copyBtn);
|
|
109
|
+
|
|
110
|
+
exportCol.appendChild(codeBlock);
|
|
111
|
+
|
|
112
|
+
const exportNote = document.createElement("p");
|
|
113
|
+
exportNote.className = "vw-modal__note";
|
|
114
|
+
exportNote.textContent = "Export is CLI-only — runs ffmpeg + Playwright on your machine.";
|
|
115
|
+
exportCol.appendChild(exportNote);
|
|
116
|
+
|
|
117
|
+
columns.appendChild(exportCol);
|
|
118
|
+
|
|
119
|
+
// --- Screen Record column ---
|
|
120
|
+
const recordCol = document.createElement("div");
|
|
121
|
+
recordCol.className = "vw-modal__column";
|
|
122
|
+
|
|
123
|
+
const recordLabel = document.createElement("div");
|
|
124
|
+
recordLabel.className = "vw-modal__column-label";
|
|
125
|
+
recordLabel.textContent = "SCREEN RECORD";
|
|
126
|
+
recordCol.appendChild(recordLabel);
|
|
127
|
+
|
|
128
|
+
const recordDesc = document.createElement("p");
|
|
129
|
+
recordDesc.className = "vw-modal__desc";
|
|
130
|
+
recordDesc.textContent =
|
|
131
|
+
"Capture in a live browser with your screen recorder. Manual pace, great for live VO.";
|
|
132
|
+
recordCol.appendChild(recordDesc);
|
|
133
|
+
|
|
134
|
+
const tips = document.createElement("ul");
|
|
135
|
+
tips.className = "vw-modal__tips";
|
|
136
|
+
|
|
137
|
+
const tipItems = ["Press H to hide HUD", "→ next | ← prev", "Space to play/pause"];
|
|
138
|
+
for (const tipText of tipItems) {
|
|
139
|
+
const li = document.createElement("li");
|
|
140
|
+
li.textContent = tipText;
|
|
141
|
+
tips.appendChild(li);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
recordCol.appendChild(tips);
|
|
145
|
+
columns.appendChild(recordCol);
|
|
146
|
+
|
|
147
|
+
dialog.appendChild(columns);
|
|
148
|
+
backdrop.appendChild(dialog);
|
|
149
|
+
|
|
150
|
+
// Event handlers
|
|
151
|
+
backdrop.addEventListener("click", (e) => {
|
|
152
|
+
if (e.target === backdrop) dismiss();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
function getFocusableElements(): HTMLElement[] {
|
|
156
|
+
const selectors = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
157
|
+
return Array.from(dialog.querySelectorAll<HTMLElement>(selectors)).filter(
|
|
158
|
+
(el) => !el.hasAttribute("disabled"),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function onKeyDown(e: KeyboardEvent): void {
|
|
163
|
+
if (e.key === "Escape") {
|
|
164
|
+
e.preventDefault();
|
|
165
|
+
dismiss();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Focus trap: cycle Tab among focusable elements inside the dialog
|
|
170
|
+
if (e.key === "Tab") {
|
|
171
|
+
const focusable = getFocusableElements();
|
|
172
|
+
if (focusable.length === 0) return;
|
|
173
|
+
|
|
174
|
+
const first = focusable[0];
|
|
175
|
+
const last = focusable[focusable.length - 1];
|
|
176
|
+
|
|
177
|
+
if (e.shiftKey) {
|
|
178
|
+
if (document.activeElement === first) {
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
last.focus();
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
if (document.activeElement === last) {
|
|
184
|
+
e.preventDefault();
|
|
185
|
+
first.focus();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function dismiss(): void {
|
|
192
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
193
|
+
backdrop.remove();
|
|
194
|
+
onClose();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
document.addEventListener("keydown", onKeyDown);
|
|
198
|
+
document.body.appendChild(backdrop);
|
|
199
|
+
|
|
200
|
+
// Focus the close button for keyboard accessibility
|
|
201
|
+
closeBtn.focus();
|
|
202
|
+
|
|
203
|
+
return dismiss;
|
|
204
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cold-start empty state panel.
|
|
3
|
+
* Shown on the homepage when no videos exist.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { renderCopyButton } from "./copy_button.js";
|
|
7
|
+
|
|
8
|
+
export function renderEmptyState(): HTMLElement {
|
|
9
|
+
const panel = document.createElement("div");
|
|
10
|
+
panel.className = "vw-empty";
|
|
11
|
+
|
|
12
|
+
// Hero text
|
|
13
|
+
const hero = document.createElement("h2");
|
|
14
|
+
hero.className = "vw-empty__hero";
|
|
15
|
+
hero.textContent = "No videos yet";
|
|
16
|
+
panel.appendChild(hero);
|
|
17
|
+
|
|
18
|
+
// Instruction
|
|
19
|
+
const desc = document.createElement("p");
|
|
20
|
+
desc.className = "vw-empty__desc";
|
|
21
|
+
desc.textContent = "Ask your coding agent to create one for you:";
|
|
22
|
+
panel.appendChild(desc);
|
|
23
|
+
|
|
24
|
+
// Code block with copy button
|
|
25
|
+
const codeBlock = document.createElement("div");
|
|
26
|
+
codeBlock.className = "vw-empty__code";
|
|
27
|
+
|
|
28
|
+
const code = document.createElement("code");
|
|
29
|
+
code.textContent = "/videowright new video";
|
|
30
|
+
codeBlock.appendChild(code);
|
|
31
|
+
|
|
32
|
+
const copyBtn = renderCopyButton("/videowright new video");
|
|
33
|
+
codeBlock.appendChild(copyBtn);
|
|
34
|
+
|
|
35
|
+
panel.appendChild(codeBlock);
|
|
36
|
+
|
|
37
|
+
// Docs link
|
|
38
|
+
const docsLine = document.createElement("p");
|
|
39
|
+
docsLine.className = "vw-empty__docs";
|
|
40
|
+
|
|
41
|
+
const docsText = document.createTextNode("New to Videowright? ");
|
|
42
|
+
docsLine.appendChild(docsText);
|
|
43
|
+
|
|
44
|
+
const docsLink = document.createElement("a");
|
|
45
|
+
docsLink.className = "vw-empty__docs-link";
|
|
46
|
+
docsLink.href = "https://github.com/scosman/videowright";
|
|
47
|
+
docsLink.target = "_blank";
|
|
48
|
+
docsLink.rel = "noopener noreferrer";
|
|
49
|
+
docsLink.textContent = "Read the docs →";
|
|
50
|
+
docsLine.appendChild(docsLink);
|
|
51
|
+
|
|
52
|
+
panel.appendChild(docsLine);
|
|
53
|
+
|
|
54
|
+
return panel;
|
|
55
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hide-HUD tab: a small clickable tab anchored to the top edge of the HUD
|
|
3
|
+
* strip. Click toggles HUD visibility; chevron icon indicates direction.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { iconChevronDown, iconChevronUp } from "./icons.js";
|
|
7
|
+
|
|
8
|
+
export interface HideHudTabProps {
|
|
9
|
+
onToggle: () => boolean; // returns new visibility state
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create the hide-HUD tab element.
|
|
14
|
+
* The tab starts in the "HUD visible" state (chevron down = collapse).
|
|
15
|
+
*/
|
|
16
|
+
export function renderHideHudTab(props: HideHudTabProps): HTMLElement {
|
|
17
|
+
const tab = document.createElement("button");
|
|
18
|
+
tab.className = "vw-hide-hud-tab";
|
|
19
|
+
tab.type = "button";
|
|
20
|
+
tab.setAttribute("aria-label", "Toggle HUD");
|
|
21
|
+
|
|
22
|
+
let hudVisible = true;
|
|
23
|
+
|
|
24
|
+
function updateIcon(): void {
|
|
25
|
+
tab.innerHTML = hudVisible ? iconChevronDown() : iconChevronUp();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
updateIcon();
|
|
29
|
+
|
|
30
|
+
tab.addEventListener("click", (e) => {
|
|
31
|
+
e.stopPropagation();
|
|
32
|
+
hudVisible = props.onToggle();
|
|
33
|
+
updateIcon();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return tab;
|
|
37
|
+
}
|