livemark 0.0.0-dev
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/LICENSE.md +9 -0
- package/README.md +3 -0
- package/build/actions/config/define.d.ts +6 -0
- package/build/actions/config/define.js +8 -0
- package/build/actions/config/define.unit.d.ts +1 -0
- package/build/actions/config/define.unit.js +34 -0
- package/build/actions/config/load.d.ts +5 -0
- package/build/actions/config/load.js +22 -0
- package/build/actions/config/load.unit.d.ts +1 -0
- package/build/actions/config/load.unit.js +23 -0
- package/build/commands/build.d.ts +5 -0
- package/build/commands/build.js +13 -0
- package/build/commands/preview.d.ts +5 -0
- package/build/commands/preview.js +18 -0
- package/build/commands/start.d.ts +5 -0
- package/build/commands/start.js +19 -0
- package/build/docs/markdown/includes/example.d.ts +9 -0
- package/build/docs/markdown/includes/example.js +5 -0
- package/build/entrypoints/main.d.ts +2 -0
- package/build/entrypoints/main.js +12 -0
- package/build/helpers/program.d.ts +2 -0
- package/build/helpers/program.js +5 -0
- package/build/index.d.ts +1 -0
- package/build/index.js +2 -0
- package/build/livemark.config.d.ts +9 -0
- package/build/livemark.config.js +5 -0
- package/build/models/config.d.ts +33 -0
- package/build/models/config.js +14 -0
- package/build/program.d.ts +2 -0
- package/build/program.js +14 -0
- package/build/vite.config.d.ts +2 -0
- package/build/vite.config.js +48 -0
- package/build/website/client.tsx +10 -0
- package/build/website/components/Abbr.tsx +17 -0
- package/build/website/components/AnsiCode.tsx +15 -0
- package/build/website/components/Banner.tsx +32 -0
- package/build/website/components/Breadcrumbs.tsx +19 -0
- package/build/website/components/Callout.tsx +72 -0
- package/build/website/components/Cards.tsx +66 -0
- package/build/website/components/CodeBlock.tsx +118 -0
- package/build/website/components/CodeTabs.tsx +83 -0
- package/build/website/components/Columns.tsx +17 -0
- package/build/website/components/Content.tsx +7 -0
- package/build/website/components/ContentTab.tsx +7 -0
- package/build/website/components/ContentTabs.tsx +82 -0
- package/build/website/components/DefaultCatchBoundary.tsx +54 -0
- package/build/website/components/DefinitionList.tsx +21 -0
- package/build/website/components/Details.tsx +30 -0
- package/build/website/components/FileTree.tsx +52 -0
- package/build/website/components/Footer.tsx +9 -0
- package/build/website/components/FootnoteRef.tsx +52 -0
- package/build/website/components/Heading.tsx +20 -0
- package/build/website/components/InlineBadge.tsx +9 -0
- package/build/website/components/InlineIcon.tsx +21 -0
- package/build/website/components/InlineToc.tsx +31 -0
- package/build/website/components/Layout.tsx +220 -0
- package/build/website/components/LinkButton.tsx +31 -0
- package/build/website/components/Mermaid.tsx +37 -0
- package/build/website/components/NotFound.tsx +27 -0
- package/build/website/components/PackageTabs.tsx +74 -0
- package/build/website/components/PageToolbar.tsx +65 -0
- package/build/website/components/PrevNext.tsx +52 -0
- package/build/website/components/SearchDialog.tsx +245 -0
- package/build/website/components/Section.tsx +24 -0
- package/build/website/components/SectionHeader.tsx +35 -0
- package/build/website/components/SiteTitle.tsx +16 -0
- package/build/website/components/SortableTable.tsx +81 -0
- package/build/website/components/SoundCloud.tsx +16 -0
- package/build/website/components/Theme.tsx +40 -0
- package/build/website/components/Toc.tsx +99 -0
- package/build/website/components/YouTube.tsx +16 -0
- package/build/website/components/ZoomImage.tsx +30 -0
- package/build/website/content-collections.ts +181 -0
- package/build/website/elements/accordion.tsx +78 -0
- package/build/website/elements/alert-dialog.tsx +185 -0
- package/build/website/elements/alert.tsx +74 -0
- package/build/website/elements/aspect-ratio.tsx +22 -0
- package/build/website/elements/avatar.tsx +107 -0
- package/build/website/elements/badge.tsx +51 -0
- package/build/website/elements/breadcrumb.tsx +120 -0
- package/build/website/elements/button-group.tsx +87 -0
- package/build/website/elements/button.tsx +57 -0
- package/build/website/elements/calendar.tsx +229 -0
- package/build/website/elements/card.tsx +102 -0
- package/build/website/elements/carousel.tsx +240 -0
- package/build/website/elements/chart.tsx +372 -0
- package/build/website/elements/checkbox.tsx +26 -0
- package/build/website/elements/collapsible.tsx +19 -0
- package/build/website/elements/command.tsx +210 -0
- package/build/website/elements/context-menu.tsx +270 -0
- package/build/website/elements/dialog.tsx +152 -0
- package/build/website/elements/direction.tsx +4 -0
- package/build/website/elements/drawer.tsx +127 -0
- package/build/website/elements/dropdown-menu.tsx +260 -0
- package/build/website/elements/empty.tsx +104 -0
- package/build/website/elements/field.tsx +235 -0
- package/build/website/elements/hover-card.tsx +49 -0
- package/build/website/elements/input-group.tsx +171 -0
- package/build/website/elements/input-otp.tsx +84 -0
- package/build/website/elements/input.tsx +18 -0
- package/build/website/elements/item.tsx +199 -0
- package/build/website/elements/kbd.tsx +26 -0
- package/build/website/elements/label.tsx +17 -0
- package/build/website/elements/menubar.tsx +282 -0
- package/build/website/elements/native-select.tsx +56 -0
- package/build/website/elements/navigation-menu.tsx +171 -0
- package/build/website/elements/pagination.tsx +133 -0
- package/build/website/elements/popover.tsx +88 -0
- package/build/website/elements/progress.tsx +81 -0
- package/build/website/elements/radio-group.tsx +36 -0
- package/build/website/elements/resizable.tsx +48 -0
- package/build/website/elements/scroll-area.tsx +51 -0
- package/build/website/elements/select.tsx +200 -0
- package/build/website/elements/separator.tsx +22 -0
- package/build/website/elements/sheet.tsx +130 -0
- package/build/website/elements/sidebar.tsx +717 -0
- package/build/website/elements/skeleton.tsx +13 -0
- package/build/website/elements/slider.tsx +57 -0
- package/build/website/elements/sonner.tsx +40 -0
- package/build/website/elements/spinner.tsx +16 -0
- package/build/website/elements/switch.tsx +30 -0
- package/build/website/elements/table.tsx +112 -0
- package/build/website/elements/tabs.tsx +79 -0
- package/build/website/elements/textarea.tsx +16 -0
- package/build/website/elements/toggle-group.tsx +87 -0
- package/build/website/elements/toggle.tsx +43 -0
- package/build/website/elements/tooltip.tsx +67 -0
- package/build/website/env.d.ts +10 -0
- package/build/website/globals.d.ts +8 -0
- package/build/website/helpers/article-icon.ts +59 -0
- package/build/website/helpers/articles.ts +24 -0
- package/build/website/helpers/resolve-includes.ts +131 -0
- package/build/website/helpers/resolve-includes.unit.ts +126 -0
- package/build/website/helpers/search-text.ts +15 -0
- package/build/website/helpers/style.ts +6 -0
- package/build/website/helpers/theme.ts +27 -0
- package/build/website/helpers/toc-context.ts +10 -0
- package/build/website/helpers/toc.ts +51 -0
- package/build/website/helpers/toc.unit.ts +74 -0
- package/build/website/hooks/in-view.ts +30 -0
- package/build/website/hooks/media.ts +19 -0
- package/build/website/models/article.ts +40 -0
- package/build/website/plugins/rehype-inline-code.ts +107 -0
- package/build/website/plugins/remark-abbr.ts +46 -0
- package/build/website/plugins/remark-abbr.unit.ts +99 -0
- package/build/website/plugins/remark-ansi.ts +247 -0
- package/build/website/plugins/remark-ansi.unit.ts +197 -0
- package/build/website/plugins/remark-badge.ts +50 -0
- package/build/website/plugins/remark-badge.unit.ts +96 -0
- package/build/website/plugins/remark-button.ts +60 -0
- package/build/website/plugins/remark-button.unit.ts +147 -0
- package/build/website/plugins/remark-callout.ts +57 -0
- package/build/website/plugins/remark-card.ts +80 -0
- package/build/website/plugins/remark-code-tabs.ts +87 -0
- package/build/website/plugins/remark-columns.ts +66 -0
- package/build/website/plugins/remark-custom-heading-id.ts +70 -0
- package/build/website/plugins/remark-custom-heading-id.unit.ts +131 -0
- package/build/website/plugins/remark-deflist.ts +36 -0
- package/build/website/plugins/remark-details.ts +49 -0
- package/build/website/plugins/remark-filetree.ts +89 -0
- package/build/website/plugins/remark-filetree.unit.ts +137 -0
- package/build/website/plugins/remark-github-callout.ts +71 -0
- package/build/website/plugins/remark-icon.ts +42 -0
- package/build/website/plugins/remark-image.ts +25 -0
- package/build/website/plugins/remark-media.ts +62 -0
- package/build/website/plugins/remark-media.unit.ts +74 -0
- package/build/website/plugins/remark-mermaid.ts +23 -0
- package/build/website/plugins/remark-npm.ts +52 -0
- package/build/website/plugins/remark-steps.ts +36 -0
- package/build/website/plugins/remark-steps.unit.ts +97 -0
- package/build/website/plugins/remark-tab.ts +100 -0
- package/build/website/plugins/remark-tab.unit.ts +181 -0
- package/build/website/plugins/remark-toc.ts +39 -0
- package/build/website/plugins/remark-toc.unit.ts +82 -0
- package/build/website/plugins/shiki-icon.ts +82 -0
- package/build/website/plugins/shiki-line-highlight.ts +174 -0
- package/build/website/public/logo.svg +72 -0
- package/build/website/routeTree.gen.ts +86 -0
- package/build/website/router.tsx +34 -0
- package/build/website/routes/$.tsx +149 -0
- package/build/website/routes/__root.tsx +84 -0
- package/build/website/routes/index.tsx +13 -0
- package/build/website/server.ts +7 -0
- package/build/website/styles/general.css +347 -0
- package/build/website/vite.config.ts +36 -0
- package/entrypoints/main.ts +12 -0
- package/package.json +120 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Link } from "@tanstack/react-router"
|
|
2
|
+
import { icons } from "lucide-react"
|
|
3
|
+
import {
|
|
4
|
+
Card as CardElement,
|
|
5
|
+
CardContent,
|
|
6
|
+
CardHeader,
|
|
7
|
+
CardTitle,
|
|
8
|
+
} from "../elements/card.tsx"
|
|
9
|
+
|
|
10
|
+
function toPascalCase(name: string) {
|
|
11
|
+
return name
|
|
12
|
+
.split("-")
|
|
13
|
+
.map(s => s.charAt(0).toUpperCase() + s.slice(1))
|
|
14
|
+
.join("")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Grid container for card components */
|
|
18
|
+
export function Cards(props: { children: React.ReactNode }) {
|
|
19
|
+
return (
|
|
20
|
+
<div className="not-prose grid grid-cols-1 gap-4 my-4 sm:grid-cols-2">
|
|
21
|
+
{props.children}
|
|
22
|
+
</div>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Card component with optional icon, title, description, and link */
|
|
27
|
+
export function Card(props: {
|
|
28
|
+
title: string
|
|
29
|
+
href?: string
|
|
30
|
+
icon?: string
|
|
31
|
+
children?: React.ReactNode
|
|
32
|
+
}) {
|
|
33
|
+
const Icon = props.icon
|
|
34
|
+
? icons[toPascalCase(props.icon) as keyof typeof icons]
|
|
35
|
+
: undefined
|
|
36
|
+
|
|
37
|
+
const card = (
|
|
38
|
+
<CardElement size="sm" className="transition-colors hover:bg-accent">
|
|
39
|
+
<CardHeader>
|
|
40
|
+
<CardTitle className="flex items-center gap-2">
|
|
41
|
+
{Icon && <Icon className="size-5 text-primary" />}
|
|
42
|
+
{props.title}
|
|
43
|
+
</CardTitle>
|
|
44
|
+
</CardHeader>
|
|
45
|
+
{props.children && (
|
|
46
|
+
<CardContent>
|
|
47
|
+
<div className="text-muted-foreground text-sm">{props.children}</div>
|
|
48
|
+
</CardContent>
|
|
49
|
+
)}
|
|
50
|
+
</CardElement>
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if (props.href?.startsWith("http")) {
|
|
54
|
+
return (
|
|
55
|
+
<a href={props.href} target="_blank" rel="noopener noreferrer">
|
|
56
|
+
{card}
|
|
57
|
+
</a>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (props.href) {
|
|
62
|
+
return <Link to={props.href}>{card}</Link>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return card
|
|
66
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Check, ChevronDown, Clipboard, WrapText } from "lucide-react"
|
|
2
|
+
import { useRef, useState } from "react"
|
|
3
|
+
|
|
4
|
+
export function CodeBlock(props: React.ComponentProps<"pre">) {
|
|
5
|
+
const [copied, setCopied] = useState(false)
|
|
6
|
+
const [wrap, setWrap] = useState(false)
|
|
7
|
+
const [expanded, setExpanded] = useState(false)
|
|
8
|
+
const preRef = useRef<HTMLPreElement>(null)
|
|
9
|
+
const extra = props as Record<string, unknown>
|
|
10
|
+
const meta = extra["data-meta"] as string | undefined
|
|
11
|
+
const title = meta?.match(/title="([^"]+)"/)?.[1]
|
|
12
|
+
const maxLines = meta?.match(/maxLines=(\d+)/)?.[1]
|
|
13
|
+
const icon = extra.icon as string | undefined
|
|
14
|
+
const style = props.style as Record<string, string> | undefined
|
|
15
|
+
const collapsedHeight = maxLines
|
|
16
|
+
? `${Number(maxLines) * 1.5 + 1.75}rem`
|
|
17
|
+
: undefined
|
|
18
|
+
|
|
19
|
+
const handleCopy = () => {
|
|
20
|
+
const text = preRef.current?.textContent ?? ""
|
|
21
|
+
navigator.clipboard.writeText(text)
|
|
22
|
+
setCopied(true)
|
|
23
|
+
setTimeout(() => setCopied(false), 2000)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const handleWrapToggle = () => {
|
|
27
|
+
setWrap(prev => !prev)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<figure
|
|
32
|
+
className="shiki not-prose my-4 relative rounded-xl border shadow-sm overflow-hidden text-sm bg-sidebar dark:bg-sidebar"
|
|
33
|
+
style={{ ...style, backgroundColor: undefined }}
|
|
34
|
+
>
|
|
35
|
+
{title ? (
|
|
36
|
+
<div className="flex items-center gap-2 h-9.5 border-b px-4 text-muted-foreground">
|
|
37
|
+
{icon && (
|
|
38
|
+
<span
|
|
39
|
+
className="size-3.5 [&>svg]:size-full"
|
|
40
|
+
dangerouslySetInnerHTML={{ __html: icon }}
|
|
41
|
+
/>
|
|
42
|
+
)}
|
|
43
|
+
<span className="flex-1 truncate text-xs font-medium">{title}</span>
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
onClick={handleWrapToggle}
|
|
47
|
+
className={`-me-2 p-1 rounded-md hover:text-foreground transition-colors ${wrap ? "text-foreground" : ""}`}
|
|
48
|
+
aria-label="Toggle word wrap"
|
|
49
|
+
>
|
|
50
|
+
<WrapText className="size-3.5" />
|
|
51
|
+
</button>
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
onClick={handleCopy}
|
|
55
|
+
className="-me-2 p-1 rounded-md hover:text-foreground transition-colors"
|
|
56
|
+
aria-label="Copy code"
|
|
57
|
+
>
|
|
58
|
+
{copied ? (
|
|
59
|
+
<Check className="size-3.5" />
|
|
60
|
+
) : (
|
|
61
|
+
<Clipboard className="size-3.5" />
|
|
62
|
+
)}
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
65
|
+
) : (
|
|
66
|
+
<div className="empty:hidden absolute top-2 right-2 z-10 backdrop-blur-lg rounded-lg text-muted-foreground">
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
onClick={handleWrapToggle}
|
|
70
|
+
className={`p-1 rounded-md hover:text-foreground transition-colors ${wrap ? "text-foreground" : ""}`}
|
|
71
|
+
aria-label="Toggle word wrap"
|
|
72
|
+
>
|
|
73
|
+
<WrapText className="size-3.5" />
|
|
74
|
+
</button>
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
onClick={handleCopy}
|
|
78
|
+
className="p-1 rounded-md hover:text-foreground transition-colors"
|
|
79
|
+
aria-label="Copy code"
|
|
80
|
+
>
|
|
81
|
+
{copied ? (
|
|
82
|
+
<Check className="size-3.5" />
|
|
83
|
+
) : (
|
|
84
|
+
<Clipboard className="size-3.5" />
|
|
85
|
+
)}
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
<div
|
|
90
|
+
className="text-[0.8125rem] py-3.5 overflow-auto transition-[max-height] duration-300"
|
|
91
|
+
style={{
|
|
92
|
+
maxHeight: collapsedHeight && !expanded ? collapsedHeight : "600px",
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
<pre
|
|
96
|
+
{...props}
|
|
97
|
+
ref={preRef}
|
|
98
|
+
style={undefined}
|
|
99
|
+
className={
|
|
100
|
+
wrap
|
|
101
|
+
? "min-w-full whitespace-pre-wrap break-words [&>code]:flex [&>code]:flex-col"
|
|
102
|
+
: "min-w-full w-max [&>code]:flex [&>code]:flex-col"
|
|
103
|
+
}
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
{collapsedHeight && !expanded && (
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
onClick={() => setExpanded(true)}
|
|
110
|
+
className="flex items-center justify-center gap-1 w-full py-1.5 text-xs text-muted-foreground hover:text-foreground border-t cursor-pointer transition-colors"
|
|
111
|
+
>
|
|
112
|
+
<ChevronDown className="size-3.5" />
|
|
113
|
+
Expand
|
|
114
|
+
</button>
|
|
115
|
+
)}
|
|
116
|
+
</figure>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Children, useEffect, useState } from "react"
|
|
2
|
+
|
|
3
|
+
/** Tabbed code block component for grouping multiple code blocks with optional sync */
|
|
4
|
+
export function CodeTabs(props: {
|
|
5
|
+
tabs: string
|
|
6
|
+
sync?: string
|
|
7
|
+
children: React.ReactNode
|
|
8
|
+
}) {
|
|
9
|
+
const names = JSON.parse(props.tabs) as string[]
|
|
10
|
+
const children = Children.toArray(props.children)
|
|
11
|
+
|
|
12
|
+
const getInitialIndex = () => {
|
|
13
|
+
if (!props.sync) return 0
|
|
14
|
+
const saved =
|
|
15
|
+
typeof window !== "undefined"
|
|
16
|
+
? localStorage.getItem("livemark-tab-" + props.sync)
|
|
17
|
+
: null
|
|
18
|
+
if (!saved) return 0
|
|
19
|
+
const idx = names.indexOf(saved)
|
|
20
|
+
return idx >= 0 ? idx : 0
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const [active, setActive] = useState(getInitialIndex)
|
|
24
|
+
|
|
25
|
+
const handleSelect = (i: number) => {
|
|
26
|
+
setActive(i)
|
|
27
|
+
if (props.sync) {
|
|
28
|
+
localStorage.setItem("livemark-tab-" + props.sync, names[i]!)
|
|
29
|
+
window.dispatchEvent(
|
|
30
|
+
new CustomEvent("livemark-tab-sync", {
|
|
31
|
+
detail: { sync: props.sync, title: names[i] },
|
|
32
|
+
}),
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!props.sync) return
|
|
39
|
+
const handler = (e: Event) => {
|
|
40
|
+
const { sync, title } = (
|
|
41
|
+
e as CustomEvent<{ sync: string; title: string }>
|
|
42
|
+
).detail
|
|
43
|
+
if (sync !== props.sync) return
|
|
44
|
+
const idx = names.indexOf(title)
|
|
45
|
+
if (idx >= 0) setActive(idx)
|
|
46
|
+
}
|
|
47
|
+
window.addEventListener("livemark-tab-sync", handler)
|
|
48
|
+
return () => window.removeEventListener("livemark-tab-sync", handler)
|
|
49
|
+
}, [props.sync, names])
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="not-prose my-4 rounded-xl border shadow-sm overflow-hidden text-sm bg-sidebar dark:bg-sidebar">
|
|
53
|
+
<div className="flex border-b">
|
|
54
|
+
{names.map((name, i) => (
|
|
55
|
+
<button
|
|
56
|
+
key={name}
|
|
57
|
+
type="button"
|
|
58
|
+
onClick={() => handleSelect(i)}
|
|
59
|
+
className={`px-4 py-2 text-xs font-medium transition-colors cursor-pointer ${
|
|
60
|
+
i === active
|
|
61
|
+
? "text-foreground border-b-2 border-primary -mb-px"
|
|
62
|
+
: "text-muted-foreground hover:text-foreground"
|
|
63
|
+
}`}
|
|
64
|
+
>
|
|
65
|
+
{name}
|
|
66
|
+
</button>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
{children.map((child, i) => (
|
|
70
|
+
<div
|
|
71
|
+
key={names[i]}
|
|
72
|
+
className={
|
|
73
|
+
i === active
|
|
74
|
+
? "[&>figure]:my-0 [&>figure]:rounded-none [&>figure]:border-0 [&>figure]:shadow-none"
|
|
75
|
+
: "hidden"
|
|
76
|
+
}
|
|
77
|
+
>
|
|
78
|
+
{child}
|
|
79
|
+
</div>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Grid layout for arranging content side by side */
|
|
2
|
+
export function Columns(props: { cols?: string; children: React.ReactNode }) {
|
|
3
|
+
const cols = props.cols ? parseInt(props.cols, 10) : undefined
|
|
4
|
+
const style = cols
|
|
5
|
+
? { gridTemplateColumns: `repeat(${cols}, 1fr)` }
|
|
6
|
+
: undefined
|
|
7
|
+
return (
|
|
8
|
+
<div className="grid gap-6 my-4 sm:grid-cols-2" style={style}>
|
|
9
|
+
{props.children}
|
|
10
|
+
</div>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Single column within a Columns grid */
|
|
15
|
+
export function Column(props: { children: React.ReactNode }) {
|
|
16
|
+
return <div className="max-w-none">{props.children}</div>
|
|
17
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Children, isValidElement, useEffect, useState } from "react"
|
|
2
|
+
|
|
3
|
+
/** Tabbed container component for grouping content tabs with optional sync across groups */
|
|
4
|
+
export function ContentTabs(props: {
|
|
5
|
+
sync?: string
|
|
6
|
+
children: React.ReactNode
|
|
7
|
+
}) {
|
|
8
|
+
const children = Children.toArray(props.children)
|
|
9
|
+
const titles = children.map(child =>
|
|
10
|
+
isValidElement<{ title: string }>(child) ? child.props.title : "",
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
const getInitialIndex = () => {
|
|
14
|
+
if (!props.sync) return 0
|
|
15
|
+
const saved =
|
|
16
|
+
typeof window !== "undefined"
|
|
17
|
+
? localStorage.getItem("livemark-tab-" + props.sync)
|
|
18
|
+
: null
|
|
19
|
+
if (!saved) return 0
|
|
20
|
+
const idx = titles.indexOf(saved)
|
|
21
|
+
return idx >= 0 ? idx : 0
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const [active, setActive] = useState(getInitialIndex)
|
|
25
|
+
|
|
26
|
+
const handleSelect = (i: number) => {
|
|
27
|
+
setActive(i)
|
|
28
|
+
if (props.sync) {
|
|
29
|
+
localStorage.setItem("livemark-tab-" + props.sync, titles[i]!)
|
|
30
|
+
window.dispatchEvent(
|
|
31
|
+
new CustomEvent("livemark-tab-sync", {
|
|
32
|
+
detail: { sync: props.sync, title: titles[i] },
|
|
33
|
+
}),
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!props.sync) return
|
|
40
|
+
const handler = (e: Event) => {
|
|
41
|
+
const { sync, title } = (
|
|
42
|
+
e as CustomEvent<{ sync: string; title: string }>
|
|
43
|
+
).detail
|
|
44
|
+
if (sync !== props.sync) return
|
|
45
|
+
const idx = titles.indexOf(title)
|
|
46
|
+
if (idx >= 0) setActive(idx)
|
|
47
|
+
}
|
|
48
|
+
window.addEventListener("livemark-tab-sync", handler)
|
|
49
|
+
return () => window.removeEventListener("livemark-tab-sync", handler)
|
|
50
|
+
}, [props.sync, titles])
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className="not-prose my-4 rounded-xl border bg-card">
|
|
54
|
+
<div className="flex border-b">
|
|
55
|
+
{titles.map((title, i) => (
|
|
56
|
+
<button
|
|
57
|
+
key={title}
|
|
58
|
+
type="button"
|
|
59
|
+
onClick={() => handleSelect(i)}
|
|
60
|
+
className={`px-4 py-2 text-sm font-medium transition-colors cursor-pointer ${
|
|
61
|
+
i === active
|
|
62
|
+
? "text-foreground border-b-2 border-primary -mb-px"
|
|
63
|
+
: "text-muted-foreground hover:text-foreground"
|
|
64
|
+
}`}
|
|
65
|
+
>
|
|
66
|
+
{title}
|
|
67
|
+
</button>
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
{children.map((child, i) => (
|
|
71
|
+
<div
|
|
72
|
+
key={titles[i]}
|
|
73
|
+
className={
|
|
74
|
+
i === active ? "p-4 prose dark:prose-invert max-w-none" : "hidden"
|
|
75
|
+
}
|
|
76
|
+
>
|
|
77
|
+
{child}
|
|
78
|
+
</div>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ErrorComponentProps } from "@tanstack/react-router"
|
|
2
|
+
import {
|
|
3
|
+
ErrorComponent,
|
|
4
|
+
Link,
|
|
5
|
+
rootRouteId,
|
|
6
|
+
useMatch,
|
|
7
|
+
useRouter,
|
|
8
|
+
} from "@tanstack/react-router"
|
|
9
|
+
|
|
10
|
+
export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
|
|
11
|
+
const router = useRouter()
|
|
12
|
+
const isRoot = useMatch({
|
|
13
|
+
strict: false,
|
|
14
|
+
select: state => state.id === rootRouteId,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
console.error(error)
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="min-h-[60vh] flex flex-col items-center justify-center gap-6 px-4">
|
|
21
|
+
<ErrorComponent error={error} />
|
|
22
|
+
<div className="flex gap-3 items-center flex-wrap">
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
onClick={() => {
|
|
26
|
+
router.invalidate()
|
|
27
|
+
}}
|
|
28
|
+
className="bg-primary text-primary-foreground px-4 py-2 text-sm font-medium hover:bg-primary/80 transition-colors"
|
|
29
|
+
>
|
|
30
|
+
Try Again
|
|
31
|
+
</button>
|
|
32
|
+
{isRoot ? (
|
|
33
|
+
<Link
|
|
34
|
+
to="/"
|
|
35
|
+
className="border border-border px-4 py-2 text-sm font-medium hover:bg-card transition-colors"
|
|
36
|
+
>
|
|
37
|
+
Home
|
|
38
|
+
</Link>
|
|
39
|
+
) : (
|
|
40
|
+
<Link
|
|
41
|
+
to="/"
|
|
42
|
+
className="border border-border px-4 py-2 text-sm font-medium hover:bg-card transition-colors"
|
|
43
|
+
onClick={(e: any) => {
|
|
44
|
+
e.preventDefault()
|
|
45
|
+
window.history.back()
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
Go Back
|
|
49
|
+
</Link>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ComponentProps } from "react"
|
|
2
|
+
|
|
3
|
+
/** Styled wrapper for definition lists produced by remark-definition-list */
|
|
4
|
+
export function DefinitionList(props: ComponentProps<"dl">) {
|
|
5
|
+
return <dl {...props} className="not-prose space-y-4" />
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Styled definition term rendered as bold text */
|
|
9
|
+
export function DefinitionTerm(props: ComponentProps<"dt">) {
|
|
10
|
+
return <dt {...props} className="text-foreground font-semibold" />
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Styled definition detail with a left border accent */
|
|
14
|
+
export function DefinitionDetail(props: ComponentProps<"dd">) {
|
|
15
|
+
return (
|
|
16
|
+
<dd
|
|
17
|
+
{...props}
|
|
18
|
+
className="ml-0 pl-4 border-l-2 border-border text-muted-foreground"
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useState } from "react"
|
|
2
|
+
import { ChevronRight } from "lucide-react"
|
|
3
|
+
import {
|
|
4
|
+
Collapsible,
|
|
5
|
+
CollapsibleTrigger,
|
|
6
|
+
CollapsibleContent,
|
|
7
|
+
} from "../elements/collapsible.tsx"
|
|
8
|
+
|
|
9
|
+
/** Collapsible content section with a summary label */
|
|
10
|
+
export function Details(props: { summary: string; children: React.ReactNode }) {
|
|
11
|
+
const [open, setOpen] = useState(false)
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Collapsible
|
|
15
|
+
open={open}
|
|
16
|
+
onOpenChange={setOpen}
|
|
17
|
+
className="not-prose my-4 rounded-xl border border-border bg-card px-4 py-3"
|
|
18
|
+
>
|
|
19
|
+
<CollapsibleTrigger className="flex w-full cursor-pointer items-center gap-2 text-sm font-semibold select-none">
|
|
20
|
+
<ChevronRight
|
|
21
|
+
className={`size-4 shrink-0 transition-transform ${open ? "rotate-90" : ""}`}
|
|
22
|
+
/>
|
|
23
|
+
{props.summary}
|
|
24
|
+
</CollapsibleTrigger>
|
|
25
|
+
<CollapsibleContent className="mt-3 text-sm [&>p]:my-1 [&>p:first-child]:mt-0 [&>p:last-child]:mb-0">
|
|
26
|
+
{props.children}
|
|
27
|
+
</CollapsibleContent>
|
|
28
|
+
</Collapsible>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { File, Folder } from "lucide-react"
|
|
2
|
+
|
|
3
|
+
interface TreeItem {
|
|
4
|
+
name: string
|
|
5
|
+
isDir: boolean
|
|
6
|
+
children?: TreeItem[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Renders a file/directory tree from a JSON-serialized tree structure */
|
|
10
|
+
export function FileTree(props: { tree: string }) {
|
|
11
|
+
const items: TreeItem[] = JSON.parse(props.tree)
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className="not-prose my-4 rounded-xl border border-border bg-card p-4 font-mono text-sm">
|
|
15
|
+
<TreeItems items={items} depth={0} />
|
|
16
|
+
</div>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function TreeItems(props: { items: TreeItem[]; depth: number }) {
|
|
21
|
+
return (
|
|
22
|
+
<ul className={props.depth > 0 ? "ml-4 border-l border-border pl-2" : ""}>
|
|
23
|
+
{props.items.map((item, index) => (
|
|
24
|
+
<TreeNode
|
|
25
|
+
key={`${item.name}-${index}`}
|
|
26
|
+
item={item}
|
|
27
|
+
depth={props.depth}
|
|
28
|
+
/>
|
|
29
|
+
))}
|
|
30
|
+
</ul>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function TreeNode(props: { item: TreeItem; depth: number }) {
|
|
35
|
+
const { item, depth } = props
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<li className="py-0.5">
|
|
39
|
+
<span className="inline-flex items-center gap-2">
|
|
40
|
+
{item.isDir ? (
|
|
41
|
+
<Folder className="size-4 shrink-0 text-primary" />
|
|
42
|
+
) : (
|
|
43
|
+
<File className="size-4 shrink-0 text-muted-foreground" />
|
|
44
|
+
)}
|
|
45
|
+
<span className={item.isDir ? "font-semibold" : ""}>{item.name}</span>
|
|
46
|
+
</span>
|
|
47
|
+
{item.children && item.children.length > 0 && (
|
|
48
|
+
<TreeItems items={item.children} depth={depth + 1} />
|
|
49
|
+
)}
|
|
50
|
+
</li>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function Footer() {
|
|
2
|
+
return (
|
|
3
|
+
<footer className="border-t bg-card">
|
|
4
|
+
<div className="w-full max-w-7xl mx-auto px-4 md:px-10 py-4 flex items-center justify-center text-xs text-muted-foreground">
|
|
5
|
+
<span>© {new Date().getFullYear()} My Project</span>
|
|
6
|
+
</div>
|
|
7
|
+
</footer>
|
|
8
|
+
)
|
|
9
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type ComponentProps, useEffect, useState } from "react"
|
|
2
|
+
import {
|
|
3
|
+
HoverCard,
|
|
4
|
+
HoverCardContent,
|
|
5
|
+
HoverCardTrigger,
|
|
6
|
+
} from "../elements/hover-card.tsx"
|
|
7
|
+
|
|
8
|
+
const FOOTNOTE_PREFIX = "#user-content-fn-"
|
|
9
|
+
|
|
10
|
+
/** Wraps footnote reference links in a HoverCard that previews footnote content inline */
|
|
11
|
+
export function FootnoteRef(props: ComponentProps<"a">) {
|
|
12
|
+
const href = props.href ?? ""
|
|
13
|
+
if (!href.startsWith(FOOTNOTE_PREFIX)) {
|
|
14
|
+
return <a {...props} />
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return <FootnoteHoverCard {...props} href={href} />
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function FootnoteHoverCard(props: ComponentProps<"a"> & { href: string }) {
|
|
21
|
+
const [footnoteHtml, setFootnoteHtml] = useState("")
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const targetId = props.href.slice(1)
|
|
25
|
+
const el = document.getElementById(targetId)
|
|
26
|
+
if (!el) return
|
|
27
|
+
|
|
28
|
+
const wrapper = document.createElement("div")
|
|
29
|
+
wrapper.innerHTML = el.innerHTML
|
|
30
|
+
for (const backref of wrapper.querySelectorAll("[data-footnote-backref]")) {
|
|
31
|
+
backref.remove()
|
|
32
|
+
}
|
|
33
|
+
setFootnoteHtml(wrapper.innerHTML)
|
|
34
|
+
}, [props.href])
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<HoverCard>
|
|
38
|
+
<HoverCardTrigger render={<a {...props} />} />
|
|
39
|
+
<HoverCardContent
|
|
40
|
+
className="prose dark:prose-invert text-sm w-80 [&>p]:my-1 [&>p:first-child]:mt-0 [&>p:last-child]:mb-0"
|
|
41
|
+
side="top"
|
|
42
|
+
sideOffset={8}
|
|
43
|
+
>
|
|
44
|
+
{footnoteHtml ? (
|
|
45
|
+
<div dangerouslySetInnerHTML={{ __html: footnoteHtml }} />
|
|
46
|
+
) : (
|
|
47
|
+
<p className="text-muted-foreground">Footnote not found</p>
|
|
48
|
+
)}
|
|
49
|
+
</HoverCardContent>
|
|
50
|
+
</HoverCard>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Heading component that wraps content in an anchor link to itself */
|
|
2
|
+
function createHeading(Tag: "h1" | "h2" | "h3" | "h4" | "h5" | "h6") {
|
|
3
|
+
return function Heading(props: React.HTMLAttributes<HTMLHeadingElement>) {
|
|
4
|
+
if (!props.id) return <Tag {...props} />
|
|
5
|
+
return (
|
|
6
|
+
<Tag {...props}>
|
|
7
|
+
<a href={`#${props.id}`}>{props.children}</a>
|
|
8
|
+
</Tag>
|
|
9
|
+
)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const headingComponents = {
|
|
14
|
+
h1: createHeading("h1"),
|
|
15
|
+
h2: createHeading("h2"),
|
|
16
|
+
h3: createHeading("h3"),
|
|
17
|
+
h4: createHeading("h4"),
|
|
18
|
+
h5: createHeading("h5"),
|
|
19
|
+
h6: createHeading("h6"),
|
|
20
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Badge } from "../elements/badge.tsx"
|
|
2
|
+
|
|
3
|
+
/** Inline badge rendered from :badge text directives */
|
|
4
|
+
export function InlineBadge(props: {
|
|
5
|
+
label: string
|
|
6
|
+
variant?: "default" | "secondary" | "destructive" | "outline"
|
|
7
|
+
}) {
|
|
8
|
+
return <Badge variant={props.variant}>{props.label}</Badge>
|
|
9
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { icons } from "lucide-react"
|
|
2
|
+
|
|
3
|
+
function toPascalCase(name: string) {
|
|
4
|
+
return name
|
|
5
|
+
.split("-")
|
|
6
|
+
.map(s => s.charAt(0).toUpperCase() + s.slice(1))
|
|
7
|
+
.join("")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Renders a Lucide icon inline by name */
|
|
11
|
+
export function InlineIcon(props: { name: string; className?: string }) {
|
|
12
|
+
const key = toPascalCase(props.name)
|
|
13
|
+
if (!(key in icons)) return null
|
|
14
|
+
const Icon = icons[key as keyof typeof icons]
|
|
15
|
+
|
|
16
|
+
const className = props.className
|
|
17
|
+
? `inline-block size-4 align-text-bottom ${props.className}`
|
|
18
|
+
: "inline-block size-4 align-text-bottom"
|
|
19
|
+
|
|
20
|
+
return <Icon className={className} />
|
|
21
|
+
}
|