camelmind 0.2.8 → 0.2.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "camelmind",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Create and manage CamelMind documentation sites",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -11,6 +11,7 @@ import {
11
11
  getSectionForSlugFromConfig,
12
12
  } from "@/lib/nav"
13
13
  import { loadMdxFile } from "@/lib/mdx"
14
+ import { preprocessTabs } from "@/lib/remark-tabs"
14
15
  import { getSession, hasAccess, pageRequiresAuth, shouldRedirectToLogin } from "@/lib/auth"
15
16
  import { getConfig, isAuthEnabled, showLastUpdated, showLastUpdateAuthor, showFeedbackWidget } from "@/lib/config"
16
17
  import { loadVersions, getVersionFromSlug, getNavForVersion } from "@/lib/versions"
@@ -98,7 +99,7 @@ export default async function DocPage({ params }: Props) {
98
99
  const activeGroup = getGroupForSlugFromConfig(nav, fullSlug) as NavGroup | null
99
100
  const sectionEntry = getSectionForSlugFromConfig(nav, fullSlug) as NavEntry | null
100
101
  const { frontmatter, source: rawSource, toc, lastUpdated, lastUpdatedAuthor } = loadMdxFile(navEntry.file)
101
- const source = rawSource.replace(/\n+([ \t]*<\/Tab>)/g, "\n\n{' '}\n\n$1")
102
+ const source = preprocessTabs(rawSource).replace(/\n+([ \t]*<\/Tab>)/g, "\n\n{' '}\n\n$1")
102
103
  const isSectionRoot = sectionEntry?.slug === fullSlug
103
104
 
104
105
  return (
@@ -6,10 +6,11 @@ import { Link } from "lucide-react"
6
6
  type Props = {
7
7
  id?: string
8
8
  summary: string
9
+ bold?: boolean
9
10
  children: React.ReactNode
10
11
  }
11
12
 
12
- export function Details({ id, summary, children }: Props) {
13
+ export function Details({ id, summary, bold = false, children }: Props) {
13
14
  const ref = useRef<HTMLDetailsElement>(null)
14
15
 
15
16
  useEffect(() => {
@@ -35,10 +36,10 @@ export function Details({ id, summary, children }: Props) {
35
36
  <details
36
37
  id={id}
37
38
  ref={ref}
38
- className="group my-3 rounded-lg border border-gray-200 bg-white overflow-hidden"
39
+ className="group my-3 rounded-lg border border-gray-200 bg-white"
39
40
  >
40
41
  <summary className="flex items-center justify-between gap-3 px-4 py-3 cursor-pointer select-none list-none hover:bg-gray-50 transition-colors">
41
- <span className="text-sm font-medium text-gray-800">{summary}</span>
42
+ <span className={`text-sm text-gray-800 ${bold ? "font-semibold" : "font-medium"}`}>{summary}</span>
42
43
  <div className="flex items-center gap-2 shrink-0">
43
44
  {id && (
44
45
  <button
@@ -57,7 +58,7 @@ export function Details({ id, summary, children }: Props) {
57
58
  </svg>
58
59
  </div>
59
60
  </summary>
60
- <div className="px-4 pb-4 pt-2 text-sm text-gray-700 border-t border-gray-100">
61
+ <div className="px-4 pb-4 pt-2 text-sm text-gray-700 border-t border-gray-100 overflow-x-auto">
61
62
  {children}
62
63
  </div>
63
64
  </details>
@@ -1,6 +1,6 @@
1
1
  "use client"
2
2
 
3
- import { useState, useEffect, useId, Children, isValidElement } from "react"
3
+ import { useState, useEffect, useId, useCallback, Children, isValidElement } from "react"
4
4
 
5
5
  type TabProps = {
6
6
  label: string
@@ -22,22 +22,21 @@ export function Tabs({ children }: { children: React.ReactNode }) {
22
22
  // Stable per-instance group id — SSR-safe, unique across multiple Tabs on the same page
23
23
  const uid = useId().replace(/[^a-z0-9]/g, "")
24
24
 
25
- const getIndexFromHash = () => {
25
+ const getIndexFromHash = useCallback(() => {
26
26
  if (typeof window === "undefined") return 0
27
27
  const match = window.location.hash.match(new RegExp(`^#__tabbed_${uid}_(\\d+)$`))
28
28
  if (!match) return 0
29
29
  const idx = parseInt(match[1], 10) - 1 // hash is 1-based
30
30
  return idx >= 0 && idx < tabs.length ? idx : 0
31
- }
31
+ }, [tabs.length, uid])
32
32
 
33
- const [active, setActive] = useState(0)
33
+ const [active, setActive] = useState(() => getIndexFromHash())
34
34
 
35
35
  useEffect(() => {
36
- setActive(getIndexFromHash())
37
36
  const onHashChange = () => setActive(getIndexFromHash())
38
37
  window.addEventListener("hashchange", onHashChange)
39
38
  return () => window.removeEventListener("hashchange", onHashChange)
40
- }, [])
39
+ }, [getIndexFromHash])
41
40
 
42
41
  const handleClick = (i: number) => {
43
42
  setActive(i)
@@ -48,45 +47,9 @@ export function Tabs({ children }: { children: React.ReactNode }) {
48
47
 
49
48
  return (
50
49
  <div className="my-4">
51
- {/* Screen: tabbed UI hidden in PDF via data-print="hide" */}
52
- <div data-print="hide" className="rounded-lg border border-gray-200 overflow-hidden">
53
- {/* Tab bar — not-prose scoped here so prose styles don't bleed into buttons */}
54
- <div className="not-prose flex border-b border-gray-200 bg-gray-50">
55
- {tabs.map((tab, i) => (
56
- <button
57
- key={i}
58
- onClick={() => handleClick(i)}
59
- className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
60
- i === active
61
- ? "border-gray-900 text-gray-900 bg-white"
62
- : "border-transparent text-gray-500 hover:text-gray-700 hover:bg-gray-100"
63
- }`}
64
- >
65
- {tab.props.label}
66
- </button>
67
- ))}
68
- </div>
69
- {/* Active tab content */}
70
- <div className="p-4 prose prose-gray max-w-none">
71
- {tabs[active]?.props.children}
72
- </div>
73
- </div>
74
-
75
- {/* PDF: all panels expanded — hidden on screen via class, shown by removing it in generator */}
76
- <div data-print="show" className="hidden">
77
- {tabs.map((tab, i) => (
78
- <div key={i} className="mb-4">
79
- <div className="text-xs font-semibold uppercase tracking-wide text-gray-500 mb-2 pb-1 border-b border-gray-200">
80
- {tab.props.label}
81
- </div>
82
- <div className="prose prose-gray max-w-none">
83
- {tab.props.children}
84
- </div>
85
- </div>
86
- ))}
87
- </div>
50
+ {/* rest of your JSX stays the same */}
88
51
  </div>
89
52
  )
90
53
  }
91
54
 
92
- Tab.displayName = "Tab"
55
+ Tab.displayName = "Tab"
@@ -0,0 +1,85 @@
1
+ // Preprocessor that converts MkDocs-style tab syntax into <Tabs>/<Tab> JSX
2
+ // before the MDX compiler sees the source. This avoids all markdown-in-JSX
3
+ // indentation and blank-line rules that make hand-authoring tabs painful.
4
+ //
5
+ // Syntax:
6
+ //
7
+ // === "Tab Label"
8
+ // Content for this tab. Any markdown works — bullets, code, callouts, etc.
9
+ //
10
+ // === "Another Tab"
11
+ // More content.
12
+ //
13
+ // ===
14
+ //
15
+ // Rules:
16
+ // - === "Label" opens a tab (single or double quotes both work)
17
+ // - === closes the group (required)
18
+ // - No indentation needed inside tabs
19
+ // - Blank lines within tab content are fine
20
+ // - Multiple tab groups per document are supported
21
+
22
+ export function preprocessTabs(source: string): string {
23
+ const lines = source.split("\n")
24
+ const output: string[] = []
25
+ let i = 0
26
+
27
+ while (i < lines.length) {
28
+ const line = lines[i]
29
+ const openMatch = line.match(/^===\s+["']([^"']+)["']\s*$/)
30
+
31
+ if (!openMatch) {
32
+ output.push(line)
33
+ i++
34
+ continue
35
+ }
36
+
37
+ // Collect all tabs in this group until the closing ===
38
+ const tabs: { label: string; content: string[] }[] = []
39
+ let currentLabel = openMatch[1]
40
+ let currentContent: string[] = []
41
+ let closed = false
42
+ i++
43
+
44
+ while (i < lines.length) {
45
+ const groupLine = lines[i]
46
+ const nextTabMatch = groupLine.match(/^===\s+["']([^"']+)["']\s*$/)
47
+ const isCloser = /^===\s*$/.test(groupLine)
48
+
49
+ if (isCloser) {
50
+ tabs.push({ label: currentLabel, content: currentContent })
51
+ closed = true
52
+ i++
53
+ break
54
+ } else if (nextTabMatch) {
55
+ tabs.push({ label: currentLabel, content: currentContent })
56
+ currentLabel = nextTabMatch[1]
57
+ currentContent = []
58
+ i++
59
+ } else {
60
+ currentContent.push(groupLine)
61
+ i++
62
+ }
63
+ }
64
+
65
+ // Handle unclosed group (EOF without ===)
66
+ if (!closed) {
67
+ tabs.push({ label: currentLabel, content: currentContent })
68
+ }
69
+
70
+ if (tabs.length === 0) continue
71
+
72
+ output.push("<Tabs>")
73
+ for (const tab of tabs) {
74
+ output.push(`<Tab label="${tab.label}">`)
75
+ output.push("")
76
+ output.push(tab.content.join("\n").trim())
77
+ output.push("")
78
+ output.push("</Tab>")
79
+ }
80
+ output.push("</Tabs>")
81
+ output.push("")
82
+ }
83
+
84
+ return output.join("\n")
85
+ }