@writedocs/generator 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/writedocs.js CHANGED
@@ -9,6 +9,7 @@ import { runBuild } from '../src/cli/build.js';
9
9
  import { runInit } from '../src/cli/init.js';
10
10
  import { requireBuildKey } from '../src/cli/build-auth.js';
11
11
  import { log, step, plural, color, CliExit, errorText, stopActiveStep } from '../src/cli/output.js';
12
+ import { startUpdateCheck, showUpdateNotice } from '../src/cli/update-check.js';
12
13
  // O MESMO modulo que o build usa (via loadDocsConfig, que reexporta daqui) e
13
14
  // que a plataforma importa por `@writedocs/generator/config-schema` - e o que
14
15
  // faz os tres reportarem os mesmos problemas com as mesmas palavras, em vez de
@@ -37,13 +38,18 @@ const packageRoot = path.resolve(__dirname, '..');
37
38
  // package.json). `writedocs --version` should always reflect what actually
38
39
  // got published, not whatever this string happened to say at the time this
39
40
  // line was last hand-edited.
40
- const { version } = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf-8'));
41
+ const { name: packageName, version } = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf-8'));
41
42
 
42
43
  const program = new Command();
43
44
  program
44
45
  .name('writedocs')
45
46
  .description('Static site generator for writedocs.json + MDX')
46
- .version(version);
47
+ .version(version)
48
+ // Every command checks for a newer writedocs (from a cache - see
49
+ // src/cli/update-check.js); the notice prints after the command's output.
50
+ .hook('preAction', (_program, command) => {
51
+ startUpdateCheck({ command: command.name(), name: packageName, version, packageRoot });
52
+ });
47
53
 
48
54
  program
49
55
  .command('dev')
@@ -265,6 +271,14 @@ program
265
271
  });
266
272
  });
267
273
 
274
+ program
275
+ .command('update')
276
+ .description('Update writedocs to the latest version')
277
+ .action(async () => {
278
+ const { runUpdate } = await import('../src/cli/update.js');
279
+ await runUpdate({ name: packageName, version, packageRoot });
280
+ });
281
+
268
282
  program
269
283
  .command('init')
270
284
  .description('Scaffold a writedocs.json and starter docs/ folder')
@@ -273,10 +287,13 @@ program
273
287
  await runInit({ targetDir: path.resolve(process.cwd(), dir) });
274
288
  });
275
289
 
276
- program.parseAsync(process.argv).catch((err) => {
277
- // A command that already printed its own error throws CliExit.
278
- stopActiveStep();
279
- if (err instanceof CliExit) process.exit(err.code);
280
- log.error(errorText(err));
281
- process.exit(1);
282
- });
290
+ program
291
+ .parseAsync(process.argv)
292
+ .then(showUpdateNotice)
293
+ .catch((err) => {
294
+ stopActiveStep();
295
+ // A command that already printed its own error throws CliExit.
296
+ if (!(err instanceof CliExit)) log.error(errorText(err));
297
+ showUpdateNotice();
298
+ process.exit(err instanceof CliExit ? err.code : 1);
299
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@writedocs/generator",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Static site generator for docs — a writedocs.json + MDX folder in, a static site out.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli/dev.js CHANGED
@@ -5,6 +5,7 @@ import { log, step, duration, formatProblems, color, CliExit } from './output.js
5
5
  import { describeError, requestLog, authorWarning, verboseLine, stripAnsi } from './astro-output.js';
6
6
  import { reportApiPages } from './api-pages-output.js';
7
7
  import { runningPreview, writeLock, removeLock } from './dev-lock.js';
8
+ import { showUpdateNotice } from './update-check.js';
8
9
 
9
10
  // The same problem tends to arrive more than once in a row - Vite and
10
11
  // Astro each log a failed page, and a page compiles for more than one
@@ -96,6 +97,9 @@ export async function runDev({ contentDir, packageRoot, port, verbose = false })
96
97
  log.line();
97
98
  log.line(color.dim(' Edit any page and the preview updates. Press Ctrl+C to stop.'));
98
99
  log.line();
100
+ // `dev` runs until Ctrl+C - the notice goes under the ready screen,
101
+ // not after the command like everywhere else.
102
+ showUpdateNotice();
99
103
  return;
100
104
  }
101
105
  case 'fatal':
@@ -0,0 +1,18 @@
1
+ // Detached background process started by update-check.js: asks the npm
2
+ // registry for the latest version of package `process.argv[2]` and caches
3
+ // the answer for the next writedocs run. Prints nothing. A failure (offline,
4
+ // registry down) keeps the last known version but still records the
5
+ // attempt, so it's retried the next day, not on every command.
6
+ import { fetchLatestVersion, readCache, writeCache } from './update-check.js';
7
+
8
+ const name = process.argv[2];
9
+ let latest = null;
10
+ try {
11
+ latest = await fetchLatestVersion(name);
12
+ } catch {
13
+ const previous = readCache();
14
+ latest = previous?.name === name ? previous.latest : null;
15
+ }
16
+ try {
17
+ writeCache({ name, latest, checkedAt: Date.now() });
18
+ } catch {}
@@ -0,0 +1,104 @@
1
+ // "A newer writedocs is available" - shown after any command's output.
2
+ //
3
+ // Never slows a command down: the notice comes from a cached answer, and
4
+ // when that's more than a day old, a detached background process
5
+ // (update-check-refresh.js) asks the npm registry again and rewrites the
6
+ // cache for the next run - the same approach as npm's own update notifier.
7
+ //
8
+ // Not shown for `build` (the WriteDocs platform runs it, not a person) or
9
+ // `update` itself, in CI, when output isn't a terminal, when writedocs runs
10
+ // from a source checkout (updated with git, not npm), or with
11
+ // WRITEDOCS_NO_UPDATE_CHECK set.
12
+ import fs from 'node:fs';
13
+ import os from 'node:os';
14
+ import path from 'node:path';
15
+ import { spawn } from 'node:child_process';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { log, color } from './output.js';
18
+
19
+ const DAY = 24 * 60 * 60 * 1000;
20
+ const SILENT_COMMANDS = new Set(['build', 'update']);
21
+
22
+ export function cacheFile() {
23
+ const base =
24
+ process.env.XDG_CACHE_HOME ||
25
+ (process.platform === 'win32' ? process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local') : path.join(os.homedir(), '.cache'));
26
+ return path.join(base, 'writedocs', 'update-check.json');
27
+ }
28
+
29
+ export function readCache() {
30
+ try {
31
+ return JSON.parse(fs.readFileSync(cacheFile(), 'utf8'));
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ export function writeCache(data) {
38
+ const file = cacheFile();
39
+ fs.mkdirSync(path.dirname(file), { recursive: true });
40
+ fs.writeFileSync(file, JSON.stringify(data));
41
+ }
42
+
43
+ /** The registry the user's npm uses, or npm's own. */
44
+ export function registryUrl() {
45
+ const configured = process.env.npm_config_registry || process.env.NPM_CONFIG_REGISTRY;
46
+ return (configured || 'https://registry.npmjs.org').replace(/\/+$/, '');
47
+ }
48
+
49
+ /** The version npm's `latest` tag points at. Throws on a network error. */
50
+ export async function fetchLatestVersion(name, { timeoutMs = 5000 } = {}) {
51
+ const res = await fetch(`${registryUrl()}/-/package/${name.replace('/', '%2f')}/dist-tags`, { signal: AbortSignal.timeout(timeoutMs) });
52
+ if (!res.ok) throw new Error(`the registry answered ${res.status}`);
53
+ const tags = await res.json();
54
+ if (typeof tags?.latest !== 'string') throw new Error('the registry has no "latest" version');
55
+ return tags.latest;
56
+ }
57
+
58
+ /** Whether `latest` is a newer release than `current` - x.y.z only; a
59
+ * prerelease never counts as newer. */
60
+ export function isNewer(latest, current) {
61
+ const parse = (v) => /^(\d+)\.(\d+)\.(\d+)$/.exec(String(v).trim())?.slice(1).map(Number);
62
+ const a = parse(latest);
63
+ const b = parse(current);
64
+ if (!a || !b) return false;
65
+ for (let i = 0; i < 3; i += 1) if (a[i] !== b[i]) return a[i] > b[i];
66
+ return false;
67
+ }
68
+
69
+ export function isSourceCheckout(packageRoot) {
70
+ return fs.existsSync(path.join(packageRoot, '.git'));
71
+ }
72
+
73
+ function enabled(command, packageRoot) {
74
+ if (SILENT_COMMANDS.has(command)) return false;
75
+ if (process.env.WRITEDOCS_NO_UPDATE_CHECK || process.env.CI) return false;
76
+ if (!process.stdout.isTTY) return false;
77
+ return !isSourceCheckout(packageRoot);
78
+ }
79
+
80
+ let state = null;
81
+
82
+ /** Called once at startup: loads the cached answer, and refreshes it in the
83
+ * background when it's old. */
84
+ export function startUpdateCheck({ command, name, version, packageRoot }) {
85
+ if (!enabled(command, packageRoot)) return;
86
+ const cache = readCache();
87
+ state = { name, version, latest: cache?.name === name ? cache.latest : null, shown: false };
88
+ if (cache?.name === name && Date.now() - (cache.checkedAt ?? 0) < DAY) return;
89
+ try {
90
+ const worker = path.join(path.dirname(fileURLToPath(import.meta.url)), 'update-check-refresh.js');
91
+ const child = spawn(process.execPath, [worker, name], { detached: true, stdio: 'ignore', windowsHide: true });
92
+ child.unref();
93
+ } catch {
94
+ // No notice this time - not worth failing a command over.
95
+ }
96
+ }
97
+
98
+ /** Prints the notice, if there's a newer version - once per run. */
99
+ export function showUpdateNotice() {
100
+ if (!state || state.shown || !state.latest || !isNewer(state.latest, state.version)) return;
101
+ state.shown = true;
102
+ log.line();
103
+ log.info(`writedocs ${color.bold(state.latest)} is available ${color.dim(`(you have ${state.version})`)}. Run ${color.cyan('writedocs update')} to update.`);
104
+ }
@@ -0,0 +1,107 @@
1
+ // `writedocs update` - installs the latest writedocs, the same way the running
2
+ // one was installed: globally with npm (the documented way), as a project
3
+ // dependency (npm, pnpm or yarn, by the project's lockfile), or globally
4
+ // with pnpm or yarn. npx and a source checkout get an explanation instead.
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { spawn } from 'node:child_process';
8
+ import { log, step, color, CliExit } from './output.js';
9
+ import { fetchLatestVersion, isNewer, isSourceCheckout, writeCache } from './update-check.js';
10
+
11
+ /** Runs a command, capturing its output. Resolves { code, output }. */
12
+ function run(command, args, cwd) {
13
+ return new Promise((resolve) => {
14
+ // npm/pnpm/yarn are .cmd scripts on Windows, which spawn() only runs
15
+ // through a shell - given as one string there (Node deprecates an
16
+ // argument list with `shell`). The arguments are fixed: flags and the
17
+ // package name, never user input.
18
+ const options = { cwd, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true };
19
+ const child = process.platform === 'win32' ? spawn([command, ...args].join(' '), { ...options, shell: true }) : spawn(command, args, options);
20
+ let output = '';
21
+ child.stdout.on('data', (d) => (output += d));
22
+ child.stderr.on('data', (d) => (output += d));
23
+ child.on('error', (err) => resolve({ code: 1, output: err.message }));
24
+ child.on('close', (code) => resolve({ code: code ?? 1, output }));
25
+ });
26
+ }
27
+
28
+ /** How the running writedocs was installed:
29
+ * { kind: 'checkout' | 'npx' | 'unknown' } or
30
+ * { kind: 'global' | 'local', command, args, cwd }. */
31
+ export async function installation(packageRoot, name) {
32
+ if (isSourceCheckout(packageRoot)) return { kind: 'checkout' };
33
+ const posix = packageRoot.split(path.sep).join('/');
34
+ if (/\/_npx\//.test(posix)) return { kind: 'npx' };
35
+ if (/\/pnpm\/global\//.test(posix)) return { kind: 'global', command: 'pnpm', args: ['add', '-g', `${name}@latest`] };
36
+ if (/\/yarn\/global\//.test(posix)) return { kind: 'global', command: 'yarn', args: ['global', 'add', `${name}@latest`] };
37
+ const at = posix.lastIndexOf(`/node_modules/${name}`);
38
+ if (at === -1) return { kind: 'unknown' };
39
+ const container = posix.slice(0, at);
40
+ const npmRoot = await run('npm', ['root', '-g']);
41
+ const globalRoot = npmRoot.code === 0 ? npmRoot.output.trim().split(/\r?\n/).pop() : null;
42
+ if (globalRoot && path.resolve(globalRoot) === path.resolve(`${container}/node_modules`)) {
43
+ return { kind: 'global', command: 'npm', args: ['install', '-g', `${name}@latest`] };
44
+ }
45
+ const cwd = path.resolve(container);
46
+ if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return { kind: 'local', command: 'pnpm', args: ['add', `${name}@latest`], cwd };
47
+ if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return { kind: 'local', command: 'yarn', args: ['add', `${name}@latest`], cwd };
48
+ return { kind: 'local', command: 'npm', args: ['install', `${name}@latest`], cwd };
49
+ }
50
+
51
+ export async function runUpdate({ name, version, packageRoot }) {
52
+ const checking = step('Checking for a new version');
53
+ let latest;
54
+ try {
55
+ latest = await fetchLatestVersion(name, { timeoutMs: 15000 });
56
+ } catch (err) {
57
+ checking.fail("Couldn't check for a new version");
58
+ log.detail(color.dim(`${err.message}. Check your connection, and try again.`));
59
+ throw new CliExit(1);
60
+ }
61
+ checking.stop();
62
+ try {
63
+ writeCache({ name, latest, checkedAt: Date.now() });
64
+ } catch {}
65
+ if (!isNewer(latest, version)) {
66
+ log.success(`writedocs is up to date ${color.dim(`(${version})`)}`);
67
+ return;
68
+ }
69
+
70
+ const install = await installation(packageRoot, name);
71
+ if (install.kind === 'checkout') {
72
+ log.info(`writedocs ${latest} is available, but this one runs from a source checkout (${packageRoot}).`);
73
+ log.detail(color.dim('Update it with git - `writedocs update` only updates installed copies.'));
74
+ return;
75
+ }
76
+ if (install.kind === 'npx') {
77
+ log.info(`writedocs ${latest} is available. You're running writedocs through npx - to use the latest, run:`);
78
+ log.detail(color.cyan(`npx ${name}@latest <command>`));
79
+ log.detail(color.dim(`Or install it once, and use \`writedocs\` directly: npm install -g ${name}`));
80
+ return;
81
+ }
82
+ if (install.kind === 'unknown') {
83
+ log.info(`writedocs ${latest} is available. Update it the way you installed it - for example:`);
84
+ log.detail(color.cyan(`npm install -g ${name}@latest`));
85
+ return;
86
+ }
87
+
88
+ const shown = `${install.command} ${install.args.join(' ')}`;
89
+ const updating = step(`Updating writedocs ${version} → ${latest} ${color.dim(`(${shown}${install.cwd ? ` in ${install.cwd}` : ''})`)}`);
90
+ const result = await run(install.command, install.args, install.cwd);
91
+ if (result.code !== 0) {
92
+ updating.fail(`Couldn't update writedocs - \`${shown}\` failed`);
93
+ const tail = result.output.trim().split(/\r?\n/).slice(-12).join('\n');
94
+ if (tail) {
95
+ log.line();
96
+ log.line(color.dim(tail));
97
+ }
98
+ if (/EACCES|permission denied/i.test(result.output)) {
99
+ log.line();
100
+ log.detail(
101
+ `npm needs permission to write to its global folder. Run ${color.cyan(`sudo ${shown}`)}, or set npm up to install without sudo: https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally`
102
+ );
103
+ }
104
+ throw new CliExit(1);
105
+ }
106
+ updating.succeed(`Updated writedocs ${version} → ${color.bold(latest)}`);
107
+ }
@@ -220,19 +220,9 @@ const darkNavbarFgMuted = darkNavbarConfigured
220
220
  // design choice, unlike text color above), falling back to the sitewide
221
221
  // --wd-primary (today's exact behavior) when unset - a site that sets a
222
222
  // navbar background but no accent keeps its ordinary brand-colored
223
- // active-tab fill exactly as before.
223
+ // active-tab underline.
224
224
  const lightNavbarAccent = navLight.accent ?? lightPrimary;
225
225
  const darkNavbarAccent = navDark.accent ?? darkPrimary;
226
- // --wd-navbar-accent-text: the active tab's own text color, painted on top
227
- // of --wd-navbar-accent above. Hardcoded to white before this feature
228
- // existed, which only ever looked right because every accent color in
229
- // practice (always --wd-primary until now) happened to be dark/saturated
230
- // enough for white text - contrastTextColor() (lib/config.ts) picks
231
- // black or white by actual luminance instead, so a light `accent` (e.g.
232
- // a pale one against a dark navbar) still gets legible active-tab text
233
- // rather than assuming white always works.
234
- const lightNavbarAccentText = contrastTextColor(lightNavbarAccent);
235
- const darkNavbarAccentText = contrastTextColor(darkNavbarAccent);
236
226
  // --wd-navbar-border: the switcher pills' (version/language/product) own
237
227
  // border. Never the flat --wd-border (a fixed neutral gray/slate tuned for
238
228
  // sitting on the page background) once navbar is configured - that reads
@@ -249,6 +239,14 @@ const lightNavbarBorder = lightNavbarConfigured
249
239
  const darkNavbarBorder = darkNavbarConfigured
250
240
  ? `color-mix(in srgb, ${darkNavbarFg} 25%, transparent)`
251
241
  : 'var(--wd-border)';
242
+ // --wd-navbar-hover: a topbar tab's background on hover. On a navbar with
243
+ // the page's own background, the sidebar's hover tint (NavTree.astro) -
244
+ // the same 6% of --wd-primary. A configured navbar can *be* the primary
245
+ // color, where that tint would vanish, so there it's a light tint of the
246
+ // navbar's own contrast-correct text color instead, like --wd-navbar-border.
247
+ const sidebarHover = 'color-mix(in srgb, var(--wd-primary) 6%, transparent)';
248
+ const lightNavbarHover = lightNavbarConfigured ? `color-mix(in srgb, ${lightNavbarFg} 12%, transparent)` : sidebarHover;
249
+ const darkNavbarHover = darkNavbarConfigured ? `color-mix(in srgb, ${darkNavbarFg} 12%, transparent)` : sidebarHover;
252
250
  // The <body> canvas's own paint - same color as --wd-background above
253
251
  // (there's only one background color to configure now), plus an optional
254
252
  // image layered on top. `images.light`/`.dark` are plain public/-relative
@@ -517,8 +515,8 @@ const fontExtraCss = [fontFaceCss, fontWeightCss].filter(Boolean).join('\n');
517
515
  wdNavbarFgLight: lightNavbarFg,
518
516
  wdNavbarFgMutedLight: lightNavbarFgMuted,
519
517
  wdNavbarAccentLight: lightNavbarAccent,
520
- wdNavbarAccentTextLight: lightNavbarAccentText,
521
518
  wdNavbarBorderLight: lightNavbarBorder,
519
+ wdNavbarHoverLight: lightNavbarHover,
522
520
  wdPageBgColorLight: lightPageBgColor,
523
521
  wdPageBgImageLight: lightPageBgImage,
524
522
  wdFontFamily,
@@ -531,8 +529,8 @@ const fontExtraCss = [fontFaceCss, fontWeightCss].filter(Boolean).join('\n');
531
529
  wdNavbarFgDark: darkNavbarFg,
532
530
  wdNavbarFgMutedDark: darkNavbarFgMuted,
533
531
  wdNavbarAccentDark: darkNavbarAccent,
534
- wdNavbarAccentTextDark: darkNavbarAccentText,
535
532
  wdNavbarBorderDark: darkNavbarBorder,
533
+ wdNavbarHoverDark: darkNavbarHover,
536
534
  wdPageBgColorDark: darkPageBgColor,
537
535
  wdPageBgImageDark: darkPageBgImage,
538
536
  }}
@@ -545,8 +543,8 @@ const fontExtraCss = [fontFaceCss, fontWeightCss].filter(Boolean).join('\n');
545
543
  --wd-navbar-foreground: var(--wdNavbarFgLight);
546
544
  --wd-navbar-foreground-muted: var(--wdNavbarFgMutedLight);
547
545
  --wd-navbar-accent: var(--wdNavbarAccentLight);
548
- --wd-navbar-accent-text: var(--wdNavbarAccentTextLight);
549
546
  --wd-navbar-border: var(--wdNavbarBorderLight);
547
+ --wd-navbar-hover: var(--wdNavbarHoverLight);
550
548
  --wd-page-bg-color: var(--wdPageBgColorLight);
551
549
  --wd-page-bg-image: var(--wdPageBgImageLight);
552
550
  --wd-text-muted: #64748b;
@@ -572,8 +570,8 @@ const fontExtraCss = [fontFaceCss, fontWeightCss].filter(Boolean).join('\n');
572
570
  --wd-navbar-foreground: var(--wdNavbarFgDark);
573
571
  --wd-navbar-foreground-muted: var(--wdNavbarFgMutedDark);
574
572
  --wd-navbar-accent: var(--wdNavbarAccentDark);
575
- --wd-navbar-accent-text: var(--wdNavbarAccentTextDark);
576
573
  --wd-navbar-border: var(--wdNavbarBorderDark);
574
+ --wd-navbar-hover: var(--wdNavbarHoverDark);
577
575
  --wd-page-bg-color: var(--wdPageBgColorDark);
578
576
  --wd-page-bg-image: var(--wdPageBgImageDark);
579
577
  --wd-text-muted: #94a3b8;
@@ -37,9 +37,8 @@
37
37
  (below) sits flush against this row's bottom edge (the outer
38
38
  .wd-topbar's own border-bottom, since this is the last row) rather
39
39
  than floating partway up inside a padded gap. That's what lets a
40
- plain border-bottom color change read as a real underline on hover,
41
- and what makes the active tab's own fill look like it's actually
42
- attached to the row's bottom edge instead of a floating pill. */
40
+ plain border-bottom color change read as a real underline under the
41
+ active tab. */
43
42
  padding-bottom: 0;
44
43
  }
45
44
  .wd-topbar-brand {
@@ -139,47 +138,31 @@
139
138
  /* Rounded top corners, square bottom - reads as an actual "tab"
140
139
  attached to the row's bottom edge (see .wd-topbar-row-tabs
141
140
  .wd-topbar-inner's own comment) rather than a floating pill. Same
142
- radius for every tab, active or not, so nothing needs to change
143
- shape on top when a tab becomes active - only the fill/border-bottom
144
- below do. */
141
+ radius for every tab, active or not - it also shapes the hover
142
+ background (below). */
145
143
  border-radius: 0.5rem 0.5rem 0 0;
146
- /* Transparent by default - this is what a hover (below) or the active
147
- state colors in, rather than a separate underline element. Sized to
148
- land exactly on the row's own bottom edge now that this row's inner
149
- wrapper has no bottom padding of its own. */
144
+ /* Transparent by default - the active state (below) colors it in,
145
+ rather than a separate underline element. Sized to land exactly on the
146
+ row's own bottom edge now that this row's inner wrapper has no bottom
147
+ padding of its own. */
150
148
  border-bottom: 2px solid transparent;
151
149
  text-decoration: none;
152
150
  color: var(--wd-navbar-foreground);
153
151
  font-size: 0.92rem;
154
152
  font-weight: 600;
155
153
  }
156
- /* The selected tab: a solid fill, not just a tinted background -
157
- AppIcon's <svg> render mode already uses fill="currentColor" (astro-
158
- icon's default), so its icon recolors along with the text; the
159
- emoji/text <span> fallback inherits `color` the same way.
160
- border-bottom-color matches the fill (rather than being reset to
161
- transparent) so there's no 2px seam of the row's own background
162
- showing between the fill and the row's bottom edge/border.
163
- --wd-navbar-accent (falls back to --wd-primary - see BaseLayout.astro),
164
- not a bare --wd-primary reference: without this, a site whose navbar
165
- background *is* --wd-primary (a plausible "brand-colored navbar" choice
166
- - the exact case that motivated this variable) would have its active
167
- tab's own fill disappear into the row's background entirely. Text color
168
- is --wd-navbar-accent-text (also BaseLayout.astro) rather than a
169
- hardcoded #fff for the matching reason - a light accent color needs
170
- dark text, not white, to stay legible on top of it. */
154
+ /* The selected tab: just its border-bottom, in --wd-navbar-accent (falls
155
+ back to --wd-primary - see BaseLayout.astro), landing on the row's
156
+ bottom edge as an underline. No fill - the text keeps the navbar's own
157
+ color. */
171
158
  .wd-tab-link.active {
172
- color: var(--wd-navbar-accent-text);
173
- background: var(--wd-navbar-accent);
174
159
  border-bottom-color: var(--wd-navbar-accent);
175
160
  }
176
- /* Inactive tabs just get their border-bottom colored in on hover -
177
- simpler than a separate underline element, and lines up naturally
178
- with the active tab's own border-bottom above since both are the same
179
- property. Text/icon color deliberately stays as-is on hover (only the
180
- border appears), matching the reference this was built against. */
181
- .wd-tab-link:hover:not(.active) {
182
- border-bottom-color: var(--wd-navbar-accent);
161
+ /* Any tab on hover: a light background tint, the same as a sidebar item's
162
+ hover (see --wd-navbar-hover in BaseLayout.astro). The rounded top
163
+ corners above shape it. */
164
+ .wd-tab-link:hover {
165
+ background: var(--wd-navbar-hover);
183
166
  }
184
167
  .wd-topbar-links {
185
168
  display: flex;
@@ -158,7 +158,7 @@ const stylesSchema = z.object({
158
158
  // Each side (`light`/`dark`) is either a bare string - just the
159
159
  // background color, exactly the original shape, kept for backwards
160
160
  // compatibility - or `{ background, accent? }`, for when a site also
161
- // wants the navbar's own active-tab-fill/hover-underline color to
161
+ // wants the navbar's own active-tab underline color to
162
162
  // differ from the sitewide `styles.colors.primary` (`accent`'s only
163
163
  // job - see BaseLayout.astro's lightNavbarAccent). Deliberately no
164
164
  // text/icon color field here at all: BaseLayout.astro always resolves
@@ -263,7 +263,7 @@ const logoSchema = z.union([
263
263
  // field's comment for what it now covers.
264
264
  // One side of `styles.navbar` (light or dark) - either a bare background
265
265
  // color (the original shape) or `{ background, accent? }` once a site
266
- // wants its own accent color (the active-tab fill / hover underline)
266
+ // wants its own accent color (the active tab's underline)
267
267
  // inside the navbar specifically, independent of the sitewide
268
268
  // `styles.colors.primary`. Deliberately does NOT carry a text/icon color
269
269
  // field at all - see `navbar`'s own comment below (stylesSchema) for why
@@ -400,7 +400,7 @@ const stylesSchema = z
400
400
  // Each side (`light`/`dark`) is either a bare string - just the
401
401
  // background color, exactly the original shape, kept for backwards
402
402
  // compatibility - or `{ background, accent? }`, for when a site also
403
- // wants the navbar's own active-tab-fill/hover-underline color to
403
+ // wants the navbar's own active-tab underline color to
404
404
  // differ from the sitewide `styles.colors.primary` (`accent`'s only
405
405
  // job - see BaseLayout.astro's lightNavbarAccent). Deliberately no
406
406
  // text/icon color field here at all: BaseLayout.astro always resolves
@@ -111,10 +111,10 @@ export const DESCRIPTIONS = {
111
111
  'styles.navbar': 'The topbar\'s own background. Without it, the topbar matches the page background. Its text turns black or white automatically.',
112
112
  'styles.navbar.light': 'Topbar in light mode: a background color, or `{ background, accent }`.',
113
113
  'styles.navbar.light.background': 'Topbar background color in light mode.',
114
- 'styles.navbar.light.accent': 'Color of the active tab and hover states in the topbar, in place of `styles.colors.primary`.',
114
+ 'styles.navbar.light.accent': 'Color of the underline under the active tab in the topbar, in place of `styles.colors.primary`.',
115
115
  'styles.navbar.dark': 'Topbar in dark mode: a background color, or `{ background, accent }`.',
116
116
  'styles.navbar.dark.background': 'Topbar background color in dark mode.',
117
- 'styles.navbar.dark.accent': 'Color of the active tab and hover states in the topbar, in dark mode.',
117
+ 'styles.navbar.dark.accent': 'Color of the underline under the active tab in the topbar, in dark mode.',
118
118
  'styles.background': 'Background color of the site, and an optional background image.',
119
119
  'styles.background.colors': 'Background colors.',
120
120
  'styles.background.colors.light': 'Background color in light mode. Default "#ffffff".',
@@ -252,8 +252,8 @@
252
252
  },
253
253
  "accent": {
254
254
  "type": "string",
255
- "description": "Color of the active tab and hover states in the topbar, in place of `styles.colors.primary`.",
256
- "markdownDescription": "Color of the active tab and hover states in the topbar, in place of `styles.colors.primary`."
255
+ "description": "Color of the underline under the active tab in the topbar, in place of `styles.colors.primary`.",
256
+ "markdownDescription": "Color of the underline under the active tab in the topbar, in place of `styles.colors.primary`."
257
257
  }
258
258
  },
259
259
  "required": [
@@ -280,8 +280,8 @@
280
280
  },
281
281
  "accent": {
282
282
  "type": "string",
283
- "description": "Color of the active tab and hover states in the topbar, in dark mode.",
284
- "markdownDescription": "Color of the active tab and hover states in the topbar, in dark mode."
283
+ "description": "Color of the underline under the active tab in the topbar, in dark mode.",
284
+ "markdownDescription": "Color of the underline under the active tab in the topbar, in dark mode."
285
285
  }
286
286
  },
287
287
  "required": [