jamdesk 1.1.175 → 1.1.177

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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=dev-workspace-symlinks.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-workspace-symlinks.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/unit/dev-workspace-symlinks.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @vitest-environment node
3
+ *
4
+ * Tests prepareProjectWorkspaceLinks — replaces the single
5
+ * <workspace>/projects/<name> -> <projectDir> symlink with per-entry
6
+ * symlinks that skip non-active language directories. This is what
7
+ * actually reduces Turbopack's filesystem scan from 403 MDX files to
8
+ * 135 on jamdesk-docs.
9
+ */
10
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
11
+ import fs from 'fs-extra';
12
+ import path from 'path';
13
+ import { tmpdir } from 'os';
14
+ import { prepareProjectWorkspaceLinks } from '../../commands/dev.js';
15
+ import { output } from '../../lib/output.js';
16
+ describe('prepareProjectWorkspaceLinks', () => {
17
+ let tmpRoot;
18
+ let projectDir;
19
+ let workspaceProjectDir;
20
+ beforeEach(() => {
21
+ tmpRoot = fs.mkdtempSync(path.join(tmpdir(), 'jam-ws-'));
22
+ projectDir = path.join(tmpRoot, 'project');
23
+ workspaceProjectDir = path.join(tmpRoot, 'ws', 'projects', 'project');
24
+ // Set up a project layout that mirrors a multi-language docs project:
25
+ // project/
26
+ // ai/intro.mdx (en at root)
27
+ // development/foo.mdx (en at root)
28
+ // es/ai/intro.mdx (spanish)
29
+ // fr/ai/intro.mdx (french)
30
+ // docs.json
31
+ // images/logo.png
32
+ fs.mkdirpSync(path.join(projectDir, 'ai'));
33
+ fs.writeFileSync(path.join(projectDir, 'ai', 'intro.mdx'), '# en');
34
+ fs.mkdirpSync(path.join(projectDir, 'development'));
35
+ fs.writeFileSync(path.join(projectDir, 'development', 'foo.mdx'), '# en');
36
+ fs.mkdirpSync(path.join(projectDir, 'es', 'ai'));
37
+ fs.writeFileSync(path.join(projectDir, 'es', 'ai', 'intro.mdx'), '# es');
38
+ fs.mkdirpSync(path.join(projectDir, 'fr', 'ai'));
39
+ fs.writeFileSync(path.join(projectDir, 'fr', 'ai', 'intro.mdx'), '# fr');
40
+ fs.writeFileSync(path.join(projectDir, 'docs.json'), '{}');
41
+ fs.mkdirpSync(path.join(projectDir, 'images'));
42
+ fs.writeFileSync(path.join(projectDir, 'images', 'logo.png'), '');
43
+ });
44
+ afterEach(() => {
45
+ fs.removeSync(tmpRoot);
46
+ });
47
+ it('symlinks every top-level entry when skip set is empty', async () => {
48
+ await prepareProjectWorkspaceLinks(projectDir, workspaceProjectDir, new Set());
49
+ expect(fs.existsSync(path.join(workspaceProjectDir, 'ai', 'intro.mdx'))).toBe(true);
50
+ expect(fs.existsSync(path.join(workspaceProjectDir, 'es', 'ai', 'intro.mdx'))).toBe(true);
51
+ expect(fs.existsSync(path.join(workspaceProjectDir, 'fr', 'ai', 'intro.mdx'))).toBe(true);
52
+ });
53
+ it('does not symlink docs.json (caller writes a filtered copy)', async () => {
54
+ // docs.json must not be symlinked — the caller writes a per-language
55
+ // filtered copy, and fs.writeFile through a symlink would clobber the
56
+ // user's source docs.json.
57
+ await prepareProjectWorkspaceLinks(projectDir, workspaceProjectDir, new Set());
58
+ expect(fs.existsSync(path.join(workspaceProjectDir, 'docs.json'))).toBe(false);
59
+ });
60
+ it('skips entries whose names are in the skip set', async () => {
61
+ await prepareProjectWorkspaceLinks(projectDir, workspaceProjectDir, new Set(['es', 'fr']));
62
+ expect(fs.existsSync(path.join(workspaceProjectDir, 'ai', 'intro.mdx'))).toBe(true);
63
+ expect(fs.existsSync(path.join(workspaceProjectDir, 'development', 'foo.mdx'))).toBe(true);
64
+ expect(fs.existsSync(path.join(workspaceProjectDir, 'images', 'logo.png'))).toBe(true);
65
+ // Skipped:
66
+ expect(fs.existsSync(path.join(workspaceProjectDir, 'es'))).toBe(false);
67
+ expect(fs.existsSync(path.join(workspaceProjectDir, 'fr'))).toBe(false);
68
+ // docs.json not symlinked — caller writes a filtered copy.
69
+ expect(fs.existsSync(path.join(workspaceProjectDir, 'docs.json'))).toBe(false);
70
+ });
71
+ it('rebuilds the workspace links from scratch on subsequent calls', async () => {
72
+ // First call: no skip
73
+ await prepareProjectWorkspaceLinks(projectDir, workspaceProjectDir, new Set());
74
+ expect(fs.existsSync(path.join(workspaceProjectDir, 'es'))).toBe(true);
75
+ // Second call: skip es
76
+ await prepareProjectWorkspaceLinks(projectDir, workspaceProjectDir, new Set(['es']));
77
+ expect(fs.existsSync(path.join(workspaceProjectDir, 'es'))).toBe(false);
78
+ expect(fs.existsSync(path.join(workspaceProjectDir, 'fr'))).toBe(true);
79
+ expect(fs.existsSync(path.join(workspaceProjectDir, 'ai', 'intro.mdx'))).toBe(true);
80
+ });
81
+ it('handles a pre-existing single symlink at workspaceProjectDir (legacy layout)', async () => {
82
+ // Pre-create the legacy single-symlink layout
83
+ fs.mkdirpSync(path.dirname(workspaceProjectDir));
84
+ fs.symlinkSync(projectDir, workspaceProjectDir, 'junction');
85
+ await prepareProjectWorkspaceLinks(projectDir, workspaceProjectDir, new Set(['es', 'fr']));
86
+ const lstat = fs.lstatSync(workspaceProjectDir);
87
+ expect(lstat.isDirectory()).toBe(true);
88
+ expect(fs.existsSync(path.join(workspaceProjectDir, 'ai', 'intro.mdx'))).toBe(true);
89
+ expect(fs.existsSync(path.join(workspaceProjectDir, 'es'))).toBe(false);
90
+ });
91
+ it('surfaces friendly error (not raw stack trace) when fs.rm throws ENOTEMPTY', async () => {
92
+ // Regression: before safeRemoveCache, fs.remove raised an unfriendly stack
93
+ // trace when Turbopack still held open files. Now safeRemoveCache detects
94
+ // the race and calls process.exit(1) with a human-readable message.
95
+ const rmSpy = vi.spyOn(fs, 'rm');
96
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code) => {
97
+ throw new Error(`process.exit:${code}`);
98
+ }));
99
+ const errorSpy = vi.spyOn(output, 'error').mockImplementation(() => undefined);
100
+ const enotempty = Object.assign(new Error('ENOTEMPTY'), { code: 'ENOTEMPTY' });
101
+ // fs.rm will be called by safeRemoveCache; make it fail with ENOTEMPTY
102
+ // even after the internal maxRetries — simulate persistent race condition.
103
+ rmSpy.mockRejectedValue(enotempty);
104
+ await expect(prepareProjectWorkspaceLinks(projectDir, workspaceProjectDir, new Set())).rejects.toThrow('process.exit:1');
105
+ const msg = errorSpy.mock.calls[0]?.[0] ?? '';
106
+ expect(msg).toContain('Another `jamdesk dev` instance');
107
+ expect(msg).toContain('pkill -f');
108
+ expect(exitSpy).toHaveBeenCalledWith(1);
109
+ vi.restoreAllMocks();
110
+ });
111
+ });
112
+ //# sourceMappingURL=dev-workspace-symlinks.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-workspace-symlinks.test.js","sourceRoot":"","sources":["../../../src/__tests__/unit/dev-workspace-symlinks.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=language-filter.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"language-filter.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/unit/language-filter.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,166 @@
1
+ /**
2
+ * @vitest-environment node
3
+ *
4
+ * Tests for getActiveLanguageFilter — pure helper that decides which
5
+ * top-level language directories to skip when symlinking project content
6
+ * into the dev workspace. Multi-language sites' non-default languages
7
+ * inflate Turbopack's filesystem scan and balloon cold compile time
8
+ * (jamdesk-docs: 67s with 3 langs vs. 12.5s with 1 lang).
9
+ */
10
+ import { describe, it, expect } from 'vitest';
11
+ import { getActiveLanguageFilter, isPageInSkippedLanguage, filterConfigByActiveLanguage, } from '../../lib/language-filter.js';
12
+ const config = (langs) => ({
13
+ navigation: { languages: langs.map(l => ({ language: l.language, default: l.default })) },
14
+ });
15
+ describe('getActiveLanguageFilter', () => {
16
+ it('returns null filter when project has no languages array', () => {
17
+ const result = getActiveLanguageFilter({ navigation: {} }, undefined, false);
18
+ expect(result).toEqual({ active: null, skip: new Set() });
19
+ });
20
+ it('returns null filter when project has only one language', () => {
21
+ const result = getActiveLanguageFilter(config([{ language: 'en', default: true }]), undefined, false);
22
+ expect(result).toEqual({ active: 'en', skip: new Set() });
23
+ });
24
+ it('skips non-default languages when active is the default', () => {
25
+ const result = getActiveLanguageFilter(config([
26
+ { language: 'en', default: true },
27
+ { language: 'es' },
28
+ { language: 'fr' },
29
+ ]), undefined, false);
30
+ expect(result.active).toBe('en');
31
+ expect(result.skip).toEqual(new Set(['es', 'fr']));
32
+ });
33
+ it('falls back to first language when none is marked default', () => {
34
+ const result = getActiveLanguageFilter(config([{ language: 'en' }, { language: 'es' }, { language: 'fr' }]), undefined, false);
35
+ expect(result.active).toBe('en');
36
+ expect(result.skip).toEqual(new Set(['es', 'fr']));
37
+ });
38
+ it('accepts --lang matching the default language (no-op equivalence)', () => {
39
+ const result = getActiveLanguageFilter(config([
40
+ { language: 'en', default: true },
41
+ { language: 'es' },
42
+ ]), 'en', false);
43
+ expect(result.active).toBe('en');
44
+ expect(result.skip).toEqual(new Set(['es']));
45
+ });
46
+ it('throws on --lang for a non-default language (initial release scope)', () => {
47
+ // Non-default-language layouts mix root-level default-lang content with
48
+ // <lang>/ subdirs for translations; correctly stripping the default
49
+ // content while keeping <lang>/ takes more work and is deferred to a
50
+ // follow-up. For now, surface the workaround clearly.
51
+ expect(() => getActiveLanguageFilter(config([
52
+ { language: 'en', default: true },
53
+ { language: 'es' },
54
+ { language: 'fr' },
55
+ ]), 'es', false)).toThrow(/--lang es: previewing non-default languages.*--all-langs/i);
56
+ });
57
+ it('returns empty skip set when --all-langs is set', () => {
58
+ const result = getActiveLanguageFilter(config([
59
+ { language: 'en', default: true },
60
+ { language: 'es' },
61
+ ]), undefined, true);
62
+ expect(result.active).toBe('en');
63
+ expect(result.skip).toEqual(new Set());
64
+ });
65
+ it('throws on --lang code that does not exist in docs.json', () => {
66
+ expect(() => getActiveLanguageFilter(config([{ language: 'en', default: true }, { language: 'es' }]), 'de', false)).toThrow(/--lang de.*not.*docs\.json.*Available: en, es/);
67
+ });
68
+ it('handles malformed languages array (entries missing language field) by ignoring them', () => {
69
+ const malformed = {
70
+ navigation: {
71
+ languages: [
72
+ { language: 'en', default: true },
73
+ { default: false }, // missing language: ignored
74
+ { language: 'es' },
75
+ { language: 42 }, // wrong type: ignored
76
+ ],
77
+ },
78
+ };
79
+ const result = getActiveLanguageFilter(malformed, undefined, false);
80
+ expect(result.active).toBe('en');
81
+ expect(result.skip).toEqual(new Set(['es']));
82
+ });
83
+ it('returns null active when no valid language entries exist', () => {
84
+ const result = getActiveLanguageFilter({ navigation: { languages: [{ default: true }] } }, undefined, false);
85
+ expect(result).toEqual({ active: null, skip: new Set() });
86
+ });
87
+ it('rejects empty-string --lang as an invalid code (commander passes "" through)', () => {
88
+ // commander.js treats `--lang ""` as a value, not as missing — so
89
+ // langOption is "" (defined, but empty). Empty string is never a valid
90
+ // language code; surface it as a clear error rather than silently
91
+ // falling back to the default.
92
+ expect(() => getActiveLanguageFilter(config([{ language: 'en', default: true }, { language: 'es' }]), '', false)).toThrow(/--lang.*not.*docs\.json/);
93
+ });
94
+ });
95
+ describe('isPageInSkippedLanguage', () => {
96
+ it('returns false when skip set is empty', () => {
97
+ expect(isPageInSkippedLanguage('fr/introduction', new Set())).toBe(false);
98
+ });
99
+ it('returns true when first path segment is a skipped language', () => {
100
+ expect(isPageInSkippedLanguage('fr/introduction', new Set(['fr']))).toBe(true);
101
+ });
102
+ it('returns true for nested paths inside a skipped language', () => {
103
+ expect(isPageInSkippedLanguage('fr/setup/connecting-github', new Set(['fr', 'de']))).toBe(true);
104
+ });
105
+ it('returns false when first segment is the active language', () => {
106
+ expect(isPageInSkippedLanguage('en/introduction', new Set(['fr']))).toBe(false);
107
+ });
108
+ it('returns false for unprefixed root pages', () => {
109
+ expect(isPageInSkippedLanguage('introduction', new Set(['fr']))).toBe(false);
110
+ });
111
+ it('does not match when the skip code is a prefix-substring of a different segment', () => {
112
+ // Guard against startsWith('fr/') matching e.g. 'fr-something/foo'
113
+ expect(isPageInSkippedLanguage('fr-something/foo', new Set(['fr']))).toBe(false);
114
+ });
115
+ it('returns true for a path with a fragment anchor in a skipped language', () => {
116
+ // Broken-anchor warnings from validate-links.cjs surface as link values
117
+ // like `fr/introduction#missing-section` — the fragment lives past the
118
+ // first segment so the language check still works.
119
+ expect(isPageInSkippedLanguage('fr/introduction#setup', new Set(['fr']))).toBe(true);
120
+ });
121
+ });
122
+ describe('filterConfigByActiveLanguage', () => {
123
+ it('returns the input unchanged (same reference) when skip is empty', () => {
124
+ const config = { name: 'foo', navigation: { languages: [{ language: 'en' }] } };
125
+ const result = filterConfigByActiveLanguage(config, { active: 'en', skip: new Set() });
126
+ expect(result).toBe(config);
127
+ });
128
+ it('drops skipped languages from navigation.languages', () => {
129
+ const config = {
130
+ name: 'jamdesk-docs',
131
+ navigation: {
132
+ languages: [
133
+ { language: 'en', default: true, tabs: [{ tab: 'Guide' }] },
134
+ { language: 'fr', tabs: [{ tab: 'Guide' }] },
135
+ { language: 'de', tabs: [{ tab: 'Guide' }] },
136
+ ],
137
+ },
138
+ };
139
+ const result = filterConfigByActiveLanguage(config, {
140
+ active: 'en',
141
+ skip: new Set(['fr', 'de']),
142
+ });
143
+ expect(result.navigation.languages).toEqual([
144
+ { language: 'en', default: true, tabs: [{ tab: 'Guide' }] },
145
+ ]);
146
+ // Top-level fields preserved.
147
+ expect(result.name).toBe('jamdesk-docs');
148
+ // Original config untouched.
149
+ expect(config.navigation.languages).toHaveLength(3);
150
+ });
151
+ it('preserves other navigation fields (tabs, global, anchors, etc.)', () => {
152
+ const config = {
153
+ navigation: {
154
+ languages: [{ language: 'en' }, { language: 'fr' }],
155
+ global: { anchors: [{ anchor: 'Support' }] },
156
+ },
157
+ };
158
+ const result = filterConfigByActiveLanguage(config, {
159
+ active: 'en',
160
+ skip: new Set(['fr']),
161
+ });
162
+ expect(result.navigation.global).toEqual({ anchors: [{ anchor: 'Support' }] });
163
+ expect(result.navigation.languages).toHaveLength(1);
164
+ });
165
+ });
166
+ //# sourceMappingURL=language-filter.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"language-filter.test.js","sourceRoot":"","sources":["../../../src/__tests__/unit/language-filter.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Picks which `<lang>/` directories under projectDir to keep as workspace
3
+ * symlinks and which to skip. Skipping non-active language directories
4
+ * shrinks Turbopack's filesystem scan and dropped cold compile from ~67s
5
+ * to ~12s on jamdesk-docs (commit 90d781b4).
6
+ *
7
+ * The dev server still reads content from the user's source tree via
8
+ * JAMDESK_PROJECTS_DIR, so the language picker keeps showing every
9
+ * language and clicks always 200 OK — the workspace symlinks are a
10
+ * Turbopack-only performance layer.
11
+ *
12
+ * Default rules:
13
+ * 1. Language with `default: true`
14
+ * 2. First language in `navigation.languages[]`
15
+ */
16
+ export interface LanguageFilter {
17
+ active: string | null;
18
+ skip: Set<string>;
19
+ }
20
+ interface NavigationLanguageEntry {
21
+ language?: string;
22
+ default?: boolean;
23
+ }
24
+ interface MinimalConfig {
25
+ navigation?: {
26
+ languages?: NavigationLanguageEntry[];
27
+ };
28
+ }
29
+ export declare function getActiveLanguageFilter(config: MinimalConfig): LanguageFilter;
30
+ export {};
31
+ //# sourceMappingURL=language-filter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"language-filter.d.ts","sourceRoot":"","sources":["../../src/lib/language-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACnB;AAED,UAAU,uBAAuB;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,UAAU,aAAa;IACrB,UAAU,CAAC,EAAE;QACX,SAAS,CAAC,EAAE,uBAAuB,EAAE,CAAC;KACvC,CAAC;CACH;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,aAAa,GAAG,cAAc,CAc7E"}
@@ -0,0 +1,14 @@
1
+ export function getActiveLanguageFilter(config) {
2
+ const validEntries = (config.navigation?.languages ?? []).filter((l) => typeof l.language === 'string');
3
+ if (validEntries.length === 0) {
4
+ return { active: null, skip: new Set() };
5
+ }
6
+ const codes = validEntries.map((l) => l.language);
7
+ const active = validEntries.find((l) => l.default)?.language ?? validEntries[0].language;
8
+ if (codes.length === 1) {
9
+ return { active, skip: new Set() };
10
+ }
11
+ const skip = new Set(codes.filter((c) => c !== active));
12
+ return { active, skip };
13
+ }
14
+ //# sourceMappingURL=language-filter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"language-filter.js","sourceRoot":"","sources":["../../src/lib/language-filter.ts"],"names":[],"mappings":"AA+BA,MAAM,UAAU,uBAAuB,CAAC,MAAqB;IAC3D,MAAM,YAAY,GAAG,CAAC,MAAM,CAAC,UAAU,EAAE,SAAS,IAAI,EAAE,CAAC,CAAC,MAAM,CAC9D,CAAC,CAAC,EAAgD,EAAE,CAAC,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ,CACpF,CAAC;IACF,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,GAAG,EAAU,EAAE,CAAC;IACnD,CAAC;IACD,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IAClD,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,QAAQ,IAAI,YAAY,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;IACzF,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,EAAU,EAAE,CAAC;IAC7C,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC;IACxD,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AAC1B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jamdesk",
3
- "version": "1.1.175",
3
+ "version": "1.1.177",
4
4
  "description": "CLI for Jamdesk — build, preview, and deploy documentation sites from MDX. Dev server with hot reload, 50+ components, OpenAPI support, AI search, and Mintlify migration",
5
5
  "keywords": [
6
6
  "jamdesk",
@@ -132,8 +132,8 @@ const promptUtilityClassName = [
132
132
  ].join(' ');
133
133
 
134
134
  const primaryActionClassName = [
135
- 'inline-flex min-h-10 min-w-0 items-center gap-2 px-4 py-2 text-sm font-semibold',
136
- 'prompt-primary-action bg-[var(--color-text-primary)] text-[var(--color-bg-primary)] transition-opacity cursor-pointer',
135
+ 'inline-flex min-h-8 min-w-0 items-center gap-2 px-3.5 py-1 text-sm font-semibold',
136
+ 'prompt-primary-action bg-[var(--color-primary)] text-white transition-opacity cursor-pointer',
137
137
  'hover:opacity-90',
138
138
  contrastSafeFocusClassName,
139
139
  ].join(' ');
@@ -210,7 +210,7 @@ function PromptDialog({
210
210
  >
211
211
  <div className="flex max-h-[calc(100dvh-2rem)] min-h-0 flex-col">
212
212
  <div className="flex shrink-0 items-center justify-between gap-3 border-b border-[var(--color-border)] px-4 py-3">
213
- <h2 className="m-0 min-w-0 truncate text-sm font-semibold text-[var(--color-text-primary)]">
213
+ <h2 className="prompt-dialog-title m-0 min-w-0 truncate text-sm font-semibold text-[var(--color-text-primary)]">
214
214
  {title ?? 'Prompt'}
215
215
  </h2>
216
216
  <button
@@ -243,11 +243,21 @@ function PromptDialog({
243
243
  </span>
244
244
  </div>
245
245
  <style jsx>{`
246
+ /* A fixed black scrim: the text token flips to near-white in dark
247
+ mode and washed the whole page out behind the dialog. */
246
248
  .prompt-dialog::backdrop {
247
- background: var(--color-text-primary);
248
- opacity: 0.6;
249
+ background: rgb(0 0 0 / 0.55);
249
250
  backdrop-filter: blur(4px);
250
251
  }
252
+ /* Outranks the docs themes' .prose h2 rules, which reach into the
253
+ dialog and would inflate this chrome heading. */
254
+ .prompt-dialog .prompt-dialog-title {
255
+ margin: 0;
256
+ font-size: 0.875rem;
257
+ line-height: 1.5;
258
+ font-weight: 600;
259
+ letter-spacing: normal;
260
+ }
251
261
  `}</style>
252
262
  </dialog>
253
263
  );
@@ -413,7 +423,7 @@ export function Prompt({
413
423
  ) : null}
414
424
  <div
415
425
  data-testid="prompt-actions"
416
- className="flex min-w-0 max-w-full items-stretch justify-end self-end sm:ml-auto sm:self-auto"
426
+ className="flex min-w-0 max-w-full items-center justify-end self-end sm:ml-auto sm:self-auto"
417
427
  >
418
428
  {primary ? (
419
429
  <a
@@ -421,7 +431,7 @@ export function Prompt({
421
431
  target="_blank"
422
432
  rel="noopener noreferrer"
423
433
  aria-label={primary.label}
424
- className={`${primaryActionClassName} ${secondary.length > 0 ? 'rounded-l-[var(--radius-lg)]' : 'rounded-[var(--radius-lg)]'}`}
434
+ className={`${primaryActionClassName} ${secondary.length > 0 ? 'rounded-l-full' : 'rounded-full'}`}
425
435
  >
426
436
  <span aria-hidden="true"><ActionIcon action={primary.action} /></span>
427
437
  <span className="min-w-0 truncate">{primary.label}</span>
@@ -436,7 +446,7 @@ export function Prompt({
436
446
  aria-haspopup="menu"
437
447
  aria-expanded={menuOpen}
438
448
  aria-controls={menuId}
439
- className={`${primaryActionClassName} min-w-10 rounded-r-[var(--radius-lg)] border-l border-[var(--color-bg-primary)] px-3`}
449
+ className={`${primaryActionClassName} min-w-8 rounded-r-full border-l border-[var(--color-bg-primary)] px-2.5`}
440
450
  onMouseDown={(event) => {
441
451
  if (menuOpen) event.preventDefault();
442
452
  }}
@@ -468,7 +478,7 @@ export function Prompt({
468
478
  target="_blank"
469
479
  rel="noopener noreferrer"
470
480
  aria-label={action.label}
471
- className={`flex min-h-10 min-w-0 max-w-full cursor-pointer items-center gap-2 rounded-[var(--radius-lg)] px-3 py-2 text-sm text-[var(--color-text-primary)] hover:bg-[var(--color-border)] ${contrastSafeFocusClassName}`}
481
+ className={`prompt-menu-item flex min-h-10 min-w-0 max-w-full cursor-pointer items-center gap-2 rounded-[var(--radius-lg)] px-3 py-2 text-sm text-[var(--color-text-primary)] hover:bg-[var(--color-border)] ${contrastSafeFocusClassName}`}
472
482
  onClick={closeMenuAndFocusTrigger}
473
483
  >
474
484
  <span aria-hidden="true"><ActionIcon action={action.action} /></span>
@@ -497,7 +507,7 @@ export function Prompt({
497
507
  >
498
508
  <span
499
509
  data-testid="prompt-icon"
500
- className="mt-1 shrink-0 text-[var(--color-primary)]"
510
+ className="flex h-6 shrink-0 items-center text-[var(--color-primary)]"
501
511
  aria-hidden="true"
502
512
  >
503
513
  <Icon icon={authorIcon} size={18} />
@@ -508,8 +518,11 @@ export function Prompt({
508
518
  >
509
519
  {hasDescription ? renderInlineMarkdown(description) : prompt || children}
510
520
  </div>
521
+ {/* -my-2 keeps the 40px hit targets while the buttons occupy one
522
+ 24px text line, so they center on the first preview line and a
523
+ one-line prompt yields a compact well. */}
511
524
  {hasPrompt ? (
512
- <div data-testid="prompt-utilities" className="flex shrink-0 items-center gap-1">
525
+ <div data-testid="prompt-utilities" className="-my-2 flex shrink-0 items-center gap-1">
513
526
  <button
514
527
  ref={expandTriggerRef}
515
528
  type="button"
@@ -553,12 +566,32 @@ export function Prompt({
553
566
  border-color: var(--color-border);
554
567
  }
555
568
  .prompt-primary-action {
556
- background: var(--color-text-primary);
557
- color: var(--color-bg-primary);
569
+ background: var(--color-primary);
570
+ color: #fff;
558
571
  }
559
572
  .prompt-title {
560
573
  color: var(--color-text-primary);
561
574
  }
575
+ /* The docs themes' prose CSS is not Tailwind Typography — .prose h3 and
576
+ .prose a match descendants even inside .not-prose, and the jam theme's
577
+ body[data-theme] variants outrank Tailwind utilities. These scoped
578
+ rules win on specificity to keep the header compact and link-free. */
579
+ .prompt-card-fern .prompt-title {
580
+ margin: 0;
581
+ font-size: 1rem;
582
+ line-height: 1.5;
583
+ font-weight: 600;
584
+ letter-spacing: normal;
585
+ }
586
+ .prompt-card-fern .prompt-primary-action {
587
+ text-decoration: none;
588
+ font-weight: 600;
589
+ }
590
+ .prompt-card-fern .prompt-menu-item {
591
+ text-decoration: none;
592
+ font-weight: 400;
593
+ color: var(--color-text-primary);
594
+ }
562
595
  @supports (color: color-mix(in srgb, black, white)) {
563
596
  .prompt-card-fern {
564
597
  border-color: color-mix(in srgb, var(--color-primary) 48%, var(--color-border));
@@ -568,9 +601,16 @@ export function Prompt({
568
601
  border-color: color-mix(in srgb, var(--color-border) 88%, var(--color-primary));
569
602
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 3%, transparent);
570
603
  }
604
+ /* 40% primary keeps >=4.5:1 against the card background even when
605
+ --color-primary is set to the mode's background extreme; 45% fails. */
606
+ .prompt-card-fern .prompt-title {
607
+ color: color-mix(in srgb, var(--color-primary) 40%, var(--color-text-primary));
608
+ }
571
609
  }
610
+ /* Three lines of leading-6, then scroll — the Expand dialog carries
611
+ the full prompt. */
572
612
  .prompt-preview {
573
- max-height: 10rem;
613
+ max-height: 4.5rem;
574
614
  overflow-y: auto;
575
615
  overflow-wrap: anywhere;
576
616
  }
@@ -760,7 +760,11 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
760
760
  const visibilityClass = showTabsInSidebar ? '' : 'lg:hidden';
761
761
 
762
762
  return (
763
- <div className={`sidebar-tabs flex flex-col gap-2 px-4 pt-2 pb-6 ${visibilityClass}`}>
763
+ // Inline-start padding is 12px (pl-3), not 16px, so each tab's px-3
764
+ // padding lands its icon at 24px — flush with the sidebar group icons
765
+ // (ml-6) below — and the active tab's highlight left edge lands at 12px
766
+ // to match the lower nav items' pill (.sidebar-scroll ...::before).
767
+ <div className={`sidebar-tabs flex flex-col gap-2 pl-3 pr-4 pt-2 pb-6 ${visibilityClass}`}>
764
768
  {resolvedNav.tabs.map((tab) => {
765
769
  const isActive = tab.name === resolvedNav.activeTab;
766
770