@nubisco/docs-sdk 0.0.0-stage → 0.0.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 +20 -0
- package/README.md +101 -2
- package/THIRD_PARTY_NOTICES.md +100 -0
- package/dist/cli.js +33 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.js +361 -0
- package/package.json +122 -4
- package/src/plugins/mermaid/Mermaid.vue +93 -0
- package/src/plugins/mermaid/index.ts +66 -0
- package/src/plugins/mermaid/mermaid-markdown.ts +34 -0
- package/src/plugins/mermaid/mermaid-plugin.ts +46 -0
- package/src/plugins/mermaid/mermaid.ts +21 -0
- package/src/plugins/tabs/components/ContentTab.vue +41 -0
- package/src/plugins/tabs/components/ContentTabs.vue +150 -0
- package/src/plugins/tabs/components/index.ts +10 -0
- package/src/plugins/tabs/components/useStabilizeScrollPosition.ts +36 -0
- package/src/plugins/tabs/components/useTabLabels.ts +18 -0
- package/src/plugins/tabs/components/useTabsSelectedState.ts +95 -0
- package/src/plugins/tabs/components/useTabsSingleState.ts +22 -0
- package/src/plugins/tabs/index.ts +1 -0
- package/src/plugins/tabs/markdown/index.ts +1 -0
- package/src/plugins/tabs/markdown/markdownPlugin.ts +47 -0
- package/src/plugins/tabs/markdown/ruleBlockTab.ts +97 -0
- package/src/theme/Layout.vue +32 -0
- package/src/theme/Nubisco.vue +480 -0
- package/src/theme/components/DocHeader.vue +599 -0
- package/src/theme/components/DocOutline.vue +235 -0
- package/src/theme/components/DocsControls.vue +173 -0
- package/src/theme/components/DocsSidebarNav.vue +313 -0
- package/src/theme/custom.css +591 -0
- package/src/theme/index.ts +33 -0
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<header
|
|
3
|
+
ref="hero"
|
|
4
|
+
class="doc-header"
|
|
5
|
+
:class="{ 'doc-header--plain': !tabs.length }"
|
|
6
|
+
>
|
|
7
|
+
<div class="doc-header__hero">
|
|
8
|
+
<div class="doc-header__lead">
|
|
9
|
+
<h1 class="doc-header__title">{{ title }}</h1>
|
|
10
|
+
<div v-if="status || updatedLabel" class="doc-header__meta">
|
|
11
|
+
<NbBadge v-if="status" :variant="statusVariant" size="sm">{{
|
|
12
|
+
statusLabel
|
|
13
|
+
}}</NbBadge>
|
|
14
|
+
<span v-if="updatedLabel">Last updated {{ updatedLabel }}</span>
|
|
15
|
+
</div>
|
|
16
|
+
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
17
|
+
<p v-if="intro" class="doc-header__intro" v-html="introHtml" />
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<aside v-if="links.length" class="doc-header__side" aria-label="Links">
|
|
21
|
+
<div class="doc-header__side-title">
|
|
22
|
+
<img v-if="logoSrc" :src="logoSrc" alt="" width="20" height="20" />
|
|
23
|
+
<span>{{ siteTitle }}</span>
|
|
24
|
+
</div>
|
|
25
|
+
<ul class="doc-header__links">
|
|
26
|
+
<li v-for="link in links" :key="link.href">
|
|
27
|
+
<a
|
|
28
|
+
:href="link.href"
|
|
29
|
+
target="_blank"
|
|
30
|
+
rel="noreferrer"
|
|
31
|
+
class="doc-header__link"
|
|
32
|
+
>
|
|
33
|
+
<NbIcon :name="link.icon" :size="16" aria-hidden="true" />
|
|
34
|
+
<span>{{ link.text }}</span>
|
|
35
|
+
</a>
|
|
36
|
+
</li>
|
|
37
|
+
</ul>
|
|
38
|
+
</aside>
|
|
39
|
+
</div>
|
|
40
|
+
</header>
|
|
41
|
+
|
|
42
|
+
<!--
|
|
43
|
+
The tab bar, and the compact title that joins it once the hero has
|
|
44
|
+
scrolled away. The title is in the same row as the tabs and only grows
|
|
45
|
+
in, so the bar keeps one height and nothing under it moves when it
|
|
46
|
+
sticks.
|
|
47
|
+
|
|
48
|
+
It is a sibling of the hero, not a child: a sticky element only sticks
|
|
49
|
+
inside its parent, and the hero scrolls away.
|
|
50
|
+
-->
|
|
51
|
+
<div
|
|
52
|
+
v-if="tabs.length"
|
|
53
|
+
ref="bar"
|
|
54
|
+
class="doc-header doc-header__bar"
|
|
55
|
+
:class="{ 'is-stuck': stuck }"
|
|
56
|
+
>
|
|
57
|
+
<nav class="doc-header__tabs" aria-label="Page sections">
|
|
58
|
+
<span class="doc-header__compact-title" :aria-hidden="!stuck">{{
|
|
59
|
+
title
|
|
60
|
+
}}</span>
|
|
61
|
+
<component
|
|
62
|
+
:is="tab.href ? 'a' : 'button'"
|
|
63
|
+
v-for="tab in tabs"
|
|
64
|
+
:key="tab.key"
|
|
65
|
+
:href="tab.href"
|
|
66
|
+
:type="tab.href ? undefined : 'button'"
|
|
67
|
+
class="doc-header__tab"
|
|
68
|
+
:class="{ 'is-active': tab.active }"
|
|
69
|
+
:aria-current="tab.active && tab.href ? 'page' : undefined"
|
|
70
|
+
:aria-pressed="tab.href ? undefined : tab.active"
|
|
71
|
+
@click="onTab($event, tab)"
|
|
72
|
+
>
|
|
73
|
+
{{ tab.label }}
|
|
74
|
+
</component>
|
|
75
|
+
<span class="doc-header__tabs-end">
|
|
76
|
+
<a
|
|
77
|
+
v-for="link in links.slice(0, 1)"
|
|
78
|
+
:key="link.href"
|
|
79
|
+
:href="link.href"
|
|
80
|
+
target="_blank"
|
|
81
|
+
rel="noreferrer"
|
|
82
|
+
class="doc-header__compact-link"
|
|
83
|
+
:aria-label="link.text"
|
|
84
|
+
:tabindex="stuck ? 0 : -1"
|
|
85
|
+
>
|
|
86
|
+
<NbIcon :name="link.icon" :size="18" aria-hidden="true" />
|
|
87
|
+
</a>
|
|
88
|
+
</span>
|
|
89
|
+
</nav>
|
|
90
|
+
</div>
|
|
91
|
+
</template>
|
|
92
|
+
|
|
93
|
+
<script setup lang="ts">
|
|
94
|
+
/**
|
|
95
|
+
* The page header every documentation page wears, modelled on Carbon's.
|
|
96
|
+
*
|
|
97
|
+
* A large title, a status tag when the page declares one, the intro paragraph
|
|
98
|
+
* and a block of links, then the tab bar. Once the hero scrolls under the
|
|
99
|
+
* site header, the bar sticks with the title in a compact row above the tabs,
|
|
100
|
+
* so a reader deep in a long page still knows which component and which tab
|
|
101
|
+
* they are on.
|
|
102
|
+
*/
|
|
103
|
+
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
104
|
+
import { useData, useRouter, withBase } from 'vitepress'
|
|
105
|
+
import { NbBadge, NbIcon } from '@nubisco/ui'
|
|
106
|
+
import * as iconGithub from '@nubisco/ui/icons/github-logo'
|
|
107
|
+
import * as iconPencil from '@nubisco/ui/icons/pencil-simple'
|
|
108
|
+
import * as iconBug from '@nubisco/ui/icons/bug'
|
|
109
|
+
import type { INubiscoDocsThemeConfig } from '../../node/types'
|
|
110
|
+
|
|
111
|
+
export interface IDocHeaderTab {
|
|
112
|
+
key: string
|
|
113
|
+
label: string
|
|
114
|
+
href?: string
|
|
115
|
+
active: boolean
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const props = defineProps<{
|
|
119
|
+
title?: string
|
|
120
|
+
intro?: string
|
|
121
|
+
status?: string
|
|
122
|
+
source?: string
|
|
123
|
+
editLink?: string | null
|
|
124
|
+
tabs: IDocHeaderTab[]
|
|
125
|
+
}>()
|
|
126
|
+
|
|
127
|
+
const emit = defineEmits<{ select: [key: string] }>()
|
|
128
|
+
|
|
129
|
+
const { site, page, theme } = useData()
|
|
130
|
+
const router = useRouter()
|
|
131
|
+
|
|
132
|
+
const STATUS: Record<string, string> = {
|
|
133
|
+
stable: 'green',
|
|
134
|
+
beta: 'blue',
|
|
135
|
+
experimental: 'purple',
|
|
136
|
+
deprecated: 'red',
|
|
137
|
+
draft: 'grey',
|
|
138
|
+
labs: 'purple',
|
|
139
|
+
}
|
|
140
|
+
const statusLabel = computed(() => {
|
|
141
|
+
const s = props.status ?? ''
|
|
142
|
+
return s.charAt(0).toUpperCase() + s.slice(1)
|
|
143
|
+
})
|
|
144
|
+
const statusVariant = computed(
|
|
145
|
+
() => (STATUS[(props.status ?? '').toLowerCase()] ?? 'grey') as 'grey',
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
const updatedLabel = computed(() =>
|
|
149
|
+
page.value.lastUpdated
|
|
150
|
+
? new Date(page.value.lastUpdated).toLocaleDateString('en-GB', {
|
|
151
|
+
day: 'numeric',
|
|
152
|
+
month: 'short',
|
|
153
|
+
year: 'numeric',
|
|
154
|
+
})
|
|
155
|
+
: '',
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
// The site's own name and mark, from the VitePress config, so the links block
|
|
159
|
+
// reads as the product the page belongs to.
|
|
160
|
+
const siteTitle = computed(() => theme.value.siteTitle || site.value.title)
|
|
161
|
+
const logoSrc = computed(() => {
|
|
162
|
+
const logo = theme.value.logo
|
|
163
|
+
const src = typeof logo === 'string' ? logo : (logo?.src ?? logo?.light)
|
|
164
|
+
return src ? withBase(src) : ''
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const nubisco = computed<INubiscoDocsThemeConfig | undefined>(
|
|
168
|
+
() => theme.value.nubisco,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
const links = computed(() => {
|
|
172
|
+
const out: { href: string; text: string; icon: unknown }[] = []
|
|
173
|
+
const repo = nubisco.value?.repo
|
|
174
|
+
const branch = nubisco.value?.branch ?? 'master'
|
|
175
|
+
if (props.source && repo)
|
|
176
|
+
out.push({
|
|
177
|
+
href: `https://github.com/${repo}/blob/${branch}/${props.source}`,
|
|
178
|
+
text: 'Source on GitHub',
|
|
179
|
+
icon: iconGithub,
|
|
180
|
+
})
|
|
181
|
+
if (props.editLink)
|
|
182
|
+
out.push({
|
|
183
|
+
href: props.editLink,
|
|
184
|
+
text: 'Edit this page',
|
|
185
|
+
icon: iconPencil,
|
|
186
|
+
})
|
|
187
|
+
if (props.source && repo)
|
|
188
|
+
out.push({
|
|
189
|
+
href: `https://github.com/${repo}/issues/new`,
|
|
190
|
+
text: 'Report an issue',
|
|
191
|
+
icon: iconBug,
|
|
192
|
+
})
|
|
193
|
+
return out
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
/*
|
|
197
|
+
* The intro is one paragraph of inline Markdown carried in frontmatter, so it
|
|
198
|
+
* can be shown on every tab of a page without repeating it in each file. Only
|
|
199
|
+
* the inline forms an intro uses are handled: code, bold, emphasis and links.
|
|
200
|
+
*/
|
|
201
|
+
function escape(s: string) {
|
|
202
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
203
|
+
}
|
|
204
|
+
function href(url: string) {
|
|
205
|
+
if (!url.startsWith('/')) return url
|
|
206
|
+
const [path, hash] = url.split('#')
|
|
207
|
+
const ext = site.value.cleanUrls || /\.\w+$/.test(path) ? '' : '.html'
|
|
208
|
+
return withBase(path + ext) + (hash ? `#${hash}` : '')
|
|
209
|
+
}
|
|
210
|
+
const introHtml = computed(() => {
|
|
211
|
+
const codes: string[] = []
|
|
212
|
+
let s = escape(props.intro ?? '').replace(/`([^`]+)`/g, (_, c) => {
|
|
213
|
+
codes.push(`<code>${c}</code>`)
|
|
214
|
+
return `\u0000${codes.length - 1}\u0000`
|
|
215
|
+
})
|
|
216
|
+
s = s
|
|
217
|
+
.replace(
|
|
218
|
+
/\[([^\]]+)\]\(([^)\s]+)\)/g,
|
|
219
|
+
(_, t, u) => `<a href="${href(u)}">${t}</a>`,
|
|
220
|
+
)
|
|
221
|
+
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
222
|
+
.replace(/(^|[^\w])_([^_]+)_(?=[^\w]|$)/g, '$1<em>$2</em>')
|
|
223
|
+
.replace(/(^|[^*])\*([^*]+)\*/g, '$1<em>$2</em>')
|
|
224
|
+
// eslint-disable-next-line no-control-regex
|
|
225
|
+
return s.replace(/\u0000(\d+)\u0000/g, (_, i) => codes[Number(i)])
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
// ── Sticky state ──────────────────────────────────────────────────────────
|
|
229
|
+
const hero = ref<HTMLElement | null>(null)
|
|
230
|
+
const bar = ref<HTMLElement | null>(null)
|
|
231
|
+
const stuck = ref(false)
|
|
232
|
+
|
|
233
|
+
/** Where the bar sticks: under the site header, or the local nav on narrow screens. */
|
|
234
|
+
function stickTop() {
|
|
235
|
+
return bar.value ? parseFloat(getComputedStyle(bar.value).top) || 0 : 0
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const clamp = (value: number) => Math.min(1, Math.max(0, value))
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* `--dock` runs from 0 (top of the page) to 1 (the bar is pinned under the
|
|
242
|
+
* site header). It closes the inset around the hero and the bar together, so
|
|
243
|
+
* the card turns into a band edge to edge and the two edges stay flush the
|
|
244
|
+
* whole way. `--dock-late` brings the compact title in only near the end,
|
|
245
|
+
* once the big title has left. Both are written straight to the element as
|
|
246
|
+
* custom properties: going through reactive state would re-render the header
|
|
247
|
+
* on every scroll frame.
|
|
248
|
+
*/
|
|
249
|
+
let frame = 0
|
|
250
|
+
function measure() {
|
|
251
|
+
frame = 0
|
|
252
|
+
if (!bar.value || !hero.value) return
|
|
253
|
+
// The distance the bar has to travel to reach the site header, measured
|
|
254
|
+
// from the hero's own bottom edge so it holds at any height.
|
|
255
|
+
const travel = Math.max(
|
|
256
|
+
1,
|
|
257
|
+
hero.value.getBoundingClientRect().bottom + window.scrollY - stickTop(),
|
|
258
|
+
)
|
|
259
|
+
const dock = clamp(window.scrollY / travel)
|
|
260
|
+
const late = clamp((dock - 0.6) / 0.4)
|
|
261
|
+
|
|
262
|
+
const host = hero.value.parentElement
|
|
263
|
+
host?.style.setProperty('--dock', dock.toFixed(3))
|
|
264
|
+
host?.style.setProperty('--dock-late', late.toFixed(3))
|
|
265
|
+
stuck.value = late > 0.5
|
|
266
|
+
}
|
|
267
|
+
function onScroll() {
|
|
268
|
+
if (!frame) frame = requestAnimationFrame(measure)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
onMounted(() => {
|
|
272
|
+
window.addEventListener('scroll', onScroll, { passive: true })
|
|
273
|
+
window.addEventListener('resize', onScroll, { passive: true })
|
|
274
|
+
measure()
|
|
275
|
+
})
|
|
276
|
+
onBeforeUnmount(() => {
|
|
277
|
+
window.removeEventListener('scroll', onScroll)
|
|
278
|
+
window.removeEventListener('resize', onScroll)
|
|
279
|
+
if (frame) cancelAnimationFrame(frame)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Switching tab while the bar is stuck keeps it stuck: the new tab opens at
|
|
284
|
+
* its own first line, under the bar, instead of jumping back to the top of
|
|
285
|
+
* the hero.
|
|
286
|
+
*/
|
|
287
|
+
async function onTab(event: MouseEvent, tab: IDocHeaderTab) {
|
|
288
|
+
if (!tab.href) {
|
|
289
|
+
emit('select', tab.key)
|
|
290
|
+
keepPlace()
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.button !== 0)
|
|
294
|
+
return
|
|
295
|
+
event.preventDefault()
|
|
296
|
+
const wasStuck = stuck.value
|
|
297
|
+
await router.go(tab.href)
|
|
298
|
+
if (wasStuck) keepPlace()
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function keepPlace() {
|
|
302
|
+
requestAnimationFrame(() => {
|
|
303
|
+
if (!bar.value || !hero.value) return
|
|
304
|
+
// The scroll position at which the bar has just reached its sticky top.
|
|
305
|
+
const heroBottom =
|
|
306
|
+
hero.value.getBoundingClientRect().bottom + window.scrollY
|
|
307
|
+
const y = heroBottom - stickTop()
|
|
308
|
+
if (window.scrollY > y) window.scrollTo({ top: y })
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
</script>
|
|
312
|
+
|
|
313
|
+
<style lang="scss" scoped>
|
|
314
|
+
@use '@nubisco/ui/variables' as v;
|
|
315
|
+
|
|
316
|
+
/*
|
|
317
|
+
* Carbon's structure, in Nubisco's tokens: the header is a layer-1 card on
|
|
318
|
+
* the layer-0 page, divided from it by a hairline, and the only colour in it
|
|
319
|
+
* is the accent on the active tab.
|
|
320
|
+
*
|
|
321
|
+
* At the top of the page the card is inset by `--doc-inset`, so the ground
|
|
322
|
+
* shows around it. The hero and the bar close that inset together, on the one
|
|
323
|
+
* `--dock` curve the script writes, and end as a band edge to edge. The text
|
|
324
|
+
* does not move while they do: each side's padding takes up what its margin
|
|
325
|
+
* gives.
|
|
326
|
+
*/
|
|
327
|
+
.doc-header {
|
|
328
|
+
--doc-inset: calc(var(--nb-base-unit) * 2);
|
|
329
|
+
|
|
330
|
+
color: var(--nb-c-text);
|
|
331
|
+
|
|
332
|
+
// The hairline closes whichever comes last: the tab bar, or the hero on a
|
|
333
|
+
// page without tabs.
|
|
334
|
+
&--plain &__hero,
|
|
335
|
+
&__bar {
|
|
336
|
+
border-bottom: 1px solid var(--nb-c-layer-2);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
&__hero {
|
|
340
|
+
// The card is the hero and the bar, not the whole header: the root stays
|
|
341
|
+
// clear so the ground shows in the inset.
|
|
342
|
+
background: var(--nb-c-layer-1);
|
|
343
|
+
display: grid;
|
|
344
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
345
|
+
gap: calc(var(--nb-base-unit) * 4);
|
|
346
|
+
align-items: end;
|
|
347
|
+
// The top inset stays fixed: it changes the card's height, and that
|
|
348
|
+
// height is what `--dock` is measured against.
|
|
349
|
+
margin: var(--doc-inset) calc((1 - var(--dock, 0)) * var(--doc-inset)) 0;
|
|
350
|
+
// The left edge lines up with the tab labels below, which sit one tab
|
|
351
|
+
// padding in from the bar.
|
|
352
|
+
padding: calc(var(--nb-base-unit) * 6)
|
|
353
|
+
calc(var(--nb-base-unit) * 3 + var(--dock, 0) * var(--doc-inset))
|
|
354
|
+
calc(var(--nb-base-unit) * 3)
|
|
355
|
+
calc(var(--nb-base-unit) * 5 + var(--dock, 0) * var(--doc-inset));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
&__title {
|
|
359
|
+
margin: 0;
|
|
360
|
+
font-size: clamp(2.25rem, 4.2vw, 3.5rem);
|
|
361
|
+
font-weight: 300;
|
|
362
|
+
line-height: 1.1;
|
|
363
|
+
letter-spacing: -0.015em;
|
|
364
|
+
color: var(--nb-c-text);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
&__meta {
|
|
368
|
+
display: flex;
|
|
369
|
+
align-items: center;
|
|
370
|
+
gap: calc(var(--nb-base-unit) * 1.5);
|
|
371
|
+
margin-top: calc(var(--nb-base-unit) * 2);
|
|
372
|
+
font-size: 0.875rem;
|
|
373
|
+
color: var(--nb-c-text-muted);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
&__intro {
|
|
377
|
+
max-width: 44rem;
|
|
378
|
+
margin: calc(var(--nb-base-unit) * 5) 0 0;
|
|
379
|
+
font-size: 1.25rem;
|
|
380
|
+
line-height: 1.5;
|
|
381
|
+
color: var(--nb-c-text);
|
|
382
|
+
|
|
383
|
+
:deep(code) {
|
|
384
|
+
font-family: var(--nb-font-family-mono, monospace);
|
|
385
|
+
font-size: 0.85em;
|
|
386
|
+
background: var(--nb-c-layer-2);
|
|
387
|
+
padding: 1px 6px;
|
|
388
|
+
border-radius: 4px;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
:deep(a) {
|
|
392
|
+
color: var(--vp-c-brand-1);
|
|
393
|
+
text-decoration: underline;
|
|
394
|
+
text-underline-offset: 3px;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
&__side {
|
|
399
|
+
min-width: 13rem;
|
|
400
|
+
padding-bottom: calc(var(--nb-base-unit) * 0.5);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
&__side-title {
|
|
404
|
+
display: flex;
|
|
405
|
+
align-items: center;
|
|
406
|
+
gap: var(--nb-base-unit);
|
|
407
|
+
font-weight: 600;
|
|
408
|
+
font-size: 0.95rem;
|
|
409
|
+
margin-bottom: calc(var(--nb-base-unit) * 1.5);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
&__links {
|
|
413
|
+
list-style: none;
|
|
414
|
+
margin: 0;
|
|
415
|
+
padding: 0;
|
|
416
|
+
display: grid;
|
|
417
|
+
gap: var(--nb-base-unit);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
&__link {
|
|
421
|
+
display: inline-flex;
|
|
422
|
+
align-items: center;
|
|
423
|
+
gap: var(--nb-base-unit);
|
|
424
|
+
font-size: 0.9rem;
|
|
425
|
+
color: var(--vp-c-brand-1);
|
|
426
|
+
text-decoration: none;
|
|
427
|
+
|
|
428
|
+
&:hover {
|
|
429
|
+
text-decoration: underline;
|
|
430
|
+
text-underline-offset: 3px;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/* ── The bar ─────────────────────────────────────────────────────────── */
|
|
435
|
+
&__bar {
|
|
436
|
+
position: sticky;
|
|
437
|
+
top: var(--vp-nav-height, 64px);
|
|
438
|
+
z-index: 20;
|
|
439
|
+
background: var(--nb-c-layer-1);
|
|
440
|
+
margin: 0 calc((1 - var(--dock, 0)) * var(--doc-inset));
|
|
441
|
+
padding: 0 calc(var(--dock, 0) * var(--doc-inset));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// The compact title sits in the tab row and grows in near the end of the
|
|
445
|
+
// dock, so the bar is one row at every point and nothing under it moves.
|
|
446
|
+
&__compact-title {
|
|
447
|
+
flex: none;
|
|
448
|
+
align-self: center;
|
|
449
|
+
overflow: hidden;
|
|
450
|
+
max-width: calc(var(--dock-late, 0) * 16rem);
|
|
451
|
+
margin-right: calc(var(--dock-late, 0) * var(--nb-base-unit) * 3);
|
|
452
|
+
opacity: var(--dock-late, 0);
|
|
453
|
+
font-size: 1rem;
|
|
454
|
+
font-weight: 600;
|
|
455
|
+
white-space: nowrap;
|
|
456
|
+
text-overflow: ellipsis;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
&__tabs {
|
|
460
|
+
display: flex;
|
|
461
|
+
align-items: stretch;
|
|
462
|
+
padding: 0 calc(var(--nb-base-unit) * 3);
|
|
463
|
+
overflow-x: auto;
|
|
464
|
+
scrollbar-width: none;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
&__tab {
|
|
468
|
+
appearance: none;
|
|
469
|
+
position: relative;
|
|
470
|
+
display: inline-flex;
|
|
471
|
+
align-items: center;
|
|
472
|
+
flex: none;
|
|
473
|
+
min-height: calc(var(--nb-base-unit) * 6);
|
|
474
|
+
padding: 0 calc(var(--nb-base-unit) * 2);
|
|
475
|
+
border: 0;
|
|
476
|
+
background: transparent;
|
|
477
|
+
font: inherit;
|
|
478
|
+
font-size: 0.95rem;
|
|
479
|
+
color: var(--nb-c-text-subtle);
|
|
480
|
+
text-decoration: none;
|
|
481
|
+
cursor: pointer;
|
|
482
|
+
transition: color 0.15s ease;
|
|
483
|
+
|
|
484
|
+
// Carbon draws a hairline under every tab and the accent under the
|
|
485
|
+
// current one. Both sit on the bar's bottom edge.
|
|
486
|
+
&::after {
|
|
487
|
+
content: '';
|
|
488
|
+
position: absolute;
|
|
489
|
+
left: 0;
|
|
490
|
+
right: 0;
|
|
491
|
+
bottom: 0;
|
|
492
|
+
height: 2px;
|
|
493
|
+
background: var(--nb-c-layer-2);
|
|
494
|
+
transition:
|
|
495
|
+
background 0.15s ease,
|
|
496
|
+
height 0.15s ease;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
&:hover {
|
|
500
|
+
color: var(--nb-c-text);
|
|
501
|
+
|
|
502
|
+
&::after {
|
|
503
|
+
background: var(--nb-c-layer-border-1);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
&.is-active {
|
|
508
|
+
color: var(--nb-c-text);
|
|
509
|
+
font-weight: 600;
|
|
510
|
+
|
|
511
|
+
&::after {
|
|
512
|
+
height: 3px;
|
|
513
|
+
background: var(--nb-c-primary);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
&:focus-visible {
|
|
518
|
+
outline: 2px solid var(--nb-c-primary);
|
|
519
|
+
outline-offset: -2px;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
&__tabs-end {
|
|
524
|
+
margin-left: auto;
|
|
525
|
+
display: flex;
|
|
526
|
+
align-items: center;
|
|
527
|
+
padding-left: calc(var(--nb-base-unit) * 2);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
&__compact-link {
|
|
531
|
+
display: inline-flex;
|
|
532
|
+
padding: calc(var(--nb-base-unit) * 0.75);
|
|
533
|
+
color: var(--nb-c-text);
|
|
534
|
+
opacity: 0;
|
|
535
|
+
pointer-events: none;
|
|
536
|
+
transition: opacity 0.18s ease;
|
|
537
|
+
|
|
538
|
+
.is-stuck & {
|
|
539
|
+
opacity: 1;
|
|
540
|
+
pointer-events: auto;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/* Below VitePress's own breakpoint the site header scrolls away and the local
|
|
546
|
+
nav (the "Menu" row) is what stays at the top, so the bar sticks under it. */
|
|
547
|
+
@media (max-width: 959px) {
|
|
548
|
+
.doc-header__bar {
|
|
549
|
+
top: 48px;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
@include v.phone {
|
|
554
|
+
.doc-header {
|
|
555
|
+
// No ground to show around the card on a phone.
|
|
556
|
+
--doc-inset: 0px;
|
|
557
|
+
|
|
558
|
+
&__hero {
|
|
559
|
+
grid-template-columns: minmax(0, 1fr);
|
|
560
|
+
gap: calc(var(--nb-base-unit) * 3);
|
|
561
|
+
padding: calc(var(--nb-base-unit) * 4) calc(var(--nb-base-unit) * 2)
|
|
562
|
+
calc(var(--nb-base-unit) * 2);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
&__intro {
|
|
566
|
+
margin-top: calc(var(--nb-base-unit) * 3);
|
|
567
|
+
font-size: 1.075rem;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
&__side-title {
|
|
571
|
+
display: none;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
&__links {
|
|
575
|
+
display: flex;
|
|
576
|
+
flex-wrap: wrap;
|
|
577
|
+
gap: var(--nb-base-unit) calc(var(--nb-base-unit) * 2.5);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
&__link {
|
|
581
|
+
white-space: nowrap;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// On a phone the tab row is the whole bar, with no compact title: the
|
|
585
|
+
// name is already in the local nav's breadcrumb.
|
|
586
|
+
&__compact-title {
|
|
587
|
+
display: none;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
&__tabs {
|
|
591
|
+
padding: 0 var(--nb-base-unit);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
&__tabs-end {
|
|
595
|
+
display: none;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
</style>
|