@sewadah/selaras-docs 1.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/README.md +75 -0
- package/app/app.config.ts +22 -0
- package/app/app.vue +10 -0
- package/app/assets/style.css +187 -0
- package/app/components/DocsEditLink.vue +31 -0
- package/app/components/DocsFooter.vue +27 -0
- package/app/components/DocsHeader.vue +69 -0
- package/app/components/DocsSearch.vue +28 -0
- package/app/components/DocsSearchButton.vue +13 -0
- package/app/components/DocsSidebar.vue +13 -0
- package/app/components/DocsTableOfContents.vue +21 -0
- package/app/components/content/DocsExample.vue +26 -0
- package/app/composables/useDocsNavigation.ts +4 -0
- package/app/composables/useDocsSurround.ts +20 -0
- package/app/layouts/selaras-docs.vue +13 -0
- package/app/pages/[...slug].vue +50 -0
- package/content.config.ts +20 -0
- package/modules/docs.mjs +41 -0
- package/nuxt.config.mjs +15 -0
- package/package.json +33 -0
- package/types.d.ts +92 -0
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Selaras Docs
|
|
2
|
+
|
|
3
|
+
A zero-config Nuxt documentation layer for Selaras.
|
|
4
|
+
|
|
5
|
+
Install Nuxt and the layer as development dependencies:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bun add -d nuxt @sewadah/selaras-docs
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Add the layer to `nuxt.config.ts`:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
export default defineNuxtConfig({
|
|
15
|
+
extends: ['@sewadah/selaras-docs'],
|
|
16
|
+
})
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then create `content/index.md`:
|
|
20
|
+
|
|
21
|
+
```md
|
|
22
|
+
---
|
|
23
|
+
title: My documentation
|
|
24
|
+
description: Documentation for my project.
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
# My documentation
|
|
28
|
+
|
|
29
|
+
Start writing.
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The layer supplies Nuxt Content, Tailwind CSS v4, Selaras, an accessible
|
|
33
|
+
responsive documentation shell, local navigation search, and dark mode. It
|
|
34
|
+
uses neutral system font stacks by default. You do not
|
|
35
|
+
need an app root, Content config, or stylesheet for the default setup.
|
|
36
|
+
|
|
37
|
+
`nuxt` remains a peer because the documentation site is a Nuxt application;
|
|
38
|
+
the layer is normally a development dependency, just like Nuxt itself.
|
|
39
|
+
|
|
40
|
+
## Customization
|
|
41
|
+
|
|
42
|
+
Use `app.config.ts` for site identity and the default shell:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
export default defineAppConfig({
|
|
46
|
+
selarasDocs: {
|
|
47
|
+
site: {
|
|
48
|
+
name: 'My project',
|
|
49
|
+
description: 'Documentation for my project.',
|
|
50
|
+
},
|
|
51
|
+
repository: {
|
|
52
|
+
url: 'https://github.com/example/project',
|
|
53
|
+
editLinks: true,
|
|
54
|
+
},
|
|
55
|
+
header: {
|
|
56
|
+
links: [
|
|
57
|
+
{ label: 'Guide', to: '/guide/getting-started' },
|
|
58
|
+
{
|
|
59
|
+
label: 'Resources',
|
|
60
|
+
children: [
|
|
61
|
+
{ label: 'API reference', to: '/api' },
|
|
62
|
+
{ label: 'GitHub', to: 'https://github.com/example/project', target: '_blank', rel: 'noreferrer' },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Create an application component with the same name as a layer component, such
|
|
72
|
+
as `components/DocsHeader.vue`, to replace that part of the shell. For a host
|
|
73
|
+
that owns Tailwind compilation or uses a class prefix, set
|
|
74
|
+
`selarasDocs: { css: false }` in `nuxt.config.ts` and import Selaras and the
|
|
75
|
+
docs stylesheet from the host CSS entry.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export default defineAppConfig({
|
|
2
|
+
selarasDocs: {
|
|
3
|
+
site: {
|
|
4
|
+
name: 'Documentation',
|
|
5
|
+
},
|
|
6
|
+
repository: {
|
|
7
|
+
branch: 'main',
|
|
8
|
+
contentDirectory: 'content',
|
|
9
|
+
editLinks: true,
|
|
10
|
+
},
|
|
11
|
+
header: {
|
|
12
|
+
showTitle: true,
|
|
13
|
+
search: true,
|
|
14
|
+
colorMode: true,
|
|
15
|
+
},
|
|
16
|
+
toc: {
|
|
17
|
+
enabled: true,
|
|
18
|
+
title: 'On this page',
|
|
19
|
+
},
|
|
20
|
+
footer: {},
|
|
21
|
+
},
|
|
22
|
+
})
|
package/app/app.vue
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
.selaras-docs-content button p,
|
|
2
|
+
.selaras-docs-content a p {
|
|
3
|
+
margin: 0;
|
|
4
|
+
color: inherit;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.selaras-docs-root {
|
|
8
|
+
background: var(--selaras-resolved-surface-canvas, var(--selaras-resolved-surface-default));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.selaras-docs-skip-link {
|
|
12
|
+
position: fixed;
|
|
13
|
+
z-index: 100;
|
|
14
|
+
top: 0.75rem;
|
|
15
|
+
left: 0.75rem;
|
|
16
|
+
transform: translateY(-160%);
|
|
17
|
+
border-radius: var(--selaras-resolved-radius-md);
|
|
18
|
+
background: var(--selaras-resolved-color-primary-fill);
|
|
19
|
+
color: var(--selaras-resolved-color-primary-on-fill);
|
|
20
|
+
padding: 0.5rem 0.75rem;
|
|
21
|
+
text-decoration: none;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.selaras-docs-skip-link:focus {
|
|
25
|
+
transform: translateY(0);
|
|
26
|
+
outline: 2px solid var(--selaras-resolved-color-primary-focus);
|
|
27
|
+
outline-offset: 2px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.selaras-docs-shell {
|
|
31
|
+
display: flex;
|
|
32
|
+
width: min(100%, 90rem);
|
|
33
|
+
min-height: calc(100vh - 4rem);
|
|
34
|
+
margin: 0 auto;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.selaras-docs-sidebar {
|
|
38
|
+
border-inline-end: 1px solid var(--selaras-resolved-border-default);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.selaras-docs-main {
|
|
42
|
+
min-width: 0;
|
|
43
|
+
flex: 1;
|
|
44
|
+
padding: 2rem;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.selaras-docs-page {
|
|
48
|
+
max-width: 78rem;
|
|
49
|
+
margin: 0 auto;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.selaras-docs-page-grid {
|
|
53
|
+
display: grid;
|
|
54
|
+
grid-template-columns: minmax(0, 1fr);
|
|
55
|
+
gap: 3rem;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.selaras-docs-article {
|
|
59
|
+
min-width: 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.selaras-docs-page-header {
|
|
63
|
+
margin-bottom: 1.5rem;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.selaras-docs-content {
|
|
67
|
+
min-width: 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.selaras-docs-toc {
|
|
71
|
+
width: 14rem;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.selaras-docs-edit-link {
|
|
75
|
+
display: inline-flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
gap: 0.375rem;
|
|
78
|
+
margin-top: 2.5rem;
|
|
79
|
+
color: var(--selaras-resolved-text-muted);
|
|
80
|
+
font-size: 0.875rem;
|
|
81
|
+
text-decoration: none;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.selaras-docs-edit-link:hover {
|
|
85
|
+
color: var(--selaras-resolved-text-default);
|
|
86
|
+
text-decoration: underline;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.selaras-docs-surround {
|
|
90
|
+
margin-top: 2rem;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.selaras-docs-footer {
|
|
94
|
+
border-top: 1px solid var(--selaras-resolved-border-default);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.selaras-docs-footer-inner {
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: space-between;
|
|
101
|
+
gap: 1rem;
|
|
102
|
+
min-height: 5rem;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.selaras-docs-footer-text {
|
|
106
|
+
margin: 0;
|
|
107
|
+
color: var(--selaras-resolved-text-muted);
|
|
108
|
+
font-size: 0.875rem;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.selaras-docs-footer-links {
|
|
112
|
+
display: flex;
|
|
113
|
+
flex-wrap: wrap;
|
|
114
|
+
gap: 1rem;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.selaras-docs-footer-links a {
|
|
118
|
+
display: inline-flex;
|
|
119
|
+
align-items: center;
|
|
120
|
+
gap: 0.375rem;
|
|
121
|
+
color: var(--selaras-resolved-text-muted);
|
|
122
|
+
font-size: 0.875rem;
|
|
123
|
+
text-decoration: none;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.selaras-docs-footer-links a:hover {
|
|
127
|
+
color: var(--selaras-resolved-text-default);
|
|
128
|
+
text-decoration: underline;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@media (min-width: 64rem) {
|
|
132
|
+
.selaras-docs-nav-toggle {
|
|
133
|
+
display: none;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.selaras-docs-page-grid {
|
|
137
|
+
grid-template-columns: minmax(0, 1fr) 14rem;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@media (max-width: 63.999rem) {
|
|
142
|
+
.selaras-docs-main {
|
|
143
|
+
padding: 1.5rem;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.selaras-docs-header-link.selaras-docs-header-link--desktop {
|
|
147
|
+
display: none;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.selaras-docs-footer-inner {
|
|
151
|
+
align-items: flex-start;
|
|
152
|
+
flex-direction: column;
|
|
153
|
+
justify-content: center;
|
|
154
|
+
padding-top: 1rem;
|
|
155
|
+
padding-bottom: 1rem;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.selaras-docs-brand {
|
|
160
|
+
display: inline-flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
color: inherit;
|
|
163
|
+
text-decoration: none;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.selaras-docs-brand {
|
|
167
|
+
gap: 0.625rem;
|
|
168
|
+
font-weight: 600;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.selaras-docs-logo {
|
|
172
|
+
display: block;
|
|
173
|
+
width: auto;
|
|
174
|
+
height: 1.75rem;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.selaras-docs-logo--dark {
|
|
178
|
+
display: none;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.dark .selaras-docs-logo--light {
|
|
182
|
+
display: none;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.dark .selaras-docs-logo--dark {
|
|
186
|
+
display: block;
|
|
187
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = defineProps<{
|
|
3
|
+
path: string
|
|
4
|
+
}>()
|
|
5
|
+
|
|
6
|
+
const appConfig = useAppConfig()
|
|
7
|
+
const docsConfig = computed(() => appConfig.selarasDocs ?? {})
|
|
8
|
+
|
|
9
|
+
const editUrl = computed(() => {
|
|
10
|
+
const repository = docsConfig.value.repository
|
|
11
|
+
if (!repository?.url || repository.editLinks === false)
|
|
12
|
+
return undefined
|
|
13
|
+
|
|
14
|
+
const base = repository.url.replace(/\/$/, '')
|
|
15
|
+
const relativePath = props.path === '/' ? 'index.md' : `${props.path.replace(/^\//, '')}.md`
|
|
16
|
+
const contentPath = [repository.contentDirectory ?? 'content', relativePath].join('/')
|
|
17
|
+
const branch = repository.branch ?? 'main'
|
|
18
|
+
|
|
19
|
+
if (base.startsWith('https://github.com/'))
|
|
20
|
+
return `${base}/edit/${branch}/${contentPath}`
|
|
21
|
+
if (base.startsWith('https://gitlab.com/'))
|
|
22
|
+
return `${base}/-/edit/${branch}/${contentPath}`
|
|
23
|
+
return undefined
|
|
24
|
+
})
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<a v-if="editUrl" :href="editUrl" class="selaras-docs-edit-link" target="_blank" rel="noreferrer">
|
|
29
|
+
Edit this page
|
|
30
|
+
</a>
|
|
31
|
+
</template>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const appConfig = useAppConfig()
|
|
3
|
+
const docsConfig = computed(() => appConfig.selarasDocs ?? {})
|
|
4
|
+
const footer = computed(() => docsConfig.value.footer)
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<footer v-if="footer?.text || footer?.links?.length" class="selaras-docs-footer">
|
|
9
|
+
<SContainer size="full" class="selaras-docs-footer-inner">
|
|
10
|
+
<p v-if="footer?.text" class="selaras-docs-footer-text">
|
|
11
|
+
{{ footer.text }}
|
|
12
|
+
</p>
|
|
13
|
+
<nav v-if="footer?.links?.length" class="selaras-docs-footer-links" aria-label="Footer navigation">
|
|
14
|
+
<NuxtLink
|
|
15
|
+
v-for="link in footer.links"
|
|
16
|
+
:key="`${link.to}:${link.label ?? link.icon ?? ''}`"
|
|
17
|
+
:to="link.to"
|
|
18
|
+
:target="link.target"
|
|
19
|
+
:aria-label="link.ariaLabel ?? link.label"
|
|
20
|
+
>
|
|
21
|
+
<SIcon v-if="link.icon" :name="link.icon" />
|
|
22
|
+
<span v-if="link.label">{{ link.label }}</span>
|
|
23
|
+
</NuxtLink>
|
|
24
|
+
</nav>
|
|
25
|
+
</SContainer>
|
|
26
|
+
</footer>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const route = useRoute()
|
|
3
|
+
const appConfig = useAppConfig()
|
|
4
|
+
const { data: navigation } = useDocsNavigation()
|
|
5
|
+
const navigationOpen = ref(false)
|
|
6
|
+
|
|
7
|
+
const docsConfig = computed(() => appConfig.selarasDocs ?? {})
|
|
8
|
+
const siteName = computed(() => docsConfig.value.header?.title ?? docsConfig.value.site?.name ?? 'Documentation')
|
|
9
|
+
const showTitle = computed(() => docsConfig.value.header?.showTitle !== false)
|
|
10
|
+
const logo = computed(() => docsConfig.value.site?.logo)
|
|
11
|
+
const logoAlt = computed(() => typeof logo.value === 'object' ? (logo.value.alt ?? siteName.value) : siteName.value)
|
|
12
|
+
const lightLogo = computed(() => typeof logo.value === 'string' ? logo.value : (logo.value?.light ?? logo.value?.dark))
|
|
13
|
+
const darkLogo = computed(() => typeof logo.value === 'object' ? (logo.value.dark ?? logo.value.light) : logo.value)
|
|
14
|
+
const hasDistinctLogos = computed(() => Boolean(lightLogo.value && darkLogo.value && lightLogo.value !== darkLogo.value))
|
|
15
|
+
const headerLinks = computed(() => docsConfig.value.header?.links ?? [])
|
|
16
|
+
const repositoryUrl = computed(() => docsConfig.value.repository?.url)
|
|
17
|
+
|
|
18
|
+
watch(() => route.path, () => {
|
|
19
|
+
navigationOpen.value = false
|
|
20
|
+
})
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<SHeader>
|
|
25
|
+
<NuxtLink to="/" class="selaras-docs-brand" :aria-label="`${siteName} home`">
|
|
26
|
+
<template v-if="lightLogo">
|
|
27
|
+
<img :src="lightLogo" :alt="logoAlt" class="selaras-docs-logo" :class="{ 'selaras-docs-logo--light': hasDistinctLogos }">
|
|
28
|
+
<img v-if="hasDistinctLogos" :src="darkLogo" :alt="logoAlt" class="selaras-docs-logo selaras-docs-logo--dark">
|
|
29
|
+
</template>
|
|
30
|
+
<span v-if="showTitle">{{ siteName }}</span>
|
|
31
|
+
</NuxtLink>
|
|
32
|
+
<template #right>
|
|
33
|
+
<SNavigationMenu
|
|
34
|
+
v-if="headerLinks.length"
|
|
35
|
+
:items="headerLinks"
|
|
36
|
+
variant="link"
|
|
37
|
+
:ui="{ root: 'hidden w-auto lg:flex' }"
|
|
38
|
+
/>
|
|
39
|
+
<DocsSearchButton v-if="docsConfig.header?.search !== false" />
|
|
40
|
+
<SColorModeToggle v-if="docsConfig.header?.colorMode !== false" />
|
|
41
|
+
<SButton
|
|
42
|
+
v-if="repositoryUrl"
|
|
43
|
+
as="a"
|
|
44
|
+
:href="repositoryUrl"
|
|
45
|
+
target="_blank"
|
|
46
|
+
rel="noreferrer"
|
|
47
|
+
aria-label="Source repository"
|
|
48
|
+
title="Source repository"
|
|
49
|
+
icon="hugeicons:github"
|
|
50
|
+
variant="ghost"
|
|
51
|
+
color="neutral"
|
|
52
|
+
/>
|
|
53
|
+
<SButton
|
|
54
|
+
variant="ghost"
|
|
55
|
+
color="neutral"
|
|
56
|
+
class="selaras-docs-nav-toggle"
|
|
57
|
+
aria-label="Open documentation navigation"
|
|
58
|
+
@click="navigationOpen = true"
|
|
59
|
+
>
|
|
60
|
+
Menu
|
|
61
|
+
</SButton>
|
|
62
|
+
</template>
|
|
63
|
+
</SHeader>
|
|
64
|
+
<SDrawer v-model:open="navigationOpen" side="left" title="Documentation navigation" :handle="false">
|
|
65
|
+
<template #body>
|
|
66
|
+
<SContentNavigation :navigation="navigation ?? []" />
|
|
67
|
+
</template>
|
|
68
|
+
</SDrawer>
|
|
69
|
+
</template>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CommandPaletteGroup } from '@sewadah/selaras/components/CommandPalette.vue'
|
|
3
|
+
import type { ContentNavigationLink } from '@sewadah/selaras/components/ContentNavigation.vue'
|
|
4
|
+
|
|
5
|
+
const appConfig = useAppConfig()
|
|
6
|
+
const { data: navigation } = useDocsNavigation()
|
|
7
|
+
const docsConfig = computed(() => appConfig.selarasDocs ?? {})
|
|
8
|
+
|
|
9
|
+
function leafPages(links: ContentNavigationLink[]): ContentNavigationLink[] {
|
|
10
|
+
return links.flatMap(link => link.children?.length ? leafPages(link.children) : [link])
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const groups = computed<CommandPaletteGroup[]>(() => {
|
|
14
|
+
return (navigation.value ?? [])
|
|
15
|
+
.map(section => ({
|
|
16
|
+
label: section.title,
|
|
17
|
+
items: leafPages(section.children?.length ? section.children : [section]).map(page => ({
|
|
18
|
+
label: page.title,
|
|
19
|
+
onSelect: () => navigateTo(page.path),
|
|
20
|
+
})),
|
|
21
|
+
}))
|
|
22
|
+
.filter(group => group.items.length > 0)
|
|
23
|
+
})
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<SCommandPalette v-if="docsConfig.header?.search !== false" :groups="groups" />
|
|
28
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const { open } = useCommandPalette()
|
|
3
|
+
const icons = useIcons()
|
|
4
|
+
const messages = useMessages()
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<SButton variant="soft" color="neutral" size="sm" :icon="icons.search" aria-label="Search documentation" @click="open">
|
|
9
|
+
<span class="hidden sm:inline">{{ messages.search }}</span>
|
|
10
|
+
<SKbd value="meta" size="sm" />
|
|
11
|
+
<SKbd value="K" size="sm" />
|
|
12
|
+
</SButton>
|
|
13
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const { data: navigation } = useDocsNavigation()
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<SPageAside
|
|
7
|
+
class="selaras-docs-sidebar"
|
|
8
|
+
aria-label="Documentation navigation"
|
|
9
|
+
:ui="{ root: 'top-16 h-[calc(100vh-4rem)]' }"
|
|
10
|
+
>
|
|
11
|
+
<SContentNavigation :navigation="navigation ?? []" />
|
|
12
|
+
</SPageAside>
|
|
13
|
+
</template>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { TocLink } from '@sewadah/selaras/components/ContentToc.vue'
|
|
3
|
+
|
|
4
|
+
defineProps<{
|
|
5
|
+
links: TocLink[]
|
|
6
|
+
}>()
|
|
7
|
+
|
|
8
|
+
const appConfig = useAppConfig()
|
|
9
|
+
const docsConfig = computed(() => appConfig.selarasDocs ?? {})
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<SPageAside
|
|
14
|
+
v-if="links.length && docsConfig.toc?.enabled !== false"
|
|
15
|
+
class="selaras-docs-toc"
|
|
16
|
+
aria-label="Table of contents"
|
|
17
|
+
:ui="{ root: 'top-16 h-[calc(100vh-4rem)]' }"
|
|
18
|
+
>
|
|
19
|
+
<SContentToc :links="links" :title="docsConfig.toc?.title" />
|
|
20
|
+
</SPageAside>
|
|
21
|
+
</template>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = defineProps<{
|
|
3
|
+
name: string
|
|
4
|
+
}>()
|
|
5
|
+
|
|
6
|
+
const exampleModules = import.meta.glob('~/components/content/examples/**/*.vue')
|
|
7
|
+
|
|
8
|
+
function pascalCase(value: string) {
|
|
9
|
+
return value.split('-').map(part => part.charAt(0).toUpperCase() + part.slice(1)).join('')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const resolvedComponent = computed(() => {
|
|
13
|
+
const suffix = `/${pascalCase(props.name)}.vue`
|
|
14
|
+
const match = Object.entries(exampleModules).find(([path]) => path.endsWith(suffix))
|
|
15
|
+
return match ? defineAsyncComponent(match[1] as any) : null
|
|
16
|
+
})
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<section v-if="resolvedComponent" :data-example="name">
|
|
21
|
+
<component :is="resolvedComponent" />
|
|
22
|
+
</section>
|
|
23
|
+
<p v-else role="alert">
|
|
24
|
+
Example "{{ name }}" not found
|
|
25
|
+
</p>
|
|
26
|
+
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ContentNavigationLink } from '@sewadah/selaras/components/ContentNavigation.vue'
|
|
2
|
+
|
|
3
|
+
function flattenNavigation(links: ContentNavigationLink[]): ContentNavigationLink[] {
|
|
4
|
+
return links.flatMap(link => link.children?.length ? flattenNavigation(link.children) : [link])
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** The adjacent leaf pages in the collection's reading order. */
|
|
8
|
+
export function useDocsSurround() {
|
|
9
|
+
const route = useRoute()
|
|
10
|
+
const { data: navigation } = useDocsNavigation()
|
|
11
|
+
|
|
12
|
+
return computed(() => {
|
|
13
|
+
const links = flattenNavigation(navigation.value ?? [])
|
|
14
|
+
const index = links.findIndex(link => link.path === route.path)
|
|
15
|
+
|
|
16
|
+
return index === -1
|
|
17
|
+
? { prev: undefined, next: undefined }
|
|
18
|
+
: { prev: links[index - 1], next: links[index + 1] }
|
|
19
|
+
})
|
|
20
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="selaras-docs-root min-h-screen text-[var(--selaras-resolved-text-default)]">
|
|
3
|
+
<a class="selaras-docs-skip-link" href="#selaras-docs-main">Skip to content</a>
|
|
4
|
+
<DocsHeader />
|
|
5
|
+
<div class="selaras-docs-shell">
|
|
6
|
+
<DocsSidebar />
|
|
7
|
+
<main id="selaras-docs-main" class="selaras-docs-main" tabindex="-1">
|
|
8
|
+
<slot />
|
|
9
|
+
</main>
|
|
10
|
+
</div>
|
|
11
|
+
<DocsFooter />
|
|
12
|
+
</div>
|
|
13
|
+
</template>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { TocLink } from '@sewadah/selaras/components/ContentToc.vue'
|
|
3
|
+
import ProseH1 from '@sewadah/selaras/components/ProseH1.vue'
|
|
4
|
+
import ProseH2 from '@sewadah/selaras/components/ProseH2.vue'
|
|
5
|
+
import ProseH3 from '@sewadah/selaras/components/ProseH3.vue'
|
|
6
|
+
import ProseH4 from '@sewadah/selaras/components/ProseH4.vue'
|
|
7
|
+
import ProseH5 from '@sewadah/selaras/components/ProseH5.vue'
|
|
8
|
+
import ProseH6 from '@sewadah/selaras/components/ProseH6.vue'
|
|
9
|
+
import ProsePre from '@sewadah/selaras/components/ProsePre.vue'
|
|
10
|
+
|
|
11
|
+
const route = useRoute()
|
|
12
|
+
const appConfig = useAppConfig()
|
|
13
|
+
const { data: page } = await useAsyncData(`selaras-docs:${route.path}`, () => queryCollection('docs').path(route.path).first())
|
|
14
|
+
|
|
15
|
+
if (!page.value) {
|
|
16
|
+
throw createError({ statusCode: 404, statusMessage: 'Documentation page not found', fatal: true })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
definePageMeta({ layout: 'selaras-docs' })
|
|
20
|
+
useSeoMeta({ title: page.value.title, description: page.value.description ?? appConfig.selarasDocs?.site?.description })
|
|
21
|
+
|
|
22
|
+
const proseComponents = {
|
|
23
|
+
h1: ProseH1,
|
|
24
|
+
h2: ProseH2,
|
|
25
|
+
h3: ProseH3,
|
|
26
|
+
h4: ProseH4,
|
|
27
|
+
h5: ProseH5,
|
|
28
|
+
h6: ProseH6,
|
|
29
|
+
pre: ProsePre,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const tocLinks = computed(() => (page.value?.body?.toc?.links ?? []) as unknown as TocLink[])
|
|
33
|
+
const surround = useDocsSurround()
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<SContainer size="full" class="selaras-docs-page">
|
|
38
|
+
<div class="selaras-docs-page-grid">
|
|
39
|
+
<article class="selaras-docs-article">
|
|
40
|
+
<p v-if="page!.description" class="selaras-docs-description">
|
|
41
|
+
{{ page!.description }}
|
|
42
|
+
</p>
|
|
43
|
+
<ContentRenderer :value="page!" :components="proseComponents" class="selaras-docs-content selaras-prose" />
|
|
44
|
+
<DocsEditLink :path="route.path" />
|
|
45
|
+
<SContentSurround :prev="surround.prev" :next="surround.next" class="selaras-docs-surround" />
|
|
46
|
+
</article>
|
|
47
|
+
<DocsTableOfContents :links="tocLinks" />
|
|
48
|
+
</div>
|
|
49
|
+
</SContainer>
|
|
50
|
+
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import { defineCollection, defineContentConfig, z } from '@nuxt/content'
|
|
3
|
+
import { useNuxt } from '@nuxt/kit'
|
|
4
|
+
|
|
5
|
+
const { options } = useNuxt()
|
|
6
|
+
|
|
7
|
+
export default defineContentConfig({
|
|
8
|
+
collections: {
|
|
9
|
+
docs: defineCollection({
|
|
10
|
+
type: 'page',
|
|
11
|
+
source: {
|
|
12
|
+
cwd: resolve(options.rootDir, 'content'),
|
|
13
|
+
include: '**/*.md',
|
|
14
|
+
},
|
|
15
|
+
schema: z.object({
|
|
16
|
+
order: z.number().optional(),
|
|
17
|
+
}),
|
|
18
|
+
}),
|
|
19
|
+
},
|
|
20
|
+
})
|
package/modules/docs.mjs
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import { addTemplate, createResolver, defineNuxtModule } from '@nuxt/kit'
|
|
3
|
+
|
|
4
|
+
function cssPath(path) {
|
|
5
|
+
return path.replaceAll('\\', '/')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default defineNuxtModule({
|
|
9
|
+
meta: {
|
|
10
|
+
name: '@sewadah/selaras-docs',
|
|
11
|
+
configKey: 'selarasDocs',
|
|
12
|
+
},
|
|
13
|
+
defaults: {
|
|
14
|
+
css: true,
|
|
15
|
+
},
|
|
16
|
+
setup(options, nuxt) {
|
|
17
|
+
if (!options.css)
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
const { resolve: resolveLayer } = createResolver(import.meta.url)
|
|
21
|
+
const layerApp = cssPath(resolveLayer('../app'))
|
|
22
|
+
const consumerContent = cssPath(resolve(nuxt.options.rootDir, 'content'))
|
|
23
|
+
const consumerComponents = cssPath(resolve(nuxt.options.rootDir, 'components'))
|
|
24
|
+
const stylesheet = addTemplate({
|
|
25
|
+
filename: 'selaras-docs.css',
|
|
26
|
+
write: true,
|
|
27
|
+
getContents: () => `@import "tailwindcss";
|
|
28
|
+
@import "@sewadah/selaras/theme.css";
|
|
29
|
+
@import "#selaras/tailwind.css";
|
|
30
|
+
@import "@sewadah/selaras/prose.css";
|
|
31
|
+
@import "@sewadah/selaras-docs/style.css";
|
|
32
|
+
|
|
33
|
+
@source "${layerApp}/**/*";
|
|
34
|
+
@source "${consumerContent}/**/*";
|
|
35
|
+
@source "${consumerComponents}/**/*";
|
|
36
|
+
`,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
nuxt.options.css.unshift(stylesheet.dst)
|
|
40
|
+
},
|
|
41
|
+
})
|
package/nuxt.config.mjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createResolver } from '@nuxt/kit'
|
|
2
|
+
import { defineNuxtConfig } from 'nuxt/config'
|
|
3
|
+
|
|
4
|
+
const { resolve } = createResolver(import.meta.url)
|
|
5
|
+
|
|
6
|
+
export default defineNuxtConfig({
|
|
7
|
+
modules: ['@sewadah/selaras', '@nuxt/content', resolve('./modules/docs.mjs')],
|
|
8
|
+
// Selaras supports Node versions with node:sqlite. Keeping Content on its
|
|
9
|
+
// native connector lets an installed docs layer avoid a native addon.
|
|
10
|
+
content: {
|
|
11
|
+
experimental: {
|
|
12
|
+
sqliteConnector: 'native',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sewadah/selaras-docs",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Nuxt Content documentation layer for Selaras",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./types.d.ts",
|
|
9
|
+
"import": "./nuxt.config.mjs"
|
|
10
|
+
},
|
|
11
|
+
"./style.css": "./app/assets/style.css"
|
|
12
|
+
},
|
|
13
|
+
"main": "./nuxt.config.mjs",
|
|
14
|
+
"files": [
|
|
15
|
+
"app",
|
|
16
|
+
"content.config.ts",
|
|
17
|
+
"modules",
|
|
18
|
+
"nuxt.config.mjs",
|
|
19
|
+
"types.d.ts"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": "^22.19.0 || ^24.11.0 || >=26.0.0"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"nuxt": "^4.5.2"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@nuxt/content": "^3.16.0",
|
|
29
|
+
"@nuxt/kit": "^4.5.2",
|
|
30
|
+
"@sewadah/selaras": "^0.3.0",
|
|
31
|
+
"tailwindcss": "^4.3.0"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export interface SelarasDocsModuleOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Inject the default unprefixed Tailwind and Selaras stylesheet.
|
|
4
|
+
* Disable this when the consuming app owns a prefixed or complete-theme CSS entry.
|
|
5
|
+
* @default true
|
|
6
|
+
*/
|
|
7
|
+
css?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SelarasDocsLink {
|
|
11
|
+
label?: string
|
|
12
|
+
to: string
|
|
13
|
+
icon?: string
|
|
14
|
+
target?: string
|
|
15
|
+
ariaLabel?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A top-level header item rendered by Selaras' NavigationMenu. */
|
|
19
|
+
export interface SelarasDocsNavigationItem {
|
|
20
|
+
/** Required so every header control has an accessible name. */
|
|
21
|
+
label: string
|
|
22
|
+
to?: string
|
|
23
|
+
icon?: string
|
|
24
|
+
target?: string
|
|
25
|
+
rel?: string
|
|
26
|
+
ariaLabel?: string
|
|
27
|
+
disabled?: boolean
|
|
28
|
+
children?: SelarasDocsNavigationItem[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SelarasDocsLogo {
|
|
32
|
+
light?: string
|
|
33
|
+
dark?: string
|
|
34
|
+
alt?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SelarasDocsAppConfig {
|
|
38
|
+
site?: {
|
|
39
|
+
name?: string
|
|
40
|
+
description?: string
|
|
41
|
+
logo?: string | SelarasDocsLogo
|
|
42
|
+
}
|
|
43
|
+
repository?: {
|
|
44
|
+
url?: string
|
|
45
|
+
branch?: string
|
|
46
|
+
contentDirectory?: string
|
|
47
|
+
editLinks?: boolean
|
|
48
|
+
}
|
|
49
|
+
header?: {
|
|
50
|
+
title?: string
|
|
51
|
+
showTitle?: boolean
|
|
52
|
+
search?: boolean
|
|
53
|
+
colorMode?: boolean
|
|
54
|
+
links?: SelarasDocsNavigationItem[]
|
|
55
|
+
}
|
|
56
|
+
toc?: {
|
|
57
|
+
enabled?: boolean
|
|
58
|
+
title?: string
|
|
59
|
+
}
|
|
60
|
+
footer?: {
|
|
61
|
+
text?: string
|
|
62
|
+
links?: SelarasDocsLink[]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
declare module '@nuxt/schema' {
|
|
67
|
+
interface AppConfig {
|
|
68
|
+
selarasDocs?: SelarasDocsAppConfig
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface NuxtConfig {
|
|
72
|
+
selarasDocs?: SelarasDocsModuleOptions
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface NuxtOptions {
|
|
76
|
+
selarasDocs: SelarasDocsModuleOptions
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
declare module 'nuxt/schema' {
|
|
81
|
+
interface AppConfig {
|
|
82
|
+
selarasDocs?: SelarasDocsAppConfig
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface NuxtConfig {
|
|
86
|
+
selarasDocs?: SelarasDocsModuleOptions
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface NuxtOptions {
|
|
90
|
+
selarasDocs: SelarasDocsModuleOptions
|
|
91
|
+
}
|
|
92
|
+
}
|