feat(preview): cross-browser scrollbar, keyboard nav, dev-mode client simulation, docs overhaul

preview/index.html:
- Fix scrollbar CSS: scoped webkit pseudo-elements + standards-track fallback, removed deprecated overflow:overlay
- Keyboard navigation: arrows cycle focus between Theme/Template/Client selects, up/down select within focused dropdown, d/m toggle viewport
- Dev/static mode detection: delayed static warning only when WebSocket absent, WebSocket errors only on disconnect (not on initial fail)
- iframe sandbox: removed static sandbox attr, applied dynamically only in dev mode (file: protocol rejects sandboxed srcdoc)
- Loading fix: removed double-toggle between init and render(); added 5s safety timeout
- Transform clean: returns HTML as-is; CSS stripping moved to server-side
- Dev disclaimer: blue info banner shown on WebSocket connect

tools/server/inliner.mjs:
- Added stripGmail() / stripOutlook() — server-side CSS property stripping for email client simulation

tools/server/server.mjs:
- Fixed WebSocket upgrade: use app.listen() instead of createServer(app).listen()
- Juice post-processing now generates three rendered.js variants (modern/gmail/outlook)
- Initial startup runs juice-only pass (avoids duplicate Go compilation)

docs/ (all 6 languages — en, zh-CN, zh-TW, ja, ko, ru):
- Image size limits: max 50KiB, recommended 10-20KiB
- Dev disclaimer: simulation cannot 100% reproduce every client
- Changed 'accurate' to 'relatively accurate' across all static-mode warnings
- Converted all plain blockquotes to GitHub admonitions ([!WARNING] / [!NOTE])
This commit is contained in:
KenanZhu committed 2026-06-04 16:01:42 +08:00
1 parent 4bc9a0667f
commit d8a714b2b5
17 files changed
+315 -196

No files matched your search

+64
View File
@@ -19,6 +19,70 @@ export function inlineCSS(html) {
return juice(html, JUICE_OPTIONS);
}
// Gmail / Outlook CSS stripping
const GMAIL_UNSUPPORTED = [
'border-radius','box-shadow','text-shadow',
'backdrop-filter','filter',
'animation','transition','transform',
'background-image\\s*:\\s*(linear|radial|conic)-gradient',
];
const OUTLOOK_UNSUPPORTED = GMAIL_UNSUPPORTED.concat([
'display\\s*:\\s*(flex|grid|inline-flex|inline-grid)',
'flex[^;]*','grid[^;]*','gap\\s*:','order\\s*:',
]);
function stripDeclarations(styleValue, patterns) {
let v = styleValue;
for (const p of patterns) {
v = v.replace(new RegExp('(^|;\\s*)' + p + '[^;]*(;|$)', 'gi'), (m) => m.endsWith(';') ? ';' : '');
v = v.replace(new RegExp(p + '[^;]*;', 'gi'), '');
}
return v.replace(/;;+/g, ';').replace(/^\s*;\s*/, '').replace(/\s*;\s*$/, '').trim();
}
/**
* Strip CSS properties unsupported by Gmail from inline styles and <style> blocks.
*/
export function stripGmail(html) {
return stripClientCSS(html, GMAIL_UNSUPPORTED, false);
}
/**
* Strip CSS properties unsupported by Outlook (more aggressive than Gmail).
*/
export function stripOutlook(html) {
return stripClientCSS(html, OUTLOOK_UNSUPPORTED, true);
}
function stripClientCSS(html, patterns, isOutlook) {
let result = html;
// 1. Strip from inline style="..." attributes
result = result.replace(/style="([^"]*)"/gi, (_, styles) => {
const cleaned = stripDeclarations(styles, patterns);
return cleaned ? 'style="' + cleaned + '"' : '';
});
// 2. Strip from <style> blocks
result = result.replace(/<style[^>]*>([\s\S]*?)<\/style>/gi, (_, cssContent) => {
let cleaned = cssContent;
for (const p of patterns) {
const re = new RegExp(p + '[^;{}]*[;}]?', 'gi');
cleaned = cleaned.replace(re, (m) => m.endsWith('}') ? '}' : '');
}
return '<style>' + cleaned + '</style>';
});
// 3. Outlook: replace CSS custom properties with initial
if (isOutlook) {
result = result.replace(/style="([^"]*)"/gi,
(_, s) => 'style="' + s.replace(/var\(--[^)]*\)/g, 'initial') + '"');
}
return result;
}
/**
* Apply juice inlining to a rendered.js-style HTML snippet and return the
* result. Handles escaped HTML (JSON-stringified) by first unescaping,