@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
package/package.json
CHANGED
|
@@ -1,6 +1,124 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nubisco/docs-sdk",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "The Nubisco documentation design as a package: VitePress, the theme, the plugins and a config helper, so a product supplies only its content.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Jos\u00e9 Silva <jose@nubisco.io>",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/nubisco/docs-sdk.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/nubisco/docs-sdk#readme",
|
|
12
|
+
"bugs": "https://github.com/nubisco/docs-sdk/issues",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"bin": {
|
|
15
|
+
"nubisco-docs": "./dist/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./theme": "./src/theme/index.ts",
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"src/theme",
|
|
28
|
+
"src/plugins",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"THIRD_PARTY_NOTICES.md",
|
|
31
|
+
"README.md",
|
|
32
|
+
"!src/**/*.test.ts"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup",
|
|
36
|
+
"dev": "pnpm run build && node dist/cli.js dev fixture/docs",
|
|
37
|
+
"docs:build": "pnpm run build && node dist/cli.js build fixture/docs",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"types:check": "tsc --noEmit",
|
|
40
|
+
"format:check": "prettier --check .",
|
|
41
|
+
"format": "prettier --write .",
|
|
42
|
+
"quality:check": "pnpm run test && pnpm run types:check && pnpm run format:check && pnpm run docs:build",
|
|
43
|
+
"prepare": "simple-git-hooks"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@fontsource/fira-code": "^5.2.0",
|
|
47
|
+
"@fontsource/plus-jakarta-sans": "^5.2.0",
|
|
48
|
+
"markdown-it-container": "^4.0.0",
|
|
49
|
+
"mermaid": "^11.13.0",
|
|
50
|
+
"sass-embedded": "^1.98.0",
|
|
51
|
+
"vite-svg-loader": "^5.1.1",
|
|
52
|
+
"vitepress": "^1.6.4",
|
|
53
|
+
"vitepress-plugin-group-icons": "^1.7.1"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@nubisco/ui": "^5.40.0",
|
|
57
|
+
"vue": "^3.5.0"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@nubisco/ui": "^5.40.0",
|
|
61
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
62
|
+
"@semantic-release/git": "^10.0.1",
|
|
63
|
+
"@types/markdown-it": "^14.2.0",
|
|
64
|
+
"@types/markdown-it-container": "^2.0.12",
|
|
65
|
+
"@types/node": "^22.0.0",
|
|
66
|
+
"conventional-changelog-conventionalcommits": "^9.0.0",
|
|
67
|
+
"markdown-it": "^14.3.3",
|
|
68
|
+
"prettier": "^3.3.0",
|
|
69
|
+
"semantic-release": "^25.0.3",
|
|
70
|
+
"simple-git-hooks": "^2.13.1",
|
|
71
|
+
"tsup": "^8.3.0",
|
|
72
|
+
"typescript": "^5.6.0",
|
|
73
|
+
"vite": "^5.4.21",
|
|
74
|
+
"vitest": "^3.2.7",
|
|
75
|
+
"vue": "^3.5.30"
|
|
76
|
+
},
|
|
77
|
+
"engines": {
|
|
78
|
+
"node": ">=22"
|
|
79
|
+
},
|
|
80
|
+
"publishConfig": {
|
|
81
|
+
"access": "public"
|
|
82
|
+
},
|
|
83
|
+
"funding": [
|
|
84
|
+
{
|
|
85
|
+
"type": "github",
|
|
86
|
+
"url": "https://github.com/sponsors/joseporto"
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
"keywords": [
|
|
90
|
+
"vitepress",
|
|
91
|
+
"documentation",
|
|
92
|
+
"docs",
|
|
93
|
+
"theme",
|
|
94
|
+
"nubisco",
|
|
95
|
+
"design-system"
|
|
96
|
+
],
|
|
97
|
+
"simple-git-hooks": {
|
|
98
|
+
"pre-commit": "pnpm exec prettier --check . && pnpm run test",
|
|
99
|
+
"pre-push": "pnpm run quality:check"
|
|
100
|
+
},
|
|
101
|
+
"release": {
|
|
102
|
+
"branches": [
|
|
103
|
+
"master"
|
|
104
|
+
],
|
|
105
|
+
"plugins": [
|
|
106
|
+
"@semantic-release/commit-analyzer",
|
|
107
|
+
"@semantic-release/release-notes-generator",
|
|
108
|
+
"@semantic-release/changelog",
|
|
109
|
+
"@semantic-release/npm",
|
|
110
|
+
"@semantic-release/github",
|
|
111
|
+
[
|
|
112
|
+
"@semantic-release/git",
|
|
113
|
+
{
|
|
114
|
+
"assets": [
|
|
115
|
+
"package.json",
|
|
116
|
+
"CHANGELOG.md"
|
|
117
|
+
],
|
|
118
|
+
"message": "chore(release): ${nextRelease.version} [skip ci]"
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
"packageManager": "pnpm@10.33.0"
|
|
124
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="props.class" v-html="svg" />
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup>
|
|
6
|
+
import { onMounted, onUnmounted, ref, toRaw } from 'vue'
|
|
7
|
+
import { render, init } from './mermaid'
|
|
8
|
+
|
|
9
|
+
//get mermaid settings
|
|
10
|
+
import { useData } from 'vitepress'
|
|
11
|
+
|
|
12
|
+
const pluginSettings = ref({
|
|
13
|
+
securityLevel: 'loose',
|
|
14
|
+
startOnLoad: false,
|
|
15
|
+
externalDiagrams: [],
|
|
16
|
+
})
|
|
17
|
+
const { page } = useData()
|
|
18
|
+
const { frontmatter } = toRaw(page.value)
|
|
19
|
+
const mermaidPageTheme = frontmatter.mermaidTheme || ''
|
|
20
|
+
|
|
21
|
+
const props = defineProps({
|
|
22
|
+
graph: {
|
|
23
|
+
type: String,
|
|
24
|
+
required: true,
|
|
25
|
+
},
|
|
26
|
+
id: {
|
|
27
|
+
type: String,
|
|
28
|
+
required: true,
|
|
29
|
+
},
|
|
30
|
+
class: {
|
|
31
|
+
type: String,
|
|
32
|
+
required: false,
|
|
33
|
+
default: 'mermaid',
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const svg = ref(null)
|
|
38
|
+
let mut = null
|
|
39
|
+
|
|
40
|
+
onMounted(async () => {
|
|
41
|
+
await init(pluginSettings.value.externalDiagrams)
|
|
42
|
+
let settings = await import('virtual:mermaid-config')
|
|
43
|
+
if (settings?.default) pluginSettings.value = settings.default
|
|
44
|
+
|
|
45
|
+
mut = new MutationObserver(async () => await renderChart())
|
|
46
|
+
mut.observe(document.documentElement, { attributes: true })
|
|
47
|
+
await renderChart()
|
|
48
|
+
|
|
49
|
+
//refresh images on first render
|
|
50
|
+
const hasImages =
|
|
51
|
+
/<img([\w\W]+?)>/.exec(decodeURIComponent(props.graph))?.length > 0
|
|
52
|
+
if (hasImages)
|
|
53
|
+
setTimeout(() => {
|
|
54
|
+
let imgElements = document.getElementsByTagName('img')
|
|
55
|
+
let imgs = Array.from(imgElements)
|
|
56
|
+
if (imgs.length) {
|
|
57
|
+
Promise.all(
|
|
58
|
+
imgs
|
|
59
|
+
.filter((img) => !img.complete)
|
|
60
|
+
.map(
|
|
61
|
+
(img) =>
|
|
62
|
+
new Promise((resolve) => {
|
|
63
|
+
img.onload = img.onerror = resolve
|
|
64
|
+
}),
|
|
65
|
+
),
|
|
66
|
+
).then(async () => {
|
|
67
|
+
await renderChart()
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
}, 100)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
onUnmounted(() => mut.disconnect())
|
|
74
|
+
|
|
75
|
+
const renderChart = async () => {
|
|
76
|
+
const hasDarkClass = document.documentElement.classList.contains('dark')
|
|
77
|
+
let mermaidConfig = {
|
|
78
|
+
...pluginSettings.value,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (mermaidPageTheme) mermaidConfig.theme = mermaidPageTheme
|
|
82
|
+
if (hasDarkClass) mermaidConfig.theme = 'dark'
|
|
83
|
+
|
|
84
|
+
let svgCode = await render(
|
|
85
|
+
props.id,
|
|
86
|
+
decodeURIComponent(props.graph),
|
|
87
|
+
mermaidConfig,
|
|
88
|
+
)
|
|
89
|
+
// required for c4c, mindmap and zenuml diagrams, this hack forces v-html to re-render, otherwise the diagram disappears
|
|
90
|
+
const salt = Math.random().toString(36).substring(7)
|
|
91
|
+
svg.value = `${svgCode} <span style="display: none">${salt}</span>`
|
|
92
|
+
}
|
|
93
|
+
</script>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type UserConfig } from 'vitepress'
|
|
2
|
+
import { MermaidMarkdown } from './mermaid-markdown'
|
|
3
|
+
import { MermaidPlugin, MermaidPluginConfig } from './mermaid-plugin'
|
|
4
|
+
import { MermaidConfig } from 'mermaid'
|
|
5
|
+
|
|
6
|
+
export { MermaidMarkdown } from './mermaid-markdown'
|
|
7
|
+
export { MermaidPlugin } from './mermaid-plugin'
|
|
8
|
+
|
|
9
|
+
export { UserConfig }
|
|
10
|
+
|
|
11
|
+
declare module 'vitepress' {
|
|
12
|
+
interface UserConfig {
|
|
13
|
+
mermaid?: MermaidConfig
|
|
14
|
+
mermaidPlugin?: MermaidPluginConfig
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const withMermaid = (config: UserConfig) => {
|
|
19
|
+
if (!config.markdown) config.markdown = {}
|
|
20
|
+
const markdownConfigOriginal = config.markdown.config || (() => {})
|
|
21
|
+
config.markdown.config = (...args) => {
|
|
22
|
+
MermaidMarkdown(...args, config.mermaidPlugin)
|
|
23
|
+
markdownConfigOriginal(...args)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!config.vite) config.vite = {}
|
|
27
|
+
if (!config.vite.plugins) config.vite.plugins = []
|
|
28
|
+
config.vite.plugins.push(MermaidPlugin(config.mermaid))
|
|
29
|
+
if (!config.vite.optimizeDeps) config.vite.optimizeDeps = {}
|
|
30
|
+
if (!config.vite.optimizeDeps.include) config.vite.optimizeDeps.include = []
|
|
31
|
+
|
|
32
|
+
config.vite.optimizeDeps.include = [
|
|
33
|
+
...config.vite.optimizeDeps.include,
|
|
34
|
+
'@braintree/sanitize-url',
|
|
35
|
+
'dayjs',
|
|
36
|
+
'debug',
|
|
37
|
+
'cytoscape-cose-bilkent',
|
|
38
|
+
'cytoscape',
|
|
39
|
+
]
|
|
40
|
+
if (!config.vite.resolve) config.vite.resolve = {}
|
|
41
|
+
|
|
42
|
+
const mermaidPluginAlias = {
|
|
43
|
+
'dayjs/plugin/advancedFormat.js': 'dayjs/esm/plugin/advancedFormat',
|
|
44
|
+
'dayjs/plugin/customParseFormat.js': 'dayjs/esm/plugin/customParseFormat',
|
|
45
|
+
'dayjs/plugin/isoWeek.js': 'dayjs/esm/plugin/isoWeek',
|
|
46
|
+
'cytoscape/dist/cytoscape.umd.js': 'cytoscape/dist/cytoscape.esm.js',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!config.vite.resolve.alias) config.vite.resolve.alias = mermaidPluginAlias
|
|
50
|
+
else if (Array.isArray(config.vite.resolve.alias)) {
|
|
51
|
+
config.vite.resolve.alias = [
|
|
52
|
+
...config.vite.resolve.alias,
|
|
53
|
+
...Object.entries(mermaidPluginAlias).map(([find, replacement]) => ({
|
|
54
|
+
find,
|
|
55
|
+
replacement,
|
|
56
|
+
})),
|
|
57
|
+
]
|
|
58
|
+
} else {
|
|
59
|
+
config.vite.resolve.alias = {
|
|
60
|
+
...config.vite.resolve.alias,
|
|
61
|
+
...mermaidPluginAlias,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return config
|
|
66
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// @ts-nocheck: adapted third-party code (see THIRD_PARTY_NOTICES.md), typed loosely upstream.
|
|
2
|
+
export const MermaidMarkdown = (md, pluginOptions) => {
|
|
3
|
+
const fence = md.renderer.rules.fence.bind(md.renderer.rules)
|
|
4
|
+
|
|
5
|
+
md.renderer.rules.fence = (tokens, index, options, env, slf) => {
|
|
6
|
+
const token = tokens[index]
|
|
7
|
+
|
|
8
|
+
if (token.info.trim() === 'mermaid') {
|
|
9
|
+
try {
|
|
10
|
+
const key = index
|
|
11
|
+
const cssClass = pluginOptions?.class || 'mermaid'
|
|
12
|
+
return `
|
|
13
|
+
<Suspense>
|
|
14
|
+
<template #default>
|
|
15
|
+
<Mermaid id="mermaid-${key}" class="${cssClass}" graph="${encodeURIComponent(token.content)}"></Mermaid>
|
|
16
|
+
</template>
|
|
17
|
+
<!-- loading state via #fallback slot -->
|
|
18
|
+
<template #fallback>
|
|
19
|
+
Loading...
|
|
20
|
+
</template>
|
|
21
|
+
</Suspense>`
|
|
22
|
+
} catch (err) {
|
|
23
|
+
return `<pre>${err}</pre>`
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// allow to write fences to highlight mermaid code itself
|
|
28
|
+
if (token.info.trim() === 'mmd') {
|
|
29
|
+
tokens[index].info = 'mermaid'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return fence(tokens, index, options, env, slf)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Plugin } from 'vite'
|
|
2
|
+
import { ExternalDiagramDefinition, MermaidConfig } from 'mermaid'
|
|
3
|
+
|
|
4
|
+
interface MermaidPluginOptions extends MermaidConfig {
|
|
5
|
+
externalDiagrams: ExternalDiagramDefinition[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// additional configuration for plugin itself. Separate model, not to risk name conflicts with future MermaidConfig options
|
|
9
|
+
export interface MermaidPluginConfig {
|
|
10
|
+
class?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DEFAULT_OPTIONS: MermaidConfig = {
|
|
14
|
+
// we set loose as default here because is needed to load images
|
|
15
|
+
securityLevel: 'loose',
|
|
16
|
+
startOnLoad: false,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function MermaidPlugin(
|
|
20
|
+
inlineOptions?: Partial<MermaidPluginOptions>,
|
|
21
|
+
): Plugin {
|
|
22
|
+
const options = {
|
|
23
|
+
...DEFAULT_OPTIONS,
|
|
24
|
+
...inlineOptions,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const virtualModuleId = 'virtual:mermaid-config'
|
|
28
|
+
const resolvedVirtualModuleId = '\0' + virtualModuleId
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
name: 'vite-plugin-mermaid',
|
|
32
|
+
// The component is registered by the theme (`src/theme/index.ts`), not
|
|
33
|
+
// injected into VitePress's client entry by a path in the consumer's tree.
|
|
34
|
+
|
|
35
|
+
async resolveId(id) {
|
|
36
|
+
if (id === virtualModuleId) {
|
|
37
|
+
return resolvedVirtualModuleId
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
async load(this, id) {
|
|
41
|
+
if (id === resolvedVirtualModuleId) {
|
|
42
|
+
return `export default ${JSON.stringify(options)};`
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import mermaid, { ExternalDiagramDefinition, MermaidConfig } from 'mermaid'
|
|
2
|
+
|
|
3
|
+
export const init = async (externalDiagrams: ExternalDiagramDefinition[]) => {
|
|
4
|
+
try {
|
|
5
|
+
if (mermaid.registerExternalDiagrams)
|
|
6
|
+
await mermaid.registerExternalDiagrams(externalDiagrams)
|
|
7
|
+
} catch (e) {
|
|
8
|
+
console.error(e)
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const render = async (
|
|
13
|
+
id: string,
|
|
14
|
+
code: string,
|
|
15
|
+
config: MermaidConfig,
|
|
16
|
+
): Promise<string> => {
|
|
17
|
+
// await init;
|
|
18
|
+
mermaid.initialize(config)
|
|
19
|
+
const { svg } = await mermaid.render(id, code)
|
|
20
|
+
return svg
|
|
21
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
v-if="selected === label"
|
|
4
|
+
:id="`panel-${label}-${uid}`"
|
|
5
|
+
:class="{
|
|
6
|
+
'content-tabs__content': true,
|
|
7
|
+
preview: label.toLowerCase() === 'preview',
|
|
8
|
+
}"
|
|
9
|
+
role="tabpanel"
|
|
10
|
+
tabindex="-1"
|
|
11
|
+
:aria-labelledby="`tab-${label}-${uid}`"
|
|
12
|
+
>
|
|
13
|
+
<slot />
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import { useTabsSingleState } from './useTabsSingleState'
|
|
19
|
+
|
|
20
|
+
defineProps<{ label: string }>()
|
|
21
|
+
|
|
22
|
+
const { uid, selected } = useTabsSingleState()
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<style lang="scss" scoped>
|
|
26
|
+
.content-tabs__content {
|
|
27
|
+
&:not(.preview) {
|
|
28
|
+
padding: var(--nb-base-unit);
|
|
29
|
+
}
|
|
30
|
+
> :first-child:first-child {
|
|
31
|
+
margin-top: 0;
|
|
32
|
+
}
|
|
33
|
+
> :last-child:last-child {
|
|
34
|
+
margin-bottom: 0;
|
|
35
|
+
}
|
|
36
|
+
> :deep(div[class*='language-']) {
|
|
37
|
+
border-radius: 8px;
|
|
38
|
+
margin: 16px 0px;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
</style>
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="content-tabs">
|
|
3
|
+
<div class="content-tabs__header">
|
|
4
|
+
<div
|
|
5
|
+
ref="tablist"
|
|
6
|
+
class="content-tabs__tab-list"
|
|
7
|
+
role="tablist"
|
|
8
|
+
@keydown="onKeydown"
|
|
9
|
+
>
|
|
10
|
+
<button
|
|
11
|
+
v-for="tabLabel in tabLabels"
|
|
12
|
+
:id="`tab-${tabLabel}-${uid}`"
|
|
13
|
+
ref="buttonRefs"
|
|
14
|
+
:key="tabLabel"
|
|
15
|
+
role="tab"
|
|
16
|
+
class="content-tabs__tab"
|
|
17
|
+
:aria-selected="tabLabel === selected"
|
|
18
|
+
:aria-controls="`panel-${tabLabel}-${uid}`"
|
|
19
|
+
:tabindex="tabLabel === selected ? 0 : -1"
|
|
20
|
+
@click="() => selectStable(tabLabel)"
|
|
21
|
+
>
|
|
22
|
+
<NbIcon
|
|
23
|
+
:name="iconName(tabLabel)"
|
|
24
|
+
color="var(--vp-content-tabs-tab-text-color)"
|
|
25
|
+
/>{{ tabLabel }}
|
|
26
|
+
</button>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<slot />
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script setup lang="ts">
|
|
34
|
+
import { ref, toRef, useId } from 'vue'
|
|
35
|
+
import { useStabilizeScrollPosition } from './useStabilizeScrollPosition'
|
|
36
|
+
import { useTabsSelectedState } from './useTabsSelectedState'
|
|
37
|
+
import { useTabLabels } from './useTabLabels'
|
|
38
|
+
import { provideTabsSingleState } from './useTabsSingleState'
|
|
39
|
+
|
|
40
|
+
const props = defineProps<{ sharedStateKey?: string }>()
|
|
41
|
+
|
|
42
|
+
const tabLabels = useTabLabels()
|
|
43
|
+
|
|
44
|
+
const { selected, select } = useTabsSelectedState(
|
|
45
|
+
tabLabels,
|
|
46
|
+
toRef(props, 'sharedStateKey'),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const tablist = ref<HTMLDivElement>()
|
|
50
|
+
const { stabilizeScrollPosition } = useStabilizeScrollPosition(tablist)
|
|
51
|
+
const selectStable = stabilizeScrollPosition(select)
|
|
52
|
+
|
|
53
|
+
const buttonRefs = ref<HTMLButtonElement[]>([])
|
|
54
|
+
|
|
55
|
+
const iconName = (label: string): string => {
|
|
56
|
+
switch (label.toLowerCase()) {
|
|
57
|
+
case 'preview':
|
|
58
|
+
return 'eye'
|
|
59
|
+
case 'code':
|
|
60
|
+
return 'code'
|
|
61
|
+
case 'api':
|
|
62
|
+
return 'plugs'
|
|
63
|
+
case 'params':
|
|
64
|
+
case 'parameters':
|
|
65
|
+
return 'toggle-right'
|
|
66
|
+
case 'returns':
|
|
67
|
+
return 'key-return'
|
|
68
|
+
case 'design':
|
|
69
|
+
return 'compass-tool'
|
|
70
|
+
case 'notes':
|
|
71
|
+
return 'note'
|
|
72
|
+
case 'accessibility':
|
|
73
|
+
return 'person-arms-spread'
|
|
74
|
+
default:
|
|
75
|
+
return 'info'
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const onKeydown = (e: KeyboardEvent) => {
|
|
80
|
+
const currentIndex = tabLabels.value.indexOf(selected.value)
|
|
81
|
+
let selectIndex: number | undefined
|
|
82
|
+
|
|
83
|
+
if (e.key === 'ArrowLeft') {
|
|
84
|
+
selectIndex =
|
|
85
|
+
currentIndex >= 1 ? currentIndex - 1 : tabLabels.value.length - 1
|
|
86
|
+
} else if (e.key === 'ArrowRight') {
|
|
87
|
+
selectIndex =
|
|
88
|
+
currentIndex < tabLabels.value.length - 1 ? currentIndex + 1 : 0
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (selectIndex !== undefined) {
|
|
92
|
+
selectStable(tabLabels.value[selectIndex])
|
|
93
|
+
buttonRefs.value[selectIndex]?.focus()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const uid = useId()
|
|
98
|
+
|
|
99
|
+
provideTabsSingleState({ uid, selected })
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
<style lang="scss">
|
|
103
|
+
:root {
|
|
104
|
+
--vp-content-tabs-tab-text-color: var(--vp-c-text-2);
|
|
105
|
+
--vp-content-tabs-tab-active-text-color: var(--vp-c-text-1);
|
|
106
|
+
--vp-content-tabs-tab-hover-text-color: var(--vp-c-text-1);
|
|
107
|
+
--vp-content-tabs-tab-active-bg: var(--vp-c-bg);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.content-tabs {
|
|
111
|
+
margin: 16px 0;
|
|
112
|
+
display: flex;
|
|
113
|
+
flex-direction: column;
|
|
114
|
+
row-gap: 4px;
|
|
115
|
+
border: solid 1px var(--vp-c-divider);
|
|
116
|
+
&__header {
|
|
117
|
+
display: flex;
|
|
118
|
+
justify-content: flex-end;
|
|
119
|
+
}
|
|
120
|
+
&__tab-list {
|
|
121
|
+
padding: 4px;
|
|
122
|
+
display: flex;
|
|
123
|
+
column-gap: 4px;
|
|
124
|
+
overflow-x: auto;
|
|
125
|
+
overflow-y: hidden;
|
|
126
|
+
}
|
|
127
|
+
&__tab {
|
|
128
|
+
position: relative;
|
|
129
|
+
padding: 0 12px;
|
|
130
|
+
line-height: 48px;
|
|
131
|
+
color: var(--vp-content-tabs-tab-text-color);
|
|
132
|
+
font-size: 14px;
|
|
133
|
+
font-weight: 400;
|
|
134
|
+
white-space: nowrap;
|
|
135
|
+
transition: color 0.25s;
|
|
136
|
+
border-radius: 8px;
|
|
137
|
+
display: flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
gap: 8px;
|
|
140
|
+
&[aria-selected='true'] {
|
|
141
|
+
color: var(--vp-content-tabs-tab-active-text-color);
|
|
142
|
+
font-weight: 600;
|
|
143
|
+
}
|
|
144
|
+
&:hover {
|
|
145
|
+
color: var(--vp-content-tabs-tab-hover-text-color);
|
|
146
|
+
background-color: var(--vp-content-tabs-tab-active-bg);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { App } from 'vue'
|
|
2
|
+
import ContentTabs from './ContentTabs.vue'
|
|
3
|
+
import ContentTab from './ContentTab.vue'
|
|
4
|
+
import { provideTabsSharedState } from './useTabsSelectedState'
|
|
5
|
+
|
|
6
|
+
export const enhanceAppWithTabs = (app: App) => {
|
|
7
|
+
provideTabsSharedState(app)
|
|
8
|
+
app.component('ContentTabs', ContentTabs)
|
|
9
|
+
app.component('ContentTab', ContentTab)
|
|
10
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Ref } from 'vue'
|
|
2
|
+
import { nextTick } from 'vue'
|
|
3
|
+
|
|
4
|
+
type StabilizeScrollPosition = <Args extends readonly unknown[], Return>(
|
|
5
|
+
func: (...args: Args) => Return,
|
|
6
|
+
) => (...args: Args) => Promise<Return>
|
|
7
|
+
|
|
8
|
+
export const useStabilizeScrollPosition = (
|
|
9
|
+
targetEle: Ref<HTMLElement | undefined>,
|
|
10
|
+
) => {
|
|
11
|
+
if (typeof document === 'undefined') {
|
|
12
|
+
const mock: StabilizeScrollPosition =
|
|
13
|
+
(f) =>
|
|
14
|
+
async (...args) =>
|
|
15
|
+
f(...args)
|
|
16
|
+
return { stabilizeScrollPosition: mock }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const scrollableEleVal = document.documentElement
|
|
20
|
+
|
|
21
|
+
const stabilizeScrollPosition: StabilizeScrollPosition =
|
|
22
|
+
(func) =>
|
|
23
|
+
async (...args) => {
|
|
24
|
+
const result = func(...args)
|
|
25
|
+
const eleVal = targetEle.value
|
|
26
|
+
if (!eleVal) return result
|
|
27
|
+
|
|
28
|
+
const offset = eleVal.offsetTop - scrollableEleVal.scrollTop
|
|
29
|
+
await nextTick()
|
|
30
|
+
scrollableEleVal.scrollTop = eleVal.offsetTop - offset
|
|
31
|
+
|
|
32
|
+
return result
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { stabilizeScrollPosition }
|
|
36
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Ref } from 'vue'
|
|
2
|
+
import { computed, useSlots } from 'vue'
|
|
3
|
+
import ContentTab from './ContentTab.vue'
|
|
4
|
+
|
|
5
|
+
export function useTabLabels(): Ref<string[]> {
|
|
6
|
+
const slots = useSlots()
|
|
7
|
+
const tabLabels = computed(() => {
|
|
8
|
+
const defaultSlot = slots.default?.()
|
|
9
|
+
if (!defaultSlot) {
|
|
10
|
+
return []
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return defaultSlot
|
|
14
|
+
.filter((vnode) => vnode.type === ContentTab && vnode.props)
|
|
15
|
+
.map((vnode) => vnode.props?.label)
|
|
16
|
+
})
|
|
17
|
+
return tabLabels
|
|
18
|
+
}
|