@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,117 @@
1
+ ---
2
+ export interface TocHeading {
3
+ depth: number;
4
+ slug: string;
5
+ text: string;
6
+ }
7
+
8
+ interface Props {
9
+ headings: TocHeading[];
10
+ }
11
+
12
+ const { headings } = Astro.props as Props;
13
+ ---
14
+ {headings.length > 0 && (
15
+ <nav class="wd-toc" aria-label="On this page">
16
+ <div class="wd-toc-title">On this page</div>
17
+ <ul>
18
+ {headings.map((h) => (
19
+ <li class={`wd-toc-item wd-toc-level-${h.depth}`}>
20
+ <a href={`#${h.slug}`} data-toc-slug={h.slug}>{h.text}</a>
21
+ </li>
22
+ ))}
23
+ </ul>
24
+ </nav>
25
+ )}
26
+
27
+ <script>
28
+ function initToc(root: ParentNode) {
29
+ const tocLinks = Array.from(root.querySelectorAll<HTMLAnchorElement>('.wd-toc a[data-toc-slug]'));
30
+ if (tocLinks.length === 0) return;
31
+
32
+ const headingEls = tocLinks
33
+ .map((link) => document.getElementById(link.dataset.tocSlug ?? ''))
34
+ .filter((el): el is HTMLElement => el !== null);
35
+
36
+ if (headingEls.length === 0) return;
37
+
38
+ const linkBySlug = new Map(tocLinks.map((link) => [link.dataset.tocSlug, link]));
39
+
40
+ const setActive = (slug: string | null) => {
41
+ tocLinks.forEach((link) => link.classList.remove('active'));
42
+ if (slug) linkBySlug.get(slug)?.classList.add('active');
43
+ };
44
+
45
+ const observer = new IntersectionObserver(
46
+ (entries) => {
47
+ // Pick the topmost heading currently intersecting the "active band"
48
+ // near the top of the viewport, rather than whichever fires last -
49
+ // IntersectionObserver doesn't guarantee entries arrive in DOM order.
50
+ const visible = entries
51
+ .filter((e) => e.isIntersecting)
52
+ .map((e) => e.target as HTMLElement);
53
+ if (visible.length > 0) {
54
+ const topMost = visible.reduce((a, b) =>
55
+ a.getBoundingClientRect().top < b.getBoundingClientRect().top ? a : b
56
+ );
57
+ setActive(topMost.id);
58
+ }
59
+ },
60
+ { rootMargin: '0px 0px -70% 0px', threshold: 1.0 }
61
+ );
62
+
63
+ headingEls.forEach((el) => observer.observe(el));
64
+ }
65
+
66
+ initToc(document);
67
+ document.addEventListener('astro:page-load', () => initToc(document));
68
+ </script>
69
+
70
+ <style>
71
+ /* top/max-height deliberately match Sidebar.astro's own .wd-sidebar
72
+ values exactly (see its comment) - both driven by --wd-topbar-offset
73
+ (src/scripts/topbar-offset.ts), the topbar's real measured height. */
74
+ .wd-toc {
75
+ position: sticky;
76
+ top: var(--wd-topbar-offset, 5rem);
77
+ padding: 1.5rem 1rem 1.5rem 1.5rem;
78
+ font-size: 0.85rem;
79
+ max-height: calc(100vh - var(--wd-topbar-offset, 5rem));
80
+ overflow-y: auto;
81
+ }
82
+ .wd-toc-title {
83
+ font-size: 0.75rem;
84
+ font-weight: 600;
85
+ text-transform: uppercase;
86
+ letter-spacing: 0.04em;
87
+ color: var(--wd-text-muted);
88
+ margin-bottom: 0.6rem;
89
+ }
90
+ .wd-toc ul {
91
+ list-style: none;
92
+ margin: 0;
93
+ padding: 0;
94
+ border-left: 1px solid var(--wd-border);
95
+ }
96
+ .wd-toc-item a {
97
+ display: block;
98
+ padding: 0.3rem 0 0.3rem 0.85rem;
99
+ margin-left: -1px;
100
+ border-left: 2px solid transparent;
101
+ text-decoration: none;
102
+ color: var(--wd-text-muted);
103
+ line-height: 1.4;
104
+ }
105
+ .wd-toc-level-3 a {
106
+ padding-left: 1.6rem;
107
+ font-size: 0.82rem;
108
+ }
109
+ .wd-toc-item a:hover {
110
+ color: var(--wd-text);
111
+ }
112
+ .wd-toc-item a.active {
113
+ color: var(--wd-primary);
114
+ border-left-color: var(--wd-primary);
115
+ font-weight: 500;
116
+ }
117
+ </style>
@@ -0,0 +1,311 @@
1
+ ---
2
+ // The site's persistent header: brand, version/language/product switchers,
3
+ // tabs + global dropdowns (second row), writedocs.json's `topbar.links` (as a
4
+ // plain row above 860px, collapsed into a single ellipsis dropdown below
5
+ // it), the search trigger, and the theme toggle. Also renders the
6
+ // hamburger button that opens MobileMenu.astro's own drawer - the button
7
+ // itself lives here (it's positioned as the first child of
8
+ // .wd-topbar-inner, part of this row), the panel it opens is a separate
9
+ // component/file.
10
+ //
11
+ // Split out of BaseLayout.astro, which had grown to ~2200 lines covering
12
+ // several unrelated concerns in one file - see that component's own
13
+ // comment for the fuller rationale.
14
+ import { isExternalHref, type DocsConfig, type Selector, type GlobalDropdownView } from '../../lib/config';
15
+ import AppIcon from '../../components/AppIcon.astro';
16
+ import '../styles/dropdown.css';
17
+ import '../styles/topbar.css';
18
+
19
+ interface Props {
20
+ config: DocsConfig;
21
+ // One Selector per level of the active page's navigation path (tabs,
22
+ // versions, languages, products, or a dropdown used as a nested level) -
23
+ // see resolveSections()/buildSelectors() in lib/config.ts.
24
+ selectors: Selector[];
25
+ // The always-visible topbar dropdown list from navigation.global.dropdowns.
26
+ globalDropdowns: GlobalDropdownView[];
27
+ // Whether this page would show a sidebar at all (mode === 'default' or
28
+ // 'wide') - gates whether the hamburger toggle renders, same condition
29
+ // MobileMenu.astro itself is gated on for the panel it opens.
30
+ showSidebarCol: boolean;
31
+ logoLight?: string;
32
+ logoDark?: string;
33
+ brandLabel?: string;
34
+ }
35
+ const { config, selectors, globalDropdowns, showSidebarCol, logoLight, logoDark, brandLabel } = Astro.props as Props;
36
+
37
+ // The topbar is two stacked rows, matching Mintlify's own layout: a top
38
+ // row with the brand and the "pick one" switchers (versions/languages/
39
+ // products, or a dropdown used as a nested path segment) on the right,
40
+ // and a second row underneath for tabs plus the always-visible global
41
+ // dropdowns - both of which behave like a row of labeled links rather
42
+ // than a single current-value switcher.
43
+ //
44
+ // A dropdown container nested directly inside a tab (writedocs.json's "API
45
+ // Reference" tab having a `dropdowns` array instead of `pages`) reads as
46
+ // part of that tab's own local navigation, not as an independent
47
+ // top-level "pick one" choice - it's rendered by turning the *tab's own
48
+ // pill* into a dropdown-trigger (see SelectorOption.dropdown /
49
+ // buildSelectors() in lib/config.ts, and the tabSelectors.map below),
50
+ // rather than as a separate control. So unlike a root-level dropdown/
51
+ // version/language/product selector (which does surface as a brand-row
52
+ // switcher), this one is dropped from switcherSelectors entirely -
53
+ // `selectors` is in root-to-leaf path order, so "nested directly inside a
54
+ // tab" just means the previous entry in the array is the 'tab' selector.
55
+ const switcherSelectors = selectors.filter(
56
+ (sel, i) => sel.kind !== 'tab' && !(sel.kind === 'dropdown' && selectors[i - 1]?.kind === 'tab')
57
+ );
58
+ const tabSelectors = selectors.filter((sel) => sel.kind === 'tab');
59
+ const showSecondRow = tabSelectors.length > 0 || globalDropdowns.length > 0;
60
+ ---
61
+ <header class="wd-topbar">
62
+ <div class="wd-topbar-row wd-topbar-row-brand">
63
+ <div class="wd-topbar-inner">
64
+ {/* Only rendered on pages that would show a sidebar at all
65
+ (showSidebarCol - same condition MobileMenu.astro's own
66
+ rendering is gated on). Hidden entirely above 860px via CSS
67
+ (.wd-mobile-menu-toggle's own media query in topbar.css), not
68
+ JS - so there's no flash-of-hamburger on a desktop-width load. */}
69
+ {showSidebarCol && (
70
+ <button
71
+ type="button"
72
+ class="wd-mobile-menu-toggle"
73
+ aria-label="Open menu"
74
+ aria-expanded="false"
75
+ aria-controls="wd-mobile-menu"
76
+ data-mobile-menu-trigger
77
+ >
78
+ <svg class="wd-mobile-menu-icon wd-mobile-menu-icon-bars" width="18" height="18" viewBox="0 0 18 18" aria-hidden="true">
79
+ <path d="M2.5 5H15.5M2.5 9H15.5M2.5 13H15.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
80
+ </svg>
81
+ <svg class="wd-mobile-menu-icon wd-mobile-menu-icon-close" width="18" height="18" viewBox="0 0 18 18" aria-hidden="true">
82
+ <path d="M4 4L14 14M14 4L4 14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
83
+ </svg>
84
+ </button>
85
+ )}
86
+ <a class="wd-topbar-brand" href="/">
87
+ {logoLight && <img src={logoLight} alt={config.name} height="24" class="wd-logo-light" />}
88
+ {logoDark && <img src={logoDark} alt={config.name} height="24" class="wd-logo-dark" />}
89
+ {brandLabel && <span>{brandLabel}</span>}
90
+ </a>
91
+
92
+ {switcherSelectors.length > 0 && (
93
+ <nav class="wd-topbar-switchers" aria-label="Version, language and product selection">
94
+ {switcherSelectors.map((sel) => {
95
+ // versions / languages / products, and a dropdown used as a
96
+ // nested path segment, all render as a single switcher: the
97
+ // trigger shows the *current* option, the menu lists the
98
+ // alternatives - unlike tabs, where every option is its own
99
+ // always-visible pill in the row below.
100
+ const current = sel.options.find((o) => o.active) ?? sel.options[0];
101
+ return (
102
+ <div class="wd-dropdown" data-selector-kind={sel.kind}>
103
+ <button type="button" class="wd-dropdown-trigger wd-switcher-trigger" aria-haspopup="true" aria-expanded="false">
104
+ <AppIcon icon={current.icon} class="wd-tab-icon" />
105
+ {current.label}
106
+ {current.tag && <span class="wd-selector-tag">{current.tag}</span>}
107
+ <svg class="wd-dropdown-caret" width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
108
+ <path d="M2 3.5L5 6.5L8 3.5" stroke="currentColor" stroke-width="1.4" fill="none" stroke-linecap="round" stroke-linejoin="round" />
109
+ </svg>
110
+ </button>
111
+ <div class="wd-dropdown-menu" role="menu">
112
+ <div class="wd-dropdown-menu-panel">
113
+ {sel.options.map((opt) => (
114
+ <a
115
+ href={opt.href}
116
+ role="menuitem"
117
+ class={opt.active ? 'active' : ''}
118
+ target={isExternalHref(opt.href) ? '_blank' : undefined}
119
+ rel={isExternalHref(opt.href) ? 'noopener noreferrer' : undefined}
120
+ >
121
+ <AppIcon icon={opt.icon} class="wd-tab-icon" />
122
+ {opt.label}
123
+ {opt.tag && <span class="wd-selector-tag">{opt.tag}</span>}
124
+ </a>
125
+ ))}
126
+ </div>
127
+ </div>
128
+ </div>
129
+ );
130
+ })}
131
+ </nav>
132
+ )}
133
+
134
+ <nav class="wd-topbar-links">
135
+ {config.topbar.links.map((link) => (
136
+ <a
137
+ href={link.href}
138
+ target={isExternalHref(link.href) ? '_blank' : undefined}
139
+ rel={isExternalHref(link.href) ? 'noopener noreferrer' : undefined}
140
+ aria-label={!link.label ? link.icon : undefined}
141
+ >
142
+ <AppIcon icon={link.icon} class="wd-tab-icon" />
143
+ {link.label}
144
+ </a>
145
+ ))}
146
+ </nav>
147
+
148
+ {/* Below 860px, .wd-topbar-links above is hidden (see topbar.css)
149
+ and this takes its place instead - same link set, same
150
+ writedocs.json config.topbar.links, just collapsed behind a single
151
+ icon button rather than an inline row of text links that would
152
+ otherwise wrap and eat vertical space next to the brand. Reuses
153
+ the existing .wd-dropdown/.wd-dropdown-trigger/.wd-dropdown-menu/
154
+ .wd-dropdown-menu-panel pattern (see the switcher/tab dropdowns
155
+ above) rather than inventing a new toggle mechanism -
156
+ initDropdowns() (src/scripts/dropdowns.ts, wired below) already
157
+ wires up every .wd-dropdown on the page generically, so this
158
+ needs no JS of its own. */}
159
+ {config.topbar.links.length > 0 && (
160
+ <div class="wd-dropdown wd-topbar-links-mobile" data-selector-kind="topbar-links">
161
+ <button
162
+ type="button"
163
+ class="wd-dropdown-trigger wd-topbar-links-toggle"
164
+ aria-haspopup="true"
165
+ aria-expanded="false"
166
+ aria-label="More links"
167
+ >
168
+ <svg width="4" height="16" viewBox="0 0 4 16" aria-hidden="true">
169
+ <circle cx="2" cy="2" r="1.7" fill="currentColor" />
170
+ <circle cx="2" cy="8" r="1.7" fill="currentColor" />
171
+ <circle cx="2" cy="14" r="1.7" fill="currentColor" />
172
+ </svg>
173
+ </button>
174
+ <div class="wd-dropdown-menu" role="menu">
175
+ <div class="wd-dropdown-menu-panel">
176
+ {config.topbar.links.map((link) => (
177
+ <a
178
+ href={link.href}
179
+ role="menuitem"
180
+ target={isExternalHref(link.href) ? '_blank' : undefined}
181
+ rel={isExternalHref(link.href) ? 'noopener noreferrer' : undefined}
182
+ aria-label={!link.label ? link.icon : undefined}
183
+ >
184
+ <AppIcon icon={link.icon} class="wd-tab-icon" />
185
+ {link.label}
186
+ </a>
187
+ ))}
188
+ </div>
189
+ </div>
190
+ </div>
191
+ )}
192
+
193
+ <button type="button" class="wd-search-trigger" aria-label="Search" data-search-trigger>
194
+ <svg class="wd-search-icon" width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">
195
+ <circle cx="6" cy="6" r="4.5" stroke="currentColor" stroke-width="1.4" fill="none" />
196
+ <path d="M9.5 9.5L13 13" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" />
197
+ </svg>
198
+ <span class="wd-search-placeholder">Search...</span>
199
+ <kbd class="wd-search-kbd">⌘K</kbd>
200
+ </button>
201
+
202
+ <button type="button" class="wd-theme-toggle" aria-label="Toggle color theme">
203
+ <AppIcon icon="sun" class="wd-theme-icon wd-theme-icon-sun" />
204
+ <AppIcon icon="moon" class="wd-theme-icon wd-theme-icon-moon" />
205
+ </button>
206
+ </div>
207
+ </div>
208
+
209
+ {showSecondRow && (
210
+ <div class="wd-topbar-row wd-topbar-row-tabs">
211
+ <div class="wd-topbar-inner">
212
+ <nav class="wd-topbar-tabs" aria-label="Sections">
213
+ {tabSelectors.map((sel) =>
214
+ sel.options.map((opt) =>
215
+ opt.dropdown ? (
216
+ // A tab whose own content is `dropdowns` (not `pages`)
217
+ // renders as a dropdown-trigger in place of the plain
218
+ // pill - the tab's own label/icon stay on the trigger
219
+ // (not "whichever dropdown is current", so it reads as
220
+ // "this tab, which happens to expand" rather than
221
+ // jumping around as you navigate its sub-pages), and its
222
+ // dropdowns become the menu.
223
+ <div class="wd-dropdown" data-selector-kind="tab">
224
+ <button
225
+ type="button"
226
+ class={`wd-dropdown-trigger wd-tab-link ${opt.active ? 'active' : ''}`}
227
+ aria-haspopup="true"
228
+ aria-expanded="false"
229
+ >
230
+ <AppIcon icon={opt.icon} class="wd-tab-icon" />
231
+ {opt.label}
232
+ <svg class="wd-dropdown-caret" width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
233
+ <path d="M2 3.5L5 6.5L8 3.5" stroke="currentColor" stroke-width="1.4" fill="none" stroke-linecap="round" stroke-linejoin="round" />
234
+ </svg>
235
+ </button>
236
+ <div class="wd-dropdown-menu" role="menu">
237
+ <div class="wd-dropdown-menu-panel">
238
+ {opt.dropdown.map((d) => (
239
+ <a
240
+ href={d.href}
241
+ role="menuitem"
242
+ class={d.active ? 'active' : ''}
243
+ target={isExternalHref(d.href) ? '_blank' : undefined}
244
+ rel={isExternalHref(d.href) ? 'noopener noreferrer' : undefined}
245
+ >
246
+ <AppIcon icon={d.icon} class="wd-tab-icon" />
247
+ {d.label}
248
+ </a>
249
+ ))}
250
+ </div>
251
+ </div>
252
+ </div>
253
+ ) : (
254
+ <a
255
+ href={opt.href}
256
+ class={`wd-tab-link ${opt.active ? 'active' : ''}`}
257
+ target={isExternalHref(opt.href) ? '_blank' : undefined}
258
+ rel={isExternalHref(opt.href) ? 'noopener noreferrer' : undefined}
259
+ >
260
+ <AppIcon icon={opt.icon} class="wd-tab-icon" />
261
+ {opt.label}
262
+ </a>
263
+ )
264
+ )
265
+ )}
266
+ {globalDropdowns.map((d) =>
267
+ d.href !== null ? (
268
+ <a
269
+ href={d.href}
270
+ class="wd-tab-link"
271
+ target={isExternalHref(d.href) ? '_blank' : undefined}
272
+ rel={isExternalHref(d.href) ? 'noopener noreferrer' : undefined}
273
+ >
274
+ <AppIcon icon={d.icon} class="wd-tab-icon" />
275
+ {d.label}
276
+ </a>
277
+ ) : (
278
+ <div class="wd-dropdown">
279
+ <button type="button" class="wd-dropdown-trigger" aria-haspopup="true" aria-expanded="false">
280
+ <AppIcon icon={d.icon} class="wd-tab-icon" />
281
+ {d.label}
282
+ <svg class="wd-dropdown-caret" width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
283
+ <path d="M2 3.5L5 6.5L8 3.5" stroke="currentColor" stroke-width="1.4" fill="none" stroke-linecap="round" stroke-linejoin="round" />
284
+ </svg>
285
+ </button>
286
+ <div class="wd-dropdown-menu" role="menu">
287
+ <div class="wd-dropdown-menu-panel">
288
+ {d.items.map((item) => (
289
+ <a href={item.href} role="menuitem" class={item.active ? 'active' : ''}>{item.label}</a>
290
+ ))}
291
+ </div>
292
+ </div>
293
+ </div>
294
+ )
295
+ )}
296
+ </nav>
297
+ </div>
298
+ </div>
299
+ )}
300
+ </header>
301
+ <script>
302
+ import { initDropdowns } from '../../scripts/dropdowns';
303
+ import { initThemeToggle } from '../../scripts/theme-toggle';
304
+
305
+ initDropdowns(document);
306
+ initThemeToggle(document);
307
+ document.addEventListener('astro:page-load', () => {
308
+ initDropdowns(document);
309
+ initThemeToggle(document);
310
+ });
311
+ </script>
@@ -0,0 +1,44 @@
1
+ /* writedocs.json's `banner` - see bannerSchema in lib/config.ts. Markup stays
2
+ inline in BaseLayout.astro (it's tiny, and its own [data-banner-dismissed]
3
+ flash-prevention script already lives there) - this is just its styling,
4
+ split out for the same organizational reasons as every other file in
5
+ this directory. Sits above .wd-topbar, full-width, no max-width wrapper -
6
+ a banner reads as a site-wide strip, not page content, so it shouldn't
7
+ line up with .wd-shell's centered column. Hidden via the
8
+ [data-banner-dismissed] attribute set by BaseLayout's own inline <head>
9
+ script rather than a client-side mount check, so a previously-dismissed
10
+ banner never flashes visible before JS runs - same technique as the
11
+ [data-theme] flash-prevention script. */
12
+ :root[data-banner-dismissed='true'] .wd-banner {
13
+ display: none;
14
+ }
15
+ .wd-banner {
16
+ width: 100%;
17
+ }
18
+ .wd-banner-info { background: var(--wd-primary); color: #fff; }
19
+ .wd-banner-warning { background: #b45309; color: #fff; }
20
+ .wd-banner-critical { background: #b91c1c; color: #fff; }
21
+ .wd-banner-inner {
22
+ max-width: 1440px;
23
+ margin: 0 auto;
24
+ padding: 0.6rem 1.5rem;
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: center;
28
+ gap: 1rem;
29
+ font-size: 0.88rem;
30
+ text-align: center;
31
+ }
32
+ .wd-banner-content { flex: 1; }
33
+ .wd-banner-dismiss {
34
+ flex-shrink: 0;
35
+ border: none;
36
+ background: transparent;
37
+ color: inherit;
38
+ cursor: pointer;
39
+ font-size: 1.1rem;
40
+ line-height: 1;
41
+ padding: 0 0.25rem;
42
+ opacity: 0.85;
43
+ }
44
+ .wd-banner-dismiss:hover { opacity: 1; }