@writedocs/generator 0.8.0 → 0.9.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/astro.config.mjs +6 -2
- package/bin/writedocs.js +12 -7
- package/package.json +1 -1
- package/src/cli/build.js +15 -0
- package/src/cli/check-built-styles.js +33 -0
- package/src/cli/convert.js +3 -0
- package/src/cli/dev.js +13 -1
- package/src/cli/open-browser.js +32 -0
- package/src/cli/run-astro.js +4 -0
- package/src/components/ApiLangSelect.astro +1 -2
- package/src/components/CopyPageMenu.astro +161 -40
- package/src/layout/BaseLayout.astro +2 -0
- package/src/layout/components/TopBar.astro +113 -94
- package/src/layout/styles/search-modal.css +27 -0
- package/src/layout/styles/topbar.css +107 -1
- package/src/lib/canonical-path.js +11 -0
- package/src/lib/config-schema.js +119 -7
- package/src/lib/config-schema.ts +158 -20
- package/src/lib/content-check.js +15 -0
- package/src/lib/json-schema-descriptions.js +2 -2
- package/src/lib/link-check.js +2 -2
- package/src/lib/llms-index.ts +3 -3
- package/src/lib/mcp-links.js +30 -0
- package/src/lib/mintlify-convert.js +7 -4
- package/src/lib/search-scope.js +44 -0
- package/src/lib/tabs-strip-offset.js +31 -0
- package/src/lib/writedocs-temp-dir.js +3 -1
- package/src/pages/[...slug].astro +72 -6
- package/src/pages/[...slug].md.ts +8 -8
- package/src/scripts/dropdowns.ts +70 -35
- package/src/scripts/search.ts +58 -4
- package/src/scripts/tabs-strip.ts +169 -0
- package/writedocs.schema.json +33 -15
|
@@ -39,7 +39,7 @@ const { config, selectors, globalDropdowns, showSidebarCol, logoLight, logoDark,
|
|
|
39
39
|
// products, or a dropdown used as a nested path segment) on the right,
|
|
40
40
|
// and a second row underneath for tabs plus the always-visible global
|
|
41
41
|
// dropdowns - both of which behave like a row of labeled links rather
|
|
42
|
-
// than a single current-value switcher.
|
|
42
|
+
// than a single current-value switcher. Tabs inside a tab get a third row.
|
|
43
43
|
//
|
|
44
44
|
// A dropdown container nested directly inside a tab (writedocs.json's "API
|
|
45
45
|
// Reference" tab having a `dropdowns` array instead of `pages`) reads as
|
|
@@ -56,7 +56,10 @@ const switcherSelectors = selectors.filter(
|
|
|
56
56
|
(sel, i) => sel.kind !== 'tab' && !(sel.kind === 'dropdown' && selectors[i - 1]?.kind === 'tab')
|
|
57
57
|
);
|
|
58
58
|
const tabSelectors = selectors.filter((sel) => sel.kind === 'tab');
|
|
59
|
-
|
|
59
|
+
// One row per level of tabs: a tab holding its own tabs gets a second row
|
|
60
|
+
// underneath, with that tab's tabs. The global dropdowns stay in the first
|
|
61
|
+
// row, which renders on its own when there are only those.
|
|
62
|
+
const tabRows = tabSelectors.length > 0 ? tabSelectors : globalDropdowns.length > 0 ? [null] : [];
|
|
60
63
|
---
|
|
61
64
|
<header class="wd-topbar">
|
|
62
65
|
<div class="wd-topbar-row wd-topbar-row-brand">
|
|
@@ -86,7 +89,7 @@ const showSecondRow = tabSelectors.length > 0 || globalDropdowns.length > 0;
|
|
|
86
89
|
<a class="wd-topbar-brand" href="/">
|
|
87
90
|
{logoLight && <img src={logoLight} alt={config.name} height="24" class="wd-logo-light" />}
|
|
88
91
|
{logoDark && <img src={logoDark} alt={config.name} height="24" class="wd-logo-dark" />}
|
|
89
|
-
{brandLabel && <span>{brandLabel}</span>}
|
|
92
|
+
{brandLabel && <span title={brandLabel}>{brandLabel}</span>}
|
|
90
93
|
</a>
|
|
91
94
|
|
|
92
95
|
{switcherSelectors.length > 0 && (
|
|
@@ -100,7 +103,7 @@ const showSecondRow = tabSelectors.length > 0 || globalDropdowns.length > 0;
|
|
|
100
103
|
const current = sel.options.find((o) => o.active) ?? sel.options[0];
|
|
101
104
|
return (
|
|
102
105
|
<div class="wd-dropdown" data-selector-kind={sel.kind}>
|
|
103
|
-
<button type="button" class="wd-dropdown-trigger wd-switcher-trigger" aria-
|
|
106
|
+
<button type="button" class="wd-dropdown-trigger wd-switcher-trigger" aria-expanded="false">
|
|
104
107
|
<AppIcon icon={current.icon} class="wd-tab-icon" />
|
|
105
108
|
{current.label}
|
|
106
109
|
{current.tag && <span class="wd-selector-tag">{current.tag}</span>}
|
|
@@ -108,13 +111,12 @@ const showSecondRow = tabSelectors.length > 0 || globalDropdowns.length > 0;
|
|
|
108
111
|
<path d="M2 3.5L5 6.5L8 3.5" stroke="currentColor" stroke-width="1.4" fill="none" stroke-linecap="round" stroke-linejoin="round" />
|
|
109
112
|
</svg>
|
|
110
113
|
</button>
|
|
111
|
-
<div class="wd-dropdown-menu"
|
|
114
|
+
<div class="wd-dropdown-menu">
|
|
112
115
|
<div class="wd-dropdown-menu-panel">
|
|
113
116
|
{sel.options.map((opt) => (
|
|
114
117
|
<a
|
|
115
118
|
href={opt.href}
|
|
116
|
-
|
|
117
|
-
class={opt.active ? 'active' : ''}
|
|
119
|
+
class={opt.active ? 'active' : ''} aria-current={opt.active ? 'true' : undefined}
|
|
118
120
|
target={isExternalHref(opt.href) ? '_blank' : undefined}
|
|
119
121
|
rel={isExternalHref(opt.href) ? 'noopener noreferrer' : undefined}
|
|
120
122
|
>
|
|
@@ -161,7 +163,6 @@ const showSecondRow = tabSelectors.length > 0 || globalDropdowns.length > 0;
|
|
|
161
163
|
<button
|
|
162
164
|
type="button"
|
|
163
165
|
class="wd-dropdown-trigger wd-topbar-links-toggle"
|
|
164
|
-
aria-haspopup="true"
|
|
165
166
|
aria-expanded="false"
|
|
166
167
|
aria-label="More links"
|
|
167
168
|
>
|
|
@@ -171,12 +172,11 @@ const showSecondRow = tabSelectors.length > 0 || globalDropdowns.length > 0;
|
|
|
171
172
|
<circle cx="2" cy="14" r="1.7" fill="currentColor" />
|
|
172
173
|
</svg>
|
|
173
174
|
</button>
|
|
174
|
-
<div class="wd-dropdown-menu"
|
|
175
|
+
<div class="wd-dropdown-menu">
|
|
175
176
|
<div class="wd-dropdown-menu-panel">
|
|
176
177
|
{config.topbar.links.map((link) => (
|
|
177
178
|
<a
|
|
178
179
|
href={link.href}
|
|
179
|
-
role="menuitem"
|
|
180
180
|
target={isExternalHref(link.href) ? '_blank' : undefined}
|
|
181
181
|
rel={isExternalHref(link.href) ? 'noopener noreferrer' : undefined}
|
|
182
182
|
aria-label={!link.label ? link.icon : undefined}
|
|
@@ -206,106 +206,125 @@ const showSecondRow = tabSelectors.length > 0 || globalDropdowns.length > 0;
|
|
|
206
206
|
</div>
|
|
207
207
|
</div>
|
|
208
208
|
|
|
209
|
-
{
|
|
210
|
-
<div class=
|
|
209
|
+
{tabRows.map((sel, row) => (
|
|
210
|
+
<div class={`wd-topbar-row wd-topbar-row-tabs${row > 0 ? ' wd-topbar-row-subtabs' : ''}`}>
|
|
211
211
|
<div class="wd-topbar-inner">
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
212
|
+
{/* More tabs than fit: the row slides sideways instead of wrapping,
|
|
213
|
+
with an arrow on each side that has more (src/scripts/tabs-strip.ts).
|
|
214
|
+
The arrows are only for the mouse - keyboard focus slides a tab
|
|
215
|
+
into view by itself. */}
|
|
216
|
+
<div class="wd-tabs-strip" data-tabs-strip>
|
|
217
|
+
<button type="button" class="wd-tabs-arrow wd-tabs-arrow-prev" tabindex="-1" aria-hidden="true" data-tabs-prev>
|
|
218
|
+
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">
|
|
219
|
+
<path d="M8.5 3L4.5 7L8.5 11" stroke="currentColor" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round" />
|
|
220
|
+
</svg>
|
|
221
|
+
</button>
|
|
222
|
+
<div class="wd-tabs-viewport">
|
|
223
|
+
<nav class="wd-topbar-tabs" aria-label={row === 0 ? 'Sections' : 'Subsections'}>
|
|
224
|
+
{(sel?.options ?? []).map((opt) =>
|
|
225
|
+
opt.dropdown ? (
|
|
226
|
+
// A tab whose own content is `dropdowns` (not `pages`)
|
|
227
|
+
// renders as a dropdown-trigger in place of the plain
|
|
228
|
+
// pill - the tab's own label/icon stay on the trigger
|
|
229
|
+
// (not "whichever dropdown is current", so it reads as
|
|
230
|
+
// "this tab, which happens to expand" rather than
|
|
231
|
+
// jumping around as you navigate its sub-pages), and its
|
|
232
|
+
// dropdowns become the menu.
|
|
233
|
+
<div class="wd-dropdown" data-selector-kind="tab">
|
|
234
|
+
<button
|
|
235
|
+
type="button"
|
|
236
|
+
class={`wd-dropdown-trigger wd-tab-link ${opt.active ? 'active' : ''}`}
|
|
237
|
+
aria-expanded="false"
|
|
238
|
+
>
|
|
239
|
+
<AppIcon icon={opt.icon} class="wd-tab-icon" />
|
|
240
|
+
{opt.label}
|
|
241
|
+
<svg class="wd-dropdown-caret" width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
|
|
242
|
+
<path d="M2 3.5L5 6.5L8 3.5" stroke="currentColor" stroke-width="1.4" fill="none" stroke-linecap="round" stroke-linejoin="round" />
|
|
243
|
+
</svg>
|
|
244
|
+
</button>
|
|
245
|
+
<div class="wd-dropdown-menu">
|
|
246
|
+
<div class="wd-dropdown-menu-panel">
|
|
247
|
+
{opt.dropdown.map((d) => (
|
|
248
|
+
<a
|
|
249
|
+
href={d.href}
|
|
250
|
+
class={d.active ? 'active' : ''} aria-current={d.active ? 'true' : undefined}
|
|
251
|
+
target={isExternalHref(d.href) ? '_blank' : undefined}
|
|
252
|
+
rel={isExternalHref(d.href) ? 'noopener noreferrer' : undefined}
|
|
253
|
+
>
|
|
254
|
+
<AppIcon icon={d.icon} class="wd-tab-icon" />
|
|
255
|
+
{d.label}
|
|
256
|
+
</a>
|
|
257
|
+
))}
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
) : (
|
|
262
|
+
<a
|
|
263
|
+
href={opt.href}
|
|
264
|
+
class={`wd-tab-link ${opt.active ? 'active' : ''}`}
|
|
265
|
+
aria-current={opt.active ? 'true' : undefined}
|
|
266
|
+
target={isExternalHref(opt.href) ? '_blank' : undefined}
|
|
267
|
+
rel={isExternalHref(opt.href) ? 'noopener noreferrer' : undefined}
|
|
229
268
|
>
|
|
230
269
|
<AppIcon icon={opt.icon} class="wd-tab-icon" />
|
|
231
270
|
{opt.label}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
271
|
+
</a>
|
|
272
|
+
)
|
|
273
|
+
)}
|
|
274
|
+
{row === 0 &&
|
|
275
|
+
globalDropdowns.map((d) =>
|
|
276
|
+
d.href !== null ? (
|
|
277
|
+
<a
|
|
278
|
+
href={d.href}
|
|
279
|
+
class="wd-tab-link"
|
|
280
|
+
target={isExternalHref(d.href) ? '_blank' : undefined}
|
|
281
|
+
rel={isExternalHref(d.href) ? 'noopener noreferrer' : undefined}
|
|
282
|
+
>
|
|
283
|
+
<AppIcon icon={d.icon} class="wd-tab-icon" />
|
|
284
|
+
{d.label}
|
|
285
|
+
</a>
|
|
286
|
+
) : (
|
|
287
|
+
<div class="wd-dropdown">
|
|
288
|
+
<button type="button" class="wd-dropdown-trigger" aria-expanded="false">
|
|
289
|
+
<AppIcon icon={d.icon} class="wd-tab-icon" />
|
|
290
|
+
{d.label}
|
|
291
|
+
<svg class="wd-dropdown-caret" width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
|
|
292
|
+
<path d="M2 3.5L5 6.5L8 3.5" stroke="currentColor" stroke-width="1.4" fill="none" stroke-linecap="round" stroke-linejoin="round" />
|
|
293
|
+
</svg>
|
|
294
|
+
</button>
|
|
295
|
+
<div class="wd-dropdown-menu">
|
|
296
|
+
<div class="wd-dropdown-menu-panel">
|
|
297
|
+
{d.items.map((item) => (
|
|
298
|
+
<a href={item.href} class={item.active ? 'active' : ''} aria-current={item.active ? 'true' : undefined}>{item.label}</a>
|
|
299
|
+
))}
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
250
302
|
</div>
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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>
|
|
303
|
+
)
|
|
304
|
+
)}
|
|
305
|
+
</nav>
|
|
306
|
+
</div>
|
|
307
|
+
<button type="button" class="wd-tabs-arrow wd-tabs-arrow-next" tabindex="-1" aria-hidden="true" data-tabs-next>
|
|
308
|
+
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">
|
|
309
|
+
<path d="M5.5 3L9.5 7L5.5 11" stroke="currentColor" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round" />
|
|
310
|
+
</svg>
|
|
311
|
+
</button>
|
|
312
|
+
</div>
|
|
297
313
|
</div>
|
|
298
314
|
</div>
|
|
299
|
-
)}
|
|
315
|
+
))}
|
|
300
316
|
</header>
|
|
301
317
|
<script>
|
|
302
318
|
import { initDropdowns } from '../../scripts/dropdowns';
|
|
303
319
|
import { initThemeToggle } from '../../scripts/theme-toggle';
|
|
320
|
+
import { initTabsStrips } from '../../scripts/tabs-strip';
|
|
304
321
|
|
|
305
322
|
initDropdowns(document);
|
|
306
323
|
initThemeToggle(document);
|
|
324
|
+
initTabsStrips(document);
|
|
307
325
|
document.addEventListener('astro:page-load', () => {
|
|
308
326
|
initDropdowns(document);
|
|
309
327
|
initThemeToggle(document);
|
|
328
|
+
initTabsStrips(document);
|
|
310
329
|
});
|
|
311
330
|
</script>
|
|
@@ -95,6 +95,33 @@
|
|
|
95
95
|
browser's default yellow highlight, to match the rest of the site's own
|
|
96
96
|
"tint primary" pattern for active/matched state (sidebar links,
|
|
97
97
|
selector options, ...). */
|
|
98
|
+
/* Where the page sits ("API Reference › Webhooks"), between the title and
|
|
99
|
+
the excerpt. */
|
|
100
|
+
.wd-search-result-section {
|
|
101
|
+
font-size: 0.75rem;
|
|
102
|
+
color: var(--wd-text-muted);
|
|
103
|
+
margin: -0.15rem 0 0.3rem;
|
|
104
|
+
white-space: nowrap;
|
|
105
|
+
overflow: hidden;
|
|
106
|
+
text-overflow: ellipsis;
|
|
107
|
+
}
|
|
108
|
+
/* Under the results: which version/language/product they're from, and the
|
|
109
|
+
switch to search the whole site. */
|
|
110
|
+
.wd-search-scope {
|
|
111
|
+
padding: 0.6rem 0.8rem 0.3rem;
|
|
112
|
+
font-size: 0.8rem;
|
|
113
|
+
color: var(--wd-text-muted);
|
|
114
|
+
}
|
|
115
|
+
.wd-search-scope-toggle {
|
|
116
|
+
padding: 0;
|
|
117
|
+
border: none;
|
|
118
|
+
background: none;
|
|
119
|
+
font: inherit;
|
|
120
|
+
color: var(--wd-primary);
|
|
121
|
+
cursor: pointer;
|
|
122
|
+
text-decoration: underline;
|
|
123
|
+
text-underline-offset: 2px;
|
|
124
|
+
}
|
|
98
125
|
.wd-search-result-excerpt mark {
|
|
99
126
|
background: none;
|
|
100
127
|
color: var(--wd-primary);
|
|
@@ -41,6 +41,17 @@
|
|
|
41
41
|
active tab. */
|
|
42
42
|
padding-bottom: 0;
|
|
43
43
|
}
|
|
44
|
+
/* A tab holding its own tabs: those go in a row underneath. The first row
|
|
45
|
+
gets a divider for its active tab's underline to land on, and the second
|
|
46
|
+
row's tabs are a step lighter, so the two levels read as section and
|
|
47
|
+
subsection. */
|
|
48
|
+
.wd-topbar-row-tabs:not(:last-child) {
|
|
49
|
+
border-bottom: 1px solid var(--wd-border);
|
|
50
|
+
}
|
|
51
|
+
.wd-topbar-row-subtabs .wd-tab-link {
|
|
52
|
+
font-size: 0.85rem;
|
|
53
|
+
font-weight: 500;
|
|
54
|
+
}
|
|
44
55
|
.wd-topbar-brand {
|
|
45
56
|
display: flex;
|
|
46
57
|
align-items: center;
|
|
@@ -56,14 +67,29 @@
|
|
|
56
67
|
see this file's own top comment. */
|
|
57
68
|
color: var(--wd-navbar-foreground);
|
|
58
69
|
font-size: 1.05rem;
|
|
70
|
+
/* When the row runs short, a long site name gives way (ending in "..."),
|
|
71
|
+
not the switchers next to it - their labels would break onto two lines. */
|
|
72
|
+
flex-shrink: 1;
|
|
73
|
+
min-width: 0;
|
|
74
|
+
}
|
|
75
|
+
.wd-topbar-brand img {
|
|
59
76
|
flex-shrink: 0;
|
|
60
77
|
}
|
|
78
|
+
.wd-topbar-brand span {
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
text-overflow: ellipsis;
|
|
81
|
+
white-space: nowrap;
|
|
82
|
+
}
|
|
61
83
|
/* Switchers sit right next to the brand (no auto margin here) -
|
|
62
84
|
.wd-topbar-links below is what gets pushed to the far right. */
|
|
63
85
|
.wd-topbar-switchers {
|
|
64
86
|
display: flex;
|
|
65
87
|
align-items: center;
|
|
66
88
|
gap: 0.4rem;
|
|
89
|
+
flex-shrink: 0;
|
|
90
|
+
}
|
|
91
|
+
.wd-switcher-trigger {
|
|
92
|
+
white-space: nowrap;
|
|
67
93
|
}
|
|
68
94
|
/* --wd-navbar-border, not --wd-border - falls back to the literal
|
|
69
95
|
var(--wd-border) reference when theme.navbar isn't configured (see
|
|
@@ -111,8 +137,88 @@
|
|
|
111
137
|
visibly above the row's own divider line instead of touching it. */
|
|
112
138
|
align-items: stretch;
|
|
113
139
|
gap: 0.25rem;
|
|
140
|
+
/* One line, as wide as its tabs: .wd-tabs-viewport shows what fits, and
|
|
141
|
+
src/scripts/tabs-strip.ts slides the rest into view. */
|
|
142
|
+
flex: none;
|
|
143
|
+
width: max-content;
|
|
144
|
+
position: relative;
|
|
145
|
+
}
|
|
146
|
+
.wd-topbar-tabs > * {
|
|
147
|
+
flex-shrink: 0;
|
|
148
|
+
white-space: nowrap;
|
|
149
|
+
}
|
|
150
|
+
.wd-tabs-animate .wd-topbar-tabs {
|
|
151
|
+
transition: transform 0.25s ease;
|
|
152
|
+
}
|
|
153
|
+
.wd-tabs-strip {
|
|
154
|
+
position: relative;
|
|
155
|
+
flex: 1;
|
|
156
|
+
min-width: 0;
|
|
157
|
+
display: flex;
|
|
158
|
+
align-items: stretch;
|
|
159
|
+
}
|
|
160
|
+
/* clip, not hidden: `hidden` would make this a scroll container and cut off
|
|
161
|
+
the tab menus that open below the row. */
|
|
162
|
+
.wd-tabs-viewport {
|
|
114
163
|
flex: 1;
|
|
115
|
-
|
|
164
|
+
min-width: 0;
|
|
165
|
+
display: flex;
|
|
166
|
+
align-items: stretch;
|
|
167
|
+
overflow-x: clip;
|
|
168
|
+
/* Sideways touch moves are src/scripts/tabs-strip.ts's to drag the row;
|
|
169
|
+
up/down ones still scroll the page. */
|
|
170
|
+
touch-action: pan-y pinch-zoom;
|
|
171
|
+
}
|
|
172
|
+
/* An arrow on each side that has more tabs, over the row's edge, fading
|
|
173
|
+
the tab under it out. */
|
|
174
|
+
.wd-tabs-arrow {
|
|
175
|
+
display: none;
|
|
176
|
+
position: absolute;
|
|
177
|
+
top: 0;
|
|
178
|
+
bottom: 0;
|
|
179
|
+
z-index: 2;
|
|
180
|
+
width: 2.5rem;
|
|
181
|
+
padding: 0;
|
|
182
|
+
border: none;
|
|
183
|
+
align-items: center;
|
|
184
|
+
cursor: pointer;
|
|
185
|
+
color: var(--wd-navbar-foreground);
|
|
186
|
+
}
|
|
187
|
+
.wd-tabs-arrow-prev {
|
|
188
|
+
left: 0;
|
|
189
|
+
justify-content: flex-start;
|
|
190
|
+
background: linear-gradient(to right, var(--wd-navbar-background) 55%, transparent);
|
|
191
|
+
}
|
|
192
|
+
.wd-tabs-arrow-next {
|
|
193
|
+
right: 0;
|
|
194
|
+
justify-content: flex-end;
|
|
195
|
+
background: linear-gradient(to left, var(--wd-navbar-background) 55%, transparent);
|
|
196
|
+
}
|
|
197
|
+
.wd-tabs-arrow svg {
|
|
198
|
+
box-sizing: content-box;
|
|
199
|
+
padding: 0.3rem;
|
|
200
|
+
border-radius: 999px;
|
|
201
|
+
}
|
|
202
|
+
.wd-tabs-arrow:hover svg {
|
|
203
|
+
background: var(--wd-navbar-hover);
|
|
204
|
+
}
|
|
205
|
+
.wd-can-prev .wd-tabs-arrow-prev,
|
|
206
|
+
.wd-can-next .wd-tabs-arrow-next {
|
|
207
|
+
display: flex;
|
|
208
|
+
}
|
|
209
|
+
/* The menu of a tab near the right end opens leftwards, to stay in view. */
|
|
210
|
+
.wd-tabs-strip .wd-dropdown-align-end .wd-dropdown-menu {
|
|
211
|
+
left: auto;
|
|
212
|
+
right: 0;
|
|
213
|
+
}
|
|
214
|
+
/* Browsers without overflow-x: clip (Safari before 16): the tabs wrap onto
|
|
215
|
+
more lines, as before, and the script leaves them alone. */
|
|
216
|
+
@supports not (overflow-x: clip) {
|
|
217
|
+
.wd-topbar-tabs {
|
|
218
|
+
flex: 1;
|
|
219
|
+
width: auto;
|
|
220
|
+
flex-wrap: wrap;
|
|
221
|
+
}
|
|
116
222
|
}
|
|
117
223
|
/* .wd-dropdown itself is just a plain block (position: relative, for its
|
|
118
224
|
own .wd-dropdown-menu to anchor to) - stretching *it* to the row's
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// A Windows path with its drive letter in capitals: `c:\docs` -> `C:\docs`.
|
|
2
|
+
// The same folder can reach writedocs spelled either way (VS Code's
|
|
3
|
+
// terminal often starts in `c:\...`, and a shell prompt in `C:\...`), and
|
|
4
|
+
// Vite compares module ids as text - with the package's own folder spelled
|
|
5
|
+
// `c:`, a build came out with every page missing its stylesheet. Every
|
|
6
|
+
// path the CLI hands Astro goes through this first. Other platforms, and
|
|
7
|
+
// paths without a drive letter, come back unchanged.
|
|
8
|
+
export function canonicalPath(p, platform = process.platform) {
|
|
9
|
+
if (platform !== 'win32' || typeof p !== 'string') return p;
|
|
10
|
+
return /^[a-z]:/.test(p) ? p[0].toUpperCase() + p.slice(1) : p;
|
|
11
|
+
}
|
package/src/lib/config-schema.js
CHANGED
|
@@ -63,6 +63,7 @@ const dropdownSchema = z.lazy(
|
|
|
63
63
|
const productSchema = z.lazy(
|
|
64
64
|
() => withChildren({ product: z.string(), icon: z.string().optional(), description: z.string().optional() })
|
|
65
65
|
);
|
|
66
|
+
const MAX_TAB_LEVELS = 2;
|
|
66
67
|
const globalSchema = z.object({ dropdowns: z.array(dropdownSchema).min(1) });
|
|
67
68
|
const navigationSchema = z.union([
|
|
68
69
|
z.array(navItemSchema),
|
|
@@ -71,7 +72,39 @@ const navigationSchema = z.union([
|
|
|
71
72
|
z.object({ global: globalSchema.optional(), languages: z.array(languageSchema).min(1) }).strict(),
|
|
72
73
|
z.object({ global: globalSchema.optional(), dropdowns: z.array(dropdownSchema).min(1) }).strict(),
|
|
73
74
|
z.object({ global: globalSchema.optional(), products: z.array(productSchema).min(1) }).strict()
|
|
74
|
-
])
|
|
75
|
+
]).superRefine((navigation, ctx) => {
|
|
76
|
+
const CONTAINERS = ["tabs", "versions", "languages", "dropdowns", "products"];
|
|
77
|
+
const walk = (node, path, language, tabs) => {
|
|
78
|
+
if (!node || typeof node !== "object" || Array.isArray(node)) return;
|
|
79
|
+
const own = node;
|
|
80
|
+
for (const key of CONTAINERS) {
|
|
81
|
+
const items = own[key];
|
|
82
|
+
if (!Array.isArray(items)) continue;
|
|
83
|
+
if (key === "languages" && language !== null) {
|
|
84
|
+
ctx.addIssue({
|
|
85
|
+
code: "custom",
|
|
86
|
+
path: [...path, key],
|
|
87
|
+
message: `The language "${language}" contains another "languages" list. A page can only be in one language - put the languages at one level, and the rest (tabs, versions, ...) inside each of them.`
|
|
88
|
+
});
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (key === "tabs" && tabs.length >= MAX_TAB_LEVELS) {
|
|
92
|
+
ctx.addIssue({
|
|
93
|
+
code: "custom",
|
|
94
|
+
path: [...path, key],
|
|
95
|
+
message: `Tabs go at most ${MAX_TAB_LEVELS} levels deep, and these are a third level (inside "${tabs.join('" > "')}"). Each level of tabs is a row in the top bar - use a dropdown or groups for this level instead.`
|
|
96
|
+
});
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
items.forEach((item, i) => {
|
|
100
|
+
const inner = key === "languages" ? String(item?.language ?? "") : language;
|
|
101
|
+
const tab = item?.tab;
|
|
102
|
+
walk(item, [...path, key, i], inner, key === "tabs" ? [...tabs, String(tab ?? "")] : tabs);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
walk(navigation, [], null, []);
|
|
107
|
+
});
|
|
75
108
|
const DEFAULT_PRIMARY = "#6366f1";
|
|
76
109
|
const logoSchema = z.union([
|
|
77
110
|
z.string(),
|
|
@@ -390,9 +423,28 @@ const apiSchema = z.object({
|
|
|
390
423
|
// site can set this to `false` to never involve a third party.
|
|
391
424
|
proxy: z.boolean().default(true)
|
|
392
425
|
}).strict().default({ proxy: true });
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
426
|
+
const CONTEXT_MENU_OPTIONS = ["copy", "view", "chatgpt", "claude", "perplexity", "mcp", "cursor", "vscode"];
|
|
427
|
+
const ASSISTANT_OPTIONS = ["chatgpt", "claude", "perplexity"];
|
|
428
|
+
const MCP_MENU_OPTIONS = ["mcp", "cursor", "vscode"];
|
|
429
|
+
const contextMenuSchema = z.union([
|
|
430
|
+
z.boolean(),
|
|
431
|
+
z.array(z.enum(CONTEXT_MENU_OPTIONS)),
|
|
432
|
+
z.object({
|
|
433
|
+
openIn: z.array(z.enum(ASSISTANT_OPTIONS)).optional()
|
|
434
|
+
}).strict()
|
|
435
|
+
]);
|
|
436
|
+
function contextMenuOptions(value, { mcp = true } = {}) {
|
|
437
|
+
const available = CONTEXT_MENU_OPTIONS.filter((option) => mcp || !MCP_MENU_OPTIONS.includes(option));
|
|
438
|
+
if (value === false) return [];
|
|
439
|
+
if (value === void 0 || value === null || value === true) return [...available];
|
|
440
|
+
if (Array.isArray(value)) return available.filter((option) => value.includes(option));
|
|
441
|
+
if (typeof value === "object") {
|
|
442
|
+
const openIn = value.openIn;
|
|
443
|
+
const assistants = Array.isArray(openIn) ? openIn : [...ASSISTANT_OPTIONS];
|
|
444
|
+
return CONTEXT_MENU_OPTIONS.filter((option) => option === "copy" || option === "view" || assistants.includes(option));
|
|
445
|
+
}
|
|
446
|
+
return [];
|
|
447
|
+
}
|
|
396
448
|
const redirectSchema = z.object({
|
|
397
449
|
source: z.string(),
|
|
398
450
|
destination: z.string()
|
|
@@ -519,8 +571,8 @@ const docsConfigSchema = z.object({
|
|
|
519
571
|
// field by field via mergeSeo(), rather than needing to repeat every
|
|
520
572
|
// field on every page.
|
|
521
573
|
seo: seoFieldsSchema.default({}),
|
|
522
|
-
// The "Copy page"
|
|
523
|
-
//
|
|
574
|
+
// The "Copy page" menu - see contextMenuSchema above. Absent means on,
|
|
575
|
+
// with every option; read it through contextMenuOptions().
|
|
524
576
|
contextMenu: contextMenuSchema.optional(),
|
|
525
577
|
// The site's MCP server - an AI tool connects to `/mcp` and searches and
|
|
526
578
|
// reads the docs. On by default: `writedocs build` writes the page index
|
|
@@ -626,6 +678,62 @@ function desembrulharUnioes(issues, prefixo = []) {
|
|
|
626
678
|
}
|
|
627
679
|
return saida;
|
|
628
680
|
}
|
|
681
|
+
const LISTAS_DE_CONTAINER = ["tabs", "versions", "languages", "dropdowns", "products"];
|
|
682
|
+
const ITENS_DE_CONTAINER = {
|
|
683
|
+
tab: { lista: "tabs", nome: "A tab" },
|
|
684
|
+
version: { lista: "versions", nome: "A version" },
|
|
685
|
+
language: { lista: "languages", nome: "A language" },
|
|
686
|
+
dropdown: { lista: "dropdowns", nome: "A dropdown" },
|
|
687
|
+
product: { lista: "products", nome: "A product" }
|
|
688
|
+
};
|
|
689
|
+
function containersEmPaginas(issues, raiz) {
|
|
690
|
+
const vistos = /* @__PURE__ */ new Set();
|
|
691
|
+
const saida = [];
|
|
692
|
+
for (const issue of issues) {
|
|
693
|
+
const achado = containerEmPaginas(issue.caminho, raiz);
|
|
694
|
+
if (!achado) {
|
|
695
|
+
saida.push(issue);
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
const chave = achado.caminho.join(".");
|
|
699
|
+
if (vistos.has(chave)) continue;
|
|
700
|
+
vistos.add(chave);
|
|
701
|
+
saida.push({ ...issue, code: "custom", caminho: achado.caminho, message: achado.mensagem, keys: void 0 });
|
|
702
|
+
}
|
|
703
|
+
return saida;
|
|
704
|
+
}
|
|
705
|
+
function containerEmPaginas(caminho, raiz) {
|
|
706
|
+
for (let k = 1; k < caminho.length; k++) {
|
|
707
|
+
if (typeof caminho[k] !== "number") continue;
|
|
708
|
+
const naRaiz = k === 1 && caminho[0] === "navigation";
|
|
709
|
+
if (!naRaiz && caminho[k - 1] !== "pages") continue;
|
|
710
|
+
const item = valorEm(raiz, caminho.slice(0, k + 1));
|
|
711
|
+
if (!item || typeof item !== "object" || Array.isArray(item) || "group" in item) continue;
|
|
712
|
+
const lugar = naRaiz ? "the navigation list" : 'a "pages" list';
|
|
713
|
+
const rotulo = (texto) => (naRaiz ? "The navigation list" : 'A "pages" list') + ` holds pages, groups and links only. ${texto}`;
|
|
714
|
+
const lista = LISTAS_DE_CONTAINER.find((l) => l in item);
|
|
715
|
+
if (lista) {
|
|
716
|
+
return {
|
|
717
|
+
caminho: caminho.slice(0, k + 1),
|
|
718
|
+
mensagem: `"${lista}" can't go inside ${lugar}. ` + rotulo(
|
|
719
|
+
naRaiz ? `To use ${lista}, make "navigation" an object: "navigation": { "${lista}": [ ... ] }, with the groups inside each of them.` : `Put "${lista}" on "navigation" itself, or on a tab, version, language, dropdown or product in place of its "pages".`
|
|
720
|
+
)
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
const tipo = Object.keys(ITENS_DE_CONTAINER).find((t) => t in item);
|
|
724
|
+
if (tipo) {
|
|
725
|
+
const { lista: dela, nome } = ITENS_DE_CONTAINER[tipo];
|
|
726
|
+
const nomeDoItem = item[tipo];
|
|
727
|
+
return {
|
|
728
|
+
caminho: caminho.slice(0, k + 1),
|
|
729
|
+
mensagem: `${nome}${typeof nomeDoItem === "string" ? ` ("${nomeDoItem}")` : ""} can't go inside ${lugar}. ` + rotulo(
|
|
730
|
+
naRaiz ? `${nome} goes in a "${dela}" list: "navigation": { "${dela}": [ ... ] }.` : `${nome} goes in a "${dela}" list.`
|
|
731
|
+
)
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
629
737
|
function createJsonLocator(rawText) {
|
|
630
738
|
return criarLocalizador(rawText);
|
|
631
739
|
}
|
|
@@ -813,7 +921,7 @@ function validateDocsConfig(rawText) {
|
|
|
813
921
|
ok: false,
|
|
814
922
|
data: null,
|
|
815
923
|
kind: "schema",
|
|
816
|
-
issues: desembrulharUnioes(result.error.issues).map((i) => {
|
|
924
|
+
issues: containersEmPaginas(desembrulharUnioes(result.error.issues), raw).map((i) => {
|
|
817
925
|
const alvoDaLinha = i.code === "unrecognized_keys" && i.keys?.length ? [...i.caminho, i.keys[0]] : i.caminho;
|
|
818
926
|
return {
|
|
819
927
|
path: i.caminho.join(".") || "(root)",
|
|
@@ -829,7 +937,11 @@ function validateDocsConfig(rawText) {
|
|
|
829
937
|
return { ok: true, data: result.data, issues: [] };
|
|
830
938
|
}
|
|
831
939
|
export {
|
|
940
|
+
CONTEXT_MENU_OPTIONS,
|
|
941
|
+
MAX_TAB_LEVELS,
|
|
942
|
+
MCP_MENU_OPTIONS,
|
|
832
943
|
ROOT_ALLOWED_EXTRA_KEYS,
|
|
944
|
+
contextMenuOptions,
|
|
833
945
|
createJsonLocator,
|
|
834
946
|
docsConfigSchema,
|
|
835
947
|
formatValidationIssues,
|