@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,66 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import { spawn } from 'node:child_process';
4
+
5
+ /**
6
+ * Locates the installed pagefind package by walking up node_modules
7
+ * directories from packageRoot - the same lookup algorithm Node itself
8
+ * uses for bare-specifier resolution, just done by hand with fs instead
9
+ * of require.resolve()/import.meta.resolve(). This is necessary (rather
10
+ * than mirroring resolveAstroBin()'s simpler require.resolve()-based
11
+ * approach in run-astro.js) because pagefind's own package.json declares
12
+ * an "exports" map with only a "." entry scoped to the "import"
13
+ * condition: require.resolve('pagefind/package.json') is blocked
14
+ * outright (exports doesn't list a package.json subpath at all), and
15
+ * require.resolve('pagefind') also fails under CJS require() semantics
16
+ * (no "require"/"default" condition for it to satisfy).
17
+ * import.meta.resolve() would sidestep both, but needs Node 20.6+ -
18
+ * newer than this package's documented minimum (20.3.0, see engines in
19
+ * package.json). Walking node_modules ourselves avoids all of this - we
20
+ * only need pagefind's package.json (for its "bin" field) and the CLI
21
+ * script path, both plain file reads that "exports" doesn't restrict at
22
+ * all (only module *resolution* is subject to it).
23
+ */
24
+ function findPagefindDir(startDir) {
25
+ let dir = startDir;
26
+ while (true) {
27
+ const candidate = path.join(dir, 'node_modules', 'pagefind');
28
+ if (fs.existsSync(path.join(candidate, 'package.json'))) return candidate;
29
+ const parent = path.dirname(dir);
30
+ if (parent === dir) {
31
+ throw new Error(`Could not find "pagefind" in any node_modules directory above ${startDir}`);
32
+ }
33
+ dir = parent;
34
+ }
35
+ }
36
+
37
+ function resolvePagefindBin(packageRoot) {
38
+ const dir = findPagefindDir(packageRoot);
39
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
40
+ return path.join(dir, pkg.bin);
41
+ }
42
+
43
+ /**
44
+ * Indexes a built static site with Pagefind, writing the search bundle
45
+ * into <siteDir>/pagefind. Run as a postbuild step, after astro build has
46
+ * already produced the final HTML - Pagefind crawls that HTML directly
47
+ * rather than hooking into the Astro/Vite build pipeline, so search stays
48
+ * decoupled from whatever templating produced the pages. Only indexes
49
+ * elements carrying `data-pagefind-body` (see [...slug].astro's
50
+ * `.wd-article`) - without that scoping, Pagefind falls back to indexing
51
+ * every page's entire <body>, polluting every result with sidebar/topbar
52
+ * chrome text repeated on every page.
53
+ */
54
+ export function runPagefind(siteDir, { packageRoot }) {
55
+ const bin = resolvePagefindBin(packageRoot);
56
+ return new Promise((resolve, reject) => {
57
+ const child = spawn(process.execPath, [bin, '--site', siteDir, '--quiet'], {
58
+ stdio: 'inherit',
59
+ });
60
+ child.on('exit', (code) => {
61
+ if (code === 0) resolve();
62
+ else reject(new Error(`pagefind exited with code ${code}`));
63
+ });
64
+ child.on('error', reject);
65
+ });
66
+ }
@@ -0,0 +1,80 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ // Astro's own `output: 'static'` redirects (both writedocs.json's `redirects`
5
+ // feature and the automatic "/" -> first-nav-page redirect in
6
+ // [...slug].astro) are never real server/edge redirects - with no adapter
7
+ // installed, Astro can only ever emit a client-side
8
+ // `<meta http-equiv="refresh">` page (see redirectSchema's own comment in
9
+ // lib/config.ts for why `permanent`/status-code isn't even offered as a
10
+ // config option). That page is a real, working redirect, but it's a flash
11
+ // of unstyled content before the browser honors the meta-refresh - visibly
12
+ // bad on a deployed site, and exactly what a reader lands on for a second
13
+ // or more depending on network/host latency.
14
+ //
15
+ // Cloudflare Pages and Netlify both read a `_redirects` file at the root
16
+ // of the deployed output and turn each line into a real edge-level
17
+ // redirect - no page ever renders, no flash. This step generates that file
18
+ // as a purely additive improvement: hosts that honor `_redirects` get an
19
+ // instant real redirect, and every other static host (S3, GitHub Pages,
20
+ // etc.) falls back to the meta-refresh page Astro already generates, which
21
+ // keeps working exactly as before. Neither this file nor its absence ever
22
+ // changes what Astro itself builds.
23
+ // `contentDir` rather than a pre-loaded config object: this runs from
24
+ // build.js, invoked by plain `node`, not through Astro/Vite's TS
25
+ // transform - loadDocsConfig() (lib/config.ts) can't be imported here the
26
+ // way astro.config.mjs and every .astro file import it. writedocs.json has
27
+ // already been through preflightCheck() (valid JSON) and Astro's own
28
+ // loadDocsConfig() call inside astro.config.mjs (schema-valid, or the
29
+ // build above would already have failed) by the time this runs, so a
30
+ // second, un-validated JSON.parse() here is safe - this only ever reads
31
+ // the one `redirects` array back out, nothing that needs zod's defaults
32
+ // or transforms.
33
+ export function writeRedirectsFile(distDir, contentDir) {
34
+ const writedocsJson = JSON.parse(fs.readFileSync(path.join(contentDir, 'writedocs.json'), 'utf8'));
35
+ const redirects = writedocsJson.redirects ?? [];
36
+ const lines = [];
37
+
38
+ // writedocs.json's own `redirects` array - one line per entry, always 301
39
+ // (there's nowhere else in this project to source a status code from;
40
+ // see redirectSchema's comment in lib/config.ts on why the schema itself
41
+ // has no field for one - a redirect a site author deliberately
42
+ // configured is safe to treat as permanent).
43
+ for (const { source, destination } of redirects) {
44
+ lines.push(`${source} ${destination} 301`);
45
+ }
46
+
47
+ // The automatic root "/" redirect (see the `rootRedirect` block in
48
+ // [...slug].astro's getStaticPaths()) isn't reachable from here without
49
+ // re-deriving the same first-nav-page resolution Astro already did -
50
+ // instead, read it straight back out of the meta-refresh tag in the
51
+ // already-built dist/index.html. If writedocs.json already defines its own
52
+ // `/` redirect above, that one wins (first match wins in `_redirects`,
53
+ // and it's already listed first) - skip the automatic one entirely
54
+ // rather than emit a second, unreachable rule for the same path.
55
+ const hasExplicitRootRedirect = redirects.some((r) => r.source === '/');
56
+ const indexHtmlPath = path.join(distDir, 'index.html');
57
+ if (!hasExplicitRootRedirect && fs.existsSync(indexHtmlPath)) {
58
+ const html = fs.readFileSync(indexHtmlPath, 'utf8');
59
+ const match = html.match(/<meta http-equiv="refresh" content="\d+;\s*url=([^"]+)"/i);
60
+ if (match) lines.push(`/ ${match[1]} 301`);
61
+ }
62
+
63
+ if (lines.length === 0) return;
64
+
65
+ const generated = [
66
+ '# Generated by writedocs (see src/cli/write-redirects-file.js).',
67
+ '# Gives Cloudflare Pages / Netlify a real edge redirect for the same',
68
+ "# paths Astro's own static-mode <meta http-equiv=\"refresh\"> pages",
69
+ '# already cover - other static hosts keep using those pages as-is.',
70
+ ...lines,
71
+ '',
72
+ ].join('\n');
73
+
74
+ const redirectsPath = path.join(distDir, '_redirects');
75
+ // A hand-written public/_redirects is already copied into dist by Astro
76
+ // by this point - preserve it (and its priority: first match wins) by
77
+ // appending after it rather than overwriting.
78
+ const existing = fs.existsSync(redirectsPath) ? fs.readFileSync(redirectsPath, 'utf8').replace(/\s*$/, '\n\n') : '';
79
+ fs.writeFileSync(redirectsPath, existing + generated);
80
+ }
@@ -0,0 +1,164 @@
1
+ ---
2
+ import AppIcon from "./AppIcon.astro";
3
+
4
+ interface Props {
5
+ title: string;
6
+ icon?: string;
7
+ // Set automatically by remarkTitleAnchorIds (src/lib/mdx-title-anchor-ids.js)
8
+ // for every <Accordion title="..."> it finds in an .mdx file's own JSX,
9
+ // already deduped (-2, -3, ...) against every other Accordion/Callout
10
+ // title on that same page - see that plugin's own comment for why the
11
+ // dedup has to happen there, one level up, rather than here. Not meant
12
+ // to be set by hand in content; the slugify(title) fallback below covers
13
+ // every case this doesn't reach (plain .md content, a dynamic
14
+ // title={expr}, or this component used directly from a .astro file).
15
+ _titleId?: string;
16
+ }
17
+ const { title, icon, _titleId } = Astro.props as Props;
18
+ // Astro's own heading-id slugging (github-slugger, via the markdown
19
+ // pipeline) never sees this - `title` is a component prop, not a markdown
20
+ // heading, so it gets no id unless we make one ourselves. Same slugify
21
+ // Callout.astro's own fallback uses.
22
+ function slugify(value: string): string {
23
+ return value
24
+ .toLowerCase()
25
+ .replace(/[^a-z0-9]+/g, "-")
26
+ .replace(/^-+|-+$/g, "");
27
+ }
28
+ const titleId = _titleId ?? slugify(title);
29
+ ---
30
+
31
+ <details class="wd-accordion">
32
+ <summary>
33
+ <span class="wd-accordion-heading">
34
+ {icon && <AppIcon icon={icon} class="wd-accordion-icon" />}
35
+ <span class="wd-accordion-title" id={titleId}>
36
+ {title}
37
+ <a class="wd-heading-anchor" href={`#${titleId}`} aria-label="Link to this section"> # </a>
38
+ </span>
39
+ </span>
40
+ {
41
+ /* Same "hide the native marker, draw our own instead" approach as
42
+ the sidebar's own collapsible groups (NavTree.astro's
43
+ .wd-nav-chevron) - see the ::-webkit-details-marker / ::marker
44
+ rules below. Drawn pointing down already (a native summary
45
+ marker instead defaults to pointing right, then rotating open),
46
+ since that's the resting/idle direction the reference design
47
+ this was built from actually uses - it flips 180deg on [open]
48
+ instead, in the CSS below. */
49
+ }
50
+ <svg class="wd-accordion-chevron" width="11" height="11" viewBox="0 0 10 10" aria-hidden="true">
51
+ <path
52
+ d="M2 3.5L5 6.5L8 3.5"
53
+ stroke="currentColor"
54
+ stroke-width="1.4"
55
+ fill="none"
56
+ stroke-linecap="round"
57
+ stroke-linejoin="round"></path>
58
+ </svg>
59
+ </summary>
60
+ <div class="wd-accordion-body"><slot /></div>
61
+ </details>
62
+ <script>
63
+ // The anchor link lives inside <summary> - a real <a>, but nested
64
+ // inside the element <details> itself listens on for its native
65
+ // open/close toggle, so without this a click on "#" would both
66
+ // navigate/copy the link *and* toggle the accordion as an unrelated
67
+ // side effect of where it happens to sit in the DOM. stopPropagation()
68
+ // only, not preventDefault() - the link itself should still navigate
69
+ // normally, just not also bubble up into summary's own click handler.
70
+ function initAccordionAnchors(root: ParentNode) {
71
+ root.querySelectorAll<HTMLAnchorElement>(".wd-accordion > summary .wd-heading-anchor").forEach((a) => {
72
+ if (a.dataset.wdInit) return;
73
+ a.dataset.wdInit = "true";
74
+ a.addEventListener("click", (e) => e.stopPropagation());
75
+ });
76
+ }
77
+ initAccordionAnchors(document);
78
+ document.addEventListener("astro:page-load", () => initAccordionAnchors(document));
79
+ </script>
80
+ <style>
81
+ .wd-accordion {
82
+ border: 1px solid var(--wd-border);
83
+ border-radius: 0.6rem;
84
+ margin: 0.6rem 0;
85
+ overflow: hidden;
86
+ }
87
+ .wd-accordion summary {
88
+ display: flex;
89
+ align-items: center;
90
+ justify-content: space-between;
91
+ gap: 0.75rem;
92
+ padding: 0.85rem 1.1rem;
93
+ cursor: pointer;
94
+ list-style: none;
95
+ }
96
+ /* Firefox/Chrome use ::marker for the default disclosure triangle,
97
+ Safari still needs the older, WebKit-specific pseudo-element -
98
+ both zeroed out so only the svg above ever shows. */
99
+ .wd-accordion summary::-webkit-details-marker {
100
+ display: none;
101
+ }
102
+ .wd-accordion summary::marker {
103
+ content: "";
104
+ }
105
+ .wd-accordion summary:hover {
106
+ background: var(--wd-surface);
107
+ }
108
+ .wd-accordion-heading {
109
+ display: flex;
110
+ align-items: center;
111
+ gap: 0.6rem;
112
+ min-width: 0;
113
+ }
114
+ /* :global() is load-bearing: AppIcon.astro renders this span/svg, not
115
+ Accordion.astro, so it never carries Accordion's own scope
116
+ attribute - without :global() these selectors compile to something
117
+ that can never match. Same fix Card.astro/Callout.astro's own icon
118
+ rules already needed for the identical reason. */
119
+ :global(span.wd-accordion-icon) {
120
+ display: block;
121
+ font-size: 1rem;
122
+ flex-shrink: 0;
123
+ }
124
+ :global(svg.wd-accordion-icon) {
125
+ display: block;
126
+ width: 1rem;
127
+ height: 1rem;
128
+ flex-shrink: 0;
129
+ color: var(--wd-text-muted);
130
+ }
131
+ .wd-accordion-title {
132
+ font-weight: 700;
133
+ color: var(--wd-text);
134
+ scroll-margin-top: calc(var(--wd-topbar-offset, 5rem) + 1rem);
135
+ }
136
+ /* .wd-heading-anchor's base look (color, opacity: 0, transition) comes
137
+ from [...slug].astro's own <style is:global> block - see
138
+ Callout.astro's identical comment on its own title anchor. Only the
139
+ hover trigger needs to live here, and - since both
140
+ .wd-accordion-title and the anchor inside it are rendered directly
141
+ by this component's own template - no :global() is needed for it
142
+ either. */
143
+ .wd-accordion-title:hover .wd-heading-anchor {
144
+ opacity: 1;
145
+ }
146
+ .wd-accordion-chevron {
147
+ flex-shrink: 0;
148
+ color: var(--wd-text-muted);
149
+ transition: transform 0.15s ease;
150
+ }
151
+ .wd-accordion[open] > summary .wd-accordion-chevron {
152
+ transform: rotate(180deg);
153
+ }
154
+ .wd-accordion-body {
155
+ padding: 1.1rem 1rem;
156
+ color: var(--wd-text-muted);
157
+ }
158
+ .wd-accordion-body :global(p:first-child) {
159
+ margin-top: 0;
160
+ }
161
+ .wd-accordion-body :global(p:last-child) {
162
+ margin-bottom: 0;
163
+ }
164
+ </style>
@@ -0,0 +1,40 @@
1
+ <div class="wd-accordion-group">
2
+ <slot />
3
+ </div>
4
+ <style is:global>
5
+ /* Same "one shared border, not N separate ones" pattern CodeGroup.astro
6
+ already uses for its own children (see that file's comment) -
7
+ overflow: hidden here is what actually clips the first/last
8
+ accordion's corners into this container's own radius, since every
9
+ .wd-accordion inside loses its own border-radius below. */
10
+ .wd-accordion-group {
11
+ margin: 1.25rem 0;
12
+ border: 1px solid var(--wd-border);
13
+ border-radius: 0.6rem;
14
+ overflow: hidden;
15
+ }
16
+ /* Overrides Accordion.astro's own scoped border/radius/margin - needs
17
+ is:global (this whole <style> block already is) since .wd-accordion
18
+ carries Accordion.astro's own scope attribute, not this file's. */
19
+ .wd-accordion-group > .wd-accordion {
20
+ margin: 0;
21
+ border: none;
22
+ border-radius: 0;
23
+ }
24
+ /* Divides one accordion from the next - not before the first one,
25
+ the group's own outer border above already closes that edge off.
26
+ ~ (general sibling), not + (strictly adjacent): Accordion.astro's
27
+ own per-instance <script> tag is small enough that Astro/Vite
28
+ inlines it directly in the page body rather than extracting it to
29
+ an external bundle - it lands as a literal sibling element between
30
+ whichever pair of accordions happens to trigger the (deduped,
31
+ so it only ever appears once) inline, breaking + for exactly that
32
+ one pair while every other pair still matched. ~ tolerates
33
+ whatever non-.wd-accordion siblings end up between two accordions
34
+ for reasons like this, matching any accordion preceded by another
35
+ one anywhere earlier among its siblings instead of requiring zero
36
+ gap. */
37
+ .wd-accordion-group > .wd-accordion ~ .wd-accordion {
38
+ border-top: 1px solid var(--wd-border);
39
+ }
40
+ </style>
@@ -0,0 +1,168 @@
1
+ ---
2
+ // Icon + label + checkmark language dropdown used by both of
3
+ // ApiReferencePanel's language pickers - the sidebar Request card's, and
4
+ // the Try-it modal's own copy (see rolePrefix below). Reuses the site's
5
+ // shared .wd-dropdown/.wd-dropdown-trigger/.wd-dropdown-menu/
6
+ // .wd-dropdown-menu-panel pattern (dropdown.css + initDropdowns() in
7
+ // src/scripts/dropdowns.ts, already wired globally against `document` by
8
+ // TopBar.astro/MobileMenu.astro on load and on every astro:page-load) so
9
+ // open/close/outside-click/Escape all come free here too, the same way
10
+ // CopyPageMenu.astro's own dropdown does - no separate wiring needed for
11
+ // any of that in this file or in ApiReferencePanel's own <script>.
12
+ //
13
+ // A hidden native <select> (data-role={rolePrefix}-select) stays the
14
+ // actual source of truth for "which language is selected" - every
15
+ // existing 'change' listener in ApiReferencePanel.astro's <script>
16
+ // (panel-toggle logic, live-preview refresh) was already written against
17
+ // that select, so ApiReferencePanel's own initLangDropdown() only ever
18
+ // *drives* it (sets .value, dispatches 'change') from a click on one of
19
+ // the buttons below, rather than replacing that wiring.
20
+ import { Icon } from 'astro-icon/components';
21
+
22
+ interface LangOption {
23
+ id: string;
24
+ label: string;
25
+ icon: string; // an astro-icon/Iconify id, e.g. "mdi:language-python"
26
+ }
27
+ interface Props {
28
+ options: LangOption[];
29
+ rolePrefix: string; // "lang" (sidebar) or "modal-lang" (Try-it modal)
30
+ ariaLabel: string;
31
+ }
32
+ const { options, rolePrefix, ariaLabel } = Astro.props as Props;
33
+ const first = options[0];
34
+ ---
35
+
36
+ {first && (
37
+ <div class="wd-dropdown wd-api-lang-dropdown">
38
+ <select class="wd-api-lang-native-select" data-role={`${rolePrefix}-select`} tabindex="-1" aria-hidden="true">
39
+ {options.map((o) => <option value={o.id}>{o.label}</option>)}
40
+ </select>
41
+ <button
42
+ type="button"
43
+ class="wd-dropdown-trigger wd-api-lang-trigger"
44
+ data-role={`${rolePrefix}-trigger`}
45
+ aria-haspopup="true"
46
+ aria-expanded="false"
47
+ aria-label={ariaLabel}
48
+ >
49
+ <Icon name={first.icon} class="wd-api-lang-icon" />
50
+ <span class="wd-api-lang-label">{first.label}</span>
51
+ <svg class="wd-dropdown-caret" width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
52
+ <path d="M2 3.5L5 6.5L8 3.5" stroke="currentColor" stroke-width="1.4" fill="none" stroke-linecap="round" stroke-linejoin="round" />
53
+ </svg>
54
+ </button>
55
+ <div class="wd-dropdown-menu wd-api-lang-menu" role="menu">
56
+ <div class="wd-dropdown-menu-panel wd-api-lang-menu-panel">
57
+ {options.map((o, i) => (
58
+ <button
59
+ type="button"
60
+ class={`wd-api-lang-item${i === 0 ? ' active' : ''}`}
61
+ data-role={`${rolePrefix}-item`}
62
+ data-value={o.id}
63
+ >
64
+ <Icon name={o.icon} class="wd-api-lang-icon" />
65
+ <span class="wd-api-lang-label">{o.label}</span>
66
+ <svg class="wd-api-lang-check" width="12" height="12" viewBox="0 0 12 12" aria-hidden="true">
67
+ <path d="M2.5 6.2L5 8.7L9.5 3.5" stroke="currentColor" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round" />
68
+ </svg>
69
+ </button>
70
+ ))}
71
+ </div>
72
+ </div>
73
+ </div>
74
+ )}
75
+
76
+ <style>
77
+ .wd-api-lang-native-select {
78
+ position: absolute;
79
+ width: 1px;
80
+ height: 1px;
81
+ padding: 0;
82
+ margin: -1px;
83
+ overflow: hidden;
84
+ clip: rect(0, 0, 0, 0);
85
+ white-space: nowrap;
86
+ border: 0;
87
+ }
88
+ /* Overrides the shared .wd-dropdown-trigger base (dropdown.css, sized
89
+ for the topbar's larger switchers) down to the same small pill this
90
+ control replaced (.wd-api-select-wrap in ApiReferencePanel.astro's
91
+ own CSS) - scoped-style specificity wins this over the plain global
92
+ class rule regardless of import order. */
93
+ .wd-api-lang-trigger {
94
+ padding: 0.2rem 0.55rem;
95
+ border-radius: 999px;
96
+ border: 1px solid var(--wd-border);
97
+ background: var(--wd-background);
98
+ font-size: 0.75rem;
99
+ color: var(--wd-text);
100
+ }
101
+ .wd-api-lang-trigger:hover {
102
+ color: var(--wd-text);
103
+ border-color: var(--wd-text-muted);
104
+ }
105
+ .wd-api-lang-icon {
106
+ width: 0.95rem;
107
+ height: 0.95rem;
108
+ flex-shrink: 0;
109
+ color: var(--wd-text-muted);
110
+ }
111
+ /* Right-aligned, same as CopyPageMenu's own .wd-copy-page-menu override
112
+ - the default .wd-dropdown-menu (dropdown.css) is left: 0, correct
113
+ for the topbar's own left-anchored triggers but not for a trigger
114
+ that sits at the right edge of its own card. */
115
+ .wd-api-lang-menu {
116
+ left: auto;
117
+ right: 0;
118
+ }
119
+ .wd-api-lang-menu-panel {
120
+ min-width: 11rem;
121
+ display: flex;
122
+ flex-direction: column;
123
+ gap: 0.05rem;
124
+ }
125
+ .wd-api-lang-item {
126
+ display: flex;
127
+ align-items: center;
128
+ gap: 0.55rem;
129
+ width: 100%;
130
+ padding: 0.4rem 0.6rem;
131
+ border: none;
132
+ border-radius: 0.35rem;
133
+ background: none;
134
+ font-family: inherit;
135
+ font-size: 0.82rem;
136
+ color: var(--wd-text);
137
+ text-align: left;
138
+ cursor: pointer;
139
+ }
140
+ .wd-api-lang-item:hover {
141
+ background: var(--wd-surface);
142
+ }
143
+ .wd-api-lang-item .wd-api-lang-icon {
144
+ color: var(--wd-text-muted);
145
+ }
146
+ .wd-api-lang-item .wd-api-lang-label {
147
+ flex: 1;
148
+ }
149
+ .wd-api-lang-check {
150
+ flex-shrink: 0;
151
+ color: var(--wd-primary);
152
+ opacity: 0;
153
+ }
154
+ /* Same active-item convention as .wd-dropdown-menu a.active
155
+ (dropdown.css) - tinted text/background in the primary color, plus
156
+ the checkmark fading in, kept in sync with the hidden <select> by
157
+ ApiReferencePanel.astro's initLangDropdown(). */
158
+ .wd-api-lang-item.active {
159
+ color: var(--wd-primary);
160
+ background: color-mix(in srgb, var(--wd-primary) 10%, transparent);
161
+ }
162
+ .wd-api-lang-item.active .wd-api-lang-icon {
163
+ color: var(--wd-primary);
164
+ }
165
+ .wd-api-lang-item.active .wd-api-lang-check {
166
+ opacity: 1;
167
+ }
168
+ </style>