Front-end fixes
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -42,5 +42,8 @@ tests/e2e/test_firmware.zip
|
||||
# NickelMenu build artifacts
|
||||
nickelmenu/kobo-config/
|
||||
|
||||
# Proposals
|
||||
proposals/
|
||||
|
||||
# Claude
|
||||
.claude
|
||||
|
||||
@@ -1150,3 +1150,57 @@ test.describe('Custom patches', () => {
|
||||
await expect(page.locator('#step-connect')).not.toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Build output
|
||||
// ============================================================
|
||||
|
||||
test.describe('Build output', () => {
|
||||
const path = require('path');
|
||||
const distDir = path.join(__dirname, '..', '..', 'web', 'dist');
|
||||
|
||||
test('CSS cache-bust hash is present on style.css link', async () => {
|
||||
const html = fs.readFileSync(path.join(distDir, 'index.html'), 'utf-8');
|
||||
expect(html).toMatch(/css\/style\.css\?h=[0-9a-f]{8}/);
|
||||
});
|
||||
|
||||
test('JS cache-bust hash is present on bundle.js script', async () => {
|
||||
const html = fs.readFileSync(path.join(distDir, 'index.html'), 'utf-8');
|
||||
expect(html).toMatch(/bundle\.js\?h=[0-9a-f]{8}/);
|
||||
});
|
||||
|
||||
test('critical CSS is inlined with :root tokens', async () => {
|
||||
const html = fs.readFileSync(path.join(distDir, 'index.html'), 'utf-8');
|
||||
// :root block should be inside an inline <style> tag, not in a <link>
|
||||
expect(html).toMatch(/<style>[^<]*:root\{[^}]*--primary:/);
|
||||
// var() references should be used (not hardcoded hex colors for themed values)
|
||||
expect(html).toMatch(/<style>[^<]*var\(--primary\)/);
|
||||
});
|
||||
|
||||
test('style.css does not contain a :root block', async () => {
|
||||
const css = fs.readFileSync(path.join(distDir, 'css', 'style.css'), 'utf-8');
|
||||
expect(css).not.toContain(':root');
|
||||
});
|
||||
|
||||
test('--primary-hover differs from --primary', async () => {
|
||||
const critical = fs.readFileSync(
|
||||
path.join(__dirname, '..', '..', 'web', 'src', 'css', 'critical.css'), 'utf-8'
|
||||
);
|
||||
const primary = critical.match(/--primary:\s*([^;]+);/);
|
||||
const hover = critical.match(/--primary-hover:\s*([^;]+);/);
|
||||
expect(primary).not.toBeNull();
|
||||
expect(hover).not.toBeNull();
|
||||
expect(primary[1].trim()).not.toBe(hover[1].trim());
|
||||
});
|
||||
|
||||
test('no jszip script tag in built HTML', async () => {
|
||||
const html = fs.readFileSync(path.join(distDir, 'index.html'), 'utf-8');
|
||||
expect(html).not.toContain('jszip');
|
||||
});
|
||||
|
||||
test('no unreplaced template placeholders in built HTML', async () => {
|
||||
const html = fs.readFileSync(path.join(distDir, 'index.html'), 'utf-8');
|
||||
expect(html).not.toMatch(/\{\{[\w-]+\}\}/);
|
||||
expect(html).not.toContain('@critical-css');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -102,6 +102,18 @@ async function build() {
|
||||
|
||||
let html = readFileSync(join(srcDir, 'index.html'), 'utf-8');
|
||||
|
||||
// Inline critical.css into the <head> so :root tokens and loading styles
|
||||
// are available before style.css arrives on slow connections.
|
||||
const criticalCss = readFileSync(join(srcDir, 'css', 'critical.css'), 'utf-8');
|
||||
const { code: criticalMinified } = await esbuild.transform(criticalCss, {
|
||||
loader: 'css',
|
||||
minify: !isDev && !isWatch,
|
||||
});
|
||||
html = html.replace(
|
||||
'<!-- @critical-css -->',
|
||||
`<style>${criticalMinified.trimEnd()}</style>`
|
||||
);
|
||||
|
||||
// Remove all <script src="js/..."> tags
|
||||
html = html.replace(/\s*<script src="js\/[^"]*"><\/script>\n/g, '');
|
||||
// Add the bundle script before </body>
|
||||
@@ -112,7 +124,7 @@ async function build() {
|
||||
|
||||
// Update CSS cache bust
|
||||
html = html.replace(
|
||||
/css\/style\.css\?[^"]*/,
|
||||
/css\/style\.css(?:\?[^"]*)?/,
|
||||
`css/style.css?h=${cssHash}`
|
||||
);
|
||||
|
||||
|
||||
46
web/src/css/critical.css
Normal file
46
web/src/css/critical.css
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Critical CSS — inlined into index.html at build time.
|
||||
*
|
||||
* Contains the :root design tokens and the minimal styles needed to render
|
||||
* the hero header and loading spinner before style.css arrives. Because this
|
||||
* is inlined, var() references resolve immediately.
|
||||
*
|
||||
* This is the single source of truth for all CSS custom properties.
|
||||
* style.css uses these variables but does not redefine them.
|
||||
*/
|
||||
|
||||
:root {
|
||||
--bg: #f5f5f7;
|
||||
--card-bg: #fff;
|
||||
--border: #d1d5db;
|
||||
--border-light: #e5e7eb;
|
||||
--text: #111827;
|
||||
--text-secondary: #6b7280;
|
||||
--primary: #01a7c4;
|
||||
--primary-hover: #018da6;
|
||||
--primary-light: #eff6ff;
|
||||
--error-bg: #fef2f2;
|
||||
--error-border: #fca5a5;
|
||||
--error-text: #991b1b;
|
||||
--warning-bg: #fffbeb;
|
||||
--warning-border: #fcd34d;
|
||||
--warning-text: #92400e;
|
||||
--success-bg: #f0fdf4;
|
||||
--success-border: #86efac;
|
||||
--success-text: #166534;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
body { background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif; margin: 0; }
|
||||
main { max-width: 640px; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
|
||||
.hero { margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--border-light); }
|
||||
h1 { font-size: 1.75rem; font-weight: 700; letter-spacing: -0.02em; margin: 0; }
|
||||
.hero-accent { color: var(--primary); font-weight: 600; }
|
||||
.beta-pill { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; background: var(--primary); color: #fff; padding: 0.35rem 0.7rem; border-radius: 10px; vertical-align: middle; position: relative; top: -0.15rem; margin-left: 5px; }
|
||||
.subtitle { color: var(--text-secondary); font-size: 0.95rem; margin-top: 0.25rem; }
|
||||
.initial-loader { display: flex; align-items: center; justify-content: center; gap: 0.75rem; padding: 2rem 0; }
|
||||
.initial-loader p { margin: 0; font-size: 0.93rem; color: var(--text-secondary); }
|
||||
.spinner { width: 22px; height: 22px; border: 2.5px solid var(--border-light); border-top-color: var(--primary); border-radius: 50%; animation: spin 0.7s linear infinite; flex-shrink: 0; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
[hidden] { display: none !important; }
|
||||
@@ -6,41 +6,21 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg: #f5f5f7;
|
||||
--card-bg: #fff;
|
||||
--border: #d1d5db;
|
||||
--border-light: #e5e7eb;
|
||||
--text: #111827;
|
||||
--text-secondary: #6b7280;
|
||||
--primary: #01a7c4;
|
||||
--primary-hover: #01a7c4;
|
||||
--primary-light: #eff6ff;
|
||||
--error-bg: #fef2f2;
|
||||
--error-border: #fca5a5;
|
||||
--error-text: #991b1b;
|
||||
--warning-bg: #fffbeb;
|
||||
--warning-border: #fcd34d;
|
||||
--warning-text: #92400e;
|
||||
--success-bg: #f0fdf4;
|
||||
--success-border: #86efac;
|
||||
--success-text: #166534;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
|
||||
button, input, select, textarea {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* Design tokens (:root variables) are defined in critical.css, which is
|
||||
inlined into <head> at build time — do not redefine them here. */
|
||||
|
||||
body {
|
||||
font-family: "Google Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif !important;
|
||||
font-family: "Google Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: "Google Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif !important;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
@@ -863,6 +843,7 @@ input[type="file"] {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Also defined in critical.css (inlined into <head>) for the pre-CSS loading spinner */
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@@ -55,23 +55,9 @@
|
||||
<link rel="preload" href="/patches/downloads.json" as="fetch" crossorigin>
|
||||
<link rel="preload" href="/patches/index.json" as="fetch" crossorigin>
|
||||
|
||||
<!-- Inline critical styles so the hero + loader render before style.css arrives on slow connections -->
|
||||
<style>
|
||||
body { background: #f5f5f7; color: #111827; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif; margin: 0; }
|
||||
main { max-width: 640px; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
|
||||
.hero { margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid #e5e7eb; }
|
||||
h1 { font-size: 1.75rem; font-weight: 700; letter-spacing: -0.02em; margin: 0; }
|
||||
.hero-accent { color: #00bedf; font-weight: 600; }
|
||||
.beta-pill { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; background: #00bedf; color: #fff; padding: 0.35rem 0.7rem; border-radius: 10px; vertical-align: middle; position: relative; top: -0.15rem; margin-left: 5px; }
|
||||
.subtitle { color: #6b7280; font-size: 0.95rem; margin-top: 0.25rem; }
|
||||
.initial-loader { display: flex; align-items: center; justify-content: center; gap: 0.75rem; padding: 2rem 0; }
|
||||
.initial-loader p { margin: 0; font-size: 0.93rem; color: #6b7280; }
|
||||
.spinner { width: 22px; height: 22px; border: 2.5px solid #e5e7eb; border-top-color: #00bedf; border-radius: 50%; animation: spin 0.7s linear infinite; flex-shrink: 0; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
[hidden] { display: none !important; }
|
||||
</style>
|
||||
<!-- Inlined at build time from css/critical.css so the hero + loader render before style.css arrives -->
|
||||
<!-- @critical-css -->
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<script src="js/jszip.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
|
||||
Reference in New Issue
Block a user