@writedocs/generator 0.1.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/LICENSE +15 -0
- package/README.md +17 -0
- package/astro.config.mjs +419 -0
- package/bin/writedocs.js +73 -0
- package/package.json +79 -0
- package/src/assets/wd_watermark.png +0 -0
- package/src/assets/wd_watermark_dark.png +0 -0
- package/src/cli/build-auth.js +53 -0
- package/src/cli/build.js +40 -0
- package/src/cli/dev.js +12 -0
- package/src/cli/generate-api-pages.js +359 -0
- package/src/cli/init.js +81 -0
- package/src/cli/preflight.js +40 -0
- package/src/cli/run-astro.js +57 -0
- package/src/cli/run-pagefind.js +66 -0
- package/src/cli/write-redirects-file.js +80 -0
- package/src/components/Accordion.astro +164 -0
- package/src/components/AccordionGroup.astro +40 -0
- package/src/components/ApiLangSelect.astro +168 -0
- package/src/components/ApiPlayground.astro +281 -0
- package/src/components/ApiReferencePanel.astro +1754 -0
- package/src/components/ApiSchemaField.astro +54 -0
- package/src/components/AppIcon.astro +32 -0
- package/src/components/Badge.astro +128 -0
- package/src/components/Callout.astro +168 -0
- package/src/components/Card.astro +136 -0
- package/src/components/CardGroup.astro +20 -0
- package/src/components/CodeGroup.astro +184 -0
- package/src/components/CopyPageMenu.astro +246 -0
- package/src/components/Danger.astro +12 -0
- package/src/components/Expandable.astro +126 -0
- package/src/components/Frame.astro +102 -0
- package/src/components/Hint.astro +99 -0
- package/src/components/Icon.astro +70 -0
- package/src/components/Image.astro +147 -0
- package/src/components/Info.astro +12 -0
- package/src/components/Note.astro +12 -0
- package/src/components/Parameter.astro +119 -0
- package/src/components/RequestExample.astro +33 -0
- package/src/components/ResponseExample.astro +19 -0
- package/src/components/Searchbar.astro +117 -0
- package/src/components/Step.astro +10 -0
- package/src/components/Steps.astro +32 -0
- package/src/components/Tab.astro +9 -0
- package/src/components/Tabs.astro +52 -0
- package/src/components/Tip.astro +12 -0
- package/src/components/Video.astro +135 -0
- package/src/components/Warning.astro +12 -0
- package/src/components/index.ts +48 -0
- package/src/content.config.ts +223 -0
- package/src/layout/BaseLayout.astro +750 -0
- package/src/layout/components/AnalyticsScripts.astro +77 -0
- package/src/layout/components/AskAiWidget.astro +37 -0
- package/src/layout/components/Breadcrumbs.astro +97 -0
- package/src/layout/components/ImageZoom.astro +19 -0
- package/src/layout/components/MobileMenu.astro +200 -0
- package/src/layout/components/NavTree.astro +351 -0
- package/src/layout/components/SearchModal.astro +42 -0
- package/src/layout/components/Sidebar.astro +122 -0
- package/src/layout/components/SiteFooter.astro +85 -0
- package/src/layout/components/TableOfContents.astro +117 -0
- package/src/layout/components/TopBar.astro +311 -0
- package/src/layout/styles/banner.css +44 -0
- package/src/layout/styles/base.css +234 -0
- package/src/layout/styles/dropdown.css +133 -0
- package/src/layout/styles/footer.css +108 -0
- package/src/layout/styles/image-zoom.css +50 -0
- package/src/layout/styles/mobile-menu.css +258 -0
- package/src/layout/styles/search-modal.css +122 -0
- package/src/layout/styles/topbar.css +437 -0
- package/src/lib/config.ts +2131 -0
- package/src/lib/mdx-auto-hydrate.js +70 -0
- package/src/lib/mdx-inject-builtins.js +87 -0
- package/src/lib/mdx-substitute-variables.js +66 -0
- package/src/lib/mdx-title-anchor-ids.js +84 -0
- package/src/lib/mermaid-rehype.js +72 -0
- package/src/lib/openapi-render.ts +479 -0
- package/src/lib/shiki-code-block.js +102 -0
- package/src/lib/shiki-copy-button.js +45 -0
- package/src/lib/styles-asset-integration.js +210 -0
- package/src/lib/writedocs-temp-dir.js +93 -0
- package/src/pages/404.astro +62 -0
- package/src/pages/[...slug].astro +1270 -0
- package/src/pages/[...slug].md.ts +78 -0
- package/src/pages/llms-full.txt.ts +71 -0
- package/src/pages/llms.txt.ts +141 -0
- package/src/scripts/banner.ts +20 -0
- package/src/scripts/dropdowns.ts +61 -0
- package/src/scripts/image-zoom.ts +66 -0
- package/src/scripts/mobile-menu.ts +55 -0
- package/src/scripts/search.ts +155 -0
- package/src/scripts/sidebar-scroll.ts +65 -0
- package/src/scripts/theme-toggle.ts +35 -0
- package/src/scripts/topbar-offset.ts +141 -0
- package/src/styles/global.css +18 -0
|
@@ -0,0 +1,1270 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getCollection, render, type CollectionEntry } from "astro:content";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
loadDocsConfig,
|
|
7
|
+
resolveSections,
|
|
8
|
+
resolveGlobalDropdowns,
|
|
9
|
+
findSectionIndexForSlug,
|
|
10
|
+
buildNavTree,
|
|
11
|
+
ancestorGroupsForSlug,
|
|
12
|
+
buildSelectors,
|
|
13
|
+
buildGlobalDropdowns,
|
|
14
|
+
fileIdForEntry,
|
|
15
|
+
normalizeEntryId,
|
|
16
|
+
flattenNav,
|
|
17
|
+
firstSlugOfNavigation,
|
|
18
|
+
mergeSeo,
|
|
19
|
+
resolveSiteUrl,
|
|
20
|
+
findAllPages,
|
|
21
|
+
} from "../lib/config";
|
|
22
|
+
import { writedocsTempDir } from "../lib/writedocs-temp-dir.js";
|
|
23
|
+
import BaseLayout from "../layout/BaseLayout.astro";
|
|
24
|
+
import Sidebar from "../layout/components/Sidebar.astro";
|
|
25
|
+
import TableOfContents from "../layout/components/TableOfContents.astro";
|
|
26
|
+
import Breadcrumbs from "../layout/components/Breadcrumbs.astro";
|
|
27
|
+
import CopyPageMenu from "../components/CopyPageMenu.astro";
|
|
28
|
+
import Callout from "../components/Callout.astro";
|
|
29
|
+
import Note from "../components/Note.astro";
|
|
30
|
+
import Info from "../components/Info.astro";
|
|
31
|
+
import Tip from "../components/Tip.astro";
|
|
32
|
+
import Warning from "../components/Warning.astro";
|
|
33
|
+
import Danger from "../components/Danger.astro";
|
|
34
|
+
import Card from "../components/Card.astro";
|
|
35
|
+
import CardGroup from "../components/CardGroup.astro";
|
|
36
|
+
import Tabs from "../components/Tabs.astro";
|
|
37
|
+
import Tab from "../components/Tab.astro";
|
|
38
|
+
import CodeGroup from "../components/CodeGroup.astro";
|
|
39
|
+
import Accordion from "../components/Accordion.astro";
|
|
40
|
+
import AccordionGroup from "../components/AccordionGroup.astro";
|
|
41
|
+
import Steps from "../components/Steps.astro";
|
|
42
|
+
import Step from "../components/Step.astro";
|
|
43
|
+
import Hint from "../components/Hint.astro";
|
|
44
|
+
import Image from "../components/Image.astro";
|
|
45
|
+
import Frame from "../components/Frame.astro";
|
|
46
|
+
import Video from "../components/Video.astro";
|
|
47
|
+
import Parameter from "../components/Parameter.astro";
|
|
48
|
+
import Expandable from "../components/Expandable.astro";
|
|
49
|
+
import Searchbar from "../components/Searchbar.astro";
|
|
50
|
+
import Badge from "../components/Badge.astro";
|
|
51
|
+
import Icon from "../components/Icon.astro";
|
|
52
|
+
import RequestExample from "../components/RequestExample.astro";
|
|
53
|
+
import ResponseExample from "../components/ResponseExample.astro";
|
|
54
|
+
import ApiPlayground from "../components/ApiPlayground.astro";
|
|
55
|
+
import ApiReferencePanel from "../components/ApiReferencePanel.astro";
|
|
56
|
+
|
|
57
|
+
// A page is hand-written (the `pages` collection, sourced from anywhere
|
|
58
|
+
// in the project - docs/ has no special status, see findAllPages() in
|
|
59
|
+
// lib/config.ts) or an auto-generated OpenAPI stub (the `generatedDocs`
|
|
60
|
+
// collection, sourced from writedocsTempDir()'s generated-docs/
|
|
61
|
+
// directory - see content.config.ts / generate-api-pages.js) - kept as two separate
|
|
62
|
+
// collections rather than one, since Astro's loader API has no supported
|
|
63
|
+
// way to point a single collection's glob() at more than one base
|
|
64
|
+
// directory without one's own cleanup pass deleting the other's entries
|
|
65
|
+
// (see content.config.ts's own comment on this). Both share the exact
|
|
66
|
+
// same schema, so every consumer below just treats them as one combined
|
|
67
|
+
// list.
|
|
68
|
+
//
|
|
69
|
+
// The two `getCollection()` calls below are inlined at each of their two
|
|
70
|
+
// call sites (getStaticPaths, and the component body further down)
|
|
71
|
+
// rather than factored into one shared top-level helper: Astro's
|
|
72
|
+
// getStaticPaths extraction (a separate bundling pass from the rest of
|
|
73
|
+
// the component - see the pathDescription comment further down for
|
|
74
|
+
// another instance of this) was observed to silently drop a top-level
|
|
75
|
+
// function declaration referenced from inside getStaticPaths, producing
|
|
76
|
+
// a raw "getAllDocsEntries is not defined" ReferenceError at build time
|
|
77
|
+
// instead of ever running. Keeping both call sites self-contained avoids
|
|
78
|
+
// relying on whatever static-analysis pass decides what counts as
|
|
79
|
+
// "used" by the extracted chunk.
|
|
80
|
+
//
|
|
81
|
+
// Each site also only calls getCollection('generatedDocs') when there's
|
|
82
|
+
// actually something in it (most sites have no OpenAPI groups - see
|
|
83
|
+
// generate-api-pages.js), rather than unconditionally calling it and
|
|
84
|
+
// letting content.config.ts's skipIfMissing() loader no-op internally:
|
|
85
|
+
// astro:content's own getCollection() logs "The collection ... does not
|
|
86
|
+
// exist or is empty" to the console whenever a collection ends up with
|
|
87
|
+
// zero registered entries, regardless of *why* it's empty - the data
|
|
88
|
+
// store has no supported way to mark a collection as "intentionally
|
|
89
|
+
// empty" short of writing at least one entry to it. Not calling
|
|
90
|
+
// getCollection() at all for an empty one sidesteps that warning at the
|
|
91
|
+
// source instead of trying to suppress it after the fact.
|
|
92
|
+
type DocsEntry = CollectionEntry<"pages"> | CollectionEntry<"generatedDocs">;
|
|
93
|
+
|
|
94
|
+
export async function getStaticPaths() {
|
|
95
|
+
const contentDir = process.env.WRITEDOCS_CONTENT_DIR || process.cwd();
|
|
96
|
+
const packageRoot = process.env.WRITEDOCS_PACKAGE_ROOT || process.cwd();
|
|
97
|
+
const config = loadDocsConfig(contentDir);
|
|
98
|
+
const sections = resolveSections(config.navigation);
|
|
99
|
+
const hasGeneratedDocs = fs.existsSync(path.join(writedocsTempDir(contentDir), "generated-docs"));
|
|
100
|
+
const hasPages = findAllPages(contentDir).length > 0;
|
|
101
|
+
const [pagesEntries, generatedDocsEntries] = await Promise.all([
|
|
102
|
+
hasPages ? getCollection("pages") : Promise.resolve([]),
|
|
103
|
+
hasGeneratedDocs ? getCollection("generatedDocs") : Promise.resolve([]),
|
|
104
|
+
]);
|
|
105
|
+
const entries: DocsEntry[] = [...pagesEntries, ...generatedDocsEntries];
|
|
106
|
+
// writedocs.json always references a page by its file id; `entry.id` is
|
|
107
|
+
// Astro's own resolved slug for it (the file-path default, or a
|
|
108
|
+
// frontmatter `slug` override it recognizes natively - see
|
|
109
|
+
// fileIdForEntry() in lib/config.ts) and is what the page is actually
|
|
110
|
+
// routed at. Keyed by file id so every navEntry.slug below finds its
|
|
111
|
+
// entry regardless of which one is in play.
|
|
112
|
+
const entryByFileId = new Map(entries.map((e) => [fileIdForEntry(contentDir, packageRoot, e), e]));
|
|
113
|
+
|
|
114
|
+
// "/" is only produced below when some page's *resolved* slug
|
|
115
|
+
// (entry.id) is literally "index" - the conventional home page (an
|
|
116
|
+
// index.mdx at the project root; a root-level index.mdx keeps file id
|
|
117
|
+
// "index" verbatim, since Astro's own default id computation only
|
|
118
|
+
// strips a trailing `/index` *segment*, which a single-segment root
|
|
119
|
+
// file doesn't have - see fileIdForEntry()'s own comment on this in
|
|
120
|
+
// lib/config.ts), or any page overriding its own slug to "index"/"/".
|
|
121
|
+
// Most navigations don't have such a page - Astro then warns "no
|
|
122
|
+
// matching static path was found for /" on every request. Rather than
|
|
123
|
+
// requiring every writedocs.json to name a page "index", synthesize a
|
|
124
|
+
// redirect-only route for "/" pointing at the site's first real page
|
|
125
|
+
// whenever nothing already claims root.
|
|
126
|
+
// Checked against every entry, not just nav-referenced ones - a hidden
|
|
127
|
+
// page (see hiddenRoutes below) sitting at "index" claims root exactly
|
|
128
|
+
// as well as a nav-listed one would, and needs to suppress the synthetic
|
|
129
|
+
// redirect below the same way (two routes both wanting `params.slug:
|
|
130
|
+
// undefined` is a build error, not a silent pick-one).
|
|
131
|
+
const rootAlreadyClaimed = entries.some((e) => normalizeEntryId(e.id) === "index");
|
|
132
|
+
const firstSlug = rootAlreadyClaimed ? null : firstSlugOfNavigation(config.navigation);
|
|
133
|
+
const firstEntry = firstSlug ? entryByFileId.get(firstSlug) : undefined;
|
|
134
|
+
// Not calling the top-level hrefForSlug() here: it's only otherwise
|
|
135
|
+
// referenced from the component body, not from inside getStaticPaths,
|
|
136
|
+
// and per the describeSectionPath lesson above, closures introduced
|
|
137
|
+
// into getStaticPaths should be self-contained rather than assume a
|
|
138
|
+
// module-level binding survives Astro's bundling extraction. firstEntry
|
|
139
|
+
// can never resolve to "index" here (that's exactly what
|
|
140
|
+
// rootAlreadyClaimed rules out), so the ternary below always resolves
|
|
141
|
+
// to the non-root branch.
|
|
142
|
+
const rootRedirect = firstEntry
|
|
143
|
+
? [{ params: { slug: undefined }, props: { redirectTo: `/${normalizeEntryId(firstEntry.id)}/` } }]
|
|
144
|
+
: [];
|
|
145
|
+
|
|
146
|
+
// Tracks every file id a nav-driven route below already claims, so
|
|
147
|
+
// hiddenRoutes (further down) knows which entries are left over - any
|
|
148
|
+
// page/generatedDocs entry that exists (findAllPages() already required
|
|
149
|
+
// it to have frontmatter with a `title` - see content.config.ts's
|
|
150
|
+
// docsSchema) but isn't reachable by walking writedocs.json's navigation at
|
|
151
|
+
// all. Those still get routed here, just without a nav-derived
|
|
152
|
+
// prev/next or section - a page doesn't have to be in the sidebar to be
|
|
153
|
+
// a real, directly-linkable page (an in-progress draft, an unlisted
|
|
154
|
+
// landing page, etc.).
|
|
155
|
+
const claimedFileIds = new Set<string>();
|
|
156
|
+
|
|
157
|
+
const pageRoutes = sections.flatMap((section, sectionIndex) => {
|
|
158
|
+
const flatNav = flattenNav(section.pages);
|
|
159
|
+
return flatNav.map((navEntry, i) => {
|
|
160
|
+
const entry = entryByFileId.get(navEntry.slug);
|
|
161
|
+
if (!entry) {
|
|
162
|
+
// Inlined rather than a separate top-level helper: Astro's
|
|
163
|
+
// getStaticPaths extraction (a separate bundling pass from the
|
|
164
|
+
// rest of the component) was observed to silently drop a
|
|
165
|
+
// top-level function declaration that's only referenced inside
|
|
166
|
+
// this rarely-taken branch, producing a raw "X is not defined"
|
|
167
|
+
// ReferenceError instead of this readable message. Keeping the
|
|
168
|
+
// logic inside getStaticPaths' own closure avoids relying on
|
|
169
|
+
// whatever static-analysis pass decides what counts as "used".
|
|
170
|
+
const pathDescription =
|
|
171
|
+
section.path.length === 0
|
|
172
|
+
? "(root)"
|
|
173
|
+
: section.path
|
|
174
|
+
.map((seg) => {
|
|
175
|
+
const item = seg.items[seg.index] as Record<string, unknown>;
|
|
176
|
+
const name = (item.tab ?? item.version ?? item.language ?? item.dropdown ?? item.product) as string;
|
|
177
|
+
return `${seg.kind} "${name}"`;
|
|
178
|
+
})
|
|
179
|
+
.join(" > ");
|
|
180
|
+
throw new Error(
|
|
181
|
+
`[writedocs] writedocs.json references page "${navEntry.slug}"` +
|
|
182
|
+
` (${pathDescription})` +
|
|
183
|
+
` but no matching file was found`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
claimedFileIds.add(navEntry.slug);
|
|
187
|
+
const prev = i > 0 ? flatNav[i - 1] : null;
|
|
188
|
+
const next = i < flatNav.length - 1 ? flatNav[i + 1] : null;
|
|
189
|
+
const urlSlug = normalizeEntryId(entry.id);
|
|
190
|
+
return {
|
|
191
|
+
params: { slug: urlSlug === "index" ? undefined : urlSlug },
|
|
192
|
+
props: { entry, prev, next, sectionIndex },
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// "Hidden" pages: any file the `pages`/`generatedDocs` collections
|
|
198
|
+
// picked up that writedocs.json's navigation never references (see
|
|
199
|
+
// claimedFileIds above) still gets a real route here - just with no
|
|
200
|
+
// prev/next (nothing in nav to derive that from) and no section of its
|
|
201
|
+
// own to be "active" in. findSectionIndexForSlug's existing "0 if this
|
|
202
|
+
// slug isn't in any section" fallback (already used nowhere else in
|
|
203
|
+
// this file, but built for exactly this) picks which section's
|
|
204
|
+
// sidebar/selectors a hidden page borrows - arbitrary, but no worse
|
|
205
|
+
// than the alternative of hidden pages having no chrome at all.
|
|
206
|
+
const hiddenRoutes = entries
|
|
207
|
+
.filter((entry) => !claimedFileIds.has(fileIdForEntry(contentDir, packageRoot, entry)))
|
|
208
|
+
.map((entry) => {
|
|
209
|
+
const urlSlug = normalizeEntryId(entry.id);
|
|
210
|
+
return {
|
|
211
|
+
params: { slug: urlSlug === "index" ? undefined : urlSlug },
|
|
212
|
+
props: {
|
|
213
|
+
entry,
|
|
214
|
+
prev: null,
|
|
215
|
+
next: null,
|
|
216
|
+
sectionIndex: findSectionIndexForSlug(sections, fileIdForEntry(contentDir, packageRoot, entry)),
|
|
217
|
+
isHidden: true,
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
return [...rootRedirect, ...pageRoutes, ...hiddenRoutes];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
interface Props {
|
|
226
|
+
redirectTo?: string;
|
|
227
|
+
entry?: DocsEntry;
|
|
228
|
+
prev?: { slug: string; group: string | null } | null;
|
|
229
|
+
next?: { slug: string; group: string | null } | null;
|
|
230
|
+
sectionIndex?: number;
|
|
231
|
+
// Set by hiddenRoutes in getStaticPaths above - a page with no place in
|
|
232
|
+
// writedocs.json's navigation has no sidebar section of its own to show, so
|
|
233
|
+
// its default mode drops the sidebar entirely (see pageMode below)
|
|
234
|
+
// rather than borrowing findSectionIndexForSlug's arbitrary fallback
|
|
235
|
+
// section's sidebar contents, which would misleadingly suggest the page
|
|
236
|
+
// belongs there.
|
|
237
|
+
isHidden?: boolean;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const rawProps = Astro.props as Props;
|
|
241
|
+
if (rawProps.redirectTo) {
|
|
242
|
+
return Astro.redirect(rawProps.redirectTo);
|
|
243
|
+
}
|
|
244
|
+
const {
|
|
245
|
+
entry,
|
|
246
|
+
prev = null,
|
|
247
|
+
next = null,
|
|
248
|
+
sectionIndex = 0,
|
|
249
|
+
isHidden = false,
|
|
250
|
+
} = rawProps as Required<Props>;
|
|
251
|
+
|
|
252
|
+
const contentDir = process.env.WRITEDOCS_CONTENT_DIR || process.cwd();
|
|
253
|
+
const packageRoot = process.env.WRITEDOCS_PACKAGE_ROOT || process.cwd();
|
|
254
|
+
const config = loadDocsConfig(contentDir);
|
|
255
|
+
const sections = resolveSections(config.navigation);
|
|
256
|
+
// Falls back to an empty stand-in section rather than crashing on
|
|
257
|
+
// `.pages` below - only reachable for a hidden page (see hiddenRoutes in
|
|
258
|
+
// getStaticPaths above) on a site whose writedocs.json has no navigation
|
|
259
|
+
// sections at all, which previously could never produce a route to
|
|
260
|
+
// render in the first place.
|
|
261
|
+
const activeSection = sections[sectionIndex] ?? { pages: [], path: [] };
|
|
262
|
+
|
|
263
|
+
const { Content, headings } = await render(entry);
|
|
264
|
+
const tocHeadings = headings.filter((h) => h.depth === 2 || h.depth === 3);
|
|
265
|
+
|
|
266
|
+
const hasGeneratedDocs = fs.existsSync(path.join(writedocsTempDir(contentDir), "generated-docs"));
|
|
267
|
+
const hasPages = findAllPages(contentDir).length > 0;
|
|
268
|
+
const [pagesEntries, generatedDocsEntries] = await Promise.all([
|
|
269
|
+
hasPages ? getCollection("pages") : Promise.resolve([]),
|
|
270
|
+
hasGeneratedDocs ? getCollection("generatedDocs") : Promise.resolve([]),
|
|
271
|
+
]);
|
|
272
|
+
const entries: DocsEntry[] = [...pagesEntries, ...generatedDocsEntries];
|
|
273
|
+
// Two maps keyed by file id (not `entry.id` - see fileIdForEntry() in
|
|
274
|
+
// lib/config.ts for why those can differ once a page overrides its own
|
|
275
|
+
// slug): titles for prev/next and sidebar labels, and the entries
|
|
276
|
+
// themselves so hrefForSlug can resolve a writedocs.json file-id reference to
|
|
277
|
+
// wherever that page actually got routed.
|
|
278
|
+
const titleByFileId = new Map<string, string>();
|
|
279
|
+
const entryByFileId = new Map<string, DocsEntry>();
|
|
280
|
+
// The HTTP method badge NavTree.astro shows next to an API operation
|
|
281
|
+
// page in the sidebar - read straight off the page's own `openapi:
|
|
282
|
+
// "METHOD /path"` frontmatter rather than the resolved operation JSON,
|
|
283
|
+
// since the method itself is right there in that string already and
|
|
284
|
+
// doesn't need a spec lookup.
|
|
285
|
+
const methodByFileId = new Map<string, string>();
|
|
286
|
+
for (const e of entries) {
|
|
287
|
+
const fileId = fileIdForEntry(contentDir, packageRoot, e);
|
|
288
|
+
titleByFileId.set(fileId, e.data.title);
|
|
289
|
+
entryByFileId.set(fileId, e);
|
|
290
|
+
if (e.data.openapi) {
|
|
291
|
+
methodByFileId.set(fileId, e.data.openapi.trim().split(/\s+/, 1)[0].toUpperCase());
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const titleForSlug = (slug: string) => titleByFileId.get(slug) ?? slug;
|
|
295
|
+
const methodForSlug = (slug: string) => methodByFileId.get(slug) ?? null;
|
|
296
|
+
const hrefForSlug = (slug: string) => {
|
|
297
|
+
const raw = entryByFileId.get(slug)?.id ?? slug;
|
|
298
|
+
const effective = normalizeEntryId(raw);
|
|
299
|
+
return effective === "index" ? "/" : `/${effective}/`;
|
|
300
|
+
};
|
|
301
|
+
// The current page's own file id (as writedocs.json references it) - distinct
|
|
302
|
+
// from `entry.id` (its resolved URL slug) for the same reason as above,
|
|
303
|
+
// and what every "is this the active page" comparison below needs, since
|
|
304
|
+
// navTree/globalDropdowns are built from writedocs.json's file-id space.
|
|
305
|
+
const currentFileId = fileIdForEntry(contentDir, packageRoot, entry);
|
|
306
|
+
const navTree = buildNavTree(activeSection.pages, titleForSlug, methodForSlug);
|
|
307
|
+
// The ancestor sidebar-group trail above the auto <h1> (Breadcrumbs.astro)
|
|
308
|
+
// - empty for a page sitting at the top level of its Section with no
|
|
309
|
+
// enclosing group, in which case [...slug].astro below skips rendering
|
|
310
|
+
// the component entirely rather than showing a breadcrumb bar with
|
|
311
|
+
// nothing in it but the page's own title.
|
|
312
|
+
const breadcrumbs = ancestorGroupsForSlug(navTree, currentFileId, hrefForSlug);
|
|
313
|
+
|
|
314
|
+
// The reader's page's own position within the active Section's flattened
|
|
315
|
+
// pages list - passed to buildSelectors() so a version/language switch
|
|
316
|
+
// can try to land on the *same* page in the target option instead of
|
|
317
|
+
// always its first page (equivalentPageIn() in lib/config.ts). -1 (not
|
|
318
|
+
// found) just disables that - firstSlugOf() is still the fallback.
|
|
319
|
+
const activePagePosition = flattenNav(activeSection.pages).findIndex((e) => e.slug === currentFileId);
|
|
320
|
+
|
|
321
|
+
// One selector per level of the active page's navigation path (tabs,
|
|
322
|
+
// versions, languages, products, or a dropdown nested partway down),
|
|
323
|
+
// plus the always-visible global dropdown list - see BaseLayout.astro
|
|
324
|
+
// for how each renders.
|
|
325
|
+
const selectors = buildSelectors(activeSection.path, hrefForSlug, activePagePosition);
|
|
326
|
+
const globalDropdowns = buildGlobalDropdowns(
|
|
327
|
+
resolveGlobalDropdowns(config.navigation),
|
|
328
|
+
currentFileId,
|
|
329
|
+
titleForSlug,
|
|
330
|
+
hrefForSlug,
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
// Meta tags: this page's own frontmatter `seo` overrides writedocs.json's
|
|
334
|
+
// site-wide `seo` field by field (mergeSeo() in lib/config.ts), and the
|
|
335
|
+
// canonical/OG/Twitter URLs need this page's actual served path - reusing
|
|
336
|
+
// hrefForSlug (already built above for prev/next and the sidebar) rather
|
|
337
|
+
// than recomputing it, since it already encodes the same
|
|
338
|
+
// frontmatter-slug-override-aware routing logic.
|
|
339
|
+
const pageSeo = mergeSeo(config.seo, entry.data.seo);
|
|
340
|
+
const currentPath = hrefForSlug(currentFileId);
|
|
341
|
+
|
|
342
|
+
// Page mode (content.config.ts's docsSchema `mode` field, defaulted to
|
|
343
|
+
// 'default' there so this is never actually undefined) - see
|
|
344
|
+
// BaseLayout.astro for what each value strips out of the topbar/shell,
|
|
345
|
+
// and the comment on <BaseLayout mode=...> below for what this file
|
|
346
|
+
// itself controls (which slots get filled at all, and how the article
|
|
347
|
+
// itself renders).
|
|
348
|
+
// A hidden page (isHidden - see hiddenRoutes/Props above) defaults to
|
|
349
|
+
// "frame" instead of "default": it has no real section of its own, so
|
|
350
|
+
// the sidebar would otherwise show findSectionIndexForSlug's arbitrary
|
|
351
|
+
// fallback section with nothing in it actually active - "frame" drops
|
|
352
|
+
// the sidebar (and ToC) while keeping the topbar and normal article
|
|
353
|
+
// styling. Only a *default*: a hidden page's own frontmatter mode, if
|
|
354
|
+
// it set one explicitly, still wins.
|
|
355
|
+
const pageMode = isHidden && entry.data.mode === "default" ? "frame" : entry.data.mode;
|
|
356
|
+
const showSidebar = pageMode === "default" || pageMode === "wide";
|
|
357
|
+
const showToc = pageMode === "default";
|
|
358
|
+
// 'custom' and 'blank' both get the bare wd-canvas treatment below (no
|
|
359
|
+
// auto <h1>, no prev/next, no prose width/padding) - they only differ in
|
|
360
|
+
// whether BaseLayout keeps the topbar, which is BaseLayout's own concern
|
|
361
|
+
// (mode is passed straight through), not something this file needs to
|
|
362
|
+
// branch on separately.
|
|
363
|
+
const isCanvasMode = pageMode === "custom" || pageMode === "blank";
|
|
364
|
+
|
|
365
|
+
// The "Copy page" dropdown (CopyPageMenu.astro) - opt-in via writedocs.json's
|
|
366
|
+
// `contextMenu` field (see contextMenuSchema in lib/config.ts), and only
|
|
367
|
+
// shown where there's a real auto-rendered <h1> to sit next to: canvas
|
|
368
|
+
// mode pages (custom/blank) have no such header (a hand-built landing
|
|
369
|
+
// page controls its own layout, there's nothing standard to anchor the
|
|
370
|
+
// menu to), and OpenAPI operation pages render almost entirely from the
|
|
371
|
+
// spec rather than prose - see [...slug].md.ts's own comment on why
|
|
372
|
+
// those are excluded from the .md route this menu links to in the first
|
|
373
|
+
// place, which this mirrors on the UI side.
|
|
374
|
+
const showCopyPageMenu = Boolean(config.contextMenu) && !isCanvasMode && !entry.data.openapi;
|
|
375
|
+
const siteUrl = resolveSiteUrl(config);
|
|
376
|
+
|
|
377
|
+
const components = {
|
|
378
|
+
Callout,
|
|
379
|
+
Note,
|
|
380
|
+
Info,
|
|
381
|
+
Tip,
|
|
382
|
+
Warning,
|
|
383
|
+
Danger,
|
|
384
|
+
Card,
|
|
385
|
+
CardGroup,
|
|
386
|
+
Tabs,
|
|
387
|
+
Tab,
|
|
388
|
+
CodeGroup,
|
|
389
|
+
Accordion,
|
|
390
|
+
AccordionGroup,
|
|
391
|
+
Steps,
|
|
392
|
+
Step,
|
|
393
|
+
Hint,
|
|
394
|
+
Image,
|
|
395
|
+
Frame,
|
|
396
|
+
Video,
|
|
397
|
+
Parameter,
|
|
398
|
+
Expandable,
|
|
399
|
+
Searchbar,
|
|
400
|
+
Badge,
|
|
401
|
+
Icon,
|
|
402
|
+
RequestExample,
|
|
403
|
+
ResponseExample,
|
|
404
|
+
};
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
<BaseLayout
|
|
408
|
+
config={config}
|
|
409
|
+
title={entry.data.title}
|
|
410
|
+
description={entry.data.description}
|
|
411
|
+
path={currentPath}
|
|
412
|
+
seo={pageSeo}
|
|
413
|
+
selectors={selectors}
|
|
414
|
+
globalDropdowns={globalDropdowns}
|
|
415
|
+
mode={pageMode}
|
|
416
|
+
>
|
|
417
|
+
{showSidebar && <Sidebar slot="sidebar" navTree={navTree} hrefForSlug={hrefForSlug} currentSlug={currentFileId} />}
|
|
418
|
+
{
|
|
419
|
+
isCanvasMode ? (
|
|
420
|
+
// 'custom' and 'blank' both get this treatment: no auto <h1>, no
|
|
421
|
+
// prev/next nav, none of .wd-article's own prose max-width/padding -
|
|
422
|
+
// just the page's own MDX/component content, edge to edge. Still gets
|
|
423
|
+
// a data-pagefind-body wrapper so a hand-built page's own text is
|
|
424
|
+
// still searchable.
|
|
425
|
+
<div class="wd-canvas" data-pagefind-body>
|
|
426
|
+
<Content components={components} />
|
|
427
|
+
{entry.data.openapi && <ApiPlayground operation={entry.data.openapi} contentDir={contentDir} />}
|
|
428
|
+
</div>
|
|
429
|
+
) : (
|
|
430
|
+
<article class={`wd-article ${pageMode === "wide" ? "wd-article-wide" : ""}`} data-pagefind-body>
|
|
431
|
+
{breadcrumbs.length > 0 && <Breadcrumbs items={breadcrumbs} />}
|
|
432
|
+
<div class="wd-article-header">
|
|
433
|
+
<h1 data-pagefind-meta="title">{entry.data.title}</h1>
|
|
434
|
+
{showCopyPageMenu && (
|
|
435
|
+
<CopyPageMenu currentPath={currentPath} siteUrl={siteUrl} contextMenu={config.contextMenu!} />
|
|
436
|
+
)}
|
|
437
|
+
</div>
|
|
438
|
+
<Content components={components} />
|
|
439
|
+
{entry.data.openapi && <ApiPlayground operation={entry.data.openapi} contentDir={contentDir} />}
|
|
440
|
+
<nav class="wd-prevnext" data-pagefind-ignore>
|
|
441
|
+
{prev && (
|
|
442
|
+
<a class="wd-prevnext-card wd-prevnext-prev" href={hrefForSlug(prev.slug)}>
|
|
443
|
+
<span class="wd-prevnext-row">
|
|
444
|
+
<svg
|
|
445
|
+
class="wd-prevnext-icon"
|
|
446
|
+
width="16"
|
|
447
|
+
height="16"
|
|
448
|
+
viewBox="0 0 24 24"
|
|
449
|
+
fill="none"
|
|
450
|
+
stroke="currentColor"
|
|
451
|
+
stroke-width="2"
|
|
452
|
+
stroke-linecap="round"
|
|
453
|
+
stroke-linejoin="round"
|
|
454
|
+
aria-hidden="true"
|
|
455
|
+
>
|
|
456
|
+
<line x1="19" y1="12" x2="5" y2="12" />
|
|
457
|
+
<polyline points="12 19 5 12 12 5" />
|
|
458
|
+
</svg>
|
|
459
|
+
<span class="wd-prevnext-label">Previous</span>
|
|
460
|
+
</span>
|
|
461
|
+
<span class="wd-prevnext-title">{titleForSlug(prev.slug)}</span>
|
|
462
|
+
</a>
|
|
463
|
+
)}
|
|
464
|
+
{next && (
|
|
465
|
+
<a class="wd-prevnext-card wd-prevnext-next" href={hrefForSlug(next.slug)}>
|
|
466
|
+
<span class="wd-prevnext-row">
|
|
467
|
+
<span class="wd-prevnext-label">Next</span>
|
|
468
|
+
<svg
|
|
469
|
+
class="wd-prevnext-icon"
|
|
470
|
+
width="16"
|
|
471
|
+
height="16"
|
|
472
|
+
viewBox="0 0 24 24"
|
|
473
|
+
fill="none"
|
|
474
|
+
stroke="currentColor"
|
|
475
|
+
stroke-width="2"
|
|
476
|
+
stroke-linecap="round"
|
|
477
|
+
stroke-linejoin="round"
|
|
478
|
+
aria-hidden="true"
|
|
479
|
+
>
|
|
480
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
481
|
+
<polyline points="12 5 19 12 12 19" />
|
|
482
|
+
</svg>
|
|
483
|
+
</span>
|
|
484
|
+
<span class="wd-prevnext-title">{titleForSlug(next.slug)}</span>
|
|
485
|
+
</a>
|
|
486
|
+
)}
|
|
487
|
+
</nav>
|
|
488
|
+
</article>
|
|
489
|
+
)
|
|
490
|
+
}
|
|
491
|
+
{
|
|
492
|
+
showToc &&
|
|
493
|
+
(entry.data.openapi ? (
|
|
494
|
+
<ApiReferencePanel slot="toc" operation={entry.data.openapi} contentDir={contentDir} />
|
|
495
|
+
) : (
|
|
496
|
+
<TableOfContents slot="toc" headings={tocHeadings} />
|
|
497
|
+
))
|
|
498
|
+
}
|
|
499
|
+
</BaseLayout>
|
|
500
|
+
|
|
501
|
+
<script>
|
|
502
|
+
// Astro assigns every h2-h4 an id automatically, but doesn't add a
|
|
503
|
+
// clickable anchor next to it. rehype-autolink-headings can't be used
|
|
504
|
+
// for this directly: Astro's own heading-id rehype plugin always runs
|
|
505
|
+
// AFTER any user-supplied rehypePlugins in its pipeline, so a plugin
|
|
506
|
+
// that links existing ids never sees them. Adding the anchor client-side
|
|
507
|
+
// sidesteps that ordering constraint entirely.
|
|
508
|
+
function addHeadingAnchors(root: ParentNode) {
|
|
509
|
+
root
|
|
510
|
+
.querySelectorAll<HTMLElement>(".wd-article > h2[id], .wd-article > h3[id], .wd-article > h4[id]")
|
|
511
|
+
.forEach((heading) => {
|
|
512
|
+
if (heading.querySelector(".wd-heading-anchor")) return;
|
|
513
|
+
const anchor = document.createElement("a");
|
|
514
|
+
anchor.className = "wd-heading-anchor";
|
|
515
|
+
anchor.href = `#${heading.id}`;
|
|
516
|
+
anchor.setAttribute("aria-label", "Link to this section");
|
|
517
|
+
anchor.textContent = "#";
|
|
518
|
+
heading.appendChild(anchor);
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
addHeadingAnchors(document);
|
|
522
|
+
document.addEventListener("astro:page-load", () => addHeadingAnchors(document));
|
|
523
|
+
|
|
524
|
+
// Copy-to-clipboard button for every fenced (```) code block - the
|
|
525
|
+
// button itself is emitted at build time by codeBlockTransformer
|
|
526
|
+
// (astro.config.mjs / src/lib/shiki-code-block.js), wrapping each
|
|
527
|
+
// <pre> in a .wd-code-block container; this just wires up the click
|
|
528
|
+
// behavior client-side, same dataset-guarded init pattern as
|
|
529
|
+
// addHeadingAnchors above.
|
|
530
|
+
function initCodeCopyButtons(root: ParentNode) {
|
|
531
|
+
root.querySelectorAll<HTMLButtonElement>(".wd-code-copy-btn").forEach((btn) => {
|
|
532
|
+
if (btn.dataset.wdInit) return;
|
|
533
|
+
btn.dataset.wdInit = "true";
|
|
534
|
+
btn.addEventListener("click", async () => {
|
|
535
|
+
const pre = btn.closest(".wd-code-block")?.querySelector("pre");
|
|
536
|
+
if (!pre) return;
|
|
537
|
+
try {
|
|
538
|
+
await navigator.clipboard.writeText(pre.textContent ?? "");
|
|
539
|
+
} catch {
|
|
540
|
+
return; // clipboard unavailable (e.g. insecure context) - fail silently
|
|
541
|
+
}
|
|
542
|
+
btn.textContent = "✓";
|
|
543
|
+
btn.classList.add("wd-code-copied");
|
|
544
|
+
window.setTimeout(() => {
|
|
545
|
+
btn.textContent = "⧉";
|
|
546
|
+
btn.classList.remove("wd-code-copied");
|
|
547
|
+
}, 1500);
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
initCodeCopyButtons(document);
|
|
552
|
+
document.addEventListener("astro:page-load", () => initCodeCopyButtons(document));
|
|
553
|
+
|
|
554
|
+
// Clipboard-copy button for CopyPageMenu.astro's primary "Copy page"
|
|
555
|
+
// button (the split button's left half - the right half is just a
|
|
556
|
+
// .wd-dropdown-trigger caret, handled by BaseLayout's own
|
|
557
|
+
// initDropdowns()) - same dataset-guarded init pattern as
|
|
558
|
+
// initCodeCopyButtons above, but fetches its content from the button's
|
|
559
|
+
// own data-md-path (the [...slug].md.ts route for the current page -
|
|
560
|
+
// see CopyPageMenu.astro) rather than reading it from an
|
|
561
|
+
// already-rendered element on the page, since the raw Markdown source
|
|
562
|
+
// isn't otherwise present in the DOM anywhere (the rendered HTML
|
|
563
|
+
// content is not the same text).
|
|
564
|
+
function initCopyPageMenu(root: ParentNode) {
|
|
565
|
+
root.querySelectorAll<HTMLButtonElement>(".wd-copy-page-primary").forEach((btn) => {
|
|
566
|
+
if (btn.dataset.wdInit) return;
|
|
567
|
+
btn.dataset.wdInit = "true";
|
|
568
|
+
const label = btn.querySelector<HTMLElement>(".wd-copy-page-primary-label");
|
|
569
|
+
const originalLabel = label?.textContent ?? "Copy page";
|
|
570
|
+
btn.addEventListener("click", async () => {
|
|
571
|
+
const mdPath = btn.dataset.mdPath;
|
|
572
|
+
if (!mdPath) return;
|
|
573
|
+
try {
|
|
574
|
+
const res = await fetch(mdPath);
|
|
575
|
+
const text = await res.text();
|
|
576
|
+
await navigator.clipboard.writeText(text);
|
|
577
|
+
} catch {
|
|
578
|
+
return; // fetch/clipboard unavailable - fail silently, same as initCodeCopyButtons
|
|
579
|
+
}
|
|
580
|
+
btn.classList.add("wd-copy-page-copied");
|
|
581
|
+
if (label) label.textContent = "Copied!";
|
|
582
|
+
window.setTimeout(() => {
|
|
583
|
+
btn.classList.remove("wd-copy-page-copied");
|
|
584
|
+
if (label) label.textContent = originalLabel;
|
|
585
|
+
}, 1500);
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
initCopyPageMenu(document);
|
|
590
|
+
document.addEventListener("astro:page-load", () => initCopyPageMenu(document));
|
|
591
|
+
|
|
592
|
+
// Show more/less toggle for ```js expandable code blocks - same
|
|
593
|
+
// build-time-emitted-button + client-wired-click pattern as the copy
|
|
594
|
+
// button above; codeBlockTransformer only adds this button when the
|
|
595
|
+
// fence's meta string contains the `expandable` keyword.
|
|
596
|
+
function initCodeExpandButtons(root: ParentNode) {
|
|
597
|
+
root.querySelectorAll<HTMLButtonElement>(".wd-code-expand-btn").forEach((btn) => {
|
|
598
|
+
if (btn.dataset.wdInit) return;
|
|
599
|
+
btn.dataset.wdInit = "true";
|
|
600
|
+
btn.addEventListener("click", () => {
|
|
601
|
+
const block = btn.closest(".wd-code-expandable");
|
|
602
|
+
if (!block) return;
|
|
603
|
+
const expanded = block.classList.toggle("wd-code-expanded");
|
|
604
|
+
btn.textContent = expanded ? "Show less" : "Show more";
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
initCodeExpandButtons(document);
|
|
609
|
+
document.addEventListener("astro:page-load", () => initCodeExpandButtons(document));
|
|
610
|
+
|
|
611
|
+
// Renders ```mermaid fenced blocks - already reshaped at build time into
|
|
612
|
+
// plain <div class="mermaid">source text</div> containers (see
|
|
613
|
+
// rehypeMermaid in astro.config.mjs/src/lib/mermaid-rehype.js) - into
|
|
614
|
+
// actual SVG diagrams. Lazily imports the mermaid package itself, same
|
|
615
|
+
// reasoning as initSearch's Pagefind import above: most pages have no
|
|
616
|
+
// diagrams at all, no reason to ship/parse that bundle on every load.
|
|
617
|
+
let mermaidLoadPromise: Promise<typeof import("mermaid")> | null = null;
|
|
618
|
+
function ensureMermaid() {
|
|
619
|
+
if (!mermaidLoadPromise) mermaidLoadPromise = import("mermaid");
|
|
620
|
+
return mermaidLoadPromise;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// mermaid.run() replaces a block's contents with the rendered SVG,
|
|
624
|
+
// destroying the original diagram source - stashed in a data attribute
|
|
625
|
+
// the first time a block is seen (before it's ever rendered), so a
|
|
626
|
+
// later re-render (see the wd:theme-change listener below - diagrams
|
|
627
|
+
// are baked to static SVG at render time, so a theme change needs an
|
|
628
|
+
// actual re-render, unlike Shiki code blocks which swap colors live via
|
|
629
|
+
// CSS custom properties) has something to restore from.
|
|
630
|
+
function stashMermaidSource(root: ParentNode) {
|
|
631
|
+
root.querySelectorAll<HTMLElement>(".mermaid").forEach((el) => {
|
|
632
|
+
if (el.dataset.mermaidSource === undefined) el.dataset.mermaidSource = el.textContent ?? "";
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async function renderMermaid(root: ParentNode, theme: "light" | "dark") {
|
|
637
|
+
const blocks = Array.from(root.querySelectorAll<HTMLElement>(".mermaid"));
|
|
638
|
+
if (blocks.length === 0) return;
|
|
639
|
+
const { default: mermaid } = await ensureMermaid();
|
|
640
|
+
mermaid.initialize({ startOnLoad: false, theme: theme === "dark" ? "dark" : "default", securityLevel: "strict" });
|
|
641
|
+
blocks.forEach((el) => {
|
|
642
|
+
el.removeAttribute("data-processed"); // mermaid's own re-render guard - see run()'s docs
|
|
643
|
+
el.innerHTML = el.dataset.mermaidSource ?? "";
|
|
644
|
+
});
|
|
645
|
+
// suppressErrors: a malformed diagram on one block shouldn't take
|
|
646
|
+
// down every other diagram on the page - mermaid still renders a
|
|
647
|
+
// per-block error SVG in place and logs details to the console.
|
|
648
|
+
await mermaid.run({ nodes: blocks, suppressErrors: true });
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function initMermaid(root: ParentNode) {
|
|
652
|
+
stashMermaidSource(root);
|
|
653
|
+
if (!root.querySelector(".mermaid")) return;
|
|
654
|
+
const theme = document.documentElement.dataset.theme === "dark" ? "dark" : "light";
|
|
655
|
+
renderMermaid(root, theme);
|
|
656
|
+
}
|
|
657
|
+
initMermaid(document);
|
|
658
|
+
document.addEventListener("astro:page-load", () => initMermaid(document));
|
|
659
|
+
document.addEventListener("wd:theme-change", ((e: CustomEvent<{ theme: "light" | "dark" }>) => {
|
|
660
|
+
renderMermaid(document, e.detail.theme);
|
|
661
|
+
}) as EventListener);
|
|
662
|
+
|
|
663
|
+
// RequestExample/ResponseExample.astro's own docking behavior - on
|
|
664
|
+
// desktop, moves their rendered .wd-example-panel(s) out of the normal
|
|
665
|
+
// article flow and into .wd-toc-col (replacing whatever it would
|
|
666
|
+
// otherwise show - the heading-based TableOfContents or
|
|
667
|
+
// ApiReferencePanel, per those two components' own comment), matching
|
|
668
|
+
// Mintlify's stated behavior for the components this was modeled on:
|
|
669
|
+
// pinned in the sidebar on desktop, plain in-place code blocks below
|
|
670
|
+
// 1150px (the exact breakpoint .wd-toc-col itself disappears at - see
|
|
671
|
+
// base.css). This can't be done at build time: MDX compiles a page's
|
|
672
|
+
// whole body into one content flow, with no way for a static render to
|
|
673
|
+
// place part of that flow into a *different* column's own slot output
|
|
674
|
+
// - a client-side move after the fact is the only way to get this
|
|
675
|
+
// content into a sibling column's subtree at all.
|
|
676
|
+
function initExamplePanels(root: ParentNode) {
|
|
677
|
+
const tocCol = document.querySelector<HTMLElement>(".wd-toc-col");
|
|
678
|
+
if (!tocCol) return; // no toc column on this page (mode !== 'default') - nothing to dock into
|
|
679
|
+
|
|
680
|
+
const panels = Array.from(root.querySelectorAll<HTMLElement>(".wd-example-panel"));
|
|
681
|
+
if (panels.length === 0) return;
|
|
682
|
+
if (tocCol.dataset.wdExampleInit) return;
|
|
683
|
+
tocCol.dataset.wdExampleInit = "true";
|
|
684
|
+
|
|
685
|
+
// Response always docks below Request regardless of source order,
|
|
686
|
+
// matching Mintlify's own stated behavior for these two components.
|
|
687
|
+
panels.sort((a, b) => {
|
|
688
|
+
const rank = (el: HTMLElement) => (el.classList.contains("wd-request-example") ? 0 : 1);
|
|
689
|
+
return rank(a) - rank(b);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// A comment node marks each panel's original in-flow position, so
|
|
693
|
+
// undock() (a desktop -> mobile resize, or simply no matching media
|
|
694
|
+
// query at all) can put it back exactly where the article's own
|
|
695
|
+
// content expects it, rather than leaving a permanent gap.
|
|
696
|
+
const anchors = panels.map((panel) => {
|
|
697
|
+
const anchor = document.createComment("wd-example-anchor");
|
|
698
|
+
panel.before(anchor);
|
|
699
|
+
return anchor;
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
let host = tocCol.querySelector<HTMLElement>(".wd-example-host");
|
|
703
|
+
if (!host) {
|
|
704
|
+
host = document.createElement("div");
|
|
705
|
+
host.className = "wd-example-host";
|
|
706
|
+
tocCol.prepend(host);
|
|
707
|
+
}
|
|
708
|
+
const dockedHost = host;
|
|
709
|
+
|
|
710
|
+
// Whatever .wd-toc-col would otherwise render (TableOfContents' own
|
|
711
|
+
// <nav class="wd-toc">, or ApiReferencePanel's own markup) - hidden
|
|
712
|
+
// while panels are docked rather than removed, so undock() has
|
|
713
|
+
// something to restore instead of needing to re-render it.
|
|
714
|
+
const existingTocContent = Array.from(tocCol.children).filter((el) => el !== dockedHost) as HTMLElement[];
|
|
715
|
+
|
|
716
|
+
function dock() {
|
|
717
|
+
existingTocContent.forEach((el) => (el.style.display = "none"));
|
|
718
|
+
dockedHost.hidden = false;
|
|
719
|
+
panels.forEach((panel) => dockedHost.appendChild(panel));
|
|
720
|
+
}
|
|
721
|
+
function undock() {
|
|
722
|
+
dockedHost.hidden = true;
|
|
723
|
+
existingTocContent.forEach((el) => (el.style.display = ""));
|
|
724
|
+
panels.forEach((panel, i) => anchors[i].after(panel));
|
|
725
|
+
}
|
|
726
|
+
const desktopQuery = window.matchMedia("(min-width: 1151px)");
|
|
727
|
+
const sync = () => (desktopQuery.matches ? dock() : undock());
|
|
728
|
+
sync();
|
|
729
|
+
desktopQuery.addEventListener("change", sync);
|
|
730
|
+
}
|
|
731
|
+
initExamplePanels(document);
|
|
732
|
+
document.addEventListener("astro:page-load", () => initExamplePanels(document));
|
|
733
|
+
</script>
|
|
734
|
+
|
|
735
|
+
<style>
|
|
736
|
+
.wd-article {
|
|
737
|
+
max-width: 900px;
|
|
738
|
+
margin: 0 auto;
|
|
739
|
+
/* Top padding matches Sidebar.astro's .wd-sidebar and
|
|
740
|
+
TableOfContents.astro's .wd-toc exactly (both 1.5rem) - all three
|
|
741
|
+
are siblings in the same .wd-shell row with no padding of their
|
|
742
|
+
own on .wd-main-col in between, so this is what actually lines up
|
|
743
|
+
the article's top edge with the sidebar/TOC's first item instead
|
|
744
|
+
of starting visibly lower than both. */
|
|
745
|
+
padding: 1.5rem 1.5rem 4rem;
|
|
746
|
+
}
|
|
747
|
+
/* frontmatter `mode: wide` - the table of contents column is gone (see
|
|
748
|
+
BaseLayout.astro), so the article itself gets more room too, rather
|
|
749
|
+
than just recentering the same 760px column into now-empty space. */
|
|
750
|
+
.wd-article-wide {
|
|
751
|
+
max-width: 1040px;
|
|
752
|
+
}
|
|
753
|
+
/* frontmatter `mode: custom` - no prose width/padding at all, no
|
|
754
|
+
max-width, no centering: a hand-built landing page controls its own
|
|
755
|
+
layout completely, full-bleed if it wants to. */
|
|
756
|
+
.wd-canvas {
|
|
757
|
+
width: 100%;
|
|
758
|
+
}
|
|
759
|
+
/* Wraps the auto-rendered <h1> together with CopyPageMenu (only
|
|
760
|
+
rendered when writedocs.json's `contextMenu` is set - see
|
|
761
|
+
showCopyPageMenu above) so the two sit on one row, menu pinned to
|
|
762
|
+
the right. Always present (even with no menu) rather than only
|
|
763
|
+
wrapping the <h1> conditionally, so the <h1>'s own top/bottom
|
|
764
|
+
spacing comes from exactly one place regardless of whether the menu
|
|
765
|
+
renders - flex items don't margin-collapse with their container the
|
|
766
|
+
way a bare <h1> would with .wd-article, so that margin is
|
|
767
|
+
reset/reapplied here instead of relying on the browser's default
|
|
768
|
+
heading margin still doing the right thing once it's no longer a
|
|
769
|
+
direct .wd-article child. */
|
|
770
|
+
.wd-article-header {
|
|
771
|
+
display: flex;
|
|
772
|
+
align-items: center;
|
|
773
|
+
justify-content: space-between;
|
|
774
|
+
gap: 1rem;
|
|
775
|
+
margin: 0 0 1rem;
|
|
776
|
+
}
|
|
777
|
+
.wd-article-header h1 {
|
|
778
|
+
margin: 0;
|
|
779
|
+
}
|
|
780
|
+
.wd-prevnext {
|
|
781
|
+
display: flex;
|
|
782
|
+
justify-content: space-between;
|
|
783
|
+
margin-top: 3rem;
|
|
784
|
+
padding-top: 1.5rem;
|
|
785
|
+
border-top: 1px solid var(--wd-border);
|
|
786
|
+
gap: 1rem;
|
|
787
|
+
}
|
|
788
|
+
/* Two side-by-side cards, each taking up to half the row - flex: 1 1
|
|
789
|
+
0 rather than a fixed 50% so a single remaining card (first page:
|
|
790
|
+
only next, last page: only prev) grows to fill the width instead
|
|
791
|
+
of leaving an empty gap next to it. */
|
|
792
|
+
.wd-prevnext-card {
|
|
793
|
+
display: flex;
|
|
794
|
+
flex: 1 1 0;
|
|
795
|
+
min-width: 0;
|
|
796
|
+
flex-direction: column;
|
|
797
|
+
gap: 0.1rem;
|
|
798
|
+
padding: 0.75rem 1.1rem;
|
|
799
|
+
border: 1px solid var(--wd-border);
|
|
800
|
+
border-radius: 0.75rem;
|
|
801
|
+
text-decoration: none;
|
|
802
|
+
color: inherit;
|
|
803
|
+
transition:
|
|
804
|
+
border-color 0.15s ease,
|
|
805
|
+
box-shadow 0.15s ease;
|
|
806
|
+
}
|
|
807
|
+
/* Both cards are identical at rest - the accent border/glow in the
|
|
808
|
+
reference screenshot is that card's :hover state, not a permanent
|
|
809
|
+
treatment on Next, so both wd-prevnext-prev and wd-prevnext-next
|
|
810
|
+
share this one hover rule instead of Next getting its own always-on
|
|
811
|
+
copy. Built from --wd-primary via color-mix() rather than a flat
|
|
812
|
+
hardcoded color, so it stays correct under any theme's own accent. */
|
|
813
|
+
.wd-prevnext-card:hover {
|
|
814
|
+
border-color: var(--wd-primary);
|
|
815
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wd-primary) 10%, transparent);
|
|
816
|
+
}
|
|
817
|
+
.wd-prevnext-row {
|
|
818
|
+
display: flex;
|
|
819
|
+
align-items: center;
|
|
820
|
+
justify-content: space-between;
|
|
821
|
+
gap: 0.5rem;
|
|
822
|
+
color: var(--wd-text-muted);
|
|
823
|
+
font-size: 0.75rem;
|
|
824
|
+
}
|
|
825
|
+
.wd-prevnext-icon {
|
|
826
|
+
flex-shrink: 0;
|
|
827
|
+
color: var(--wd-text-muted);
|
|
828
|
+
transition: color 0.15s ease;
|
|
829
|
+
}
|
|
830
|
+
.wd-prevnext-title {
|
|
831
|
+
font-weight: 600;
|
|
832
|
+
font-size: 0.85rem;
|
|
833
|
+
color: var(--wd-text);
|
|
834
|
+
white-space: nowrap;
|
|
835
|
+
overflow: hidden;
|
|
836
|
+
text-overflow: ellipsis;
|
|
837
|
+
transition: color 0.15s ease;
|
|
838
|
+
}
|
|
839
|
+
/* Previous's title sits at the same end of the card its own
|
|
840
|
+
"Previous" label already does (right-aligned, next to the arrow's
|
|
841
|
+
fixed spot on the left) - Next's title stays left-aligned, at the
|
|
842
|
+
same end its own "Next" label sits. Keeps each card's two rows
|
|
843
|
+
reading as one consistent left/right-anchored column instead of
|
|
844
|
+
the title always defaulting to the left regardless of which side
|
|
845
|
+
its card's own label/arrow occupy. */
|
|
846
|
+
.wd-prevnext-prev .wd-prevnext-title {
|
|
847
|
+
text-align: right;
|
|
848
|
+
}
|
|
849
|
+
/* Icon and title pick up the accent color on hover - the
|
|
850
|
+
"Next"/"Previous" label itself stays muted even then, since it's a
|
|
851
|
+
plain UI affordance rather than the actual content being pointed
|
|
852
|
+
to. */
|
|
853
|
+
.wd-prevnext-card:hover .wd-prevnext-icon,
|
|
854
|
+
.wd-prevnext-card:hover .wd-prevnext-title {
|
|
855
|
+
color: var(--wd-primary);
|
|
856
|
+
}
|
|
857
|
+
@media (width <= 640px) {
|
|
858
|
+
.wd-prevnext {
|
|
859
|
+
flex-direction: column;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
</style>
|
|
863
|
+
|
|
864
|
+
<style is:global>
|
|
865
|
+
/*
|
|
866
|
+
* These target direct children of .wd-article (or, for inline marks,
|
|
867
|
+
* specific prose containers) rather than bare tag selectors. MDX
|
|
868
|
+
* component internals (Card titles/links, prev/next nav links) also
|
|
869
|
+
* live inside .wd-article but nested deeper - a bare ".wd-article h3"
|
|
870
|
+
* or ".wd-article a" would win the specificity tie against those
|
|
871
|
+
* components' own scoped styles and visibly break them (oversized
|
|
872
|
+
* card titles, underlined cards, underlined prev/next links). Real
|
|
873
|
+
* MDX content (headings, paragraphs, lists, etc.) renders as flat
|
|
874
|
+
* direct children of .wd-article, so the ">" combinator is enough to
|
|
875
|
+
* separate "actual content" from "component chrome" without needing
|
|
876
|
+
* per-component :not() exceptions.
|
|
877
|
+
*/
|
|
878
|
+
/* --wd-topbar-offset is set by initTopbarOffset() (src/scripts/
|
|
879
|
+
topbar-offset.ts) to the sticky topbar's real measured height - see
|
|
880
|
+
its own file comment for why a fixed 5rem (this var()'s own
|
|
881
|
+
fallback, for the instant before that script's first measurement)
|
|
882
|
+
isn't enough once a site's topbar grows a second row. Without this,
|
|
883
|
+
jumping straight to a #heading link (a hard load, or clicking one of
|
|
884
|
+
the .wd-heading-anchor links below) lands the heading partly hidden
|
|
885
|
+
under the sticky topbar. */
|
|
886
|
+
.wd-article > h2 {
|
|
887
|
+
font-size: 1.5rem;
|
|
888
|
+
margin: 2.75rem 0 1rem;
|
|
889
|
+
padding-top: 0.25rem;
|
|
890
|
+
scroll-margin-top: var(--wd-topbar-offset, 5rem);
|
|
891
|
+
}
|
|
892
|
+
.wd-article > h3 {
|
|
893
|
+
font-size: 1.2rem;
|
|
894
|
+
margin: 2rem 0 0.75rem;
|
|
895
|
+
scroll-margin-top: var(--wd-topbar-offset, 5rem);
|
|
896
|
+
}
|
|
897
|
+
.wd-article > h4 {
|
|
898
|
+
font-size: 1rem;
|
|
899
|
+
margin: 1.5rem 0 0.5rem;
|
|
900
|
+
scroll-margin-top: var(--wd-topbar-offset, 5rem);
|
|
901
|
+
}
|
|
902
|
+
.wd-article > h2:first-child,
|
|
903
|
+
.wd-article > h3:first-child {
|
|
904
|
+
margin-top: 0;
|
|
905
|
+
}
|
|
906
|
+
.wd-article > p {
|
|
907
|
+
margin: 0.9rem 0;
|
|
908
|
+
}
|
|
909
|
+
.wd-article > ul,
|
|
910
|
+
.wd-article > ol {
|
|
911
|
+
margin: 0.9rem 0;
|
|
912
|
+
padding-left: 1.4rem;
|
|
913
|
+
}
|
|
914
|
+
.wd-article li {
|
|
915
|
+
margin: 0.35rem 0;
|
|
916
|
+
}
|
|
917
|
+
.wd-article li > ul,
|
|
918
|
+
.wd-article li > ol {
|
|
919
|
+
margin: 0.35rem 0;
|
|
920
|
+
}
|
|
921
|
+
.wd-article > blockquote {
|
|
922
|
+
margin: 1.25rem 0;
|
|
923
|
+
padding: 0.1rem 1rem;
|
|
924
|
+
border-left: 3px solid var(--wd-primary);
|
|
925
|
+
background: var(--wd-surface);
|
|
926
|
+
border-radius: 0 0.4rem 0.4rem 0;
|
|
927
|
+
color: var(--wd-text-muted);
|
|
928
|
+
}
|
|
929
|
+
.wd-article blockquote p {
|
|
930
|
+
margin: 0.7rem 0;
|
|
931
|
+
}
|
|
932
|
+
.wd-article > table {
|
|
933
|
+
width: 100%;
|
|
934
|
+
border-collapse: collapse;
|
|
935
|
+
margin: 1.25rem 0;
|
|
936
|
+
font-size: 0.9rem;
|
|
937
|
+
}
|
|
938
|
+
.wd-article th,
|
|
939
|
+
.wd-article td {
|
|
940
|
+
text-align: left;
|
|
941
|
+
padding: 0.55rem 0.75rem;
|
|
942
|
+
border-bottom: 1px solid var(--wd-border);
|
|
943
|
+
vertical-align: top;
|
|
944
|
+
}
|
|
945
|
+
.wd-article th {
|
|
946
|
+
color: var(--wd-text-muted);
|
|
947
|
+
font-weight: 600;
|
|
948
|
+
font-size: 0.8rem;
|
|
949
|
+
text-transform: uppercase;
|
|
950
|
+
letter-spacing: 0.03em;
|
|
951
|
+
}
|
|
952
|
+
.wd-article > hr {
|
|
953
|
+
border: none;
|
|
954
|
+
border-top: 1px solid var(--wd-border);
|
|
955
|
+
margin: 2.5rem 0;
|
|
956
|
+
}
|
|
957
|
+
/* Fenced (```) code blocks - codeBlockTransformer
|
|
958
|
+
(src/lib/shiki-code-block.js) wraps every one's <pre> in this
|
|
959
|
+
.wd-code-block div at build time (plus an optional .wd-code-title
|
|
960
|
+
bar, and the copy/expand buttons - see below). The card-like chrome
|
|
961
|
+
(border/radius/background) lives on the wrapper rather than the
|
|
962
|
+
<pre> itself so a title bar, when present, can sit flush above it
|
|
963
|
+
inside the same box; Astro's Shiki pipeline (dual light/dark themes
|
|
964
|
+
configured in astro.config.mjs) already supplies the <pre>'s own
|
|
965
|
+
background/text colors via inline styles. Matches both a standalone
|
|
966
|
+
block (direct child of .wd-article) and one nested inside
|
|
967
|
+
<CodeGroup> (which re-targets the margin/radius itself via its own
|
|
968
|
+
more specific `.wd-codegroup > .wd-code-block` rule in
|
|
969
|
+
CodeGroup.astro). */
|
|
970
|
+
.wd-code-block {
|
|
971
|
+
position: relative;
|
|
972
|
+
margin: 1.25rem 0;
|
|
973
|
+
border: 1px solid var(--wd-border);
|
|
974
|
+
border-radius: 0.55rem;
|
|
975
|
+
overflow: hidden;
|
|
976
|
+
}
|
|
977
|
+
.wd-article pre.astro-code {
|
|
978
|
+
margin: 0;
|
|
979
|
+
padding: 1rem 1.1rem;
|
|
980
|
+
border: none;
|
|
981
|
+
border-radius: 0;
|
|
982
|
+
font-size: 0.85rem;
|
|
983
|
+
line-height: 1.55;
|
|
984
|
+
}
|
|
985
|
+
.wd-code-title {
|
|
986
|
+
padding: 0.5rem 1rem;
|
|
987
|
+
font-family: monospace;
|
|
988
|
+
font-size: 0.78rem;
|
|
989
|
+
font-weight: 500;
|
|
990
|
+
color: var(--wd-text-muted);
|
|
991
|
+
background: var(--wd-surface);
|
|
992
|
+
border-bottom: 1px solid var(--wd-border);
|
|
993
|
+
}
|
|
994
|
+
.wd-code-copy-btn {
|
|
995
|
+
position: absolute;
|
|
996
|
+
top: 0.6rem;
|
|
997
|
+
right: 0.6rem;
|
|
998
|
+
display: inline-flex;
|
|
999
|
+
align-items: center;
|
|
1000
|
+
justify-content: center;
|
|
1001
|
+
width: 1.85rem;
|
|
1002
|
+
height: 1.85rem;
|
|
1003
|
+
padding: 0;
|
|
1004
|
+
border-radius: 0.4rem;
|
|
1005
|
+
border: 1px solid var(--wd-border);
|
|
1006
|
+
background: var(--wd-background);
|
|
1007
|
+
color: var(--wd-text-muted);
|
|
1008
|
+
font-size: 0.9rem;
|
|
1009
|
+
line-height: 1;
|
|
1010
|
+
cursor: pointer;
|
|
1011
|
+
opacity: 0;
|
|
1012
|
+
transition:
|
|
1013
|
+
opacity 0.15s ease,
|
|
1014
|
+
color 0.15s ease,
|
|
1015
|
+
background 0.15s ease,
|
|
1016
|
+
border-color 0.15s ease;
|
|
1017
|
+
}
|
|
1018
|
+
.wd-code-block:hover .wd-code-copy-btn,
|
|
1019
|
+
.wd-code-copy-btn:focus-visible {
|
|
1020
|
+
opacity: 1;
|
|
1021
|
+
}
|
|
1022
|
+
.wd-code-copy-btn:hover {
|
|
1023
|
+
color: var(--wd-text);
|
|
1024
|
+
background: var(--wd-surface);
|
|
1025
|
+
}
|
|
1026
|
+
.wd-code-copy-btn.wd-code-copied {
|
|
1027
|
+
opacity: 1;
|
|
1028
|
+
color: #16a34a;
|
|
1029
|
+
border-color: #16a34a;
|
|
1030
|
+
}
|
|
1031
|
+
/* ```js wrap - opts a single block into wrapping long lines instead
|
|
1032
|
+
of horizontal-scrolling (the default). */
|
|
1033
|
+
.wd-code-wrap pre.astro-code,
|
|
1034
|
+
.wd-code-wrap pre.astro-code code {
|
|
1035
|
+
white-space: pre-wrap;
|
|
1036
|
+
word-break: break-word;
|
|
1037
|
+
overflow-x: hidden;
|
|
1038
|
+
}
|
|
1039
|
+
/* ```js lines (or showLineNumbers) - a CSS-counter gutter, no markup
|
|
1040
|
+
needed beyond a class since every line is already its own .line
|
|
1041
|
+
span. */
|
|
1042
|
+
.wd-code-lines pre.astro-code code {
|
|
1043
|
+
counter-reset: wd-code-line;
|
|
1044
|
+
}
|
|
1045
|
+
.wd-code-lines pre.astro-code .line {
|
|
1046
|
+
counter-increment: wd-code-line;
|
|
1047
|
+
}
|
|
1048
|
+
.wd-code-lines pre.astro-code .line::before {
|
|
1049
|
+
content: counter(wd-code-line);
|
|
1050
|
+
display: inline-block;
|
|
1051
|
+
width: 1.8em;
|
|
1052
|
+
margin-right: 1em;
|
|
1053
|
+
text-align: right;
|
|
1054
|
+
color: var(--wd-text-muted);
|
|
1055
|
+
user-select: none;
|
|
1056
|
+
}
|
|
1057
|
+
/* ```js expandable - collapses to a fixed height with a "Show more"
|
|
1058
|
+
button below; toggled via .wd-code-expanded, added by the click
|
|
1059
|
+
handler in the <script> block below. */
|
|
1060
|
+
.wd-code-expandable pre.astro-code {
|
|
1061
|
+
max-height: 340px;
|
|
1062
|
+
overflow-y: hidden;
|
|
1063
|
+
}
|
|
1064
|
+
.wd-code-expandable.wd-code-expanded pre.astro-code {
|
|
1065
|
+
max-height: none;
|
|
1066
|
+
}
|
|
1067
|
+
.wd-code-expand-btn {
|
|
1068
|
+
display: block;
|
|
1069
|
+
width: 100%;
|
|
1070
|
+
padding: 0.5rem;
|
|
1071
|
+
border: none;
|
|
1072
|
+
border-top: 1px solid var(--wd-border);
|
|
1073
|
+
background: var(--wd-surface);
|
|
1074
|
+
color: var(--wd-primary);
|
|
1075
|
+
font-size: 0.8rem;
|
|
1076
|
+
font-weight: 500;
|
|
1077
|
+
cursor: pointer;
|
|
1078
|
+
}
|
|
1079
|
+
.wd-code-expand-btn:hover {
|
|
1080
|
+
background: color-mix(in srgb, var(--wd-primary) 8%, var(--wd-surface));
|
|
1081
|
+
}
|
|
1082
|
+
/* Line/word annotation classes added by the official
|
|
1083
|
+
@shikijs/transformers transformers configured alongside
|
|
1084
|
+
codeBlockTransformer in astro.config.mjs:
|
|
1085
|
+
.line.highlighted - transformerMetaHighlight
|
|
1086
|
+
(```js {1,3-5}) and
|
|
1087
|
+
transformerNotationHighlight
|
|
1088
|
+
(// [!code highlight]/[!code hl])
|
|
1089
|
+
.line.diff.add / .diff.remove - transformerNotationDiff
|
|
1090
|
+
(// [!code ++] / // [!code --])
|
|
1091
|
+
.line.error / .warning / .info - transformerNotationErrorLevel
|
|
1092
|
+
(// [!code error]/[!code warning])
|
|
1093
|
+
(also carries .highlighted)
|
|
1094
|
+
pre.has-focused, .line.focused - transformerNotationFocus
|
|
1095
|
+
(// [!code focus])
|
|
1096
|
+
.highlighted-word - transformerMetaWordHighlight
|
|
1097
|
+
(```js /someWord/) and
|
|
1098
|
+
transformerNotationWordHighlight
|
|
1099
|
+
(// [!code word:someWord])
|
|
1100
|
+
Every .line needs display:inline-block + full width for its
|
|
1101
|
+
highlight background to stretch edge-to-edge (by default a <span>
|
|
1102
|
+
only extends as wide as its own text) - harmless for plain lines,
|
|
1103
|
+
since inline-block with no other styling renders identically to
|
|
1104
|
+
the browser's normal inline flow here. */
|
|
1105
|
+
.astro-code .line {
|
|
1106
|
+
display: inline-block;
|
|
1107
|
+
width: 100%;
|
|
1108
|
+
}
|
|
1109
|
+
.astro-code .line.highlighted {
|
|
1110
|
+
background: color-mix(in srgb, var(--wd-primary) 12%, transparent);
|
|
1111
|
+
margin: 0 -1.1rem;
|
|
1112
|
+
padding: 0 1.1rem;
|
|
1113
|
+
width: calc(100% + 2.2rem);
|
|
1114
|
+
}
|
|
1115
|
+
.astro-code .line.diff {
|
|
1116
|
+
margin: 0 -1.1rem;
|
|
1117
|
+
padding: 0 1.1rem 0 1.6rem;
|
|
1118
|
+
width: calc(100% + 2.2rem);
|
|
1119
|
+
position: relative;
|
|
1120
|
+
}
|
|
1121
|
+
.astro-code .line.diff.add {
|
|
1122
|
+
background: color-mix(in srgb, #16a34a 15%, transparent);
|
|
1123
|
+
}
|
|
1124
|
+
.astro-code .line.diff.remove {
|
|
1125
|
+
background: color-mix(in srgb, #dc2626 15%, transparent);
|
|
1126
|
+
opacity: 0.85;
|
|
1127
|
+
}
|
|
1128
|
+
.astro-code .line.diff.add::before,
|
|
1129
|
+
.astro-code .line.diff.remove::before {
|
|
1130
|
+
position: absolute;
|
|
1131
|
+
left: 0.5rem;
|
|
1132
|
+
font-weight: 700;
|
|
1133
|
+
}
|
|
1134
|
+
.astro-code .line.diff.add::before {
|
|
1135
|
+
content: "+";
|
|
1136
|
+
color: #16a34a;
|
|
1137
|
+
}
|
|
1138
|
+
.astro-code .line.diff.remove::before {
|
|
1139
|
+
content: "-";
|
|
1140
|
+
color: #dc2626;
|
|
1141
|
+
}
|
|
1142
|
+
.astro-code .line.error {
|
|
1143
|
+
background: color-mix(in srgb, #dc2626 15%, transparent);
|
|
1144
|
+
}
|
|
1145
|
+
.astro-code .line.warning {
|
|
1146
|
+
background: color-mix(in srgb, #d97706 15%, transparent);
|
|
1147
|
+
}
|
|
1148
|
+
.astro-code .line.info {
|
|
1149
|
+
background: color-mix(in srgb, #2563eb 12%, transparent);
|
|
1150
|
+
}
|
|
1151
|
+
pre.astro-code.has-focused .line {
|
|
1152
|
+
opacity: 0.45;
|
|
1153
|
+
filter: blur(0.3px);
|
|
1154
|
+
transition:
|
|
1155
|
+
opacity 0.2s ease,
|
|
1156
|
+
filter 0.2s ease;
|
|
1157
|
+
}
|
|
1158
|
+
pre.astro-code.has-focused .line.focused {
|
|
1159
|
+
opacity: 1;
|
|
1160
|
+
filter: none;
|
|
1161
|
+
}
|
|
1162
|
+
pre.astro-code.has-focused:hover .line {
|
|
1163
|
+
opacity: 1;
|
|
1164
|
+
filter: none;
|
|
1165
|
+
}
|
|
1166
|
+
.astro-code .highlighted-word {
|
|
1167
|
+
background: color-mix(in srgb, var(--wd-primary) 20%, transparent);
|
|
1168
|
+
border-radius: 0.25rem;
|
|
1169
|
+
padding: 0.05rem 0.2rem;
|
|
1170
|
+
margin: -0.05rem -0.15rem;
|
|
1171
|
+
}
|
|
1172
|
+
.wd-article img {
|
|
1173
|
+
max-width: 100%;
|
|
1174
|
+
border-radius: 0.5rem;
|
|
1175
|
+
border: 1px solid var(--wd-border);
|
|
1176
|
+
}
|
|
1177
|
+
/* ```mermaid fenced blocks (rehypeMermaid in astro.config.mjs reshapes
|
|
1178
|
+
them into this plain <div class="mermaid"> container at build time -
|
|
1179
|
+
see mermaid-rehype.js) - mermaid.js (initMermaid() below) replaces
|
|
1180
|
+
the raw source text with a rendered <svg> in place, client-side.
|
|
1181
|
+
overflow-x: auto rather than shrinking the diagram, since a wide
|
|
1182
|
+
flowchart squeezed into the article's ~760px column becomes
|
|
1183
|
+
unreadable; centering only kicks in once the svg fits, via the
|
|
1184
|
+
inline-block + text-align combo below. */
|
|
1185
|
+
.wd-article > div.mermaid {
|
|
1186
|
+
margin: 1.25rem 0;
|
|
1187
|
+
padding: 1rem;
|
|
1188
|
+
overflow-x: auto;
|
|
1189
|
+
text-align: center;
|
|
1190
|
+
background: var(--wd-surface);
|
|
1191
|
+
border: 1px solid var(--wd-border);
|
|
1192
|
+
border-radius: 0.55rem;
|
|
1193
|
+
}
|
|
1194
|
+
.wd-article > div.mermaid svg {
|
|
1195
|
+
display: inline-block;
|
|
1196
|
+
max-width: 100%;
|
|
1197
|
+
}
|
|
1198
|
+
/* Only style links inside actual prose containers - excludes Card
|
|
1199
|
+
(which is itself an <a>) and the prev/next nav links. */
|
|
1200
|
+
.wd-article p a,
|
|
1201
|
+
.wd-article li a,
|
|
1202
|
+
.wd-article td a,
|
|
1203
|
+
.wd-article blockquote a {
|
|
1204
|
+
text-decoration: underline;
|
|
1205
|
+
text-decoration-color: color-mix(in srgb, var(--wd-primary) 40%, transparent);
|
|
1206
|
+
text-underline-offset: 2px;
|
|
1207
|
+
}
|
|
1208
|
+
.wd-article strong {
|
|
1209
|
+
font-weight: 600;
|
|
1210
|
+
}
|
|
1211
|
+
.wd-article > h2 a[href^="#"],
|
|
1212
|
+
.wd-article > h3 a[href^="#"],
|
|
1213
|
+
.wd-article > h4 a[href^="#"] {
|
|
1214
|
+
text-decoration: none;
|
|
1215
|
+
}
|
|
1216
|
+
.wd-heading-anchor {
|
|
1217
|
+
display: inline-block;
|
|
1218
|
+
margin-left: 0.5rem;
|
|
1219
|
+
color: var(--wd-text-muted);
|
|
1220
|
+
opacity: 0;
|
|
1221
|
+
text-decoration: none;
|
|
1222
|
+
font-weight: 400;
|
|
1223
|
+
transition: opacity 0.1s ease;
|
|
1224
|
+
}
|
|
1225
|
+
.wd-article > h2:hover .wd-heading-anchor,
|
|
1226
|
+
.wd-article > h3:hover .wd-heading-anchor,
|
|
1227
|
+
.wd-article > h4:hover .wd-heading-anchor,
|
|
1228
|
+
.wd-heading-anchor:focus {
|
|
1229
|
+
opacity: 1;
|
|
1230
|
+
}
|
|
1231
|
+
/* RequestExample/ResponseExample.astro's own panels, and the
|
|
1232
|
+
.wd-toc-col host initExamplePanels() (see the <script> above) docks
|
|
1233
|
+
them into on desktop - is:global (this whole style block already
|
|
1234
|
+
is) since .wd-toc-col itself is BaseLayout.astro's own markup, a
|
|
1235
|
+
completely different component than this one. Widens .wd-toc-col
|
|
1236
|
+
itself only while it actually contains a non-empty host - the same
|
|
1237
|
+
:has() technique ApiReferencePanel.astro's own identical comment
|
|
1238
|
+
already uses to widen it for its own markup, so a page with neither
|
|
1239
|
+
feature keeps the narrower 230px default from base.css. */
|
|
1240
|
+
.wd-toc-col:has(.wd-example-host:not(:empty)) {
|
|
1241
|
+
width: 340px;
|
|
1242
|
+
}
|
|
1243
|
+
.wd-example-host {
|
|
1244
|
+
position: sticky;
|
|
1245
|
+
top: var(--wd-topbar-offset, 5rem);
|
|
1246
|
+
max-height: calc(100vh - var(--wd-topbar-offset, 5rem));
|
|
1247
|
+
overflow-y: auto;
|
|
1248
|
+
padding: 1.5rem 0 1.5rem 0.5rem;
|
|
1249
|
+
}
|
|
1250
|
+
/* Spacing between multiple docked panels (e.g. Request above
|
|
1251
|
+
Response) is this wrapper's own job only once docked - inline
|
|
1252
|
+
(mobile, no-toc-column, or not-yet-docked), the wrapped
|
|
1253
|
+
.wd-codegroup's own default margin already spaces stacked panels
|
|
1254
|
+
out correctly, so adding a second margin here unscoped would just
|
|
1255
|
+
double it up. */
|
|
1256
|
+
.wd-example-host .wd-example-panel {
|
|
1257
|
+
margin-bottom: 1.25rem;
|
|
1258
|
+
}
|
|
1259
|
+
.wd-example-host .wd-example-panel:last-child {
|
|
1260
|
+
margin-bottom: 0;
|
|
1261
|
+
}
|
|
1262
|
+
.wd-example-host .wd-codegroup {
|
|
1263
|
+
margin: 0;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
.wd-example-host .astro-code {
|
|
1267
|
+
margin: 0;
|
|
1268
|
+
border-radius: 0 0 0.55rem 0.55rem;
|
|
1269
|
+
}
|
|
1270
|
+
</style>
|