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
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { Link, useLocation } from "@tanstack/react-router"
|
|
2
|
+
import { useHotkey } from "@tanstack/react-hotkeys"
|
|
3
|
+
import { Clock, List, Rss, Tag } from "lucide-react"
|
|
4
|
+
import {
|
|
5
|
+
currentSection,
|
|
6
|
+
sectionFlatArticles,
|
|
7
|
+
sortedArticles,
|
|
8
|
+
} from "../content/article.ts"
|
|
9
|
+
import {
|
|
10
|
+
Sidebar as SidebarRoot,
|
|
11
|
+
SidebarContent,
|
|
12
|
+
SidebarFooter,
|
|
13
|
+
SidebarGroup,
|
|
14
|
+
SidebarGroupContent,
|
|
15
|
+
SidebarGroupLabel,
|
|
16
|
+
SidebarHeader,
|
|
17
|
+
SidebarMenu,
|
|
18
|
+
SidebarMenuButton,
|
|
19
|
+
SidebarMenuItem,
|
|
20
|
+
SidebarMenuSub,
|
|
21
|
+
SidebarMenuSubButton,
|
|
22
|
+
SidebarMenuSubItem,
|
|
23
|
+
SidebarRail,
|
|
24
|
+
useSidebar,
|
|
25
|
+
} from "../elements/sidebar.tsx"
|
|
26
|
+
import { Search } from "./Search.tsx"
|
|
27
|
+
import { SidebarLinks } from "./SidebarLinks.tsx"
|
|
28
|
+
import { SidebarSections } from "./SidebarSections.tsx"
|
|
29
|
+
import { SiteTitle } from "./SiteTitle.tsx"
|
|
30
|
+
import { Theme } from "./Theme.tsx"
|
|
31
|
+
|
|
32
|
+
/** Blog-specific sidebar with Posts, Tags, and Links */
|
|
33
|
+
export function BlogSidebar() {
|
|
34
|
+
const pathname = useLocation({ select: l => l.pathname })
|
|
35
|
+
const { toggleSidebar } = useSidebar()
|
|
36
|
+
|
|
37
|
+
useHotkey("S", toggleSidebar)
|
|
38
|
+
|
|
39
|
+
const section = currentSection(`/${pathname.replace(/^\/|\/$/g, "")}/`)
|
|
40
|
+
const flat = section ? (sectionFlatArticles.get(section.prefix) ?? []) : []
|
|
41
|
+
const posts = flat
|
|
42
|
+
.map(p => sortedArticles.find(a => a.path === p))
|
|
43
|
+
.filter(a => a !== undefined)
|
|
44
|
+
const recentPosts = posts.slice(0, 5)
|
|
45
|
+
|
|
46
|
+
const allTags = new Set<string>()
|
|
47
|
+
for (const post of posts) {
|
|
48
|
+
for (const tag of post.tags ?? []) {
|
|
49
|
+
allTags.add(tag)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const tags = [...allTags].sort()
|
|
53
|
+
|
|
54
|
+
const normalized = `/${pathname.replace(/^\/|\/$/g, "")}/`
|
|
55
|
+
const isIndex = section ? normalized === section.prefix : false
|
|
56
|
+
const sectionPath = (section?.prefix ?? "/blog/").replace(/^\/|\/$/g, "")
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<SidebarRoot>
|
|
60
|
+
<SidebarHeader>
|
|
61
|
+
<SidebarMenu>
|
|
62
|
+
<SidebarMenuItem>
|
|
63
|
+
<SidebarMenuButton size="lg" render={<Link to="/" />}>
|
|
64
|
+
<SiteTitle />
|
|
65
|
+
</SidebarMenuButton>
|
|
66
|
+
</SidebarMenuItem>
|
|
67
|
+
</SidebarMenu>
|
|
68
|
+
</SidebarHeader>
|
|
69
|
+
<SidebarContent>
|
|
70
|
+
<SidebarSections />
|
|
71
|
+
<SidebarGroup>
|
|
72
|
+
<SidebarGroupLabel className="uppercase font-mono text-xs tracking-widest">
|
|
73
|
+
Posts
|
|
74
|
+
</SidebarGroupLabel>
|
|
75
|
+
<SidebarGroupContent>
|
|
76
|
+
<SidebarMenu>
|
|
77
|
+
<SidebarMenuItem>
|
|
78
|
+
<SidebarMenuButton
|
|
79
|
+
isActive={isIndex}
|
|
80
|
+
className={isIndex ? "" : "opacity-75"}
|
|
81
|
+
render={<Link to="/$" params={{ _splat: sectionPath }} />}
|
|
82
|
+
>
|
|
83
|
+
<List className="size-4" />
|
|
84
|
+
<span>All Posts</span>
|
|
85
|
+
</SidebarMenuButton>
|
|
86
|
+
</SidebarMenuItem>
|
|
87
|
+
<SidebarMenuItem>
|
|
88
|
+
<SidebarMenuButton className="opacity-75">
|
|
89
|
+
<Clock className="size-4" />
|
|
90
|
+
<span>Recent Posts</span>
|
|
91
|
+
</SidebarMenuButton>
|
|
92
|
+
<SidebarMenuSub>
|
|
93
|
+
{recentPosts.map(post => {
|
|
94
|
+
const active = normalized === post.path
|
|
95
|
+
return (
|
|
96
|
+
<SidebarMenuSubItem key={post.path}>
|
|
97
|
+
<SidebarMenuSubButton
|
|
98
|
+
isActive={active}
|
|
99
|
+
className={active ? "" : "opacity-75"}
|
|
100
|
+
render={
|
|
101
|
+
<Link
|
|
102
|
+
to="/$"
|
|
103
|
+
params={{
|
|
104
|
+
_splat: post.path.replace(/^\/|\/$/g, ""),
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
107
|
+
}
|
|
108
|
+
>
|
|
109
|
+
<span>{post.title}</span>
|
|
110
|
+
</SidebarMenuSubButton>
|
|
111
|
+
</SidebarMenuSubItem>
|
|
112
|
+
)
|
|
113
|
+
})}
|
|
114
|
+
</SidebarMenuSub>
|
|
115
|
+
</SidebarMenuItem>
|
|
116
|
+
<SidebarMenuItem>
|
|
117
|
+
<SidebarMenuButton className="opacity-75">
|
|
118
|
+
<Rss className="size-4" />
|
|
119
|
+
<span>RSS Feed</span>
|
|
120
|
+
</SidebarMenuButton>
|
|
121
|
+
</SidebarMenuItem>
|
|
122
|
+
</SidebarMenu>
|
|
123
|
+
</SidebarGroupContent>
|
|
124
|
+
</SidebarGroup>
|
|
125
|
+
|
|
126
|
+
{tags.length > 0 && (
|
|
127
|
+
<SidebarGroup>
|
|
128
|
+
<SidebarGroupLabel className="uppercase font-mono text-xs tracking-widest">
|
|
129
|
+
Tags
|
|
130
|
+
</SidebarGroupLabel>
|
|
131
|
+
<SidebarGroupContent>
|
|
132
|
+
<SidebarMenu>
|
|
133
|
+
{tags.map(tag => {
|
|
134
|
+
const tagPath = `${sectionPath}/tags/${tag}`
|
|
135
|
+
const tagActive = normalized === `/${tagPath}/`
|
|
136
|
+
return (
|
|
137
|
+
<SidebarMenuItem key={tag}>
|
|
138
|
+
<SidebarMenuButton
|
|
139
|
+
isActive={tagActive}
|
|
140
|
+
className={tagActive ? "" : "opacity-75"}
|
|
141
|
+
render={<Link to="/$" params={{ _splat: tagPath }} />}
|
|
142
|
+
>
|
|
143
|
+
<Tag className="size-3.5" />
|
|
144
|
+
<span>{tag}</span>
|
|
145
|
+
</SidebarMenuButton>
|
|
146
|
+
</SidebarMenuItem>
|
|
147
|
+
)
|
|
148
|
+
})}
|
|
149
|
+
</SidebarMenu>
|
|
150
|
+
</SidebarGroupContent>
|
|
151
|
+
</SidebarGroup>
|
|
152
|
+
)}
|
|
153
|
+
<SidebarLinks />
|
|
154
|
+
</SidebarContent>
|
|
155
|
+
<SidebarFooter className="p-4 space-y-2">
|
|
156
|
+
<Search />
|
|
157
|
+
<Theme />
|
|
158
|
+
</SidebarFooter>
|
|
159
|
+
<SidebarRail />
|
|
160
|
+
</SidebarRoot>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import { Link, useMatches } from "@tanstack/react-router"
|
|
1
|
+
import { Link, useLocation, useMatches } from "@tanstack/react-router"
|
|
2
|
+
import { currentSection } from "../content/article.ts"
|
|
2
3
|
|
|
3
4
|
export function Breadcrumbs() {
|
|
4
5
|
const matches = useMatches()
|
|
5
6
|
const last = matches[matches.length - 1]
|
|
6
7
|
const loaderData = last?.loaderData as { title?: string } | undefined
|
|
8
|
+
const pathname = useLocation({ select: l => l.pathname })
|
|
9
|
+
const section = currentSection(`/${pathname.replace(/^\/|\/$/g, "")}/`)
|
|
7
10
|
|
|
8
11
|
return (
|
|
9
12
|
<nav className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
|
10
13
|
<Link to="/" className="hover:text-foreground transition-colors">
|
|
11
|
-
|
|
14
|
+
{section?.title ?? import.meta.env.CONFIG.title}
|
|
12
15
|
</Link>
|
|
13
16
|
<span>/</span>
|
|
14
17
|
{loaderData?.title && (
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Link } from "@tanstack/react-router"
|
|
2
|
+
import { DynamicIcon } from "../helpers/dynamic-icon.tsx"
|
|
3
|
+
import {
|
|
4
|
+
Card as CardElement,
|
|
5
|
+
CardContent,
|
|
6
|
+
CardHeader,
|
|
7
|
+
CardTitle,
|
|
8
|
+
} from "../elements/card.tsx"
|
|
9
|
+
|
|
10
|
+
/** Card component with optional icon, title, description, and link */
|
|
11
|
+
export function Card(props: {
|
|
12
|
+
title: string
|
|
13
|
+
href?: string
|
|
14
|
+
icon?: string
|
|
15
|
+
children?: React.ReactNode
|
|
16
|
+
}) {
|
|
17
|
+
const card = (
|
|
18
|
+
<CardElement size="sm" className="transition-colors hover:bg-accent">
|
|
19
|
+
<CardHeader>
|
|
20
|
+
<CardTitle className="flex items-center gap-2">
|
|
21
|
+
{props.icon && (
|
|
22
|
+
<DynamicIcon name={props.icon} className="size-5 text-primary" />
|
|
23
|
+
)}
|
|
24
|
+
{props.title}
|
|
25
|
+
</CardTitle>
|
|
26
|
+
</CardHeader>
|
|
27
|
+
{props.children && (
|
|
28
|
+
<CardContent>
|
|
29
|
+
<div className="text-muted-foreground text-sm">{props.children}</div>
|
|
30
|
+
</CardContent>
|
|
31
|
+
)}
|
|
32
|
+
</CardElement>
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if (props.href?.startsWith("http")) {
|
|
36
|
+
return (
|
|
37
|
+
<a href={props.href} target="_blank" rel="noopener noreferrer">
|
|
38
|
+
{card}
|
|
39
|
+
</a>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (props.href) {
|
|
44
|
+
return <Link to={props.href}>{card}</Link>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return card
|
|
48
|
+
}
|
|
@@ -1,19 +1,3 @@
|
|
|
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
1
|
/** Grid container for card components */
|
|
18
2
|
export function Cards(props: { children: React.ReactNode }) {
|
|
19
3
|
return (
|
|
@@ -22,45 +6,3 @@ export function Cards(props: { children: React.ReactNode }) {
|
|
|
22
6
|
</div>
|
|
23
7
|
)
|
|
24
8
|
}
|
|
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,111 @@
|
|
|
1
|
+
import { Link } from "@tanstack/react-router"
|
|
2
|
+
import { History } from "lucide-react"
|
|
3
|
+
import {
|
|
4
|
+
currentSection,
|
|
5
|
+
sectionFlatArticles,
|
|
6
|
+
sortedArticles,
|
|
7
|
+
} from "../content/article.ts"
|
|
8
|
+
import { Separator } from "../elements/separator.tsx"
|
|
9
|
+
import { TocContext } from "../helpers/toc-context.ts"
|
|
10
|
+
import { Footer } from "./Footer.tsx"
|
|
11
|
+
import { PageToolbar } from "./PageToolbar.tsx"
|
|
12
|
+
import { MobileToc, Toc } from "./Toc.tsx"
|
|
13
|
+
|
|
14
|
+
/** Auto-generated changelog index listing all versions by date */
|
|
15
|
+
export function ChangelogIndex(props: { sectionPrefix: string }) {
|
|
16
|
+
const section = currentSection(props.sectionPrefix)
|
|
17
|
+
const flat = sectionFlatArticles.get(props.sectionPrefix) ?? []
|
|
18
|
+
const entries = flat
|
|
19
|
+
.map(p => sortedArticles.find(a => a.path === p))
|
|
20
|
+
.filter(a => a !== undefined)
|
|
21
|
+
|
|
22
|
+
const tocItems = entries.map(entry => ({
|
|
23
|
+
url: `#${slugOf(entry.path)}`,
|
|
24
|
+
title: entry.title,
|
|
25
|
+
depth: 2,
|
|
26
|
+
}))
|
|
27
|
+
|
|
28
|
+
const content = [
|
|
29
|
+
`# ${section?.title ?? "Changelog"}`,
|
|
30
|
+
...entries.map(entry => {
|
|
31
|
+
const parts = [`## ${entry.title}`]
|
|
32
|
+
if (entry.date) parts.push(formatDate(entry.date))
|
|
33
|
+
if (entry.description) parts.push("", entry.description)
|
|
34
|
+
return parts.join("\n")
|
|
35
|
+
}),
|
|
36
|
+
].join("\n\n")
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<TocContext.Provider value={tocItems}>
|
|
40
|
+
<MobileToc items={tocItems}>
|
|
41
|
+
<PageToolbar content={content} />
|
|
42
|
+
</MobileToc>
|
|
43
|
+
<div className="flex flex-1 gap-10 p-4 pt-8 md:p-10">
|
|
44
|
+
<div className="flex-1 min-w-0 mx-auto max-w-3xl">
|
|
45
|
+
<div className="prose dark:prose-invert max-w-none mb-8">
|
|
46
|
+
<h1 id="top">
|
|
47
|
+
<a href="#top">{section?.title ?? "Changelog"}</a>
|
|
48
|
+
</h1>
|
|
49
|
+
{entries[0]?.date && (
|
|
50
|
+
<p className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-muted-foreground !-mt-4 !mb-0">
|
|
51
|
+
<span className="flex items-center gap-1.5">
|
|
52
|
+
<History className="size-3.5" />
|
|
53
|
+
Updated {formatDate(entries[0].date)}
|
|
54
|
+
</span>
|
|
55
|
+
</p>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
<div className="space-y-6">
|
|
59
|
+
{entries.map((entry, i) => (
|
|
60
|
+
<article key={entry.path} id={slugOf(entry.path)}>
|
|
61
|
+
{i > 0 && <Separator className="mb-6" />}
|
|
62
|
+
<h2 className="text-2xl">
|
|
63
|
+
<Link
|
|
64
|
+
to="/$"
|
|
65
|
+
params={{ _splat: entry.path.replace(/^\/|\/$/g, "") }}
|
|
66
|
+
className="font-medium hover:text-primary transition-colors"
|
|
67
|
+
>
|
|
68
|
+
{entry.title}
|
|
69
|
+
</Link>
|
|
70
|
+
</h2>
|
|
71
|
+
{entry.date && (
|
|
72
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
73
|
+
{formatDate(entry.date)}
|
|
74
|
+
</p>
|
|
75
|
+
)}
|
|
76
|
+
{entry.description && (
|
|
77
|
+
<p className="text-muted-foreground mt-2">
|
|
78
|
+
{entry.description}
|
|
79
|
+
</p>
|
|
80
|
+
)}
|
|
81
|
+
</article>
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
<div className="mt-10">
|
|
85
|
+
<Footer />
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
<Toc items={tocItems}>
|
|
89
|
+
<PageToolbar content={content} />
|
|
90
|
+
</Toc>
|
|
91
|
+
</div>
|
|
92
|
+
</TocContext.Provider>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function slugOf(path: string) {
|
|
97
|
+
return (
|
|
98
|
+
path
|
|
99
|
+
.replace(/^\/|\/$/g, "")
|
|
100
|
+
.split("/")
|
|
101
|
+
.pop() ?? ""
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function formatDate(iso: string) {
|
|
106
|
+
return new Date(iso).toLocaleDateString("en-US", {
|
|
107
|
+
year: "numeric",
|
|
108
|
+
month: "short",
|
|
109
|
+
day: "numeric",
|
|
110
|
+
})
|
|
111
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Link, useLocation } from "@tanstack/react-router"
|
|
2
|
+
import { useHotkey } from "@tanstack/react-hotkeys"
|
|
3
|
+
import { Clock, List } from "lucide-react"
|
|
4
|
+
import {
|
|
5
|
+
currentSection,
|
|
6
|
+
sectionFlatArticles,
|
|
7
|
+
sortedArticles,
|
|
8
|
+
} from "../content/article.ts"
|
|
9
|
+
import {
|
|
10
|
+
Sidebar as SidebarRoot,
|
|
11
|
+
SidebarContent,
|
|
12
|
+
SidebarFooter,
|
|
13
|
+
SidebarGroup,
|
|
14
|
+
SidebarGroupContent,
|
|
15
|
+
SidebarGroupLabel,
|
|
16
|
+
SidebarHeader,
|
|
17
|
+
SidebarMenu,
|
|
18
|
+
SidebarMenuButton,
|
|
19
|
+
SidebarMenuItem,
|
|
20
|
+
SidebarMenuSub,
|
|
21
|
+
SidebarMenuSubButton,
|
|
22
|
+
SidebarMenuSubItem,
|
|
23
|
+
SidebarRail,
|
|
24
|
+
useSidebar,
|
|
25
|
+
} from "../elements/sidebar.tsx"
|
|
26
|
+
import { Search } from "./Search.tsx"
|
|
27
|
+
import { SidebarLinks } from "./SidebarLinks.tsx"
|
|
28
|
+
import { SidebarSections } from "./SidebarSections.tsx"
|
|
29
|
+
import { SiteTitle } from "./SiteTitle.tsx"
|
|
30
|
+
import { Theme } from "./Theme.tsx"
|
|
31
|
+
|
|
32
|
+
/** Changelog-specific sidebar with Releases and shared Links */
|
|
33
|
+
export function ChangelogSidebar() {
|
|
34
|
+
const pathname = useLocation({ select: l => l.pathname })
|
|
35
|
+
const { toggleSidebar } = useSidebar()
|
|
36
|
+
|
|
37
|
+
useHotkey("S", toggleSidebar)
|
|
38
|
+
|
|
39
|
+
const section = currentSection(`/${pathname.replace(/^\/|\/$/g, "")}/`)
|
|
40
|
+
const flat = section ? (sectionFlatArticles.get(section.prefix) ?? []) : []
|
|
41
|
+
const entries = flat
|
|
42
|
+
.map(p => sortedArticles.find(a => a.path === p))
|
|
43
|
+
.filter(a => a !== undefined)
|
|
44
|
+
const recent = entries.slice(0, 10)
|
|
45
|
+
|
|
46
|
+
const normalized = `/${pathname.replace(/^\/|\/$/g, "")}/`
|
|
47
|
+
const isIndex = section ? normalized === section.prefix : false
|
|
48
|
+
const sectionPath = (section?.prefix ?? "/changelog/").replace(/^\/|\/$/g, "")
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<SidebarRoot>
|
|
52
|
+
<SidebarHeader>
|
|
53
|
+
<SidebarMenu>
|
|
54
|
+
<SidebarMenuItem>
|
|
55
|
+
<SidebarMenuButton size="lg" render={<Link to="/" />}>
|
|
56
|
+
<SiteTitle />
|
|
57
|
+
</SidebarMenuButton>
|
|
58
|
+
</SidebarMenuItem>
|
|
59
|
+
</SidebarMenu>
|
|
60
|
+
</SidebarHeader>
|
|
61
|
+
<SidebarContent>
|
|
62
|
+
<SidebarSections />
|
|
63
|
+
<SidebarGroup>
|
|
64
|
+
<SidebarGroupLabel className="uppercase font-mono text-xs tracking-widest">
|
|
65
|
+
Releases
|
|
66
|
+
</SidebarGroupLabel>
|
|
67
|
+
<SidebarGroupContent>
|
|
68
|
+
<SidebarMenu>
|
|
69
|
+
<SidebarMenuItem>
|
|
70
|
+
<SidebarMenuButton
|
|
71
|
+
isActive={isIndex}
|
|
72
|
+
className={isIndex ? "" : "opacity-75"}
|
|
73
|
+
render={<Link to="/$" params={{ _splat: sectionPath }} />}
|
|
74
|
+
>
|
|
75
|
+
<List className="size-4" />
|
|
76
|
+
<span>All Releases</span>
|
|
77
|
+
</SidebarMenuButton>
|
|
78
|
+
</SidebarMenuItem>
|
|
79
|
+
<SidebarMenuItem>
|
|
80
|
+
<SidebarMenuButton className="opacity-75">
|
|
81
|
+
<Clock className="size-4" />
|
|
82
|
+
<span>Recent Releases</span>
|
|
83
|
+
</SidebarMenuButton>
|
|
84
|
+
<SidebarMenuSub>
|
|
85
|
+
{recent.map(entry => {
|
|
86
|
+
const active = normalized === entry.path
|
|
87
|
+
return (
|
|
88
|
+
<SidebarMenuSubItem key={entry.path}>
|
|
89
|
+
<SidebarMenuSubButton
|
|
90
|
+
isActive={active}
|
|
91
|
+
className={active ? "" : "opacity-75"}
|
|
92
|
+
render={
|
|
93
|
+
<Link
|
|
94
|
+
to="/$"
|
|
95
|
+
params={{
|
|
96
|
+
_splat: entry.path.replace(/^\/|\/$/g, ""),
|
|
97
|
+
}}
|
|
98
|
+
/>
|
|
99
|
+
}
|
|
100
|
+
>
|
|
101
|
+
<span>{entry.title}</span>
|
|
102
|
+
</SidebarMenuSubButton>
|
|
103
|
+
</SidebarMenuSubItem>
|
|
104
|
+
)
|
|
105
|
+
})}
|
|
106
|
+
</SidebarMenuSub>
|
|
107
|
+
</SidebarMenuItem>
|
|
108
|
+
</SidebarMenu>
|
|
109
|
+
</SidebarGroupContent>
|
|
110
|
+
</SidebarGroup>
|
|
111
|
+
<SidebarLinks />
|
|
112
|
+
</SidebarContent>
|
|
113
|
+
<SidebarFooter className="p-4 space-y-2">
|
|
114
|
+
<Search />
|
|
115
|
+
<Theme />
|
|
116
|
+
</SidebarFooter>
|
|
117
|
+
<SidebarRail />
|
|
118
|
+
</SidebarRoot>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { Check, ChevronDown, Clipboard, WrapText } from "lucide-react"
|
|
2
2
|
import { useRef, useState } from "react"
|
|
3
|
+
import {
|
|
4
|
+
Tooltip,
|
|
5
|
+
TooltipContent,
|
|
6
|
+
TooltipTrigger,
|
|
7
|
+
} from "../elements/tooltip.tsx"
|
|
3
8
|
|
|
4
9
|
export function CodeBlock(props: React.ComponentProps<"pre">) {
|
|
5
10
|
const [copied, setCopied] = useState(false)
|
|
@@ -41,49 +46,61 @@ export function CodeBlock(props: React.ComponentProps<"pre">) {
|
|
|
41
46
|
/>
|
|
42
47
|
)}
|
|
43
48
|
<span className="flex-1 truncate text-xs font-medium">{title}</span>
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
49
|
+
<Tooltip>
|
|
50
|
+
<TooltipTrigger
|
|
51
|
+
onClick={handleWrapToggle}
|
|
52
|
+
aria-label={wrap ? "Disable wrap" : "Enable wrap"}
|
|
53
|
+
className={`-me-2 p-1.5 rounded-md hover:text-foreground transition-colors ${wrap ? "text-foreground" : ""}`}
|
|
54
|
+
>
|
|
55
|
+
<WrapText className="size-3.5" />
|
|
56
|
+
</TooltipTrigger>
|
|
57
|
+
<TooltipContent>
|
|
58
|
+
{wrap ? "Disable wrap" : "Enable wrap"}
|
|
59
|
+
</TooltipContent>
|
|
60
|
+
</Tooltip>
|
|
61
|
+
<Tooltip>
|
|
62
|
+
<TooltipTrigger
|
|
63
|
+
onClick={handleCopy}
|
|
64
|
+
aria-label={copied ? "Copied" : "Copy code"}
|
|
65
|
+
className="-me-2 p-1.5 rounded-md hover:text-foreground transition-colors"
|
|
66
|
+
>
|
|
67
|
+
{copied ? (
|
|
68
|
+
<Check className="size-3.5" />
|
|
69
|
+
) : (
|
|
70
|
+
<Clipboard className="size-3.5" />
|
|
71
|
+
)}
|
|
72
|
+
</TooltipTrigger>
|
|
73
|
+
<TooltipContent>{copied ? "Copied!" : "Copy code"}</TooltipContent>
|
|
74
|
+
</Tooltip>
|
|
64
75
|
</div>
|
|
65
76
|
) : (
|
|
66
77
|
<div className="empty:hidden absolute top-2 right-2 z-10 backdrop-blur-lg rounded-lg text-muted-foreground">
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
78
|
+
<Tooltip>
|
|
79
|
+
<TooltipTrigger
|
|
80
|
+
onClick={handleWrapToggle}
|
|
81
|
+
aria-label={wrap ? "Disable wrap" : "Enable wrap"}
|
|
82
|
+
className={`p-1.5 rounded-md hover:text-foreground transition-colors ${wrap ? "text-foreground" : ""}`}
|
|
83
|
+
>
|
|
84
|
+
<WrapText className="size-3.5" />
|
|
85
|
+
</TooltipTrigger>
|
|
86
|
+
<TooltipContent>
|
|
87
|
+
{wrap ? "Disable wrap" : "Enable wrap"}
|
|
88
|
+
</TooltipContent>
|
|
89
|
+
</Tooltip>
|
|
90
|
+
<Tooltip>
|
|
91
|
+
<TooltipTrigger
|
|
92
|
+
onClick={handleCopy}
|
|
93
|
+
aria-label={copied ? "Copied" : "Copy code"}
|
|
94
|
+
className="p-1.5 rounded-md hover:text-foreground transition-colors"
|
|
95
|
+
>
|
|
96
|
+
{copied ? (
|
|
97
|
+
<Check className="size-3.5" />
|
|
98
|
+
) : (
|
|
99
|
+
<Clipboard className="size-3.5" />
|
|
100
|
+
)}
|
|
101
|
+
</TooltipTrigger>
|
|
102
|
+
<TooltipContent>{copied ? "Copied!" : "Copy code"}</TooltipContent>
|
|
103
|
+
</Tooltip>
|
|
87
104
|
</div>
|
|
88
105
|
)}
|
|
89
106
|
<div
|
|
@@ -10,8 +10,3 @@ export function Columns(props: { cols?: string; children: React.ReactNode }) {
|
|
|
10
10
|
</div>
|
|
11
11
|
)
|
|
12
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
|
-
}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
/** Footer shown below article content */
|
|
1
2
|
export function Footer() {
|
|
2
3
|
return (
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
<p className="mt-12 text-center text-sm text-muted-foreground">
|
|
5
|
+
Created with <span className="text-red-500">❤</span> and{" "}
|
|
6
|
+
<a
|
|
7
|
+
href="https://github.com/datisthq/livemark"
|
|
8
|
+
target="_blank"
|
|
9
|
+
rel="noopener noreferrer"
|
|
10
|
+
className="font-medium hover:text-foreground transition-colors"
|
|
11
|
+
>
|
|
12
|
+
Livemark
|
|
13
|
+
</a>
|
|
14
|
+
</p>
|
|
8
15
|
)
|
|
9
16
|
}
|