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.
Files changed (94) hide show
  1. package/README.md +5 -1
  2. package/build/actions/changelog/build.d.ts +14 -0
  3. package/build/actions/changelog/build.js +158 -0
  4. package/build/actions/changelog/build.unit.d.ts +1 -0
  5. package/build/actions/changelog/build.unit.js +66 -0
  6. package/build/actions/config/define.unit.js +11 -16
  7. package/build/actions/config/load.js +6 -3
  8. package/build/actions/config/load.unit.js +3 -3
  9. package/build/commands/build.js +4 -3
  10. package/build/commands/escape.d.ts +9 -0
  11. package/build/commands/escape.js +56 -0
  12. package/build/docs/markdown/includes/example.d.ts +53 -4
  13. package/build/docs/markdown/includes/example.js +2 -2
  14. package/build/livemark.config.d.ts +53 -4
  15. package/build/livemark.config.js +22 -2
  16. package/build/models/config.d.ts +315 -8
  17. package/build/models/config.js +25 -5
  18. package/build/models/link.d.ts +13 -0
  19. package/build/models/link.js +9 -0
  20. package/build/models/patch.d.ts +21 -0
  21. package/build/models/patch.js +7 -0
  22. package/build/models/section.d.ts +75 -0
  23. package/build/models/section.js +26 -0
  24. package/build/models/theme.d.ts +72 -0
  25. package/build/models/theme.js +73 -0
  26. package/build/program.js +4 -2
  27. package/build/vite.config.d.ts +1 -1
  28. package/build/website/assets/logo.svg +117 -0
  29. package/build/website/components/Article.tsx +172 -0
  30. package/build/website/components/BackToTop.tsx +32 -0
  31. package/build/website/components/Banner.tsx +2 -2
  32. package/build/website/components/BlogIndex.tsx +121 -0
  33. package/build/website/components/BlogSidebar.tsx +162 -0
  34. package/build/website/components/Breadcrumbs.tsx +5 -2
  35. package/build/website/components/Card.tsx +48 -0
  36. package/build/website/components/Cards.tsx +0 -58
  37. package/build/website/components/ChangelogIndex.tsx +111 -0
  38. package/build/website/components/ChangelogSidebar.tsx +120 -0
  39. package/build/website/components/CodeBlock.tsx +57 -40
  40. package/build/website/components/Column.tsx +4 -0
  41. package/build/website/components/Columns.tsx +0 -5
  42. package/build/website/components/Footer.tsx +12 -5
  43. package/build/website/components/InlineIcon.tsx +2 -13
  44. package/build/website/components/Layout.tsx +155 -185
  45. package/build/website/components/Mermaid.tsx +14 -33
  46. package/build/website/components/MermaidRenderer.tsx +37 -0
  47. package/build/website/components/MiniSidebar.tsx +49 -0
  48. package/build/website/components/PageToolbar.tsx +21 -11
  49. package/build/website/components/PrevNext.tsx +20 -12
  50. package/build/website/components/Search.tsx +28 -0
  51. package/build/website/components/SearchDialog.tsx +10 -10
  52. package/build/website/components/Sidebar.tsx +160 -0
  53. package/build/website/components/SidebarLinks.tsx +70 -0
  54. package/build/website/components/SidebarSections.tsx +71 -0
  55. package/build/website/components/SiteTitle.tsx +13 -8
  56. package/build/website/components/TagIndex.tsx +120 -0
  57. package/build/website/components/Theme.tsx +0 -1
  58. package/build/website/components/Toc.tsx +128 -1
  59. package/build/website/components/ZoomImage.tsx +15 -20
  60. package/build/website/content/article.ts +78 -0
  61. package/build/website/content/tag.ts +29 -0
  62. package/build/website/content-collections.ts +53 -26
  63. package/build/website/elements/sidebar.tsx +4 -4
  64. package/build/website/env.d.ts +4 -0
  65. package/build/website/helpers/article-icon.ts +2 -0
  66. package/build/website/helpers/article.ts +90 -0
  67. package/build/website/helpers/changelog-source.ts +16 -0
  68. package/build/website/helpers/changelog-source.unit.ts +38 -0
  69. package/build/website/helpers/dynamic-icon-resolver.tsx +19 -0
  70. package/build/website/helpers/dynamic-icon.tsx +12 -0
  71. package/build/website/helpers/favicon.ts +7 -0
  72. package/build/website/helpers/model.ts +4 -0
  73. package/build/website/helpers/resolve-asset-path.ts +9 -0
  74. package/build/website/helpers/section.ts +50 -0
  75. package/build/website/helpers/section.unit.ts +71 -0
  76. package/build/website/helpers/toc-context.ts +1 -1
  77. package/build/website/helpers/toc.ts +1 -6
  78. package/build/website/models/article.ts +48 -33
  79. package/build/website/models/toc.ts +5 -0
  80. package/build/website/plugins/rehype-task-list-a11y.ts +33 -0
  81. package/build/website/plugins/remark-ansi.ts +1 -0
  82. package/build/website/plugins/remark-image.ts +22 -13
  83. package/build/website/plugins/vite-livemark.ts +91 -0
  84. package/build/website/routeTree.gen.ts +9 -9
  85. package/build/website/routes/$.tsx +74 -131
  86. package/build/website/routes/__root.tsx +36 -23
  87. package/build/website/styles/general.css +26 -210
  88. package/build/website/styles/markdown.css +214 -0
  89. package/build/website/vite.config.ts +38 -3
  90. package/package.json +25 -16
  91. package/build/website/globals.d.ts +0 -8
  92. package/build/website/helpers/articles.ts +0 -24
  93. package/build/website/public/logo.svg +0 -72
  94. 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
- Docs
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
- <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>
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
- <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>
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
@@ -0,0 +1,4 @@
1
+ /** Single column within a Columns grid */
2
+ export function Column(props: { children: React.ReactNode }) {
3
+ return <div className="max-w-none">{props.children}</div>
4
+ }
@@ -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
- <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>&copy; {new Date().getFullYear()} My Project</span>
6
- </div>
7
- </footer>
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
  }