docs-please 0.2.0-beta.0 → 0.2.2-beta.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.
Files changed (51) hide show
  1. package/app/app.config.ts +9 -1
  2. package/app/assets/css/main.css +2 -2
  3. package/app/components/app/AppHeader.vue +92 -37
  4. package/app/components/app/AppHeaderCenter.vue +3 -0
  5. package/app/components/app/AppHeaderLogo.vue +43 -0
  6. package/app/components/app/AppHeaderSearch.vue +3 -0
  7. package/app/components/app/AppSearch.vue +189 -0
  8. package/app/components/app/AppSearchButton.vue +51 -0
  9. package/app/components/content/Accordion.vue +117 -0
  10. package/app/components/content/AccordionItem.vue +27 -0
  11. package/app/components/content/Badge.vue +42 -0
  12. package/app/components/content/Collapsible.vue +56 -0
  13. package/app/components/content/ProseKbd.vue +9 -0
  14. package/app/components/content/ProseTable.vue +2 -2
  15. package/app/components/content/ProseTh.vue +1 -1
  16. package/app/components/content/ProseTr.vue +1 -1
  17. package/app/components/ui/command/Command.vue +86 -0
  18. package/app/components/ui/command/CommandDialog.vue +21 -0
  19. package/app/components/ui/command/CommandEmpty.vue +23 -0
  20. package/app/components/ui/command/CommandGroup.vue +44 -0
  21. package/app/components/ui/command/CommandInput.vue +35 -0
  22. package/app/components/ui/command/CommandItem.vue +75 -0
  23. package/app/components/ui/command/CommandList.vue +21 -0
  24. package/app/components/ui/command/CommandSeparator.vue +20 -0
  25. package/app/components/ui/command/CommandShortcut.vue +14 -0
  26. package/app/components/ui/command/index.ts +25 -0
  27. package/app/components/ui/dialog/Dialog.vue +19 -0
  28. package/app/components/ui/dialog/DialogClose.vue +15 -0
  29. package/app/components/ui/dialog/DialogContent.vue +53 -0
  30. package/app/components/ui/dialog/DialogDescription.vue +23 -0
  31. package/app/components/ui/dialog/DialogFooter.vue +15 -0
  32. package/app/components/ui/dialog/DialogHeader.vue +17 -0
  33. package/app/components/ui/dialog/DialogOverlay.vue +21 -0
  34. package/app/components/ui/dialog/DialogScrollContent.vue +59 -0
  35. package/app/components/ui/dialog/DialogTitle.vue +23 -0
  36. package/app/components/ui/dialog/DialogTrigger.vue +15 -0
  37. package/app/components/ui/dialog/index.ts +10 -0
  38. package/app/components/ui/kbd/Kbd.vue +21 -0
  39. package/app/components/ui/kbd/KbdGroup.vue +17 -0
  40. package/app/components/ui/kbd/index.ts +2 -0
  41. package/app/composables/useContentSearch.ts +52 -0
  42. package/app/layouts/default.vue +1 -0
  43. package/app/layouts/docs.vue +1 -0
  44. package/app/utils/navigation.ts +7 -0
  45. package/app/utils/prerender.ts +12 -0
  46. package/nuxt.config.ts +4 -0
  47. package/nuxt.schema.ts +44 -0
  48. package/package.json +9 -3
  49. package/server/routes/raw/[...slug].md.get.ts +45 -0
  50. package/utils/git.ts +59 -0
  51. package/utils/meta.ts +29 -0
@@ -0,0 +1,59 @@
1
+ <script setup lang="ts">
2
+ import type { DialogContentEmits, DialogContentProps } from "reka-ui"
3
+ import type { HTMLAttributes } from "vue"
4
+ import { reactiveOmit } from "@vueuse/core"
5
+ import { X } from "lucide-vue-next"
6
+ import {
7
+ DialogClose,
8
+ DialogContent,
9
+ DialogOverlay,
10
+ DialogPortal,
11
+ useForwardPropsEmits,
12
+ } from "reka-ui"
13
+ import { cn } from '~/lib/utils'
14
+
15
+ defineOptions({
16
+ inheritAttrs: false,
17
+ })
18
+
19
+ const props = defineProps<DialogContentProps & { class?: HTMLAttributes["class"] }>()
20
+ const emits = defineEmits<DialogContentEmits>()
21
+
22
+ const delegatedProps = reactiveOmit(props, "class")
23
+
24
+ const forwarded = useForwardPropsEmits(delegatedProps, emits)
25
+ </script>
26
+
27
+ <template>
28
+ <DialogPortal>
29
+ <DialogOverlay
30
+ class="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
31
+ >
32
+ <DialogContent
33
+ :class="
34
+ cn(
35
+ 'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
36
+ props.class,
37
+ )
38
+ "
39
+ v-bind="{ ...$attrs, ...forwarded }"
40
+ @pointer-down-outside="(event) => {
41
+ const originalEvent = event.detail.originalEvent;
42
+ const target = originalEvent.target as HTMLElement;
43
+ if (originalEvent.offsetX > target.clientWidth || originalEvent.offsetY > target.clientHeight) {
44
+ event.preventDefault();
45
+ }
46
+ }"
47
+ >
48
+ <slot />
49
+
50
+ <DialogClose
51
+ class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary"
52
+ >
53
+ <X class="w-4 h-4" />
54
+ <span class="sr-only">Close</span>
55
+ </DialogClose>
56
+ </DialogContent>
57
+ </DialogOverlay>
58
+ </DialogPortal>
59
+ </template>
@@ -0,0 +1,23 @@
1
+ <script setup lang="ts">
2
+ import type { DialogTitleProps } from "reka-ui"
3
+ import type { HTMLAttributes } from "vue"
4
+ import { reactiveOmit } from "@vueuse/core"
5
+ import { DialogTitle, useForwardProps } from "reka-ui"
6
+ import { cn } from '~/lib/utils'
7
+
8
+ const props = defineProps<DialogTitleProps & { class?: HTMLAttributes["class"] }>()
9
+
10
+ const delegatedProps = reactiveOmit(props, "class")
11
+
12
+ const forwardedProps = useForwardProps(delegatedProps)
13
+ </script>
14
+
15
+ <template>
16
+ <DialogTitle
17
+ data-slot="dialog-title"
18
+ v-bind="forwardedProps"
19
+ :class="cn('text-lg leading-none font-semibold', props.class)"
20
+ >
21
+ <slot />
22
+ </DialogTitle>
23
+ </template>
@@ -0,0 +1,15 @@
1
+ <script setup lang="ts">
2
+ import type { DialogTriggerProps } from "reka-ui"
3
+ import { DialogTrigger } from "reka-ui"
4
+
5
+ const props = defineProps<DialogTriggerProps>()
6
+ </script>
7
+
8
+ <template>
9
+ <DialogTrigger
10
+ data-slot="dialog-trigger"
11
+ v-bind="props"
12
+ >
13
+ <slot />
14
+ </DialogTrigger>
15
+ </template>
@@ -0,0 +1,10 @@
1
+ export { default as Dialog } from "./Dialog.vue"
2
+ export { default as DialogClose } from "./DialogClose.vue"
3
+ export { default as DialogContent } from "./DialogContent.vue"
4
+ export { default as DialogDescription } from "./DialogDescription.vue"
5
+ export { default as DialogFooter } from "./DialogFooter.vue"
6
+ export { default as DialogHeader } from "./DialogHeader.vue"
7
+ export { default as DialogOverlay } from "./DialogOverlay.vue"
8
+ export { default as DialogScrollContent } from "./DialogScrollContent.vue"
9
+ export { default as DialogTitle } from "./DialogTitle.vue"
10
+ export { default as DialogTrigger } from "./DialogTrigger.vue"
@@ -0,0 +1,21 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from "vue"
3
+ import { cn } from '~/lib/utils'
4
+
5
+ const props = defineProps<{
6
+ class?: HTMLAttributes["class"]
7
+ }>()
8
+ </script>
9
+
10
+ <template>
11
+ <kbd
12
+ :class="cn(
13
+ 'bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none',
14
+ '[&_svg:not([class*=\'size-\'])]:size-3',
15
+ '[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10',
16
+ props.class,
17
+ )"
18
+ >
19
+ <slot />
20
+ </kbd>
21
+ </template>
@@ -0,0 +1,17 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from "vue"
3
+ import { cn } from '~/lib/utils'
4
+
5
+ const props = defineProps<{
6
+ class?: HTMLAttributes["class"]
7
+ }>()
8
+ </script>
9
+
10
+ <template>
11
+ <kbd
12
+ data-slot="kbd-group"
13
+ :class="cn('inline-flex items-center gap-1', props.class)"
14
+ >
15
+ <slot />
16
+ </kbd>
17
+ </template>
@@ -0,0 +1,2 @@
1
+ export { default as Kbd } from "./Kbd.vue"
2
+ export { default as KbdGroup } from "./KbdGroup.vue"
@@ -0,0 +1,52 @@
1
+ import type { NavigationItem } from './useNavigation'
2
+
3
+ const open = ref(false)
4
+
5
+ export interface SearchItem {
6
+ id: string
7
+ label: string
8
+ icon?: Component
9
+ to?: string
10
+ onSelect?: () => void
11
+ prefix?: string
12
+ suffix?: string
13
+ active?: boolean
14
+ disabled?: boolean
15
+ isDoc?: boolean
16
+ }
17
+
18
+ export interface SearchGroup {
19
+ id: string
20
+ label: string
21
+ items: SearchItem[]
22
+ }
23
+
24
+ export function useContentSearch() {
25
+ return {
26
+ open,
27
+ }
28
+ }
29
+
30
+ export function flattenNavigation(items: NavigationItem[], prefix = '', isDocSection = false): SearchItem[] {
31
+ return items.flatMap((item) => {
32
+ const result: SearchItem[] = []
33
+ // Determine if this item is a doc page based on parent context or path
34
+ const itemIsDoc = isDocSection || item.path?.startsWith('/docs')
35
+
36
+ if (item.type === 'page' && item.path) {
37
+ result.push({
38
+ id: item.path,
39
+ label: item.title,
40
+ to: item.path,
41
+ prefix: prefix || undefined,
42
+ isDoc: itemIsDoc,
43
+ })
44
+ }
45
+
46
+ if (item.children?.length) {
47
+ result.push(...flattenNavigation(item.children, item.title, itemIsDoc))
48
+ }
49
+
50
+ return result
51
+ })
52
+ }
@@ -8,5 +8,6 @@
8
8
  <slot />
9
9
  </main>
10
10
  <AppFooter />
11
+ <AppSearch />
11
12
  </div>
12
13
  </template>
@@ -23,5 +23,6 @@
23
23
  </div>
24
24
  </div>
25
25
  <AppFooter />
26
+ <AppSearch />
26
27
  </div>
27
28
  </template>
@@ -0,0 +1,7 @@
1
+ import type { ContentNavigationItem } from '@nuxt/content'
2
+
3
+ export const flattenNavigation = (items?: ContentNavigationItem[]): ContentNavigationItem[] => items?.flatMap(
4
+ item => item.children
5
+ ? flattenNavigation(item.children)
6
+ : [item],
7
+ ) || []
@@ -0,0 +1,12 @@
1
+ export const addPrerenderPath = (path: string) => {
2
+ const event = useRequestEvent()
3
+ if (event) {
4
+ event.node.res.setHeader(
5
+ 'x-nitro-prerender',
6
+ [
7
+ event.node.res.getHeader('x-nitro-prerender'),
8
+ path,
9
+ ].filter(Boolean).join(','),
10
+ )
11
+ }
12
+ }
package/nuxt.config.ts CHANGED
@@ -13,6 +13,9 @@ export default defineNuxtConfig({
13
13
  '@nuxt/image',
14
14
  '@nuxt/icon',
15
15
  'nuxt-og-image',
16
+ '@nuxtjs/robots',
17
+ 'nuxt-og-image',
18
+ 'nuxt-llms',
16
19
  ],
17
20
  devtools: { enabled: true },
18
21
  css: [resolve('./app/assets/css/main.css')],
@@ -61,6 +64,7 @@ export default defineNuxtConfig({
61
64
  'tabs': 'Tabs',
62
65
  'tabs-item': 'TabsItem',
63
66
  'u-color-mode-image': 'UColorModeImage',
67
+ 'kbd': 'ProseKbd',
64
68
  },
65
69
  },
66
70
  },
package/nuxt.schema.ts CHANGED
@@ -29,6 +29,50 @@ export default defineNuxtSchema({
29
29
  type: 'string',
30
30
  },
31
31
  },
32
+ header: {
33
+ $schema: {
34
+ title: 'Header Configuration',
35
+ description: 'Header configuration options',
36
+ },
37
+ title: {
38
+ $default: '',
39
+ $schema: {
40
+ title: 'Header Title',
41
+ description: 'Title displayed in the header (defaults to site title)',
42
+ type: 'string',
43
+ },
44
+ },
45
+ logo: {
46
+ $schema: {
47
+ title: 'Logo Configuration',
48
+ description: 'Logo images for light and dark modes',
49
+ },
50
+ light: {
51
+ $default: '',
52
+ $schema: {
53
+ title: 'Light Mode Logo',
54
+ description: 'Logo image for light mode',
55
+ type: 'string',
56
+ },
57
+ },
58
+ dark: {
59
+ $default: '',
60
+ $schema: {
61
+ title: 'Dark Mode Logo',
62
+ description: 'Logo image for dark mode',
63
+ type: 'string',
64
+ },
65
+ },
66
+ alt: {
67
+ $default: '',
68
+ $schema: {
69
+ title: 'Logo Alt Text',
70
+ description: 'Alt text for the logo image',
71
+ type: 'string',
72
+ },
73
+ },
74
+ },
75
+ },
32
76
  github: {
33
77
  $schema: {
34
78
  title: 'GitHub Configuration',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "docs-please",
3
3
  "type": "module",
4
- "version": "0.2.0-beta.0",
4
+ "version": "0.2.2-beta.0",
5
5
  "description": "Nuxt layer for documentation sites using shadcn-vue",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -18,7 +18,8 @@
18
18
  "modules",
19
19
  "nuxt.config.ts",
20
20
  "nuxt.schema.ts",
21
- "server"
21
+ "server",
22
+ "utils"
22
23
  ],
23
24
  "scripts": {
24
25
  "dev": "nuxt dev",
@@ -29,7 +30,8 @@
29
30
  "typecheck": "vue-tsc -b"
30
31
  },
31
32
  "peerDependencies": {
32
- "nuxt": "^4.0.0"
33
+ "better-sqlite3": "12.x",
34
+ "nuxt": "4.x"
33
35
  },
34
36
  "dependencies": {
35
37
  "@iconify-json/lucide": "^1.2.75",
@@ -42,12 +44,14 @@
42
44
  "@nuxtjs/mdc": "^0.18.4",
43
45
  "@tabler/icons-vue": "^3.35.0",
44
46
  "@vueuse/core": "^14.1.0",
47
+ "@vueuse/integrations": "^14.1.0",
45
48
  "class-variance-authority": "^0.7.1",
46
49
  "clsx": "^2.1.1",
47
50
  "debug": "^4.4.1",
48
51
  "defu": "^6.1.4",
49
52
  "eslint": "^9.39.1",
50
53
  "exsolve": "^1.0.8",
54
+ "fuse.js": "^7.1.0",
51
55
  "lucide-vue-next": "^0.555.0",
52
56
  "nuxt-og-image": "^5.1.12",
53
57
  "oxc-parser": "^0.99.0",
@@ -70,8 +74,10 @@
70
74
  "zod-to-json-schema": "^3.25.0"
71
75
  },
72
76
  "devDependencies": {
77
+ "@nuxtjs/robots": "^5.6.1",
73
78
  "@tailwindcss/vite": "^4.1.17",
74
79
  "better-sqlite3": "^11.9.1",
80
+ "nuxt-llms": "^0.1.3",
75
81
  "typescript": "^5.9.3",
76
82
  "vue-tsc": "^2.0.0"
77
83
  },
@@ -0,0 +1,45 @@
1
+ import { withLeadingSlash } from 'ufo'
2
+ import { stringify } from 'minimark/stringify'
3
+ import { queryCollection } from '@nuxt/content/nitro'
4
+ import type { Collections } from '@nuxt/content'
5
+
6
+ export default eventHandler(async (event) => {
7
+ const slug = getRouterParams(event)['slug.md']
8
+ if (!slug?.endsWith('.md')) {
9
+ throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
10
+ }
11
+
12
+ const path = withLeadingSlash(slug.replace('.md', ''))
13
+ const config = useRuntimeConfig(event).public
14
+
15
+ let collectionName = 'docs'
16
+ if (config.i18n?.locales) {
17
+ const pathSegments = path.split('/').filter(Boolean)
18
+ const firstSegment = pathSegments[0]
19
+
20
+ const availableLocales = config.i18n.locales.map((locale: string | { code: string }) =>
21
+ typeof locale === 'string' ? locale : locale.code,
22
+ )
23
+
24
+ if (firstSegment && availableLocales.includes(firstSegment)) {
25
+ collectionName = `docs_${firstSegment}`
26
+ }
27
+ else if (config.i18n.defaultLocale) {
28
+ collectionName = `docs_${config.i18n.defaultLocale}`
29
+ }
30
+ }
31
+
32
+ const page = await queryCollection(event, collectionName as keyof Collections).path(path).first()
33
+ if (!page) {
34
+ throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
35
+ }
36
+
37
+ // Add title and description to the top of the page if missing
38
+ if (page.body.value[0]?.[0] !== 'h1') {
39
+ page.body.value.unshift(['blockquote', {}, page.description])
40
+ page.body.value.unshift(['h1', {}, page.title])
41
+ }
42
+
43
+ setHeader(event, 'Content-Type', 'text/markdown; charset=utf-8')
44
+ return stringify({ ...page.body, type: 'minimark' }, { format: 'markdown/html' })
45
+ })
package/utils/git.ts ADDED
@@ -0,0 +1,59 @@
1
+ import { execSync } from 'node:child_process'
2
+ import process from 'node:process'
3
+
4
+ export interface GitInfo {
5
+ name: string
6
+ owner: string
7
+ url: string
8
+ }
9
+
10
+ export function getGitBranch(): string {
11
+ const envName
12
+ = process.env.CF_PAGES_BRANCH
13
+ || process.env.CI_COMMIT_BRANCH
14
+ || process.env.VERCEL_GIT_COMMIT_REF
15
+ || process.env.BRANCH
16
+ || process.env.GITHUB_REF_NAME
17
+
18
+ if (envName && envName !== 'HEAD') {
19
+ return envName
20
+ }
21
+ try {
22
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim()
23
+ if (branch && branch !== 'HEAD') {
24
+ return branch
25
+ }
26
+ }
27
+ catch {
28
+ // Ignore error
29
+ }
30
+
31
+ return 'main'
32
+ }
33
+
34
+ export function getGitEnv(): GitInfo {
35
+ const envInfo = {
36
+ provider: process.env.VERCEL_GIT_PROVIDER
37
+ || (process.env.GITHUB_SERVER_URL ? 'github' : undefined)
38
+ || '',
39
+ owner: process.env.VERCEL_GIT_REPO_OWNER
40
+ || process.env.GITHUB_REPOSITORY_OWNER
41
+ || process.env.CI_PROJECT_PATH?.split('/').shift()
42
+ || '',
43
+ name: process.env.VERCEL_GIT_REPO_SLUG
44
+ || process.env.GITHUB_REPOSITORY?.split('/').pop()
45
+ || process.env.CI_PROJECT_PATH?.split('/').splice(1).join('/')
46
+ || '',
47
+ url: process.env.REPOSITORY_URL || '',
48
+ }
49
+
50
+ if (!envInfo.url && envInfo.provider && envInfo.owner && envInfo.name) {
51
+ envInfo.url = `https://${envInfo.provider}.com/${envInfo.owner}/${envInfo.name}`
52
+ }
53
+
54
+ return {
55
+ name: envInfo.name,
56
+ owner: envInfo.owner,
57
+ url: envInfo.url,
58
+ }
59
+ }
package/utils/meta.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { resolve } from 'node:path'
3
+ import process from 'node:process'
4
+
5
+ export function inferSiteURL(): string | undefined {
6
+ return (
7
+ process.env.NUXT_SITE_URL
8
+ || (process.env.NEXT_PUBLIC_VERCEL_URL && `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`)
9
+ || process.env.URL // Netlify
10
+ || process.env.CI_PAGES_URL // Gitlab Pages
11
+ || process.env.CF_PAGES_URL // Cloudflare Pages
12
+ )
13
+ }
14
+
15
+ export async function getPackageJsonMetadata(dir: string): Promise<{ name: string, description?: string }> {
16
+ try {
17
+ const packageJson = await readFile(resolve(dir, 'package.json'), 'utf-8')
18
+ const parsed = JSON.parse(packageJson)
19
+ return {
20
+ name: parsed.name,
21
+ description: parsed.description,
22
+ }
23
+ }
24
+ catch {
25
+ return {
26
+ name: 'docs',
27
+ }
28
+ }
29
+ }