');
+
+test('dark-theme: warns on the kit-only stylesheet and on single-value layout tokens', async () => {
+ const app = tmpDir();
+ assert.equal((await checkDarkTheme(app)).status, 'pass', 'no app/ directory: nothing to check');
+ write(app, 'public/input.css', KIT_CSS);
+ write(app, 'app/layout.ts', PLAIN_LAYOUT);
+ const r = await checkDarkTheme(app);
+ assert.equal(r.status, 'warn');
+ assert.match(r.message, /public\/input\.css with a light value only/);
+ assert.match(r.fix, /light-dark\(LIGHT, DARK\)/);
+ assert.match(r.fix, /prefers-color-scheme: dark/);
+ assert.match(r.fix, /head ") },
+ 'no colour tokens at all': { 'public/input.css': '@import "tailwindcss";\n/* --background: #fff is set per page */\n', 'public/tailwind.css': ':root{--background:#fff;--foreground:#000}', 'app/layout.ts': PLAIN_LAYOUT },
+ };
+ for (const [label, files] of Object.entries(cases)) {
+ const app = tmpDir();
+ for (const [rel, content] of Object.entries(files)) write(app, rel, content);
+ assert.equal((await checkDarkTheme(app)).status, 'pass', label);
+ }
+});
+
+test('dark-theme: carries the DARK_THEME_UNREACHABLE code, warns ungated and fails the exit gated', async () => {
+ const fixture = (gate) => {
+ const dir = tmpDir();
+ write(dir, 'package.json', JSON.stringify({ name: 'x', ...(gate ? { webjs: { doctor: { gate } } } : {}) }));
+ write(dir, 'tsconfig.json', JSON.stringify({ compilerOptions: { erasableSyntaxOnly: true } }));
+ write(dir, 'public/input.css', KIT_CSS);
+ write(dir, 'app/layout.ts', PLAIN_LAYOUT);
+ return dir;
+ };
+ const results = await runDoctorChecks(fixture(null), { vendor: noPinVendor });
+ const r = byName(results, 'dark-theme');
+ assert.equal(r.code, 'DARK_THEME_UNREACHABLE');
+ assert.equal(r.status, 'warn');
+ const ungated = runCliArgs(fixture(null), []);
+ assert.equal(ungated.status, 0, `ungated, the warning does not fail the exit: ${ungated.stdout}`);
+ assert.match(ungated.stdout, /\[warn\] .*DARK_THEME_UNREACHABLE/);
+ const gated = runCliArgs(fixture({ DARK_THEME_UNREACHABLE: 'error' }), []);
+ assert.equal(gated.status, 1, 'gated to error, the same warning fails the exit');
+ assert.match(gated.stdout, /\[fail\] .*\(DARK_THEME_UNREACHABLE, gated: error\)/);
+});
diff --git a/test/scaffolds/scaffold-template-validation.test.js b/test/scaffolds/scaffold-template-validation.test.js
index a8e787a10..5b7a65ee6 100644
--- a/test/scaffolds/scaffold-template-validation.test.js
+++ b/test/scaffolds/scaffold-template-validation.test.js
@@ -136,6 +136,7 @@ for (const template of ['full-stack', 'api']) {
await scaffoldApp('my-app', cwd, { template, install: false });
const pkg = JSON.parse(await readFile(join(cwd, 'my-app', 'package.json'), 'utf8'));
assert.equal(pkg.webjs.doctor.gate.UNMARKED_ASSET_LINKS, 'error');
+ assert.equal(pkg.webjs.doctor.gate.DARK_THEME_UNREACHABLE, 'error', 'the scaffold AGENTS.md mandates light-dark() tokens, so the scaffold enforces them (#1628)');
assert.equal(pkg.scripts.doctor, 'webjs doctor');
const ci = await readFile(join(cwd, 'my-app', '.github', 'workflows', 'ci.yml'), 'utf8');
assert.match(ci, /^\s+- run: npm run ci$/m, 'the workflow runs the declared step list');
diff --git a/website/app/docs/configuration/page.ts b/website/app/docs/configuration/page.ts
index 381d04bfb..d72f2a277 100644
--- a/website/app/docs/configuration/page.ts
+++ b/website/app/docs/configuration/page.ts
@@ -69,6 +69,8 @@ webjs doctor --strict # also fail on EVERY remaining warning, not just hard fa
One more check, WORKSPACE_OVERRIDES, warns when the app is a member of an npm or bun workspace and its own package.json carries overrides (or resolutions): package managers honour those only at the workspace root, so the block is silently ignored there. Move it into the root package.json.
+ DARK_THEME_UNREACHABLE warns when the app defines colour tokens (--background / --foreground in the root layout, public/*.css or styles/**.css) with a light value only and nothing applies a dark half: no light-dark(), no @media (prefers-color-scheme: dark) rule, no theme script in the root layout, so the OS dark setting is ignored and any .dark block is dead CSS. A design convention rather than a runtime break, so it is a doctor check and not a webjs check rule; the scaffold gates it error because its AGENTS.md mandates light-dark() tokens.
+
And APP_ICON warns while the app's favicon is still the placeholder app/icon.svg the scaffold shipped. Replace it with the app's own icon (see App icon and manifest).
webjs audit
diff --git a/website/app/docs/troubleshooting/page.ts b/website/app/docs/troubleshooting/page.ts
index 25ec4e73d..f5c4b6776 100644
--- a/website/app/docs/troubleshooting/page.ts
+++ b/website/app/docs/troubleshooting/page.ts
@@ -150,7 +150,7 @@ export default function Troubleshooting() {
Fix: remove the shell tags from the nested layout or page and return just the content. Move any <html lang> / <body class> customization to the root layout, which the framework respects and splices its required tags into. See Routing. The shell-in-non-root-layout check rule flags this, framework invariant 8.
The app has no dark mode, and the stylesheet's .dark block never applies
- Symptom: webjs check flags dark-theme-unreachable on public/input.css or app/layout.ts. In a browser with the OS set to dark, the app stays light.
+ Symptom: webjs doctor reports DARK_THEME_UNREACHABLE (a warning, or a failure in a scaffolded app, whose package.json gates it error) naming public/input.css or app/layout.ts. In a browser with the OS set to dark, the app stays light.
Cause: the file defines colour tokens (--background, --foreground) with a light value only. The scaffold's stylesheet keeps the ui kit's .dark { ... } token block, but nothing ever adds that class or reads the OS setting: no light-dark(), no @media (prefers-color-scheme: dark) rule, no theme script in the root layout. The dark half is dead CSS, and a light-mode screenshot cannot show it.
Fix: write each colour token once as light-dark(LIGHT, DARK) under color-scheme: light dark in the root layout (the block the skill's references/styling.md shows, and what the generated layout does), or give the tokens an @media (prefers-color-scheme: dark) half, or add a head <script> that applies the saved theme (data-theme or the .dark class). Then look at the app with the OS in dark mode; light mode passing proves nothing about dark.