@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.
Files changed (95) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +17 -0
  3. package/astro.config.mjs +419 -0
  4. package/bin/writedocs.js +73 -0
  5. package/package.json +79 -0
  6. package/src/assets/wd_watermark.png +0 -0
  7. package/src/assets/wd_watermark_dark.png +0 -0
  8. package/src/cli/build-auth.js +53 -0
  9. package/src/cli/build.js +40 -0
  10. package/src/cli/dev.js +12 -0
  11. package/src/cli/generate-api-pages.js +359 -0
  12. package/src/cli/init.js +81 -0
  13. package/src/cli/preflight.js +40 -0
  14. package/src/cli/run-astro.js +57 -0
  15. package/src/cli/run-pagefind.js +66 -0
  16. package/src/cli/write-redirects-file.js +80 -0
  17. package/src/components/Accordion.astro +164 -0
  18. package/src/components/AccordionGroup.astro +40 -0
  19. package/src/components/ApiLangSelect.astro +168 -0
  20. package/src/components/ApiPlayground.astro +281 -0
  21. package/src/components/ApiReferencePanel.astro +1754 -0
  22. package/src/components/ApiSchemaField.astro +54 -0
  23. package/src/components/AppIcon.astro +32 -0
  24. package/src/components/Badge.astro +128 -0
  25. package/src/components/Callout.astro +168 -0
  26. package/src/components/Card.astro +136 -0
  27. package/src/components/CardGroup.astro +20 -0
  28. package/src/components/CodeGroup.astro +184 -0
  29. package/src/components/CopyPageMenu.astro +246 -0
  30. package/src/components/Danger.astro +12 -0
  31. package/src/components/Expandable.astro +126 -0
  32. package/src/components/Frame.astro +102 -0
  33. package/src/components/Hint.astro +99 -0
  34. package/src/components/Icon.astro +70 -0
  35. package/src/components/Image.astro +147 -0
  36. package/src/components/Info.astro +12 -0
  37. package/src/components/Note.astro +12 -0
  38. package/src/components/Parameter.astro +119 -0
  39. package/src/components/RequestExample.astro +33 -0
  40. package/src/components/ResponseExample.astro +19 -0
  41. package/src/components/Searchbar.astro +117 -0
  42. package/src/components/Step.astro +10 -0
  43. package/src/components/Steps.astro +32 -0
  44. package/src/components/Tab.astro +9 -0
  45. package/src/components/Tabs.astro +52 -0
  46. package/src/components/Tip.astro +12 -0
  47. package/src/components/Video.astro +135 -0
  48. package/src/components/Warning.astro +12 -0
  49. package/src/components/index.ts +48 -0
  50. package/src/content.config.ts +223 -0
  51. package/src/layout/BaseLayout.astro +750 -0
  52. package/src/layout/components/AnalyticsScripts.astro +77 -0
  53. package/src/layout/components/AskAiWidget.astro +37 -0
  54. package/src/layout/components/Breadcrumbs.astro +97 -0
  55. package/src/layout/components/ImageZoom.astro +19 -0
  56. package/src/layout/components/MobileMenu.astro +200 -0
  57. package/src/layout/components/NavTree.astro +351 -0
  58. package/src/layout/components/SearchModal.astro +42 -0
  59. package/src/layout/components/Sidebar.astro +122 -0
  60. package/src/layout/components/SiteFooter.astro +85 -0
  61. package/src/layout/components/TableOfContents.astro +117 -0
  62. package/src/layout/components/TopBar.astro +311 -0
  63. package/src/layout/styles/banner.css +44 -0
  64. package/src/layout/styles/base.css +234 -0
  65. package/src/layout/styles/dropdown.css +133 -0
  66. package/src/layout/styles/footer.css +108 -0
  67. package/src/layout/styles/image-zoom.css +50 -0
  68. package/src/layout/styles/mobile-menu.css +258 -0
  69. package/src/layout/styles/search-modal.css +122 -0
  70. package/src/layout/styles/topbar.css +437 -0
  71. package/src/lib/config.ts +2131 -0
  72. package/src/lib/mdx-auto-hydrate.js +70 -0
  73. package/src/lib/mdx-inject-builtins.js +87 -0
  74. package/src/lib/mdx-substitute-variables.js +66 -0
  75. package/src/lib/mdx-title-anchor-ids.js +84 -0
  76. package/src/lib/mermaid-rehype.js +72 -0
  77. package/src/lib/openapi-render.ts +479 -0
  78. package/src/lib/shiki-code-block.js +102 -0
  79. package/src/lib/shiki-copy-button.js +45 -0
  80. package/src/lib/styles-asset-integration.js +210 -0
  81. package/src/lib/writedocs-temp-dir.js +93 -0
  82. package/src/pages/404.astro +62 -0
  83. package/src/pages/[...slug].astro +1270 -0
  84. package/src/pages/[...slug].md.ts +78 -0
  85. package/src/pages/llms-full.txt.ts +71 -0
  86. package/src/pages/llms.txt.ts +141 -0
  87. package/src/scripts/banner.ts +20 -0
  88. package/src/scripts/dropdowns.ts +61 -0
  89. package/src/scripts/image-zoom.ts +66 -0
  90. package/src/scripts/mobile-menu.ts +55 -0
  91. package/src/scripts/search.ts +155 -0
  92. package/src/scripts/sidebar-scroll.ts +65 -0
  93. package/src/scripts/theme-toggle.ts +35 -0
  94. package/src/scripts/topbar-offset.ts +141 -0
  95. 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>