clipwise 0.7.0 → 0.7.2

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/dist/cli/index.js CHANGED
@@ -11,7 +11,7 @@ var __export = (target, all) => {
11
11
 
12
12
  // src/script/types.ts
13
13
  import { z } from "zod";
14
- var SafeSelectorSchema, NavigateActionSchema, ClickActionSchema, TypeActionSchema, ScrollActionSchema, WaitActionSchema, HoverActionSchema, ScreenshotActionSchema, WaitForSelectorActionSchema, WaitForNavigationActionSchema, WaitForURLActionSchema, WaitForFunctionActionSchema, WaitForResponseActionSchema, SmartWaitActionSchema, StepActionSchema, ZoomIntensitySchema, AutoZoomConfigSchema, ZoomEffectSchema, CursorEffectSchema, BackgroundSchema, DeviceFrameSchema, SpeedRampConfigSchema, SmartSpeedConfigSchema, KeystrokeConfigSchema, WatermarkConfigSchema, EffectsConfigSchema, OutputConfigSchema, StepEffectsOverrideSchema, TransitionTypeSchema, StepSchema, AudioConfigSchema, ScenarioSchema;
14
+ var SafeSelectorSchema, NavigateActionSchema, ClickActionSchema, TypeActionSchema, ScrollActionSchema, WaitActionSchema, HoverActionSchema, ScreenshotActionSchema, WaitForSelectorActionSchema, WaitForNavigationActionSchema, WaitForURLActionSchema, WaitForFunctionActionSchema, WaitForResponseActionSchema, SmartWaitActionSchema, StepActionSchema, ZoomIntensitySchema, AutoZoomConfigSchema, ZoomEffectSchema, CursorEffectSchema, BackgroundSchema, DeviceFrameSchema, SpeedRampConfigSchema, SmartSpeedConfigSchema, KeystrokeConfigSchema, WatermarkConfigSchema, EffectsConfigSchema, OutputConfigSchema, StepEffectsOverrideSchema, TransitionTypeSchema, StepSchema, AudioConfigSchema, AuthConfigSchema, ScenarioSchema;
15
15
  var init_types = __esm({
16
16
  "src/script/types.ts"() {
17
17
  "use strict";
@@ -63,29 +63,41 @@ var init_types = __esm({
63
63
  action: z.literal("waitForSelector"),
64
64
  selector: SafeSelectorSchema,
65
65
  state: z.enum(["visible", "attached", "hidden"]).default("visible"),
66
- timeout: z.number().min(0).default(15e3)
66
+ timeout: z.number().min(0).default(15e3),
67
+ /** 대기 중 프레임 연속 캡처 (로딩 애니메이션 보존). */
68
+ captureWhileWaiting: z.boolean().default(false),
69
+ /** captureWhileWaiting 사용 시 출력 영상 속도 배율 (1-32). */
70
+ displaySpeed: z.number().min(1).max(32).default(8)
67
71
  });
68
72
  WaitForNavigationActionSchema = z.object({
69
73
  action: z.literal("waitForNavigation"),
70
74
  waitUntil: z.enum(["load", "domcontentloaded", "networkidle"]).default("networkidle"),
71
- timeout: z.number().min(0).default(15e3)
75
+ timeout: z.number().min(0).default(15e3),
76
+ captureWhileWaiting: z.boolean().default(false),
77
+ displaySpeed: z.number().min(1).max(32).default(8)
72
78
  });
73
79
  WaitForURLActionSchema = z.object({
74
80
  action: z.literal("waitForURL"),
75
81
  url: z.string().min(1),
76
- timeout: z.number().min(0).default(15e3)
82
+ timeout: z.number().min(0).default(15e3),
83
+ captureWhileWaiting: z.boolean().default(false),
84
+ displaySpeed: z.number().min(1).max(32).default(8)
77
85
  });
78
86
  WaitForFunctionActionSchema = z.object({
79
87
  action: z.literal("waitForFunction"),
80
88
  expression: z.string().min(1),
81
89
  polling: z.union([z.literal("raf"), z.number().min(0)]).default("raf"),
82
- timeout: z.number().min(0).default(3e4)
90
+ timeout: z.number().min(0).default(3e4),
91
+ captureWhileWaiting: z.boolean().default(false),
92
+ displaySpeed: z.number().min(1).max(32).default(8)
83
93
  });
84
94
  WaitForResponseActionSchema = z.object({
85
95
  action: z.literal("waitForResponse"),
86
96
  url: z.string().min(1),
87
97
  status: z.number().min(100).max(599).optional(),
88
- timeout: z.number().min(0).default(3e4)
98
+ timeout: z.number().min(0).default(3e4),
99
+ captureWhileWaiting: z.boolean().default(false),
100
+ displaySpeed: z.number().min(1).max(32).default(8)
89
101
  });
90
102
  SmartWaitActionSchema = z.object({
91
103
  action: z.literal("smartWait"),
@@ -280,6 +292,22 @@ var init_types = __esm({
280
292
  /** Fade-out duration in milliseconds. */
281
293
  fadeOut: z.number().min(0).default(0)
282
294
  });
295
+ AuthConfigSchema = z.object({
296
+ /** Path to a Playwright storageState JSON file (cookies + localStorage). */
297
+ storageState: z.string().optional(),
298
+ /** Inline cookie definitions (applied after storageState if both specified). */
299
+ cookies: z.array(
300
+ z.object({
301
+ name: z.string(),
302
+ value: z.string(),
303
+ domain: z.string(),
304
+ path: z.string().default("/"),
305
+ httpOnly: z.boolean().default(false),
306
+ secure: z.boolean().default(false),
307
+ sameSite: z.enum(["Strict", "Lax", "None"]).default("Lax")
308
+ })
309
+ ).optional()
310
+ });
283
311
  ScenarioSchema = z.object({
284
312
  name: z.string(),
285
313
  description: z.string().optional(),
@@ -287,6 +315,8 @@ var init_types = __esm({
287
315
  width: z.number().default(1280),
288
316
  height: z.number().default(800)
289
317
  }).default({}),
318
+ /** Optional authentication — restores browser session for logged-in pages. */
319
+ auth: AuthConfigSchema.optional(),
290
320
  effects: EffectsConfigSchema.default({}),
291
321
  output: OutputConfigSchema.default({}),
292
322
  /** Optional audio narration — muxed into MP4 output. */
@@ -567,9 +597,16 @@ var ClipwiseRecorder = class {
567
597
  this.cursorSpeed = scenario.effects.cursor.speed;
568
598
  this.loaderDetectionEnabled = scenario.effects.smartSpeed?.enabled ?? false;
569
599
  this.browser = await chromium.launch({ headless: true });
570
- this.context = await this.browser.newContext({
600
+ const contextOptions = {
571
601
  viewport: this.viewport
572
- });
602
+ };
603
+ if (scenario.auth?.storageState) {
604
+ contextOptions.storageState = scenario.auth.storageState;
605
+ }
606
+ this.context = await this.browser.newContext(contextOptions);
607
+ if (scenario.auth?.cookies?.length) {
608
+ await this.context.addCookies(scenario.auth.cookies);
609
+ }
573
610
  this.page = await this.context.newPage();
574
611
  this.rawFrames = [];
575
612
  this.cursorTimeline = [];
@@ -892,6 +929,31 @@ var ClipwiseRecorder = class {
892
929
  }
893
930
  }
894
931
  }
932
+ /**
933
+ * 조건 대기 중 프레임을 연속 캡처하는 헬퍼.
934
+ * smartWait과 동일한 패턴: isWaitingPhase 플래그 + forceRepaint 루프를 조건 promise와 병렬 실행.
935
+ */
936
+ async waitForConditionWithCapture(conditionPromise, displaySpeed) {
937
+ this.isWaitingPhase = true;
938
+ this.currentDisplaySpeed = displaySpeed;
939
+ try {
940
+ let waitDone = false;
941
+ const repaintLoop = (async () => {
942
+ let toggle = false;
943
+ while (!waitDone && this.isCapturing && this.page) {
944
+ await this.forceRepaint(toggle);
945
+ toggle = !toggle;
946
+ await new Promise((r) => setTimeout(r, REPAINT_INTERVAL_MS));
947
+ }
948
+ })();
949
+ await conditionPromise;
950
+ waitDone = true;
951
+ await repaintLoop;
952
+ } finally {
953
+ this.isWaitingPhase = false;
954
+ this.currentDisplaySpeed = void 0;
955
+ }
956
+ }
895
957
  /**
896
958
  * Pre-register waitForResponse listeners at the start of each step.
897
959
  * This ensures the listener is active before any preceding action
@@ -982,6 +1044,17 @@ var ClipwiseRecorder = class {
982
1044
  lastClickRefresh = now;
983
1045
  }
984
1046
  }
1047
+ await this.page.evaluate((sel) => {
1048
+ const el = document.querySelector(sel);
1049
+ if (!el) return;
1050
+ const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
1051
+ const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
1052
+ if (setter) {
1053
+ setter.call(el, el.value);
1054
+ el.dispatchEvent(new Event("input", { bubbles: true }));
1055
+ el.dispatchEvent(new Event("change", { bubbles: true }));
1056
+ }
1057
+ }, action.selector);
985
1058
  this.clickTimeline.push({
986
1059
  position: { ...inputTarget },
987
1060
  timestamp: Date.now()
@@ -1055,83 +1128,89 @@ var ClipwiseRecorder = class {
1055
1128
  }
1056
1129
  case "waitForSelector": {
1057
1130
  const locator = this.page.locator(action.selector).first();
1058
- await locator.waitFor({ state: action.state, timeout: action.timeout });
1131
+ const selectorPromise = locator.waitFor({ state: action.state, timeout: action.timeout });
1132
+ if (action.captureWhileWaiting) {
1133
+ await this.waitForConditionWithCapture(selectorPromise, action.displaySpeed);
1134
+ } else {
1135
+ await selectorPromise;
1136
+ }
1059
1137
  break;
1060
1138
  }
1061
1139
  case "waitForNavigation": {
1062
- await this.page.waitForLoadState(action.waitUntil, { timeout: action.timeout });
1140
+ const navPromise = this.page.waitForLoadState(action.waitUntil, { timeout: action.timeout });
1141
+ if (action.captureWhileWaiting) {
1142
+ await this.waitForConditionWithCapture(navPromise, action.displaySpeed);
1143
+ } else {
1144
+ await navPromise;
1145
+ }
1063
1146
  break;
1064
1147
  }
1065
1148
  case "waitForURL": {
1066
- await this.page.waitForURL(action.url, { timeout: action.timeout });
1149
+ const urlPromise = this.page.waitForURL(action.url, { timeout: action.timeout });
1150
+ if (action.captureWhileWaiting) {
1151
+ await this.waitForConditionWithCapture(urlPromise, action.displaySpeed);
1152
+ } else {
1153
+ await urlPromise;
1154
+ }
1067
1155
  break;
1068
1156
  }
1069
1157
  case "waitForFunction": {
1070
- await this.page.waitForFunction(action.expression, void 0, {
1158
+ const fnPromise = this.page.waitForFunction(action.expression, void 0, {
1071
1159
  polling: action.polling,
1072
1160
  timeout: action.timeout
1073
1161
  });
1162
+ if (action.captureWhileWaiting) {
1163
+ await this.waitForConditionWithCapture(fnPromise, action.displaySpeed);
1164
+ } else {
1165
+ await fnPromise;
1166
+ }
1074
1167
  break;
1075
1168
  }
1076
1169
  case "waitForResponse": {
1077
1170
  const pending = this.pendingResponsePromises.get(actionIndex);
1078
1171
  if (pending) {
1079
- await pending;
1172
+ if (action.captureWhileWaiting) {
1173
+ await this.waitForConditionWithCapture(pending, action.displaySpeed);
1174
+ } else {
1175
+ await pending;
1176
+ }
1080
1177
  }
1081
1178
  break;
1082
1179
  }
1083
1180
  case "smartWait": {
1084
- this.isWaitingPhase = true;
1085
- this.currentDisplaySpeed = action.displaySpeed;
1086
- try {
1087
- let conditionPromise;
1088
- switch (action.until) {
1089
- case "networkIdle":
1090
- conditionPromise = this.page.waitForLoadState("networkidle", { timeout: action.timeout });
1091
- break;
1092
- case "selector":
1093
- conditionPromise = action.selector ? this.page.locator(action.selector).first().waitFor({ state: "visible", timeout: action.timeout }) : Promise.resolve();
1094
- break;
1095
- case "domStable":
1096
- conditionPromise = this.page.waitForFunction(
1097
- () => new Promise((resolve2) => {
1098
- let timer;
1099
- const observer = new MutationObserver(() => {
1100
- clearTimeout(timer);
1101
- timer = setTimeout(() => {
1102
- observer.disconnect();
1103
- resolve2(true);
1104
- }, 500);
1105
- });
1106
- observer.observe(document.body, { childList: true, subtree: true, attributes: true });
1181
+ let conditionPromise;
1182
+ switch (action.until) {
1183
+ case "networkIdle":
1184
+ conditionPromise = this.page.waitForLoadState("networkidle", { timeout: action.timeout });
1185
+ break;
1186
+ case "selector":
1187
+ conditionPromise = action.selector ? this.page.locator(action.selector).first().waitFor({ state: "visible", timeout: action.timeout }) : Promise.resolve();
1188
+ break;
1189
+ case "domStable":
1190
+ conditionPromise = this.page.waitForFunction(
1191
+ () => new Promise((resolve2) => {
1192
+ let timer;
1193
+ const observer = new MutationObserver(() => {
1194
+ clearTimeout(timer);
1107
1195
  timer = setTimeout(() => {
1108
1196
  observer.disconnect();
1109
1197
  resolve2(true);
1110
1198
  }, 500);
1111
- }),
1112
- void 0,
1113
- { timeout: action.timeout }
1114
- );
1115
- break;
1116
- default:
1117
- conditionPromise = Promise.resolve();
1118
- }
1119
- let waitDone = false;
1120
- const repaintLoop = (async () => {
1121
- let toggle = false;
1122
- while (!waitDone && this.isCapturing && this.page) {
1123
- await this.forceRepaint(toggle);
1124
- toggle = !toggle;
1125
- await new Promise((r) => setTimeout(r, REPAINT_INTERVAL_MS));
1126
- }
1127
- })();
1128
- await conditionPromise;
1129
- waitDone = true;
1130
- await repaintLoop;
1131
- } finally {
1132
- this.isWaitingPhase = false;
1133
- this.currentDisplaySpeed = void 0;
1199
+ });
1200
+ observer.observe(document.body, { childList: true, subtree: true, attributes: true });
1201
+ timer = setTimeout(() => {
1202
+ observer.disconnect();
1203
+ resolve2(true);
1204
+ }, 500);
1205
+ }),
1206
+ void 0,
1207
+ { timeout: action.timeout }
1208
+ );
1209
+ break;
1210
+ default:
1211
+ conditionPromise = Promise.resolve();
1134
1212
  }
1213
+ await this.waitForConditionWithCapture(conditionPromise, action.displaySpeed);
1135
1214
  break;
1136
1215
  }
1137
1216
  }
@@ -1906,6 +1985,44 @@ import sharp5 from "sharp";
1906
1985
  function escapeXml(s) {
1907
1986
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1908
1987
  }
1988
+ function isCJK(ch) {
1989
+ const code = ch.codePointAt(0) ?? 0;
1990
+ return code >= 4352 && code <= 4607 || // 한글 자모
1991
+ code >= 11904 && code <= 40959 || // CJK 부수, 한자
1992
+ code >= 44032 && code <= 55215 || // 한글 음절
1993
+ code >= 63744 && code <= 64255 || // CJK 호환 한자
1994
+ code >= 65072 && code <= 65103 || // CJK 호환 형태
1995
+ code >= 65280 && code <= 65519 || // Full-width 문자
1996
+ code >= 12288 && code <= 12543 || // CJK 기호, 히라가나, 가타카나
1997
+ code >= 12784 && code <= 12799 || // 가타카나 확장
1998
+ code >= 131072 && code <= 195103;
1999
+ }
2000
+ function displayWidth(text) {
2001
+ let w = 0;
2002
+ for (const ch of text) {
2003
+ w += isCJK(ch) ? 1.7 : 1;
2004
+ }
2005
+ return w;
2006
+ }
2007
+ function wrapText(text, maxWidth) {
2008
+ if (displayWidth(text) <= maxWidth) return [text];
2009
+ const lines = [];
2010
+ let current = "";
2011
+ let currentWidth = 0;
2012
+ for (const ch of text) {
2013
+ const chWidth = isCJK(ch) ? 1.7 : 1;
2014
+ if (currentWidth + chWidth > maxWidth && current.length > 0) {
2015
+ lines.push(current);
2016
+ current = ch;
2017
+ currentWidth = chWidth;
2018
+ } else {
2019
+ current += ch;
2020
+ currentWidth += chWidth;
2021
+ }
2022
+ }
2023
+ if (current.length > 0) lines.push(current);
2024
+ return lines;
2025
+ }
1909
2026
  function buildSessions(keystrokes) {
1910
2027
  const hasSessionIds = keystrokes.some((k) => k.sessionId !== void 0);
1911
2028
  if (!hasSessionIds) {
@@ -1939,16 +2056,22 @@ async function renderKeystrokeHud(frameBuffer, keystrokes, frameTimestamp, confi
1939
2056
  const lineGap = Math.round(fontSize * 0.45);
1940
2057
  const charWidth = fontSize * 0.615;
1941
2058
  const maxHudWidth = frameWidth - 60 * dpr;
1942
- const maxCharsPerLine = Math.max(10, Math.floor((maxHudWidth - hudPadH * 2) / charWidth));
1943
- const lines = sessions.map(
1944
- (text) => text.length > maxCharsPerLine ? text.slice(-maxCharsPerLine) : text
1945
- );
1946
- const maxLineLen = Math.max(...lines.map((l) => l.length));
2059
+ const maxDisplayWidth = Math.max(10, (maxHudWidth - hudPadH * 2) / charWidth);
2060
+ const wrappedLines = [];
2061
+ sessions.forEach((text, sIdx) => {
2062
+ const wrapped = wrapText(text, maxDisplayWidth);
2063
+ for (const line of wrapped) {
2064
+ wrappedLines.push({ text: line, sessionIdx: sIdx });
2065
+ }
2066
+ });
2067
+ const lines = wrappedLines.map((l) => l.text);
2068
+ const totalLineCount = lines.length;
2069
+ const maxLineDisplayWidth = Math.max(...lines.map((l) => displayWidth(l)));
1947
2070
  const hudWidth = Math.min(
1948
- Math.ceil(maxLineLen * charWidth) + hudPadH * 2,
2071
+ Math.ceil(maxLineDisplayWidth * charWidth) + hudPadH * 2,
1949
2072
  maxHudWidth
1950
2073
  );
1951
- const hudHeight = Math.ceil(fontSize * lineCount + lineGap * (lineCount - 1) + hudPadV * 2);
2074
+ const hudHeight = Math.ceil(fontSize * totalLineCount + lineGap * (totalLineCount - 1) + hudPadV * 2);
1952
2075
  const margin = 30 * dpr;
1953
2076
  const hudY = frameHeight - hudHeight - margin;
1954
2077
  let hudX;
@@ -1963,18 +2086,20 @@ async function renderKeystrokeHud(frameBuffer, keystrokes, frameTimestamp, confi
1963
2086
  default:
1964
2087
  hudX = Math.round((frameWidth - hudWidth) / 2);
1965
2088
  }
1966
- const LINE_OPACITY_FACTORS = [0.45, 0.7, 1];
1967
- const opacityFactors = LINE_OPACITY_FACTORS.slice(-lineCount);
2089
+ const SESSION_OPACITY_FACTORS = [0.45, 0.7, 1];
2090
+ const sessionOpacities = SESSION_OPACITY_FACTORS.slice(-lineCount);
1968
2091
  const rx = (8 * dpr).toFixed(1);
1969
2092
  const boxOp = (globalOpacity * 0.92).toFixed(3);
1970
2093
  const textX = hudX + hudPadH;
1971
2094
  const baselineY = hudY + hudPadV + fontSize * 0.82;
1972
- const textElements = lines.map((line, i) => {
1973
- const op = (globalOpacity * opacityFactors[i]).toFixed(3);
2095
+ const textElements = wrappedLines.map(({ text, sessionIdx }, i) => {
2096
+ const sessionPos = sessions.length <= 3 ? sessionIdx : sessionIdx - (sessions.length - 3);
2097
+ const opFactor = sessionOpacities[Math.max(0, sessionPos)] ?? 1;
2098
+ const op = (globalOpacity * opFactor).toFixed(3);
1974
2099
  const lineY = baselineY + i * (fontSize + lineGap);
1975
2100
  return `<text x="${textX}" y="${lineY}"
1976
2101
  font-family="monospace, Menlo, Consolas" font-size="${fontSize}"
1977
- fill="${config.textColor}" opacity="${op}">${escapeXml(line)}</text>`;
2102
+ fill="${config.textColor}" opacity="${op}">${escapeXml(text)}</text>`;
1978
2103
  }).join("\n ");
1979
2104
  const hudSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${frameWidth}" height="${frameHeight}" shape-rendering="geometricPrecision" text-rendering="geometricPrecision">
1980
2105
  <rect x="${hudX}" y="${hudY}" width="${hudWidth}" height="${hudHeight}"
@@ -3454,6 +3579,7 @@ var StreamingSession = class extends EventEmitter {
3454
3579
 
3455
3580
  // src/cli/index.ts
3456
3581
  import { writeFile as writeFile2, mkdir as mkdir2, access, copyFile, readFile as readFile3 } from "fs/promises";
3582
+ import { existsSync as existsSync2 } from "fs";
3457
3583
  import { join as join2, resolve, dirname } from "path";
3458
3584
  import { pathToFileURL, fileURLToPath as fileURLToPath2 } from "url";
3459
3585
  import { homedir } from "os";
@@ -3723,152 +3849,34 @@ program.command("demo").description("Record a demo video of the Clipwise showcas
3723
3849
  ).action(async (options) => {
3724
3850
  const spinner = ora();
3725
3851
  try {
3852
+ const { loadScenario: loadScenario2 } = await Promise.resolve().then(() => (init_parser(), parser_exports));
3853
+ const demoYamlCandidates = [
3854
+ resolve(fileURLToPath2(import.meta.url), "../../..", "examples", "demo.yaml"),
3855
+ // from dist/cli/
3856
+ resolve(fileURLToPath2(import.meta.url), "../..", "examples", "demo.yaml")
3857
+ // from src/cli/
3858
+ ];
3859
+ let demoYamlPath = "";
3860
+ for (const candidate of demoYamlCandidates) {
3861
+ if (existsSync2(candidate)) {
3862
+ demoYamlPath = candidate;
3863
+ break;
3864
+ }
3865
+ }
3866
+ if (!demoYamlPath) {
3867
+ throw new Error("Cannot find examples/demo.yaml. Run from the project root.");
3868
+ }
3869
+ const scenario = await loadScenario2(demoYamlPath);
3870
+ scenario.output.format = options.format;
3871
+ scenario.output.outputDir = options.output;
3872
+ scenario.output.filename = `clipwise-demo-${options.device}`;
3726
3873
  const demoUrl = options.url ?? "https://kwakseongjae.github.io/clipwise/demo/";
3727
- const device = options.device;
3728
- const isMobile = device === "iphone" || device === "android";
3729
- const isTablet = device === "ipad";
3730
- const vpWidth = isMobile ? 390 : isTablet ? 1024 : 1280;
3731
- const vpHeight = isMobile ? 844 : isTablet ? 768 : 800;
3732
- const outWidth = isMobile ? 540 : 1280;
3733
- const outHeight = isMobile ? 1080 : isTablet ? 960 : 800;
3734
- const { parseScenario: parseScenario2 } = await Promise.resolve().then(() => (init_parser(), parser_exports));
3735
- const yaml = await import("yaml");
3736
- const steps = [
3737
- {
3738
- name: "Load dashboard",
3739
- captureDelay: 100,
3740
- holdDuration: 1e3,
3741
- actions: [
3742
- { action: "navigate", url: demoUrl, waitUntil: "load" },
3743
- { action: "waitForSelector", selector: "#stat-users", state: "visible", timeout: 15e3 }
3744
- ]
3745
- },
3746
- {
3747
- name: "Hover Users stat",
3748
- captureDelay: 50,
3749
- holdDuration: 700,
3750
- actions: [{ action: "hover", selector: "#stat-users" }]
3751
- },
3752
- {
3753
- name: "Hover Revenue",
3754
- captureDelay: 50,
3755
- holdDuration: 700,
3756
- actions: [{ action: "hover", selector: "#stat-revenue" }]
3757
- },
3758
- {
3759
- name: "Switch chart",
3760
- captureDelay: 50,
3761
- holdDuration: 800,
3762
- actions: [{ action: "click", selector: "#tab-monthly" }]
3763
- },
3764
- {
3765
- name: "Search",
3766
- captureDelay: 50,
3767
- holdDuration: 800,
3768
- actions: [
3769
- { action: "click", selector: "#search-input" },
3770
- { action: "type", selector: "#search-input", text: "conversion", delay: 18 }
3771
- ]
3772
- },
3773
- ...!isMobile ? [{
3774
- name: "Scroll to projects",
3775
- captureDelay: 100,
3776
- holdDuration: 600,
3777
- actions: [{ action: "scroll", y: 420, smooth: true }]
3778
- }] : [{
3779
- name: "Scroll to chart",
3780
- captureDelay: 100,
3781
- holdDuration: 600,
3782
- actions: [{ action: "scroll", y: 250, smooth: true }]
3783
- }],
3784
- {
3785
- name: "Hover row",
3786
- captureDelay: 50,
3787
- holdDuration: 600,
3788
- actions: [{ action: "hover", selector: "#row-1" }]
3789
- },
3790
- {
3791
- name: "Open modal",
3792
- captureDelay: 100,
3793
- holdDuration: 800,
3794
- actions: [{ action: "click", selector: "#btn-new-project" }]
3795
- },
3796
- {
3797
- name: "Type name",
3798
- captureDelay: 50,
3799
- holdDuration: 600,
3800
- actions: [
3801
- { action: "click", selector: "#project-name" },
3802
- { action: "type", selector: "#project-name", text: "Clipwise Demo", delay: 20 }
3803
- ]
3804
- },
3805
- {
3806
- name: "Type desc",
3807
- captureDelay: 50,
3808
- holdDuration: 600,
3809
- actions: [
3810
- { action: "click", selector: "#project-desc" },
3811
- { action: "type", selector: "#project-desc", text: "Automated screen recording", delay: 16 }
3812
- ]
3813
- },
3814
- {
3815
- name: "Create",
3816
- captureDelay: 100,
3817
- holdDuration: 1e3,
3818
- actions: [{ action: "click", selector: "#btn-create" }]
3874
+ if (scenario.steps.length > 0) {
3875
+ const navAction = scenario.steps[0].actions.find((a) => a.action === "navigate");
3876
+ if (navAction && "url" in navAction) {
3877
+ navAction.url = demoUrl;
3819
3878
  }
3820
- ];
3821
- const scenarioObj = {
3822
- name: `Clipwise Demo (${device})`,
3823
- viewport: { width: vpWidth, height: vpHeight },
3824
- effects: {
3825
- zoom: {
3826
- enabled: true,
3827
- scale: 1.8,
3828
- duration: 500,
3829
- autoZoom: { followCursor: true, maxScale: 2 }
3830
- },
3831
- cursor: {
3832
- enabled: true,
3833
- size: isMobile ? 16 : 20,
3834
- clickEffect: true,
3835
- highlight: true,
3836
- trail: !isMobile,
3837
- trailLength: 6
3838
- },
3839
- background: {
3840
- type: "gradient",
3841
- value: "linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)",
3842
- padding: isMobile ? 60 : 48,
3843
- borderRadius: 14,
3844
- shadow: true
3845
- },
3846
- deviceFrame: { enabled: true, type: device, darkMode: true },
3847
- keystroke: {
3848
- enabled: true,
3849
- position: "bottom-center",
3850
- fontSize: isMobile ? 14 : 16
3851
- },
3852
- watermark: {
3853
- enabled: true,
3854
- text: "Clipwise",
3855
- position: "bottom-right",
3856
- opacity: 0.35,
3857
- fontSize: 13
3858
- }
3859
- },
3860
- output: {
3861
- format: options.format,
3862
- width: outWidth,
3863
- height: outHeight,
3864
- fps: 30,
3865
- preset: "social",
3866
- outputDir: options.output,
3867
- filename: `clipwise-demo-${device}`
3868
- },
3869
- steps
3870
- };
3871
- const scenario = parseScenario2(yaml.stringify(scenarioObj));
3879
+ }
3872
3880
  spinner.succeed(`Demo scenario ready: ${chalk.bold(scenario.name)}`);
3873
3881
  spinner.start("Checking browser...");
3874
3882
  try {
@@ -3890,7 +3898,7 @@ program.command("demo").description("Record a demo video of the Clipwise showcas
3890
3898
  await mkdir2(options.output, { recursive: true });
3891
3899
  const demoRenderer = new CanvasRenderer(scenario.effects, scenario.output, scenario.steps);
3892
3900
  const ext = scenario.output.format === "gif" ? "gif" : "mp4";
3893
- const outputPath = join2(options.output, `clipwise-demo-${device}.${ext}`);
3901
+ const outputPath = join2(options.output, `clipwise-demo-${options.device}.${ext}`);
3894
3902
  const isConcurrentEligible = ext === "mp4" && demoRenderer.canStreamOnline();
3895
3903
  if (isConcurrentEligible) {
3896
3904
  const recorder = new ClipwiseRecorder();