skrypt-ai 0.4.2 → 0.6.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 (159) hide show
  1. package/dist/auth/index.d.ts +13 -3
  2. package/dist/auth/index.js +101 -9
  3. package/dist/auth/keychain.d.ts +5 -0
  4. package/dist/auth/keychain.js +82 -0
  5. package/dist/auth/notices.d.ts +3 -0
  6. package/dist/auth/notices.js +42 -0
  7. package/dist/autofix/index.d.ts +0 -4
  8. package/dist/autofix/index.js +10 -24
  9. package/dist/capture/browser.d.ts +11 -0
  10. package/dist/capture/browser.js +173 -0
  11. package/dist/capture/diff.d.ts +23 -0
  12. package/dist/capture/diff.js +52 -0
  13. package/dist/capture/index.d.ts +23 -0
  14. package/dist/capture/index.js +210 -0
  15. package/dist/capture/naming.d.ts +17 -0
  16. package/dist/capture/naming.js +45 -0
  17. package/dist/capture/parser.d.ts +15 -0
  18. package/dist/capture/parser.js +80 -0
  19. package/dist/capture/types.d.ts +57 -0
  20. package/dist/capture/types.js +1 -0
  21. package/dist/cli.js +20 -3
  22. package/dist/commands/autofix.js +136 -120
  23. package/dist/commands/cron.js +58 -47
  24. package/dist/commands/deploy.js +123 -102
  25. package/dist/commands/generate.js +125 -7
  26. package/dist/commands/heal.d.ts +10 -0
  27. package/dist/commands/heal.js +201 -0
  28. package/dist/commands/i18n.js +146 -111
  29. package/dist/commands/import.d.ts +2 -0
  30. package/dist/commands/import.js +157 -0
  31. package/dist/commands/init.js +19 -7
  32. package/dist/commands/lint.js +50 -44
  33. package/dist/commands/llms-txt.js +59 -49
  34. package/dist/commands/login.js +63 -34
  35. package/dist/commands/mcp.js +6 -0
  36. package/dist/commands/monitor.js +13 -8
  37. package/dist/commands/qa.d.ts +2 -0
  38. package/dist/commands/qa.js +43 -0
  39. package/dist/commands/review-pr.js +108 -92
  40. package/dist/commands/sdk.js +128 -122
  41. package/dist/commands/security.d.ts +2 -0
  42. package/dist/commands/security.js +109 -0
  43. package/dist/commands/test.js +91 -92
  44. package/dist/commands/version.js +104 -75
  45. package/dist/commands/watch.js +130 -114
  46. package/dist/config/types.js +2 -2
  47. package/dist/context-hub/index.d.ts +23 -0
  48. package/dist/context-hub/index.js +179 -0
  49. package/dist/context-hub/mappings.d.ts +8 -0
  50. package/dist/context-hub/mappings.js +55 -0
  51. package/dist/context-hub/types.d.ts +33 -0
  52. package/dist/context-hub/types.js +1 -0
  53. package/dist/generator/generator.js +39 -6
  54. package/dist/generator/types.d.ts +7 -0
  55. package/dist/generator/writer.d.ts +3 -1
  56. package/dist/generator/writer.js +36 -7
  57. package/dist/importers/confluence.d.ts +5 -0
  58. package/dist/importers/confluence.js +137 -0
  59. package/dist/importers/detect.d.ts +20 -0
  60. package/dist/importers/detect.js +121 -0
  61. package/dist/importers/docusaurus.d.ts +5 -0
  62. package/dist/importers/docusaurus.js +279 -0
  63. package/dist/importers/gitbook.d.ts +5 -0
  64. package/dist/importers/gitbook.js +189 -0
  65. package/dist/importers/github.d.ts +8 -0
  66. package/dist/importers/github.js +99 -0
  67. package/dist/importers/index.d.ts +15 -0
  68. package/dist/importers/index.js +30 -0
  69. package/dist/importers/markdown.d.ts +6 -0
  70. package/dist/importers/markdown.js +105 -0
  71. package/dist/importers/mintlify.d.ts +5 -0
  72. package/dist/importers/mintlify.js +172 -0
  73. package/dist/importers/notion.d.ts +5 -0
  74. package/dist/importers/notion.js +174 -0
  75. package/dist/importers/readme.d.ts +5 -0
  76. package/dist/importers/readme.js +184 -0
  77. package/dist/importers/transform.d.ts +90 -0
  78. package/dist/importers/transform.js +457 -0
  79. package/dist/importers/types.d.ts +37 -0
  80. package/dist/importers/types.js +1 -0
  81. package/dist/llm/anthropic-client.d.ts +1 -0
  82. package/dist/llm/anthropic-client.js +3 -1
  83. package/dist/llm/index.d.ts +6 -4
  84. package/dist/llm/index.js +76 -261
  85. package/dist/llm/openai-client.d.ts +1 -0
  86. package/dist/llm/openai-client.js +7 -2
  87. package/dist/plugins/index.js +7 -0
  88. package/dist/qa/checks.d.ts +10 -0
  89. package/dist/qa/checks.js +492 -0
  90. package/dist/qa/fixes.d.ts +30 -0
  91. package/dist/qa/fixes.js +277 -0
  92. package/dist/qa/index.d.ts +29 -0
  93. package/dist/qa/index.js +187 -0
  94. package/dist/qa/types.d.ts +24 -0
  95. package/dist/qa/types.js +1 -0
  96. package/dist/scanner/csharp.d.ts +23 -0
  97. package/dist/scanner/csharp.js +421 -0
  98. package/dist/scanner/index.js +53 -26
  99. package/dist/scanner/java.d.ts +39 -0
  100. package/dist/scanner/java.js +318 -0
  101. package/dist/scanner/kotlin.d.ts +23 -0
  102. package/dist/scanner/kotlin.js +389 -0
  103. package/dist/scanner/php.d.ts +57 -0
  104. package/dist/scanner/php.js +351 -0
  105. package/dist/scanner/python.js +17 -0
  106. package/dist/scanner/ruby.d.ts +36 -0
  107. package/dist/scanner/ruby.js +431 -0
  108. package/dist/scanner/swift.d.ts +25 -0
  109. package/dist/scanner/swift.js +392 -0
  110. package/dist/scanner/types.d.ts +1 -1
  111. package/dist/template/content/docs/_navigation.json +46 -0
  112. package/dist/template/content/docs/_sidebars.json +684 -0
  113. package/dist/template/content/docs/core.md +4544 -0
  114. package/dist/template/content/docs/index.mdx +89 -0
  115. package/dist/template/content/docs/integrations.md +1158 -0
  116. package/dist/template/content/docs/llms-full.md +403 -0
  117. package/dist/template/content/docs/llms.txt +4588 -0
  118. package/dist/template/content/docs/other.md +10379 -0
  119. package/dist/template/content/docs/tools.md +746 -0
  120. package/dist/template/content/docs/types.md +531 -0
  121. package/dist/template/docs.json +13 -11
  122. package/dist/template/mdx-components.tsx +27 -2
  123. package/dist/template/package.json +6 -0
  124. package/dist/template/public/search-index.json +1 -1
  125. package/dist/template/scripts/build-search-index.mjs +149 -13
  126. package/dist/template/src/app/api/chat/route.ts +83 -128
  127. package/dist/template/src/app/docs/[...slug]/page.tsx +75 -20
  128. package/dist/template/src/app/docs/llms-full.md +151 -4
  129. package/dist/template/src/app/docs/llms.txt +2464 -847
  130. package/dist/template/src/app/docs/page.mdx +48 -38
  131. package/dist/template/src/app/layout.tsx +3 -1
  132. package/dist/template/src/app/page.tsx +22 -8
  133. package/dist/template/src/components/ai-chat.tsx +73 -64
  134. package/dist/template/src/components/breadcrumbs.tsx +21 -23
  135. package/dist/template/src/components/copy-button.tsx +13 -9
  136. package/dist/template/src/components/copy-page-button.tsx +54 -0
  137. package/dist/template/src/components/docs-layout.tsx +37 -25
  138. package/dist/template/src/components/header.tsx +51 -10
  139. package/dist/template/src/components/mdx/card.tsx +17 -3
  140. package/dist/template/src/components/mdx/code-block.tsx +13 -9
  141. package/dist/template/src/components/mdx/code-group.tsx +13 -8
  142. package/dist/template/src/components/mdx/heading.tsx +15 -2
  143. package/dist/template/src/components/mdx/highlighted-code.tsx +13 -8
  144. package/dist/template/src/components/mdx/index.tsx +2 -0
  145. package/dist/template/src/components/mdx/mermaid.tsx +110 -0
  146. package/dist/template/src/components/mdx/screenshot.tsx +150 -0
  147. package/dist/template/src/components/scroll-to-hash.tsx +48 -0
  148. package/dist/template/src/components/sidebar.tsx +12 -18
  149. package/dist/template/src/components/table-of-contents.tsx +9 -0
  150. package/dist/template/src/lib/highlight.ts +3 -88
  151. package/dist/template/src/lib/navigation.ts +159 -0
  152. package/dist/template/src/lib/search-types.ts +4 -1
  153. package/dist/template/src/lib/search.ts +30 -7
  154. package/dist/template/src/styles/globals.css +17 -6
  155. package/dist/utils/files.d.ts +9 -1
  156. package/dist/utils/files.js +59 -10
  157. package/dist/utils/validation.d.ts +0 -3
  158. package/dist/utils/validation.js +0 -26
  159. package/package.json +5 -1
@@ -0,0 +1,48 @@
1
+ 'use client'
2
+
3
+ import { useEffect } from 'react'
4
+ import { usePathname } from 'next/navigation'
5
+
6
+ /**
7
+ * Handles scrolling to hash targets after page navigation.
8
+ * Next.js App Router can intercept hash links before content is rendered.
9
+ * This component uses a MutationObserver to wait for the target element.
10
+ */
11
+ export function ScrollToHash() {
12
+ const pathname = usePathname()
13
+
14
+ useEffect(() => {
15
+ const hash = window.location.hash
16
+ if (!hash) return
17
+
18
+ const id = hash.slice(1)
19
+
20
+ // Try immediate scroll first
21
+ const target = document.getElementById(id)
22
+ if (target) {
23
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' })
24
+ return
25
+ }
26
+
27
+ // If element not found yet (SSR/hydration timing), observe DOM
28
+ const observer = new MutationObserver(() => {
29
+ const el = document.getElementById(id)
30
+ if (el) {
31
+ observer.disconnect()
32
+ el.scrollIntoView({ behavior: 'smooth', block: 'start' })
33
+ }
34
+ })
35
+
36
+ observer.observe(document.body, { childList: true, subtree: true })
37
+
38
+ // Cleanup after 5 seconds
39
+ const timeout = setTimeout(() => observer.disconnect(), 5000)
40
+
41
+ return () => {
42
+ observer.disconnect()
43
+ clearTimeout(timeout)
44
+ }
45
+ }, [pathname])
46
+
47
+ return null
48
+ }
@@ -5,15 +5,13 @@ import { usePathname } from 'next/navigation'
5
5
  import { ChevronRight, BookOpen, Code, FileText, Settings, Key, Zap, Shield, Globe, Terminal, Database, Cloud, Lock, Rocket, Search, Star, Heart, Package, Puzzle, GitBranch, Cpu } from 'lucide-react'
6
6
  import { useState, useMemo } from 'react'
7
7
  import { cn } from '@/lib/utils'
8
-
9
- interface NavPage { title: string; path: string; pages?: NavPage[] }
10
- interface NavGroup { group: string; icon?: string; pages: (NavPage | NavGroup)[] }
11
- interface DocsConfig { navigation: NavGroup[] }
8
+ import type { NavPage, NavGroup } from '@/lib/navigation'
9
+ import { isNavGroup } from '@/lib/navigation'
12
10
 
13
11
  interface SidebarProps {
14
12
  open?: boolean
15
13
  onClose?: () => void
16
- docsConfig: DocsConfig
14
+ groups: NavGroup[]
17
15
  }
18
16
 
19
17
  const iconMap: Record<string, React.ComponentType<{ size?: number; className?: string }>> = {
@@ -22,10 +20,6 @@ const iconMap: Record<string, React.ComponentType<{ size?: number; className?: s
22
20
  Package, Puzzle, GitBranch, Cpu,
23
21
  }
24
22
 
25
- function isNavGroup(item: NavPage | NavGroup): item is NavGroup {
26
- return 'group' in item && !('path' in item)
27
- }
28
-
29
23
  /** Check if any descendant page matches the current pathname */
30
24
  function hasActivePage(pages: (NavPage | NavGroup)[], pathname: string): boolean {
31
25
  for (const item of pages) {
@@ -39,7 +33,7 @@ function hasActivePage(pages: (NavPage | NavGroup)[], pathname: string): boolean
39
33
  return false
40
34
  }
41
35
 
42
- export function Sidebar({ open, onClose, docsConfig }: SidebarProps) {
36
+ export function Sidebar({ open, onClose, groups }: SidebarProps) {
43
37
  const pathname = usePathname()
44
38
 
45
39
  return (
@@ -61,8 +55,8 @@ export function Sidebar({ open, onClose, docsConfig }: SidebarProps) {
61
55
  open ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
62
56
  )}
63
57
  >
64
- <nav className="py-4 pr-2 space-y-6 divide-y divide-[var(--color-border)]">
65
- {docsConfig.navigation.map((group) => (
58
+ <nav className="py-4 pr-2 space-y-5">
59
+ {groups.map((group) => (
66
60
  <SidebarGroup key={group.group} group={group} pathname={pathname} onNavigate={onClose} level={0} />
67
61
  ))}
68
62
  </nav>
@@ -80,7 +74,7 @@ function SidebarGroup({ group, pathname, onNavigate, level }: { group: NavGroup;
80
74
  // Limit nesting to 3 levels
81
75
  if (level > 2) return null
82
76
 
83
- const paddingLeft = level === 0 ? 'pl-4' : level === 1 ? 'pl-10' : 'pl-14'
77
+ const paddingLeft = level === 0 ? 'pl-4' : level === 1 ? 'pl-8' : 'pl-12'
84
78
 
85
79
  return (
86
80
  <div>
@@ -88,15 +82,15 @@ function SidebarGroup({ group, pathname, onNavigate, level }: { group: NavGroup;
88
82
  onClick={() => setExpanded(!expanded)}
89
83
  aria-expanded={expanded}
90
84
  className={cn(
91
- 'flex items-center gap-2.5 w-full mb-2 text-[0.8125rem] font-medium text-[var(--color-text)] hover:text-[var(--color-text)] transition-colors',
85
+ 'flex items-center gap-2 w-full mb-1 py-1 text-[0.6875rem] font-semibold uppercase tracking-wider text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] transition-colors',
92
86
  paddingLeft
93
87
  )}
94
88
  >
95
89
  <ChevronRight
96
- size={14}
97
- className={cn('text-[var(--color-text-tertiary)] transition-transform duration-100', expanded && 'rotate-90')}
90
+ size={10}
91
+ className={cn('shrink-0 transition-transform duration-100', expanded && 'rotate-90')}
98
92
  />
99
- {Icon && <Icon size={14} className="text-[var(--color-text-tertiary)]" />}
93
+ {Icon && <Icon size={12} className="shrink-0" />}
100
94
  {group.group}
101
95
  </button>
102
96
  {expanded && (
@@ -141,7 +135,7 @@ function SidebarPageItem({ page, pathname, onNavigate, level }: { page: NavPage;
141
135
  }, [page, pathname])
142
136
  const [expanded, setExpanded] = useState(isActive)
143
137
 
144
- const paddingClass = level === 0 ? 'pl-10' : level === 1 ? 'pl-14' : 'pl-18'
138
+ const paddingClass = level === 0 ? 'pl-7' : level === 1 ? 'pl-11' : 'pl-14'
145
139
 
146
140
  return (
147
141
  <div>
@@ -21,6 +21,7 @@ export function TableOfContents() {
21
21
 
22
22
  const elements = article.querySelectorAll('h2, h3')
23
23
  const items: Heading[] = []
24
+ const seenIds = new Set<string>()
24
25
 
25
26
  // Generic headings that repeat in API reference pages — skip them in TOC
26
27
  const genericHeadings = new Set([
@@ -28,6 +29,7 @@ export function TableOfContents() {
28
29
  'returned validator function', 'requirements',
29
30
  'when results are returned', 'when each value is returned',
30
31
  'validationresult shape',
32
+ 'example', 'related', 'notes', 'properties',
31
33
  ])
32
34
 
33
35
  elements.forEach((el) => {
@@ -37,9 +39,16 @@ export function TableOfContents() {
37
39
  // Skip generic/repeated headings
38
40
  if (genericHeadings.has(normalized)) return
39
41
 
42
+ // Skip headings inside Card components (Cards use h3 for titles, not doc headings)
43
+ if (el.closest('.card-link') || el.closest('[data-card]')) return
44
+
40
45
  const id = el.id || normalized.replace(/\s+/g, '-')
41
46
  if (!el.id) el.id = id
42
47
 
48
+ // Deduplicate — skip if we've already seen this ID
49
+ if (seenIds.has(id)) return
50
+ seenIds.add(id)
51
+
43
52
  items.push({
44
53
  id,
45
54
  text,
@@ -1,7 +1,4 @@
1
- import { createHighlighter, type Highlighter, type BundledTheme } from 'shiki'
2
-
3
- // Cache highlighter instances per loaded theme set
4
- const highlighterCache = new Map<string, Highlighter>()
1
+ import { codeToHtml, type BundledTheme } from 'shiki'
5
2
 
6
3
  export type ThemeName =
7
4
  | 'catppuccin-mocha'
@@ -28,98 +25,16 @@ export const AVAILABLE_THEMES: { name: ThemeName; label: string; isDark: boolean
28
25
 
29
26
  export const DEFAULT_THEME: ThemeName = 'catppuccin-mocha'
30
27
 
31
- const SUPPORTED_LANGS = [
32
- 'typescript',
33
- 'javascript',
34
- 'python',
35
- 'go',
36
- 'rust',
37
- 'json',
38
- 'yaml',
39
- 'bash',
40
- 'shell',
41
- 'markdown',
42
- 'html',
43
- 'css',
44
- 'jsx',
45
- 'tsx',
46
- 'sql',
47
- 'graphql',
48
- 'diff',
49
- ]
50
-
51
- // Start with just the default theme; load others on demand
52
- let defaultHighlighter: Highlighter | null = null
53
- let defaultHighlighterPromise: Promise<Highlighter> | null = null
54
-
55
- async function getDefaultHighlighter(): Promise<Highlighter> {
56
- if (defaultHighlighter) return defaultHighlighter
57
- if (defaultHighlighterPromise) return defaultHighlighterPromise
58
-
59
- defaultHighlighterPromise = createHighlighter({
60
- themes: [DEFAULT_THEME] as BundledTheme[],
61
- langs: SUPPORTED_LANGS,
62
- }).then(hl => {
63
- defaultHighlighter = hl
64
- highlighterCache.set(DEFAULT_THEME, hl)
65
- return hl
66
- })
67
-
68
- return defaultHighlighterPromise
69
- }
70
-
71
- async function getHighlighterForTheme(theme: ThemeName): Promise<Highlighter> {
72
- if (theme === DEFAULT_THEME || highlighterCache.has(DEFAULT_THEME)) {
73
- // Try to load the theme into the existing default highlighter
74
- const hl = await getDefaultHighlighter()
75
- try {
76
- // Check if the theme is already loaded by attempting to use it
77
- const loadedThemes = hl.getLoadedThemes()
78
- if (!loadedThemes.includes(theme as BundledTheme)) {
79
- await hl.loadTheme(theme as BundledTheme)
80
- }
81
- return hl
82
- } catch {
83
- // If loading into existing highlighter fails, create a new one
84
- }
85
- }
86
-
87
- // Check cache
88
- const cached = highlighterCache.get(theme)
89
- if (cached) return cached
90
-
91
- // Create a new highlighter with this theme
92
- const hl = await createHighlighter({
93
- themes: [theme] as BundledTheme[],
94
- langs: SUPPORTED_LANGS,
95
- })
96
- highlighterCache.set(theme, hl)
97
- return hl
98
- }
99
-
100
- export async function getHighlighter(): Promise<Highlighter> {
101
- return getDefaultHighlighter()
102
- }
103
-
104
28
  export async function highlight(
105
29
  code: string,
106
30
  language: string,
107
31
  theme: ThemeName = DEFAULT_THEME
108
32
  ): Promise<string> {
109
- const hl = await getHighlighterForTheme(theme)
110
-
111
33
  const lang = normalizeLanguage(language)
112
-
113
34
  try {
114
- return hl.codeToHtml(code, {
115
- lang,
116
- theme: theme as BundledTheme,
117
- })
35
+ return await codeToHtml(code, { lang, theme: theme as BundledTheme })
118
36
  } catch {
119
- return hl.codeToHtml(code, {
120
- lang: 'text',
121
- theme: theme as BundledTheme,
122
- })
37
+ return await codeToHtml(code, { lang: 'text', theme: theme as BundledTheme })
123
38
  }
124
39
  }
125
40
 
@@ -0,0 +1,159 @@
1
+ export interface NavPage {
2
+ title: string
3
+ path: string
4
+ description?: string
5
+ pages?: NavPage[]
6
+ }
7
+
8
+ export interface NavGroup {
9
+ group: string
10
+ icon?: string
11
+ pages: (NavPage | NavGroup)[]
12
+ }
13
+
14
+ export interface NavTab {
15
+ tab: string
16
+ icon?: string
17
+ groups: NavGroup[]
18
+ href?: string
19
+ }
20
+
21
+ export type Navigation = NavGroup[] | NavTab[]
22
+
23
+ export function isNavGroup(item: NavPage | NavGroup): item is NavGroup {
24
+ return 'group' in item && !('path' in item)
25
+ }
26
+
27
+ /** Check if navigation uses tabs format */
28
+ export function hasTabs(navigation: Navigation): navigation is NavTab[] {
29
+ return navigation.length > 0 && 'tab' in navigation[0]
30
+ }
31
+
32
+ /** Recursively check if a page path exists within a list of groups */
33
+ function groupsContainPath(groups: NavGroup[], pathname: string): boolean {
34
+ for (const group of groups) {
35
+ for (const item of group.pages) {
36
+ if (isNavGroup(item)) {
37
+ if (groupsContainPath([item], pathname)) return true
38
+ } else {
39
+ if (item.path === pathname) return true
40
+ if (item.pages && pagesContainPath(item.pages, pathname)) return true
41
+ }
42
+ }
43
+ }
44
+ return false
45
+ }
46
+
47
+ function pagesContainPath(pages: NavPage[], pathname: string): boolean {
48
+ for (const page of pages) {
49
+ if (page.path === pathname) return true
50
+ if (page.pages && pagesContainPath(page.pages, pathname)) return true
51
+ }
52
+ return false
53
+ }
54
+
55
+ /** Find the active tab based on current pathname */
56
+ export function getActiveTab(pathname: string, tabs: NavTab[]): NavTab | null {
57
+ for (const tab of tabs) {
58
+ if (tab.href && tab.href === pathname) return tab
59
+ if (tab.groups && groupsContainPath(tab.groups, pathname)) return tab
60
+ }
61
+ // Default to first tab if no match
62
+ return tabs.length > 0 ? tabs[0] : null
63
+ }
64
+
65
+ /** Get sidebar groups for current pathname (works for both flat and tabbed nav) */
66
+ export function getTabGroups(navigation: Navigation, pathname: string): NavGroup[] {
67
+ if (!hasTabs(navigation)) return navigation
68
+ const active = getActiveTab(pathname, navigation)
69
+ return active?.groups ?? []
70
+ }
71
+
72
+ /** Get first page path for a tab (for linking) */
73
+ export function getTabHref(tab: NavTab): string {
74
+ if (tab.href) return tab.href
75
+ for (const group of tab.groups) {
76
+ for (const item of group.pages) {
77
+ if (isNavGroup(item)) {
78
+ for (const subItem of item.pages) {
79
+ if (!isNavGroup(subItem) && subItem.path) return subItem.path
80
+ }
81
+ } else {
82
+ return item.path
83
+ }
84
+ }
85
+ }
86
+ return '/'
87
+ }
88
+
89
+ /** Flatten all pages across all tabs/groups for prev/next navigation */
90
+ export function getAllPagesFlat(navigation: Navigation): Array<{ title: string; path: string }> {
91
+ const pages: Array<{ title: string; path: string }> = []
92
+
93
+ function collectFromPages(items: (NavPage | NavGroup)[]) {
94
+ for (const item of items) {
95
+ if (isNavGroup(item)) {
96
+ collectFromPages(item.pages)
97
+ } else {
98
+ pages.push({ title: item.title, path: item.path })
99
+ if (item.pages) collectFromPages(item.pages)
100
+ }
101
+ }
102
+ }
103
+
104
+ if (hasTabs(navigation)) {
105
+ for (const tab of navigation) {
106
+ if (tab.groups) {
107
+ for (const group of tab.groups) {
108
+ collectFromPages(group.pages)
109
+ }
110
+ }
111
+ }
112
+ } else {
113
+ for (const group of navigation) {
114
+ collectFromPages(group.pages)
115
+ }
116
+ }
117
+
118
+ return pages
119
+ }
120
+
121
+ /** Look up page info from navigation (works with both formats) */
122
+ export function findPageInNavigation(
123
+ navigation: Navigation,
124
+ pathname: string
125
+ ): { title?: string; description?: string } {
126
+ function searchPages(items: (NavPage | NavGroup)[]): { title?: string; description?: string } | null {
127
+ for (const item of items) {
128
+ if (isNavGroup(item)) {
129
+ const found = searchPages(item.pages)
130
+ if (found) return found
131
+ } else {
132
+ if (item.path === pathname) return { title: item.title, description: item.description }
133
+ if (item.pages) {
134
+ const found = searchPages(item.pages)
135
+ if (found) return found
136
+ }
137
+ }
138
+ }
139
+ return null
140
+ }
141
+
142
+ if (hasTabs(navigation)) {
143
+ for (const tab of navigation) {
144
+ if (tab.groups) {
145
+ for (const group of tab.groups) {
146
+ const found = searchPages(group.pages)
147
+ if (found) return found
148
+ }
149
+ }
150
+ }
151
+ } else {
152
+ for (const group of navigation) {
153
+ const found = searchPages(group.pages)
154
+ if (found) return found
155
+ }
156
+ }
157
+
158
+ return {}
159
+ }
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * Type definitions for Orama search database
3
- * Fixes P0: Removes `any` types from search functionality
4
3
  */
5
4
 
6
5
  import type { Orama, Results, SearchParams } from '@orama/orama'
@@ -9,6 +8,8 @@ import type { Orama, Results, SearchParams } from '@orama/orama'
9
8
  export interface SearchDocument {
10
9
  id: string
11
10
  title: string
11
+ headings: string
12
+ keywords: string
12
13
  content: string
13
14
  href: string
14
15
  section: string
@@ -18,6 +19,8 @@ export interface SearchDocument {
18
19
  export type SearchDatabase = Orama<{
19
20
  id: 'string'
20
21
  title: 'string'
22
+ headings: 'string'
23
+ keywords: 'string'
21
24
  content: 'string'
22
25
  href: 'string'
23
26
  section: 'string'
@@ -7,6 +7,8 @@ let loadPromise: Promise<void> | null = null
7
7
  const schema = {
8
8
  id: 'string' as const,
9
9
  title: 'string' as const,
10
+ headings: 'string' as const,
11
+ keywords: 'string' as const,
10
12
  content: 'string' as const,
11
13
  href: 'string' as const,
12
14
  section: 'string' as const,
@@ -41,7 +43,15 @@ async function loadSearchIndex(): Promise<void> {
41
43
  return
42
44
  }
43
45
 
44
- const newDb = await create({ schema }) as SearchDatabase
46
+ const newDb = await create({
47
+ schema,
48
+ components: {
49
+ tokenizer: {
50
+ stemming: true,
51
+ language: 'english',
52
+ },
53
+ },
54
+ }) as SearchDatabase
45
55
  await load(newDb, data as RawData)
46
56
  db = newDb
47
57
  } catch (err) {
@@ -55,10 +65,21 @@ async function loadSearchIndex(): Promise<void> {
55
65
  }
56
66
 
57
67
  /**
58
- * Generate a snippet with highlighted search terms
68
+ * Generate a snippet with highlighted search terms, preferring heading matches
59
69
  */
60
- function generateSnippet(content: string, query: string, maxLength: number = 150): string {
70
+ function generateSnippet(content: string, headings: string, query: string, maxLength: number = 150): string {
61
71
  const terms = query.toLowerCase().split(/\s+/).filter(Boolean)
72
+
73
+ // Check if any headings match — show that context first
74
+ if (headings) {
75
+ const headingList = headings.split(' | ')
76
+ for (const heading of headingList) {
77
+ if (terms.some(term => heading.toLowerCase().includes(term))) {
78
+ return heading
79
+ }
80
+ }
81
+ }
82
+
62
83
  const contentLower = content.toLowerCase()
63
84
 
64
85
  // Find the best position to start the snippet (where most terms match)
@@ -110,11 +131,13 @@ export async function search(query: string): Promise<SearchResultWithHighlight[]
110
131
  try {
111
132
  const results = await oramaSearch(db, {
112
133
  term: query,
113
- properties: ['title', 'content'],
134
+ properties: ['title', 'headings', 'keywords', 'content'],
114
135
  limit: 10,
115
- tolerance: 1,
136
+ tolerance: 2,
116
137
  boost: {
117
- title: 2,
138
+ title: 5,
139
+ headings: 3,
140
+ keywords: 2,
118
141
  content: 1,
119
142
  },
120
143
  })
@@ -124,7 +147,7 @@ export async function search(query: string): Promise<SearchResultWithHighlight[]
124
147
  href: hit.document.href,
125
148
  content: hit.document.content,
126
149
  section: hit.document.section || undefined,
127
- snippet: generateSnippet(hit.document.content, query),
150
+ snippet: generateSnippet(hit.document.content, hit.document.headings, query),
128
151
  score: hit.score,
129
152
  }))
130
153
  } catch (err) {
@@ -80,14 +80,14 @@
80
80
  --color-bg: #0f1117;
81
81
  --color-bg-secondary: #1a1d27;
82
82
  --color-bg-tertiary: #252833;
83
- --color-text: #f3f4f6;
84
- --color-text-secondary: #d1d5db;
83
+ --color-text: #e0e2e6;
84
+ --color-text-secondary: #b0b4bc;
85
85
  --color-text-tertiary: #6b7280;
86
86
  --color-border: rgba(255, 255, 255, 0.07);
87
87
  --color-border-strong: rgba(255, 255, 255, 0.12);
88
88
  --color-code-bg: #0b0c0e;
89
89
  --color-code-border: rgba(255, 255, 255, 0.1);
90
- --color-code-text: #f3f4f6;
90
+ --color-code-text: #e0e2e6;
91
91
  }
92
92
  }
93
93
 
@@ -95,14 +95,14 @@
95
95
  --color-bg: #0f1117;
96
96
  --color-bg-secondary: #1a1d27;
97
97
  --color-bg-tertiary: #252833;
98
- --color-text: #f3f4f6;
99
- --color-text-secondary: #d1d5db;
98
+ --color-text: #e0e2e6;
99
+ --color-text-secondary: #b0b4bc;
100
100
  --color-text-tertiary: #6b7280;
101
101
  --color-border: rgba(255, 255, 255, 0.07);
102
102
  --color-border-strong: rgba(255, 255, 255, 0.12);
103
103
  --color-code-bg: #0b0c0e;
104
104
  --color-code-border: rgba(255, 255, 255, 0.1);
105
- --color-code-text: #f3f4f6;
105
+ --color-code-text: #e0e2e6;
106
106
  }
107
107
 
108
108
  /* Error page icon — dark mode overrides (avoids Tailwind dark: variant mismatch) */
@@ -257,6 +257,17 @@ code {
257
257
  border-bottom-width: 2px;
258
258
  }
259
259
 
260
+ /* Cards and card groups should not inherit prose link styling */
261
+ .prose .card-link,
262
+ .prose a:has(> .group) {
263
+ border-bottom: none !important;
264
+ font-weight: inherit;
265
+ }
266
+ .prose .card-link:hover,
267
+ .prose a:has(> .group):hover {
268
+ border-bottom: none !important;
269
+ }
270
+
260
271
  .prose p {
261
272
  margin-top: 1.25em;
262
273
  margin-bottom: 1.25em;
@@ -1,9 +1,17 @@
1
1
  /**
2
2
  * Recursively find all .md and .mdx files in a directory.
3
- * Skips hidden directories and node_modules.
3
+ * Skips hidden directories, node_modules, and symlinks.
4
4
  */
5
5
  export declare function findMdxFiles(dir: string): string[];
6
6
  /**
7
7
  * Convert string to URL-safe slug
8
8
  */
9
9
  export declare function slugify(str: string): string;
10
+ /**
11
+ * Simple YAML frontmatter parser — splits on --- markers.
12
+ * Returns parsed key-value data and remaining content body.
13
+ */
14
+ export declare function parseFrontmatter(content: string): {
15
+ data: Record<string, unknown>;
16
+ content: string;
17
+ };