docmk 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.
Files changed (70) hide show
  1. package/.claude/skills/pdf/SKILL.md +89 -0
  2. package/.claude/skills/web-scraping/SKILL.md +78 -0
  3. package/CLAUDE.md +90 -0
  4. package/bin/docmk.js +3 -0
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.js +636 -0
  7. package/dist/index.js.map +1 -0
  8. package/final-site/assets/main-B4orIFxK.css +1 -0
  9. package/final-site/assets/main-CSoKXua6.js +25 -0
  10. package/final-site/favicon.svg +4 -0
  11. package/final-site/index.html +26 -0
  12. package/final-site/robots.txt +4 -0
  13. package/final-site/sitemap.xml +14 -0
  14. package/my-docs/api/README.md +152 -0
  15. package/my-docs/api/advanced.md +260 -0
  16. package/my-docs/getting-started/README.md +24 -0
  17. package/my-docs/tutorials/README.md +272 -0
  18. package/my-docs/tutorials/customization.md +492 -0
  19. package/package.json +59 -0
  20. package/postcss.config.js +6 -0
  21. package/site/assets/main-BZUsYUCF.css +1 -0
  22. package/site/assets/main-q6laQtCD.js +114 -0
  23. package/site/favicon.svg +4 -0
  24. package/site/index.html +23 -0
  25. package/site/robots.txt +4 -0
  26. package/site/sitemap.xml +34 -0
  27. package/site-output/assets/main-B4orIFxK.css +1 -0
  28. package/site-output/assets/main-CSoKXua6.js +25 -0
  29. package/site-output/favicon.svg +4 -0
  30. package/site-output/index.html +26 -0
  31. package/site-output/robots.txt +4 -0
  32. package/site-output/sitemap.xml +14 -0
  33. package/src/builder/index.ts +189 -0
  34. package/src/builder/vite-dev.ts +117 -0
  35. package/src/cli/commands/build.ts +48 -0
  36. package/src/cli/commands/dev.ts +53 -0
  37. package/src/cli/commands/preview.ts +57 -0
  38. package/src/cli/index.ts +42 -0
  39. package/src/client/App.vue +15 -0
  40. package/src/client/components/SearchBox.vue +204 -0
  41. package/src/client/components/Sidebar.vue +18 -0
  42. package/src/client/components/SidebarItem.vue +108 -0
  43. package/src/client/index.html +21 -0
  44. package/src/client/layouts/AppLayout.vue +99 -0
  45. package/src/client/lib/utils.ts +6 -0
  46. package/src/client/main.ts +42 -0
  47. package/src/client/pages/Home.vue +279 -0
  48. package/src/client/pages/SkillPage.vue +565 -0
  49. package/src/client/router.ts +16 -0
  50. package/src/client/styles/global.css +92 -0
  51. package/src/client/utils/routes.ts +69 -0
  52. package/src/parser/index.ts +253 -0
  53. package/src/scanner/index.ts +127 -0
  54. package/src/types/index.ts +45 -0
  55. package/tailwind.config.js +65 -0
  56. package/test-build/assets/main-C2ARPC0e.css +1 -0
  57. package/test-build/assets/main-CHIQpV3B.js +25 -0
  58. package/test-build/favicon.svg +4 -0
  59. package/test-build/index.html +47 -0
  60. package/test-build/robots.txt +4 -0
  61. package/test-build/sitemap.xml +19 -0
  62. package/test-dist/assets/main-B4orIFxK.css +1 -0
  63. package/test-dist/assets/main-CSoKXua6.js +25 -0
  64. package/test-dist/favicon.svg +4 -0
  65. package/test-dist/index.html +26 -0
  66. package/test-dist/robots.txt +4 -0
  67. package/test-dist/sitemap.xml +14 -0
  68. package/tsconfig.json +30 -0
  69. package/tsup.config.ts +13 -0
  70. package/vite.config.ts +21 -0
@@ -0,0 +1,69 @@
1
+ import { SkillFile } from '../../types/index.js'
2
+
3
+ /**
4
+ * Generate route from file path based on the configured skills directory
5
+ * This replaces hardcoded .claude/skills references
6
+ */
7
+ export function getFileRoute(file: SkillFile, skillsDir?: string): string {
8
+ const filePath = file.path
9
+
10
+ // If skillsDir is provided, use it; otherwise try to detect from path
11
+ if (skillsDir) {
12
+ const normalizedPath = filePath.replace(/\\/g, '/')
13
+ const normalizedSkillsDir = skillsDir.replace(/\\/g, '/')
14
+
15
+ const index = normalizedPath.indexOf(normalizedSkillsDir)
16
+ if (index !== -1) {
17
+ const relativePath = normalizedPath.slice(index + normalizedSkillsDir.length)
18
+ return pathToRoute(relativePath)
19
+ }
20
+ }
21
+
22
+ // Fallback: try common patterns
23
+ const patterns = ['.claude/skills', 'docs', 'documentation', 'my-docs']
24
+ for (const pattern of patterns) {
25
+ const index = filePath.indexOf(pattern)
26
+ if (index !== -1) {
27
+ const relativePath = filePath.slice(index + pattern.length)
28
+ return pathToRoute(relativePath)
29
+ }
30
+ }
31
+
32
+ // Last resort: use filename
33
+ return pathToRoute('/' + file.name)
34
+ }
35
+
36
+ function pathToRoute(relativePath: string): string {
37
+ const segments = relativePath.split('/').filter(Boolean)
38
+
39
+ if (segments.length === 0) return '/'
40
+
41
+ const lastSegment = segments[segments.length - 1]
42
+ if (lastSegment.endsWith('.md')) {
43
+ segments[segments.length - 1] = lastSegment.slice(0, -3)
44
+ }
45
+
46
+ // SKILL.md and README.md should map to parent directory route
47
+ const finalSegment = segments[segments.length - 1]
48
+ if (finalSegment === 'SKILL' || finalSegment === 'README') {
49
+ segments.pop()
50
+ }
51
+
52
+ return '/' + segments.join('/')
53
+ }
54
+
55
+ /**
56
+ * Get display path for search results
57
+ */
58
+ export function getPathDisplay(filePath: string, skillsDir?: string): string {
59
+ if (skillsDir) {
60
+ const index = filePath.indexOf(skillsDir)
61
+ if (index !== -1) {
62
+ return filePath.slice(index)
63
+ }
64
+ }
65
+
66
+ // Fallback to showing last few segments
67
+ const segments = filePath.split('/').filter(Boolean)
68
+ return segments.slice(-3).join('/')
69
+ }
@@ -0,0 +1,253 @@
1
+ import matter from 'gray-matter'
2
+ import MarkdownIt from 'markdown-it'
3
+ import { createHighlighter, Highlighter } from 'shiki'
4
+ import { SkillDirectory, SkillFile, SiteConfig, DocGenConfig, Navigation } from '../types/index.js'
5
+
6
+ let highlighter: Highlighter | null = null
7
+
8
+ async function getHighlighter() {
9
+ if (!highlighter) {
10
+ highlighter = await createHighlighter({
11
+ themes: ['github-dark', 'github-light'],
12
+ langs: ['javascript', 'typescript', 'bash', 'shell', 'json', 'html', 'css', 'vue', 'jsx', 'tsx', 'python', 'markdown', 'yaml', 'sql', 'go', 'rust', 'java', 'c', 'cpp']
13
+ })
14
+ }
15
+ return highlighter
16
+ }
17
+
18
+ const md = new MarkdownIt({
19
+ html: true,
20
+ linkify: true,
21
+ typographer: true,
22
+ highlight: function (str: string, lang: string) {
23
+ // Synchronous fallback - actual highlighting done in enhanceFileContent
24
+ return `<pre class="shiki-pending" data-lang="${lang || 'text'}"><code>${md.utils.escapeHtml(str)}</code></pre>`
25
+ }
26
+ })
27
+
28
+ export async function parseSkillsToConfig(
29
+ directories: SkillDirectory[],
30
+ siteConfig: SiteConfig
31
+ ): Promise<DocGenConfig> {
32
+ const files: SkillFile[] = []
33
+ const navigation = await generateNavigation(directories)
34
+
35
+ // Collect all files for search indexing
36
+ collectAllFiles(directories, files)
37
+
38
+ // Enhance files with parsed content
39
+ for (const file of files) {
40
+ await enhanceFileContent(file)
41
+ }
42
+
43
+ return {
44
+ siteConfig,
45
+ navigation,
46
+ files,
47
+ directories
48
+ }
49
+ }
50
+
51
+ function collectAllFiles(items: (SkillDirectory | SkillFile)[], files: SkillFile[]) {
52
+ for (const item of items) {
53
+ if ('content' in item) {
54
+ // It's a SkillFile
55
+ files.push(item)
56
+ } else {
57
+ // It's a SkillDirectory
58
+ if (item.skillFile) {
59
+ files.push(item.skillFile)
60
+ }
61
+ collectAllFiles(item.children, files)
62
+ }
63
+ }
64
+ }
65
+
66
+ async function enhanceFileContent(file: SkillFile) {
67
+ try {
68
+ let content = file.content
69
+
70
+ // Update title and description from frontmatter if available
71
+ file.title = file.frontmatter.title || file.title
72
+ file.description = file.frontmatter.description || file.description
73
+
74
+ // Remove duplicate h1 if it matches frontmatter title
75
+ if (file.frontmatter.title) {
76
+ // Match first h1 heading in content (may have leading whitespace/newlines)
77
+ const h1Match = content.match(/^\s*#\s+(.+)$/m)
78
+ if (h1Match) {
79
+ const h1Text = h1Match[1].trim()
80
+ // If h1 matches title, remove it to avoid duplication
81
+ if (h1Text === file.frontmatter.title) {
82
+ content = content.replace(/^\s*#\s+.+\n*/, '')
83
+ }
84
+ }
85
+ }
86
+
87
+ // Render markdown to HTML
88
+ let html = md.render(content)
89
+
90
+ // Apply syntax highlighting with shiki
91
+ html = await highlightCodeBlocks(html)
92
+ file.frontmatter.html = html
93
+
94
+ // Extract headings for TOC
95
+ const headings = extractHeadings(content)
96
+ file.frontmatter.headings = headings
97
+
98
+ } catch (error) {
99
+ console.warn(`Failed to enhance content for ${file.path}:`, error)
100
+ try {
101
+ file.frontmatter.html = md.render(file.content)
102
+ } catch (e) {
103
+ console.error(`Failed to render markdown for ${file.path}:`, e)
104
+ }
105
+ }
106
+ }
107
+
108
+ async function highlightCodeBlocks(html: string): Promise<string> {
109
+ const hl = await getHighlighter()
110
+
111
+ // Find all pending shiki code blocks and highlight them
112
+ const codeBlockRegex = /<pre class="shiki-pending" data-lang="([^"]*)"[^>]*><code>([^]*?)<\/code><\/pre>/g
113
+
114
+ const matches = [...html.matchAll(codeBlockRegex)]
115
+
116
+ for (const match of matches) {
117
+ const [fullMatch, lang, escapedCode] = match
118
+ // Unescape HTML entities
119
+ const code = escapedCode
120
+ .replace(/&lt;/g, '<')
121
+ .replace(/&gt;/g, '>')
122
+ .replace(/&amp;/g, '&')
123
+ .replace(/&quot;/g, '"')
124
+ .replace(/&#39;/g, "'")
125
+
126
+ try {
127
+ const validLang = hl.getLoadedLanguages().includes(lang) ? lang : 'text'
128
+ const highlighted = hl.codeToHtml(code, {
129
+ lang: validLang,
130
+ theme: 'github-dark'
131
+ })
132
+ html = html.replace(fullMatch, highlighted)
133
+ } catch (e) {
134
+ // Keep original if highlighting fails
135
+ console.warn(`Failed to highlight ${lang}:`, e)
136
+ }
137
+ }
138
+
139
+ return html
140
+ }
141
+
142
+ function extractHeadings(content: string) {
143
+ const headings: Array<{ level: number; text: string; anchor: string }> = []
144
+ const lines = content.split('\n')
145
+
146
+ for (const line of lines) {
147
+ const match = line.match(/^(#{1,6})\\s+(.+)$/)
148
+ if (match) {
149
+ const level = match[1].length
150
+ const text = match[2].trim()
151
+ const anchor = text.toLowerCase()
152
+ .replace(/[^\\w\\s-]/g, '')
153
+ .replace(/\\s+/g, '-')
154
+ .trim()
155
+
156
+ headings.push({ level, text, anchor })
157
+ }
158
+ }
159
+
160
+ return headings
161
+ }
162
+
163
+ async function generateNavigation(directories: SkillDirectory[]): Promise<Navigation[]> {
164
+ const navigation: Navigation[] = []
165
+
166
+ for (const dir of directories) {
167
+ const navItem: Navigation = {
168
+ text: dir.skillFile?.title || formatDirName(dir.name),
169
+ link: dir.skillFile ? getFileRoute(dir.skillFile) : undefined
170
+ }
171
+
172
+ if (dir.children.length > 0) {
173
+ navItem.children = []
174
+
175
+ for (const child of dir.children) {
176
+ if ('content' in child) {
177
+ // It's a file
178
+ navItem.children.push({
179
+ text: child.title || formatFileName(child.name),
180
+ link: getFileRoute(child)
181
+ })
182
+ } else {
183
+ // It's a subdirectory
184
+ const subNav = await generateNavigation([child])
185
+ navItem.children.push(...subNav)
186
+ }
187
+ }
188
+ }
189
+
190
+ navigation.push(navItem)
191
+ }
192
+
193
+ return navigation
194
+ }
195
+
196
+ function getFileRoute(file: SkillFile): string {
197
+ // Import the utility function from client utils
198
+ // For now, use inline implementation
199
+ const filePath = file.path
200
+
201
+ // Try common patterns
202
+ const patterns = ['my-docs', 'docs', 'documentation', '.claude/skills']
203
+ let relativePath = ''
204
+
205
+ for (const pattern of patterns) {
206
+ const index = filePath.indexOf(pattern)
207
+ if (index !== -1) {
208
+ relativePath = filePath.slice(index + pattern.length)
209
+ break
210
+ }
211
+ }
212
+
213
+ if (!relativePath) {
214
+ // Fallback: use last 2 segments
215
+ const segments = filePath.split('/').filter(Boolean)
216
+ if (segments.length >= 2) {
217
+ relativePath = '/' + segments.slice(-2).join('/')
218
+ } else {
219
+ return '/'
220
+ }
221
+ }
222
+
223
+ const segments = relativePath.split('/').filter(Boolean)
224
+
225
+ if (segments.length === 0) return '/'
226
+
227
+ // Remove file extension
228
+ const lastSegment = segments[segments.length - 1]
229
+ if (lastSegment.endsWith('.md')) {
230
+ segments[segments.length - 1] = lastSegment.slice(0, -3)
231
+ }
232
+
233
+ // SKILL.md and README.md should map to parent directory route
234
+ const finalSegment = segments[segments.length - 1]
235
+ if (finalSegment === 'SKILL' || finalSegment === 'README') {
236
+ segments.pop()
237
+ }
238
+
239
+ return '/' + segments.join('/')
240
+ }
241
+
242
+ function formatDirName(name: string): string {
243
+ return name.split('-').map(word =>
244
+ word.charAt(0).toUpperCase() + word.slice(1)
245
+ ).join(' ')
246
+ }
247
+
248
+ function formatFileName(name: string): string {
249
+ const baseName = name.replace('.md', '')
250
+ return formatDirName(baseName)
251
+ }
252
+
253
+ export { md as markdownRenderer }
@@ -0,0 +1,127 @@
1
+ import fs from 'fs/promises'
2
+ import path from 'path'
3
+ import { SkillDirectory, SkillFile } from '../types/index.js'
4
+
5
+ export async function scanSkillsDirectory(sourceDir: string): Promise<SkillDirectory[]> {
6
+ try {
7
+ await fs.access(sourceDir)
8
+ } catch {
9
+ throw new Error(`Source directory not found: ${sourceDir}`)
10
+ }
11
+
12
+ const entries = await fs.readdir(sourceDir, { withFileTypes: true })
13
+ const directories: SkillDirectory[] = []
14
+
15
+ for (const entry of entries) {
16
+ if (entry.isDirectory()) {
17
+ const dirPath = path.join(sourceDir, entry.name)
18
+ const skillDirectory = await scanDirectory(dirPath, entry.name)
19
+ directories.push(skillDirectory)
20
+ }
21
+ }
22
+
23
+ return directories
24
+ }
25
+
26
+ async function scanDirectory(dirPath: string, name: string): Promise<SkillDirectory> {
27
+ const entries = await fs.readdir(dirPath, { withFileTypes: true })
28
+ const children: (SkillDirectory | SkillFile)[] = []
29
+ let skillFile: SkillFile | undefined
30
+
31
+ for (const entry of entries) {
32
+ const entryPath = path.join(dirPath, entry.name)
33
+
34
+ if (entry.isDirectory()) {
35
+ const subDir = await scanDirectory(entryPath, entry.name)
36
+ children.push(subDir)
37
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
38
+ const file = await parseMarkdownFile(entryPath, entry.name)
39
+
40
+ if (entry.name === 'SKILL.md') {
41
+ skillFile = file
42
+ } else {
43
+ children.push(file)
44
+ }
45
+ }
46
+ }
47
+
48
+ return {
49
+ path: dirPath,
50
+ name,
51
+ children,
52
+ skillFile
53
+ }
54
+ }
55
+
56
+ async function parseMarkdownFile(filePath: string, fileName: string): Promise<SkillFile> {
57
+ const content = await fs.readFile(filePath, 'utf-8')
58
+ const stats = await fs.stat(filePath)
59
+
60
+ // Basic frontmatter extraction
61
+ const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/)
62
+ const frontmatter: Record<string, any> = {}
63
+ let markdownContent = content
64
+
65
+ if (frontmatterMatch) {
66
+ // Remove frontmatter from content
67
+ markdownContent = content.slice(frontmatterMatch[0].length)
68
+
69
+ // Parse frontmatter key-value pairs
70
+ const fmLines = frontmatterMatch[1].split(/\r?\n/)
71
+ for (const line of fmLines) {
72
+ const colonIndex = line.indexOf(':')
73
+ if (colonIndex > 0) {
74
+ const key = line.slice(0, colonIndex).trim()
75
+ const value = line.slice(colonIndex + 1).trim().replace(/^["']|["']$/g, '')
76
+ if (key && value) {
77
+ frontmatter[key] = value
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ return {
84
+ path: filePath,
85
+ name: fileName,
86
+ title: frontmatter.title || fileName.replace('.md', ''),
87
+ description: frontmatter.description,
88
+ content: markdownContent,
89
+ frontmatter,
90
+ lastModified: stats.mtime.getTime()
91
+ }
92
+ }
93
+
94
+ export async function watchSkillsDirectory(
95
+ sourceDir: string,
96
+ callback: (directories: SkillDirectory[]) => void
97
+ ) {
98
+ const chokidar = await import('chokidar')
99
+
100
+ const watcher = chokidar.watch(sourceDir, {
101
+ ignored: /node_modules/,
102
+ persistent: true,
103
+ ignoreInitial: true
104
+ })
105
+
106
+ let debounceTimer: NodeJS.Timeout
107
+
108
+ const handleChange = () => {
109
+ clearTimeout(debounceTimer)
110
+ debounceTimer = setTimeout(async () => {
111
+ try {
112
+ const directories = await scanSkillsDirectory(sourceDir)
113
+ callback(directories)
114
+ } catch (error) {
115
+ console.error('Error rescanning source directory:', error)
116
+ }
117
+ }, 300)
118
+ }
119
+
120
+ watcher.on('add', handleChange)
121
+ watcher.on('change', handleChange)
122
+ watcher.on('unlink', handleChange)
123
+ watcher.on('addDir', handleChange)
124
+ watcher.on('unlinkDir', handleChange)
125
+
126
+ return watcher
127
+ }
@@ -0,0 +1,45 @@
1
+ export interface SkillFile {
2
+ path: string
3
+ name: string
4
+ title?: string
5
+ description?: string
6
+ category?: string
7
+ content: string
8
+ frontmatter: Record<string, any>
9
+ lastModified: number
10
+ }
11
+
12
+ export interface SkillDirectory {
13
+ path: string
14
+ name: string
15
+ children: (SkillDirectory | SkillFile)[]
16
+ skillFile?: SkillFile // SKILL.md file if exists
17
+ }
18
+
19
+ export interface SiteConfig {
20
+ title: string
21
+ description: string
22
+ baseUrl: string
23
+ skillsDir: string
24
+ outputDir: string
25
+ }
26
+
27
+ export interface Navigation {
28
+ text: string
29
+ link?: string
30
+ children?: Navigation[]
31
+ }
32
+
33
+ export interface DocGenConfig {
34
+ siteConfig: SiteConfig
35
+ navigation: Navigation[]
36
+ files: SkillFile[]
37
+ directories: SkillDirectory[]
38
+ }
39
+
40
+ export interface BuildOptions {
41
+ input: string
42
+ output: string
43
+ mode: 'development' | 'production'
44
+ watch?: boolean
45
+ }
@@ -0,0 +1,65 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ darkMode: ["class"],
4
+ content: [
5
+ './src/**/*.{ts,tsx,vue}',
6
+ ],
7
+ theme: {
8
+ container: {
9
+ center: true,
10
+ padding: "2rem",
11
+ screens: {
12
+ "2xl": "1400px",
13
+ },
14
+ },
15
+ extend: {
16
+ colors: {
17
+ border: "hsl(var(--border))",
18
+ input: "hsl(var(--input))",
19
+ ring: "hsl(var(--ring))",
20
+ background: "hsl(var(--background))",
21
+ foreground: "hsl(var(--foreground))",
22
+ primary: {
23
+ DEFAULT: "hsl(var(--primary))",
24
+ foreground: "hsl(var(--primary-foreground))",
25
+ },
26
+ secondary: {
27
+ DEFAULT: "hsl(var(--secondary))",
28
+ foreground: "hsl(var(--secondary-foreground))",
29
+ },
30
+ destructive: {
31
+ DEFAULT: "hsl(var(--destructive))",
32
+ foreground: "hsl(var(--destructive-foreground))",
33
+ },
34
+ muted: {
35
+ DEFAULT: "hsl(var(--muted))",
36
+ foreground: "hsl(var(--muted-foreground))",
37
+ },
38
+ accent: {
39
+ DEFAULT: "hsl(var(--accent))",
40
+ foreground: "hsl(var(--accent-foreground))",
41
+ },
42
+ popover: {
43
+ DEFAULT: "hsl(var(--popover))",
44
+ foreground: "hsl(var(--popover-foreground))",
45
+ },
46
+ card: {
47
+ DEFAULT: "hsl(var(--card))",
48
+ foreground: "hsl(var(--card-foreground))",
49
+ },
50
+ },
51
+ borderRadius: {
52
+ lg: "var(--radius)",
53
+ md: "calc(var(--radius) - 2px)",
54
+ sm: "calc(var(--radius) - 4px)",
55
+ },
56
+ fontFamily: {
57
+ sans: ["Inter", "ui-sans-serif", "system-ui", "sans-serif"],
58
+ mono: ["ui-monospace", "SF Mono", "Menlo", "monospace"],
59
+ },
60
+ },
61
+ },
62
+ plugins: [
63
+ require("@tailwindcss/typography"),
64
+ ],
65
+ }
@@ -0,0 +1 @@
1
+ .sidebar-item[data-v-b5febdd6]{margin-bottom:.25rem}.item-wrapper[data-v-b5febdd6]{display:flex;align-items:center;padding:.5rem 1.5rem;position:relative}.expand-button[data-v-b5febdd6]{background:none;border:none;color:#666;cursor:pointer;padding:.25rem;margin-right:.5rem;display:flex;align-items:center;justify-content:center;transition:transform .2s ease}.expand-button.expanded[data-v-b5febdd6]{transform:rotate(90deg)}.expand-button[data-v-b5febdd6]:hover{color:#333}.nav-link[data-v-b5febdd6],.nav-text[data-v-b5febdd6]{flex:1;text-decoration:none;color:#555;font-weight:500;font-size:.9rem;line-height:1.4;transition:color .2s ease}.nav-link[data-v-b5febdd6]:hover{color:#3182ce}.nav-link.active[data-v-b5febdd6]{color:#3182ce;font-weight:600;position:relative}.nav-link.active[data-v-b5febdd6]:before{content:"";position:absolute;left:-1.5rem;top:50%;transform:translateY(-50%);width:3px;height:20px;background-color:#3182ce;border-radius:2px}.nav-text[data-v-b5febdd6]{color:#333;font-weight:600}.children[data-v-b5febdd6]{overflow:hidden;max-height:0;transition:max-height .3s ease;padding-left:1rem}.children.expanded[data-v-b5febdd6]{max-height:1000px}.children .sidebar-item[data-v-b5febdd6]{margin-left:1rem}.children .nav-link[data-v-b5febdd6],.children .nav-text[data-v-b5febdd6]{font-size:.85rem;font-weight:400;color:#666}.children .nav-link[data-v-b5febdd6]:hover{color:#3182ce}.children .nav-link.active[data-v-b5febdd6]{color:#3182ce;font-weight:500}.sidebar-nav[data-v-014c42bf]{height:100%;overflow-y:auto}.nav-content[data-v-014c42bf]{padding:1.5rem 0}.search-box[data-v-b8e3e134]{position:relative;width:320px}.search-input-wrapper[data-v-b8e3e134]{position:relative;display:flex;align-items:center}.search-input[data-v-b8e3e134]{width:100%;padding:.75rem 2.5rem .75rem 1rem;border:2px solid #e1e5e9;border-radius:6px;font-size:.9rem;transition:border-color .2s ease;outline:none}.search-input[data-v-b8e3e134]:focus{border-color:#3182ce}.search-icon[data-v-b8e3e134]{position:absolute;right:.75rem;color:#666;pointer-events:none}.search-results[data-v-b8e3e134]{position:absolute;top:100%;left:0;right:0;background:#fff;border:1px solid #e1e5e9;border-radius:6px;box-shadow:0 10px 25px #0000001a;z-index:1000;max-height:400px;overflow-y:auto;margin-top:.5rem}.no-results[data-v-b8e3e134]{padding:1rem;color:#666;text-align:center;font-style:italic}.search-result[data-v-b8e3e134]{display:block;padding:1rem;text-decoration:none;color:inherit;border-bottom:1px solid #f0f0f0;transition:background-color .2s ease}.search-result[data-v-b8e3e134]:hover{background-color:#f8f9fa}.search-result[data-v-b8e3e134]:last-child{border-bottom:none}.result-title[data-v-b8e3e134]{font-weight:600;color:#2c3e50;margin-bottom:.25rem}.result-description[data-v-b8e3e134]{font-size:.85rem;color:#666;margin-bottom:.25rem;line-height:1.4}.result-path[data-v-b8e3e134]{font-size:.75rem;color:#999;font-family:monospace}@media (max-width: 768px){.search-box[data-v-b8e3e134]{width:100%;max-width:280px}}.app-layout[data-v-52037cca]{min-height:100vh;display:flex;flex-direction:column}.header[data-v-52037cca]{background:#fff;border-bottom:1px solid #e1e5e9;padding:1rem 0;position:sticky;top:0;z-index:100}.header-content[data-v-52037cca]{max-width:1400px;margin:0 auto;padding:0 2rem;display:flex;justify-content:space-between;align-items:center}.site-title[data-v-52037cca]{font-size:1.5rem;font-weight:600;color:#2c3e50;margin:0}.main-container[data-v-52037cca]{flex:1;display:flex;max-width:1400px;margin:0 auto;width:100%}.sidebar[data-v-52037cca]{width:280px;flex-shrink:0;background:#f8f9fa;border-right:1px solid #e1e5e9;height:calc(100vh - 80px);overflow-y:auto;position:sticky;top:80px}.content[data-v-52037cca]{flex:1;padding:2rem;overflow-y:auto;max-width:calc(100vw - 280px)}.loading[data-v-52037cca]{display:flex;justify-content:center;align-items:center;height:200px;color:#666;font-size:1.1rem}@media (max-width: 768px){.main-container[data-v-52037cca]{flex-direction:column}.sidebar[data-v-52037cca]{width:100%;height:auto;position:static;border-right:none;border-bottom:1px solid #e1e5e9}.content[data-v-52037cca]{max-width:100%;padding:1rem}.header-content[data-v-52037cca]{padding:0 1rem;flex-direction:column;gap:1rem}}#app[data-v-00943de1]{min-height:100vh}.home-page[data-v-83887790]{max-width:1000px;margin:0 auto}.hero-section[data-v-83887790]{text-align:center;padding:3rem 0;margin-bottom:3rem}.hero-title[data-v-83887790]{font-size:3rem;font-weight:700;color:#2c3e50;margin-bottom:1rem}.hero-description[data-v-83887790]{font-size:1.2rem;color:#666;max-width:600px;margin:0 auto;line-height:1.6}.content-overview[data-v-83887790]{space-y:3rem}.stats-grid[data-v-83887790]{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1.5rem;margin-bottom:3rem}.stat-card[data-v-83887790]{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;padding:2rem;border-radius:12px;text-align:center;box-shadow:0 4px 6px #0000001a}.stat-number[data-v-83887790]{font-size:2.5rem;font-weight:700;margin-bottom:.5rem}.stat-label[data-v-83887790]{font-size:1rem;opacity:.9}.recent-section[data-v-83887790],.navigation-section[data-v-83887790]{margin-bottom:3rem}.recent-section h2[data-v-83887790],.navigation-section h2[data-v-83887790]{font-size:1.8rem;font-weight:600;color:#2c3e50;margin-bottom:1.5rem}.recent-files[data-v-83887790]{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:1rem}.recent-file-card[data-v-83887790]{background:#fff;border:1px solid #e1e5e9;border-radius:8px;padding:1.5rem;text-decoration:none;color:inherit;transition:all .2s ease}.recent-file-card[data-v-83887790]:hover{transform:translateY(-2px);box-shadow:0 4px 12px #0000001a;border-color:#3182ce}.file-title[data-v-83887790]{font-weight:600;color:#2c3e50;margin-bottom:.5rem}.file-description[data-v-83887790]{color:#666;font-size:.9rem;margin-bottom:1rem;line-height:1.4}.file-meta[data-v-83887790]{color:#999;font-size:.8rem}.skill-grid[data-v-83887790]{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1.5rem}.skill-card[data-v-83887790]{background:#fff;border:1px solid #e1e5e9;border-radius:8px;padding:1.5rem;text-decoration:none;color:inherit;transition:all .2s ease}.skill-card[data-v-83887790]:hover{transform:translateY(-2px);box-shadow:0 4px 12px #0000001a;border-color:#3182ce}.skill-name[data-v-83887790]{font-weight:600;color:#2c3e50;font-size:1.1rem;margin-bottom:.5rem}.skill-description[data-v-83887790]{color:#666;font-size:.9rem;line-height:1.4;margin-bottom:1rem}.skill-files-count[data-v-83887790]{color:#3182ce;font-size:.8rem;font-weight:500}@media (max-width: 768px){.hero-title[data-v-83887790]{font-size:2rem}.hero-description[data-v-83887790]{font-size:1rem}.stats-grid[data-v-83887790],.recent-files[data-v-83887790],.skill-grid[data-v-83887790]{grid-template-columns:1fr}}.skill-page[data-v-ec306008]{max-width:800px;margin:0 auto}.skill-header[data-v-ec306008]{margin-bottom:2rem;padding-bottom:1.5rem;border-bottom:1px solid #e1e5e9}.skill-title[data-v-ec306008]{font-size:2.5rem;font-weight:700;color:#2c3e50;margin-bottom:1rem;line-height:1.2}.skill-description[data-v-ec306008]{font-size:1.2rem;color:#666;margin-bottom:1rem;line-height:1.6}.skill-meta[data-v-ec306008]{color:#999;font-size:.9rem}.toc[data-v-ec306008]{background:#f8f9fa;border:1px solid #e1e5e9;border-radius:8px;padding:1.5rem;margin-bottom:2rem}.toc h3[data-v-ec306008]{margin:0 0 1rem;color:#2c3e50;font-size:1.1rem}.toc-list[data-v-ec306008]{list-style:none;padding:0;margin:0}.toc-list li[data-v-ec306008]{margin-bottom:.5rem}.toc-level-1[data-v-ec306008]{padding-left:0}.toc-level-2[data-v-ec306008]{padding-left:1rem}.toc-level-3[data-v-ec306008]{padding-left:2rem}.toc-level-4[data-v-ec306008]{padding-left:3rem}.toc-level-5[data-v-ec306008]{padding-left:4rem}.toc-level-6[data-v-ec306008]{padding-left:5rem}.toc-link[data-v-ec306008]{color:#555;text-decoration:none;font-size:.9rem;transition:color .2s ease}.toc-link[data-v-ec306008]:hover{color:#3182ce}.markdown-content[data-v-ec306008]{line-height:1.7;color:#333}.page-navigation[data-v-ec306008]{display:flex;justify-content:space-between;margin-top:3rem;padding-top:2rem;border-top:1px solid #e1e5e9;gap:1rem}.nav-link[data-v-ec306008]{flex:1;text-decoration:none;color:inherit;padding:1.5rem;border:1px solid #e1e5e9;border-radius:8px;transition:all .2s ease}.nav-link[data-v-ec306008]:hover{border-color:#3182ce;box-shadow:0 2px 8px #0000001a}.nav-prev[data-v-ec306008]{text-align:left}.nav-next[data-v-ec306008]{text-align:right}.nav-direction[data-v-ec306008]{color:#3182ce;font-size:.9rem;font-weight:500;margin-bottom:.25rem}.nav-title[data-v-ec306008]{color:#2c3e50;font-weight:600}.not-found[data-v-ec306008]{text-align:center;padding:3rem 0}.not-found h1[data-v-ec306008]{font-size:2rem;color:#2c3e50;margin-bottom:1rem}.not-found p[data-v-ec306008]{color:#666;margin-bottom:2rem}.back-home[data-v-ec306008]{color:#3182ce;text-decoration:none;font-weight:500}.back-home[data-v-ec306008]:hover{text-decoration:underline}@media (max-width: 768px){.skill-title[data-v-ec306008]{font-size:2rem}.page-navigation[data-v-ec306008]{flex-direction:column}.nav-next[data-v-ec306008]{text-align:left}}.markdown-content h1,.markdown-content h2,.markdown-content h3,.markdown-content h4,.markdown-content h5,.markdown-content h6{color:#2c3e50;font-weight:600;margin-top:2rem;margin-bottom:1rem;line-height:1.3}.markdown-content h1{font-size:2rem}.markdown-content h2{font-size:1.5rem}.markdown-content h3{font-size:1.25rem}.markdown-content h4{font-size:1.1rem}.markdown-content p{margin-bottom:1rem}.markdown-content ul,.markdown-content ol{margin-bottom:1rem;padding-left:1.5rem}.markdown-content li{margin-bottom:.5rem}.markdown-content code{background:#f1f3f4;padding:.2rem .4rem;border-radius:3px;font-family:SF Mono,Consolas,monospace;font-size:.9em}.markdown-content pre{background:#f8f9fa;border:1px solid #e1e5e9;border-radius:6px;padding:1rem;overflow-x:auto;margin:1rem 0}.markdown-content pre code{background:none;padding:0;border-radius:0}.markdown-content blockquote{border-left:4px solid #3182ce;padding-left:1rem;margin:1rem 0;color:#666;font-style:italic}.markdown-content table{width:100%;border-collapse:collapse;margin:1rem 0}.markdown-content th,.markdown-content td{border:1px solid #e1e5e9;padding:.5rem;text-align:left}.markdown-content th{background:#f8f9fa;font-weight:600}