argos-harness 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/LICENSE +21 -0
- package/README.md +21 -0
- package/assets/agents/auditor.md +140 -0
- package/assets/agents/commit-pr-pilot.md +154 -0
- package/assets/agents/explorer.md +93 -0
- package/assets/agents/implementer.md +117 -0
- package/assets/agents/leader.md +149 -0
- package/assets/agents/researcher.md +89 -0
- package/assets/agents/review-readability.md +87 -0
- package/assets/agents/review-reliability.md +100 -0
- package/assets/agents/review-resilience.md +87 -0
- package/assets/agents/review-risk.md +87 -0
- package/assets/agents/reviewer.md +167 -0
- package/assets/agents/ticket-audit.md +129 -0
- package/assets/hooks/argos-guard-destructive.sh +127 -0
- package/assets/hooks/argos-quality-gate.sh +129 -0
- package/assets/managed/aterrizaje.md +24 -0
- package/assets/managed/formato-respuesta.md +21 -0
- package/assets/managed/identidad.md +48 -0
- package/assets/managed/operaciones-seguras.md +14 -0
- package/assets/managed/orquestacion.md +169 -0
- package/assets/output-styles/argos.md +71 -0
- package/assets/skills/ai-sdk-5/SKILL.md +230 -0
- package/assets/skills/angular/SKILL.md +19 -0
- package/assets/skills/angular/references/architecture.md +137 -0
- package/assets/skills/angular/references/core.md +197 -0
- package/assets/skills/angular/references/forms.md +115 -0
- package/assets/skills/angular/references/performance.md +124 -0
- package/assets/skills/apollo-client/SKILL.md +61 -0
- package/assets/skills/app-blueprint/SKILL.md +45 -0
- package/assets/skills/app-blueprint/assets/module-template.md +48 -0
- package/assets/skills/app-blueprint/assets/system-template.md +38 -0
- package/assets/skills/app-blueprint/references/workflow.md +101 -0
- package/assets/skills/app-builder/SKILL.md +43 -0
- package/assets/skills/app-builder/phases/0-product.md +58 -0
- package/assets/skills/app-builder/phases/1-scaffold.md +33 -0
- package/assets/skills/app-builder/phases/10-store.md +40 -0
- package/assets/skills/app-builder/phases/2-data.md +34 -0
- package/assets/skills/app-builder/phases/3-domain.md +33 -0
- package/assets/skills/app-builder/phases/4-ui-nav.md +32 -0
- package/assets/skills/app-builder/phases/5-identity.md +30 -0
- package/assets/skills/app-builder/phases/6-polish.md +34 -0
- package/assets/skills/app-builder/phases/7-brand.md +31 -0
- package/assets/skills/app-builder/phases/8-web.md +30 -0
- package/assets/skills/app-builder/phases/9-docs.md +31 -0
- package/assets/skills/app-ia/SKILL.md +54 -0
- package/assets/skills/astro/SKILL.md +39 -0
- package/assets/skills/axios/SKILL.md +61 -0
- package/assets/skills/branch-pr/SKILL.md +200 -0
- package/assets/skills/bullmq/SKILL.md +55 -0
- package/assets/skills/chained-pr/SKILL.md +48 -0
- package/assets/skills/chained-pr/references/chaining-details.md +99 -0
- package/assets/skills/cognitive-doc-design/SKILL.md +81 -0
- package/assets/skills/comment-writer/SKILL.md +74 -0
- package/assets/skills/dashboard-ia/SKILL.md +54 -0
- package/assets/skills/django-drf/SKILL.md +180 -0
- package/assets/skills/go-testing/SKILL.md +47 -0
- package/assets/skills/go-testing/references/examples.md +89 -0
- package/assets/skills/issue-creation/SKILL.md +223 -0
- package/assets/skills/jira-epic/SKILL.md +306 -0
- package/assets/skills/jira-task/SKILL.md +382 -0
- package/assets/skills/judgment-day/SKILL.md +52 -0
- package/assets/skills/judgment-day/references/prompts-and-formats.md +98 -0
- package/assets/skills/lightsail-deploy/SKILL.md +44 -0
- package/assets/skills/lightsail-deploy/references/runbook.md +102 -0
- package/assets/skills/loop-back-debug/SKILL.md +102 -0
- package/assets/skills/mantine-form/SKILL.md +57 -0
- package/assets/skills/mongoose/SKILL.md +66 -0
- package/assets/skills/nextjs-15/SKILL.md +144 -0
- package/assets/skills/not-boring-mobile/SKILL.md +43 -0
- package/assets/skills/not-boring-mobile/references/not-boring-playbook.md +65 -0
- package/assets/skills/playwright/SKILL.md +315 -0
- package/assets/skills/pr-comments/SKILL.md +93 -0
- package/assets/skills/pr-create/SKILL.md +64 -0
- package/assets/skills/promo-video/SKILL.md +52 -0
- package/assets/skills/promo-video/assets/package.template.json +22 -0
- package/assets/skills/promo-video/assets/promo.template.tsx +469 -0
- package/assets/skills/promo-video/assets/theme.template.ts +27 -0
- package/assets/skills/promo-video/references/pipeline.md +119 -0
- package/assets/skills/promo-video-web/SKILL.md +51 -0
- package/assets/skills/promo-video-web/assets/browser-promo.template.tsx +385 -0
- package/assets/skills/promo-video-web/assets/capture.template.ts +70 -0
- package/assets/skills/promo-video-web/assets/package.template.json +26 -0
- package/assets/skills/promo-video-web/references/pipeline.md +84 -0
- package/assets/skills/pytest/SKILL.md +180 -0
- package/assets/skills/react-19/SKILL.md +118 -0
- package/assets/skills/react-hook-form/SKILL.md +59 -0
- package/assets/skills/react-router/SKILL.md +60 -0
- package/assets/skills/redux-toolkit/SKILL.md +60 -0
- package/assets/skills/review-diff/SKILL.md +101 -0
- package/assets/skills/ship-docs/SKILL.md +45 -0
- package/assets/skills/ship-docs/references/ship-docs-playbook.md +31 -0
- package/assets/skills/skill-creator/SKILL.md +97 -0
- package/assets/skills/skill-creator/assets/SKILL-TEMPLATE.md +68 -0
- package/assets/skills/skill-creator/references/skill-style-guide.md +79 -0
- package/assets/skills/skill-improver/SKILL.md +50 -0
- package/assets/skills/skill-improver/references/skill-style-guide.md +79 -0
- package/assets/skills/socketio/SKILL.md +58 -0
- package/assets/skills/spec-bootstrap/SKILL.md +62 -0
- package/assets/skills/store-ship/SKILL.md +52 -0
- package/assets/skills/store-ship/assets/android-supply.template.md +22 -0
- package/assets/skills/store-ship/assets/eas.template.json +32 -0
- package/assets/skills/store-ship/assets/maestro-flow.template.yaml +27 -0
- package/assets/skills/store-ship/assets/store.config.template.json +28 -0
- package/assets/skills/store-ship/references/pipeline.md +198 -0
- package/assets/skills/stripe/SKILL.md +83 -0
- package/assets/skills/tailwind-4/SKILL.md +193 -0
- package/assets/skills/tamagui/SKILL.md +60 -0
- package/assets/skills/tanstack-query/SKILL.md +58 -0
- package/assets/skills/ticket-intake/SKILL.md +55 -0
- package/assets/skills/typescript/SKILL.md +134 -0
- package/assets/skills/verify-before-done/SKILL.md +111 -0
- package/assets/skills/webapp-rebuilder/SKILL.md +48 -0
- package/assets/skills/webapp-rebuilder/assets/charter-template.md +46 -0
- package/assets/skills/webapp-rebuilder/references/workflow.md +47 -0
- package/assets/skills/winston-logging/SKILL.md +61 -0
- package/assets/skills/work-unit-commits/SKILL.md +84 -0
- package/assets/skills/zod-4/SKILL.md +210 -0
- package/assets/skills/zustand-5/SKILL.md +216 -0
- package/bin/argos.js +6 -0
- package/dist/commands/adopt.d.ts +35 -0
- package/dist/commands/adopt.d.ts.map +1 -0
- package/dist/commands/adopt.js +347 -0
- package/dist/commands/adopt.js.map +1 -0
- package/dist/commands/doctor.d.ts +20 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +478 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/init.d.ts +32 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +358 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/remove.d.ts +62 -0
- package/dist/commands/remove.d.ts.map +1 -0
- package/dist/commands/remove.js +487 -0
- package/dist/commands/remove.js.map +1 -0
- package/dist/commands/workspace.d.ts +73 -0
- package/dist/commands/workspace.d.ts.map +1 -0
- package/dist/commands/workspace.js +354 -0
- package/dist/commands/workspace.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/assets.d.ts +29 -0
- package/dist/lib/assets.d.ts.map +1 -0
- package/dist/lib/assets.js +57 -0
- package/dist/lib/assets.js.map +1 -0
- package/dist/lib/atomic-write.d.ts +17 -0
- package/dist/lib/atomic-write.d.ts.map +1 -0
- package/dist/lib/atomic-write.js +41 -0
- package/dist/lib/atomic-write.js.map +1 -0
- package/dist/lib/backup.d.ts +11 -0
- package/dist/lib/backup.d.ts.map +1 -0
- package/dist/lib/backup.js +42 -0
- package/dist/lib/backup.js.map +1 -0
- package/dist/lib/config.d.ts +36 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +52 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/detect.d.ts +58 -0
- package/dist/lib/detect.d.ts.map +1 -0
- package/dist/lib/detect.js +330 -0
- package/dist/lib/detect.js.map +1 -0
- package/dist/lib/ficha.d.ts +10 -0
- package/dist/lib/ficha.d.ts.map +1 -0
- package/dist/lib/ficha.js +38 -0
- package/dist/lib/ficha.js.map +1 -0
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.d.ts.map +1 -0
- package/dist/lib/git.js +79 -0
- package/dist/lib/git.js.map +1 -0
- package/dist/lib/managed-files.d.ts +35 -0
- package/dist/lib/managed-files.d.ts.map +1 -0
- package/dist/lib/managed-files.js +97 -0
- package/dist/lib/managed-files.js.map +1 -0
- package/dist/lib/markers.d.ts +64 -0
- package/dist/lib/markers.d.ts.map +1 -0
- package/dist/lib/markers.js +157 -0
- package/dist/lib/markers.js.map +1 -0
- package/dist/lib/navori-import.d.ts +42 -0
- package/dist/lib/navori-import.d.ts.map +1 -0
- package/dist/lib/navori-import.js +65 -0
- package/dist/lib/navori-import.js.map +1 -0
- package/dist/lib/openclaw-agents.d.ts +53 -0
- package/dist/lib/openclaw-agents.d.ts.map +1 -0
- package/dist/lib/openclaw-agents.js +118 -0
- package/dist/lib/openclaw-agents.js.map +1 -0
- package/dist/lib/package-root.d.ts +11 -0
- package/dist/lib/package-root.d.ts.map +1 -0
- package/dist/lib/package-root.js +23 -0
- package/dist/lib/package-root.js.map +1 -0
- package/dist/lib/paths.d.ts +11 -0
- package/dist/lib/paths.d.ts.map +1 -0
- package/dist/lib/paths.js +16 -0
- package/dist/lib/paths.js.map +1 -0
- package/dist/lib/settings-merge.d.ts +125 -0
- package/dist/lib/settings-merge.d.ts.map +1 -0
- package/dist/lib/settings-merge.js +373 -0
- package/dist/lib/settings-merge.js.map +1 -0
- package/dist/lib/version.d.ts +3 -0
- package/dist/lib/version.d.ts.map +1 -0
- package/dist/lib/version.js +13 -0
- package/dist/lib/version.js.map +1 -0
- package/dist/lib/which.d.ts +8 -0
- package/dist/lib/which.d.ts.map +1 -0
- package/dist/lib/which.js +30 -0
- package/dist/lib/which.js.map +1 -0
- package/dist/lib/workspaces.d.ts +133 -0
- package/dist/lib/workspaces.d.ts.map +1 -0
- package/dist/lib/workspaces.js +241 -0
- package/dist/lib/workspaces.js.map +1 -0
- package/dist/lib/zod-messages.d.ts +4 -0
- package/dist/lib/zod-messages.d.ts.map +1 -0
- package/dist/lib/zod-messages.js +28 -0
- package/dist/lib/zod-messages.js.map +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
// Landscape (1920x1080) web-surface scene architecture.
|
|
2
|
+
// Replace PRODUCT_DOMAIN, copy placeholders, and capture filenames.
|
|
3
|
+
// Register: PromoWeb = benefit-led marketing; PromoDashboard = calm, outcome-led.
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import {
|
|
6
|
+
AbsoluteFill,
|
|
7
|
+
Img,
|
|
8
|
+
interpolate,
|
|
9
|
+
spring,
|
|
10
|
+
staticFile,
|
|
11
|
+
useCurrentFrame,
|
|
12
|
+
useVideoConfig,
|
|
13
|
+
} from 'remotion';
|
|
14
|
+
import {TransitionSeries, linearTiming} from '@remotion/transitions';
|
|
15
|
+
import {fade} from '@remotion/transitions/fade';
|
|
16
|
+
import {slide} from '@remotion/transitions/slide';
|
|
17
|
+
import {colors, fonts, radialBackground} from './theme';
|
|
18
|
+
|
|
19
|
+
const PRODUCT_DOMAIN = 'app.product.com';
|
|
20
|
+
|
|
21
|
+
export const DURATIONS = {
|
|
22
|
+
intro: 100,
|
|
23
|
+
feature1: 140,
|
|
24
|
+
feature2: 140,
|
|
25
|
+
feature3: 140,
|
|
26
|
+
outro: 120,
|
|
27
|
+
};
|
|
28
|
+
const TRANSITION_FRAMES = 12;
|
|
29
|
+
export const TOTAL_DURATION =
|
|
30
|
+
Object.values(DURATIONS).reduce((a, b) => a + b, 0) - 4 * TRANSITION_FRAMES;
|
|
31
|
+
|
|
32
|
+
const springIn = (frame: number, fps: number, delay = 0) =>
|
|
33
|
+
spring({frame: frame - delay, fps, config: {damping: 16}});
|
|
34
|
+
|
|
35
|
+
const Backdrop: React.FC<{children: React.ReactNode}> = ({children}) => (
|
|
36
|
+
<AbsoluteFill style={{background: radialBackground}}>{children}</AbsoluteFill>
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const Watermark: React.FC<{word: string}> = ({word}) => {
|
|
40
|
+
const frame = useCurrentFrame();
|
|
41
|
+
const {durationInFrames} = useVideoConfig();
|
|
42
|
+
const drift = interpolate(frame, [0, durationInFrames], [0, -24]);
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
style={{
|
|
46
|
+
position: 'absolute',
|
|
47
|
+
bottom: 40,
|
|
48
|
+
left: -140,
|
|
49
|
+
fontFamily: fonts.display,
|
|
50
|
+
fontWeight: 700,
|
|
51
|
+
fontSize: 420,
|
|
52
|
+
textTransform: 'uppercase',
|
|
53
|
+
letterSpacing: 8,
|
|
54
|
+
whiteSpace: 'nowrap',
|
|
55
|
+
color: 'transparent',
|
|
56
|
+
WebkitTextStroke: '1px rgba(250,250,250,0.05)',
|
|
57
|
+
transform: `translateX(${drift}px)`,
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
{word}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Browser chrome window: traffic lights + URL pill + capture with Ken Burns.
|
|
66
|
+
const BrowserFrame: React.FC<{
|
|
67
|
+
src: string;
|
|
68
|
+
enterFrom: 'left' | 'right';
|
|
69
|
+
focusX?: string; // objectPosition x-target of the Ken Burns drift
|
|
70
|
+
}> = ({src, enterFrom, focusX = 'center'}) => {
|
|
71
|
+
const frame = useCurrentFrame();
|
|
72
|
+
const {fps, durationInFrames} = useVideoConfig();
|
|
73
|
+
const enter = springIn(frame, fps, 4);
|
|
74
|
+
|
|
75
|
+
const offset = interpolate(enter, [0, 1], [1, 0]);
|
|
76
|
+
const translateX = (enterFrom === 'left' ? -1200 : 1200) * offset;
|
|
77
|
+
const tilt = interpolate(enter, [0, 1], [enterFrom === 'left' ? 3 : -3, 0.8]);
|
|
78
|
+
const kenBurns = interpolate(frame, [0, durationInFrames], [1.0, 1.06]);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div
|
|
82
|
+
style={{
|
|
83
|
+
position: 'absolute',
|
|
84
|
+
top: '50%',
|
|
85
|
+
[enterFrom === 'left' ? 'left' : 'right']: 60,
|
|
86
|
+
width: '58%',
|
|
87
|
+
perspective: 1600,
|
|
88
|
+
transform: `translateY(-50%) translateX(${translateX}px)`,
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
<div
|
|
92
|
+
style={{
|
|
93
|
+
borderRadius: 16,
|
|
94
|
+
border: `1px solid ${colors.borderStrong}`,
|
|
95
|
+
backgroundColor: colors.surface,
|
|
96
|
+
overflow: 'hidden',
|
|
97
|
+
boxShadow: '0 40px 90px rgba(0,0,0,0.65)',
|
|
98
|
+
transform: `rotateY(${tilt}deg)`,
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
<div
|
|
102
|
+
style={{
|
|
103
|
+
display: 'flex',
|
|
104
|
+
alignItems: 'center',
|
|
105
|
+
gap: 16,
|
|
106
|
+
padding: '14px 20px',
|
|
107
|
+
backgroundColor: colors.panel,
|
|
108
|
+
borderBottom: `1px solid ${colors.border}`,
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
<div style={{display: 'flex', gap: 8}}>
|
|
112
|
+
{['#FF5F57', '#FEBC2E', '#28C840'].map((c) => (
|
|
113
|
+
<div
|
|
114
|
+
key={c}
|
|
115
|
+
style={{width: 12, height: 12, borderRadius: 6, backgroundColor: c}}
|
|
116
|
+
/>
|
|
117
|
+
))}
|
|
118
|
+
</div>
|
|
119
|
+
<div
|
|
120
|
+
style={{
|
|
121
|
+
flex: 1,
|
|
122
|
+
maxWidth: 420,
|
|
123
|
+
margin: '0 auto',
|
|
124
|
+
padding: '6px 18px',
|
|
125
|
+
borderRadius: 8,
|
|
126
|
+
backgroundColor: colors.bg,
|
|
127
|
+
fontFamily: fonts.body,
|
|
128
|
+
fontSize: 16,
|
|
129
|
+
color: colors.textMuted,
|
|
130
|
+
textAlign: 'center',
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
{PRODUCT_DOMAIN}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
<div style={{aspectRatio: '16 / 9', overflow: 'hidden'}}>
|
|
137
|
+
<Img
|
|
138
|
+
src={staticFile(src)}
|
|
139
|
+
style={{
|
|
140
|
+
width: '100%',
|
|
141
|
+
height: '100%',
|
|
142
|
+
objectFit: 'cover',
|
|
143
|
+
objectPosition: `${focusX} top`,
|
|
144
|
+
transform: `scale(${kenBurns})`,
|
|
145
|
+
}}
|
|
146
|
+
/>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Copy block on the opposite side, vertically centered.
|
|
154
|
+
const CopyBlock: React.FC<{
|
|
155
|
+
eyebrow: string;
|
|
156
|
+
headline: string;
|
|
157
|
+
body: string;
|
|
158
|
+
side: 'left' | 'right';
|
|
159
|
+
}> = ({eyebrow, headline, body, side}) => {
|
|
160
|
+
const frame = useCurrentFrame();
|
|
161
|
+
const {fps} = useVideoConfig();
|
|
162
|
+
const rows = [
|
|
163
|
+
{
|
|
164
|
+
text: eyebrow,
|
|
165
|
+
style: {
|
|
166
|
+
fontFamily: fonts.display,
|
|
167
|
+
fontWeight: 600,
|
|
168
|
+
fontSize: 26,
|
|
169
|
+
letterSpacing: 5,
|
|
170
|
+
textTransform: 'uppercase' as const,
|
|
171
|
+
color: colors.accent,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
text: headline,
|
|
176
|
+
style: {
|
|
177
|
+
fontFamily: fonts.display,
|
|
178
|
+
fontWeight: 700,
|
|
179
|
+
fontSize: 64,
|
|
180
|
+
letterSpacing: 2,
|
|
181
|
+
textTransform: 'uppercase' as const,
|
|
182
|
+
color: colors.textPrimary,
|
|
183
|
+
lineHeight: 1.08,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
text: body,
|
|
188
|
+
style: {
|
|
189
|
+
fontFamily: fonts.body,
|
|
190
|
+
fontWeight: 500,
|
|
191
|
+
fontSize: 28,
|
|
192
|
+
color: colors.textSecondary,
|
|
193
|
+
lineHeight: 1.4,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
return (
|
|
198
|
+
<div
|
|
199
|
+
style={{
|
|
200
|
+
position: 'absolute',
|
|
201
|
+
top: '50%',
|
|
202
|
+
[side]: 90,
|
|
203
|
+
width: '32%',
|
|
204
|
+
transform: 'translateY(-50%)',
|
|
205
|
+
display: 'flex',
|
|
206
|
+
flexDirection: 'column',
|
|
207
|
+
gap: 22,
|
|
208
|
+
}}
|
|
209
|
+
>
|
|
210
|
+
{rows.map((row, i) => {
|
|
211
|
+
const s = springIn(frame, fps, 8 + i * 5);
|
|
212
|
+
return (
|
|
213
|
+
<div
|
|
214
|
+
key={row.text}
|
|
215
|
+
style={{
|
|
216
|
+
...row.style,
|
|
217
|
+
opacity: s,
|
|
218
|
+
transform: `translateY(${interpolate(s, [0, 1], [36, 0])}px)`,
|
|
219
|
+
}}
|
|
220
|
+
>
|
|
221
|
+
{row.text}
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
})}
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const FeatureScene: React.FC<{
|
|
230
|
+
watermark: string;
|
|
231
|
+
eyebrow: string;
|
|
232
|
+
headline: string;
|
|
233
|
+
body: string;
|
|
234
|
+
capture: string;
|
|
235
|
+
browserSide: 'left' | 'right';
|
|
236
|
+
}> = ({watermark, eyebrow, headline, body, capture, browserSide}) => (
|
|
237
|
+
<Backdrop>
|
|
238
|
+
<Watermark word={watermark} />
|
|
239
|
+
<BrowserFrame src={capture} enterFrom={browserSide} />
|
|
240
|
+
<CopyBlock
|
|
241
|
+
eyebrow={eyebrow}
|
|
242
|
+
headline={headline}
|
|
243
|
+
body={body}
|
|
244
|
+
side={browserSide === 'left' ? 'right' : 'left'}
|
|
245
|
+
/>
|
|
246
|
+
</Backdrop>
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const Lockup: React.FC<{speed?: number}> = ({speed = 1}) => {
|
|
250
|
+
const frame = useCurrentFrame();
|
|
251
|
+
const {fps} = useVideoConfig();
|
|
252
|
+
const logoS = springIn(frame, fps, 2 * speed);
|
|
253
|
+
const titleS = springIn(frame, fps, 14 * speed);
|
|
254
|
+
const barS = springIn(frame, fps, 24 * speed);
|
|
255
|
+
return (
|
|
256
|
+
<div style={{display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 24}}>
|
|
257
|
+
<Img
|
|
258
|
+
src={staticFile('logo.png')}
|
|
259
|
+
style={{
|
|
260
|
+
width: 240,
|
|
261
|
+
height: 240,
|
|
262
|
+
opacity: logoS,
|
|
263
|
+
transform: `scale(${interpolate(logoS, [0, 1], [0.7, 1])})`,
|
|
264
|
+
}}
|
|
265
|
+
/>
|
|
266
|
+
<div
|
|
267
|
+
style={{
|
|
268
|
+
fontFamily: fonts.display,
|
|
269
|
+
fontWeight: 700,
|
|
270
|
+
fontSize: 96,
|
|
271
|
+
letterSpacing: 4,
|
|
272
|
+
textTransform: 'uppercase',
|
|
273
|
+
color: colors.textPrimary,
|
|
274
|
+
lineHeight: 1,
|
|
275
|
+
opacity: titleS,
|
|
276
|
+
transform: `translateY(${interpolate(titleS, [0, 1], [60, 0])}px)`,
|
|
277
|
+
}}
|
|
278
|
+
>
|
|
279
|
+
APP NAME
|
|
280
|
+
</div>
|
|
281
|
+
<div
|
|
282
|
+
style={{
|
|
283
|
+
width: 160,
|
|
284
|
+
height: 6,
|
|
285
|
+
backgroundColor: colors.accent,
|
|
286
|
+
transformOrigin: 'left',
|
|
287
|
+
transform: `scaleX(${barS})`,
|
|
288
|
+
}}
|
|
289
|
+
/>
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const IntroScene: React.FC = () => (
|
|
295
|
+
<AbsoluteFill
|
|
296
|
+
style={{backgroundColor: colors.bg, justifyContent: 'center', alignItems: 'center'}}
|
|
297
|
+
>
|
|
298
|
+
<Lockup />
|
|
299
|
+
</AbsoluteFill>
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
const OutroScene: React.FC = () => {
|
|
303
|
+
const frame = useCurrentFrame();
|
|
304
|
+
const {fps} = useVideoConfig();
|
|
305
|
+
const ctaS = springIn(frame, fps, 22);
|
|
306
|
+
return (
|
|
307
|
+
<AbsoluteFill
|
|
308
|
+
style={{backgroundColor: colors.bg, justifyContent: 'center', alignItems: 'center'}}
|
|
309
|
+
>
|
|
310
|
+
<div style={{display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 30}}>
|
|
311
|
+
<Lockup speed={0.6} />
|
|
312
|
+
<div
|
|
313
|
+
style={{
|
|
314
|
+
fontFamily: fonts.body,
|
|
315
|
+
fontWeight: 600,
|
|
316
|
+
fontSize: 34,
|
|
317
|
+
color: colors.textPrimary,
|
|
318
|
+
opacity: ctaS,
|
|
319
|
+
transform: `translateY(${interpolate(ctaS, [0, 1], [26, 0])}px)`,
|
|
320
|
+
}}
|
|
321
|
+
>
|
|
322
|
+
{PRODUCT_DOMAIN}
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
</AbsoluteFill>
|
|
326
|
+
);
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
export const PromoWeb: React.FC = () => {
|
|
330
|
+
const timing = linearTiming({durationInFrames: TRANSITION_FRAMES});
|
|
331
|
+
return (
|
|
332
|
+
<TransitionSeries>
|
|
333
|
+
<TransitionSeries.Sequence durationInFrames={DURATIONS.intro}>
|
|
334
|
+
<IntroScene />
|
|
335
|
+
</TransitionSeries.Sequence>
|
|
336
|
+
<TransitionSeries.Transition presentation={fade()} timing={timing} />
|
|
337
|
+
|
|
338
|
+
<TransitionSeries.Sequence durationInFrames={DURATIONS.feature1}>
|
|
339
|
+
<FeatureScene
|
|
340
|
+
watermark="FEATURE WORD"
|
|
341
|
+
eyebrow="EYEBROW"
|
|
342
|
+
headline="HEADLINE"
|
|
343
|
+
body="One-sentence body copy."
|
|
344
|
+
capture="web/01-home.png"
|
|
345
|
+
browserSide="right"
|
|
346
|
+
/>
|
|
347
|
+
</TransitionSeries.Sequence>
|
|
348
|
+
<TransitionSeries.Transition
|
|
349
|
+
presentation={slide({direction: 'from-right'})}
|
|
350
|
+
timing={timing}
|
|
351
|
+
/>
|
|
352
|
+
|
|
353
|
+
<TransitionSeries.Sequence durationInFrames={DURATIONS.feature2}>
|
|
354
|
+
<FeatureScene
|
|
355
|
+
watermark="FEATURE WORD"
|
|
356
|
+
eyebrow="EYEBROW"
|
|
357
|
+
headline="HEADLINE"
|
|
358
|
+
body="One-sentence body copy."
|
|
359
|
+
capture="web/02-feature.png"
|
|
360
|
+
browserSide="left"
|
|
361
|
+
/>
|
|
362
|
+
</TransitionSeries.Sequence>
|
|
363
|
+
<TransitionSeries.Transition
|
|
364
|
+
presentation={slide({direction: 'from-left'})}
|
|
365
|
+
timing={timing}
|
|
366
|
+
/>
|
|
367
|
+
|
|
368
|
+
<TransitionSeries.Sequence durationInFrames={DURATIONS.feature3}>
|
|
369
|
+
<FeatureScene
|
|
370
|
+
watermark="FEATURE WORD"
|
|
371
|
+
eyebrow="EYEBROW"
|
|
372
|
+
headline="HEADLINE"
|
|
373
|
+
body="One-sentence body copy."
|
|
374
|
+
capture="web/03-feature.png"
|
|
375
|
+
browserSide="right"
|
|
376
|
+
/>
|
|
377
|
+
</TransitionSeries.Sequence>
|
|
378
|
+
<TransitionSeries.Transition presentation={fade()} timing={timing} />
|
|
379
|
+
|
|
380
|
+
<TransitionSeries.Sequence durationInFrames={DURATIONS.outro}>
|
|
381
|
+
<OutroScene />
|
|
382
|
+
</TransitionSeries.Sequence>
|
|
383
|
+
</TransitionSeries>
|
|
384
|
+
);
|
|
385
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Playwright capture script for web-surface promos.
|
|
2
|
+
// Run from tools/promo/: PROMO_EMAIL=... PROMO_PASSWORD=... npx tsx capture/capture.ts
|
|
3
|
+
// Requires: npx playwright install chromium (once).
|
|
4
|
+
import {chromium} from 'playwright';
|
|
5
|
+
|
|
6
|
+
const BASE_URL = process.env.PROMO_URL ?? 'http://localhost:5173';
|
|
7
|
+
const EMAIL = process.env.PROMO_EMAIL;
|
|
8
|
+
const PASSWORD = process.env.PROMO_PASSWORD;
|
|
9
|
+
// Multi-tenant products: seed an isolated demo tenant and scope every
|
|
10
|
+
// navigation to it — credentials alone do not make the state predictable.
|
|
11
|
+
const TENANT = process.env.PROMO_TENANT;
|
|
12
|
+
|
|
13
|
+
// Loading UI that must be GONE before any capture (adapt to the app).
|
|
14
|
+
const LOADING_SELECTORS = ['[data-loading]', '.skeleton', '[role="progressbar"]'];
|
|
15
|
+
|
|
16
|
+
// One entry per feature scene. `readySelector` must match REAL data,
|
|
17
|
+
// not a skeleton — captures with spinners are a quality failure.
|
|
18
|
+
const SHOTS: Array<{path: string; readySelector: string; out: string}> = [
|
|
19
|
+
{path: '/', readySelector: 'main', out: '01-home.png'},
|
|
20
|
+
// {path: '/students', readySelector: 'table tbody tr', out: '02-students.png'},
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
async function main() {
|
|
24
|
+
// headless is mandatory: a headed browser hangs the run in terminal envs.
|
|
25
|
+
const browser = await chromium.launch({headless: true});
|
|
26
|
+
const context = await browser.newContext({
|
|
27
|
+
viewport: {width: 1920, height: 1080},
|
|
28
|
+
deviceScaleFactor: 2,
|
|
29
|
+
});
|
|
30
|
+
const page = await context.newPage();
|
|
31
|
+
|
|
32
|
+
// Stabilize captures: kill CSS animations and transitions.
|
|
33
|
+
await page.addInitScript(() => {
|
|
34
|
+
const style = document.createElement('style');
|
|
35
|
+
style.textContent = '*{animation:none!important;transition:none!important}';
|
|
36
|
+
document.addEventListener('DOMContentLoaded', () =>
|
|
37
|
+
document.head.appendChild(style)
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Login (dashboard surfaces). Wait for a post-login element, never the redirect.
|
|
42
|
+
if (EMAIL && PASSWORD) {
|
|
43
|
+
await page.goto(`${BASE_URL}/login`);
|
|
44
|
+
await page.fill('input[type="email"]', EMAIL);
|
|
45
|
+
await page.fill('input[type="password"]', PASSWORD);
|
|
46
|
+
await page.click('button[type="submit"]');
|
|
47
|
+
await page.waitForSelector('[data-authenticated], main', {timeout: 15000});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const shot of SHOTS) {
|
|
51
|
+
const prefix = TENANT ? `/t/${TENANT}` : ''; // adapt to the app's tenant routing
|
|
52
|
+
await page.goto(`${BASE_URL}${prefix}${shot.path}`);
|
|
53
|
+
await page.waitForLoadState('networkidle');
|
|
54
|
+
// networkidle is not enough on async dashboards: also require every
|
|
55
|
+
// loading indicator to unmount, then a real-data selector.
|
|
56
|
+
for (const sel of LOADING_SELECTORS) {
|
|
57
|
+
await page.waitForSelector(sel, {state: 'detached', timeout: 15000}).catch(() => {});
|
|
58
|
+
}
|
|
59
|
+
await page.waitForSelector(shot.readySelector, {timeout: 15000});
|
|
60
|
+
await page.screenshot({path: `public/web/${shot.out}`});
|
|
61
|
+
console.log(`captured ${shot.out}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await browser.close();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
main().catch((err) => {
|
|
68
|
+
console.error(err);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "APP_SLUG-promo",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Remotion promotional videos for APP_NAME",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"studio": "remotion studio",
|
|
8
|
+
"capture": "tsx capture/capture.ts",
|
|
9
|
+
"render:web": "remotion render PromoWeb out/promo-web.mp4",
|
|
10
|
+
"render:dashboard": "remotion render PromoDashboard out/promo-dashboard.mp4"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@remotion/cli": "4.0.496",
|
|
14
|
+
"@remotion/google-fonts": "4.0.496",
|
|
15
|
+
"@remotion/transitions": "4.0.496",
|
|
16
|
+
"react": "^19.0.0",
|
|
17
|
+
"react-dom": "^19.0.0",
|
|
18
|
+
"remotion": "4.0.496"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/react": "^19.0.0",
|
|
22
|
+
"playwright": "^1.50.0",
|
|
23
|
+
"tsx": "^4.19.0",
|
|
24
|
+
"typescript": "^5.6.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Promo Video (Web Surfaces) — Runbook
|
|
2
|
+
|
|
3
|
+
Extends the `promo-video` pipeline for landscape web-surface promos. Shared fundamentals (workspace isolation, timeline math, foreground renders, verify protocol) are identical — this file covers only what differs.
|
|
4
|
+
|
|
5
|
+
## §1 Capture protocol (Playwright)
|
|
6
|
+
|
|
7
|
+
- Adapt `assets/capture.template.ts`. Run with `npx playwright install chromium` done once, then `npx tsx capture.ts` (or `node` after tsc) from `tools/promo/` — FOREGROUND, and `headless: true` set explicitly on `chromium.launch()`: in a terminal environment a headed browser hangs the agent waiting for a GUI.
|
|
8
|
+
- Viewport fixed at 1920×1080, `deviceScaleFactor: 2` → 3840×2160 PNGs; the browser frame downscales them crisp.
|
|
9
|
+
- Target: local dev server with seeded demo data (preferred — deterministic) or the deployed URL. 4–6 captures per surface, each a distinct feature screen.
|
|
10
|
+
- Multi-tenant products: seeding credentials is not enough — the STATE must be predictable. Seed an isolated demo tenant and pass its ID via env (`PROMO_TENANT`); the script scopes every navigation to it. This guarantees no empty states and no leakage of another tenant's data into the capture.
|
|
11
|
+
- Dashboard auth: credentials from env (`PROMO_EMAIL` / `PROMO_PASSWORD`), never hardcoded. Sequence per capture: authenticate → wait for the state to hydrate (`networkidle` AND spinners/skeletons UNMOUNTED — wait `state: 'detached'` on known loading selectors — AND a real-data selector present) → snap. Async dashboards (tables, metrics, payments) render piecemeal; `networkidle` alone still shows skeletons.
|
|
12
|
+
- Hide flaky UI before capture: dismiss toasts/cookie banners, `page.addStyleTag` to disable CSS animations (`*{animation:none!important;transition:none!important}`) so captures are stable.
|
|
13
|
+
- Motion variant: `browser.newContext({recordVideo: {dir, size}})`, script the flow slowly (deliberate 800ms pauses between actions), close context to flush the webm, convert to mp4 with ffmpeg, embed via `<OffthreadVideo>`.
|
|
14
|
+
|
|
15
|
+
## §2 Project layout (multi-composition)
|
|
16
|
+
|
|
17
|
+
If `tools/promo/` exists (mobile promo), extend it — one Remotion project, several compositions:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
tools/promo/
|
|
21
|
+
├── capture/capture.ts # Playwright capture script
|
|
22
|
+
├── public/ # mobile assets (existing)
|
|
23
|
+
├── public/web/ # web captures
|
|
24
|
+
├── src/Root.tsx # registers Promo + PromoWeb + PromoDashboard
|
|
25
|
+
├── src/Promo.tsx # mobile (existing)
|
|
26
|
+
├── src/web/theme.ts # web-surface token bridge
|
|
27
|
+
├── src/web/PromoWeb.tsx
|
|
28
|
+
└── src/web/PromoDashboard.tsx
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Add `playwright` + `tsx` as devDependencies; add scripts `render:web` / `render:dashboard`; root scripts `promo:render:web` / `promo:render:dashboard` (cd form: `cd tools/promo && npm run render:web`). If scaffolding fresh, use `assets/package.template.json`.
|
|
32
|
+
|
|
33
|
+
### Per-surface theming via input props
|
|
34
|
+
|
|
35
|
+
To avoid rewriting `src/web/theme.ts` per surface, drive COLOR tokens through Remotion input props — one scene codebase, N surface renders:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// src/web/theme.ts
|
|
39
|
+
import {getInputProps} from 'remotion';
|
|
40
|
+
const base = {bg: '#0A0A0B', /* … */ accent: '#DC2626'};
|
|
41
|
+
const {colors: overrides} = getInputProps() as {colors?: Partial<typeof base>};
|
|
42
|
+
export const colors = {...base, ...overrides};
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Render with `npx remotion render PromoDashboard out/promo-dashboard.mp4 --props=./dashboard-tokens.json` where the JSON is `{"colors": {"accent": "#1D4ED8", …}}`. LIMIT: fonts cannot come from input props — `loadFont()` executes at module scope; font choices stay code-level per surface.
|
|
46
|
+
|
|
47
|
+
## §3 Scene system (landscape 1920×1080)
|
|
48
|
+
|
|
49
|
+
Same animation grammar as `promo-video` (springs damping 14–18, 12-frame transitions, staggered copy, watermark words) with landscape-specific composition:
|
|
50
|
+
|
|
51
|
+
- **Browser frame** instead of phone bezel (see `assets/browser-promo.template.tsx`): rounded ~16px window, top chrome bar with three traffic-light dots and a URL pill showing the REAL product domain, capture below, big soft shadow. Slight rotateY tilt optional and subtler than mobile (±3°).
|
|
52
|
+
- **Split layout**: copy block vertically centered on one side (~38% width), browser frame filling the other (~58%), alternating sides per scene. Watermark word behind, larger canvas → ~420px.
|
|
53
|
+
- **Ken Burns on captures**: slow scale 1.0→1.06 across the scene (linear interpolate is fine here — it is a drift, not a gesture), `objectPosition` aimed at the screen's key region. Wide screenshots carry motion this way instead of tilt-float.
|
|
54
|
+
- **Intro/outro**: same lockup pattern; outro CTA is the surface URL (e.g. `app.product.com`) instead of a store line.
|
|
55
|
+
- Default length 20–25s, 5–6 scenes.
|
|
56
|
+
|
|
57
|
+
## §4 Register per surface
|
|
58
|
+
|
|
59
|
+
| Surface | Audience | Copy register | Example headline shape |
|
|
60
|
+
|---|---|---|---|
|
|
61
|
+
| Web app (`PromoWeb`) | End users | Product marketing, benefit-led — same energy as the mobile promo | "YOUR PROGRESS, ANYWHERE" |
|
|
62
|
+
| Dashboard (`PromoDashboard`) | Operators/staff | Calm, outcome-led, zero hype; verbs about control and time saved | "EVERY STUDENT, ONE SCREEN" |
|
|
63
|
+
|
|
64
|
+
Dashboard scenes favor showing dense real data (tables, filters, detail panels) — density IS the selling point. Do not crop tables down to look "clean"; show the tool doing work.
|
|
65
|
+
|
|
66
|
+
## §5 Render + verify
|
|
67
|
+
|
|
68
|
+
Identical to `promo-video` §6, once per composition:
|
|
69
|
+
`npx remotion render PromoWeb out/promo-web.mp4` and `npx remotion render PromoDashboard out/promo-dashboard.mp4`, then ffprobe check (1920×1080, 30fps, expected duration) and ≥2 `remotion still` spot-checks per video (one feature scene, the outro).
|
|
70
|
+
|
|
71
|
+
## §6 Troubleshooting
|
|
72
|
+
|
|
73
|
+
| Symptom | Cause / Fix |
|
|
74
|
+
|---|---|
|
|
75
|
+
| Captures blurry in frame | Missing `deviceScaleFactor: 2`, or upscaling a 1x capture |
|
|
76
|
+
| Dashboard capture shows skeletons/spinners | Waited on `networkidle` only — add explicit selector waits for real data |
|
|
77
|
+
| Captures differ between runs | Animations not disabled, toasts present, or non-seeded data — §1 |
|
|
78
|
+
| Login flow flaky in capture script | Race on redirect: wait for a post-login selector, not the navigation event |
|
|
79
|
+
| Second Remotion project appeared | Wrong — consolidate into `tools/promo/` (one project, many compositions) |
|
|
80
|
+
| Playwright browser missing at capture | Run `npx playwright install chromium` once inside `tools/promo/` |
|
|
81
|
+
| Login returns `invalid_credentials` despite a correct password hash | Directly-seeded `auth.users` rows are often incomplete for GoTrue: NULL `instance_id`, no matching `auth.identities` row, and NULL string-token columns (GoTrue errors "converting NULL to string is unsupported"). Fix in the local DB before capture: set `instance_id` to the zero UUID, insert an email identity per user, and coalesce token columns to `''`. |
|
|
82
|
+
| Capture script hangs in terminal | Headed browser waiting for a GUI — enforce `headless: true` on launch |
|
|
83
|
+
| Dashboard capture shows empty states | Tenant not seeded/scoped — seed the demo tenant and pass `PROMO_TENANT` (§1) |
|
|
84
|
+
| Colors ignored at render | `--props` file path wrong or `getInputProps()` read after module init in the wrong file — override in `theme.ts` itself (§2) |
|