skrypt-ai 0.4.1 → 0.5.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/dist/auth/index.d.ts +13 -3
- package/dist/auth/index.js +94 -9
- package/dist/auth/keychain.d.ts +5 -0
- package/dist/auth/keychain.js +82 -0
- package/dist/auth/notices.d.ts +3 -0
- package/dist/auth/notices.js +42 -0
- package/dist/autofix/index.js +10 -3
- package/dist/cli.js +16 -3
- package/dist/commands/generate.js +37 -1
- package/dist/commands/import.d.ts +2 -0
- package/dist/commands/import.js +157 -0
- package/dist/commands/init.js +19 -7
- package/dist/commands/login.js +15 -4
- package/dist/commands/review-pr.js +10 -0
- package/dist/commands/security.d.ts +2 -0
- package/dist/commands/security.js +103 -0
- package/dist/config/loader.js +2 -2
- package/dist/generator/writer.js +12 -3
- package/dist/importers/confluence.d.ts +5 -0
- package/dist/importers/confluence.js +137 -0
- package/dist/importers/detect.d.ts +20 -0
- package/dist/importers/detect.js +121 -0
- package/dist/importers/docusaurus.d.ts +5 -0
- package/dist/importers/docusaurus.js +279 -0
- package/dist/importers/gitbook.d.ts +5 -0
- package/dist/importers/gitbook.js +189 -0
- package/dist/importers/github.d.ts +8 -0
- package/dist/importers/github.js +99 -0
- package/dist/importers/index.d.ts +15 -0
- package/dist/importers/index.js +30 -0
- package/dist/importers/markdown.d.ts +6 -0
- package/dist/importers/markdown.js +105 -0
- package/dist/importers/mintlify.d.ts +5 -0
- package/dist/importers/mintlify.js +172 -0
- package/dist/importers/notion.d.ts +5 -0
- package/dist/importers/notion.js +174 -0
- package/dist/importers/readme.d.ts +5 -0
- package/dist/importers/readme.js +184 -0
- package/dist/importers/transform.d.ts +90 -0
- package/dist/importers/transform.js +457 -0
- package/dist/importers/types.d.ts +37 -0
- package/dist/importers/types.js +1 -0
- package/dist/plugins/index.js +7 -0
- package/dist/scanner/index.js +37 -24
- package/dist/scanner/python.js +17 -0
- package/dist/template/public/search-index.json +1 -1
- package/dist/template/scripts/build-search-index.mjs +67 -9
- package/dist/template/src/components/mdx/dark-image.tsx +56 -0
- package/dist/template/src/components/mdx/frame.tsx +64 -0
- package/dist/template/src/components/mdx/highlighted-code.tsx +145 -31
- package/dist/template/src/components/mdx/index.tsx +4 -0
- package/dist/template/src/components/mdx/link-preview.tsx +119 -0
- package/dist/template/src/components/mdx/tooltip.tsx +101 -0
- package/dist/template/src/components/syntax-theme-selector.tsx +167 -20
- package/dist/template/src/lib/search-types.ts +4 -1
- package/dist/template/src/lib/search.ts +30 -7
- package/dist/template/src/styles/globals.css +39 -0
- package/dist/utils/files.d.ts +9 -1
- package/dist/utils/files.js +59 -10
- package/dist/utils/validation.js +1 -1
- package/package.json +4 -1
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useCallback, type ReactNode } from 'react'
|
|
4
|
+
|
|
5
|
+
interface TooltipProps {
|
|
6
|
+
children: ReactNode
|
|
7
|
+
content: string
|
|
8
|
+
side?: 'top' | 'bottom' | 'left' | 'right'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const arrowStyles: Record<string, React.CSSProperties> = {
|
|
12
|
+
top: {
|
|
13
|
+
bottom: -4,
|
|
14
|
+
left: '50%',
|
|
15
|
+
transform: 'translateX(-50%) rotate(45deg)',
|
|
16
|
+
},
|
|
17
|
+
bottom: {
|
|
18
|
+
top: -4,
|
|
19
|
+
left: '50%',
|
|
20
|
+
transform: 'translateX(-50%) rotate(45deg)',
|
|
21
|
+
},
|
|
22
|
+
left: {
|
|
23
|
+
right: -4,
|
|
24
|
+
top: '50%',
|
|
25
|
+
transform: 'translateY(-50%) rotate(45deg)',
|
|
26
|
+
},
|
|
27
|
+
right: {
|
|
28
|
+
left: -4,
|
|
29
|
+
top: '50%',
|
|
30
|
+
transform: 'translateY(-50%) rotate(45deg)',
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const positionStyles: Record<string, React.CSSProperties> = {
|
|
35
|
+
top: {
|
|
36
|
+
bottom: '100%',
|
|
37
|
+
left: '50%',
|
|
38
|
+
transform: 'translateX(-50%)',
|
|
39
|
+
marginBottom: 8,
|
|
40
|
+
},
|
|
41
|
+
bottom: {
|
|
42
|
+
top: '100%',
|
|
43
|
+
left: '50%',
|
|
44
|
+
transform: 'translateX(-50%)',
|
|
45
|
+
marginTop: 8,
|
|
46
|
+
},
|
|
47
|
+
left: {
|
|
48
|
+
right: '100%',
|
|
49
|
+
top: '50%',
|
|
50
|
+
transform: 'translateY(-50%)',
|
|
51
|
+
marginRight: 8,
|
|
52
|
+
},
|
|
53
|
+
right: {
|
|
54
|
+
left: '100%',
|
|
55
|
+
top: '50%',
|
|
56
|
+
transform: 'translateY(-50%)',
|
|
57
|
+
marginLeft: 8,
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function Tooltip({ children, content, side = 'top' }: TooltipProps) {
|
|
62
|
+
const [visible, setVisible] = useState(false)
|
|
63
|
+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
64
|
+
|
|
65
|
+
const show = useCallback(() => {
|
|
66
|
+
timeoutRef.current = setTimeout(() => setVisible(true), 150)
|
|
67
|
+
}, [])
|
|
68
|
+
|
|
69
|
+
const hide = useCallback(() => {
|
|
70
|
+
if (timeoutRef.current) {
|
|
71
|
+
clearTimeout(timeoutRef.current)
|
|
72
|
+
timeoutRef.current = null
|
|
73
|
+
}
|
|
74
|
+
setVisible(false)
|
|
75
|
+
}, [])
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<span
|
|
79
|
+
className="relative inline-flex"
|
|
80
|
+
onMouseEnter={show}
|
|
81
|
+
onMouseLeave={hide}
|
|
82
|
+
onFocus={show}
|
|
83
|
+
onBlur={hide}
|
|
84
|
+
>
|
|
85
|
+
{children}
|
|
86
|
+
{visible && (
|
|
87
|
+
<span
|
|
88
|
+
role="tooltip"
|
|
89
|
+
className="tooltip-content absolute z-50 whitespace-nowrap bg-[var(--color-gray-900)] text-white text-xs px-2.5 py-1.5 rounded-lg shadow-lg tooltip-fade-in pointer-events-none"
|
|
90
|
+
style={positionStyles[side]}
|
|
91
|
+
>
|
|
92
|
+
{content}
|
|
93
|
+
<span
|
|
94
|
+
className="tooltip-content absolute h-2 w-2 bg-[var(--color-gray-900)]"
|
|
95
|
+
style={arrowStyles[side]}
|
|
96
|
+
/>
|
|
97
|
+
</span>
|
|
98
|
+
)}
|
|
99
|
+
</span>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
@@ -1,49 +1,196 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useContext } from 'react'
|
|
3
|
+
import { useContext, useState, useRef, useEffect, useCallback, KeyboardEvent } from 'react'
|
|
4
4
|
import { SyntaxThemeContext } from '@/contexts/syntax-theme'
|
|
5
5
|
import { AVAILABLE_THEMES, DEFAULT_THEME } from '@/lib/highlight'
|
|
6
6
|
import { Palette } from 'lucide-react'
|
|
7
7
|
|
|
8
8
|
export function SyntaxThemeSelector() {
|
|
9
9
|
const context = useContext(SyntaxThemeContext)
|
|
10
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
11
|
+
const [activeIndex, setActiveIndex] = useState(-1)
|
|
12
|
+
const triggerRef = useRef<HTMLButtonElement>(null)
|
|
13
|
+
const listRef = useRef<HTMLDivElement>(null)
|
|
14
|
+
const optionRefs = useRef<(HTMLButtonElement | null)[]>([])
|
|
15
|
+
const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
10
16
|
|
|
11
|
-
// Safely handle when context isn't available (during SSG)
|
|
12
17
|
const theme = context?.theme ?? DEFAULT_THEME
|
|
13
18
|
const setTheme = context?.setTheme ?? (() => {})
|
|
14
19
|
const availableThemes = context?.availableThemes ?? AVAILABLE_THEMES
|
|
15
20
|
|
|
21
|
+
// Find the index of the current active theme
|
|
22
|
+
const activeThemeIndex = availableThemes.findIndex((t) => t.name === theme)
|
|
23
|
+
|
|
24
|
+
const open = useCallback(() => {
|
|
25
|
+
if (closeTimeoutRef.current) {
|
|
26
|
+
clearTimeout(closeTimeoutRef.current)
|
|
27
|
+
closeTimeoutRef.current = null
|
|
28
|
+
}
|
|
29
|
+
setIsOpen(true)
|
|
30
|
+
setActiveIndex(activeThemeIndex >= 0 ? activeThemeIndex : 0)
|
|
31
|
+
}, [activeThemeIndex])
|
|
32
|
+
|
|
33
|
+
const close = useCallback(() => {
|
|
34
|
+
setIsOpen(false)
|
|
35
|
+
setActiveIndex(-1)
|
|
36
|
+
}, [])
|
|
37
|
+
|
|
38
|
+
const delayedClose = useCallback(() => {
|
|
39
|
+
closeTimeoutRef.current = setTimeout(close, 150)
|
|
40
|
+
}, [close])
|
|
41
|
+
|
|
42
|
+
const cancelClose = useCallback(() => {
|
|
43
|
+
if (closeTimeoutRef.current) {
|
|
44
|
+
clearTimeout(closeTimeoutRef.current)
|
|
45
|
+
closeTimeoutRef.current = null
|
|
46
|
+
}
|
|
47
|
+
}, [])
|
|
48
|
+
|
|
49
|
+
// Focus the active option when activeIndex changes while open
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (isOpen && activeIndex >= 0) {
|
|
52
|
+
optionRefs.current[activeIndex]?.focus()
|
|
53
|
+
}
|
|
54
|
+
}, [isOpen, activeIndex])
|
|
55
|
+
|
|
56
|
+
// Close on outside click
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!isOpen) return
|
|
59
|
+
function handleClick(e: MouseEvent) {
|
|
60
|
+
const target = e.target as Node
|
|
61
|
+
if (
|
|
62
|
+
!triggerRef.current?.contains(target) &&
|
|
63
|
+
!listRef.current?.contains(target)
|
|
64
|
+
) {
|
|
65
|
+
close()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
document.addEventListener('mousedown', handleClick)
|
|
69
|
+
return () => document.removeEventListener('mousedown', handleClick)
|
|
70
|
+
}, [isOpen, close])
|
|
71
|
+
|
|
72
|
+
// Close on Escape
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!isOpen) return
|
|
75
|
+
function handleEsc(e: globalThis.KeyboardEvent) {
|
|
76
|
+
if (e.key === 'Escape') {
|
|
77
|
+
close()
|
|
78
|
+
triggerRef.current?.focus()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
document.addEventListener('keydown', handleEsc)
|
|
82
|
+
return () => document.removeEventListener('keydown', handleEsc)
|
|
83
|
+
}, [isOpen, close])
|
|
84
|
+
|
|
85
|
+
function selectTheme(index: number) {
|
|
86
|
+
const t = availableThemes[index]
|
|
87
|
+
if (t) {
|
|
88
|
+
setTheme(t.name)
|
|
89
|
+
close()
|
|
90
|
+
triggerRef.current?.focus()
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function handleTriggerKeyDown(e: KeyboardEvent) {
|
|
95
|
+
if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
|
|
96
|
+
e.preventDefault()
|
|
97
|
+
open()
|
|
98
|
+
} else if (e.key === 'ArrowUp') {
|
|
99
|
+
e.preventDefault()
|
|
100
|
+
open()
|
|
101
|
+
setActiveIndex(availableThemes.length - 1)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function handleOptionKeyDown(e: KeyboardEvent, index: number) {
|
|
106
|
+
switch (e.key) {
|
|
107
|
+
case 'ArrowDown':
|
|
108
|
+
e.preventDefault()
|
|
109
|
+
setActiveIndex((index + 1) % availableThemes.length)
|
|
110
|
+
break
|
|
111
|
+
case 'ArrowUp':
|
|
112
|
+
e.preventDefault()
|
|
113
|
+
setActiveIndex((index - 1 + availableThemes.length) % availableThemes.length)
|
|
114
|
+
break
|
|
115
|
+
case 'Home':
|
|
116
|
+
e.preventDefault()
|
|
117
|
+
setActiveIndex(0)
|
|
118
|
+
break
|
|
119
|
+
case 'End':
|
|
120
|
+
e.preventDefault()
|
|
121
|
+
setActiveIndex(availableThemes.length - 1)
|
|
122
|
+
break
|
|
123
|
+
case 'Enter':
|
|
124
|
+
case ' ':
|
|
125
|
+
e.preventDefault()
|
|
126
|
+
selectTheme(index)
|
|
127
|
+
break
|
|
128
|
+
case 'Tab':
|
|
129
|
+
close()
|
|
130
|
+
break
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const listboxId = 'syntax-theme-listbox'
|
|
135
|
+
|
|
16
136
|
return (
|
|
17
|
-
<div
|
|
137
|
+
<div
|
|
138
|
+
className="relative"
|
|
139
|
+
onMouseEnter={() => { open(); cancelClose() }}
|
|
140
|
+
onMouseLeave={delayedClose}
|
|
141
|
+
>
|
|
18
142
|
<button
|
|
143
|
+
ref={triggerRef}
|
|
19
144
|
className="flex items-center gap-2 px-3 py-1.5 text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text)] rounded-md hover:bg-[var(--color-bg-secondary)] transition-colors"
|
|
20
145
|
title="Syntax theme"
|
|
21
146
|
aria-label={`Syntax theme: ${theme}`}
|
|
22
147
|
aria-haspopup="listbox"
|
|
148
|
+
aria-expanded={isOpen}
|
|
149
|
+
aria-controls={isOpen ? listboxId : undefined}
|
|
150
|
+
onClick={() => { isOpen ? close() : open() }}
|
|
151
|
+
onKeyDown={handleTriggerKeyDown}
|
|
23
152
|
>
|
|
24
153
|
<Palette size={16} />
|
|
25
154
|
<span className="hidden sm:inline">Theme</span>
|
|
26
155
|
</button>
|
|
27
156
|
|
|
28
|
-
<div
|
|
29
|
-
|
|
157
|
+
<div
|
|
158
|
+
ref={listRef}
|
|
159
|
+
id={listboxId}
|
|
160
|
+
role="listbox"
|
|
161
|
+
aria-label="Syntax theme"
|
|
162
|
+
aria-activedescendant={activeIndex >= 0 ? `syntax-theme-option-${activeIndex}` : undefined}
|
|
163
|
+
className={`absolute right-0 top-full mt-1 w-48 py-1 bg-[var(--color-bg)] border border-[var(--color-border)] rounded-lg shadow-lg transition-all z-50 ${
|
|
164
|
+
isOpen ? 'opacity-100 visible' : 'opacity-0 invisible pointer-events-none'
|
|
165
|
+
}`}
|
|
166
|
+
>
|
|
167
|
+
<div className="px-3 py-1.5 text-xs font-medium text-[var(--color-text-tertiary)] uppercase tracking-wide" aria-hidden="true">
|
|
30
168
|
Syntax Theme
|
|
31
169
|
</div>
|
|
32
|
-
{availableThemes.map((t) =>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
170
|
+
{availableThemes.map((t, index) => {
|
|
171
|
+
const isSelected = theme === t.name
|
|
172
|
+
return (
|
|
173
|
+
<button
|
|
174
|
+
key={t.name}
|
|
175
|
+
ref={(el) => { optionRefs.current[index] = el }}
|
|
176
|
+
id={`syntax-theme-option-${index}`}
|
|
177
|
+
role="option"
|
|
178
|
+
aria-selected={isSelected}
|
|
179
|
+
tabIndex={-1}
|
|
180
|
+
onClick={() => selectTheme(index)}
|
|
181
|
+
onKeyDown={(e) => handleOptionKeyDown(e, index)}
|
|
182
|
+
className={`w-full px-3 py-1.5 text-left text-sm flex items-center gap-2 hover:bg-[var(--color-bg-secondary)] transition-colors ${
|
|
183
|
+
isSelected ? 'text-[var(--color-primary)]' : 'text-[var(--color-text)]'
|
|
184
|
+
} ${activeIndex === index ? 'bg-[var(--color-bg-secondary)]' : ''}`}
|
|
185
|
+
>
|
|
186
|
+
<span
|
|
187
|
+
className={`w-3 h-3 rounded-full ${t.isDark ? 'bg-gray-800' : 'bg-gray-200'}`}
|
|
188
|
+
/>
|
|
189
|
+
{t.label}
|
|
190
|
+
{isSelected && <span className="ml-auto text-xs">Active</span>}
|
|
191
|
+
</button>
|
|
192
|
+
)
|
|
193
|
+
})}
|
|
47
194
|
</div>
|
|
48
195
|
</div>
|
|
49
196
|
)
|
|
@@ -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({
|
|
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:
|
|
136
|
+
tolerance: 2,
|
|
116
137
|
boost: {
|
|
117
|
-
title:
|
|
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) {
|
|
@@ -121,6 +121,12 @@
|
|
|
121
121
|
:root:not(.light) .feedback-negative { background-color: rgba(127, 29, 29, 0.3); color: #f87171; }
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/* Tooltip — dark mode overrides */
|
|
125
|
+
:root.dark .tooltip-content { background-color: var(--color-gray-100); color: var(--color-gray-900); }
|
|
126
|
+
@media (prefers-color-scheme: dark) {
|
|
127
|
+
:root:not(.light) .tooltip-content { background-color: var(--color-gray-100); color: var(--color-gray-900); }
|
|
128
|
+
}
|
|
129
|
+
|
|
124
130
|
/* ========================
|
|
125
131
|
Base — in @layer base so Tailwind utilities can override
|
|
126
132
|
======================== */
|
|
@@ -463,3 +469,36 @@ input:focus-visible, textarea:focus-visible {
|
|
|
463
469
|
animation: shimmer 1.5s infinite;
|
|
464
470
|
border-radius: 0.25rem;
|
|
465
471
|
}
|
|
472
|
+
|
|
473
|
+
/* ========================
|
|
474
|
+
Tooltip fade-in
|
|
475
|
+
======================== */
|
|
476
|
+
@keyframes tooltip-fade-in {
|
|
477
|
+
from { opacity: 0; }
|
|
478
|
+
to { opacity: 1; }
|
|
479
|
+
}
|
|
480
|
+
.tooltip-fade-in {
|
|
481
|
+
animation: tooltip-fade-in 150ms ease-out;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/* ========================
|
|
485
|
+
Page transitions — subtle fade-in for article content
|
|
486
|
+
======================== */
|
|
487
|
+
@keyframes page-fade-in {
|
|
488
|
+
from { opacity: 0; transform: translateY(4px); }
|
|
489
|
+
to { opacity: 1; transform: translateY(0); }
|
|
490
|
+
}
|
|
491
|
+
.prose {
|
|
492
|
+
animation: page-fade-in 200ms ease-out;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/* ========================
|
|
496
|
+
Link preview card
|
|
497
|
+
======================== */
|
|
498
|
+
@keyframes link-preview-in {
|
|
499
|
+
from { opacity: 0; transform: translate(-50%, -100%) translateY(4px); }
|
|
500
|
+
to { opacity: 1; transform: translate(-50%, -100%) translateY(0); }
|
|
501
|
+
}
|
|
502
|
+
.link-preview-card {
|
|
503
|
+
animation: link-preview-in 150ms ease-out;
|
|
504
|
+
}
|
package/dist/utils/files.d.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Recursively find all .md and .mdx files in a directory.
|
|
3
|
-
* Skips hidden directories and
|
|
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
|
+
};
|
package/dist/utils/files.js
CHANGED
|
@@ -1,25 +1,41 @@
|
|
|
1
|
-
import { readdirSync, statSync } from 'fs';
|
|
1
|
+
import { readdirSync, statSync, lstatSync } from 'fs';
|
|
2
2
|
import { join, extname } from 'path';
|
|
3
3
|
/**
|
|
4
4
|
* Recursively find all .md and .mdx files in a directory.
|
|
5
|
-
* Skips hidden directories and
|
|
5
|
+
* Skips hidden directories, node_modules, and symlinks.
|
|
6
6
|
*/
|
|
7
7
|
export function findMdxFiles(dir) {
|
|
8
8
|
const files = [];
|
|
9
|
-
function walk(currentDir) {
|
|
10
|
-
|
|
9
|
+
function walk(currentDir, depth) {
|
|
10
|
+
if (depth > 30)
|
|
11
|
+
return;
|
|
12
|
+
let entries;
|
|
13
|
+
try {
|
|
14
|
+
entries = readdirSync(currentDir);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
11
19
|
for (const entry of entries) {
|
|
12
20
|
const fullPath = join(currentDir, entry);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
21
|
+
try {
|
|
22
|
+
// Skip symlinks to prevent infinite loops
|
|
23
|
+
if (lstatSync(fullPath).isSymbolicLink())
|
|
24
|
+
continue;
|
|
25
|
+
const stat = statSync(fullPath);
|
|
26
|
+
if (stat.isDirectory() && !entry.startsWith('.') && entry !== 'node_modules') {
|
|
27
|
+
walk(fullPath, depth + 1);
|
|
28
|
+
}
|
|
29
|
+
else if (stat.isFile() && (extname(entry) === '.mdx' || extname(entry) === '.md')) {
|
|
30
|
+
files.push(fullPath);
|
|
31
|
+
}
|
|
16
32
|
}
|
|
17
|
-
|
|
18
|
-
|
|
33
|
+
catch {
|
|
34
|
+
continue;
|
|
19
35
|
}
|
|
20
36
|
}
|
|
21
37
|
}
|
|
22
|
-
walk(dir);
|
|
38
|
+
walk(dir, 0);
|
|
23
39
|
return files;
|
|
24
40
|
}
|
|
25
41
|
/**
|
|
@@ -31,3 +47,36 @@ export function slugify(str) {
|
|
|
31
47
|
.replace(/[^a-z0-9]+/g, '-')
|
|
32
48
|
.replace(/^-|-$/g, '');
|
|
33
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Simple YAML frontmatter parser — splits on --- markers.
|
|
52
|
+
* Returns parsed key-value data and remaining content body.
|
|
53
|
+
*/
|
|
54
|
+
export function parseFrontmatter(content) {
|
|
55
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
56
|
+
if (!match)
|
|
57
|
+
return { data: {}, content };
|
|
58
|
+
const yamlStr = match[1];
|
|
59
|
+
const body = match[2];
|
|
60
|
+
const data = {};
|
|
61
|
+
for (const line of yamlStr.split('\n')) {
|
|
62
|
+
const kvMatch = line.match(/^(\w[\w-]*)\s*:\s*(.*)$/);
|
|
63
|
+
if (!kvMatch)
|
|
64
|
+
continue;
|
|
65
|
+
const key = kvMatch[1];
|
|
66
|
+
let value = kvMatch[2].trim();
|
|
67
|
+
if (typeof value === 'string') {
|
|
68
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
69
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
70
|
+
value = value.slice(1, -1);
|
|
71
|
+
}
|
|
72
|
+
else if (value === 'true')
|
|
73
|
+
value = true;
|
|
74
|
+
else if (value === 'false')
|
|
75
|
+
value = false;
|
|
76
|
+
else if (/^\d+$/.test(value))
|
|
77
|
+
value = parseInt(value, 10);
|
|
78
|
+
}
|
|
79
|
+
data[key] = value;
|
|
80
|
+
}
|
|
81
|
+
return { data, content: body };
|
|
82
|
+
}
|
package/dist/utils/validation.js
CHANGED
|
@@ -31,7 +31,7 @@ export function validateSlug(input) {
|
|
|
31
31
|
}
|
|
32
32
|
export function sanitizeForShell(input) {
|
|
33
33
|
// Only allow safe characters for git refs, filenames, etc.
|
|
34
|
-
if (!/^[a-zA-Z0-9
|
|
34
|
+
if (!/^[a-zA-Z0-9/_~.^@{}-]+$/.test(input)) {
|
|
35
35
|
throw new Error(`Unsafe characters in input: ${input}`);
|
|
36
36
|
}
|
|
37
37
|
return input;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skrypt-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "AI-powered documentation generator with code examples",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cli.js",
|
|
@@ -54,6 +54,9 @@
|
|
|
54
54
|
"openai": "^6.27.0",
|
|
55
55
|
"typescript": "^5.9.3"
|
|
56
56
|
},
|
|
57
|
+
"optionalDependencies": {
|
|
58
|
+
"@napi-rs/keyring": "^1.1.6"
|
|
59
|
+
},
|
|
57
60
|
"devDependencies": {
|
|
58
61
|
"@eslint/js": "^10.0.1",
|
|
59
62
|
"@types/js-yaml": "^4.0.9",
|