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
@@ -1,21 +1,10 @@
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
- }
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 <Icon className={className} />
9
+ return <DynamicIcon name={props.name} className={className} />
21
10
  }
@@ -1,46 +1,42 @@
1
- import { useState } from "react"
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
- ChevronRight,
6
- ExternalLink,
7
- FileText,
8
- Search as SearchIcon,
9
- } from "lucide-react"
10
- import { articleTree } from "../helpers/articles.ts"
11
- import { articleIcons } from "../helpers/article-icon.ts"
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 { SearchDialog } from "./SearchDialog.tsx"
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
- export function Layout(props: { children?: React.ReactNode }) {
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
- return (
52
- <SidebarProvider>
53
- <AppSidebar />
54
- <SidebarInset>
55
- <header className="sticky top-0 z-20 flex h-16 shrink-0 items-center bg-background">
56
- <div className="flex items-center self-stretch pl-4 pr-0 border-b">
57
- <SidebarTrigger />
58
- </div>
59
- <div className="flex flex-1 items-center gap-8 self-stretch border-b px-6 text-sm">
60
- <Link
61
- to="/"
62
- className="text-foreground font-medium underline underline-offset-4"
63
- >
64
- Docs
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
- href="/blog"
68
- className="text-muted-foreground hover:text-foreground transition-colors"
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-muted-foreground hover:text-foreground transition-colors"
130
+ className="text-foreground opacity-80 hover:opacity-100 transition-opacity"
77
131
  >
78
- GitHub <ExternalLink className="inline size-3 -mt-0.5" />
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
- <Banner />
81
- </div>
82
- </header>
83
- <main className="flex-1">{props.children}</main>
84
- </SidebarInset>
85
- </SidebarProvider>
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
- useHotkey("S", toggleSidebar)
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
- <Sidebar>
97
- <SidebarHeader>
98
- <SidebarMenu>
99
- <SidebarMenuItem>
100
- <SidebarMenuButton size="lg" render={<Link to="/" />}>
101
- <SiteTitle />
102
- </SidebarMenuButton>
103
- </SidebarMenuItem>
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 splatFor(pathname: string) {
134
- return { _splat: pathname.replace(/^\/|\/$/g, "") }
135
- }
136
-
137
- function isActive(articlePathname: string, currentPath: string) {
138
- const normalized = `/${currentPath.replace(/^\/|\/$/g, "")}/`
139
- return articlePathname === normalized
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
- <Collapsible defaultOpen className="group/collapsible">
164
- <SidebarMenuItem>
165
- <SidebarMenuButton
166
- isActive={active}
167
- className={active ? "" : "opacity-75"}
168
- render={<Link to="/$" params={splatFor(node.pathname)} />}
169
- >
170
- <Icon className="size-4" />
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 { useEffect, useRef } from "react"
2
- import mermaid from "mermaid"
1
+ import { Suspense, lazy } from "react"
3
2
 
4
- mermaid.initialize({ startOnLoad: false })
3
+ const MermaidRenderer = lazy(() => import("./MermaidRenderer.tsx"))
5
4
 
6
- /** Renders a Mermaid diagram from a chart definition string */
5
+ /** Lazily loads and renders a Mermaid diagram */
7
6
  export function Mermaid(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" />
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 below the article content */
5
- export function PageToolbar(props: { filePath: string; content: string }) {
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
- <a
30
- href={`https://github.com/datisthq/livemark/edit/main/${props.filePath}`}
31
- target="_blank"
32
- rel="noopener noreferrer"
33
- className={buttonClass}
34
- >
35
- <Pencil className="size-3.5" />
36
- Edit on GitHub
37
- </a>
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 { sortedArticles, flatArticles } from "../helpers/articles.ts"
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: { pathname: string }) {
7
- const index = flatArticles.indexOf(props.pathname)
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 ? flatArticles[index - 1] : undefined
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.pathname === prevPath)
22
+ ? sortedArticles.find(a => a.path === prevPath)
15
23
  : undefined
16
24
  const next = nextPath
17
- ? sortedArticles.find(a => a.pathname === nextPath)
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.pathname.replace(/^\/|\/$/g, "") }}
28
- className="flex-1 rounded-xl border shadow-sm bg-sidebar p-4 hover:bg-background transition-colors"
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.pathname.replace(/^\/|\/$/g, "") }}
41
- className="flex-1 rounded-xl border shadow-sm bg-sidebar p-4 hover:bg-background transition-colors text-right"
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
+ }