@writedocs/generator 0.8.1 → 0.9.1
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 +2 -1
- package/bin/writedocs.js +10 -7
- package/package.json +1 -1
- package/src/cli/build.js +21 -0
- package/src/cli/check-built-styles.js +33 -0
- package/src/cli/dev.js +4 -0
- package/src/cli/rewrite-redirect-pages.js +84 -0
- package/src/cli/run-astro.js +4 -0
- package/src/components/ApiLangSelect.astro +1 -2
- package/src/components/CopyPageMenu.astro +122 -34
- package/src/layout/BaseLayout.astro +2 -0
- package/src/layout/components/MobileMenu.astro +10 -4
- package/src/layout/components/Sidebar.astro +110 -2
- package/src/layout/components/TopBar.astro +127 -103
- package/src/layout/styles/mobile-menu.css +6 -0
- package/src/layout/styles/search-modal.css +27 -0
- package/src/layout/styles/topbar.css +113 -1
- package/src/lib/canonical-path.js +11 -0
- package/src/lib/config-schema.js +99 -6
- package/src/lib/config-schema.ts +120 -7
- package/src/lib/config.ts +4 -0
- package/src/lib/json-schema-descriptions.js +1 -1
- package/src/lib/mcp-links.js +30 -0
- package/src/lib/mintlify-convert.js +5 -5
- package/src/lib/search-scope.js +44 -0
- package/src/lib/selector-placement.js +30 -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 -5
- 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 +6 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
import type
|
|
2
|
+
import { isExternalHref, type NavTreeNode, type Selector } from "../../lib/config";
|
|
3
3
|
import NavTree from "./NavTree.astro";
|
|
4
|
+
import AppIcon from "../../components/AppIcon.astro";
|
|
4
5
|
// Two colorways of the same "Powered by writedocs" mark, not a
|
|
5
6
|
// theme-matched pair in the wd-logo-light/wd-logo-dark sense (that
|
|
6
7
|
// convention names each file by which theme it's *shown in*) - these
|
|
@@ -32,11 +33,50 @@ interface Props {
|
|
|
32
33
|
navTree: NavTreeNode[];
|
|
33
34
|
hrefForSlug: (slug: string) => string;
|
|
34
35
|
currentSlug: string;
|
|
36
|
+
// Switchers that sit at the top of the sidebar instead of the top bar -
|
|
37
|
+
// products inside a tab or dropdown (lib/selector-placement.js).
|
|
38
|
+
switchers?: Selector[];
|
|
35
39
|
}
|
|
36
|
-
const { navTree, hrefForSlug, currentSlug } = Astro.props as Props;
|
|
40
|
+
const { navTree, hrefForSlug, currentSlug, switchers = [] } = Astro.props as Props;
|
|
37
41
|
---
|
|
38
42
|
|
|
39
43
|
<nav class="wd-sidebar">
|
|
44
|
+
{switchers.map((sel) => {
|
|
45
|
+
const current = sel.options.find((o) => o.active) ?? sel.options[0];
|
|
46
|
+
return (
|
|
47
|
+
<div class="wd-dropdown wd-sidebar-switcher" data-selector-kind={sel.kind}>
|
|
48
|
+
<button type="button" class="wd-dropdown-trigger wd-sidebar-switcher-trigger" aria-expanded="false">
|
|
49
|
+
<AppIcon icon={current.icon} class="wd-tab-icon wd-sidebar-switcher-icon" />
|
|
50
|
+
<span class="wd-sidebar-switcher-text">
|
|
51
|
+
<span class="wd-sidebar-switcher-label">{current.label}</span>
|
|
52
|
+
{current.description && <span class="wd-sidebar-switcher-desc">{current.description}</span>}
|
|
53
|
+
</span>
|
|
54
|
+
<svg class="wd-sidebar-switcher-chevrons" width="12" height="12" viewBox="0 0 12 12" aria-hidden="true">
|
|
55
|
+
<path d="M3.5 4.5L6 2L8.5 4.5M3.5 7.5L6 10L8.5 7.5" stroke="currentColor" stroke-width="1.3" fill="none" stroke-linecap="round" stroke-linejoin="round" />
|
|
56
|
+
</svg>
|
|
57
|
+
</button>
|
|
58
|
+
<div class="wd-dropdown-menu wd-sidebar-switcher-menu">
|
|
59
|
+
<div class="wd-dropdown-menu-panel">
|
|
60
|
+
{sel.options.map((opt) => (
|
|
61
|
+
<a
|
|
62
|
+
href={opt.href}
|
|
63
|
+
class={opt.active ? "active" : ""}
|
|
64
|
+
aria-current={opt.active ? "true" : undefined}
|
|
65
|
+
target={isExternalHref(opt.href) ? "_blank" : undefined}
|
|
66
|
+
rel={isExternalHref(opt.href) ? "noopener noreferrer" : undefined}
|
|
67
|
+
>
|
|
68
|
+
<AppIcon icon={opt.icon} class="wd-tab-icon wd-sidebar-switcher-icon" />
|
|
69
|
+
<span class="wd-sidebar-switcher-text">
|
|
70
|
+
<span class="wd-sidebar-switcher-label">{opt.label}</span>
|
|
71
|
+
{opt.description && <span class="wd-sidebar-switcher-desc">{opt.description}</span>}
|
|
72
|
+
</span>
|
|
73
|
+
</a>
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
})}
|
|
40
80
|
<NavTree nodes={navTree} hrefForSlug={hrefForSlug} currentSlug={currentSlug} />
|
|
41
81
|
{!watermarkDisabled && (
|
|
42
82
|
<div class="wd-sidebar-watermark">
|
|
@@ -46,6 +86,74 @@ const { navTree, hrefForSlug, currentSlug } = Astro.props as Props;
|
|
|
46
86
|
)}
|
|
47
87
|
</nav>
|
|
48
88
|
<style>
|
|
89
|
+
/* The product switcher at the top of the sidebar - Mintlify's place for
|
|
90
|
+
it. Full width, the current product (and its description) on the
|
|
91
|
+
button, every product in the menu. It opens on click only: the shared
|
|
92
|
+
hover-to-open (dropdown.css) would pop it open every time the pointer
|
|
93
|
+
crosses the sidebar. */
|
|
94
|
+
.wd-sidebar-switcher {
|
|
95
|
+
margin-bottom: 1rem;
|
|
96
|
+
}
|
|
97
|
+
.wd-sidebar-switcher-trigger {
|
|
98
|
+
width: 100%;
|
|
99
|
+
box-sizing: border-box;
|
|
100
|
+
gap: 0.6rem;
|
|
101
|
+
padding: 0.5rem 0.65rem;
|
|
102
|
+
border: 1px solid var(--wd-border);
|
|
103
|
+
border-radius: 0.5rem;
|
|
104
|
+
color: var(--wd-text);
|
|
105
|
+
text-align: left;
|
|
106
|
+
}
|
|
107
|
+
.wd-sidebar-switcher-trigger:hover {
|
|
108
|
+
background: var(--wd-surface);
|
|
109
|
+
}
|
|
110
|
+
.wd-sidebar-switcher-text {
|
|
111
|
+
display: flex;
|
|
112
|
+
flex-direction: column;
|
|
113
|
+
flex: 1;
|
|
114
|
+
min-width: 0;
|
|
115
|
+
}
|
|
116
|
+
.wd-sidebar-switcher-label {
|
|
117
|
+
font-size: 0.88rem;
|
|
118
|
+
font-weight: 600;
|
|
119
|
+
overflow: hidden;
|
|
120
|
+
text-overflow: ellipsis;
|
|
121
|
+
white-space: nowrap;
|
|
122
|
+
}
|
|
123
|
+
.wd-sidebar-switcher-desc {
|
|
124
|
+
font-size: 0.75rem;
|
|
125
|
+
font-weight: 400;
|
|
126
|
+
color: var(--wd-text-muted);
|
|
127
|
+
}
|
|
128
|
+
.wd-sidebar-switcher-trigger .wd-sidebar-switcher-desc {
|
|
129
|
+
overflow: hidden;
|
|
130
|
+
text-overflow: ellipsis;
|
|
131
|
+
white-space: nowrap;
|
|
132
|
+
}
|
|
133
|
+
.wd-sidebar-switcher-chevrons {
|
|
134
|
+
flex-shrink: 0;
|
|
135
|
+
color: var(--wd-text-muted);
|
|
136
|
+
}
|
|
137
|
+
.wd-sidebar-switcher:not(.open) .wd-sidebar-switcher-menu {
|
|
138
|
+
display: none;
|
|
139
|
+
}
|
|
140
|
+
.wd-sidebar-switcher-menu {
|
|
141
|
+
right: 0;
|
|
142
|
+
}
|
|
143
|
+
.wd-sidebar-switcher-menu .wd-dropdown-menu-panel {
|
|
144
|
+
min-width: 0;
|
|
145
|
+
}
|
|
146
|
+
.wd-sidebar-switcher-menu a {
|
|
147
|
+
align-items: flex-start;
|
|
148
|
+
}
|
|
149
|
+
.wd-sidebar-switcher-menu .wd-sidebar-switcher-icon {
|
|
150
|
+
margin-top: 0.2rem;
|
|
151
|
+
}
|
|
152
|
+
/* The mobile menu already lists every level as its own row above the
|
|
153
|
+
sidebar - no second copy there. */
|
|
154
|
+
:global(#wd-mobile-menu) .wd-sidebar-switcher {
|
|
155
|
+
display: none;
|
|
156
|
+
}
|
|
49
157
|
/* Sticky + its own scrollbar, not just "moves along until it runs out
|
|
50
158
|
of column to stick within" (position: sticky's default behavior,
|
|
51
159
|
which is what this had before - a tall nav tree would eventually
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// comment for the fuller rationale.
|
|
14
14
|
import { isExternalHref, type DocsConfig, type Selector, type GlobalDropdownView } from '../../lib/config';
|
|
15
15
|
import AppIcon from '../../components/AppIcon.astro';
|
|
16
|
+
import { selectorPlacements, hasMobileMenu } from '../../lib/selector-placement.js';
|
|
16
17
|
import '../styles/dropdown.css';
|
|
17
18
|
import '../styles/topbar.css';
|
|
18
19
|
|
|
@@ -39,7 +40,7 @@ const { config, selectors, globalDropdowns, showSidebarCol, logoLight, logoDark,
|
|
|
39
40
|
// products, or a dropdown used as a nested path segment) on the right,
|
|
40
41
|
// and a second row underneath for tabs plus the always-visible global
|
|
41
42
|
// dropdowns - both of which behave like a row of labeled links rather
|
|
42
|
-
// than a single current-value switcher.
|
|
43
|
+
// than a single current-value switcher. Tabs inside a tab get a third row.
|
|
43
44
|
//
|
|
44
45
|
// A dropdown container nested directly inside a tab (writedocs.json's "API
|
|
45
46
|
// Reference" tab having a `dropdowns` array instead of `pages`) reads as
|
|
@@ -52,21 +53,28 @@ const { config, selectors, globalDropdowns, showSidebarCol, logoLight, logoDark,
|
|
|
52
53
|
// switcher), this one is dropped from switcherSelectors entirely -
|
|
53
54
|
// `selectors` is in root-to-leaf path order, so "nested directly inside a
|
|
54
55
|
// tab" just means the previous entry in the array is the 'tab' selector.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
// Which level goes where - lib/selector-placement.js. A product inside a tab
|
|
57
|
+
// or dropdown sits at the top of the sidebar instead (Sidebar.astro), when
|
|
58
|
+
// the page has one.
|
|
59
|
+
const placements = selectorPlacements(selectors, { sidebar: showSidebarCol });
|
|
60
|
+
const showMenuButton = hasMobileMenu({ sidebar: showSidebarCol, selectors, globalDropdowns });
|
|
61
|
+
const switcherSelectors = selectors.filter((_, i) => placements[i] === 'topbar');
|
|
58
62
|
const tabSelectors = selectors.filter((sel) => sel.kind === 'tab');
|
|
59
|
-
|
|
63
|
+
// One row per level of tabs: a tab holding its own tabs gets a second row
|
|
64
|
+
// underneath, with that tab's tabs. The global dropdowns stay in the first
|
|
65
|
+
// row, which renders on its own when there are only those.
|
|
66
|
+
const tabRows = tabSelectors.length > 0 ? tabSelectors : globalDropdowns.length > 0 ? [null] : [];
|
|
60
67
|
---
|
|
61
68
|
<header class="wd-topbar">
|
|
62
69
|
<div class="wd-topbar-row wd-topbar-row-brand">
|
|
63
70
|
<div class="wd-topbar-inner">
|
|
64
|
-
{/* Only rendered on pages that
|
|
65
|
-
|
|
66
|
-
rendering is gated on). Hidden
|
|
67
|
-
(.wd-mobile-menu-toggle's own media
|
|
68
|
-
JS - so there's no flash-of-hamburger on
|
|
69
|
-
|
|
71
|
+
{/* Only rendered on pages that have a mobile menu - a sidebar, or
|
|
72
|
+
navigation the top bar hides on a phone (hasMobileMenu(), the same
|
|
73
|
+
condition MobileMenu.astro's own rendering is gated on). Hidden
|
|
74
|
+
entirely above 860px via CSS (.wd-mobile-menu-toggle's own media
|
|
75
|
+
query in topbar.css), not JS - so there's no flash-of-hamburger on
|
|
76
|
+
a desktop-width load. */}
|
|
77
|
+
{showMenuButton && (
|
|
70
78
|
<button
|
|
71
79
|
type="button"
|
|
72
80
|
class="wd-mobile-menu-toggle"
|
|
@@ -86,7 +94,7 @@ const showSecondRow = tabSelectors.length > 0 || globalDropdowns.length > 0;
|
|
|
86
94
|
<a class="wd-topbar-brand" href="/">
|
|
87
95
|
{logoLight && <img src={logoLight} alt={config.name} height="24" class="wd-logo-light" />}
|
|
88
96
|
{logoDark && <img src={logoDark} alt={config.name} height="24" class="wd-logo-dark" />}
|
|
89
|
-
{brandLabel && <span>{brandLabel}</span>}
|
|
97
|
+
{brandLabel && <span title={brandLabel}>{brandLabel}</span>}
|
|
90
98
|
</a>
|
|
91
99
|
|
|
92
100
|
{switcherSelectors.length > 0 && (
|
|
@@ -100,7 +108,7 @@ const showSecondRow = tabSelectors.length > 0 || globalDropdowns.length > 0;
|
|
|
100
108
|
const current = sel.options.find((o) => o.active) ?? sel.options[0];
|
|
101
109
|
return (
|
|
102
110
|
<div class="wd-dropdown" data-selector-kind={sel.kind}>
|
|
103
|
-
<button type="button" class="wd-dropdown-trigger wd-switcher-trigger" aria-
|
|
111
|
+
<button type="button" class="wd-dropdown-trigger wd-switcher-trigger" aria-expanded="false">
|
|
104
112
|
<AppIcon icon={current.icon} class="wd-tab-icon" />
|
|
105
113
|
{current.label}
|
|
106
114
|
{current.tag && <span class="wd-selector-tag">{current.tag}</span>}
|
|
@@ -108,13 +116,12 @@ const showSecondRow = tabSelectors.length > 0 || globalDropdowns.length > 0;
|
|
|
108
116
|
<path d="M2 3.5L5 6.5L8 3.5" stroke="currentColor" stroke-width="1.4" fill="none" stroke-linecap="round" stroke-linejoin="round" />
|
|
109
117
|
</svg>
|
|
110
118
|
</button>
|
|
111
|
-
<div class="wd-dropdown-menu"
|
|
119
|
+
<div class="wd-dropdown-menu">
|
|
112
120
|
<div class="wd-dropdown-menu-panel">
|
|
113
121
|
{sel.options.map((opt) => (
|
|
114
122
|
<a
|
|
115
123
|
href={opt.href}
|
|
116
|
-
|
|
117
|
-
class={opt.active ? 'active' : ''}
|
|
124
|
+
class={opt.active ? 'active' : ''} aria-current={opt.active ? 'true' : undefined}
|
|
118
125
|
target={isExternalHref(opt.href) ? '_blank' : undefined}
|
|
119
126
|
rel={isExternalHref(opt.href) ? 'noopener noreferrer' : undefined}
|
|
120
127
|
>
|
|
@@ -161,7 +168,6 @@ const showSecondRow = tabSelectors.length > 0 || globalDropdowns.length > 0;
|
|
|
161
168
|
<button
|
|
162
169
|
type="button"
|
|
163
170
|
class="wd-dropdown-trigger wd-topbar-links-toggle"
|
|
164
|
-
aria-haspopup="true"
|
|
165
171
|
aria-expanded="false"
|
|
166
172
|
aria-label="More links"
|
|
167
173
|
>
|
|
@@ -171,12 +177,11 @@ const showSecondRow = tabSelectors.length > 0 || globalDropdowns.length > 0;
|
|
|
171
177
|
<circle cx="2" cy="14" r="1.7" fill="currentColor" />
|
|
172
178
|
</svg>
|
|
173
179
|
</button>
|
|
174
|
-
<div class="wd-dropdown-menu"
|
|
180
|
+
<div class="wd-dropdown-menu">
|
|
175
181
|
<div class="wd-dropdown-menu-panel">
|
|
176
182
|
{config.topbar.links.map((link) => (
|
|
177
183
|
<a
|
|
178
184
|
href={link.href}
|
|
179
|
-
role="menuitem"
|
|
180
185
|
target={isExternalHref(link.href) ? '_blank' : undefined}
|
|
181
186
|
rel={isExternalHref(link.href) ? 'noopener noreferrer' : undefined}
|
|
182
187
|
aria-label={!link.label ? link.icon : undefined}
|
|
@@ -206,106 +211,125 @@ const showSecondRow = tabSelectors.length > 0 || globalDropdowns.length > 0;
|
|
|
206
211
|
</div>
|
|
207
212
|
</div>
|
|
208
213
|
|
|
209
|
-
{
|
|
210
|
-
<div class=
|
|
214
|
+
{tabRows.map((sel, row) => (
|
|
215
|
+
<div class={`wd-topbar-row wd-topbar-row-tabs${row > 0 ? ' wd-topbar-row-subtabs' : ''}`}>
|
|
211
216
|
<div class="wd-topbar-inner">
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
217
|
+
{/* More tabs than fit: the row slides sideways instead of wrapping,
|
|
218
|
+
with an arrow on each side that has more (src/scripts/tabs-strip.ts).
|
|
219
|
+
The arrows are only for the mouse - keyboard focus slides a tab
|
|
220
|
+
into view by itself. */}
|
|
221
|
+
<div class="wd-tabs-strip" data-tabs-strip>
|
|
222
|
+
<button type="button" class="wd-tabs-arrow wd-tabs-arrow-prev" tabindex="-1" aria-hidden="true" data-tabs-prev>
|
|
223
|
+
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">
|
|
224
|
+
<path d="M8.5 3L4.5 7L8.5 11" stroke="currentColor" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round" />
|
|
225
|
+
</svg>
|
|
226
|
+
</button>
|
|
227
|
+
<div class="wd-tabs-viewport">
|
|
228
|
+
<nav class="wd-topbar-tabs" aria-label={row === 0 ? 'Sections' : 'Subsections'}>
|
|
229
|
+
{(sel?.options ?? []).map((opt) =>
|
|
230
|
+
opt.dropdown ? (
|
|
231
|
+
// A tab whose own content is `dropdowns` (not `pages`)
|
|
232
|
+
// renders as a dropdown-trigger in place of the plain
|
|
233
|
+
// pill - the tab's own label/icon stay on the trigger
|
|
234
|
+
// (not "whichever dropdown is current", so it reads as
|
|
235
|
+
// "this tab, which happens to expand" rather than
|
|
236
|
+
// jumping around as you navigate its sub-pages), and its
|
|
237
|
+
// dropdowns become the menu.
|
|
238
|
+
<div class="wd-dropdown" data-selector-kind="tab">
|
|
239
|
+
<button
|
|
240
|
+
type="button"
|
|
241
|
+
class={`wd-dropdown-trigger wd-tab-link ${opt.active ? 'active' : ''}`}
|
|
242
|
+
aria-expanded="false"
|
|
243
|
+
>
|
|
244
|
+
<AppIcon icon={opt.icon} class="wd-tab-icon" />
|
|
245
|
+
{opt.label}
|
|
246
|
+
<svg class="wd-dropdown-caret" width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
|
|
247
|
+
<path d="M2 3.5L5 6.5L8 3.5" stroke="currentColor" stroke-width="1.4" fill="none" stroke-linecap="round" stroke-linejoin="round" />
|
|
248
|
+
</svg>
|
|
249
|
+
</button>
|
|
250
|
+
<div class="wd-dropdown-menu">
|
|
251
|
+
<div class="wd-dropdown-menu-panel">
|
|
252
|
+
{opt.dropdown.map((d) => (
|
|
253
|
+
<a
|
|
254
|
+
href={d.href}
|
|
255
|
+
class={d.active ? 'active' : ''} aria-current={d.active ? 'true' : undefined}
|
|
256
|
+
target={isExternalHref(d.href) ? '_blank' : undefined}
|
|
257
|
+
rel={isExternalHref(d.href) ? 'noopener noreferrer' : undefined}
|
|
258
|
+
>
|
|
259
|
+
<AppIcon icon={d.icon} class="wd-tab-icon" />
|
|
260
|
+
{d.label}
|
|
261
|
+
</a>
|
|
262
|
+
))}
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
) : (
|
|
267
|
+
<a
|
|
268
|
+
href={opt.href}
|
|
269
|
+
class={`wd-tab-link ${opt.active ? 'active' : ''}`}
|
|
270
|
+
aria-current={opt.active ? 'true' : undefined}
|
|
271
|
+
target={isExternalHref(opt.href) ? '_blank' : undefined}
|
|
272
|
+
rel={isExternalHref(opt.href) ? 'noopener noreferrer' : undefined}
|
|
229
273
|
>
|
|
230
274
|
<AppIcon icon={opt.icon} class="wd-tab-icon" />
|
|
231
275
|
{opt.label}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
276
|
+
</a>
|
|
277
|
+
)
|
|
278
|
+
)}
|
|
279
|
+
{row === 0 &&
|
|
280
|
+
globalDropdowns.map((d) =>
|
|
281
|
+
d.href !== null ? (
|
|
282
|
+
<a
|
|
283
|
+
href={d.href}
|
|
284
|
+
class="wd-tab-link"
|
|
285
|
+
target={isExternalHref(d.href) ? '_blank' : undefined}
|
|
286
|
+
rel={isExternalHref(d.href) ? 'noopener noreferrer' : undefined}
|
|
287
|
+
>
|
|
288
|
+
<AppIcon icon={d.icon} class="wd-tab-icon" />
|
|
289
|
+
{d.label}
|
|
290
|
+
</a>
|
|
291
|
+
) : (
|
|
292
|
+
<div class="wd-dropdown">
|
|
293
|
+
<button type="button" class="wd-dropdown-trigger" aria-expanded="false">
|
|
294
|
+
<AppIcon icon={d.icon} class="wd-tab-icon" />
|
|
295
|
+
{d.label}
|
|
296
|
+
<svg class="wd-dropdown-caret" width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
|
|
297
|
+
<path d="M2 3.5L5 6.5L8 3.5" stroke="currentColor" stroke-width="1.4" fill="none" stroke-linecap="round" stroke-linejoin="round" />
|
|
298
|
+
</svg>
|
|
299
|
+
</button>
|
|
300
|
+
<div class="wd-dropdown-menu">
|
|
301
|
+
<div class="wd-dropdown-menu-panel">
|
|
302
|
+
{d.items.map((item) => (
|
|
303
|
+
<a href={item.href} class={item.active ? 'active' : ''} aria-current={item.active ? 'true' : undefined}>{item.label}</a>
|
|
304
|
+
))}
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
250
307
|
</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>
|
|
308
|
+
)
|
|
309
|
+
)}
|
|
310
|
+
</nav>
|
|
311
|
+
</div>
|
|
312
|
+
<button type="button" class="wd-tabs-arrow wd-tabs-arrow-next" tabindex="-1" aria-hidden="true" data-tabs-next>
|
|
313
|
+
<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true">
|
|
314
|
+
<path d="M5.5 3L9.5 7L5.5 11" stroke="currentColor" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round" />
|
|
315
|
+
</svg>
|
|
316
|
+
</button>
|
|
317
|
+
</div>
|
|
297
318
|
</div>
|
|
298
319
|
</div>
|
|
299
|
-
)}
|
|
320
|
+
))}
|
|
300
321
|
</header>
|
|
301
322
|
<script>
|
|
302
323
|
import { initDropdowns } from '../../scripts/dropdowns';
|
|
303
324
|
import { initThemeToggle } from '../../scripts/theme-toggle';
|
|
325
|
+
import { initTabsStrips } from '../../scripts/tabs-strip';
|
|
304
326
|
|
|
305
327
|
initDropdowns(document);
|
|
306
328
|
initThemeToggle(document);
|
|
329
|
+
initTabsStrips(document);
|
|
307
330
|
document.addEventListener('astro:page-load', () => {
|
|
308
331
|
initDropdowns(document);
|
|
309
332
|
initThemeToggle(document);
|
|
333
|
+
initTabsStrips(document);
|
|
310
334
|
});
|
|
311
335
|
</script>
|
|
@@ -146,6 +146,12 @@
|
|
|
146
146
|
padding-bottom: 0.75rem;
|
|
147
147
|
border-bottom: 1px solid var(--wd-border);
|
|
148
148
|
}
|
|
149
|
+
/* A page without a sidebar (`mode: custom`): nothing below to divide off. */
|
|
150
|
+
.wd-mobile-menu-selectors:last-child {
|
|
151
|
+
margin-bottom: 0;
|
|
152
|
+
padding-bottom: 0;
|
|
153
|
+
border-bottom: none;
|
|
154
|
+
}
|
|
149
155
|
/* position: relative makes this the containing block for its own
|
|
150
156
|
.wd-mobile-accordion-options panel below, which floats over whatever
|
|
151
157
|
comes after it instead of pushing it down the page - a `<details>`'s
|
|
@@ -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
|
|
@@ -417,4 +523,10 @@
|
|
|
417
523
|
.wd-mobile-menu-toggle {
|
|
418
524
|
display: flex;
|
|
419
525
|
}
|
|
526
|
+
/* The site name takes what's left of the row and ends in "..." when it
|
|
527
|
+
doesn't fit - otherwise a long name wraps onto a row of its own and the
|
|
528
|
+
top bar grows to three rows. */
|
|
529
|
+
.wd-topbar-brand {
|
|
530
|
+
flex: 1 1 0;
|
|
531
|
+
}
|
|
420
532
|
}
|
|
@@ -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
|
+
}
|