1
0

Restructured CSS files

This commit is contained in:
2026-03-24 12:46:49 +01:00
parent 2a87ec67af
commit f24b1b7759
23 changed files with 1222 additions and 1291 deletions

View File

@@ -235,7 +235,7 @@ test.describe('NickelMenu', () => {
await expect(page.locator('#step-nickelmenu')).not.toBeHidden();
// Remove option should be disabled (no device connected)
await expect(page.locator('#nm-option-remove')).toHaveClass(/nm-option-disabled/);
await expect(page.locator('#nm-option-remove')).toHaveClass(/selection-card--disabled/);
await expect(page.locator('input[name="nm-option"][value="remove"]')).toBeDisabled();
});
@@ -257,7 +257,7 @@ test.describe('NickelMenu', () => {
await expect(page.locator('#step-nickelmenu')).not.toBeHidden();
// Remove option should be disabled (no NickelMenu installed)
await expect(page.locator('#nm-option-remove')).toHaveClass(/nm-option-disabled/);
await expect(page.locator('#nm-option-remove')).toHaveClass(/selection-card--disabled/);
// Select "Install NickelMenu and configure"
await page.click('input[name="nm-option"][value="preset"]');
@@ -360,7 +360,7 @@ test.describe('NickelMenu', () => {
await expect(page.locator('#step-nickelmenu')).not.toBeHidden();
// Remove option should be enabled (NickelMenu is installed)
await expect(page.locator('#nm-option-remove')).not.toHaveClass(/nm-option-disabled/);
await expect(page.locator('#nm-option-remove')).not.toHaveClass(/selection-card--disabled/);
await expect(page.locator('input[name="nm-option"][value="remove"]')).not.toBeDisabled();
// Select remove

View File

@@ -44,7 +44,7 @@ test('capture all steps', async ({ page }, testInfo) => {
await shot(page, '02-connect-instructions', testInfo);
// 2b. Connection instructions with disclaimer open
await page.click('.disclaimer summary');
await page.click('details.banner--accent summary');
await page.waitForTimeout(100);
await shot(page, '03-connect-instructions-disclaimer', testInfo);

View File

@@ -50,14 +50,15 @@ async function build() {
// Copy all of src/ to dist/, skipping js/ (bundled separately), css/ (minified), and index.html (generated)
copyDir(srcDir, distDir, new Set(['js', 'css', 'index.html']));
// Minify CSS
// Bundle and minify CSS (@import statements are resolved by esbuild)
mkdirSync(join(distDir, 'css'), { recursive: true });
const cssSrc = readFileSync(join(srcDir, 'css', 'style.css'), 'utf-8');
const { code: cssMinified } = await esbuild.transform(cssSrc, {
loader: 'css',
await esbuild.build({
entryPoints: [join(srcDir, 'css', 'style.css')],
bundle: true,
outfile: join(distDir, 'css', 'style.css'),
minify: !isDev && !isWatch,
logLevel: 'warning',
});
writeFileSync(join(distDir, 'css', 'style.css'), cssMinified);
// Copy worker files from src/js/ (not bundled, served separately)
mkdirSync(join(distDir, 'js'), { recursive: true });

66
web/src/css/base.css Normal file
View File

@@ -0,0 +1,66 @@
/* Reset */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
button, input, select, textarea {
font-family: inherit;
}
/* Typography & body */
body {
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;
}
main {
max-width: 640px;
margin: 0 auto;
padding: 2rem 1.5rem 4rem;
}
h2 {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0.75rem;
color: var(--text);
}
/* Firmware file input */
input[type="file"] {
display: block;
margin-bottom: 1rem;
font-size: 0.88rem;
}
/* Select */
select {
display: block;
width: 100%;
padding: 0.5rem 0.75rem;
font-size: 0.93rem;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--card-bg);
color: var(--text);
margin-bottom: 1rem;
box-shadow: var(--shadow);
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%236b7280' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.75rem center;
padding-right: 2rem;
}
select:focus,
button:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}

View File

@@ -0,0 +1,95 @@
/* Unified banner component.
Replaces the old .notice, .warning, .error, .hint, .info-banner, .disclaimer
classes with a single base + color modifiers. */
.banner {
padding: 0.75rem 1rem;
border: 1px solid;
border-radius: 8px;
font-size: 0.88rem;
line-height: 1.5;
}
.banner-heading {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 0.3rem;
}
/* --- Color modifiers --- */
.banner--info {
background: var(--info-bg);
border-color: var(--info-border);
color: var(--info-text);
& a {
color: var(--info-text);
text-decoration: underline;
}
}
.banner--warning {
background: var(--warning-bg);
border-color: var(--warning-border);
color: var(--warning-text);
& a {
color: var(--warning-text);
text-decoration: underline;
}
}
.banner--error {
background: var(--error-bg);
border-color: var(--error-border);
color: var(--error-text);
}
.banner--success {
background: var(--success-bg);
border-color: var(--success-border);
color: var(--success-text);
}
.banner--accent {
background: var(--primary-light);
border-color: var(--accent-border);
color: var(--accent-text);
}
/* --- Context-specific margins --- */
#step-connect > .banner { margin-bottom: 1.5rem; }
#device-unknown-warning { margin-bottom: 1.5rem; }
#existing-tgz-warning { margin-bottom: 1.5rem; }
#error-message { margin-top: 1rem; }
.install-instructions .banner--warning { margin-bottom: 0.75rem; }
.install-instructions .banner--success { margin-top: 0; }
.step .banner--success { margin-top: 1rem; }
/* --- Disclaimer (details/summary variant) --- */
details.banner--accent {
font-size: 0.80rem;
line-height: 1.55;
& summary {
cursor: pointer;
font-size: 0.83rem;
}
& ol {
margin: 0.5rem 0 0 1.25rem;
font-size: 0.80rem;
color: inherit;
}
& li {
margin-bottom: 0.25rem;
}
}

View File

@@ -0,0 +1,59 @@
button {
font-size: 0.9rem;
padding: 0.55rem 1.25rem;
border-radius: 8px;
border: 1px solid var(--border);
cursor: pointer;
font-weight: 500;
transition: all 0.15s ease;
&.primary {
background: var(--primary);
color: #fff;
border-color: var(--primary);
box-shadow: var(--shadow);
&:hover {
background: var(--primary-hover);
border-color: var(--primary-hover);
box-shadow: var(--shadow-md);
}
}
&.secondary {
background: var(--card-bg);
color: var(--text);
box-shadow: var(--shadow);
&:hover {
background: #f9fafb;
border-color: #9ca3af;
}
}
&.danger {
background: #fff;
color: var(--error-text);
border-color: var(--error-border);
&:hover {
background: var(--error-bg);
border-color: var(--error-text);
}
}
&:disabled {
opacity: 0.4;
cursor: not-allowed;
box-shadow: none;
}
&.btn-success,
&.btn-success:hover {
background: var(--success-text);
border-color: var(--success-text);
color: #fff;
cursor: default;
opacity: 0.7;
}
}

View File

@@ -0,0 +1,33 @@
.info-card {
background: var(--card-bg);
border: 1px solid var(--border-light);
border-radius: 10px;
padding: 0.75rem 1.25rem;
margin-bottom: 1rem;
box-shadow: var(--shadow);
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid var(--border-light);
&:last-child {
border-bottom: none;
}
& .label {
font-weight: 500;
color: var(--text-secondary);
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.03em;
}
& .value {
font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
font-size: 0.88rem;
}
}

View File

@@ -0,0 +1,105 @@
.modal {
border: none;
border-radius: 12px;
padding: 0;
max-width: 560px;
width: calc(100% - 2rem); /* 1rem breathing room on each side */
max-height: 80vh;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin: 0;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05);
&::backdrop {
background: rgba(0, 0, 0, 0.4);
}
}
.modal-content {
display: flex;
flex-direction: column;
max-height: 80vh;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--border-light);
flex-shrink: 0;
& h2 {
margin-bottom: 0;
font-size: 1rem;
}
}
.modal-close {
background: none;
border: none;
font-size: 1.4rem;
color: var(--text-secondary);
cursor: pointer;
padding: 0 0.25rem;
line-height: 1;
box-shadow: none;
&:hover {
color: var(--text);
}
}
.modal-body {
padding: 1.25rem;
overflow-y: auto;
font-size: 0.88rem;
color: var(--text-secondary);
line-height: 1.7;
& h3 {
font-size: 0.88rem;
font-weight: 600;
color: var(--text);
margin-top: 1.25rem;
margin-bottom: 0.5rem;
}
& p {
margin-bottom: 0.75rem;
}
& ol,
& ul {
margin: 0 0 0.75rem 1.25rem;
}
& li {
margin-bottom: 0.5rem;
}
& a {
color: var(--primary);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
& code {
font-size: 0.8rem;
background: #f1f5f9;
padding: 0.1rem 0.35rem;
border-radius: 3px;
}
}
.modal-footer {
padding: 1rem 1.25rem;
border-top: 1px solid var(--border-light);
display: flex;
justify-content: flex-end;
}

View File

@@ -0,0 +1,144 @@
/* Unified selection card component.
Replaces the old .mode-card and .nm-option classes with a single base + modifiers. */
.selection-cards {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.selection-card {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 1rem 1.25rem;
background: var(--card-bg);
border: 2px solid var(--border-light);
border-radius: 10px;
cursor: pointer;
transition: border-color 0.15s, box-shadow 0.15s;
box-shadow: var(--shadow);
&:hover {
border-color: var(--border);
}
& input[type="radio"] {
margin-top: 0.2rem;
flex-shrink: 0;
accent-color: var(--primary);
}
}
.selection-card-icon {
width: 28px;
height: 28px;
flex-shrink: 0;
color: var(--text-secondary);
}
.selection-card-title {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
font-size: 0.93rem;
color: var(--text);
margin-bottom: 0.25rem;
}
.selection-card-desc {
font-size: 0.83rem;
color: var(--text-secondary);
line-height: 1.5;
}
.recommended-label {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--primary);
margin-bottom: 0.35rem;
}
/* --- State modifiers --- */
.selection-card--selected {
border-color: var(--primary);
box-shadow: 0 0 0 1px var(--primary), var(--shadow);
}
.selection-card--disabled {
opacity: 0.45;
cursor: not-allowed;
&:hover {
border-color: var(--border-light);
}
}
.selection-card--recommended {
border-left-color: var(--primary);
}
/* --- Button variant (connect step cards with SVG icons) --- */
.selection-card--btn {
text-align: left;
width: 100%;
&:hover {
border-color: var(--primary);
box-shadow: 0 0 0 1px var(--primary), var(--shadow);
}
&:disabled {
opacity: 0.45;
cursor: not-allowed;
box-shadow: var(--shadow);
&:hover {
border-color: var(--border-light);
box-shadow: var(--shadow);
}
}
}
/* --- Danger variant (remove NickelMenu) --- */
.selection-card--danger {
&:not(.selection-card--disabled) {
border-color: var(--error-border);
&:hover {
border-color: var(--error-text);
}
}
&.selection-card--selected {
border-color: var(--error-text);
box-shadow: 0 0 0 1px var(--error-text), var(--shadow);
}
& input[type="radio"] {
accent-color: var(--error-text);
}
}
/* --- Uninstall options nested inside selection cards --- */
#nm-uninstall-options {
display: flex;
flex-direction: column;
margin-top: 0.5rem;
margin-left: 1.5rem;
& input[type="checkbox"] {
accent-color: var(--error-text);
}
}
#mode-patches-hint {
margin-top: 15px;
}

View File

@@ -0,0 +1,73 @@
.step-nav {
margin-bottom: 1.5rem;
& ol {
display: flex;
list-style: none;
gap: 0;
counter-reset: step;
}
& li {
flex: 1;
text-align: center;
padding: 0.5rem 0;
font-size: 0.8rem;
font-weight: 500;
color: var(--text-secondary);
position: relative;
counter-increment: step;
/* Step number circles (1.6rem diameter to fit two-digit numbers) */
&::before {
content: counter(step);
display: block;
width: 1.6rem;
height: 1.6rem;
line-height: 1.6rem;
margin: 0 auto 0.3rem;
border-radius: 50%;
background: var(--border-light);
color: var(--text-secondary);
font-size: 0.75rem;
font-weight: 600;
}
&.active {
color: var(--primary);
font-weight: 600;
&::before {
background: var(--primary);
color: #fff;
}
}
&.done {
color: var(--success-text);
&::before {
background: var(--success-text);
color: #fff;
content: "\2713";
}
}
/* Connector lines between steps (vertically centered on the 1.6rem circle) */
& + li::after {
content: '';
position: absolute;
top: 1.3rem;
right: 50%;
width: 100%;
height: 2px;
background: var(--border-light);
z-index: -1;
}
&.done + li::after,
&.done + li.active::after {
background: var(--success-text);
}
}
}

View File

@@ -30,6 +30,12 @@
--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);
--info-bg: #e8f7fa;
--info-border: #b2e4ed;
--info-text: #0b6e80;
--accent-border: #bfdbfe;
--accent-text: #1e40af;
--text-muted: #9ca3af;
}
body { background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif; margin: 0; }

View File

@@ -0,0 +1,119 @@
#build-actions {
display: flex;
justify-content: space-between;
gap: 0.75rem;
margin-top: 1.25rem;
}
/* Build header (spinner + progress text) */
.build-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.25rem;
& p {
margin-bottom: 0;
font-weight: 500;
color: var(--text);
}
}
/* Spinner */
.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;
}
/* Also defined in critical.css (inlined into <head>) for the pre-CSS loading spinner */
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Build log terminal */
.build-log {
margin-top: 0.75rem;
padding: 0.75rem 1rem;
background: #0f172a;
color: #94a3b8;
border-radius: 6px;
font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
font-size: 0.73rem;
white-space: pre-wrap;
height: calc(10 * 1.5em + 1.5rem); /* 10 visible lines at line-height 1.5 + padding */
overflow-y: auto;
line-height: 1.5;
}
/* Collapsible log on install step */
.log-details {
margin-top: 0.5rem;
margin-bottom: 0.25rem;
& summary {
font-size: 0.83rem;
color: var(--text-secondary);
cursor: pointer;
padding: 0.25rem 0;
&:hover {
color: var(--text);
}
}
}
/* Done screen log is shorter */
.done-log {
height: calc(7 * 1.5em + 1.5rem); /* 7 visible lines */
}
.error-log {
margin-top: 0.75rem;
padding: 0.75rem 1rem;
background: #0f172a;
color: #e2e8f0;
border-radius: 6px;
font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
font-size: 0.78rem;
white-space: pre-wrap;
max-height: 300px; /* ~15 lines visible before scrolling */
overflow-y: auto;
}
/* Selected patches summary on build step */
.selected-patches-list {
margin: 0 0 1rem;
padding: 0.5rem 1rem 0.5rem 1.75rem;
font-size: 0.85rem;
color: var(--text-secondary);
line-height: 1.5;
background: var(--card-bg);
border: 1px solid var(--border-light);
border-radius: 8px;
box-shadow: var(--shadow);
& li {
padding: 0.05rem 0;
}
}
#firmware-download-url {
display: inline-block;
margin: 0.4rem 0;
padding: 0.3rem 0.6rem;
font-size: 0.7rem;
word-break: break-all;
color: #64748b;
background: #f1f5f9;
border: 1px solid #e2e8f0;
border-radius: 4px;
}
#firmware-verify-notice {
font-size: 12px;
}

View File

@@ -0,0 +1,25 @@
#connect-unsupported-hint {
margin-top: 20px;
color: var(--error-text);
& a {
color: var(--error-text);
text-decoration: underline;
}
}
.device-unknown-ack {
display: flex;
align-items: flex-start;
gap: 0.6rem;
padding: 0.5rem 0;
font-size: 0.83rem;
color: var(--text);
cursor: pointer;
& input[type="checkbox"] {
flex-shrink: 0;
margin-top: 0.15rem;
accent-color: var(--primary);
}
}

View File

@@ -0,0 +1,66 @@
/* Install instructions card */
.install-instructions {
margin-top: 1rem;
background: var(--card-bg);
border: 1px solid var(--border-light);
border-radius: 10px;
padding: 1rem 1.25rem;
}
/* Connection instruction steps with numbered accent circles */
.connect-steps {
list-style: none;
margin: 0.5rem 0 1rem;
padding-left: 2.35rem;
counter-reset: connect-step;
& li {
padding: 0.4rem 0;
font-size: 0.88rem;
color: var(--text-secondary);
line-height: 1.6;
counter-increment: connect-step;
position: relative;
/* Numbered circle: vertically centered relative to the first text line */
&::before {
content: counter(connect-step);
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.6rem;
height: 1.6rem;
border-radius: 50%;
background: var(--primary);
color: #fff;
font-size: 0.78rem;
font-weight: 700;
line-height: 1;
position: absolute;
left: -2.35rem;
top: calc(0.55rem - 4px);
}
}
}
.install-steps {
margin: 0.25rem 0 0 1.25rem;
font-size: 0.88rem;
color: var(--text-secondary);
line-height: 1.7;
& li {
padding: 0.15rem 0;
}
& code {
display: inline-block;
background: var(--bg);
border: 1px solid var(--border-light);
border-radius: 4px;
padding: 0.15rem 0.4rem;
font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
font-size: 0.82rem;
word-break: break-all;
}
}

View File

@@ -0,0 +1,47 @@
/* NickelMenu feature checkboxes */
.nm-config-options {
display: flex;
flex-direction: column;
}
.nm-config-item {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.6rem 0;
color: var(--text);
cursor: pointer;
& + .nm-config-item {
border-top: 1px solid var(--border-light);
}
& input[type="checkbox"] {
flex-shrink: 0;
margin-top: 0.2rem;
accent-color: var(--primary);
&:disabled {
opacity: 0.6;
}
}
}
.nm-config-text {
user-select: none;
}
.nm-config-title {
display: block;
font-weight: 600;
font-size: 0.93rem;
color: var(--text);
}
.nm-config-desc {
display: block;
font-size: 0.83rem;
color: var(--text-secondary);
line-height: 1.5;
margin-top: 0.1rem;
}

View File

@@ -0,0 +1,157 @@
/* Scrollable patch container */
.patch-container-scroll {
max-height: 50vh; /* ~15 visible lines before scrolling */
overflow-y: auto;
border: 1px solid var(--border);
border-radius: 5px;
}
/* Patch file sections */
.patch-file-section {
background: var(--card-bg);
border-bottom: 1px solid var(--border-light);
&:last-child {
border-bottom: none;
}
& summary {
padding: 0.6rem 0.75rem;
cursor: pointer;
display: flex;
align-items: center;
font-weight: 500;
font-size: 0.93rem;
user-select: none;
transition: background 0.1s;
list-style: none;
&::-webkit-details-marker {
display: none;
}
&::before {
content: "\203A";
display: inline-block;
width: 1rem;
margin-right: 0.35rem;
flex-shrink: 0;
text-align: center;
font-size: 1.1rem;
font-weight: 600;
color: var(--text-secondary);
transition: transform 0.15s ease;
}
& .patch-count {
margin-left: auto;
}
&:hover {
background: #f9fafb;
}
}
&[open] summary {
border-bottom: 1px solid var(--border-light);
&::before {
transform: rotate(90deg) translateX(0.1rem);
}
}
}
.patch-count {
font-weight: 400;
font-size: 0.8rem;
background: var(--primary-light);
color: var(--primary);
padding: 0.15rem 0.6rem;
border-radius: 10px;
}
.patch-list {
padding: 0.25rem 0;
}
.patch-item {
padding: 0.4rem 1rem;
& + .patch-item {
border-top: 1px solid var(--border-light);
}
}
.patch-header {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
font-size: 0.85rem;
& input {
flex-shrink: 0;
accent-color: var(--primary);
}
}
.patch-name {
font-weight: 500;
}
.patch-name-none {
color: var(--text-secondary);
}
.patch-desc-toggle {
flex-shrink: 0;
background: none;
border: none;
padding: 0 0.3rem;
font-size: 0.7rem;
color: var(--text-secondary);
cursor: pointer;
box-shadow: none;
opacity: 0.6;
transition: opacity 0.1s;
&:hover {
opacity: 1;
}
}
/* Visual grouping for mutually exclusive patches */
.patch-group {
background: #f8fafc;
border-left: 3px solid var(--primary);
margin: 0.35rem 0.5rem;
border-radius: 0 6px 6px 0;
& .patch-item {
padding: 0.4rem 0.75rem;
}
}
.patch-group-label {
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--primary);
padding: 0.45rem 0.75rem 0;
}
.step .patch-description {
margin-top: 0.3rem;
margin-left: 1.6rem;
margin-bottom: 0;
font-size: 0.72rem;
color: var(--text-secondary);
white-space: pre-line;
line-height: 1.4;
padding: 0.25rem 0;
}
.patch-description[hidden] {
display: none;
}

View File

@@ -0,0 +1,34 @@
.site-footer {
max-width: 640px;
margin: 0 auto;
padding: 1.5rem 1.5rem 2rem;
border-top: 1px solid var(--border-light);
text-align: center;
font-size: 0.8rem;
color: var(--text-secondary);
& a {
color: var(--text-secondary);
text-decoration: underline;
&.site-footer-link {
color: var(--primary);
}
&:hover {
color: var(--text);
}
}
& p {
margin-bottom: 0.75rem;
&:last-child {
margin-bottom: 0;
}
}
}
.site-footer-attribution {
font-size: 0.7rem;
}

View File

@@ -0,0 +1,37 @@
.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;
}
.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; /* optical alignment with heading baseline */
margin-left: 5px;
}
.subtitle {
color: var(--text-secondary);
font-size: 0.95rem;
margin-top: 0.25rem;
}

View File

@@ -0,0 +1,75 @@
.step {
margin-bottom: 1rem;
& p {
color: var(--text-secondary);
margin-bottom: 1rem;
font-size: 0.93rem;
line-height: 1.6;
}
& a {
color: var(--primary);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
& .fallback-hint {
margin-top: 0.5rem;
margin-bottom: 1rem;
font-size: 0.78rem;
color: var(--text-secondary);
line-height: 1.5;
}
& .install-summary {
font-size: 0.93rem;
color: var(--text);
line-height: 1.6;
margin-bottom: 0.5rem;
}
}
/* Step action buttons (back/next) */
.step-actions {
display: flex;
justify-content: space-between;
gap: 0.75rem;
margin-top: 1.25rem;
& .primary:first-child,
& > [hidden] + .primary {
margin-left: auto;
}
}
.step-actions-right {
display: flex;
gap: 0.75rem;
margin-left: auto;
}
@media (max-width: 500px) {
.step-actions:has(.step-actions-right) {
flex-wrap: wrap;
}
.step-actions-right {
width: 100%;
justify-content: flex-end;
}
}
/* Pulls fallback hint up to compensate for select's margin-bottom */
select + .fallback-hint {
margin-top: -0.5rem;
}
.restart-hint {
margin-top: 1.5rem;
font-size: 0.78rem;
color: var(--text-muted);
}

File diff suppressed because it is too large Load Diff

View File

@@ -79,27 +79,27 @@
<!-- Step 1: Choose connection method -->
<section id="step-connect" class="step" hidden>
<div class="notice">
<div class="notice-heading">Important information</div>
<div class="banner banner--info">
<div class="banner-heading">Important information</div>
If you choose to modify your Kobo's system files via this tool, your warranty may be affected. A backup of any extra books is recommended. If something goes wrong, follow the official instructions on <a href="https://help.kobo.com/hc/en-us/articles/360017605314-Manual-reset-your-Kobo-Clara-HD-Kobo-Nia-Kobo-Elipsa-Kobo-Clara-2E-Kobo-Elipsa-2E" target="_blank">how to manually reset your device</a>.
</div>
<p>How would you like to set up your Kobo?</p>
<div class="mode-cards">
<button id="btn-connect" class="mode-card mode-card-btn mode-card-recommended">
<svg class="mode-card-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15,7V11H16V13H13V5H15L12,1L9,5H11V13H8V10.93C8.66,10.59 9.1,9.87 9.1,9A2.1,2.1 0 0,0 7,6.9C5.84,6.9 4.9,7.84 4.9,9C4.9,9.87 5.34,10.59 6,10.93V13A2,2 0 0,0 8,15H11V18.05C10.29,18.32 9.75,18.93 9.75,19.75A1.75,1.75 0 0,0 11.5,21.5C12.47,21.5 13.25,20.72 13.25,19.75C13.25,18.93 12.71,18.32 12,18.05V15H16A2,2 0 0,0 18,13V11H19V7H15Z"/></svg>
<div class="mode-card-body">
<div class="mode-card-title">Connect my Kobo</div>
<div class="selection-cards">
<button id="btn-connect" class="selection-card selection-card--btn selection-card--recommended">
<svg class="selection-card-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15,7V11H16V13H13V5H15L12,1L9,5H11V13H8V10.93C8.66,10.59 9.1,9.87 9.1,9A2.1,2.1 0 0,0 7,6.9C5.84,6.9 4.9,7.84 4.9,9C4.9,9.87 5.34,10.59 6,10.93V13A2,2 0 0,0 8,15H11V18.05C10.29,18.32 9.75,18.93 9.75,19.75A1.75,1.75 0 0,0 11.5,21.5C12.47,21.5 13.25,20.72 13.25,19.75C13.25,18.93 12.71,18.32 12,18.05V15H16A2,2 0 0,0 18,13V11H19V7H15Z"/></svg>
<div class="selection-card-body">
<div class="selection-card-title">Connect my Kobo</div>
<div class="recommended-label">Recommended</div>
<div class="mode-card-desc">Connect your device via USB. Your device will be automatically identified, and this is the easiest way to apply customizations. You will have the option to apply these changes directly, or you can download a ZIP.</div>
<div class="selection-card-desc">Connect your device via USB. Your device will be automatically identified, and this is the easiest way to apply customizations. You will have the option to apply these changes directly, or you can download a ZIP.</div>
</div>
</button>
<button id="btn-manual" class="mode-card mode-card-btn">
<svg class="mode-card-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z"/></svg>
<div class="mode-card-body">
<div class="mode-card-title">
<button id="btn-manual" class="selection-card selection-card--btn">
<svg class="selection-card-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z"/></svg>
<div class="selection-card-body">
<div class="selection-card-title">
Build downloadable archive
</div>
<div class="mode-card-desc">You may need to identify your device based on model number. Go through the wizard, download the files and copy them to your Kobo yourself. Works in any browser, but a bit more complicated. Recommended for tinkerers.</div>
<div class="selection-card-desc">You may need to identify your device based on model number. Go through the wizard, download the files and copy them to your Kobo yourself. Works in any browser, but a bit more complicated. Recommended for tinkerers.</div>
</div>
</button>
</div>
@@ -118,7 +118,7 @@
<li>In <span id="connect-file-manager">your file manager</span>, you should now see <strong>KOBOeReader</strong> appear as a drive.</li>
<li>When you press the button below, your browser will open a folder picker. Select the <strong>KOBOeReader</strong> volume, then click <strong>"Allow"</strong> when your browser asks if you want to give this site permission to write to this folder.</li>
</ol>
<details class="disclaimer">
<details class="banner banner--accent">
<summary><strong>Anything else I should know?</strong></summary>
<ol>
<li>Information about your device is <b>never</b> collected. It is only displayed on this page, temporarily, and used to determine which options are compatible with your device.</li>
@@ -171,7 +171,7 @@
</div>
</div>
<p id="device-status"></p>
<p id="device-unknown-warning" class="warning" hidden>
<p id="device-unknown-warning" class="banner banner--warning" hidden>
You seem to have a Kobo device that cannot be identified. While you can continue since a supported software version seems to be installed, please
<a href="https://github.com/nicoverbruggen/kobopatch-webui/issues/new" target="_blank">file an issue on GitHub</a>
so the developer can add this device to the list.
@@ -192,20 +192,20 @@
<!-- Step 2: Mode selection -->
<section id="step-mode" class="step" hidden>
<p>What would you like to do?</p>
<div class="mode-cards" role="radiogroup" aria-label="Mode selection">
<label class="mode-card mode-card-selected mode-card-recommended">
<div class="selection-cards" role="radiogroup" aria-label="Mode selection">
<label class="selection-card selection-card--selected selection-card--recommended">
<input type="radio" name="mode" value="nickelmenu" checked>
<div class="mode-card-body">
<div class="mode-card-title">Install or remove NickelMenu</div>
<div class="selection-card-body">
<div class="selection-card-title">Install or remove NickelMenu</div>
<div class="recommended-label">Recommended</div>
<div class="mode-card-desc">Installs a custom menu and various tweaks for your device. Works with most Kobo devices. The safest solution, as it has a lot of error checking and a failsafe mechanism which will automatically uninstall it as a last resort. </div>
<div class="selection-card-desc">Installs a custom menu and various tweaks for your device. Works with most Kobo devices. The safest solution, as it has a lot of error checking and a failsafe mechanism which will automatically uninstall it as a last resort. </div>
</div>
</label>
<label class="mode-card">
<label class="selection-card">
<input type="radio" name="mode" value="patches">
<div class="mode-card-body">
<div class="mode-card-title">Custom Patches</div>
<div class="mode-card-desc">Apply community patches to your Kobo's system software. Requires a supported software version and supported device.</div>
<div class="selection-card-body">
<div class="selection-card-title">Custom Patches</div>
<div class="selection-card-desc">Apply community patches to your Kobo's system software. Requires a supported software version and supported device.</div>
</div>
</label>
</div>
@@ -219,26 +219,26 @@
<!-- Step 2b: NickelMenu configuration -->
<section id="step-nickelmenu" class="step" hidden>
<p>Choose what to do with your Kobo.</p>
<div class="nm-options" role="radiogroup" aria-label="NickelMenu options">
<label class="nm-option">
<div class="selection-cards" role="radiogroup" aria-label="NickelMenu options">
<label class="selection-card">
<input type="radio" name="nm-option" value="preset">
<div class="nm-option-body">
<div class="nm-option-title">Install NickelMenu with preset</div>
<div class="nm-option-desc">Installs NickelMenu with a curated set of menu options. You get to decide which optional features you'd like to enable.</div>
<div class="selection-card-body">
<div class="selection-card-title">Install NickelMenu with preset</div>
<div class="selection-card-desc">Installs NickelMenu with a curated set of menu options. You get to decide which optional features you'd like to enable.</div>
</div>
</label>
<label class="nm-option">
<label class="selection-card">
<input type="radio" name="nm-option" value="nickelmenu-only">
<div class="nm-option-body">
<div class="nm-option-title">Install NickelMenu only</div>
<div class="nm-option-desc">Installs just NickelMenu without any configuration. You can set it up yourself later.</div>
<div class="selection-card-body">
<div class="selection-card-title">Install NickelMenu only</div>
<div class="selection-card-desc">Installs just NickelMenu without any configuration. You can set it up yourself later.</div>
</div>
</label>
<label id="nm-option-remove" class="nm-option nm-option-disabled nm-option-remove">
<label id="nm-option-remove" class="selection-card selection-card--disabled selection-card--danger">
<input type="radio" name="nm-option" value="remove" disabled>
<div class="nm-option-body">
<div class="nm-option-title">Remove NickelMenu</div>
<div class="nm-option-desc" id="nm-remove-desc">Removes NickelMenu from your device. (Only available when a Kobo with NickelMenu installed is connected.)</div>
<div class="selection-card-body">
<div class="selection-card-title">Remove NickelMenu</div>
<div class="selection-card-desc" id="nm-remove-desc">Removes NickelMenu from your device. (Only available when a Kobo with NickelMenu installed is connected.)</div>
</div>
</label>
<div id="nm-uninstall-options" hidden></div>
@@ -284,7 +284,7 @@
<!-- NickelMenu done -->
<section id="step-nm-done" class="step" hidden>
<p id="nm-done-status" class="install-summary"></p>
<p id="nm-write-instructions" class="hint" hidden>
<p id="nm-write-instructions" class="banner banner--success" hidden>
Files have been written to your Kobo.
<strong>Safely eject</strong> the device before unplugging the USB cable &mdash; it will reboot and install NickelMenu automatically.
</p>
@@ -301,7 +301,7 @@
<li>The device will reboot and install NickelMenu automatically.</li>
</ol>
</div>
<p id="nm-reboot-instructions" class="hint" hidden>
<p id="nm-reboot-instructions" class="banner banner--success" hidden>
<strong>Safely eject your Kobo and let it reboot.</strong> Please be patient, NickelMenu will be automatically removed during the reboot.
A "glitchy" horizontal line may briefly appear on screen after restarting — this is normal, as NickelMenu removes itself.
</p>
@@ -355,7 +355,7 @@
<!-- Step 5: Install -->
<section id="step-done" class="step" hidden>
<p id="build-status" class="install-summary"></p>
<div id="existing-tgz-warning" class="warning" hidden>
<div id="existing-tgz-warning" class="banner banner--warning" hidden>
An <b>existing</b> KoboRoot.tgz file was found on your Kobo. This means an update has not been applied yet. If you choose to write the new file to your Kobo, the existing one will be overwritten.
</div>
<details class="log-details">
@@ -366,7 +366,7 @@
<button id="btn-write" class="primary">Write to Kobo</button>
<button id="btn-download" class="secondary">Download KoboRoot.tgz</button>
</div>
<p id="write-instructions" class="hint" hidden>
<p id="write-instructions" class="banner banner--success" hidden>
KoboRoot.tgz has been written to your Kobo.
<strong>Safely eject</strong> the device before unplugging the USB cable — it will reboot and apply the patches automatically.
</p>
@@ -386,7 +386,7 @@
<h2 id="error-title">Something went wrong</h2>
<p id="error-hint" hidden>Some patches may not work correctly with your software version. You can go back and try a different selection.</p>
<pre id="error-log" class="error-log" hidden></pre>
<p id="error-message" class="error"></p>
<p id="error-message" class="banner banner--error"></p>
<div class="step-actions">
<button id="btn-error-back" class="secondary" hidden>&#x2039; Select different patches</button>
<button id="btn-retry" class="secondary">Start Over</button>

View File

@@ -131,8 +131,8 @@ const nm = initNickelMenu(state);
const patches = initPatchesFlow(state);
// Wire up card-radio interactivity for mode selection and NM option cards.
setupCardRadios(stepMode, 'mode-card-selected');
setupCardRadios($('step-nickelmenu'), 'nm-option-selected');
setupCardRadios(stepMode, 'selection-card--selected');
setupCardRadios($('step-nickelmenu'), 'selection-card--selected');
// =============================================================================
// Error handling
@@ -182,14 +182,14 @@ state.showError = showError;
function goToModeSelection() {
nm.resetNickelMenuState();
const patchesRadio = $q('input[value="patches"]', stepMode);
const patchesCard = patchesRadio.closest('.mode-card');
const patchesCard = patchesRadio.closest('.selection-card');
const autoModeNoPatchesAvailable = !state.manualMode && (!state.patchesLoaded || !state.firmwareURL);
// Disable the patches card if firmware patches aren't available.
const patchesHint = $('mode-patches-hint');
if (autoModeNoPatchesAvailable) {
patchesRadio.disabled = true;
patchesCard.classList.add('mode-card-disabled');
patchesCard.classList.add('selection-card--disabled');
patchesHint.hidden = false;
// Auto-select NickelMenu since it's the only available option.
const nmRadio = $q('input[value="nickelmenu"]', stepMode);
@@ -197,7 +197,7 @@ function goToModeSelection() {
nmRadio.dispatchEvent(new Event('change'));
} else {
patchesRadio.disabled = false;
patchesCard.classList.remove('mode-card-disabled');
patchesCard.classList.remove('selection-card--disabled');
patchesHint.hidden = true;
}
@@ -379,7 +379,7 @@ btnConnectReady.addEventListener('click', async () => {
deviceStatus.textContent =
'You seem to have an incompatible Kobo software version installed. ' +
'NickelMenu does not support it, and the custom patches are incompatible with this version.';
deviceStatus.classList.add('error');
deviceStatus.classList.add('banner', 'banner--error');
btnDeviceNext.hidden = true;
btnDeviceRestore.hidden = true;
showStep(stepDevice);
@@ -405,7 +405,7 @@ btnConnectReady.addEventListener('click', async () => {
btnDeviceRestore.hidden = !state.patchesLoaded || !state.firmwareURL;
// Handle unknown models — require explicit acknowledgment before continuing.
deviceStatus.classList.remove('error');
deviceStatus.classList.remove('banner', 'banner--error');
const isUnknownModel = info.model.startsWith('Unknown');
if (isUnknownModel) {
deviceStatus.textContent = '';

View File

@@ -164,7 +164,7 @@ export function initNickelMenu(state) {
await nmDir.getFileHandle('items');
// NickelMenu is installed — enable removal option.
removeRadio.disabled = false;
removeOption.classList.remove('nm-option-disabled');
removeOption.classList.remove('selection-card--disabled');
removeDesc.textContent = TL.STATUS.NM_REMOVAL_HINT;
// Scan for removable extras (only once per session).
@@ -188,7 +188,7 @@ export function initNickelMenu(state) {
// No device or NickelMenu not found — disable removal.
removeRadio.disabled = true;
removeOption.classList.add('nm-option-disabled');
removeOption.classList.add('selection-card--disabled');
removeDesc.textContent = TL.STATUS.NM_REMOVAL_DISABLED;
if (removeRadio.checked) {
const presetRadio = $q('input[value="preset"]', stepNickelMenu);