livemark 0.0.0-dev
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +9 -0
- package/README.md +3 -0
- package/build/actions/config/define.d.ts +6 -0
- package/build/actions/config/define.js +8 -0
- package/build/actions/config/define.unit.d.ts +1 -0
- package/build/actions/config/define.unit.js +34 -0
- package/build/actions/config/load.d.ts +5 -0
- package/build/actions/config/load.js +22 -0
- package/build/actions/config/load.unit.d.ts +1 -0
- package/build/actions/config/load.unit.js +23 -0
- package/build/commands/build.d.ts +5 -0
- package/build/commands/build.js +13 -0
- package/build/commands/preview.d.ts +5 -0
- package/build/commands/preview.js +18 -0
- package/build/commands/start.d.ts +5 -0
- package/build/commands/start.js +19 -0
- package/build/docs/markdown/includes/example.d.ts +9 -0
- package/build/docs/markdown/includes/example.js +5 -0
- package/build/entrypoints/main.d.ts +2 -0
- package/build/entrypoints/main.js +12 -0
- package/build/helpers/program.d.ts +2 -0
- package/build/helpers/program.js +5 -0
- package/build/index.d.ts +1 -0
- package/build/index.js +2 -0
- package/build/livemark.config.d.ts +9 -0
- package/build/livemark.config.js +5 -0
- package/build/models/config.d.ts +33 -0
- package/build/models/config.js +14 -0
- package/build/program.d.ts +2 -0
- package/build/program.js +14 -0
- package/build/vite.config.d.ts +2 -0
- package/build/vite.config.js +48 -0
- package/build/website/client.tsx +10 -0
- package/build/website/components/Abbr.tsx +17 -0
- package/build/website/components/AnsiCode.tsx +15 -0
- package/build/website/components/Banner.tsx +32 -0
- package/build/website/components/Breadcrumbs.tsx +19 -0
- package/build/website/components/Callout.tsx +72 -0
- package/build/website/components/Cards.tsx +66 -0
- package/build/website/components/CodeBlock.tsx +118 -0
- package/build/website/components/CodeTabs.tsx +83 -0
- package/build/website/components/Columns.tsx +17 -0
- package/build/website/components/Content.tsx +7 -0
- package/build/website/components/ContentTab.tsx +7 -0
- package/build/website/components/ContentTabs.tsx +82 -0
- package/build/website/components/DefaultCatchBoundary.tsx +54 -0
- package/build/website/components/DefinitionList.tsx +21 -0
- package/build/website/components/Details.tsx +30 -0
- package/build/website/components/FileTree.tsx +52 -0
- package/build/website/components/Footer.tsx +9 -0
- package/build/website/components/FootnoteRef.tsx +52 -0
- package/build/website/components/Heading.tsx +20 -0
- package/build/website/components/InlineBadge.tsx +9 -0
- package/build/website/components/InlineIcon.tsx +21 -0
- package/build/website/components/InlineToc.tsx +31 -0
- package/build/website/components/Layout.tsx +220 -0
- package/build/website/components/LinkButton.tsx +31 -0
- package/build/website/components/Mermaid.tsx +37 -0
- package/build/website/components/NotFound.tsx +27 -0
- package/build/website/components/PackageTabs.tsx +74 -0
- package/build/website/components/PageToolbar.tsx +65 -0
- package/build/website/components/PrevNext.tsx +52 -0
- package/build/website/components/SearchDialog.tsx +245 -0
- package/build/website/components/Section.tsx +24 -0
- package/build/website/components/SectionHeader.tsx +35 -0
- package/build/website/components/SiteTitle.tsx +16 -0
- package/build/website/components/SortableTable.tsx +81 -0
- package/build/website/components/SoundCloud.tsx +16 -0
- package/build/website/components/Theme.tsx +40 -0
- package/build/website/components/Toc.tsx +99 -0
- package/build/website/components/YouTube.tsx +16 -0
- package/build/website/components/ZoomImage.tsx +30 -0
- package/build/website/content-collections.ts +181 -0
- package/build/website/elements/accordion.tsx +78 -0
- package/build/website/elements/alert-dialog.tsx +185 -0
- package/build/website/elements/alert.tsx +74 -0
- package/build/website/elements/aspect-ratio.tsx +22 -0
- package/build/website/elements/avatar.tsx +107 -0
- package/build/website/elements/badge.tsx +51 -0
- package/build/website/elements/breadcrumb.tsx +120 -0
- package/build/website/elements/button-group.tsx +87 -0
- package/build/website/elements/button.tsx +57 -0
- package/build/website/elements/calendar.tsx +229 -0
- package/build/website/elements/card.tsx +102 -0
- package/build/website/elements/carousel.tsx +240 -0
- package/build/website/elements/chart.tsx +372 -0
- package/build/website/elements/checkbox.tsx +26 -0
- package/build/website/elements/collapsible.tsx +19 -0
- package/build/website/elements/command.tsx +210 -0
- package/build/website/elements/context-menu.tsx +270 -0
- package/build/website/elements/dialog.tsx +152 -0
- package/build/website/elements/direction.tsx +4 -0
- package/build/website/elements/drawer.tsx +127 -0
- package/build/website/elements/dropdown-menu.tsx +260 -0
- package/build/website/elements/empty.tsx +104 -0
- package/build/website/elements/field.tsx +235 -0
- package/build/website/elements/hover-card.tsx +49 -0
- package/build/website/elements/input-group.tsx +171 -0
- package/build/website/elements/input-otp.tsx +84 -0
- package/build/website/elements/input.tsx +18 -0
- package/build/website/elements/item.tsx +199 -0
- package/build/website/elements/kbd.tsx +26 -0
- package/build/website/elements/label.tsx +17 -0
- package/build/website/elements/menubar.tsx +282 -0
- package/build/website/elements/native-select.tsx +56 -0
- package/build/website/elements/navigation-menu.tsx +171 -0
- package/build/website/elements/pagination.tsx +133 -0
- package/build/website/elements/popover.tsx +88 -0
- package/build/website/elements/progress.tsx +81 -0
- package/build/website/elements/radio-group.tsx +36 -0
- package/build/website/elements/resizable.tsx +48 -0
- package/build/website/elements/scroll-area.tsx +51 -0
- package/build/website/elements/select.tsx +200 -0
- package/build/website/elements/separator.tsx +22 -0
- package/build/website/elements/sheet.tsx +130 -0
- package/build/website/elements/sidebar.tsx +717 -0
- package/build/website/elements/skeleton.tsx +13 -0
- package/build/website/elements/slider.tsx +57 -0
- package/build/website/elements/sonner.tsx +40 -0
- package/build/website/elements/spinner.tsx +16 -0
- package/build/website/elements/switch.tsx +30 -0
- package/build/website/elements/table.tsx +112 -0
- package/build/website/elements/tabs.tsx +79 -0
- package/build/website/elements/textarea.tsx +16 -0
- package/build/website/elements/toggle-group.tsx +87 -0
- package/build/website/elements/toggle.tsx +43 -0
- package/build/website/elements/tooltip.tsx +67 -0
- package/build/website/env.d.ts +10 -0
- package/build/website/globals.d.ts +8 -0
- package/build/website/helpers/article-icon.ts +59 -0
- package/build/website/helpers/articles.ts +24 -0
- package/build/website/helpers/resolve-includes.ts +131 -0
- package/build/website/helpers/resolve-includes.unit.ts +126 -0
- package/build/website/helpers/search-text.ts +15 -0
- package/build/website/helpers/style.ts +6 -0
- package/build/website/helpers/theme.ts +27 -0
- package/build/website/helpers/toc-context.ts +10 -0
- package/build/website/helpers/toc.ts +51 -0
- package/build/website/helpers/toc.unit.ts +74 -0
- package/build/website/hooks/in-view.ts +30 -0
- package/build/website/hooks/media.ts +19 -0
- package/build/website/models/article.ts +40 -0
- package/build/website/plugins/rehype-inline-code.ts +107 -0
- package/build/website/plugins/remark-abbr.ts +46 -0
- package/build/website/plugins/remark-abbr.unit.ts +99 -0
- package/build/website/plugins/remark-ansi.ts +247 -0
- package/build/website/plugins/remark-ansi.unit.ts +197 -0
- package/build/website/plugins/remark-badge.ts +50 -0
- package/build/website/plugins/remark-badge.unit.ts +96 -0
- package/build/website/plugins/remark-button.ts +60 -0
- package/build/website/plugins/remark-button.unit.ts +147 -0
- package/build/website/plugins/remark-callout.ts +57 -0
- package/build/website/plugins/remark-card.ts +80 -0
- package/build/website/plugins/remark-code-tabs.ts +87 -0
- package/build/website/plugins/remark-columns.ts +66 -0
- package/build/website/plugins/remark-custom-heading-id.ts +70 -0
- package/build/website/plugins/remark-custom-heading-id.unit.ts +131 -0
- package/build/website/plugins/remark-deflist.ts +36 -0
- package/build/website/plugins/remark-details.ts +49 -0
- package/build/website/plugins/remark-filetree.ts +89 -0
- package/build/website/plugins/remark-filetree.unit.ts +137 -0
- package/build/website/plugins/remark-github-callout.ts +71 -0
- package/build/website/plugins/remark-icon.ts +42 -0
- package/build/website/plugins/remark-image.ts +25 -0
- package/build/website/plugins/remark-media.ts +62 -0
- package/build/website/plugins/remark-media.unit.ts +74 -0
- package/build/website/plugins/remark-mermaid.ts +23 -0
- package/build/website/plugins/remark-npm.ts +52 -0
- package/build/website/plugins/remark-steps.ts +36 -0
- package/build/website/plugins/remark-steps.unit.ts +97 -0
- package/build/website/plugins/remark-tab.ts +100 -0
- package/build/website/plugins/remark-tab.unit.ts +181 -0
- package/build/website/plugins/remark-toc.ts +39 -0
- package/build/website/plugins/remark-toc.unit.ts +82 -0
- package/build/website/plugins/shiki-icon.ts +82 -0
- package/build/website/plugins/shiki-line-highlight.ts +174 -0
- package/build/website/public/logo.svg +72 -0
- package/build/website/routeTree.gen.ts +86 -0
- package/build/website/router.tsx +34 -0
- package/build/website/routes/$.tsx +149 -0
- package/build/website/routes/__root.tsx +84 -0
- package/build/website/routes/index.tsx +13 -0
- package/build/website/server.ts +7 -0
- package/build/website/styles/general.css +347 -0
- package/build/website/vite.config.ts +36 -0
- package/entrypoints/main.ts +12 -0
- package/package.json +120 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useTocItems } from "../helpers/toc-context.ts"
|
|
2
|
+
|
|
3
|
+
/** Renders the page's table of contents inline within the content */
|
|
4
|
+
export function InlineToc(props: { maxLevel?: string }) {
|
|
5
|
+
const allItems = useTocItems()
|
|
6
|
+
const maxDepth = props.maxLevel ? Number(props.maxLevel) : 4
|
|
7
|
+
const items = allItems.filter(item => item.depth <= maxDepth)
|
|
8
|
+
|
|
9
|
+
if (items.length === 0) return null
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<nav className="not-prose my-8 rounded-xl border border-border bg-card p-6">
|
|
13
|
+
<p className="text-base font-semibold mb-4">Table of Contents</p>
|
|
14
|
+
<ul className="text-base space-y-2.5">
|
|
15
|
+
{items.map(item => (
|
|
16
|
+
<li
|
|
17
|
+
key={item.url}
|
|
18
|
+
style={{ paddingLeft: `${(item.depth - 2) * 1.25}rem` }}
|
|
19
|
+
>
|
|
20
|
+
<a
|
|
21
|
+
href={item.url}
|
|
22
|
+
className="text-muted-foreground hover:text-primary transition-colors"
|
|
23
|
+
>
|
|
24
|
+
{item.title}
|
|
25
|
+
</a>
|
|
26
|
+
</li>
|
|
27
|
+
))}
|
|
28
|
+
</ul>
|
|
29
|
+
</nav>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { useState } from "react"
|
|
2
|
+
import { Link, useLocation } from "@tanstack/react-router"
|
|
3
|
+
import { useHotkey } from "@tanstack/react-hotkeys"
|
|
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"
|
|
13
|
+
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
|
+
SidebarInset,
|
|
27
|
+
SidebarMenu,
|
|
28
|
+
SidebarMenuButton,
|
|
29
|
+
SidebarMenuItem,
|
|
30
|
+
SidebarMenuSub,
|
|
31
|
+
SidebarMenuSubButton,
|
|
32
|
+
SidebarMenuSubItem,
|
|
33
|
+
SidebarProvider,
|
|
34
|
+
SidebarRail,
|
|
35
|
+
SidebarTrigger,
|
|
36
|
+
useSidebar,
|
|
37
|
+
} from "../elements/sidebar.tsx"
|
|
38
|
+
import { Banner } from "./Banner.tsx"
|
|
39
|
+
import { SearchDialog } from "./SearchDialog.tsx"
|
|
40
|
+
import { SiteTitle } from "./SiteTitle.tsx"
|
|
41
|
+
import { Theme } from "./Theme.tsx"
|
|
42
|
+
|
|
43
|
+
export function Layout(props: { children?: React.ReactNode }) {
|
|
44
|
+
useHotkey("J", () =>
|
|
45
|
+
window.scrollBy({ top: window.innerHeight * 0.8, behavior: "smooth" }),
|
|
46
|
+
)
|
|
47
|
+
useHotkey("K", () =>
|
|
48
|
+
window.scrollBy({ top: -window.innerHeight * 0.8, behavior: "smooth" }),
|
|
49
|
+
)
|
|
50
|
+
|
|
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
|
|
65
|
+
</Link>
|
|
66
|
+
<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"
|
|
74
|
+
target="_blank"
|
|
75
|
+
rel="noopener noreferrer"
|
|
76
|
+
className="text-muted-foreground hover:text-foreground transition-colors"
|
|
77
|
+
>
|
|
78
|
+
GitHub <ExternalLink className="inline size-3 -mt-0.5" />
|
|
79
|
+
</a>
|
|
80
|
+
<Banner />
|
|
81
|
+
</div>
|
|
82
|
+
</header>
|
|
83
|
+
<main className="flex-1">{props.children}</main>
|
|
84
|
+
</SidebarInset>
|
|
85
|
+
</SidebarProvider>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function AppSidebar() {
|
|
90
|
+
const pathname = useLocation({ select: l => l.pathname })
|
|
91
|
+
const { toggleSidebar } = useSidebar()
|
|
92
|
+
|
|
93
|
+
useHotkey("S", toggleSidebar)
|
|
94
|
+
|
|
95
|
+
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>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
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
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
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
|
+
</>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { buttonVariants } from "../elements/button.tsx"
|
|
2
|
+
import { cn } from "../helpers/style.ts"
|
|
3
|
+
|
|
4
|
+
/** Styled link button rendered from ::button leaf directives */
|
|
5
|
+
export function LinkButton(props: {
|
|
6
|
+
href: string
|
|
7
|
+
label: string
|
|
8
|
+
variant?:
|
|
9
|
+
| "default"
|
|
10
|
+
| "outline"
|
|
11
|
+
| "secondary"
|
|
12
|
+
| "ghost"
|
|
13
|
+
| "destructive"
|
|
14
|
+
| "link"
|
|
15
|
+
size?: "default" | "sm" | "lg"
|
|
16
|
+
}) {
|
|
17
|
+
return (
|
|
18
|
+
<a
|
|
19
|
+
href={props.href}
|
|
20
|
+
className={cn(
|
|
21
|
+
buttonVariants({
|
|
22
|
+
variant: props.variant,
|
|
23
|
+
size: props.size,
|
|
24
|
+
}),
|
|
25
|
+
"no-underline",
|
|
26
|
+
)}
|
|
27
|
+
>
|
|
28
|
+
{props.label}
|
|
29
|
+
</a>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -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 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" />
|
|
37
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Link } from "@tanstack/react-router"
|
|
2
|
+
|
|
3
|
+
export function NotFound({ children }: { children?: any }) {
|
|
4
|
+
return (
|
|
5
|
+
<div className="min-h-[60vh] flex flex-col items-center justify-center text-center px-4">
|
|
6
|
+
<h1 className="font-sans text-6xl font-semibold mb-4">404</h1>
|
|
7
|
+
<div className="text-muted-foreground mb-6">
|
|
8
|
+
{children || <p>The page you're looking for doesn't exist.</p>}
|
|
9
|
+
</div>
|
|
10
|
+
<div className="flex items-center gap-4">
|
|
11
|
+
<button
|
|
12
|
+
type="button"
|
|
13
|
+
onClick={() => window.history.back()}
|
|
14
|
+
className="bg-primary text-primary-foreground px-4 py-2 text-sm font-medium hover:bg-primary/80 transition-colors"
|
|
15
|
+
>
|
|
16
|
+
Go Back
|
|
17
|
+
</button>
|
|
18
|
+
<Link
|
|
19
|
+
to="/"
|
|
20
|
+
className="border border-border px-4 py-2 text-sm font-medium hover:bg-card transition-colors"
|
|
21
|
+
>
|
|
22
|
+
Home
|
|
23
|
+
</Link>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Check, Clipboard } from "lucide-react"
|
|
2
|
+
import { useState } from "react"
|
|
3
|
+
|
|
4
|
+
type Manager = "npm" | "pnpm" | "yarn" | "bun"
|
|
5
|
+
|
|
6
|
+
const managers: Manager[] = ["npm", "pnpm", "yarn", "bun"]
|
|
7
|
+
|
|
8
|
+
interface PackageTabsProps {
|
|
9
|
+
npm: string
|
|
10
|
+
pnpm: string
|
|
11
|
+
yarn: string
|
|
12
|
+
bun: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Tabbed code block showing a command for each package manager */
|
|
16
|
+
export function PackageTabs(props: PackageTabsProps) {
|
|
17
|
+
const [active, setActive] = useState<Manager>("npm")
|
|
18
|
+
const [copied, setCopied] = useState(false)
|
|
19
|
+
|
|
20
|
+
const commands: Record<Manager, string> = {
|
|
21
|
+
npm: props.npm,
|
|
22
|
+
pnpm: props.pnpm,
|
|
23
|
+
yarn: props.yarn,
|
|
24
|
+
bun: props.bun,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const handleCopy = () => {
|
|
28
|
+
navigator.clipboard.writeText(commands[active])
|
|
29
|
+
setCopied(true)
|
|
30
|
+
setTimeout(() => setCopied(false), 2000)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<figure className="not-prose my-4 relative rounded-xl border shadow-sm overflow-hidden text-sm bg-sidebar dark:bg-sidebar">
|
|
35
|
+
<div className="flex items-center border-b">
|
|
36
|
+
<div className="flex flex-1 items-center">
|
|
37
|
+
{managers.map(pm => (
|
|
38
|
+
<button
|
|
39
|
+
key={pm}
|
|
40
|
+
type="button"
|
|
41
|
+
onClick={() => setActive(pm)}
|
|
42
|
+
className={`px-3 py-2 text-xs font-medium transition-colors ${
|
|
43
|
+
pm === active
|
|
44
|
+
? "text-foreground border-b-2 border-foreground"
|
|
45
|
+
: "text-muted-foreground hover:text-foreground"
|
|
46
|
+
}`}
|
|
47
|
+
>
|
|
48
|
+
{pm}
|
|
49
|
+
</button>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
onClick={handleCopy}
|
|
55
|
+
className="me-2 p-1 rounded-md text-muted-foreground hover:text-foreground transition-colors"
|
|
56
|
+
aria-label="Copy code"
|
|
57
|
+
>
|
|
58
|
+
{copied ? (
|
|
59
|
+
<Check className="size-3.5" />
|
|
60
|
+
) : (
|
|
61
|
+
<Clipboard className="size-3.5" />
|
|
62
|
+
)}
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
65
|
+
<div className="text-[0.8125rem] py-3.5 overflow-auto max-h-[600px]">
|
|
66
|
+
<pre className="min-w-full w-max">
|
|
67
|
+
<code className="flex flex-col">
|
|
68
|
+
<span className="px-4">{commands[active]}</span>
|
|
69
|
+
</code>
|
|
70
|
+
</pre>
|
|
71
|
+
</div>
|
|
72
|
+
</figure>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useState, useCallback } from "react"
|
|
2
|
+
import { Pencil, Copy, Check, ExternalLink } from "lucide-react"
|
|
3
|
+
|
|
4
|
+
/** Toolbar with action buttons displayed below the article content */
|
|
5
|
+
export function PageToolbar(props: { filePath: string; content: string }) {
|
|
6
|
+
const [copied, setCopied] = useState(false)
|
|
7
|
+
|
|
8
|
+
const handleCopy = useCallback(() => {
|
|
9
|
+
navigator.clipboard.writeText(props.content)
|
|
10
|
+
setCopied(true)
|
|
11
|
+
setTimeout(() => setCopied(false), 2000)
|
|
12
|
+
}, [props.content])
|
|
13
|
+
|
|
14
|
+
const handleOpenIn = useCallback(
|
|
15
|
+
(url: string) => {
|
|
16
|
+
navigator.clipboard.writeText(props.content)
|
|
17
|
+
window.open(url, "_blank", "noopener,noreferrer")
|
|
18
|
+
},
|
|
19
|
+
[props.content],
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
const buttonClass =
|
|
23
|
+
"flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
<div className="mb-6 flex flex-col gap-2 text-sm">
|
|
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>
|
|
38
|
+
<button type="button" onClick={handleCopy} className={buttonClass}>
|
|
39
|
+
{copied ? (
|
|
40
|
+
<Check className="size-3.5" />
|
|
41
|
+
) : (
|
|
42
|
+
<Copy className="size-3.5" />
|
|
43
|
+
)}
|
|
44
|
+
Copy Markdown
|
|
45
|
+
</button>
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
onClick={() => handleOpenIn("https://chatgpt.com/")}
|
|
49
|
+
className={buttonClass}
|
|
50
|
+
>
|
|
51
|
+
<ExternalLink className="size-3.5" />
|
|
52
|
+
Open in ChatGPT
|
|
53
|
+
</button>
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
onClick={() => handleOpenIn("https://claude.ai/")}
|
|
57
|
+
className={buttonClass}
|
|
58
|
+
>
|
|
59
|
+
<ExternalLink className="size-3.5" />
|
|
60
|
+
Open in Claude
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
</>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Link } from "@tanstack/react-router"
|
|
2
|
+
import { ChevronLeft, ChevronRight } from "lucide-react"
|
|
3
|
+
import { sortedArticles, flatArticles } from "../helpers/articles.ts"
|
|
4
|
+
|
|
5
|
+
/** 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)
|
|
8
|
+
if (index < 0) return null
|
|
9
|
+
|
|
10
|
+
const prevPath = index > 0 ? flatArticles[index - 1] : undefined
|
|
11
|
+
const nextPath =
|
|
12
|
+
index < flatArticles.length - 1 ? flatArticles[index + 1] : undefined
|
|
13
|
+
const prev = prevPath
|
|
14
|
+
? sortedArticles.find(a => a.pathname === prevPath)
|
|
15
|
+
: undefined
|
|
16
|
+
const next = nextPath
|
|
17
|
+
? sortedArticles.find(a => a.pathname === nextPath)
|
|
18
|
+
: undefined
|
|
19
|
+
|
|
20
|
+
if (!prev && !next) return null
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<nav className="not-prose mt-10 flex gap-4">
|
|
24
|
+
{prev && (
|
|
25
|
+
<Link
|
|
26
|
+
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"
|
|
29
|
+
>
|
|
30
|
+
<span className="flex items-center gap-1 text-sm text-muted-foreground">
|
|
31
|
+
<ChevronLeft className="size-4" />
|
|
32
|
+
Previous
|
|
33
|
+
</span>
|
|
34
|
+
<span className="mt-1 block font-medium">{prev.title}</span>
|
|
35
|
+
</Link>
|
|
36
|
+
)}
|
|
37
|
+
{next && (
|
|
38
|
+
<Link
|
|
39
|
+
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"
|
|
42
|
+
>
|
|
43
|
+
<span className="flex items-center justify-end gap-1 text-sm text-muted-foreground">
|
|
44
|
+
Next
|
|
45
|
+
<ChevronRight className="size-4" />
|
|
46
|
+
</span>
|
|
47
|
+
<span className="mt-1 block font-medium">{next.title}</span>
|
|
48
|
+
</Link>
|
|
49
|
+
)}
|
|
50
|
+
</nav>
|
|
51
|
+
)
|
|
52
|
+
}
|