Skip to content

Commit e7e1892

Browse files
MaxGhenisclaude
andcommitted
Polish UI with entrance animations, recording vignette, and micro-interactions
Add staggered fade-up entrance animations, logo blur-reveal, idle glow on record mark, selection pop on presets/formats, recording state vignette overlay, and plan text transitions. Extract popup business logic into popup-logic.js for testability. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c285208 commit e7e1892

16 files changed

Lines changed: 1742 additions & 416 deletions

CLAUDE.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ Scrollywood is a Chrome extension (Manifest V3) that captures smooth scroll vide
1313
bun install
1414

1515
# Run tests
16-
bun test
16+
bun run test
1717

1818
# Run tests in watch mode
19-
bun test:watch
19+
bun run test:watch
2020

2121
# Build extension ZIP for Chrome Web Store
2222
bun run build
@@ -57,6 +57,7 @@ background.js → Downloads video via chrome.downloads API
5757
- **background-logic.js**: Testable business logic extracted from the service worker (badge management, offscreen setup, recording state)
5858
- **scroll-utils.js**: Pure functions for scroll calculations, used in tests
5959
- **scripts/build.js**: Packages extension files into a ZIP for distribution
60+
- **popup.html?tab=<id>**: Popup supports a tab override for debugging and automation when opened directly
6061

6162
## Testing
6263

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ One-click smooth scroll video recording for any webpage. Capture beautiful, cine
3030
bun install
3131

3232
# Run tests
33-
bun test
33+
bun run test
3434

3535
# Run tests in watch mode
36-
bun test:watch
36+
bun run test:watch
3737

3838
# Build extension ZIP for distribution
3939
bun run build
@@ -100,6 +100,16 @@ Scrollywood/
100100

101101
Chrome Manifest V3 service workers don't have DOM access, but `MediaRecorder` requires it. The offscreen document provides a DOM context for video recording while the service worker handles orchestration.
102102

103+
### Popup testing
104+
105+
For debugging or automation, the popup can target a specific tab when opened directly:
106+
107+
```text
108+
chrome-extension://<extension-id>/popup.html?tab=<tab-id>
109+
```
110+
111+
Without the `tab` query param, the popup falls back to the current active tab.
112+
103113
## License
104114

105115
MIT

background-logic.js

Lines changed: 118 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,122 @@
11
// Background logic - testable functions
22

3+
import {
4+
getScrollBehaviorOverrideCSS,
5+
getScrollbarHideCSS,
6+
SCROLL_OVERRIDE_ID,
7+
} from './scroll-utils.js';
8+
39
let recording = false;
10+
const DEFAULT_OPTIONS = {
11+
duration: 60,
12+
delay: 2,
13+
format: 'webm',
14+
};
15+
16+
function normalizeDuration(duration) {
17+
const value = Number.parseInt(duration, 10);
18+
if (Number.isNaN(value)) {
19+
return DEFAULT_OPTIONS.duration;
20+
}
21+
22+
return Math.min(300, Math.max(5, value));
23+
}
24+
25+
function normalizeDelay(delay) {
26+
const value = Number.parseInt(delay, 10);
27+
if (Number.isNaN(value)) {
28+
return DEFAULT_OPTIONS.delay;
29+
}
30+
31+
return Math.min(10, Math.max(0, value));
32+
}
33+
34+
function normalizeFormat(format) {
35+
return format === 'gif' ? 'gif' : DEFAULT_OPTIONS.format;
36+
}
437

538
function sleep(ms) {
639
return new Promise(resolve => setTimeout(resolve, ms));
740
}
841

42+
const PRE_CAPTURE_CSS = `${getScrollBehaviorOverrideCSS()}\n${getScrollbarHideCSS()}`;
43+
44+
async function prepareCaptureSurface(tabId) {
45+
await chrome.scripting.executeScript({
46+
target: { tabId, allFrames: true },
47+
func: (overrideId, css) => {
48+
window.scrollTo({ top: 0, behavior: 'instant' });
49+
50+
const mount = document.head || document.documentElement;
51+
if (!mount) {
52+
return;
53+
}
54+
55+
let styleOverride = document.getElementById(overrideId);
56+
if (!styleOverride) {
57+
styleOverride = document.createElement('style');
58+
styleOverride.id = overrideId;
59+
mount.appendChild(styleOverride);
60+
}
61+
62+
styleOverride.textContent = css;
63+
},
64+
args: [SCROLL_OVERRIDE_ID, PRE_CAPTURE_CSS],
65+
});
66+
}
67+
68+
async function clearCaptureSurface(tabId) {
69+
if (!tabId) {
70+
return;
71+
}
72+
73+
try {
74+
await chrome.scripting.executeScript({
75+
target: { tabId, allFrames: true },
76+
func: (overrideId) => {
77+
if (typeof window.__scrollywoodCancel === 'function') {
78+
window.__scrollywoodCancel();
79+
return;
80+
}
81+
82+
const styleOverride = document.getElementById(overrideId);
83+
if (styleOverride) {
84+
styleOverride.remove();
85+
}
86+
},
87+
args: [SCROLL_OVERRIDE_ID],
88+
});
89+
} catch (error) {
90+
console.warn('Failed to clear capture surface:', error);
91+
}
92+
}
93+
994
export async function startRecording(tabId, duration, delay, format) {
10-
if (recording) return;
95+
if (!tabId) {
96+
return {
97+
started: false,
98+
message: 'No active tab is available to capture.',
99+
};
100+
}
101+
102+
if (recording) {
103+
return {
104+
started: false,
105+
message: 'Recording already in progress.',
106+
};
107+
}
108+
11109
recording = true;
12-
format = format || 'webm';
110+
const normalizedDuration = normalizeDuration(duration);
111+
const normalizedDelay = normalizeDelay(delay);
112+
const normalizedFormat = normalizeFormat(format);
13113

14114
// Show "REC" badge
15115
chrome.action.setBadgeText({ text: 'REC' });
16116
chrome.action.setBadgeBackgroundColor({ color: '#ff6b6b' });
17117

18118
try {
19-
// Scroll to top first (allFrames to handle iframe-wrapped pages)
20-
await chrome.scripting.executeScript({
21-
target: { tabId, allFrames: true },
22-
func: () => window.scrollTo({ top: 0, behavior: 'instant' }),
23-
});
119+
await prepareCaptureSurface(tabId);
24120

25121
await sleep(500);
26122

@@ -43,15 +139,27 @@ export async function startRecording(tabId, duration, delay, format) {
43139
action: 'startCapture',
44140
streamId,
45141
tabId,
46-
duration,
47-
delay,
48-
format,
142+
duration: normalizedDuration,
143+
delay: normalizedDelay,
144+
format: normalizedFormat,
49145
});
50146

147+
return {
148+
started: true,
149+
duration: normalizedDuration,
150+
delay: normalizedDelay,
151+
format: normalizedFormat,
152+
};
153+
51154
} catch (error) {
52155
console.error('Recording error:', error);
156+
await clearCaptureSurface(tabId);
53157
recording = false;
54158
chrome.action.setBadgeText({ text: '' });
159+
return {
160+
started: false,
161+
message: error.message || 'Unable to start recording.',
162+
};
55163
}
56164
}
57165

0 commit comments

Comments
 (0)