camelmind 0.2.8 → 0.2.10
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 +1 -1
- package/template/app/[...slug]/page.tsx +2 -1
- package/template/components/mdx/Details.tsx +5 -4
- package/template/components/mdx/Tabs.tsx +36 -42
- package/template/lib/remark-tabs.ts +85 -0
- package/template/public/favicon.png +0 -0
- package/template/public/images/gwa-app-central-main.png +0 -0
package/package.json
CHANGED
|
@@ -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
|
|
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=
|
|
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(
|
|
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)
|
|
@@ -47,46 +46,41 @@ export function Tabs({ children }: { children: React.ReactNode }) {
|
|
|
47
46
|
if (tabs.length === 0) return null
|
|
48
47
|
|
|
49
48
|
return (
|
|
50
|
-
<div className="my-4">
|
|
51
|
-
|
|
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">
|
|
49
|
+
<div className="my-4 border border-[var(--cm-border)] rounded-[var(--cm-radius-md)] overflow-hidden">
|
|
50
|
+
<div className="flex border-b border-[var(--cm-border)] px-2 pt-1" role="tablist">
|
|
77
51
|
{tabs.map((tab, i) => (
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
52
|
+
<button
|
|
53
|
+
key={i}
|
|
54
|
+
role="tab"
|
|
55
|
+
aria-selected={active === i}
|
|
56
|
+
aria-controls={`tab-panel-${uid}-${i}`}
|
|
57
|
+
id={`tab-${uid}-${i}`}
|
|
58
|
+
onClick={() => handleClick(i)}
|
|
59
|
+
style={active === i ? { borderColor: "var(--cm-active-border)", color: "var(--cm-active)" } : {}}
|
|
60
|
+
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors ${
|
|
61
|
+
active === i
|
|
62
|
+
? "font-semibold"
|
|
63
|
+
: "border-transparent text-[var(--cm-text-muted)] hover:text-[var(--cm-text-secondary)] hover:border-[var(--cm-border)]"
|
|
64
|
+
}`}
|
|
65
|
+
>
|
|
66
|
+
{tab.props.label}
|
|
67
|
+
</button>
|
|
86
68
|
))}
|
|
87
69
|
</div>
|
|
70
|
+
{tabs.map((tab, i) => (
|
|
71
|
+
<div
|
|
72
|
+
key={i}
|
|
73
|
+
role="tabpanel"
|
|
74
|
+
id={`tab-panel-${uid}-${i}`}
|
|
75
|
+
aria-labelledby={`tab-${uid}-${i}`}
|
|
76
|
+
hidden={active !== i}
|
|
77
|
+
className="px-4 pt-4 pb-2"
|
|
78
|
+
>
|
|
79
|
+
{tab.props.children}
|
|
80
|
+
</div>
|
|
81
|
+
))}
|
|
88
82
|
</div>
|
|
89
83
|
)
|
|
90
84
|
}
|
|
91
85
|
|
|
92
|
-
Tab.displayName = "Tab"
|
|
86
|
+
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
|
+
}
|
|
Binary file
|
|
Binary file
|