@writedocs/generator 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +17 -0
- package/astro.config.mjs +419 -0
- package/bin/writedocs.js +73 -0
- package/package.json +79 -0
- package/src/assets/wd_watermark.png +0 -0
- package/src/assets/wd_watermark_dark.png +0 -0
- package/src/cli/build-auth.js +53 -0
- package/src/cli/build.js +40 -0
- package/src/cli/dev.js +12 -0
- package/src/cli/generate-api-pages.js +359 -0
- package/src/cli/init.js +81 -0
- package/src/cli/preflight.js +40 -0
- package/src/cli/run-astro.js +57 -0
- package/src/cli/run-pagefind.js +66 -0
- package/src/cli/write-redirects-file.js +80 -0
- package/src/components/Accordion.astro +164 -0
- package/src/components/AccordionGroup.astro +40 -0
- package/src/components/ApiLangSelect.astro +168 -0
- package/src/components/ApiPlayground.astro +281 -0
- package/src/components/ApiReferencePanel.astro +1754 -0
- package/src/components/ApiSchemaField.astro +54 -0
- package/src/components/AppIcon.astro +32 -0
- package/src/components/Badge.astro +128 -0
- package/src/components/Callout.astro +168 -0
- package/src/components/Card.astro +136 -0
- package/src/components/CardGroup.astro +20 -0
- package/src/components/CodeGroup.astro +184 -0
- package/src/components/CopyPageMenu.astro +246 -0
- package/src/components/Danger.astro +12 -0
- package/src/components/Expandable.astro +126 -0
- package/src/components/Frame.astro +102 -0
- package/src/components/Hint.astro +99 -0
- package/src/components/Icon.astro +70 -0
- package/src/components/Image.astro +147 -0
- package/src/components/Info.astro +12 -0
- package/src/components/Note.astro +12 -0
- package/src/components/Parameter.astro +119 -0
- package/src/components/RequestExample.astro +33 -0
- package/src/components/ResponseExample.astro +19 -0
- package/src/components/Searchbar.astro +117 -0
- package/src/components/Step.astro +10 -0
- package/src/components/Steps.astro +32 -0
- package/src/components/Tab.astro +9 -0
- package/src/components/Tabs.astro +52 -0
- package/src/components/Tip.astro +12 -0
- package/src/components/Video.astro +135 -0
- package/src/components/Warning.astro +12 -0
- package/src/components/index.ts +48 -0
- package/src/content.config.ts +223 -0
- package/src/layout/BaseLayout.astro +750 -0
- package/src/layout/components/AnalyticsScripts.astro +77 -0
- package/src/layout/components/AskAiWidget.astro +37 -0
- package/src/layout/components/Breadcrumbs.astro +97 -0
- package/src/layout/components/ImageZoom.astro +19 -0
- package/src/layout/components/MobileMenu.astro +200 -0
- package/src/layout/components/NavTree.astro +351 -0
- package/src/layout/components/SearchModal.astro +42 -0
- package/src/layout/components/Sidebar.astro +122 -0
- package/src/layout/components/SiteFooter.astro +85 -0
- package/src/layout/components/TableOfContents.astro +117 -0
- package/src/layout/components/TopBar.astro +311 -0
- package/src/layout/styles/banner.css +44 -0
- package/src/layout/styles/base.css +234 -0
- package/src/layout/styles/dropdown.css +133 -0
- package/src/layout/styles/footer.css +108 -0
- package/src/layout/styles/image-zoom.css +50 -0
- package/src/layout/styles/mobile-menu.css +258 -0
- package/src/layout/styles/search-modal.css +122 -0
- package/src/layout/styles/topbar.css +437 -0
- package/src/lib/config.ts +2131 -0
- package/src/lib/mdx-auto-hydrate.js +70 -0
- package/src/lib/mdx-inject-builtins.js +87 -0
- package/src/lib/mdx-substitute-variables.js +66 -0
- package/src/lib/mdx-title-anchor-ids.js +84 -0
- package/src/lib/mermaid-rehype.js +72 -0
- package/src/lib/openapi-render.ts +479 -0
- package/src/lib/shiki-code-block.js +102 -0
- package/src/lib/shiki-copy-button.js +45 -0
- package/src/lib/styles-asset-integration.js +210 -0
- package/src/lib/writedocs-temp-dir.js +93 -0
- package/src/pages/404.astro +62 -0
- package/src/pages/[...slug].astro +1270 -0
- package/src/pages/[...slug].md.ts +78 -0
- package/src/pages/llms-full.txt.ts +71 -0
- package/src/pages/llms.txt.ts +141 -0
- package/src/scripts/banner.ts +20 -0
- package/src/scripts/dropdowns.ts +61 -0
- package/src/scripts/image-zoom.ts +66 -0
- package/src/scripts/mobile-menu.ts +55 -0
- package/src/scripts/search.ts +155 -0
- package/src/scripts/sidebar-scroll.ts +65 -0
- package/src/scripts/theme-toggle.ts +35 -0
- package/src/scripts/topbar-offset.ts +141 -0
- package/src/styles/global.css +18 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { navTreeContainsSlug, isExternalHref, type NavTreeNode } from "../../lib/config";
|
|
3
|
+
import { methodClass } from "../../lib/openapi-render";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
nodes: NavTreeNode[];
|
|
7
|
+
hrefForSlug: (slug: string) => string;
|
|
8
|
+
currentSlug: string;
|
|
9
|
+
depth?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { nodes, hrefForSlug, currentSlug, depth = 0 } = Astro.props as Props;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<ul class={`wd-nav-list wd-nav-depth-${depth}`}>
|
|
16
|
+
{
|
|
17
|
+
nodes.map((node) => {
|
|
18
|
+
if (node.kind === "page") {
|
|
19
|
+
return (
|
|
20
|
+
<li>
|
|
21
|
+
<a href={hrefForSlug(node.slug)} class={node.slug === currentSlug ? "active" : ""}>
|
|
22
|
+
{node.method && (
|
|
23
|
+
<span class={`wd-nav-method wd-nav-method-${methodClass(node.method)}`}>{node.method}</span>
|
|
24
|
+
)}
|
|
25
|
+
<span class="wd-nav-title">{node.title}</span>
|
|
26
|
+
</a>
|
|
27
|
+
</li>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (node.kind === "link") {
|
|
32
|
+
// A bare external link sitting directly in the page tree (see
|
|
33
|
+
// NavItem's `{ label, href }` leaf in lib/config.ts) - styled like
|
|
34
|
+
// a page row, but never "active" (it's not a content page) and
|
|
35
|
+
// always opens in a new tab since it's meant to point off-site.
|
|
36
|
+
const external = isExternalHref(node.href);
|
|
37
|
+
return (
|
|
38
|
+
<li>
|
|
39
|
+
<a
|
|
40
|
+
href={node.href}
|
|
41
|
+
class="wd-nav-link-leaf"
|
|
42
|
+
target={external ? "_blank" : undefined}
|
|
43
|
+
rel={external ? "noopener noreferrer" : undefined}
|
|
44
|
+
>
|
|
45
|
+
<span>{node.label}</span>
|
|
46
|
+
{external && (
|
|
47
|
+
<svg class="wd-nav-external-icon" width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
|
|
48
|
+
<path
|
|
49
|
+
d="M3 1.5H8.5V7M8.5 1.5L1.5 8.5"
|
|
50
|
+
stroke="currentColor"
|
|
51
|
+
stroke-width="1.2"
|
|
52
|
+
fill="none"
|
|
53
|
+
stroke-linecap="round"
|
|
54
|
+
stroke-linejoin="round"
|
|
55
|
+
/>
|
|
56
|
+
</svg>
|
|
57
|
+
)}
|
|
58
|
+
</a>
|
|
59
|
+
</li>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (depth === 0) {
|
|
64
|
+
// Top-level group = a static sidebar section title, never a
|
|
65
|
+
// disclosure control - matches the reference design where the first
|
|
66
|
+
// group level is just a bold label above its pages.
|
|
67
|
+
return (
|
|
68
|
+
<li class="wd-nav-group">
|
|
69
|
+
<div class="wd-nav-section-title">
|
|
70
|
+
{node.pageSlug ? (
|
|
71
|
+
<a href={hrefForSlug(node.pageSlug)} class={node.pageSlug === currentSlug ? "active" : ""}>
|
|
72
|
+
{node.label}
|
|
73
|
+
</a>
|
|
74
|
+
) : (
|
|
75
|
+
node.label
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
<Astro.self nodes={node.children} hrefForSlug={hrefForSlug} currentSlug={currentSlug} depth={depth + 1} />
|
|
79
|
+
</li>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Depth >= 1: a nested group renders as a collapsible row, styled and
|
|
84
|
+
// inset exactly like a sibling page link, with a chevron on the
|
|
85
|
+
// right. Defaults open whenever the active page lives anywhere inside
|
|
86
|
+
// it (itself or a descendant), so landing on a child page never
|
|
87
|
+
// leaves its own parent collapsed.
|
|
88
|
+
const isActive = node.pageSlug === currentSlug;
|
|
89
|
+
const open = isActive || navTreeContainsSlug(node.children, currentSlug);
|
|
90
|
+
return (
|
|
91
|
+
<li class="wd-nav-collapsible">
|
|
92
|
+
<details open={open}>
|
|
93
|
+
<summary class={isActive ? "active" : ""}>
|
|
94
|
+
{node.pageSlug ? (
|
|
95
|
+
<a href={hrefForSlug(node.pageSlug)} class={isActive ? "active" : ""}>
|
|
96
|
+
{node.label}
|
|
97
|
+
</a>
|
|
98
|
+
) : (
|
|
99
|
+
<span class="wd-nav-group-label">{node.label}</span>
|
|
100
|
+
)}
|
|
101
|
+
<svg class="wd-nav-chevron" width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
|
|
102
|
+
<path
|
|
103
|
+
d="M3.5 2L6.5 5L3.5 8"
|
|
104
|
+
stroke="currentColor"
|
|
105
|
+
stroke-width="1.4"
|
|
106
|
+
fill="none"
|
|
107
|
+
stroke-linecap="round"
|
|
108
|
+
stroke-linejoin="round"
|
|
109
|
+
/>
|
|
110
|
+
</svg>
|
|
111
|
+
</summary>
|
|
112
|
+
<Astro.self nodes={node.children} hrefForSlug={hrefForSlug} currentSlug={currentSlug} depth={depth + 1} />
|
|
113
|
+
</details>
|
|
114
|
+
</li>
|
|
115
|
+
);
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
</ul>
|
|
119
|
+
|
|
120
|
+
<style>
|
|
121
|
+
.wd-nav-list {
|
|
122
|
+
list-style: none;
|
|
123
|
+
margin: 0;
|
|
124
|
+
padding: 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.wd-nav-list li a,
|
|
128
|
+
.wd-nav-list li summary {
|
|
129
|
+
border-left: 1px solid transparent;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.wd-nav-list li a:not(summary a):hover,
|
|
133
|
+
.wd-nav-list li summary:hover {
|
|
134
|
+
border-left: 1px solid var(--wd-primary);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.wd-nav-list li a.active:not(summary a),
|
|
138
|
+
.wd-nav-list li summary.active {
|
|
139
|
+
border-left: 1px solid var(--wd-primary);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.wd-nav-depth-0 {
|
|
143
|
+
margin-bottom: 1.25rem;
|
|
144
|
+
}
|
|
145
|
+
.wd-nav-section-title {
|
|
146
|
+
padding: 0;
|
|
147
|
+
margin-bottom: 0.4rem;
|
|
148
|
+
color: var(--wd-text-muted);
|
|
149
|
+
font-size: 0.75rem;
|
|
150
|
+
font-weight: 600;
|
|
151
|
+
text-transform: uppercase;
|
|
152
|
+
letter-spacing: 0.04em;
|
|
153
|
+
}
|
|
154
|
+
/* Overrides `.wd-nav-list a`'s own block/padding/font-size/pill-when-
|
|
155
|
+
active styling further below - without this, a group-with-a-page's
|
|
156
|
+
title (e.g. "Concepts" linking to its own overview page) inherits
|
|
157
|
+
the same big rounded background pill a regular page row gets when
|
|
158
|
+
active, which looks completely out of place sitting next to a
|
|
159
|
+
plain-text sibling title like "Getting Started". A linked section
|
|
160
|
+
title should still read as a section title - same size/weight/
|
|
161
|
+
case/color as an unlinked one - just tinted primary on hover/active
|
|
162
|
+
instead. The extra `.wd-nav-group` ancestor qualifier (2 classes)
|
|
163
|
+
is load-bearing, not decoration: `.wd-nav-section-title a` alone
|
|
164
|
+
has the exact same specificity as `.wd-nav-list a` (1 class + 1
|
|
165
|
+
type each), so whichever rule happens to sit later in the
|
|
166
|
+
stylesheet would silently win regardless of intent - this avoids
|
|
167
|
+
that tie entirely. Mirrors the same "override to neutral, re-apply
|
|
168
|
+
only color" pattern already used for .wd-nav-collapsible summary a
|
|
169
|
+
below (which sidesteps the same tie via a `summary` type selector
|
|
170
|
+
instead). */
|
|
171
|
+
.wd-nav-group .wd-nav-section-title a {
|
|
172
|
+
display: inline;
|
|
173
|
+
padding: 0;
|
|
174
|
+
border-radius: 0;
|
|
175
|
+
background: none;
|
|
176
|
+
color: inherit;
|
|
177
|
+
font-size: inherit;
|
|
178
|
+
font-weight: inherit;
|
|
179
|
+
text-decoration: none;
|
|
180
|
+
}
|
|
181
|
+
.wd-nav-group .wd-nav-section-title a.active,
|
|
182
|
+
.wd-nav-group .wd-nav-section-title a:hover {
|
|
183
|
+
background: none;
|
|
184
|
+
color: var(--wd-primary);
|
|
185
|
+
}
|
|
186
|
+
.wd-nav-depth-1,
|
|
187
|
+
.wd-nav-depth-2,
|
|
188
|
+
.wd-nav-depth-3 {
|
|
189
|
+
margin-bottom: 0.5rem;
|
|
190
|
+
border-left: 1px solid var(--wd-border);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.wd-nav-depth-2,
|
|
194
|
+
.wd-nav-depth-3 {
|
|
195
|
+
margin-bottom: 0.5rem;
|
|
196
|
+
margin-left: 0.75rem;
|
|
197
|
+
border-left: 1px solid var(--wd-border);
|
|
198
|
+
}
|
|
199
|
+
.wd-nav-list a {
|
|
200
|
+
display: flex;
|
|
201
|
+
align-items: center;
|
|
202
|
+
gap: 0.45rem;
|
|
203
|
+
padding: 0.2rem 0.75rem;
|
|
204
|
+
margin: 0.1rem 0;
|
|
205
|
+
border-radius: 0 0.4rem 0.4rem 0;
|
|
206
|
+
text-decoration: none;
|
|
207
|
+
color: var(--wd-text-muted);
|
|
208
|
+
font-size: 0.9rem;
|
|
209
|
+
}
|
|
210
|
+
/* Primary-tinted rather than a neutral surface gray - a faint preview
|
|
211
|
+
of the active pill's own color (12% alpha), at half its strength, so
|
|
212
|
+
hover reads as "this could become active" instead of a generic
|
|
213
|
+
hover state shared with every other clickable thing on the page. */
|
|
214
|
+
.wd-nav-list a:hover {
|
|
215
|
+
background: color-mix(in srgb, var(--wd-primary) 6%, transparent);
|
|
216
|
+
color: var(--wd-text);
|
|
217
|
+
}
|
|
218
|
+
.wd-nav-list a.active {
|
|
219
|
+
background: color-mix(in srgb, var(--wd-primary) 12%, transparent);
|
|
220
|
+
color: var(--wd-primary);
|
|
221
|
+
font-weight: 500;
|
|
222
|
+
}
|
|
223
|
+
.wd-nav-title {
|
|
224
|
+
flex: 1;
|
|
225
|
+
min-width: 0;
|
|
226
|
+
overflow: hidden;
|
|
227
|
+
/* text-overflow: ellipsis;
|
|
228
|
+
white-space: nowrap; */
|
|
229
|
+
}
|
|
230
|
+
.wd-nav-method {
|
|
231
|
+
flex-shrink: 0;
|
|
232
|
+
display: inline-block;
|
|
233
|
+
padding: 0.05rem 0.4rem;
|
|
234
|
+
border-radius: 0.3rem;
|
|
235
|
+
font-size: 0.62rem;
|
|
236
|
+
font-weight: 700;
|
|
237
|
+
letter-spacing: 0.02em;
|
|
238
|
+
line-height: 1.5;
|
|
239
|
+
}
|
|
240
|
+
.wd-nav-method-get {
|
|
241
|
+
background: color-mix(in srgb, #16a34a 15%, transparent);
|
|
242
|
+
color: #16a34a;
|
|
243
|
+
}
|
|
244
|
+
.wd-nav-method-post {
|
|
245
|
+
background: color-mix(in srgb, #2563eb 15%, transparent);
|
|
246
|
+
color: #2563eb;
|
|
247
|
+
}
|
|
248
|
+
.wd-nav-method-put {
|
|
249
|
+
background: color-mix(in srgb, #9333ea 15%, transparent);
|
|
250
|
+
color: #9333ea;
|
|
251
|
+
}
|
|
252
|
+
.wd-nav-method-patch {
|
|
253
|
+
background: color-mix(in srgb, #d97706 15%, transparent);
|
|
254
|
+
color: #d97706;
|
|
255
|
+
}
|
|
256
|
+
.wd-nav-method-delete {
|
|
257
|
+
background: color-mix(in srgb, #dc2626 15%, transparent);
|
|
258
|
+
color: #dc2626;
|
|
259
|
+
}
|
|
260
|
+
.wd-nav-method-other {
|
|
261
|
+
background: var(--wd-surface);
|
|
262
|
+
color: var(--wd-text-muted);
|
|
263
|
+
}
|
|
264
|
+
.wd-nav-link-leaf {
|
|
265
|
+
display: flex !important;
|
|
266
|
+
align-items: center;
|
|
267
|
+
justify-content: space-between;
|
|
268
|
+
gap: 0.5rem;
|
|
269
|
+
}
|
|
270
|
+
.wd-nav-external-icon {
|
|
271
|
+
flex-shrink: 0;
|
|
272
|
+
color: var(--wd-text-muted);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* Depth >= 1 group: a collapsible row that looks like a page link. */
|
|
276
|
+
.wd-nav-collapsible details {
|
|
277
|
+
margin: 0;
|
|
278
|
+
}
|
|
279
|
+
.wd-nav-collapsible summary {
|
|
280
|
+
display: flex;
|
|
281
|
+
align-items: center;
|
|
282
|
+
justify-content: space-between;
|
|
283
|
+
gap: 0.5rem;
|
|
284
|
+
padding: 0.2rem 0.75rem;
|
|
285
|
+
margin: 0.1rem 0;
|
|
286
|
+
border-radius: 0 0.4rem 0.4rem 0;
|
|
287
|
+
cursor: pointer;
|
|
288
|
+
color: var(--wd-text-muted);
|
|
289
|
+
font-size: 0.9rem;
|
|
290
|
+
list-style: none;
|
|
291
|
+
}
|
|
292
|
+
.wd-nav-collapsible summary::-webkit-details-marker {
|
|
293
|
+
display: none;
|
|
294
|
+
}
|
|
295
|
+
.wd-nav-collapsible summary::marker {
|
|
296
|
+
content: "";
|
|
297
|
+
}
|
|
298
|
+
/* Same primary-tinted hover as a plain page link above. */
|
|
299
|
+
.wd-nav-collapsible summary:hover {
|
|
300
|
+
background: color-mix(in srgb, var(--wd-primary) 6%, transparent);
|
|
301
|
+
color: var(--wd-text);
|
|
302
|
+
}
|
|
303
|
+
/* When the group's own attached page is the current page, the pill
|
|
304
|
+
background lives on <summary> itself (not the inner <a> below,
|
|
305
|
+
which is deliberately stripped of its own background/padding so the
|
|
306
|
+
chevron can share the row) - otherwise only the label text would be
|
|
307
|
+
highlighted, leaving the chevron sitting outside the pill. This is
|
|
308
|
+
the same background/radius a plain `.wd-nav-list a.active` pill
|
|
309
|
+
uses, so an active group header looks identical to an active leaf
|
|
310
|
+
page row instead of falling back to text-only bold+color. */
|
|
311
|
+
.wd-nav-collapsible summary.active {
|
|
312
|
+
background: color-mix(in srgb, var(--wd-primary) 12%, transparent);
|
|
313
|
+
border-left: 1px solid var(--wd-primary);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.wd-nav-collapsible summary.active > a {
|
|
317
|
+
border-left: none;
|
|
318
|
+
}
|
|
319
|
+
/* Overrides `.wd-nav-list a`'s own block/padding - the row's padding is
|
|
320
|
+
owned by <summary> now, the label inside is just flex content. Wins
|
|
321
|
+
on specificity (class + 2 tags vs class + 1 tag) regardless of
|
|
322
|
+
source order. */
|
|
323
|
+
.wd-nav-collapsible summary a,
|
|
324
|
+
.wd-nav-collapsible summary .wd-nav-group-label {
|
|
325
|
+
display: block;
|
|
326
|
+
padding: 0;
|
|
327
|
+
border-radius: 0;
|
|
328
|
+
background: none;
|
|
329
|
+
color: inherit;
|
|
330
|
+
font-weight: inherit;
|
|
331
|
+
flex: 1;
|
|
332
|
+
min-width: 0;
|
|
333
|
+
}
|
|
334
|
+
.wd-nav-collapsible summary a:hover {
|
|
335
|
+
background: none;
|
|
336
|
+
color: var(--wd-text);
|
|
337
|
+
}
|
|
338
|
+
.wd-nav-collapsible summary a.active {
|
|
339
|
+
background: none;
|
|
340
|
+
color: var(--wd-primary);
|
|
341
|
+
font-weight: 500;
|
|
342
|
+
}
|
|
343
|
+
.wd-nav-chevron {
|
|
344
|
+
flex-shrink: 0;
|
|
345
|
+
color: var(--wd-text-muted);
|
|
346
|
+
transition: transform 0.15s ease;
|
|
347
|
+
}
|
|
348
|
+
.wd-nav-collapsible details[open] > summary .wd-nav-chevron {
|
|
349
|
+
transform: rotate(90deg);
|
|
350
|
+
}
|
|
351
|
+
</style>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
// The Pagefind-backed search overlay/modal - opened by TopBar.astro's own
|
|
3
|
+
// `.wd-search-trigger` button (⌘K/Ctrl K also works, see
|
|
4
|
+
// src/scripts/search.ts) from anywhere on the site. Fully self-contained:
|
|
5
|
+
// takes no props, since none of its own markup depends on writedocs.json
|
|
6
|
+
// config - unlike every other component split out of BaseLayout.astro.
|
|
7
|
+
// Always rendered regardless of a page's `mode` (including 'blank'), so
|
|
8
|
+
// search stays reachable via the keyboard shortcut even on a page with no
|
|
9
|
+
// visible trigger button to click.
|
|
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 '../styles/search-modal.css';
|
|
15
|
+
---
|
|
16
|
+
<div class="wd-search-overlay" id="wd-search-overlay" hidden>
|
|
17
|
+
<div class="wd-search-modal" role="dialog" aria-modal="true" aria-label="Search">
|
|
18
|
+
<div class="wd-search-input-row">
|
|
19
|
+
<svg class="wd-search-icon" width="16" height="16" viewBox="0 0 14 14" aria-hidden="true">
|
|
20
|
+
<circle cx="6" cy="6" r="4.5" stroke="currentColor" stroke-width="1.4" fill="none" />
|
|
21
|
+
<path d="M9.5 9.5L13 13" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" />
|
|
22
|
+
</svg>
|
|
23
|
+
<input
|
|
24
|
+
type="text"
|
|
25
|
+
class="wd-search-input"
|
|
26
|
+
id="wd-search-input"
|
|
27
|
+
placeholder="Search docs..."
|
|
28
|
+
autocomplete="off"
|
|
29
|
+
autocapitalize="off"
|
|
30
|
+
spellcheck="false"
|
|
31
|
+
/>
|
|
32
|
+
<kbd class="wd-search-esc">Esc</kbd>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="wd-search-results" id="wd-search-results"></div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
<script>
|
|
38
|
+
import { initSearch } from '../../scripts/search';
|
|
39
|
+
|
|
40
|
+
initSearch(document);
|
|
41
|
+
document.addEventListener('astro:page-load', () => initSearch(document));
|
|
42
|
+
</script>
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { NavTreeNode } from "../../lib/config";
|
|
3
|
+
import NavTree from "./NavTree.astro";
|
|
4
|
+
// Two colorways of the same "Powered by writedocs" mark, not a
|
|
5
|
+
// theme-matched pair in the wd-logo-light/wd-logo-dark sense (that
|
|
6
|
+
// convention names each file by which theme it's *shown in*) - these
|
|
7
|
+
// are named by the mark's own ink color instead, and the darker one
|
|
8
|
+
// (wd_watermark_dark.png) is actually the better-contrast choice
|
|
9
|
+
// against the light theme's near-white background, while the lighter,
|
|
10
|
+
// unsuffixed one reads better against the dark theme's near-black one.
|
|
11
|
+
// Imported (not referenced by string path) since this is a
|
|
12
|
+
// framework-bundled asset living in src/assets, not something any
|
|
13
|
+
// individual site's own public/ folder provides - astro.config.mjs
|
|
14
|
+
// points publicDir at the site's public/ instead, so a plain <img
|
|
15
|
+
// src="/..."> here would 404 on every real site build.
|
|
16
|
+
import watermarkForLightTheme from "../../assets/wd_watermark_dark.png";
|
|
17
|
+
import watermarkForDarkTheme from "../../assets/wd_watermark.png";
|
|
18
|
+
|
|
19
|
+
// Deploy-time-only opt-out, not a writedocs.json field: a site's own writedocs.json
|
|
20
|
+
// is part of its checked-in content (something the author controls), while
|
|
21
|
+
// this is meant to be set by whatever's actually running the build (CI,
|
|
22
|
+
// hosting platform env config, etc.) without touching that content - the
|
|
23
|
+
// same reasoning as WRITEDOCS_CONTENT_DIR/WRITEDOCS_PACKAGE_ROOT already
|
|
24
|
+
// being plain process.env reads rather than config fields (see
|
|
25
|
+
// astro.config.mjs and this component's sibling files). Any value other
|
|
26
|
+
// than the literal string "true" leaves the watermark showing, so an
|
|
27
|
+
// unset/misspelled var fails toward the visible, "unmodified" default
|
|
28
|
+
// rather than silently hiding it.
|
|
29
|
+
const watermarkDisabled = process.env.WRITEDOCS_DISABLE_WATERMARK === "true";
|
|
30
|
+
|
|
31
|
+
interface Props {
|
|
32
|
+
navTree: NavTreeNode[];
|
|
33
|
+
hrefForSlug: (slug: string) => string;
|
|
34
|
+
currentSlug: string;
|
|
35
|
+
}
|
|
36
|
+
const { navTree, hrefForSlug, currentSlug } = Astro.props as Props;
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
<nav class="wd-sidebar">
|
|
40
|
+
<NavTree nodes={navTree} hrefForSlug={hrefForSlug} currentSlug={currentSlug} />
|
|
41
|
+
{!watermarkDisabled && (
|
|
42
|
+
<div class="wd-sidebar-watermark">
|
|
43
|
+
<img src={watermarkForLightTheme.src} alt="Powered by writedocs" class="wd-watermark-light" />
|
|
44
|
+
<img src={watermarkForDarkTheme.src} alt="Powered by writedocs" class="wd-watermark-dark" />
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
</nav>
|
|
48
|
+
<style>
|
|
49
|
+
/* Sticky + its own scrollbar, not just "moves along until it runs out
|
|
50
|
+
of column to stick within" (position: sticky's default behavior,
|
|
51
|
+
which is what this had before - a tall nav tree would eventually
|
|
52
|
+
unstick and get carried away by the page's own scroll once
|
|
53
|
+
.wd-sidebar-col's bottom edge passed the viewport). max-height caps
|
|
54
|
+
it to the visible area below the topbar and overflow-y gives it an
|
|
55
|
+
independent scrollbar, so the page scrolls its content while the
|
|
56
|
+
sidebar scrolls itself - same treatment TableOfContents.astro's
|
|
57
|
+
.wd-toc already has (deliberately matching its `top`/`max-height`
|
|
58
|
+
values exactly, so both columns start scrolling at the same height
|
|
59
|
+
when a page has both). Reset back to normal (non-sticky, no cap) on
|
|
60
|
+
narrow screens in BaseLayout.astro's `@media (max-width: 860px)`
|
|
61
|
+
block, where the sidebar stacks full-width above the content instead
|
|
62
|
+
of sitting beside it. */
|
|
63
|
+
.wd-sidebar {
|
|
64
|
+
padding: 1.5rem 1rem 1.5rem 0;
|
|
65
|
+
position: sticky;
|
|
66
|
+
/* --wd-topbar-offset (src/scripts/topbar-offset.ts) is the topbar's
|
|
67
|
+
real measured height - a fixed 5rem (this var()'s own fallback)
|
|
68
|
+
only matched the topbar's simplest, single-row case; matched
|
|
69
|
+
exactly by TableOfContents.astro's own .wd-toc, see this file's
|
|
70
|
+
own comment above on why that pairing matters. */
|
|
71
|
+
top: var(--wd-topbar-offset, 5rem);
|
|
72
|
+
max-height: calc(100vh - var(--wd-topbar-offset, 5rem));
|
|
73
|
+
overflow-y: auto;
|
|
74
|
+
}
|
|
75
|
+
/* Desktop only (see the :global(.wd-sidebar-col) guard) - turns the
|
|
76
|
+
sidebar into a fixed-height flex column so .wd-sidebar-watermark's
|
|
77
|
+
margin-top: auto below can push it all the way to the bottom of the
|
|
78
|
+
visible area when the nav tree doesn't fill it, instead of sitting
|
|
79
|
+
right after the last nav item with a gap of empty space beneath it.
|
|
80
|
+
height (not just the max-height above) is what makes this work -
|
|
81
|
+
max-height alone only caps how tall the box *can* get, it doesn't
|
|
82
|
+
force it to actually be that tall when content is shorter, which is
|
|
83
|
+
exactly the "empty space below the watermark" case this replaces.
|
|
84
|
+
Scoped to .wd-sidebar-col specifically (not the plain .wd-sidebar
|
|
85
|
+
this component always renders) so the *reused* copy inside
|
|
86
|
+
MobileMenu.astro's own scrollable panel is untouched - forcing a
|
|
87
|
+
tall fixed height there would leave an awkward blank stretch inside
|
|
88
|
+
the mobile drawer under a short nav list, where "pinned to the
|
|
89
|
+
bottom of the screen" isn't really representative of a panel the
|
|
90
|
+
reader can already see the bottom of by scrolling. */
|
|
91
|
+
:global(.wd-sidebar-col) .wd-sidebar {
|
|
92
|
+
height: calc(100vh - var(--wd-topbar-offset, 5rem));
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
}
|
|
96
|
+
/* Last thing in the nav column. margin-top: auto only has real estate
|
|
97
|
+
to push into on desktop (where the rule above gives .wd-sidebar a
|
|
98
|
+
real fixed height) - inside the mobile menu's reused copy, .wd-
|
|
99
|
+
sidebar is never a flex container, so this margin resolves to 0 and
|
|
100
|
+
the watermark just falls in after the last nav item instead, same
|
|
101
|
+
as before that rule existed. Either way, padding-top + border-top
|
|
102
|
+
stay unconditional, so there's always at least that much separation
|
|
103
|
+
from the nav items above it. */
|
|
104
|
+
.wd-sidebar-watermark {
|
|
105
|
+
margin-top: auto;
|
|
106
|
+
padding-top: 1rem;
|
|
107
|
+
border-top: 1px solid var(--wd-border);
|
|
108
|
+
}
|
|
109
|
+
/* No `display` here - that's the base.css wd-watermark-light/-dark
|
|
110
|
+
toggle's job alone (default display:none on the inactive one,
|
|
111
|
+
display:block on the active one). This rule's own selector
|
|
112
|
+
specificity (a class + an attribute-scoping selector + a type
|
|
113
|
+
selector) is higher than that toggle's plain single/double-class
|
|
114
|
+
rules, so a `display: block` here would win the cascade
|
|
115
|
+
unconditionally on BOTH images regardless of [data-theme] -
|
|
116
|
+
exactly the "both always visible" bug this replaces. */
|
|
117
|
+
.wd-sidebar-watermark img {
|
|
118
|
+
height: 14px;
|
|
119
|
+
width: auto;
|
|
120
|
+
opacity: 0.7;
|
|
121
|
+
}
|
|
122
|
+
</style>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
// writedocs.json's `footer.columns`/`socials` - see footerSchema/`socials` in
|
|
3
|
+
// lib/config.ts. Renders nothing at all (not even an empty bordered band)
|
|
4
|
+
// when a site hasn't configured either - both fields default to empty, so
|
|
5
|
+
// `hasFooterContent` is never undefined either way. Callers don't need to
|
|
6
|
+
// check this themselves; this component decides on its own whether it has
|
|
7
|
+
// anything to show.
|
|
8
|
+
//
|
|
9
|
+
// Split out of BaseLayout.astro, which had grown to ~2200 lines covering
|
|
10
|
+
// several unrelated concerns in one file - see that component's own
|
|
11
|
+
// comment for the fuller rationale.
|
|
12
|
+
import { isExternalHref, type DocsConfig } from '../../lib/config';
|
|
13
|
+
import AppIcon from '../../components/AppIcon.astro';
|
|
14
|
+
import '../styles/footer.css';
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
config: DocsConfig;
|
|
18
|
+
logoLight?: string;
|
|
19
|
+
logoDark?: string;
|
|
20
|
+
}
|
|
21
|
+
const { config, logoLight, logoDark } = Astro.props as Props;
|
|
22
|
+
const hasLogoImage = Boolean(logoLight || logoDark);
|
|
23
|
+
const socialEntries = Object.entries(config.socials);
|
|
24
|
+
// A `socials` key doubles as its own icon reference (resolveIcon() in
|
|
25
|
+
// lib/config.ts, via <AppIcon>) - fine as-is for a bare name like
|
|
26
|
+
// "github", but a key using the explicit "collection:icon-name" form
|
|
27
|
+
// (e.g. "simple-icons:discord", for a platform Lucide doesn't have a
|
|
28
|
+
// matching icon for) would otherwise end up as this link's own aria-label
|
|
29
|
+
// verbatim, colon and all - not what a screen reader user wants read
|
|
30
|
+
// aloud. Strips down to just the part after the last colon for the label
|
|
31
|
+
// specifically; the full key still goes to AppIcon unchanged.
|
|
32
|
+
const socialLabel = (platform: string) => platform.split(':').pop() ?? platform;
|
|
33
|
+
const hasFooterContent = config.footer.columns.length > 0 || socialEntries.length > 0;
|
|
34
|
+
---
|
|
35
|
+
{hasFooterContent && (
|
|
36
|
+
<footer class="wd-footer">
|
|
37
|
+
<div class="wd-footer-inner">
|
|
38
|
+
{hasLogoImage && (
|
|
39
|
+
<a class="wd-footer-brand" href="/">
|
|
40
|
+
{logoLight && <img src={logoLight} alt={config.name} height="24" class="wd-logo-light" />}
|
|
41
|
+
{logoDark && <img src={logoDark} alt={config.name} height="24" class="wd-logo-dark" />}
|
|
42
|
+
</a>
|
|
43
|
+
)}
|
|
44
|
+
{config.footer.columns.length > 0 && (
|
|
45
|
+
<div class="wd-footer-columns">
|
|
46
|
+
{config.footer.columns.map((column) => (
|
|
47
|
+
<div class="wd-footer-column">
|
|
48
|
+
{column.title && <div class="wd-footer-column-title">{column.title}</div>}
|
|
49
|
+
<ul class="wd-footer-column-links">
|
|
50
|
+
{column.links.map((link) => (
|
|
51
|
+
<li>
|
|
52
|
+
<a
|
|
53
|
+
href={link.href}
|
|
54
|
+
target={isExternalHref(link.href) ? '_blank' : undefined}
|
|
55
|
+
rel={isExternalHref(link.href) ? 'noopener noreferrer' : undefined}
|
|
56
|
+
aria-label={!link.label ? link.icon : undefined}
|
|
57
|
+
>
|
|
58
|
+
<AppIcon icon={link.icon} class="wd-tab-icon" />
|
|
59
|
+
{link.label}
|
|
60
|
+
</a>
|
|
61
|
+
</li>
|
|
62
|
+
))}
|
|
63
|
+
</ul>
|
|
64
|
+
</div>
|
|
65
|
+
))}
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
{socialEntries.length > 0 && (
|
|
69
|
+
<div class="wd-footer-socials">
|
|
70
|
+
{socialEntries.map(([platform, url]) => (
|
|
71
|
+
<a
|
|
72
|
+
href={url}
|
|
73
|
+
class="wd-footer-social-link"
|
|
74
|
+
aria-label={socialLabel(platform)}
|
|
75
|
+
target={isExternalHref(url) ? '_blank' : undefined}
|
|
76
|
+
rel={isExternalHref(url) ? 'noopener noreferrer' : undefined}
|
|
77
|
+
>
|
|
78
|
+
<AppIcon icon={platform} class="wd-footer-social-icon" />
|
|
79
|
+
</a>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
</footer>
|
|
85
|
+
)}
|