@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.
@@ -0,0 +1,480 @@
1
+ <template>
2
+ <div class="nb-layout">
3
+ <DocHeader
4
+ :title="frontmatter.title"
5
+ :intro="frontmatter.intro"
6
+ :status="frontmatter.status"
7
+ :source="frontmatter.source"
8
+ :edit-link="editLink"
9
+ :tabs="headerTabs"
10
+ @select="activeTab = $event"
11
+ />
12
+
13
+ <div class="nb-layout__main">
14
+ <main ref="body" class="nb-layout__body">
15
+ <Content />
16
+ </main>
17
+ <aside class="nb-layout__aside">
18
+ <div class="nb-layout__aside-inner">
19
+ <DocOutline :root="body" :watch-key="activeTab" />
20
+ </div>
21
+ </aside>
22
+ </div>
23
+
24
+ <!-- Footer: prev/next navigation + meta -->
25
+ <footer class="nb-layout__footer">
26
+ <nav v-if="prev || next" class="nb-footer__nav">
27
+ <a
28
+ v-if="prev"
29
+ :href="withBase(prev.link)"
30
+ class="nb-footer__nav-item nb-footer__nav-item--prev"
31
+ >
32
+ <span class="nb-footer__nav-label">Previous</span>
33
+ <span class="nb-footer__nav-title">{{ prev.text }}</span>
34
+ </a>
35
+ <span v-else class="nb-footer__nav-spacer" />
36
+
37
+ <a
38
+ v-if="next"
39
+ :href="withBase(next.link)"
40
+ class="nb-footer__nav-item nb-footer__nav-item--next"
41
+ >
42
+ <span class="nb-footer__nav-label">Next</span>
43
+ <span class="nb-footer__nav-title">{{ next.text }}</span>
44
+ </a>
45
+ <span v-else class="nb-footer__nav-spacer" />
46
+ </nav>
47
+
48
+ <div class="nb-footer__bar">
49
+ <a :href="withBase('/')" class="nb-footer__brand">
50
+ <NbNubiscoMark :size="24" class="nb-footer__logo" />
51
+ <span class="nb-footer__brand-name">Nubisco</span>
52
+ </a>
53
+
54
+ <div class="nb-footer__meta">
55
+ <a
56
+ v-if="editLink"
57
+ :href="editLink"
58
+ target="_blank"
59
+ rel="noreferrer"
60
+ class="nb-footer__edit-link"
61
+ >
62
+ Edit this page on GitHub
63
+ </a>
64
+ <!-- eslint-disable vue/no-v-html -->
65
+ <span
66
+ v-if="theme.footer?.copyright"
67
+ class="nb-footer__copyright"
68
+ v-html="theme.footer.copyright"
69
+ />
70
+ <span v-else class="nb-footer__copyright">
71
+ Copyright &copy; Nubisco
72
+ </span>
73
+ <span
74
+ v-if="theme.footer?.message"
75
+ class="nb-footer__copyright"
76
+ v-html="theme.footer.message"
77
+ />
78
+ </div>
79
+ </div>
80
+ </footer>
81
+ </div>
82
+ </template>
83
+
84
+ <script lang="ts" setup>
85
+ import { ref, computed, provide, watch } from 'vue'
86
+ import { useData, withBase } from 'vitepress'
87
+ import DocHeader from './components/DocHeader.vue'
88
+ import type { IDocHeaderTab } from './components/DocHeader.vue'
89
+ import DocOutline from './components/DocOutline.vue'
90
+ import { NbNubiscoMark } from '@nubisco/ui'
91
+
92
+ const { frontmatter, page, theme, site } = useData()
93
+ const body = ref<HTMLElement | null>(null)
94
+
95
+ /*
96
+ * Two kinds of tab.
97
+ *
98
+ * A component page is split into one route per tab (`/components/x/usage`,
99
+ * `/code`, ...), and lists its siblings in `pageTabs`. Those tabs are links.
100
+ *
101
+ * A few other pages still switch sections in place, with `<doc-tab>` blocks
102
+ * named in `tabs`. Those tabs are buttons, and the active one is provided to
103
+ * the blocks below.
104
+ */
105
+ const TAB_LABEL: Record<string, string> = {
106
+ usage: 'Usage',
107
+ style: 'Style',
108
+ code: 'Code',
109
+ accessibility: 'Accessibility',
110
+ }
111
+
112
+ const inPageTabs = computed<string[]>(() =>
113
+ frontmatter.value.pageTabs ? [] : (frontmatter.value.tabs ?? []),
114
+ )
115
+ const activeTab = ref<string>(inPageTabs.value[0] ?? '')
116
+
117
+ watch(inPageTabs, (next) => {
118
+ activeTab.value = next[0] ?? ''
119
+ })
120
+
121
+ provide('nb-active-tab', activeTab)
122
+
123
+ /** The page's folder, for a page split into tab routes. */
124
+ const tabDir = computed(() =>
125
+ frontmatter.value.pageTabs
126
+ ? '/' + page.value.relativePath.replace(/\/[^/]+\.md$/, '')
127
+ : null,
128
+ )
129
+
130
+ const headerTabs = computed<IDocHeaderTab[]>(() => {
131
+ const routeTabs: string[] = frontmatter.value.pageTabs ?? []
132
+ if (routeTabs.length) {
133
+ const ext = site.value.cleanUrls ? '' : '.html'
134
+ return routeTabs.map((key) => ({
135
+ key,
136
+ label: TAB_LABEL[key] ?? key,
137
+ href: withBase(`${tabDir.value}/${key}${ext}`),
138
+ active: key === frontmatter.value.tab,
139
+ }))
140
+ }
141
+ return inPageTabs.value.map((key) => ({
142
+ key,
143
+ label: key === 'Api' ? 'API' : key,
144
+ active: key === activeTab.value,
145
+ }))
146
+ })
147
+
148
+ // ── Footer: sidebar navigation ───────────────────────────────────────────
149
+ interface SidebarLink {
150
+ text: string
151
+ link: string
152
+ }
153
+
154
+ function flattenSidebar(items: any[]): SidebarLink[] {
155
+ const result: SidebarLink[] = []
156
+ for (const item of items) {
157
+ if (item.link) result.push({ text: item.text ?? '', link: item.link })
158
+ if (item.items?.length) result.push(...flattenSidebar(item.items))
159
+ }
160
+ return result
161
+ }
162
+
163
+ const allLinks = computed<SidebarLink[]>(() => {
164
+ const sidebar = theme.value.sidebar
165
+ if (!sidebar) return []
166
+ if (Array.isArray(sidebar)) return flattenSidebar(sidebar)
167
+ return Object.values(sidebar).flatMap((group: any) =>
168
+ Array.isArray(group) ? flattenSidebar(group) : [],
169
+ )
170
+ })
171
+
172
+ const currentPath = computed(() => {
173
+ return (
174
+ '/' + page.value.relativePath.replace(/\.md$/, '').replace(/\/index$/, '/')
175
+ )
176
+ })
177
+
178
+ // Every tab of a split page is the same entry in the sidebar, the one that
179
+ // links its first tab, so the footer offers the same neighbours on each.
180
+ const currentIndex = computed(() =>
181
+ allLinks.value.findIndex((l) =>
182
+ tabDir.value
183
+ ? l.link.replace(/\/[^/]+$/, '') === tabDir.value
184
+ : l.link === currentPath.value,
185
+ ),
186
+ )
187
+
188
+ const prev = computed(() =>
189
+ currentIndex.value > 0 ? allLinks.value[currentIndex.value - 1] : null,
190
+ )
191
+ const next = computed(() =>
192
+ currentIndex.value >= 0 && currentIndex.value < allLinks.value.length - 1
193
+ ? allLinks.value[currentIndex.value + 1]
194
+ : null,
195
+ )
196
+
197
+ const editLink = computed(() => {
198
+ const pattern = theme.value.editLink?.pattern
199
+ if (!pattern) return null
200
+ return pattern.replace(':path', page.value.relativePath)
201
+ })
202
+ </script>
203
+
204
+ <style lang="scss" scoped>
205
+ @use '@nubisco/ui/variables' as v;
206
+
207
+ .nb-layout {
208
+ display: flex;
209
+ flex-direction: column;
210
+ min-height: 100vh;
211
+
212
+ &__main {
213
+ flex: 1;
214
+ display: grid;
215
+ grid-template-columns: minmax(0, 1fr);
216
+ }
217
+
218
+ &__aside {
219
+ display: none;
220
+ }
221
+
222
+ @media (min-width: 1280px) {
223
+ &__main {
224
+ grid-template-columns: minmax(0, 1fr) 15rem;
225
+ column-gap: calc(var(--nb-base-unit) * 4);
226
+ }
227
+
228
+ &__aside {
229
+ display: block;
230
+ padding: calc(var(--nb-base-unit) * 6) calc(var(--nb-base-unit) * 3) 0 0;
231
+ }
232
+
233
+ &__aside-inner {
234
+ position: sticky;
235
+ // Under the site header and the page's own sticky bar.
236
+ top: calc(var(--vp-nav-height, 64px) + 96px + 24px);
237
+ max-height: calc(100vh - var(--vp-nav-height, 64px) - 140px);
238
+ overflow-y: auto;
239
+ }
240
+ }
241
+
242
+ &__body {
243
+ flex: 1;
244
+ padding: calc(var(--nb-base-unit) * 4);
245
+
246
+ /* Content spacing: handle VitePress wrapper divs and markdown paragraphs */
247
+ :deep(p) {
248
+ margin-top: 0.5em;
249
+ margin-bottom: 1.5em;
250
+ line-height: 1.6;
251
+ }
252
+
253
+ /*
254
+ * Section headings at Carbon's weight: large and regular rather than
255
+ * large and bold, so a long page of them reads as a sequence of sections
256
+ * and not as a sequence of shouts. The page title above is the one heavy
257
+ * thing.
258
+ */
259
+ :deep(h2) {
260
+ margin-top: 1.5em;
261
+ margin-bottom: 0.75em;
262
+ font-size: 2rem;
263
+ font-weight: 400;
264
+ line-height: 1.25;
265
+ letter-spacing: 0;
266
+ }
267
+
268
+ :deep(h3) {
269
+ margin-top: 1.25em;
270
+ margin-bottom: 0.5em;
271
+ font-size: 1.375rem;
272
+ font-weight: 600;
273
+ line-height: 1.3;
274
+ letter-spacing: 0;
275
+ }
276
+
277
+ :deep(ul),
278
+ :deep(ol) {
279
+ margin-bottom: 1.5em;
280
+ }
281
+
282
+ :deep(li) {
283
+ margin-bottom: 0.5em;
284
+ }
285
+
286
+ /* Prose tables only. Component demos render their own tables (NbDataTable,
287
+ NbSpreadsheet) whose scoped rules lose to this one on specificity
288
+ (0,2,2 vs 0,2,0), which silently overrode their cell padding and made
289
+ every density look identical. Anything carrying an `nb-` class styles
290
+ itself. */
291
+ :deep(table:not([class*='nb-'])) {
292
+ width: 100%;
293
+ border-collapse: collapse;
294
+ margin-top: 1.5em;
295
+ margin-bottom: 1.5em;
296
+
297
+ /*
298
+ * Prose tables follow the theme.
299
+ *
300
+ * These were a literal `--nb-c-white` header row and two black-alpha
301
+ * rules, all of which assume a light page: in dark mode the header row
302
+ * stayed white under light text, and the row rules disappeared into the
303
+ * background entirely.
304
+ */
305
+ thead tr {
306
+ background: var(--nb-c-layer-1);
307
+ }
308
+
309
+ th {
310
+ font-weight: 600;
311
+ text-align: left;
312
+ padding: 12px 16px;
313
+ border-bottom: 1px solid var(--nb-c-border);
314
+ }
315
+
316
+ td {
317
+ padding: 12px 16px;
318
+ border-bottom: 1px solid var(--nb-c-layer-border-1);
319
+ }
320
+
321
+ tbody tr:last-child td {
322
+ border-bottom: none;
323
+ }
324
+ }
325
+
326
+ /* Inline code, everywhere except inside syntax-highlighted fences */
327
+ :deep(:not(pre) > code) {
328
+ font-family: var(--nb-font-family-mono, 'Courier New', monospace);
329
+ font-size: 0.875em;
330
+ // A tint of the surface, not of black: a black wash is invisible on a
331
+ // dark page.
332
+ background: var(--nb-c-layer-2);
333
+ color: var(--nb-c-text);
334
+ padding: 2px 6px;
335
+ border-radius: 4px;
336
+ }
337
+
338
+ :deep(blockquote) {
339
+ margin-top: 1.5em;
340
+ margin-bottom: 1.5em;
341
+ }
342
+
343
+ :deep(.custom-block) {
344
+ margin-bottom: 1.5em;
345
+ }
346
+
347
+ :deep(pre) {
348
+ margin-top: 1.5em;
349
+ margin-bottom: 1.5em;
350
+ }
351
+ }
352
+
353
+ &__footer {
354
+ background: var(--nb-c-black);
355
+ color: var(--nb-c-white);
356
+ border-top: none;
357
+ padding: 40px;
358
+ margin-top: auto;
359
+ }
360
+ }
361
+
362
+ /* On a phone the body takes the same 16px gutter as the page header above it,
363
+ so the text lines up with the title instead of stepping in under it. */
364
+ @include v.phone {
365
+ .nb-layout__body {
366
+ padding: calc(var(--nb-base-unit) * 3) calc(var(--nb-base-unit) * 2);
367
+
368
+ /* A prose table wider than the screen scrolls inside itself rather than
369
+ widening the whole page sideways. */
370
+ :deep(table:not([class*='nb-'])) {
371
+ display: block;
372
+ max-width: 100%;
373
+ overflow-x: auto;
374
+ }
375
+ }
376
+ }
377
+
378
+ /* ── Footer: prev/next navigation ────────────────────────────────────────── */
379
+ .nb-footer__nav {
380
+ display: grid;
381
+ grid-template-columns: 1fr 1fr;
382
+ gap: 12px;
383
+ margin-bottom: 32px;
384
+ }
385
+
386
+ .nb-footer__nav-item {
387
+ display: flex;
388
+ flex-direction: column;
389
+ gap: 6px;
390
+ padding: 16px 20px;
391
+ border: 1px solid rgba(255, 255, 255, 0.1);
392
+ background: rgba(255, 255, 255, 0.05);
393
+ text-decoration: none;
394
+ transition:
395
+ border-color 0.15s,
396
+ background 0.15s;
397
+
398
+ &:hover {
399
+ border-color: var(--nb-c-magenta);
400
+ background: rgba(255, 255, 255, 0.08);
401
+ }
402
+
403
+ &--next {
404
+ text-align: right;
405
+ }
406
+ }
407
+
408
+ .nb-footer__nav-spacer {
409
+ display: block;
410
+ }
411
+
412
+ .nb-footer__nav-label {
413
+ font-size: 12px;
414
+ font-weight: 500;
415
+ color: rgba(255, 255, 255, 0.6);
416
+ text-transform: uppercase;
417
+ letter-spacing: 0.06em;
418
+ }
419
+
420
+ .nb-footer__nav-title {
421
+ font-size: 14px;
422
+ font-weight: 600;
423
+ color: var(--nb-c-white);
424
+ line-height: 1.4;
425
+ }
426
+
427
+ /* ── Footer: bar ─────────────────────────────────────────────────────────── */
428
+ .nb-footer__bar {
429
+ display: flex;
430
+ align-items: center;
431
+ justify-content: space-between;
432
+ gap: 16px;
433
+ flex-wrap: wrap;
434
+ padding-top: 24px;
435
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
436
+ padding-bottom: 0;
437
+ }
438
+
439
+ .nb-footer__brand {
440
+ display: flex;
441
+ align-items: center;
442
+ gap: 8px;
443
+ text-decoration: none;
444
+ }
445
+
446
+ .nb-footer__logo {
447
+ width: 24px;
448
+ height: 24px;
449
+ flex-shrink: 0;
450
+ }
451
+
452
+ .nb-footer__brand-name {
453
+ font-size: 14px;
454
+ font-weight: 600;
455
+ color: var(--nb-c-white);
456
+ }
457
+
458
+ .nb-footer__meta {
459
+ display: flex;
460
+ align-items: center;
461
+ gap: 20px;
462
+ flex-wrap: wrap;
463
+ justify-content: flex-end;
464
+ }
465
+
466
+ .nb-footer__edit-link {
467
+ font-size: 12px;
468
+ color: var(--nb-c-magenta);
469
+ text-decoration: none;
470
+
471
+ &:hover {
472
+ text-decoration: underline;
473
+ }
474
+ }
475
+
476
+ .nb-footer__copyright {
477
+ font-size: 12px;
478
+ color: rgba(255, 255, 255, 0.6);
479
+ }
480
+ </style>