@remixmate/cli 0.1.0 → 0.1.1

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.
Files changed (122) hide show
  1. package/README.md +100 -83
  2. package/README.zh-CN.md +183 -0
  3. package/dist/handlers/gen-digital-human.d.ts +12 -0
  4. package/dist/handlers/gen-digital-human.js +157 -0
  5. package/dist/handlers/gen-image.js +5 -41
  6. package/dist/handlers/gen-video.d.ts +18 -0
  7. package/dist/handlers/gen-video.js +153 -0
  8. package/dist/handlers/gen-voice.js +2 -12
  9. package/dist/handlers/index.js +4 -2
  10. package/dist/handlers/shared.d.ts +26 -0
  11. package/dist/handlers/shared.js +66 -0
  12. package/dist/http.d.ts +4 -2
  13. package/dist/http.js +5 -3
  14. package/dist/manifest.json +27 -24
  15. package/dist/registry.d.ts +5 -16
  16. package/dist/registry.js +11 -17
  17. package/dist/skill-schema.d.ts +47 -0
  18. package/dist/skill-schema.js +49 -0
  19. package/package.json +5 -4
  20. package/skills/export-jianying/scripts/gen_jianying_draft.py +4 -4
  21. package/skills/gen-digital-human/SKILL.md +23 -46
  22. package/skills/gen-digital-human/skill.json +3 -2
  23. package/skills/gen-digital-human/version.json +1 -1
  24. package/skills/gen-image/SKILL.md +2 -2
  25. package/skills/gen-image/skill.json +2 -1
  26. package/skills/gen-image/version.json +1 -1
  27. package/skills/gen-script/SKILL.md +13 -13
  28. package/skills/gen-script/scripts/gen_script.py +11 -12
  29. package/skills/gen-video/SKILL.md +22 -33
  30. package/skills/gen-video/skill.json +3 -2
  31. package/skills/gen-video/version.json +1 -1
  32. package/skills/gen-voice/SKILL.md +1 -1
  33. package/skills/gen-voice/version.json +1 -1
  34. package/skills/prepare-video-assets/SKILL.md +2 -2
  35. package/skills/prepare-video-assets/skill.json +1 -1
  36. package/skills/render-video/scripts/render_video.py +8 -8
  37. package/skills/{template-bind → template-registry}/README.md +4 -4
  38. package/skills/{template-bind → template-registry}/SKILL.md +14 -12
  39. package/skills/template-registry/scripts/list_templates.py +129 -0
  40. package/skills/{template-bind → template-registry}/scripts/match_template.py +4 -3
  41. package/skills/{template-bind → template-registry}/scripts/registry_loader.py +1 -1
  42. package/skills/{template-bind → template-registry}/scripts/template_paths.py +7 -7
  43. package/skills/{template-bind → template-registry}/skill.json +6 -5
  44. package/skills/template-registry/version.json +7 -0
  45. package/skills/{template-bind → template-registry}/video_dsl/README.md +2 -2
  46. package/skills/{template-bind → template-registry}/video_dsl/runtime/__init__.py +1 -1
  47. package/skills/video-parser/SKILL.md +47 -160
  48. package/skills/video-parser/skill.json +3 -4
  49. package/skills/video-parser/version.json +1 -1
  50. package/skills/web-capture/SKILL.md +407 -116
  51. package/skills/web-capture/scripts/_media_screenshot/__init__.py +21 -0
  52. package/skills/web-capture/scripts/_media_screenshot/bootstrap.py +34 -0
  53. package/skills/web-capture/scripts/_media_screenshot/browser.py +86 -0
  54. package/skills/web-capture/scripts/_media_screenshot/cli_args.py +111 -0
  55. package/skills/web-capture/scripts/_media_screenshot/js/arrow.js +86 -0
  56. package/skills/web-capture/scripts/_media_screenshot/js/caption.js +27 -0
  57. package/skills/web-capture/scripts/_media_screenshot/js/clear_overlay.js +5 -0
  58. package/skills/web-capture/scripts/_media_screenshot/js/cursor_bootstrap.js +26 -0
  59. package/skills/web-capture/scripts/_media_screenshot/js/cursor_hide.js +6 -0
  60. package/skills/web-capture/scripts/_media_screenshot/js/cursor_move.js +12 -0
  61. package/skills/web-capture/scripts/_media_screenshot/js/cursor_ripple.js +25 -0
  62. package/skills/web-capture/scripts/_media_screenshot/js/element_zoom.js +72 -0
  63. package/skills/web-capture/scripts/_media_screenshot/js/element_zoom_restore.js +21 -0
  64. package/skills/web-capture/scripts/_media_screenshot/js/fade_out_overlays.js +10 -0
  65. package/skills/web-capture/scripts/_media_screenshot/js/focus.js +38 -0
  66. package/skills/web-capture/scripts/_media_screenshot/js/highlight.js +97 -0
  67. package/skills/web-capture/scripts/_media_screenshot/js/overlay_bootstrap.js +19 -0
  68. package/skills/web-capture/scripts/_media_screenshot/js/page_zoom_in.js +54 -0
  69. package/skills/web-capture/scripts/_media_screenshot/js/page_zoom_out.js +11 -0
  70. package/skills/web-capture/scripts/_media_screenshot/js/page_zoom_restore.js +13 -0
  71. package/skills/web-capture/scripts/_media_screenshot/js/redact.js +33 -0
  72. package/skills/web-capture/scripts/_media_screenshot/js/scroll.js +26 -0
  73. package/skills/web-capture/scripts/_media_screenshot/js/scroll_through.js +18 -0
  74. package/skills/web-capture/scripts/_media_screenshot/js/sequence.js +76 -0
  75. package/skills/web-capture/scripts/_media_screenshot/js/settled_check.js +15 -0
  76. package/skills/web-capture/scripts/_media_screenshot/js/title_card.js +71 -0
  77. package/skills/web-capture/scripts/_media_screenshot/js_loader.py +14 -0
  78. package/skills/web-capture/scripts/_media_screenshot/overlay.py +17 -0
  79. package/skills/web-capture/scripts/_media_screenshot/recording.py +100 -0
  80. package/skills/web-capture/scripts/_media_screenshot/scenes/__init__.py +39 -0
  81. package/skills/web-capture/scripts/_media_screenshot/scenes/arrow.py +50 -0
  82. package/skills/web-capture/scripts/_media_screenshot/scenes/caption.py +20 -0
  83. package/skills/web-capture/scripts/_media_screenshot/scenes/click.py +35 -0
  84. package/skills/web-capture/scripts/_media_screenshot/scenes/code_lines.py +54 -0
  85. package/skills/web-capture/scripts/_media_screenshot/scenes/focus.py +24 -0
  86. package/skills/web-capture/scripts/_media_screenshot/scenes/highlight.py +54 -0
  87. package/skills/web-capture/scripts/_media_screenshot/scenes/hover.py +28 -0
  88. package/skills/web-capture/scripts/_media_screenshot/scenes/redact.py +32 -0
  89. package/skills/web-capture/scripts/_media_screenshot/scenes/scroll.py +32 -0
  90. package/skills/web-capture/scripts/_media_screenshot/scenes/sequence.py +43 -0
  91. package/skills/web-capture/scripts/_media_screenshot/scenes/title_card.py +20 -0
  92. package/skills/web-capture/scripts/_media_screenshot/scenes/type.py +42 -0
  93. package/skills/web-capture/scripts/_media_screenshot/scenes/wait.py +9 -0
  94. package/skills/web-capture/scripts/_media_screenshot/scenes/zoom.py +72 -0
  95. package/skills/web-capture/scripts/_media_screenshot/screenshot.py +166 -0
  96. package/skills/web-capture/scripts/_media_screenshot/storyboard.py +88 -0
  97. package/skills/web-capture/scripts/_media_screenshot/template.py +119 -0
  98. package/skills/web-capture/scripts/_media_screenshot/trim.py +82 -0
  99. package/skills/web-capture/scripts/record.py +186 -0
  100. package/skills/web-capture/scripts/screenshot.py +94 -0
  101. package/skills/web-capture/skill.json +14 -14
  102. package/skills/web-capture/templates/github-code-tour.json +59 -0
  103. package/skills/web-capture/templates/github-readme-tour.json +77 -0
  104. package/skills/web-capture/templates/github-repo-intro.json +74 -0
  105. package/skills/web-capture/version.json +2 -2
  106. package/dist/handlers/template-bind.d.ts +0 -15
  107. package/dist/handlers/template-bind.js +0 -149
  108. package/skills/gen-digital-human/scripts/gen_digital_human_video.py +0 -574
  109. package/skills/gen-video/scripts/gen_video.py +0 -591
  110. package/skills/template-bind/version.json +0 -7
  111. package/skills/web-capture/scripts/web_capture.py +0 -461
  112. /package/skills/{template-bind → template-registry}/scripts/check_contracts.py +0 -0
  113. /package/skills/{template-bind → template-registry}/scripts/render_job_client.py +0 -0
  114. /package/skills/{template-bind → template-registry}/scripts/sync_registry.py +0 -0
  115. /package/skills/{template-bind → template-registry}/video_dsl/runtime/dsl_validator.py +0 -0
  116. /package/skills/{template-bind → template-registry}/video_dsl/runtime/prompt_enhancer.py +0 -0
  117. /package/skills/{template-bind → template-registry}/video_dsl/runtime/template_binder.py +0 -0
  118. /package/skills/{template-bind → template-registry}/video_dsl/runtime/timeline_compiler.py +0 -0
  119. /package/skills/{template-bind → template-registry}/video_dsl/schema/render-plan-v1alpha1.json +0 -0
  120. /package/skills/{template-bind → template-registry}/video_dsl/schema/template-binding-v1alpha1.json +0 -0
  121. /package/skills/{template-bind → template-registry}/video_dsl/schema/template-definition-v1alpha1.json +0 -0
  122. /package/skills/{template-bind → template-registry}/video_dsl/schema/video-dsl-v1alpha1.json +0 -0
@@ -0,0 +1,86 @@
1
+ """Browser context / locator / wait helpers."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from . import js_loader
7
+ from .bootstrap import ensure_chromium
8
+
9
+
10
+ def pick_browser_type(p, name: str | None):
11
+ if name == "firefox":
12
+ return p.firefox
13
+ if name == "webkit":
14
+ return p.webkit
15
+ return p.chromium
16
+
17
+
18
+ def build_context_options(p, cfg: dict, record_video_dir: str | None) -> dict:
19
+ opts: dict[str, Any] = {}
20
+ if cfg.get("device"):
21
+ d = p.devices.get(cfg["device"])
22
+ if not d:
23
+ raise SystemExit(f"unknown device: {cfg['device']}")
24
+ opts.update(d)
25
+ elif isinstance(cfg.get("viewport"), (list, tuple)) and len(cfg["viewport"]) == 2:
26
+ opts["viewport"] = {
27
+ "width": int(cfg["viewport"][0]),
28
+ "height": int(cfg["viewport"][1]),
29
+ }
30
+ if cfg.get("colorScheme"):
31
+ opts["color_scheme"] = cfg["colorScheme"]
32
+ if cfg.get("userAgent"):
33
+ opts["user_agent"] = cfg["userAgent"]
34
+ if cfg.get("ignoreHttpsErrors"):
35
+ opts["ignore_https_errors"] = True
36
+ if cfg.get("storageState"):
37
+ opts["storage_state"] = cfg["storageState"]
38
+ if record_video_dir:
39
+ opts["record_video_dir"] = record_video_dir
40
+ if opts.get("viewport"):
41
+ opts["record_video_size"] = opts["viewport"]
42
+ return opts
43
+
44
+
45
+ def launch_with_browser_install(p, browser_name: str | None, launch_kwargs: dict | None = None):
46
+ btype = pick_browser_type(p, browser_name)
47
+ try:
48
+ return btype.launch(**(launch_kwargs or {}))
49
+ except Exception as e:
50
+ msg = str(e)
51
+ if "Executable doesn't exist" in msg or "playwright install" in msg.lower():
52
+ ensure_chromium()
53
+ return btype.launch(**(launch_kwargs or {}))
54
+ raise
55
+
56
+
57
+ def apply_pre_action_waits(page, cfg: dict) -> None:
58
+ if cfg.get("waitForSelector"):
59
+ page.wait_for_selector(cfg["waitForSelector"], timeout=cfg.get("timeout", 0))
60
+ if isinstance(cfg.get("waitForTimeout"), (int, float)):
61
+ page.wait_for_timeout(cfg["waitForTimeout"])
62
+
63
+
64
+ def wait_until_settled(page, cfg: dict) -> None:
65
+ try:
66
+ page.wait_for_load_state("networkidle", timeout=20000)
67
+ except Exception:
68
+ pass
69
+ try:
70
+ page.wait_for_function(js_loader.load("settled_check"), timeout=15000)
71
+ except Exception:
72
+ pass
73
+ for sel in cfg.get("waitForReadySelectors") or []:
74
+ try:
75
+ page.wait_for_selector(sel, state="visible", timeout=10000)
76
+ except Exception:
77
+ pass
78
+ page.wait_for_timeout(cfg.get("settleMs", 800))
79
+
80
+
81
+ def resolve_locator(page, selector: str):
82
+ locator = page.locator(selector).first
83
+ locator.wait_for(state="visible", timeout=15000)
84
+ locator.scroll_into_view_if_needed()
85
+ page.wait_for_timeout(250)
86
+ return locator
@@ -0,0 +1,111 @@
1
+ """Argparse helpers shared by ``screenshot.py`` and ``record.py``."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import tempfile
8
+ from pathlib import Path
9
+
10
+
11
+ def add_common_args(ap: argparse.ArgumentParser) -> None:
12
+ """Add the args common to both screenshot and record entry points."""
13
+ ap.add_argument(
14
+ "-b", "--browser", choices=["chromium", "firefox", "webkit"],
15
+ default="chromium", help="浏览器内核(默认 chromium)",
16
+ )
17
+ ap.add_argument("--device", help='设备模拟名,如 "iPhone 15 Pro"、"Pixel 7"')
18
+ ap.add_argument(
19
+ "--viewport",
20
+ help='viewport "宽,高",如 "1280,800";与 --device 互斥时以 --device 为准',
21
+ )
22
+ ap.add_argument("--wait-for-selector", help="动作前等待该 CSS 选择器出现")
23
+ ap.add_argument("--wait-for-timeout", type=int, help="动作前固定等待毫秒数")
24
+ ap.add_argument(
25
+ "--color-scheme", choices=["light", "dark", "no-preference"],
26
+ help="模拟 prefers-color-scheme",
27
+ )
28
+ ap.add_argument("--user-agent", help="覆盖 User-Agent")
29
+ ap.add_argument(
30
+ "--timeout", type=int, help="Playwright 全局动作超时(ms,默认无超时)",
31
+ )
32
+ ap.add_argument(
33
+ "--ignore-https-errors", action="store_true", help="忽略 HTTPS 证书错误",
34
+ )
35
+ ap.add_argument("--storage-state", help="storageState JSON 文件路径")
36
+ ap.add_argument(
37
+ "--cookies",
38
+ help='Playwright cookies JSON 字符串或 JSON 文件路径(顶层为数组)',
39
+ )
40
+
41
+
42
+ def parse_cookies(raw: str) -> list[dict]:
43
+ p = Path(raw)
44
+ if p.exists() and p.is_file():
45
+ try:
46
+ data = json.loads(p.read_text())
47
+ except json.JSONDecodeError as e:
48
+ raise SystemExit(f"--cookies 文件不是合法 JSON:{e}")
49
+ else:
50
+ try:
51
+ data = json.loads(raw)
52
+ except json.JSONDecodeError:
53
+ raise SystemExit(
54
+ "--cookies 需为 JSON 字符串或 JSON 文件路径,顶层为 cookie 数组。"
55
+ )
56
+ if not isinstance(data, list):
57
+ raise SystemExit("--cookies 顶层应为数组(Playwright cookies 格式)。")
58
+ return data
59
+
60
+
61
+ def build_storage(args) -> tuple[str | None, bool]:
62
+ """Returns (storage_state_path, is_temp). Caller must unlink if is_temp."""
63
+ if getattr(args, "storage_state", None):
64
+ return args.storage_state, False
65
+ if getattr(args, "cookies", None):
66
+ cookies = parse_cookies(args.cookies)
67
+ fd, path = tempfile.mkstemp(prefix="pw_storage_", suffix=".json")
68
+ with os.fdopen(fd, "w") as f:
69
+ json.dump({"cookies": cookies, "origins": []}, f)
70
+ return path, True
71
+ return None, False
72
+
73
+
74
+ def parse_viewport(raw: str) -> list[int]:
75
+ parts = [s.strip() for s in raw.split(",")]
76
+ if len(parts) != 2:
77
+ raise SystemExit('--viewport 需为 "宽,高"')
78
+ try:
79
+ return [int(p) for p in parts]
80
+ except ValueError:
81
+ raise SystemExit("--viewport 必须是整数")
82
+
83
+
84
+ def parse_clip(raw: str) -> list[float]:
85
+ parts = [s.strip() for s in raw.split(",")]
86
+ if len(parts) != 4:
87
+ raise SystemExit('--clip 需为 "x,y,w,h" 四个数字')
88
+ try:
89
+ return [float(p) for p in parts]
90
+ except ValueError:
91
+ raise SystemExit("--clip 的四个值必须是数字")
92
+
93
+
94
+ def common_args_to_cfg(args, cfg: dict) -> None:
95
+ """Merge the common-args values into a cfg dict (shared by entry points)."""
96
+ if args.device:
97
+ cfg["device"] = args.device
98
+ if args.viewport:
99
+ cfg["viewport"] = parse_viewport(args.viewport)
100
+ if args.color_scheme:
101
+ cfg["colorScheme"] = args.color_scheme
102
+ if args.user_agent:
103
+ cfg["userAgent"] = args.user_agent
104
+ if args.timeout is not None:
105
+ cfg["timeout"] = args.timeout
106
+ if args.ignore_https_errors:
107
+ cfg["ignoreHttpsErrors"] = True
108
+ if args.wait_for_selector:
109
+ cfg["waitForSelector"] = args.wait_for_selector
110
+ if args.wait_for_timeout is not None:
111
+ cfg["waitForTimeout"] = args.wait_for_timeout
@@ -0,0 +1,86 @@
1
+ // Draw a curved SVG arrow from `from` to `to` with an arrowhead, optional label.
2
+ // Both endpoints are page-fixed coordinates (computed in Python from bounding boxes).
3
+ ({ from, to, color, lineWidth, label }) => {
4
+ const root = document.getElementById('__pw_overlay_root');
5
+ if (!root) return;
6
+ const NS = 'http://www.w3.org/2000/svg';
7
+
8
+ const svg = document.createElementNS(NS, 'svg');
9
+ Object.assign(svg.style, {
10
+ position: 'fixed',
11
+ left: '0',
12
+ top: '0',
13
+ width: '100%',
14
+ height: '100%',
15
+ pointerEvents: 'none',
16
+ overflow: 'visible',
17
+ });
18
+ svg.setAttribute('viewBox', `0 0 ${window.innerWidth} ${window.innerHeight}`);
19
+ svg.setAttribute('preserveAspectRatio', 'none');
20
+
21
+ const markerId = `__pw_arrow_head_${Math.random().toString(36).slice(2, 8)}`;
22
+ const defs = document.createElementNS(NS, 'defs');
23
+ const marker = document.createElementNS(NS, 'marker');
24
+ marker.setAttribute('id', markerId);
25
+ marker.setAttribute('viewBox', '0 0 10 10');
26
+ marker.setAttribute('refX', '8');
27
+ marker.setAttribute('refY', '5');
28
+ marker.setAttribute('markerWidth', '6');
29
+ marker.setAttribute('markerHeight', '6');
30
+ marker.setAttribute('orient', 'auto-start-reverse');
31
+ const tri = document.createElementNS(NS, 'path');
32
+ tri.setAttribute('d', 'M0,0 L10,5 L0,10 z');
33
+ tri.setAttribute('fill', color);
34
+ marker.appendChild(tri);
35
+ defs.appendChild(marker);
36
+ svg.appendChild(defs);
37
+
38
+ const dx = to.x - from.x;
39
+ const dy = to.y - from.y;
40
+ const mx = (from.x + to.x) / 2;
41
+ const my = (from.y + to.y) / 2;
42
+ const len = Math.hypot(dx, dy) || 1;
43
+ const curve = Math.min(80, len * 0.22);
44
+ const ox = (-dy / len) * curve;
45
+ const oy = (dx / len) * curve;
46
+
47
+ const path = document.createElementNS(NS, 'path');
48
+ path.setAttribute('d', `M ${from.x} ${from.y} Q ${mx + ox} ${my + oy} ${to.x} ${to.y}`);
49
+ path.setAttribute('stroke', color);
50
+ path.setAttribute('stroke-width', String(lineWidth));
51
+ path.setAttribute('fill', 'none');
52
+ path.setAttribute('stroke-linecap', 'round');
53
+ path.setAttribute('marker-end', `url(#${markerId})`);
54
+
55
+ const dashLen = len * 1.15 + curve;
56
+ path.setAttribute('stroke-dasharray', String(dashLen));
57
+ path.setAttribute('stroke-dashoffset', String(dashLen));
58
+ path.style.transition = 'stroke-dashoffset 600ms ease-out';
59
+ svg.appendChild(path);
60
+ root.appendChild(svg);
61
+ requestAnimationFrame(() => path.setAttribute('stroke-dashoffset', '0'));
62
+
63
+ if (label) {
64
+ const lbl = document.createElement('div');
65
+ lbl.textContent = label;
66
+ Object.assign(lbl.style, {
67
+ position: 'fixed',
68
+ left: `${from.x}px`,
69
+ top: `${from.y - 36}px`,
70
+ transform: 'translateX(-50%)',
71
+ padding: '6px 12px',
72
+ background: color,
73
+ color: '#fff',
74
+ fontSize: '15px',
75
+ fontWeight: '600',
76
+ borderRadius: '6px',
77
+ fontFamily: 'system-ui, -apple-system, "PingFang SC", "Helvetica Neue", sans-serif',
78
+ boxShadow: '0 4px 12px rgba(0,0,0,0.25)',
79
+ whiteSpace: 'nowrap',
80
+ opacity: '0',
81
+ transition: 'opacity 400ms ease 200ms',
82
+ });
83
+ root.appendChild(lbl);
84
+ requestAnimationFrame(() => (lbl.style.opacity = '1'));
85
+ }
86
+ };
@@ -0,0 +1,27 @@
1
+ // Slide a caption strip in from the top or bottom edge.
2
+ ({ text, position, bg, fg, fontSize }) => {
3
+ const root = document.getElementById('__pw_overlay_root');
4
+ if (!root) return;
5
+ const isTop = position === 'top';
6
+ const strip = document.createElement('div');
7
+ strip.textContent = text;
8
+ Object.assign(strip.style, {
9
+ position: 'fixed',
10
+ left: '0',
11
+ right: '0',
12
+ [isTop ? 'top' : 'bottom']: '0',
13
+ padding: '14px 28px',
14
+ background: bg,
15
+ color: fg,
16
+ fontSize: `${fontSize}px`,
17
+ fontWeight: '600',
18
+ fontFamily: 'system-ui, -apple-system, "PingFang SC", "Helvetica Neue", sans-serif',
19
+ textAlign: 'center',
20
+ transform: `translateY(${isTop ? '-100%' : '100%'})`,
21
+ transition: 'transform 400ms cubic-bezier(0.22, 0.61, 0.36, 1)',
22
+ boxShadow: isTop ? '0 4px 16px rgba(0,0,0,0.3)' : '0 -4px 16px rgba(0,0,0,0.3)',
23
+ lineHeight: '1.4',
24
+ });
25
+ root.appendChild(strip);
26
+ requestAnimationFrame(() => (strip.style.transform = 'translateY(0)'));
27
+ };
@@ -0,0 +1,5 @@
1
+ // Empty the overlay root without removing it (fade element survives).
2
+ () => {
3
+ const r = document.getElementById('__pw_overlay_root');
4
+ if (r) r.replaceChildren();
5
+ }
@@ -0,0 +1,26 @@
1
+ // Mount a singleton fake cursor on documentElement (outside overlay root so it
2
+ // survives inter-scene fades). Idempotent.
3
+ () => {
4
+ if (window.__pwCursor && window.__pwCursor.el && document.body.contains(window.__pwCursor.el)) {
5
+ return;
6
+ }
7
+ const c = document.createElement('div');
8
+ c.id = '__pw_cursor';
9
+ Object.assign(c.style, {
10
+ position: 'fixed',
11
+ left: '20px',
12
+ top: '20px',
13
+ width: '28px',
14
+ height: '28px',
15
+ pointerEvents: 'none',
16
+ zIndex: '2147483647',
17
+ transition: 'left 0ms linear, top 0ms linear',
18
+ filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.45))',
19
+ });
20
+ c.innerHTML =
21
+ '<svg width="28" height="28" viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg">' +
22
+ '<path d="M3 2 L3 22 L9 16 L13 25 L17 23 L13 14 L21 14 Z" ' +
23
+ 'fill="white" stroke="black" stroke-width="1.5" stroke-linejoin="round"/></svg>';
24
+ document.documentElement.appendChild(c);
25
+ window.__pwCursor = { el: c, pos: { x: 20, y: 20 } };
26
+ };
@@ -0,0 +1,6 @@
1
+ // Remove the singleton cursor. Safe to call when not mounted.
2
+ () => {
3
+ const c = window.__pwCursor;
4
+ if (c && c.el && c.el.parentNode) c.el.parentNode.removeChild(c.el);
5
+ window.__pwCursor = undefined;
6
+ };
@@ -0,0 +1,12 @@
1
+ // Animate the singleton cursor to (x, y) over `ms`. Python side then waits the
2
+ // same amount; this JS just kicks off the CSS transition.
3
+ ({ x, y, ms }) => {
4
+ const c = window.__pwCursor;
5
+ if (!c) return;
6
+ c.el.style.transition =
7
+ `left ${ms}ms cubic-bezier(0.22,0.61,0.36,1), top ${ms}ms cubic-bezier(0.22,0.61,0.36,1)`;
8
+ c.el.getBoundingClientRect();
9
+ c.el.style.left = `${x}px`;
10
+ c.el.style.top = `${y}px`;
11
+ c.pos = { x, y };
12
+ };
@@ -0,0 +1,25 @@
1
+ // Emit a ring-shaped ripple at the cursor's current position.
2
+ ({ color, ms }) => {
3
+ const c = window.__pwCursor;
4
+ if (!c) return;
5
+ const r = document.createElement('div');
6
+ Object.assign(r.style, {
7
+ position: 'fixed',
8
+ left: `${c.pos.x - 4}px`,
9
+ top: `${c.pos.y - 4}px`,
10
+ width: '8px',
11
+ height: '8px',
12
+ borderRadius: '999px',
13
+ border: `2px solid ${color}`,
14
+ pointerEvents: 'none',
15
+ zIndex: '2147483647',
16
+ opacity: '0.9',
17
+ transition: `transform ${ms}ms ease-out, opacity ${ms}ms ease-out`,
18
+ });
19
+ document.documentElement.appendChild(r);
20
+ requestAnimationFrame(() => {
21
+ r.style.transform = 'scale(8)';
22
+ r.style.opacity = '0';
23
+ });
24
+ setTimeout(() => r.remove(), ms + 100);
25
+ };
@@ -0,0 +1,72 @@
1
+ // Element-level zoom: scale the element itself with a spring transition,
2
+ // optionally dimming the rest of the page. Original styles are stashed on
3
+ // the element's dataset for later restoration by element_zoom_restore.js.
4
+ ({ selector, targetScale, rampMs, color, label, dim, lineWidth }) => {
5
+ const el = document.querySelector(selector);
6
+ if (!el) return;
7
+ const rect = el.getBoundingClientRect();
8
+ const cx = rect.left + rect.width / 2;
9
+ const cy = rect.top + rect.height / 2;
10
+ el.dataset.__pwOrigTransform = el.style.transform || '';
11
+ el.dataset.__pwOrigTransition = el.style.transition || '';
12
+ el.dataset.__pwOrigOrigin = el.style.transformOrigin || '';
13
+ el.dataset.__pwOrigZ = el.style.zIndex || '';
14
+ el.dataset.__pwOrigPos = el.style.position || '';
15
+ el.dataset.__pwOrigBg = el.style.backgroundColor || '';
16
+ el.dataset.__pwOrigOutline = el.style.outline || '';
17
+ el.dataset.__pwOrigRadius = el.style.borderRadius || '';
18
+
19
+ const root = document.getElementById('__pw_overlay_root');
20
+ if (dim) {
21
+ const dimEl = document.createElement('div');
22
+ dimEl.id = '__pw_zoom_dim';
23
+ Object.assign(dimEl.style, {
24
+ position: 'fixed', inset: '0', background: 'rgba(0,0,0,0)',
25
+ transition: `background ${rampMs}ms ease`, zIndex: '2147483640',
26
+ pointerEvents: 'none',
27
+ });
28
+ root.appendChild(dimEl);
29
+ requestAnimationFrame(() => {
30
+ dimEl.style.background = 'rgba(0,0,0,0.55)';
31
+ });
32
+ }
33
+
34
+ const computed = window.getComputedStyle(el);
35
+ if (computed.position === 'static') el.style.position = 'relative';
36
+ el.style.zIndex = '2147483645';
37
+ el.style.transformOrigin = `${cx - rect.left}px ${cy - rect.top}px`;
38
+ el.style.transition = `transform ${rampMs}ms cubic-bezier(0.34, 1.4, 0.64, 1), background-color ${rampMs}ms ease, outline-color ${rampMs}ms ease`;
39
+ el.style.transform = 'scale(1)';
40
+ el.style.outline = `${lineWidth}px solid rgba(255,149,0,0)`;
41
+ el.style.borderRadius = el.dataset.__pwOrigRadius || '6px';
42
+ requestAnimationFrame(() => {
43
+ el.style.transform = `scale(${targetScale})`;
44
+ el.style.outline = `${lineWidth}px solid ${color}`;
45
+ });
46
+
47
+ if (label) {
48
+ const lbl = document.createElement('div');
49
+ lbl.id = '__pw_zoom_label';
50
+ lbl.textContent = label;
51
+ const finalH = rect.height * targetScale;
52
+ Object.assign(lbl.style, {
53
+ position: 'fixed',
54
+ left: `${cx}px`,
55
+ top: `${cy + finalH / 2 + 24}px`,
56
+ transform: 'translateX(-50%) translateY(8px)',
57
+ padding: '10px 18px',
58
+ background: color, color: '#fff',
59
+ fontSize: '20px', fontWeight: '700', borderRadius: '8px',
60
+ fontFamily: 'system-ui, -apple-system, "Helvetica Neue", sans-serif',
61
+ boxShadow: '0 6px 18px rgba(0,0,0,0.4)',
62
+ whiteSpace: 'nowrap', opacity: '0',
63
+ transition: 'opacity 280ms ease, transform 280ms ease',
64
+ zIndex: '2147483646', pointerEvents: 'none',
65
+ });
66
+ root.appendChild(lbl);
67
+ setTimeout(() => {
68
+ lbl.style.opacity = '1';
69
+ lbl.style.transform = 'translateX(-50%) translateY(0)';
70
+ }, Math.max(0, rampMs * 0.6));
71
+ }
72
+ }
@@ -0,0 +1,21 @@
1
+ // Restore the inline styles stashed by element_zoom.js on the target element.
2
+ (selector) => {
3
+ const el = document.querySelector(selector);
4
+ if (!el) return;
5
+ el.style.transform = el.dataset.__pwOrigTransform || '';
6
+ el.style.transition = el.dataset.__pwOrigTransition || '';
7
+ el.style.transformOrigin = el.dataset.__pwOrigOrigin || '';
8
+ el.style.zIndex = el.dataset.__pwOrigZ || '';
9
+ el.style.position = el.dataset.__pwOrigPos || '';
10
+ el.style.backgroundColor = el.dataset.__pwOrigBg || '';
11
+ el.style.outline = el.dataset.__pwOrigOutline || '';
12
+ el.style.borderRadius = el.dataset.__pwOrigRadius || '';
13
+ delete el.dataset.__pwOrigTransform;
14
+ delete el.dataset.__pwOrigTransition;
15
+ delete el.dataset.__pwOrigOrigin;
16
+ delete el.dataset.__pwOrigZ;
17
+ delete el.dataset.__pwOrigPos;
18
+ delete el.dataset.__pwOrigBg;
19
+ delete el.dataset.__pwOrigOutline;
20
+ delete el.dataset.__pwOrigRadius;
21
+ }
@@ -0,0 +1,10 @@
1
+ // Fade every child of the overlay root to opacity 0 over `duration` ms.
2
+ // Page content underneath stays visible — this is the inter-scene transition.
3
+ (duration) => {
4
+ const root = document.getElementById('__pw_overlay_root');
5
+ if (!root) return;
6
+ for (const child of root.children) {
7
+ child.style.transition = `opacity ${duration}ms ease, transform ${duration}ms ease`;
8
+ child.style.opacity = '0';
9
+ }
10
+ }
@@ -0,0 +1,38 @@
1
+ // Spotlight effect: dim everything except a rounded cut-out around `box`.
2
+ ({ box, pad, label, color }) => {
3
+ const root = document.getElementById('__pw_overlay_root');
4
+ if (!root) return;
5
+ const hole = document.createElement('div');
6
+ Object.assign(hole.style, {
7
+ position: 'fixed',
8
+ left: `${box.x - pad}px`,
9
+ top: `${box.y - pad}px`,
10
+ width: `${box.width + pad * 2}px`,
11
+ height: `${box.height + pad * 2}px`,
12
+ borderRadius: '12px',
13
+ boxShadow: '0 0 0 9999px rgba(0,0,0,0.65)',
14
+ border: `3px solid ${color}`,
15
+ transition: 'box-shadow 400ms ease',
16
+ });
17
+ root.appendChild(hole);
18
+ if (label) {
19
+ const lbl = document.createElement('div');
20
+ lbl.textContent = label;
21
+ Object.assign(lbl.style, {
22
+ position: 'fixed',
23
+ left: `${box.x + box.width / 2}px`,
24
+ top: `${box.y + box.height + pad + 16}px`,
25
+ transform: 'translateX(-50%)',
26
+ padding: '10px 18px',
27
+ background: color,
28
+ color: '#fff',
29
+ fontSize: '20px',
30
+ fontWeight: '700',
31
+ borderRadius: '8px',
32
+ fontFamily: 'system-ui, -apple-system, "Helvetica Neue", sans-serif',
33
+ boxShadow: '0 6px 18px rgba(0,0,0,0.4)',
34
+ whiteSpace: 'nowrap',
35
+ });
36
+ root.appendChild(lbl);
37
+ }
38
+ }
@@ -0,0 +1,97 @@
1
+ // Draw a rectangle around `box` with an optional label badge.
2
+ // drawOnMs > 0 → SVG rect with stroke-dashoffset animation: traces from the
3
+ // top-left clockwise (top edge sweeps left→right first).
4
+ // drawOnMs <= 0 → original CSS-border box with a soft pulse loop.
5
+ ({ box, pad, color, lw, label, drawOnMs }) => {
6
+ const root = document.getElementById('__pw_overlay_root');
7
+ if (!root) return;
8
+
9
+ const x = box.x - pad;
10
+ const y = box.y - pad;
11
+ const w = box.width + pad * 2;
12
+ const h = box.height + pad * 2;
13
+ const rx = 8;
14
+
15
+ const animated = drawOnMs && drawOnMs > 0;
16
+
17
+ if (animated) {
18
+ const NS = 'http://www.w3.org/2000/svg';
19
+ const svg = document.createElementNS(NS, 'svg');
20
+ Object.assign(svg.style, {
21
+ position: 'fixed',
22
+ left: `${x - lw}px`,
23
+ top: `${y - lw}px`,
24
+ width: `${w + lw * 2}px`,
25
+ height: `${h + lw * 2}px`,
26
+ overflow: 'visible',
27
+ pointerEvents: 'none',
28
+ filter: `drop-shadow(0 0 14px ${color})`,
29
+ });
30
+ const rect = document.createElementNS(NS, 'rect');
31
+ rect.setAttribute('x', String(lw));
32
+ rect.setAttribute('y', String(lw));
33
+ rect.setAttribute('width', String(w));
34
+ rect.setAttribute('height', String(h));
35
+ rect.setAttribute('rx', String(rx));
36
+ rect.setAttribute('ry', String(rx));
37
+ rect.setAttribute('fill', 'none');
38
+ rect.setAttribute('stroke', color);
39
+ rect.setAttribute('stroke-width', String(lw));
40
+ rect.setAttribute('stroke-linecap', 'round');
41
+
42
+ const dashLen = 2 * (w + h);
43
+ rect.style.strokeDasharray = String(dashLen);
44
+ rect.style.strokeDashoffset = String(dashLen);
45
+ svg.appendChild(rect);
46
+ root.appendChild(svg);
47
+ // Web Animations API: way more reliable than CSS transitions on SVG
48
+ // presentation attributes (Chrome quietly drops the transition if the
49
+ // initial state hasn't been painted yet).
50
+ rect.animate(
51
+ [{ strokeDashoffset: dashLen }, { strokeDashoffset: 0 }],
52
+ { duration: drawOnMs, easing: 'cubic-bezier(0.4, 0, 0.2, 1)', fill: 'forwards' }
53
+ );
54
+ } else {
55
+ const rect = document.createElement('div');
56
+ Object.assign(rect.style, {
57
+ position: 'fixed',
58
+ left: `${x}px`,
59
+ top: `${y}px`,
60
+ width: `${w}px`,
61
+ height: `${h}px`,
62
+ border: `${lw}px solid ${color}`,
63
+ borderRadius: '8px',
64
+ boxShadow: `0 0 0 9999px rgba(0,0,0,0.0), 0 0 24px ${color}`,
65
+ animation: 'pwPulse 1.2s ease-in-out infinite',
66
+ });
67
+ const style = document.createElement('style');
68
+ style.textContent =
69
+ '@keyframes pwPulse { 0%,100%{transform:scale(1);opacity:1} 50%{transform:scale(1.04);opacity:0.85} }';
70
+ root.appendChild(style);
71
+ root.appendChild(rect);
72
+ }
73
+
74
+ if (label) {
75
+ const lbl = document.createElement('div');
76
+ lbl.textContent = label;
77
+ Object.assign(lbl.style, {
78
+ position: 'fixed',
79
+ left: `${x}px`,
80
+ top: `${Math.max(8, y - 40)}px`,
81
+ padding: '6px 12px',
82
+ background: color,
83
+ color: '#fff',
84
+ fontSize: '16px',
85
+ fontWeight: '600',
86
+ borderRadius: '6px',
87
+ fontFamily:
88
+ 'system-ui, -apple-system, "PingFang SC", "Helvetica Neue", sans-serif',
89
+ boxShadow: '0 4px 12px rgba(0,0,0,0.25)',
90
+ whiteSpace: 'nowrap',
91
+ opacity: '0',
92
+ transition: 'opacity 300ms ease',
93
+ });
94
+ root.appendChild(lbl);
95
+ setTimeout(() => (lbl.style.opacity = '1'), animated ? Math.floor(drawOnMs * 0.7) : 0);
96
+ }
97
+ };
@@ -0,0 +1,19 @@
1
+ // Inject the fixed overlay layers used by every storyboard scene.
2
+ // Idempotent: a no-op if already mounted (e.g. across multiple scenes).
3
+ (() => {
4
+ if (window.__pwOverlayReady) return;
5
+ const root = document.createElement('div');
6
+ root.id = '__pw_overlay_root';
7
+ Object.assign(root.style, {
8
+ position: 'fixed', inset: '0', pointerEvents: 'none', zIndex: '2147483646',
9
+ });
10
+ const fade = document.createElement('div');
11
+ fade.id = '__pw_overlay_fade';
12
+ Object.assign(fade.style, {
13
+ position: 'fixed', inset: '0', background: '#000', opacity: '0',
14
+ transition: 'opacity 400ms ease', pointerEvents: 'none', zIndex: '2147483647',
15
+ });
16
+ document.documentElement.appendChild(root);
17
+ document.documentElement.appendChild(fade);
18
+ window.__pwOverlayReady = true;
19
+ })();