Skip to content

Commit 4091c55

Browse files
feat: accessibility, UX polish, and build tooling improvements
- Add touch event support in selection overlay (issue #1) - Add keyboard navigation/ARIA for asset grid (issue #2) - Add Forgot Password flow in AuthForm (issue #3) - Replace simulated progress with XHR real progress (issue #4) - Add restricted tab URL check before captureVisibleTab (issue #5) - Pass output format/quality to offscreen watermark (issue #6) - Add 30s timeout for offscreen operations (issue #7) - Add ctx.save()/ctx.restore() around canvas drawing (issue #8) - Convert share page from raw DOM to React (issue #9) - Extract inline styles to CSS classes (issue #10) - Rename createSignedMetadata to createMetadataPayload (issue #11) - Add sourcemap: 'hidden' to vite.config.ts (issue #12) - Add pre-build version sync validation (issue #13) Co-authored-by: numbers-official <181934381+numbers-official@users.noreply.github.com>
1 parent 62e760d commit 4091c55

14 files changed

Lines changed: 620 additions & 286 deletions

File tree

package-lock.json

Lines changed: 2 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"type": "module",
66
"scripts": {
77
"dev": "vite build --watch --mode development",
8+
"prebuild": "node scripts/validate-version.js",
89
"build": "tsc && vite build",
910
"preview": "vite preview",
1011
"type-check": "tsc --noEmit",

scripts/package.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ const zipPath = join(distZipDir, zipName);
6464

6565
console.log(`Creating ${zipName}...`);
6666

67-
// Use native zip command with absolute path
67+
// Use native zip command with absolute path, excluding source maps
6868
try {
69-
execSync(`zip -r "${zipPath}" .`, {
69+
execSync(`zip -r "${zipPath}" . --exclude "*.map"`, {
7070
cwd: join(rootDir, 'dist'),
7171
stdio: 'inherit'
7272
});

scripts/validate-version.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Pre-build validation: ensure package.json and manifest.template.json have the same version.
4+
*/
5+
6+
import { readFileSync } from 'fs';
7+
import { join, dirname } from 'path';
8+
import { fileURLToPath } from 'url';
9+
10+
const __filename = fileURLToPath(import.meta.url);
11+
const __dirname = dirname(__filename);
12+
const rootDir = join(__dirname, '..');
13+
14+
const pkg = JSON.parse(readFileSync(join(rootDir, 'package.json'), 'utf-8'));
15+
const manifest = JSON.parse(readFileSync(join(rootDir, 'manifest.template.json'), 'utf-8'));
16+
17+
if (pkg.version !== manifest.version) {
18+
console.error(
19+
`Version mismatch: package.json has "${pkg.version}" but manifest.template.json has "${manifest.version}".`
20+
);
21+
console.error('Please keep both versions in sync before building.');
22+
process.exit(1);
23+
}
24+
25+
console.log(`✓ Version check passed: ${pkg.version}`);

src/background/service-worker.ts

Lines changed: 65 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -238,27 +238,34 @@ async function handleSelectionComplete(payload: any) {
238238
// Crop and add watermark via offscreen document
239239
await ensureOffscreenDocument();
240240

241-
const response = await chrome.runtime.sendMessage({
242-
type: 'ADD_WATERMARK',
243-
payload: {
244-
dataUrl,
245-
timestamp: captureTime.toISOString(),
246-
width: coordinates.width,
247-
height: coordinates.height,
248-
timestampSize: settings.timestampSize,
249-
timestampFormat: settings.timestampFormat,
250-
timestampOpacity: settings.timestampOpacity,
251-
timestampPosition: settings.timestampPosition,
252-
includeTimestamp: settings.includeTimestamp,
253-
crop: coordinates,
254-
},
255-
});
256-
257-
if (response.success) {
258-
dataUrl = response.data.dataUrl;
241+
const watermarkResponse = await Promise.race([
242+
chrome.runtime.sendMessage({
243+
type: 'ADD_WATERMARK',
244+
payload: {
245+
dataUrl,
246+
timestamp: captureTime.toISOString(),
247+
width: coordinates.width,
248+
height: coordinates.height,
249+
timestampSize: settings.timestampSize,
250+
timestampFormat: settings.timestampFormat,
251+
timestampOpacity: settings.timestampOpacity,
252+
timestampPosition: settings.timestampPosition,
253+
includeTimestamp: settings.includeTimestamp,
254+
outputFormat: settings.screenshotFormat === 'jpeg' ? 'jpeg' : 'png',
255+
outputQuality: settings.screenshotFormat === 'jpeg' ? settings.screenshotQuality / 100 : undefined,
256+
crop: coordinates,
257+
},
258+
}),
259+
new Promise<never>((_, reject) =>
260+
setTimeout(() => reject(new Error('Offscreen operation timed out')), 30000)
261+
),
262+
]) as any;
263+
264+
if (watermarkResponse.success) {
265+
dataUrl = watermarkResponse.data.dataUrl;
259266
console.log('✅ Selection cropped and watermark added');
260267
} else {
261-
console.warn('Failed to process selection:', response.error);
268+
console.warn('Failed to process selection:', watermarkResponse.error);
262269
}
263270

264271
// Get location if enabled via offscreen document
@@ -418,6 +425,20 @@ async function handleScreenshotCapture(
418425
return await handleSelectionCapture(tab);
419426
}
420427

428+
// Check for restricted URLs that cannot be captured
429+
const restrictedPatterns = [
430+
/^chrome:\/\//,
431+
/^chrome-extension:\/\//,
432+
/^https:\/\/chrome\.google\.com\/webstore/,
433+
/^about:/,
434+
/^edge:\/\//,
435+
];
436+
if (tab.url && restrictedPatterns.some((re) => re.test(tab.url!))) {
437+
throw new Error(
438+
'Screenshots cannot be taken on this page. Please navigate to a regular website and try again.'
439+
);
440+
}
441+
421442
// Capture timestamp at the very start for consistency
422443
const captureTime = new Date();
423444

@@ -439,26 +460,33 @@ async function handleScreenshotCapture(
439460
try {
440461
await ensureOffscreenDocument();
441462

442-
const response = await chrome.runtime.sendMessage({
443-
type: 'ADD_WATERMARK',
444-
payload: {
445-
dataUrl,
446-
timestamp: captureTime.toISOString(),
447-
width,
448-
height,
449-
timestampSize: settings.timestampSize,
450-
timestampFormat: settings.timestampFormat,
451-
timestampOpacity: settings.timestampOpacity,
452-
timestampPosition: settings.timestampPosition,
453-
includeTimestamp: settings.includeTimestamp,
454-
},
455-
});
456-
457-
if (response.success) {
458-
dataUrl = response.data.dataUrl;
463+
const watermarkResponse = await Promise.race([
464+
chrome.runtime.sendMessage({
465+
type: 'ADD_WATERMARK',
466+
payload: {
467+
dataUrl,
468+
timestamp: captureTime.toISOString(),
469+
width,
470+
height,
471+
timestampSize: settings.timestampSize,
472+
timestampFormat: settings.timestampFormat,
473+
timestampOpacity: settings.timestampOpacity,
474+
timestampPosition: settings.timestampPosition,
475+
includeTimestamp: settings.includeTimestamp,
476+
outputFormat: settings.screenshotFormat === 'jpeg' ? 'jpeg' : 'png',
477+
outputQuality: settings.screenshotFormat === 'jpeg' ? settings.screenshotQuality / 100 : undefined,
478+
},
479+
}),
480+
new Promise<never>((_, reject) =>
481+
setTimeout(() => reject(new Error('Offscreen operation timed out')), 30000)
482+
),
483+
]) as any;
484+
485+
if (watermarkResponse.success) {
486+
dataUrl = watermarkResponse.data.dataUrl;
459487
console.log('✅ Watermark added successfully');
460488
} else {
461-
console.warn('Failed to add watermark:', response.error);
489+
console.warn('Failed to add watermark:', watermarkResponse.error);
462490
}
463491
} catch (error) {
464492
console.error('Watermark error:', error);

src/content/selection-overlay.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ if (!(window as any).__proofSnapSelectionActive) {
8989
document.addEventListener('mousemove', handleMouseMove);
9090
document.addEventListener('mouseup', handleMouseUp);
9191
document.addEventListener('keydown', handleKeyDown);
92+
93+
// Touch event support for touchscreen devices
94+
overlay.addEventListener('touchstart', handleTouchStart, { passive: false });
95+
document.addEventListener('touchmove', handleTouchMove, { passive: false });
96+
document.addEventListener('touchend', handleTouchEnd);
9297
}
9398

9499
/**
@@ -175,6 +180,90 @@ if (!(window as any).__proofSnapSelectionActive) {
175180
});
176181
}
177182

183+
/**
184+
* Handle touch start - begin selection on touchscreen devices
185+
*/
186+
function handleTouchStart(e: TouchEvent): void {
187+
e.preventDefault();
188+
const touch = e.touches[0];
189+
isSelecting = true;
190+
startX = touch.clientX;
191+
startY = touch.clientY;
192+
193+
if (selectionBox) {
194+
selectionBox.style.display = 'block';
195+
selectionBox.style.left = `${startX}px`;
196+
selectionBox.style.top = `${startY}px`;
197+
selectionBox.style.width = '0px';
198+
selectionBox.style.height = '0px';
199+
}
200+
201+
if (overlay) {
202+
overlay.style.background = 'transparent';
203+
}
204+
}
205+
206+
/**
207+
* Handle touch move - update selection box on touchscreen devices
208+
*/
209+
function handleTouchMove(e: TouchEvent): void {
210+
e.preventDefault();
211+
if (!isSelecting || !selectionBox) return;
212+
213+
const touch = e.touches[0];
214+
const currentX = touch.clientX;
215+
const currentY = touch.clientY;
216+
217+
const left = Math.min(startX, currentX);
218+
const top = Math.min(startY, currentY);
219+
const width = Math.abs(currentX - startX);
220+
const height = Math.abs(currentY - startY);
221+
222+
selectionBox.style.left = `${left}px`;
223+
selectionBox.style.top = `${top}px`;
224+
selectionBox.style.width = `${width}px`;
225+
selectionBox.style.height = `${height}px`;
226+
}
227+
228+
/**
229+
* Handle touch end - complete selection on touchscreen devices
230+
*/
231+
function handleTouchEnd(e: TouchEvent): void {
232+
if (!isSelecting) return;
233+
234+
isSelecting = false;
235+
236+
const touch = e.changedTouches[0];
237+
const currentX = touch.clientX;
238+
const currentY = touch.clientY;
239+
240+
const left = Math.min(startX, currentX);
241+
const top = Math.min(startY, currentY);
242+
const width = Math.abs(currentX - startX);
243+
const height = Math.abs(currentY - startY);
244+
245+
if (width < 10 || height < 10) {
246+
cleanup();
247+
sendResponse({ cancelled: true, reason: 'Selection too small' });
248+
return;
249+
}
250+
251+
const dpr = window.devicePixelRatio || 1;
252+
const coordinates: SelectionCoordinates = {
253+
x: Math.round(left * dpr),
254+
y: Math.round(top * dpr),
255+
width: Math.round(width * dpr),
256+
height: Math.round(height * dpr),
257+
};
258+
259+
cleanup();
260+
sendResponse({
261+
cancelled: false,
262+
coordinates,
263+
viewportCoordinates: { x: left, y: top, width, height },
264+
});
265+
}
266+
178267
/**
179268
* Handle key down - cancel on Escape
180269
*/
@@ -203,6 +292,12 @@ if (!(window as any).__proofSnapSelectionActive) {
203292
document.removeEventListener('mouseup', handleMouseUp);
204293
document.removeEventListener('keydown', handleKeyDown);
205294

295+
if (overlay) {
296+
overlay.removeEventListener('touchstart', handleTouchStart);
297+
}
298+
document.removeEventListener('touchmove', handleTouchMove);
299+
document.removeEventListener('touchend', handleTouchEnd);
300+
206301
const elements = [
207302
'proofsnap-selection-overlay',
208303
'proofsnap-selection-box',

src/offscreen/offscreen.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ interface WatermarkPayload {
1616
timestampOpacity?: number;
1717
timestampPosition?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
1818
includeTimestamp?: boolean;
19+
outputFormat?: 'png' | 'jpeg' | 'webp';
20+
outputQuality?: number;
1921
// Crop coordinates (optional)
2022
crop?: {
2123
x: number;
@@ -151,19 +153,31 @@ async function addWatermark(payload: WatermarkPayload): Promise<{ dataUrl: strin
151153

152154
// Add timestamp if enabled
153155
if (payload.includeTimestamp !== false) {
156+
ctx.save();
154157
drawTimestamp(ctx, payload.timestamp, {
155158
size: payload.timestampSize,
156159
format: payload.timestampFormat,
157160
opacity: payload.timestampOpacity,
158161
position: payload.timestampPosition,
159162
});
163+
ctx.restore();
160164
}
161165

162166
// Always draw logo
167+
ctx.save();
163168
await drawLogo(ctx, canvas.width, canvas.height);
164-
165-
// Convert to data URL
166-
return { dataUrl: canvas.toDataURL('image/png') };
169+
ctx.restore();
170+
171+
// Convert to data URL using the requested format
172+
const mimeType =
173+
payload.outputFormat === 'jpeg' ? 'image/jpeg' :
174+
payload.outputFormat === 'webp' ? 'image/webp' :
175+
'image/png';
176+
const quality =
177+
(payload.outputFormat === 'jpeg' || payload.outputFormat === 'webp')
178+
? (payload.outputQuality ?? 0.9)
179+
: undefined;
180+
return { dataUrl: canvas.toDataURL(mimeType, quality) };
167181
}
168182

169183
/**

0 commit comments

Comments
 (0)