@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/package.json CHANGED
@@ -1,6 +1,124 @@
1
1
  {
2
2
  "name": "@nubisco/docs-sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }