Skip to content

Commit d0eae68

Browse files
committed
Add reduced-motion, human-like scroll, and delete command
- Use prefers-reduced-motion to skip CSS animations - Progress GSAP animations to 100% completion state - Human-like scrolling with easing and pauses - Wait for network idle after scroll - Add smippo delete command for interactive site cleanup
1 parent 3b4f2b8 commit d0eae68

5 files changed

Lines changed: 458 additions & 185 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "smippo",
3-
"version": "0.1.1",
3+
"version": "0.1.2",
44
"description": "S.M.I.P.P.O. — Structured Mirroring of Internet Pages and Public Objects. Modern website copier that captures sites exactly as they appear in your browser.",
55
"main": "src/index.js",
66
"bin": {

src/cli.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@ export function run() {
130130
'--no-reveal-all',
131131
'Disable force-reveal of scroll-triggered content',
132132
)
133+
.option(
134+
'--reduced-motion',
135+
'Use prefers-reduced-motion for accessibility (default: true)',
136+
)
137+
.option('--no-reduced-motion', 'Disable reduced motion preference')
133138
.option('--user-agent <string>', 'Custom user agent')
134139
.option('--viewport <WxH>', 'Viewport size', '1920x1080')
135140
.option('--device <name>', 'Emulate device (e.g., "iPhone 13")')
@@ -256,6 +261,18 @@ export function run() {
256261
});
257262
});
258263

264+
// Delete command - remove captured sites
265+
program
266+
.command('delete')
267+
.alias('rm')
268+
.description('Delete captured sites from local storage')
269+
.option('-a, --all', 'Delete all captured sites')
270+
.option('-y, --yes', 'Skip confirmation prompt')
271+
.action(async options => {
272+
const {deleteSites} = await import('./delete.js');
273+
await deleteSites(options);
274+
});
275+
259276
// Screenshot capture command
260277
program
261278
.command('capture <url>')
@@ -357,6 +374,7 @@ async function capture(url, options) {
357374
scrollDelay: parseInt(options.scrollDelay, 10),
358375
scrollBehavior: options.scrollBehavior,
359376
revealAll: options.revealAll,
377+
reducedMotion: options.reducedMotion,
360378
userAgent: options.userAgent,
361379
viewport: parseViewport(options.viewport),
362380
device: options.device,

src/crawler.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ export class Crawler extends EventEmitter {
276276
scrollDelay: this.options.scrollDelay,
277277
scrollBehavior: this.options.scrollBehavior,
278278
revealAll: this.options.revealAll,
279+
reducedMotion: this.options.reducedMotion,
279280
});
280281

281282
const result = await capture.capture(url);

src/delete.js

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// @flow
2+
import * as p from '@clack/prompts';
3+
import chalk from 'chalk';
4+
import fs from 'fs-extra';
5+
import path from 'path';
6+
import {readGlobalManifest, writeGlobalManifest} from './utils/home.js';
7+
8+
/**
9+
* Format file size for display
10+
*/
11+
function formatSize(bytes) {
12+
if (bytes < 1024) return `${bytes} B`;
13+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
14+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
15+
}
16+
17+
/**
18+
* Calculate directory size recursively
19+
*/
20+
async function getDirectorySize(dirPath) {
21+
let totalSize = 0;
22+
23+
try {
24+
const entries = await fs.readdir(dirPath, {withFileTypes: true});
25+
for (const entry of entries) {
26+
const fullPath = path.join(dirPath, entry.name);
27+
if (entry.isDirectory()) {
28+
totalSize += await getDirectorySize(fullPath);
29+
} else {
30+
const stats = await fs.stat(fullPath);
31+
totalSize += stats.size;
32+
}
33+
}
34+
} catch (e) {
35+
// Ignore errors
36+
}
37+
38+
return totalSize;
39+
}
40+
41+
/**
42+
* Interactive delete command for captured sites
43+
*/
44+
export async function deleteSites(options = {}) {
45+
// Read global manifest
46+
const manifest = await readGlobalManifest();
47+
48+
if (!manifest.sites || manifest.sites.length === 0) {
49+
console.log(chalk.yellow('\nNo captured sites found.\n'));
50+
return;
51+
}
52+
53+
// Show header
54+
console.log('');
55+
p.intro(chalk.bgRed.white(' Delete Captured Sites '));
56+
57+
// Get site info with sizes
58+
const sitesWithInfo = await Promise.all(
59+
manifest.sites.map(async site => {
60+
const exists = await fs.pathExists(site.path);
61+
let size = 0;
62+
if (exists) {
63+
size = await getDirectorySize(site.path);
64+
}
65+
return {
66+
...site,
67+
exists,
68+
size,
69+
displaySize: formatSize(size),
70+
};
71+
}),
72+
);
73+
74+
// Filter to only existing sites
75+
const existingSites = sitesWithInfo.filter(s => s.exists);
76+
77+
if (existingSites.length === 0) {
78+
console.log(chalk.yellow('No captured sites found on disk.\n'));
79+
80+
// Clean up manifest
81+
manifest.sites = [];
82+
await writeGlobalManifest(manifest);
83+
console.log(chalk.dim('Cleaned up manifest.\n'));
84+
return;
85+
}
86+
87+
let sitesToDelete = [];
88+
89+
if (options.all) {
90+
// Delete all sites
91+
sitesToDelete = existingSites;
92+
} else {
93+
// Interactive selection
94+
const selected = await p.multiselect({
95+
message: 'Select sites to delete (space to select, enter to confirm):',
96+
options: existingSites.map(site => ({
97+
value: site,
98+
label: `${site.domain}`,
99+
hint: `${site.displaySize}${site.path}`,
100+
})),
101+
required: false,
102+
});
103+
104+
if (p.isCancel(selected) || !selected || selected.length === 0) {
105+
p.cancel('No sites selected.');
106+
return;
107+
}
108+
109+
sitesToDelete = selected;
110+
}
111+
112+
// Show what will be deleted
113+
console.log('');
114+
console.log(chalk.bold('Sites to delete:'));
115+
for (const site of sitesToDelete) {
116+
console.log(chalk.red(` • ${site.domain} (${site.displaySize})`));
117+
console.log(chalk.dim(` ${site.path}`));
118+
}
119+
console.log('');
120+
121+
// Calculate total size
122+
const totalSize = sitesToDelete.reduce((sum, s) => sum + s.size, 0);
123+
console.log(
124+
chalk.bold(
125+
`Total: ${sitesToDelete.length} site(s), ${formatSize(totalSize)}`,
126+
),
127+
);
128+
console.log('');
129+
130+
// Confirmation
131+
let confirmed = options.yes;
132+
if (!confirmed) {
133+
confirmed = await p.confirm({
134+
message: 'Are you sure you want to delete these sites?',
135+
initialValue: false,
136+
});
137+
}
138+
139+
if (p.isCancel(confirmed) || !confirmed) {
140+
p.cancel('Deletion cancelled.');
141+
return;
142+
}
143+
144+
// Delete the sites
145+
const spinner = p.spinner();
146+
spinner.start('Deleting sites...');
147+
148+
let deletedCount = 0;
149+
let failedCount = 0;
150+
const deletedDomains = new Set();
151+
152+
for (const site of sitesToDelete) {
153+
try {
154+
await fs.remove(site.path);
155+
deletedCount++;
156+
deletedDomains.add(site.domain);
157+
} catch (error) {
158+
failedCount++;
159+
console.error(
160+
chalk.red(`\n Failed to delete ${site.domain}: ${error.message}`),
161+
);
162+
}
163+
}
164+
165+
// Update manifest - remove deleted sites
166+
manifest.sites = manifest.sites.filter(
167+
s =>
168+
!deletedDomains.has(s.domain) ||
169+
!sitesToDelete.some(d => d.path === s.path),
170+
);
171+
await writeGlobalManifest(manifest);
172+
173+
spinner.stop('Deletion complete!');
174+
175+
// Summary
176+
console.log('');
177+
if (deletedCount > 0) {
178+
console.log(
179+
chalk.green(
180+
`✓ Deleted ${deletedCount} site(s), freed ${formatSize(totalSize)}`,
181+
),
182+
);
183+
}
184+
if (failedCount > 0) {
185+
console.log(chalk.red(`✗ Failed to delete ${failedCount} site(s)`));
186+
}
187+
console.log('');
188+
}

0 commit comments

Comments
 (0)