@writedocs/generator 0.8.1 → 0.9.0
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/astro.config.mjs +2 -1
- package/bin/writedocs.js +10 -7
- package/package.json +1 -1
- package/src/cli/build.js +15 -0
- package/src/cli/check-built-styles.js +33 -0
- package/src/cli/dev.js +4 -0
- package/src/cli/run-astro.js +4 -0
- package/src/components/ApiLangSelect.astro +1 -2
- package/src/components/CopyPageMenu.astro +119 -34
- package/src/layout/BaseLayout.astro +2 -0
- package/src/layout/components/TopBar.astro +113 -94
- package/src/layout/styles/search-modal.css +27 -0
- package/src/layout/styles/topbar.css +107 -1
- package/src/lib/canonical-path.js +11 -0
- package/src/lib/config-schema.js +99 -6
- package/src/lib/config-schema.ts +120 -7
- package/src/lib/json-schema-descriptions.js +1 -1
- package/src/lib/mcp-links.js +30 -0
- package/src/lib/mintlify-convert.js +5 -5
- package/src/lib/search-scope.js +44 -0
- package/src/lib/tabs-strip-offset.js +31 -0
- package/src/lib/writedocs-temp-dir.js +3 -1
- package/src/pages/[...slug].astro +53 -3
- package/src/scripts/dropdowns.ts +70 -35
- package/src/scripts/search.ts +58 -4
- package/src/scripts/tabs-strip.ts +169 -0
- package/writedocs.schema.json +6 -3
package/astro.config.mjs
CHANGED
|
@@ -37,6 +37,7 @@ import { remarkExtractInlineReactComponents } from './src/lib/mdx-inline-react.j
|
|
|
37
37
|
import { writedocsTempDir, writedocsBuildStagingDir } from './src/lib/writedocs-temp-dir.js';
|
|
38
38
|
import { stylesAssetFallback } from './src/lib/styles-asset-integration.js';
|
|
39
39
|
import { mcpDevServer } from './src/lib/mcp-dev-integration.js';
|
|
40
|
+
import { canonicalPath } from './src/lib/canonical-path.js';
|
|
40
41
|
import { report } from './src/lib/cli-report.js';
|
|
41
42
|
import {
|
|
42
43
|
loadDocsConfig,
|
|
@@ -54,7 +55,7 @@ const contentDir = process.env.WRITEDOCS_CONTENT_DIR || process.cwd();
|
|
|
54
55
|
// its dependencies' own module resolution keeps working), while build.js
|
|
55
56
|
// does the actual cross-filesystem-safe copy into contentDir/dist as an
|
|
56
57
|
// explicit final step once the build itself is done.
|
|
57
|
-
const packageRoot = process.env.WRITEDOCS_PACKAGE_ROOT || path.dirname(fileURLToPath(import.meta.url));
|
|
58
|
+
const packageRoot = canonicalPath(process.env.WRITEDOCS_PACKAGE_ROOT || path.dirname(fileURLToPath(import.meta.url)));
|
|
58
59
|
// Where this package's dependencies are: the node_modules folder that
|
|
59
60
|
// contains an installed package (<project>/node_modules/@writedocs/
|
|
60
61
|
// generator -> <project>/node_modules), or packageRoot itself in a checkout,
|
package/bin/writedocs.js
CHANGED
|
@@ -11,6 +11,7 @@ import { requireBuildKey } from '../src/cli/build-auth.js';
|
|
|
11
11
|
import { log, step, plural, color, CliExit, errorText, stopActiveStep } from '../src/cli/output.js';
|
|
12
12
|
import { startUpdateCheck, showUpdateNotice } from '../src/cli/update-check.js';
|
|
13
13
|
import { readConfigText } from '../src/lib/config-file.js';
|
|
14
|
+
import { canonicalPath } from '../src/lib/canonical-path.js';
|
|
14
15
|
// O MESMO modulo que o build usa (via loadDocsConfig, que reexporta daqui) e
|
|
15
16
|
// que a plataforma importa por `@writedocs/generator/config-schema` - e o que
|
|
16
17
|
// faz os tres reportarem os mesmos problemas com as mesmas palavras, em vez de
|
|
@@ -32,7 +33,9 @@ import {
|
|
|
32
33
|
} from '../src/lib/config-schema.js';
|
|
33
34
|
|
|
34
35
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
35
|
-
|
|
36
|
+
// The drive letter in capitals, however the shell spelled it - see
|
|
37
|
+
// src/lib/canonical-path.js.
|
|
38
|
+
const packageRoot = canonicalPath(path.resolve(__dirname, '..'));
|
|
36
39
|
// Read rather than hardcode - a literal version string here silently drifts
|
|
37
40
|
// from package.json's own "version" the moment either one is bumped without
|
|
38
41
|
// the other (exactly what `npm version <bump>` does: it only touches
|
|
@@ -64,7 +67,7 @@ program
|
|
|
64
67
|
.option('--no-open', "don't open the preview in the browser")
|
|
65
68
|
.action(async (dir, opts) => {
|
|
66
69
|
await runDev({
|
|
67
|
-
contentDir: path.resolve(process.cwd(), dir),
|
|
70
|
+
contentDir: canonicalPath(path.resolve(process.cwd(), dir)),
|
|
68
71
|
packageRoot,
|
|
69
72
|
port: opts.port,
|
|
70
73
|
verbose: Boolean(opts.verbose),
|
|
@@ -86,7 +89,7 @@ program
|
|
|
86
89
|
.option('-k, --key <key>', 'build authorization key (or set WRITEDOCS_API_KEY)')
|
|
87
90
|
.option('--verbose', "also print Astro's and Vite's own output")
|
|
88
91
|
.action(async (dir, opts) => {
|
|
89
|
-
const contentDir = path.resolve(process.cwd(), dir);
|
|
92
|
+
const contentDir = canonicalPath(path.resolve(process.cwd(), dir));
|
|
90
93
|
// Project-scoped, not global: a .env sitting next to this project's own
|
|
91
94
|
// writedocs.json (WRITEDOCS_API_KEY) is picked up automatically, so
|
|
92
95
|
// build doesn't need it exported by hand every
|
|
@@ -106,7 +109,7 @@ program
|
|
|
106
109
|
.argument('[dir]', 'content directory (contains writedocs.json)', '.')
|
|
107
110
|
.option('--config-only', 'check writedocs.json only, not the pages')
|
|
108
111
|
.action(async (dir, options) => {
|
|
109
|
-
const contentDir = path.resolve(process.cwd(), dir);
|
|
112
|
+
const contentDir = canonicalPath(path.resolve(process.cwd(), dir));
|
|
110
113
|
const configPath = path.join(contentDir, 'writedocs.json');
|
|
111
114
|
if (!fs.existsSync(configPath)) {
|
|
112
115
|
// Mesma mensagem que loadDocsConfig() ja da pro mesmo caso.
|
|
@@ -192,7 +195,7 @@ program
|
|
|
192
195
|
.description('Check every internal link - pages, anchors and files - in the pages and writedocs.json')
|
|
193
196
|
.argument('[dir]', 'content directory (contains writedocs.json)', '.')
|
|
194
197
|
.action(async (dir) => {
|
|
195
|
-
const contentDir = path.resolve(process.cwd(), dir);
|
|
198
|
+
const contentDir = canonicalPath(path.resolve(process.cwd(), dir));
|
|
196
199
|
const { preflightCheck } = await import('../src/cli/preflight.js');
|
|
197
200
|
preflightCheck(contentDir);
|
|
198
201
|
const configText = readConfigText(contentDir);
|
|
@@ -227,7 +230,7 @@ program
|
|
|
227
230
|
.description('Check accessibility - color contrast, image alt text, headings, link text')
|
|
228
231
|
.argument('[dir]', 'content directory (contains writedocs.json)', '.')
|
|
229
232
|
.action(async (dir) => {
|
|
230
|
-
const contentDir = path.resolve(process.cwd(), dir);
|
|
233
|
+
const contentDir = canonicalPath(path.resolve(process.cwd(), dir));
|
|
231
234
|
const { preflightCheck } = await import('../src/cli/preflight.js');
|
|
232
235
|
preflightCheck(contentDir);
|
|
233
236
|
const configText = readConfigText(contentDir);
|
|
@@ -276,7 +279,7 @@ program
|
|
|
276
279
|
const { runConvert } = await import('../src/cli/convert.js');
|
|
277
280
|
await runConvert({
|
|
278
281
|
source: legacy ? 'writedocs' : 'mintlify',
|
|
279
|
-
contentDir: path.resolve(process.cwd(), dir),
|
|
282
|
+
contentDir: canonicalPath(path.resolve(process.cwd(), dir)),
|
|
280
283
|
force: Boolean(options.force),
|
|
281
284
|
dryRun: Boolean(options.dryRun),
|
|
282
285
|
});
|
package/package.json
CHANGED
package/src/cli/build.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { canonicalPath } from '../lib/canonical-path.js';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { runAstro } from './run-astro.js';
|
|
@@ -6,12 +7,16 @@ import { preflightCheck, sameDriveCheck, writableInstallCheck } from './prefligh
|
|
|
6
7
|
import { generateApiPages } from './generate-api-pages.js';
|
|
7
8
|
import { writeRedirectsFile } from './write-redirects-file.js';
|
|
8
9
|
import { writeMcpFiles } from './write-mcp-files.js';
|
|
10
|
+
import { pagesWithoutStyles } from './check-built-styles.js';
|
|
9
11
|
import { writedocsBuildStagingDir } from '../lib/writedocs-temp-dir.js';
|
|
10
12
|
import { log, step, plural, duration, displayPath, formatProblems, color, CliExit } from './output.js';
|
|
11
13
|
import { describeError, builtRoute, authorWarning, verboseLine } from './astro-output.js';
|
|
12
14
|
import { reportApiPages } from './api-pages-output.js';
|
|
13
15
|
|
|
14
16
|
export async function runBuild({ contentDir, packageRoot, verbose = false }) {
|
|
17
|
+
// One spelling of each folder throughout - see lib/canonical-path.js.
|
|
18
|
+
contentDir = canonicalPath(contentDir);
|
|
19
|
+
packageRoot = canonicalPath(packageRoot);
|
|
15
20
|
const started = Date.now();
|
|
16
21
|
preflightCheck(contentDir);
|
|
17
22
|
writableInstallCheck(packageRoot);
|
|
@@ -108,6 +113,16 @@ export async function runBuild({ contentDir, packageRoot, verbose = false }) {
|
|
|
108
113
|
fs.rmSync(distDir, { recursive: true, force: true });
|
|
109
114
|
fs.cpSync(stagingDir, distDir, { recursive: true });
|
|
110
115
|
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
116
|
+
// Every page writedocs renders links the site's stylesheet. Builds have
|
|
117
|
+
// come out without it on every page while reporting success (the package
|
|
118
|
+
// folder spelled `c:`, fixed in lib/canonical-path.js) - so it's checked
|
|
119
|
+
// here, and a build like that stops instead of being deployed unstyled.
|
|
120
|
+
const unstyled = pagesWithoutStyles(distDir);
|
|
121
|
+
if (unstyled.length) {
|
|
122
|
+
log.error(`The build produced ${plural(unstyled.length, 'page')} without the site's styles - for example ${unstyled[0]}.`);
|
|
123
|
+
log.detail(color.dim('This is a fault in the build, not in your pages. Build again; if it happens again, report it with the output of --verbose.'));
|
|
124
|
+
throw new CliExit(1);
|
|
125
|
+
}
|
|
111
126
|
// Astro's own writedocs.json `redirects` + the automatic "/" redirect only
|
|
112
127
|
// ever produce client-side meta-refresh pages (see write-redirects-file.js)
|
|
113
128
|
// - this turns those into real instant edge redirects on hosts that read
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// After a build: the pages writedocs rendered that came out without the
|
|
2
|
+
// site's styles. Every page BaseLayout.astro renders carries
|
|
3
|
+
// <meta name="generator" content="writedocs"> and links its stylesheet
|
|
4
|
+
// (/_astro/*.css), or inlines it in a <style>. Other HTML in dist/ - a
|
|
5
|
+
// redirect page, a file from public/ - isn't writedocs' to check.
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
9
|
+
const MARKER = /<meta name="generator" content="writedocs"/;
|
|
10
|
+
const STYLES = /<link[^>]+rel="stylesheet"[^>]+href="\/_astro\/[^"]+\.css"|<link[^>]+href="\/_astro\/[^"]+\.css"[^>]+rel="stylesheet"|<style[\s>]/;
|
|
11
|
+
|
|
12
|
+
function htmlFiles(dir) {
|
|
13
|
+
const out = [];
|
|
14
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
15
|
+
const full = path.join(dir, entry.name);
|
|
16
|
+
if (entry.isDirectory()) {
|
|
17
|
+
if (entry.name !== '_astro' && entry.name !== 'pagefind') out.push(...htmlFiles(full));
|
|
18
|
+
} else if (entry.name.endsWith('.html')) out.push(full);
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Paths (relative to `distDir`, with forward slashes) of writedocs pages
|
|
24
|
+
* with no stylesheet. */
|
|
25
|
+
export function pagesWithoutStyles(distDir) {
|
|
26
|
+
return htmlFiles(distDir)
|
|
27
|
+
.filter((file) => {
|
|
28
|
+
const html = fs.readFileSync(file, 'utf8');
|
|
29
|
+
return MARKER.test(html) && !STYLES.test(html);
|
|
30
|
+
})
|
|
31
|
+
.map((file) => path.relative(distDir, file).split(path.sep).join('/'))
|
|
32
|
+
.sort();
|
|
33
|
+
}
|
package/src/cli/dev.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { canonicalPath } from '../lib/canonical-path.js';
|
|
1
2
|
import { runAstro } from './run-astro.js';
|
|
2
3
|
import { preflightCheck, sameDriveCheck, writableInstallCheck } from './preflight.js';
|
|
3
4
|
import { generateApiPages } from './generate-api-pages.js';
|
|
@@ -14,6 +15,9 @@ import { shouldOpenBrowser, openBrowser } from './open-browser.js';
|
|
|
14
15
|
const REPEAT_WINDOW_MS = 2000;
|
|
15
16
|
|
|
16
17
|
export async function runDev({ contentDir, packageRoot, port, verbose = false, open = true }) {
|
|
18
|
+
// One spelling of each folder throughout - see lib/canonical-path.js.
|
|
19
|
+
contentDir = canonicalPath(contentDir);
|
|
20
|
+
packageRoot = canonicalPath(packageRoot);
|
|
17
21
|
const started = Date.now();
|
|
18
22
|
preflightCheck(contentDir);
|
|
19
23
|
writableInstallCheck(packageRoot);
|
package/src/cli/run-astro.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
+
import { canonicalPath } from '../lib/canonical-path.js';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
|
|
5
6
|
const workerPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'astro-worker.js');
|
|
@@ -21,6 +22,9 @@ const workerPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'astr
|
|
|
21
22
|
* Returns { child, exited } - `exited` resolves with the exit code.
|
|
22
23
|
*/
|
|
23
24
|
export function runAstro(command, { packageRoot, contentDir, port, onEvent }) {
|
|
25
|
+
// One spelling of each folder for Astro and Vite - see canonical-path.js.
|
|
26
|
+
packageRoot = canonicalPath(packageRoot);
|
|
27
|
+
contentDir = canonicalPath(contentDir);
|
|
24
28
|
const child = spawn(process.execPath, [workerPath, command], {
|
|
25
29
|
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
|
26
30
|
// Explicit, not inherited: astro.config.mjs's own `outDir` lives
|
|
@@ -42,7 +42,6 @@ const first = options[0];
|
|
|
42
42
|
type="button"
|
|
43
43
|
class="wd-dropdown-trigger wd-api-lang-trigger"
|
|
44
44
|
data-role={`${rolePrefix}-trigger`}
|
|
45
|
-
aria-haspopup="true"
|
|
46
45
|
aria-expanded="false"
|
|
47
46
|
aria-label={ariaLabel}
|
|
48
47
|
>
|
|
@@ -52,7 +51,7 @@ const first = options[0];
|
|
|
52
51
|
<path d="M2 3.5L5 6.5L8 3.5" stroke="currentColor" stroke-width="1.4" fill="none" stroke-linecap="round" stroke-linejoin="round" />
|
|
53
52
|
</svg>
|
|
54
53
|
</button>
|
|
55
|
-
<div class="wd-dropdown-menu wd-api-lang-menu"
|
|
54
|
+
<div class="wd-dropdown-menu wd-api-lang-menu">
|
|
56
55
|
<div class="wd-dropdown-menu-panel wd-api-lang-menu-panel">
|
|
57
56
|
{options.map((o, i) => (
|
|
58
57
|
<button
|
|
@@ -23,18 +23,26 @@
|
|
|
23
23
|
// (which need an absolute URL - from writedocs.json's `domain` when it's
|
|
24
24
|
// set, else from the page's own address in the browser; see
|
|
25
25
|
// enabledAssistants below).
|
|
26
|
-
// Without `copy`, the
|
|
27
|
-
//
|
|
26
|
+
// Without `copy`, the options get a labeled "Page options" trigger of their
|
|
27
|
+
// own - or, when there's just one, that option is the button itself ("Open
|
|
28
|
+
// in Claude"). With only `copy`, the primary button stands alone.
|
|
29
|
+
//
|
|
30
|
+
// The last three options connect AI tools to the site's MCP server (/mcp -
|
|
31
|
+
// see docs/dev/docs/mcp.mdx): copy its address, or add it to Cursor or VS
|
|
32
|
+
// Code through their install links (lib/mcp-links.js). [...slug].astro
|
|
33
|
+
// leaves them out when writedocs.json turns `mcp` off.
|
|
28
34
|
import { Icon } from 'astro-icon/components';
|
|
29
35
|
import type { ContextMenuOption } from '../lib/config';
|
|
36
|
+
import { mcpServerUrl, cursorInstallLink, vscodeInstallLink } from '../lib/mcp-links.js';
|
|
30
37
|
|
|
31
38
|
interface Props {
|
|
32
39
|
currentPath: string; // e.g. "/" or "/guides/foo/" - see hrefForSlug() in [...slug].astro
|
|
33
40
|
siteUrl: string | null;
|
|
41
|
+
siteName: string;
|
|
34
42
|
options: ContextMenuOption[];
|
|
35
43
|
}
|
|
36
44
|
|
|
37
|
-
const { currentPath, siteUrl, options } = Astro.props as Props;
|
|
45
|
+
const { currentPath, siteUrl, siteName, options } = Astro.props as Props;
|
|
38
46
|
|
|
39
47
|
// The [...slug].md.ts route this page is also served at - same
|
|
40
48
|
// normalizeEntryId()-driven convention that file uses to build its own
|
|
@@ -42,6 +50,7 @@ const { currentPath, siteUrl, options } = Astro.props as Props;
|
|
|
42
50
|
// slug with a literal ".md" appended instead of the trailing slash).
|
|
43
51
|
const mdPath = currentPath === '/' ? '/index.md' : `${currentPath.replace(/\/$/, '')}.md`;
|
|
44
52
|
const mdAbsoluteUrl = siteUrl ? `${siteUrl}${mdPath}` : null;
|
|
53
|
+
const mcpUrl = siteUrl ? mcpServerUrl(siteUrl) : null;
|
|
45
54
|
|
|
46
55
|
// Well-known query-string conventions for starting a new conversation
|
|
47
56
|
// pre-seeded with a prompt (not an official API for any of the three -
|
|
@@ -60,29 +69,85 @@ const assistants = [
|
|
|
60
69
|
{ id: 'claude' as const, label: 'Claude', icon: 'simple-icons:claude', base: 'https://claude.ai/new?q=', home: 'https://claude.ai/new' },
|
|
61
70
|
{ id: 'perplexity' as const, label: 'Perplexity', icon: 'simple-icons:perplexity', base: 'https://www.perplexity.ai/search?q=', home: 'https://www.perplexity.ai/' },
|
|
62
71
|
];
|
|
72
|
+
const editors = [
|
|
73
|
+
{ id: 'cursor' as const, label: 'Cursor', icon: 'simple-icons:cursor', link: cursorInstallLink, home: 'https://docs.cursor.com/context/mcp' },
|
|
74
|
+
{ id: 'vscode' as const, label: 'VS Code', icon: 'simple-icons:visualstudiocode', link: vscodeInstallLink, home: 'https://code.visualstudio.com/docs/copilot/chat/mcp-servers' },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
// Every link needs an absolute URL - the page's .md for an assistant, the
|
|
78
|
+
// MCP server for an editor. With a `domain` it's known at build time and the
|
|
79
|
+
// link is complete in the HTML. Without one, the page script ([...slug].astro)
|
|
80
|
+
// completes it from the address the page is actually served from, using the
|
|
81
|
+
// data-* attributes below; until then the link goes to the tool's own page.
|
|
82
|
+
type Item = {
|
|
83
|
+
id: ContextMenuOption;
|
|
84
|
+
title: string;
|
|
85
|
+
desc: string;
|
|
86
|
+
icon: string;
|
|
87
|
+
group: 'page' | 'mcp';
|
|
88
|
+
href?: string;
|
|
89
|
+
newTab?: boolean;
|
|
90
|
+
attrs?: Record<string, string | undefined>;
|
|
91
|
+
};
|
|
92
|
+
const items: Item[] = [];
|
|
93
|
+
if (options.includes('view')) {
|
|
94
|
+
items.push({ id: 'view', title: 'View as Markdown', desc: 'View this page as plain text', icon: 'lucide:external-link', group: 'page', href: mdPath, newTab: true });
|
|
95
|
+
}
|
|
96
|
+
for (const a of assistants.filter((a) => options.includes(a.id))) {
|
|
97
|
+
items.push({
|
|
98
|
+
id: a.id,
|
|
99
|
+
title: `Open in ${a.label}`,
|
|
100
|
+
desc: 'Ask questions about this page',
|
|
101
|
+
icon: a.icon,
|
|
102
|
+
group: 'page',
|
|
103
|
+
href: mdAbsoluteUrl ? a.base + encodeURIComponent(promptFor(mdAbsoluteUrl)) : a.home,
|
|
104
|
+
newTab: true,
|
|
105
|
+
attrs: mdAbsoluteUrl ? undefined : { 'data-ask-base': a.base, 'data-md-path': mdPath },
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (options.includes('mcp')) {
|
|
109
|
+
items.push({ id: 'mcp', title: 'Copy MCP server URL', desc: 'Connect AI tools to these docs', icon: 'simple-icons:modelcontextprotocol', group: 'mcp', attrs: { 'data-copy-mcp': mcpUrl ?? '' } });
|
|
110
|
+
}
|
|
111
|
+
for (const e of editors.filter((e) => options.includes(e.id))) {
|
|
112
|
+
items.push({
|
|
113
|
+
id: e.id,
|
|
114
|
+
title: `Connect to ${e.label}`,
|
|
115
|
+
desc: `Use these docs in ${e.label}`,
|
|
116
|
+
icon: e.icon,
|
|
117
|
+
group: 'mcp',
|
|
118
|
+
// An editor's own link scheme (cursor://, vscode:) - opens the app, not a tab.
|
|
119
|
+
href: mcpUrl ? e.link(siteName, mcpUrl) : e.home,
|
|
120
|
+
attrs: mcpUrl ? undefined : { 'data-mcp-install': e.id, 'data-mcp-name': siteName },
|
|
121
|
+
});
|
|
122
|
+
}
|
|
63
123
|
|
|
64
|
-
// The assistant needs the page's absolute URL to fetch it. With a `domain`
|
|
65
|
-
// it's known at build time and the link is complete in the HTML. Without
|
|
66
|
-
// one, the page script (initCopyPageMenu() in [...slug].astro) completes it
|
|
67
|
-
// from the address the page is actually served from - `data-ask-base` +
|
|
68
|
-
// the prompt for that URL, the same prompt promptFor() writes.
|
|
69
|
-
const enabledAssistants = assistants.filter((a) => options.includes(a.id));
|
|
70
|
-
const assistantHref = (a: (typeof assistants)[number]) => (mdAbsoluteUrl ? a.base + encodeURIComponent(promptFor(mdAbsoluteUrl)) : a.home);
|
|
71
124
|
const showCopy = options.includes('copy');
|
|
72
|
-
|
|
73
|
-
const
|
|
125
|
+
// One option and no "Copy page": that option is the button.
|
|
126
|
+
const single = !showCopy && items.length === 1 ? items[0] : null;
|
|
127
|
+
const hasDropdown = items.length > 0 && !single;
|
|
74
128
|
---
|
|
75
|
-
{(showCopy ||
|
|
76
|
-
<div class="wd-copy-page-split">
|
|
129
|
+
{(showCopy || items.length > 0) && (
|
|
130
|
+
<div class="wd-copy-page-split" data-pagefind-ignore="all">
|
|
77
131
|
{showCopy && (
|
|
78
|
-
<button type="button" class:list={['wd-copy-page-primary', { 'wd-copy-page-alone': !hasDropdown }]} data-md-path={mdPath} aria-label="Copy page as Markdown">
|
|
132
|
+
<button type="button" class:list={['wd-copy-page-primary', { 'wd-copy-page-alone': !hasDropdown }]} data-copy-page data-md-path={mdPath} aria-label="Copy page as Markdown">
|
|
79
133
|
<Icon name="lucide:copy" class="wd-copy-page-icon" />
|
|
80
134
|
<span class="wd-copy-page-primary-label">Copy page</span>
|
|
81
135
|
</button>
|
|
82
136
|
)}
|
|
137
|
+
{single && (single.href ? (
|
|
138
|
+
<a class="wd-copy-page-primary wd-copy-page-alone" href={single.href} target={single.newTab ? '_blank' : undefined} rel={single.newTab ? 'noopener' : undefined} {...single.attrs}>
|
|
139
|
+
<Icon name={single.icon} class="wd-copy-page-icon" />
|
|
140
|
+
<span class="wd-copy-page-primary-label">{single.title}</span>
|
|
141
|
+
</a>
|
|
142
|
+
) : (
|
|
143
|
+
<button type="button" class="wd-copy-page-primary wd-copy-page-alone" {...single.attrs}>
|
|
144
|
+
<Icon name={single.icon} class="wd-copy-page-icon" />
|
|
145
|
+
<span class="wd-copy-page-primary-label">{single.title}</span>
|
|
146
|
+
</button>
|
|
147
|
+
))}
|
|
83
148
|
{hasDropdown && (
|
|
84
149
|
<div class="wd-dropdown wd-copy-page">
|
|
85
|
-
<button type="button" class:list={['wd-dropdown-trigger', 'wd-copy-page-caret-trigger', { 'wd-copy-page-labeled-trigger': !showCopy }]} aria-
|
|
150
|
+
<button type="button" class:list={['wd-dropdown-trigger', 'wd-copy-page-caret-trigger', { 'wd-copy-page-labeled-trigger': !showCopy }]} aria-expanded="false" aria-label={showCopy ? 'More copy options' : 'Page options'}>
|
|
86
151
|
{!showCopy && (
|
|
87
152
|
<>
|
|
88
153
|
<Icon name="lucide:sparkles" class="wd-copy-page-icon" />
|
|
@@ -93,25 +158,29 @@ const hasDropdown = showView || enabledAssistants.length > 0;
|
|
|
93
158
|
<path d="M2 3.5L5 6.5L8 3.5" stroke="currentColor" stroke-width="1.4" fill="none" stroke-linecap="round" stroke-linejoin="round" />
|
|
94
159
|
</svg>
|
|
95
160
|
</button>
|
|
96
|
-
<div class="wd-dropdown-menu wd-copy-page-menu"
|
|
161
|
+
<div class="wd-dropdown-menu wd-copy-page-menu">
|
|
97
162
|
<div class="wd-dropdown-menu-panel wd-copy-page-menu-panel">
|
|
98
|
-
{
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
163
|
+
{items.map((item, i) => (
|
|
164
|
+
<>
|
|
165
|
+
{i > 0 && item.group !== items[i - 1].group && <hr class="wd-copy-page-divider" />}
|
|
166
|
+
{item.href ? (
|
|
167
|
+
<a class="wd-copy-page-item" href={item.href} target={item.newTab ? '_blank' : undefined} rel={item.newTab ? 'noopener' : undefined} {...item.attrs}>
|
|
168
|
+
<Icon name={item.icon} class="wd-copy-page-icon" />
|
|
169
|
+
<span class="wd-copy-page-item-text">
|
|
170
|
+
<span class="wd-copy-page-item-title">{item.title}</span>
|
|
171
|
+
<span class="wd-copy-page-item-desc">{item.desc}</span>
|
|
172
|
+
</span>
|
|
173
|
+
</a>
|
|
174
|
+
) : (
|
|
175
|
+
<button type="button" class="wd-copy-page-item" {...item.attrs}>
|
|
176
|
+
<Icon name={item.icon} class="wd-copy-page-icon" />
|
|
177
|
+
<span class="wd-copy-page-item-text">
|
|
178
|
+
<span class="wd-copy-page-item-title">{item.title}</span>
|
|
179
|
+
<span class="wd-copy-page-item-desc">{item.desc}</span>
|
|
180
|
+
</span>
|
|
181
|
+
</button>
|
|
182
|
+
)}
|
|
183
|
+
</>
|
|
115
184
|
))}
|
|
116
185
|
</div>
|
|
117
186
|
</div>
|
|
@@ -201,6 +270,16 @@ const hasDropdown = showView || enabledAssistants.length > 0;
|
|
|
201
270
|
.wd-copy-page-primary.wd-copy-page-alone {
|
|
202
271
|
border-radius: 0.35rem;
|
|
203
272
|
}
|
|
273
|
+
/* The single-option button can be a link ("Open in Claude"). */
|
|
274
|
+
a.wd-copy-page-primary {
|
|
275
|
+
text-decoration: none;
|
|
276
|
+
}
|
|
277
|
+
/* Between the page's options and the MCP server's. */
|
|
278
|
+
.wd-copy-page-divider {
|
|
279
|
+
margin: 0.3rem 0.4rem;
|
|
280
|
+
border: none;
|
|
281
|
+
border-top: 1px solid var(--wd-border);
|
|
282
|
+
}
|
|
204
283
|
.wd-copy-page-caret-trigger.wd-copy-page-labeled-trigger {
|
|
205
284
|
gap: 0.35rem;
|
|
206
285
|
padding: 0.35rem 0.65rem;
|
|
@@ -259,6 +338,12 @@ const hasDropdown = showView || enabledAssistants.length > 0;
|
|
|
259
338
|
text-align: left;
|
|
260
339
|
cursor: pointer;
|
|
261
340
|
}
|
|
341
|
+
/* A button option ("Copy MCP server URL") fills the row like the links
|
|
342
|
+
do - a button sizes to its content otherwise. */
|
|
343
|
+
button.wd-copy-page-item {
|
|
344
|
+
width: 100%;
|
|
345
|
+
box-sizing: border-box;
|
|
346
|
+
}
|
|
262
347
|
.wd-copy-page-item:hover {
|
|
263
348
|
background: var(--wd-surface);
|
|
264
349
|
}
|
|
@@ -415,6 +415,8 @@ const fontExtraCss = [fontFaceCss, fontWeightCss].filter(Boolean).join('\n');
|
|
|
415
415
|
<head>
|
|
416
416
|
<meta charset="UTF-8" />
|
|
417
417
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
418
|
+
{/* Marks the pages writedocs renders - the build checks each has the site's styles (cli/check-built-styles.js). */}
|
|
419
|
+
<meta name="generator" content="writedocs" />
|
|
418
420
|
<title>{title} · {config.name}</title>
|
|
419
421
|
<ClientRouter />
|
|
420
422
|
{effectiveDescription && <meta name="description" content={effectiveDescription} />}
|