livemark 0.0.0-dev → 0.7.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/README.md +5 -1
- package/build/actions/changelog/build.d.ts +14 -0
- package/build/actions/changelog/build.js +158 -0
- package/build/actions/changelog/build.unit.d.ts +1 -0
- package/build/actions/changelog/build.unit.js +66 -0
- package/build/actions/config/define.unit.js +11 -16
- package/build/actions/config/load.js +6 -3
- package/build/actions/config/load.unit.js +3 -3
- package/build/commands/build.js +4 -3
- package/build/commands/escape.d.ts +9 -0
- package/build/commands/escape.js +56 -0
- package/build/docs/markdown/includes/example.d.ts +53 -4
- package/build/docs/markdown/includes/example.js +2 -2
- package/build/livemark.config.d.ts +53 -4
- package/build/livemark.config.js +22 -2
- package/build/models/config.d.ts +315 -8
- package/build/models/config.js +25 -5
- package/build/models/link.d.ts +13 -0
- package/build/models/link.js +9 -0
- package/build/models/patch.d.ts +21 -0
- package/build/models/patch.js +7 -0
- package/build/models/section.d.ts +75 -0
- package/build/models/section.js +26 -0
- package/build/models/theme.d.ts +72 -0
- package/build/models/theme.js +73 -0
- package/build/program.js +4 -2
- package/build/vite.config.d.ts +1 -1
- package/build/website/assets/logo.svg +117 -0
- package/build/website/components/Article.tsx +172 -0
- package/build/website/components/BackToTop.tsx +32 -0
- package/build/website/components/Banner.tsx +2 -2
- package/build/website/components/BlogIndex.tsx +121 -0
- package/build/website/components/BlogSidebar.tsx +162 -0
- package/build/website/components/Breadcrumbs.tsx +5 -2
- package/build/website/components/Card.tsx +48 -0
- package/build/website/components/Cards.tsx +0 -58
- package/build/website/components/ChangelogIndex.tsx +111 -0
- package/build/website/components/ChangelogSidebar.tsx +120 -0
- package/build/website/components/CodeBlock.tsx +57 -40
- package/build/website/components/Column.tsx +4 -0
- package/build/website/components/Columns.tsx +0 -5
- package/build/website/components/Footer.tsx +12 -5
- package/build/website/components/InlineIcon.tsx +2 -13
- package/build/website/components/Layout.tsx +155 -185
- package/build/website/components/Mermaid.tsx +14 -33
- package/build/website/components/MermaidRenderer.tsx +37 -0
- package/build/website/components/MiniSidebar.tsx +49 -0
- package/build/website/components/PageToolbar.tsx +21 -11
- package/build/website/components/PrevNext.tsx +20 -12
- package/build/website/components/Search.tsx +28 -0
- package/build/website/components/SearchDialog.tsx +10 -10
- package/build/website/components/Sidebar.tsx +160 -0
- package/build/website/components/SidebarLinks.tsx +70 -0
- package/build/website/components/SidebarSections.tsx +71 -0
- package/build/website/components/SiteTitle.tsx +13 -8
- package/build/website/components/TagIndex.tsx +120 -0
- package/build/website/components/Theme.tsx +0 -1
- package/build/website/components/Toc.tsx +128 -1
- package/build/website/components/ZoomImage.tsx +15 -20
- package/build/website/content/article.ts +78 -0
- package/build/website/content/tag.ts +29 -0
- package/build/website/content-collections.ts +53 -26
- package/build/website/elements/sidebar.tsx +4 -4
- package/build/website/env.d.ts +4 -0
- package/build/website/helpers/article-icon.ts +2 -0
- package/build/website/helpers/article.ts +90 -0
- package/build/website/helpers/changelog-source.ts +16 -0
- package/build/website/helpers/changelog-source.unit.ts +38 -0
- package/build/website/helpers/dynamic-icon-resolver.tsx +19 -0
- package/build/website/helpers/dynamic-icon.tsx +12 -0
- package/build/website/helpers/favicon.ts +7 -0
- package/build/website/helpers/model.ts +4 -0
- package/build/website/helpers/resolve-asset-path.ts +9 -0
- package/build/website/helpers/section.ts +50 -0
- package/build/website/helpers/section.unit.ts +71 -0
- package/build/website/helpers/toc-context.ts +1 -1
- package/build/website/helpers/toc.ts +1 -6
- package/build/website/models/article.ts +48 -33
- package/build/website/models/toc.ts +5 -0
- package/build/website/plugins/rehype-task-list-a11y.ts +33 -0
- package/build/website/plugins/remark-ansi.ts +1 -0
- package/build/website/plugins/remark-image.ts +22 -13
- package/build/website/plugins/vite-livemark.ts +91 -0
- package/build/website/routeTree.gen.ts +9 -9
- package/build/website/routes/$.tsx +74 -131
- package/build/website/routes/__root.tsx +36 -23
- package/build/website/styles/general.css +26 -210
- package/build/website/styles/markdown.css +214 -0
- package/build/website/vite.config.ts +38 -3
- package/package.json +25 -16
- package/build/website/globals.d.ts +0 -8
- package/build/website/helpers/articles.ts +0 -24
- package/build/website/public/logo.svg +0 -72
- package/build/website/routes/index.tsx +0 -13
|
@@ -1,21 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
function toPascalCase(name: string) {
|
|
4
|
-
return name
|
|
5
|
-
.split("-")
|
|
6
|
-
.map(s => s.charAt(0).toUpperCase() + s.slice(1))
|
|
7
|
-
.join("")
|
|
8
|
-
}
|
|
1
|
+
import { DynamicIcon } from "../helpers/dynamic-icon.tsx"
|
|
9
2
|
|
|
10
3
|
/** Renders a Lucide icon inline by name */
|
|
11
4
|
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
5
|
const className = props.className
|
|
17
6
|
? `inline-block size-4 align-text-bottom ${props.className}`
|
|
18
7
|
: "inline-block size-4 align-text-bottom"
|
|
19
8
|
|
|
20
|
-
return <
|
|
9
|
+
return <DynamicIcon name={props.name} className={className} />
|
|
21
10
|
}
|
|
@@ -1,46 +1,42 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Link, useLocation } from "@tanstack/react-router"
|
|
1
|
+
import { Link, useLocation, useMatch } from "@tanstack/react-router"
|
|
3
2
|
import { useHotkey } from "@tanstack/react-hotkeys"
|
|
3
|
+
import { ExternalLink } from "lucide-react"
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import type { ArticleNode } from "../models/article.ts"
|
|
5
|
+
currentSection,
|
|
6
|
+
sectionFirstArticle,
|
|
7
|
+
sectionFlatArticles,
|
|
8
|
+
sortedArticles,
|
|
9
|
+
} from "../content/article.ts"
|
|
10
|
+
import { DynamicIcon } from "../helpers/dynamic-icon.tsx"
|
|
11
|
+
import { sectionIcon } from "../helpers/section.ts"
|
|
13
12
|
import {
|
|
14
|
-
Collapsible,
|
|
15
|
-
CollapsibleContent,
|
|
16
|
-
CollapsibleTrigger,
|
|
17
|
-
} from "../elements/collapsible.tsx"
|
|
18
|
-
import {
|
|
19
|
-
Sidebar,
|
|
20
|
-
SidebarContent,
|
|
21
|
-
SidebarFooter,
|
|
22
|
-
SidebarGroup,
|
|
23
|
-
SidebarGroupContent,
|
|
24
|
-
SidebarGroupLabel,
|
|
25
|
-
SidebarHeader,
|
|
26
13
|
SidebarInset,
|
|
27
|
-
SidebarMenu,
|
|
28
|
-
SidebarMenuButton,
|
|
29
|
-
SidebarMenuItem,
|
|
30
|
-
SidebarMenuSub,
|
|
31
|
-
SidebarMenuSubButton,
|
|
32
|
-
SidebarMenuSubItem,
|
|
33
14
|
SidebarProvider,
|
|
34
|
-
SidebarRail,
|
|
35
15
|
SidebarTrigger,
|
|
36
16
|
useSidebar,
|
|
37
17
|
} from "../elements/sidebar.tsx"
|
|
18
|
+
import { BackToTop } from "./BackToTop.tsx"
|
|
38
19
|
import { Banner } from "./Banner.tsx"
|
|
39
|
-
import {
|
|
20
|
+
import { BlogSidebar } from "./BlogSidebar.tsx"
|
|
21
|
+
import { ChangelogSidebar } from "./ChangelogSidebar.tsx"
|
|
22
|
+
import { MiniSidebar } from "./MiniSidebar.tsx"
|
|
23
|
+
import { Sidebar } from "./Sidebar.tsx"
|
|
40
24
|
import { SiteTitle } from "./SiteTitle.tsx"
|
|
41
|
-
import { Theme } from "./Theme.tsx"
|
|
42
25
|
|
|
43
|
-
|
|
26
|
+
function splatFor(pathname: string) {
|
|
27
|
+
return { _splat: pathname.replace(/^\/|\/$/g, "") }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function Layout(props: {
|
|
31
|
+
withSidebar?: boolean
|
|
32
|
+
children?: React.ReactNode
|
|
33
|
+
}) {
|
|
34
|
+
const pathname = useLocation({ select: l => l.pathname })
|
|
35
|
+
const articleRoute = useMatch({ from: "/$", shouldThrow: false })
|
|
36
|
+
const activeSection = articleRoute
|
|
37
|
+
? currentSection(`/${pathname.replace(/^\/|\/$/g, "")}/`)
|
|
38
|
+
: undefined
|
|
39
|
+
|
|
44
40
|
useHotkey("J", () =>
|
|
45
41
|
window.scrollBy({ top: window.innerHeight * 0.8, behavior: "smooth" }),
|
|
46
42
|
)
|
|
@@ -48,173 +44,147 @@ export function Layout(props: { children?: React.ReactNode }) {
|
|
|
48
44
|
window.scrollBy({ top: -window.innerHeight * 0.8, behavior: "smooth" }),
|
|
49
45
|
)
|
|
50
46
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
>
|
|
64
|
-
|
|
47
|
+
const sections = import.meta.env.CONFIG.sections
|
|
48
|
+
|
|
49
|
+
const header = (
|
|
50
|
+
<header className="sticky top-0 z-20 flex h-16 shrink-0 items-center border-b bg-background">
|
|
51
|
+
<div
|
|
52
|
+
className={`flex items-center self-stretch pl-4 ${props.withSidebar ? "pr-4" : "md:pr-20"}`}
|
|
53
|
+
>
|
|
54
|
+
{props.withSidebar ? (
|
|
55
|
+
<>
|
|
56
|
+
<div className="hidden md:flex">
|
|
57
|
+
<SidebarTrigger />
|
|
58
|
+
</div>
|
|
59
|
+
<Link to="/" className="md:hidden flex items-center gap-2">
|
|
60
|
+
<SiteTitle />
|
|
65
61
|
</Link>
|
|
62
|
+
</>
|
|
63
|
+
) : (
|
|
64
|
+
<Link to="/" className="flex items-center gap-2">
|
|
65
|
+
<SiteTitle />
|
|
66
|
+
</Link>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
<div className="hidden md:flex flex-1 items-center gap-8 self-stretch px-6 text-sm">
|
|
70
|
+
{sections?.length ? (
|
|
71
|
+
sections
|
|
72
|
+
.filter(section => section.position === "header")
|
|
73
|
+
.map(section => {
|
|
74
|
+
const isActive = activeSection?.prefix === section.prefix
|
|
75
|
+
const target =
|
|
76
|
+
section.type === "blog" || section.type === "changelog"
|
|
77
|
+
? section.prefix
|
|
78
|
+
: sectionFirstArticle.get(section.prefix)
|
|
79
|
+
const latestVersion =
|
|
80
|
+
section.type === "changelog" && section.version
|
|
81
|
+
? sortedArticles.find(
|
|
82
|
+
a =>
|
|
83
|
+
a.path === sectionFlatArticles.get(section.prefix)?.[0],
|
|
84
|
+
)?.title
|
|
85
|
+
: undefined
|
|
86
|
+
return (
|
|
87
|
+
<Link
|
|
88
|
+
key={section.prefix}
|
|
89
|
+
to={target === "/" ? "/" : "/$"}
|
|
90
|
+
params={target && target !== "/" ? splatFor(target) : {}}
|
|
91
|
+
className={`inline-flex items-center gap-1.5 text-foreground ${
|
|
92
|
+
isActive
|
|
93
|
+
? "font-medium border-b-2 border-foreground pb-0.5 -mb-0.5"
|
|
94
|
+
: "opacity-80 hover:opacity-100 transition-opacity"
|
|
95
|
+
}`}
|
|
96
|
+
>
|
|
97
|
+
<DynamicIcon
|
|
98
|
+
name={sectionIcon(section)}
|
|
99
|
+
className="size-3.5"
|
|
100
|
+
/>
|
|
101
|
+
<span>
|
|
102
|
+
{section.title}
|
|
103
|
+
{latestVersion && (
|
|
104
|
+
<span className="opacity-80"> ({latestVersion})</span>
|
|
105
|
+
)}
|
|
106
|
+
</span>
|
|
107
|
+
</Link>
|
|
108
|
+
)
|
|
109
|
+
})
|
|
110
|
+
) : (
|
|
111
|
+
<Link
|
|
112
|
+
to="/"
|
|
113
|
+
className="text-foreground font-medium underline underline-offset-4"
|
|
114
|
+
>
|
|
115
|
+
Docs
|
|
116
|
+
</Link>
|
|
117
|
+
)}
|
|
118
|
+
{import.meta.env.CONFIG.links
|
|
119
|
+
?.filter(
|
|
120
|
+
link =>
|
|
121
|
+
link.position === "header" &&
|
|
122
|
+
(!link.prefix || link.prefix === activeSection?.prefix),
|
|
123
|
+
)
|
|
124
|
+
.map(link => (
|
|
66
125
|
<a
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
>
|
|
70
|
-
Blog
|
|
71
|
-
</a>
|
|
72
|
-
<a
|
|
73
|
-
href="https://github.com/datisthq/livemark"
|
|
126
|
+
key={link.url}
|
|
127
|
+
href={link.url}
|
|
74
128
|
target="_blank"
|
|
75
129
|
rel="noopener noreferrer"
|
|
76
|
-
className="text-
|
|
130
|
+
className="text-foreground opacity-80 hover:opacity-100 transition-opacity"
|
|
77
131
|
>
|
|
78
|
-
|
|
132
|
+
{link.icon && (
|
|
133
|
+
<DynamicIcon
|
|
134
|
+
name={link.icon}
|
|
135
|
+
className="inline size-3.5 align-[-0.125em]"
|
|
136
|
+
/>
|
|
137
|
+
)}{" "}
|
|
138
|
+
{link.title}{" "}
|
|
139
|
+
<ExternalLink className="inline size-3 align-[-0.125em]" />
|
|
79
140
|
</a>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
141
|
+
))}
|
|
142
|
+
<Banner />
|
|
143
|
+
</div>
|
|
144
|
+
<div className="md:hidden ml-auto pr-2">
|
|
145
|
+
<SidebarTrigger />
|
|
146
|
+
</div>
|
|
147
|
+
</header>
|
|
86
148
|
)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function AppSidebar() {
|
|
90
|
-
const pathname = useLocation({ select: l => l.pathname })
|
|
91
|
-
const { toggleSidebar } = useSidebar()
|
|
92
149
|
|
|
93
|
-
|
|
150
|
+
const SidebarComponent = !props.withSidebar
|
|
151
|
+
? MiniSidebar
|
|
152
|
+
: activeSection?.type === "blog"
|
|
153
|
+
? BlogSidebar
|
|
154
|
+
: activeSection?.type === "changelog"
|
|
155
|
+
? ChangelogSidebar
|
|
156
|
+
: Sidebar
|
|
94
157
|
|
|
95
158
|
return (
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
</SidebarMenu>
|
|
105
|
-
</SidebarHeader>
|
|
106
|
-
<SidebarContent>
|
|
107
|
-
<SidebarGroup>
|
|
108
|
-
<SidebarGroupLabel className="uppercase font-mono text-xs tracking-widest">
|
|
109
|
-
Articles
|
|
110
|
-
</SidebarGroupLabel>
|
|
111
|
-
<SidebarGroupContent>
|
|
112
|
-
<SidebarMenu>
|
|
113
|
-
{articleTree.map(node => (
|
|
114
|
-
<NavNode
|
|
115
|
-
key={node.pathname}
|
|
116
|
-
node={node}
|
|
117
|
-
currentPath={pathname}
|
|
118
|
-
/>
|
|
119
|
-
))}
|
|
120
|
-
</SidebarMenu>
|
|
121
|
-
</SidebarGroupContent>
|
|
122
|
-
</SidebarGroup>
|
|
123
|
-
</SidebarContent>
|
|
124
|
-
<SidebarFooter className="p-4 space-y-2">
|
|
125
|
-
<Search />
|
|
126
|
-
<Theme />
|
|
127
|
-
</SidebarFooter>
|
|
128
|
-
<SidebarRail />
|
|
129
|
-
</Sidebar>
|
|
159
|
+
<SidebarProvider
|
|
160
|
+
key={props.withSidebar ? "sidebar" : "mini"}
|
|
161
|
+
defaultOpen={!!props.withSidebar}
|
|
162
|
+
>
|
|
163
|
+
<SidebarShell SidebarComponent={SidebarComponent} header={header}>
|
|
164
|
+
{props.children}
|
|
165
|
+
</SidebarShell>
|
|
166
|
+
</SidebarProvider>
|
|
130
167
|
)
|
|
131
168
|
}
|
|
132
169
|
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
function NavNode(props: { node: ArticleNode; currentPath: string }) {
|
|
143
|
-
const { node, currentPath } = props
|
|
144
|
-
const Icon = articleIcons[node.icon] ?? FileText
|
|
145
|
-
const active = isActive(node.pathname, currentPath)
|
|
146
|
-
|
|
147
|
-
if (node.children.length === 0) {
|
|
148
|
-
return (
|
|
149
|
-
<SidebarMenuItem>
|
|
150
|
-
<SidebarMenuButton
|
|
151
|
-
isActive={active}
|
|
152
|
-
className={active ? "" : "opacity-75"}
|
|
153
|
-
render={<Link to="/$" params={splatFor(node.pathname)} />}
|
|
154
|
-
>
|
|
155
|
-
<Icon className="size-4" />
|
|
156
|
-
<span>{node.title}</span>
|
|
157
|
-
</SidebarMenuButton>
|
|
158
|
-
</SidebarMenuItem>
|
|
159
|
-
)
|
|
170
|
+
function SidebarShell(props: {
|
|
171
|
+
SidebarComponent: React.ComponentType
|
|
172
|
+
header: React.ReactNode
|
|
173
|
+
children?: React.ReactNode
|
|
174
|
+
}) {
|
|
175
|
+
const { isMobile, setOpenMobile } = useSidebar()
|
|
176
|
+
const onClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
177
|
+
if (!isMobile) return
|
|
178
|
+
if ((event.target as HTMLElement).closest("a")) setOpenMobile(false)
|
|
160
179
|
}
|
|
161
|
-
|
|
162
180
|
return (
|
|
163
|
-
<
|
|
164
|
-
<
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
<span>{node.title}</span>
|
|
172
|
-
</SidebarMenuButton>
|
|
173
|
-
<CollapsibleTrigger className="absolute right-1 top-1.5 p-1 rounded-md hover:bg-sidebar-accent">
|
|
174
|
-
<ChevronRight className="size-3.5 transition-transform group-data-[open]/collapsible:rotate-90" />
|
|
175
|
-
</CollapsibleTrigger>
|
|
176
|
-
</SidebarMenuItem>
|
|
177
|
-
<CollapsibleContent>
|
|
178
|
-
<SidebarMenuSub>
|
|
179
|
-
{node.children.map(child => {
|
|
180
|
-
const childIsActive = isActive(child.pathname, currentPath)
|
|
181
|
-
return (
|
|
182
|
-
<SidebarMenuSubItem key={child.pathname}>
|
|
183
|
-
<SidebarMenuSubButton
|
|
184
|
-
isActive={childIsActive}
|
|
185
|
-
className={childIsActive ? "" : "opacity-75"}
|
|
186
|
-
render={<Link to="/$" params={splatFor(child.pathname)} />}
|
|
187
|
-
>
|
|
188
|
-
<span>{child.title}</span>
|
|
189
|
-
</SidebarMenuSubButton>
|
|
190
|
-
</SidebarMenuSubItem>
|
|
191
|
-
)
|
|
192
|
-
})}
|
|
193
|
-
</SidebarMenuSub>
|
|
194
|
-
</CollapsibleContent>
|
|
195
|
-
</Collapsible>
|
|
196
|
-
)
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function Search() {
|
|
200
|
-
const [open, setOpen] = useState(false)
|
|
201
|
-
|
|
202
|
-
useHotkey("/", () => setOpen(true))
|
|
203
|
-
|
|
204
|
-
return (
|
|
205
|
-
<>
|
|
206
|
-
<button
|
|
207
|
-
type="button"
|
|
208
|
-
onClick={() => setOpen(true)}
|
|
209
|
-
className="flex w-full items-center gap-2 rounded-xl border border-border bg-sidebar-accent/70 px-3 py-2 text-sm text-muted-foreground shadow-xs hover:bg-background transition-colors cursor-pointer"
|
|
210
|
-
>
|
|
211
|
-
<SearchIcon className="size-4" />
|
|
212
|
-
<span className="flex-1 text-left">Search...</span>
|
|
213
|
-
<kbd className="rounded border border-border bg-muted px-1.5 py-0.5 text-[10px] font-mono">
|
|
214
|
-
/
|
|
215
|
-
</kbd>
|
|
216
|
-
</button>
|
|
217
|
-
<SearchDialog open={open} onOpenChange={setOpen} />
|
|
218
|
-
</>
|
|
181
|
+
<div style={{ display: "contents" }} onClick={onClick}>
|
|
182
|
+
<props.SidebarComponent />
|
|
183
|
+
<SidebarInset>
|
|
184
|
+
{props.header}
|
|
185
|
+
<main className="flex-1">{props.children}</main>
|
|
186
|
+
<BackToTop />
|
|
187
|
+
</SidebarInset>
|
|
188
|
+
</div>
|
|
219
189
|
)
|
|
220
190
|
}
|
|
@@ -1,37 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import mermaid from "mermaid"
|
|
1
|
+
import { Suspense, lazy } from "react"
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
const MermaidRenderer = lazy(() => import("./MermaidRenderer.tsx"))
|
|
5
4
|
|
|
6
|
-
/**
|
|
5
|
+
/** Lazily loads and renders a Mermaid diagram */
|
|
7
6
|
export function Mermaid(props: { children: string }) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
})
|
|
20
|
-
el.textContent = props.children
|
|
21
|
-
el.removeAttribute("data-processed")
|
|
22
|
-
mermaid.run({ nodes: [el] })
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
render()
|
|
26
|
-
|
|
27
|
-
const observer = new MutationObserver(render)
|
|
28
|
-
observer.observe(document.documentElement, {
|
|
29
|
-
attributes: true,
|
|
30
|
-
attributeFilter: ["class"],
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
return () => observer.disconnect()
|
|
34
|
-
}, [props.children])
|
|
35
|
-
|
|
36
|
-
return <div ref={ref} className="not-prose mermaid flex justify-center" />
|
|
7
|
+
return (
|
|
8
|
+
<Suspense
|
|
9
|
+
fallback={
|
|
10
|
+
<div className="not-prose mermaid flex justify-center">
|
|
11
|
+
Loading diagram…
|
|
12
|
+
</div>
|
|
13
|
+
}
|
|
14
|
+
>
|
|
15
|
+
<MermaidRenderer>{props.children}</MermaidRenderer>
|
|
16
|
+
</Suspense>
|
|
17
|
+
)
|
|
37
18
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react"
|
|
2
|
+
import mermaid from "mermaid"
|
|
3
|
+
|
|
4
|
+
mermaid.initialize({ startOnLoad: false })
|
|
5
|
+
|
|
6
|
+
/** Renders a Mermaid diagram from a chart definition string */
|
|
7
|
+
export default function MermaidRenderer(props: { children: string }) {
|
|
8
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const el = ref.current
|
|
12
|
+
if (!el) return
|
|
13
|
+
|
|
14
|
+
const render = () => {
|
|
15
|
+
const isDark = document.documentElement.classList.contains("dark")
|
|
16
|
+
mermaid.initialize({
|
|
17
|
+
startOnLoad: false,
|
|
18
|
+
theme: isDark ? "dark" : "default",
|
|
19
|
+
})
|
|
20
|
+
el.textContent = props.children
|
|
21
|
+
el.removeAttribute("data-processed")
|
|
22
|
+
mermaid.run({ nodes: [el] })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
render()
|
|
26
|
+
|
|
27
|
+
const observer = new MutationObserver(render)
|
|
28
|
+
observer.observe(document.documentElement, {
|
|
29
|
+
attributes: true,
|
|
30
|
+
attributeFilter: ["class"],
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return () => observer.disconnect()
|
|
34
|
+
}, [props.children])
|
|
35
|
+
|
|
36
|
+
return <div ref={ref} className="not-prose mermaid flex justify-center" />
|
|
37
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Link } from "@tanstack/react-router"
|
|
2
|
+
import { useHotkey } from "@tanstack/react-hotkeys"
|
|
3
|
+
import {
|
|
4
|
+
Sidebar as SidebarRoot,
|
|
5
|
+
SidebarContent,
|
|
6
|
+
SidebarFooter,
|
|
7
|
+
SidebarHeader,
|
|
8
|
+
SidebarMenu,
|
|
9
|
+
SidebarMenuButton,
|
|
10
|
+
SidebarMenuItem,
|
|
11
|
+
SidebarRail,
|
|
12
|
+
useSidebar,
|
|
13
|
+
} from "../elements/sidebar.tsx"
|
|
14
|
+
import { Search } from "./Search.tsx"
|
|
15
|
+
import { SidebarLinks } from "./SidebarLinks.tsx"
|
|
16
|
+
import { SidebarSections } from "./SidebarSections.tsx"
|
|
17
|
+
import { SiteTitle } from "./SiteTitle.tsx"
|
|
18
|
+
import { Theme } from "./Theme.tsx"
|
|
19
|
+
|
|
20
|
+
/** Minimal sidebar used on pages without an article tree. On desktop the
|
|
21
|
+
* SidebarProvider keeps it off-canvas; on mobile it drives the sheet so
|
|
22
|
+
* users can still reach Sections / Links / Search / Theme. */
|
|
23
|
+
export function MiniSidebar() {
|
|
24
|
+
const { toggleSidebar } = useSidebar()
|
|
25
|
+
useHotkey("S", toggleSidebar)
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<SidebarRoot>
|
|
29
|
+
<SidebarHeader>
|
|
30
|
+
<SidebarMenu>
|
|
31
|
+
<SidebarMenuItem>
|
|
32
|
+
<SidebarMenuButton size="lg" render={<Link to="/" />}>
|
|
33
|
+
<SiteTitle />
|
|
34
|
+
</SidebarMenuButton>
|
|
35
|
+
</SidebarMenuItem>
|
|
36
|
+
</SidebarMenu>
|
|
37
|
+
</SidebarHeader>
|
|
38
|
+
<SidebarContent>
|
|
39
|
+
<SidebarSections />
|
|
40
|
+
<SidebarLinks />
|
|
41
|
+
</SidebarContent>
|
|
42
|
+
<SidebarFooter className="p-4 space-y-2">
|
|
43
|
+
<Search />
|
|
44
|
+
<Theme />
|
|
45
|
+
</SidebarFooter>
|
|
46
|
+
<SidebarRail />
|
|
47
|
+
</SidebarRoot>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useState, useCallback } from "react"
|
|
2
2
|
import { Pencil, Copy, Check, ExternalLink } from "lucide-react"
|
|
3
3
|
|
|
4
|
-
/** Toolbar with action buttons displayed
|
|
5
|
-
export function PageToolbar(props: {
|
|
4
|
+
/** Toolbar with action buttons displayed for the article content */
|
|
5
|
+
export function PageToolbar(props: { file?: string; content: string }) {
|
|
6
6
|
const [copied, setCopied] = useState(false)
|
|
7
7
|
|
|
8
8
|
const handleCopy = useCallback(() => {
|
|
@@ -26,15 +26,25 @@ export function PageToolbar(props: { filePath: string; content: string }) {
|
|
|
26
26
|
<>
|
|
27
27
|
<div className="mb-6 flex flex-col gap-2 text-sm">
|
|
28
28
|
<p className="text-sm font-medium mb-1">Actions</p>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
{props.file ? (
|
|
30
|
+
<a
|
|
31
|
+
href={`https://github.com/datisthq/livemark/edit/main/${props.file}`}
|
|
32
|
+
target="_blank"
|
|
33
|
+
rel="noopener noreferrer"
|
|
34
|
+
className={buttonClass}
|
|
35
|
+
>
|
|
36
|
+
<Pencil className="size-3.5" />
|
|
37
|
+
Edit on GitHub
|
|
38
|
+
</a>
|
|
39
|
+
) : (
|
|
40
|
+
<span
|
|
41
|
+
className={`${buttonClass} opacity-50 cursor-not-allowed hover:text-muted-foreground`}
|
|
42
|
+
aria-disabled="true"
|
|
43
|
+
>
|
|
44
|
+
<Pencil className="size-3.5" />
|
|
45
|
+
Edit on GitHub
|
|
46
|
+
</span>
|
|
47
|
+
)}
|
|
38
48
|
<button type="button" onClick={handleCopy} className={buttonClass}>
|
|
39
49
|
{copied ? (
|
|
40
50
|
<Check className="size-3.5" />
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
import { Link } from "@tanstack/react-router"
|
|
2
2
|
import { ChevronLeft, ChevronRight } from "lucide-react"
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
sortedArticles,
|
|
5
|
+
flatArticles,
|
|
6
|
+
currentSection,
|
|
7
|
+
sectionFlatArticles,
|
|
8
|
+
} from "../content/article.ts"
|
|
4
9
|
|
|
5
10
|
/** Previous/next article navigation shown at the bottom of each article page */
|
|
6
|
-
export function PrevNext(props: {
|
|
7
|
-
const
|
|
11
|
+
export function PrevNext(props: { path: string }) {
|
|
12
|
+
const section = currentSection(props.path)
|
|
13
|
+
const flat = section
|
|
14
|
+
? (sectionFlatArticles.get(section.prefix) ?? flatArticles)
|
|
15
|
+
: flatArticles
|
|
16
|
+
const index = flat.indexOf(props.path)
|
|
8
17
|
if (index < 0) return null
|
|
9
18
|
|
|
10
|
-
const prevPath = index > 0 ?
|
|
11
|
-
const nextPath =
|
|
12
|
-
index < flatArticles.length - 1 ? flatArticles[index + 1] : undefined
|
|
19
|
+
const prevPath = index > 0 ? flat[index - 1] : undefined
|
|
20
|
+
const nextPath = index < flat.length - 1 ? flat[index + 1] : undefined
|
|
13
21
|
const prev = prevPath
|
|
14
|
-
? sortedArticles.find(a => a.
|
|
22
|
+
? sortedArticles.find(a => a.path === prevPath)
|
|
15
23
|
: undefined
|
|
16
24
|
const next = nextPath
|
|
17
|
-
? sortedArticles.find(a => a.
|
|
25
|
+
? sortedArticles.find(a => a.path === nextPath)
|
|
18
26
|
: undefined
|
|
19
27
|
|
|
20
28
|
if (!prev && !next) return null
|
|
@@ -24,8 +32,8 @@ export function PrevNext(props: { pathname: string }) {
|
|
|
24
32
|
{prev && (
|
|
25
33
|
<Link
|
|
26
34
|
to="/$"
|
|
27
|
-
params={{ _splat: prev.
|
|
28
|
-
className="flex-1 rounded-xl border shadow-sm
|
|
35
|
+
params={{ _splat: prev.path.replace(/^\/|\/$/g, "") }}
|
|
36
|
+
className="flex-1 rounded-xl border shadow-sm p-4 hover:bg-muted/50 transition-colors"
|
|
29
37
|
>
|
|
30
38
|
<span className="flex items-center gap-1 text-sm text-muted-foreground">
|
|
31
39
|
<ChevronLeft className="size-4" />
|
|
@@ -37,8 +45,8 @@ export function PrevNext(props: { pathname: string }) {
|
|
|
37
45
|
{next && (
|
|
38
46
|
<Link
|
|
39
47
|
to="/$"
|
|
40
|
-
params={{ _splat: next.
|
|
41
|
-
className="flex-1 rounded-xl border shadow-sm
|
|
48
|
+
params={{ _splat: next.path.replace(/^\/|\/$/g, "") }}
|
|
49
|
+
className="flex-1 rounded-xl border shadow-sm p-4 hover:bg-muted/50 transition-colors text-right"
|
|
42
50
|
>
|
|
43
51
|
<span className="flex items-center justify-end gap-1 text-sm text-muted-foreground">
|
|
44
52
|
Next
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useState } from "react"
|
|
2
|
+
import { useHotkey } from "@tanstack/react-hotkeys"
|
|
3
|
+
import { Search as SearchIcon } from "lucide-react"
|
|
4
|
+
import { SearchDialog } from "./SearchDialog.tsx"
|
|
5
|
+
|
|
6
|
+
/** Search button with "/" keyboard shortcut and command palette dialog */
|
|
7
|
+
export function Search() {
|
|
8
|
+
const [open, setOpen] = useState(false)
|
|
9
|
+
|
|
10
|
+
useHotkey("/", () => setOpen(true))
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<>
|
|
14
|
+
<button
|
|
15
|
+
type="button"
|
|
16
|
+
onClick={() => setOpen(true)}
|
|
17
|
+
className="flex w-full items-center gap-2 rounded-xl border border-border bg-sidebar-accent/70 px-3 py-2 text-sm text-muted-foreground shadow-xs hover:bg-background transition-colors cursor-pointer"
|
|
18
|
+
>
|
|
19
|
+
<SearchIcon className="size-4" />
|
|
20
|
+
<span className="flex-1 text-left">Search...</span>
|
|
21
|
+
<kbd className="rounded border border-border bg-muted px-1.5 py-0.5 text-[10px] font-mono">
|
|
22
|
+
/
|
|
23
|
+
</kbd>
|
|
24
|
+
</button>
|
|
25
|
+
<SearchDialog open={open} onOpenChange={setOpen} />
|
|
26
|
+
</>
|
|
27
|
+
)
|
|
28
|
+
}
|