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.
Files changed (187) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +3 -0
  3. package/build/actions/config/define.d.ts +6 -0
  4. package/build/actions/config/define.js +8 -0
  5. package/build/actions/config/define.unit.d.ts +1 -0
  6. package/build/actions/config/define.unit.js +34 -0
  7. package/build/actions/config/load.d.ts +5 -0
  8. package/build/actions/config/load.js +22 -0
  9. package/build/actions/config/load.unit.d.ts +1 -0
  10. package/build/actions/config/load.unit.js +23 -0
  11. package/build/commands/build.d.ts +5 -0
  12. package/build/commands/build.js +13 -0
  13. package/build/commands/preview.d.ts +5 -0
  14. package/build/commands/preview.js +18 -0
  15. package/build/commands/start.d.ts +5 -0
  16. package/build/commands/start.js +19 -0
  17. package/build/docs/markdown/includes/example.d.ts +9 -0
  18. package/build/docs/markdown/includes/example.js +5 -0
  19. package/build/entrypoints/main.d.ts +2 -0
  20. package/build/entrypoints/main.js +12 -0
  21. package/build/helpers/program.d.ts +2 -0
  22. package/build/helpers/program.js +5 -0
  23. package/build/index.d.ts +1 -0
  24. package/build/index.js +2 -0
  25. package/build/livemark.config.d.ts +9 -0
  26. package/build/livemark.config.js +5 -0
  27. package/build/models/config.d.ts +33 -0
  28. package/build/models/config.js +14 -0
  29. package/build/program.d.ts +2 -0
  30. package/build/program.js +14 -0
  31. package/build/vite.config.d.ts +2 -0
  32. package/build/vite.config.js +48 -0
  33. package/build/website/client.tsx +10 -0
  34. package/build/website/components/Abbr.tsx +17 -0
  35. package/build/website/components/AnsiCode.tsx +15 -0
  36. package/build/website/components/Banner.tsx +32 -0
  37. package/build/website/components/Breadcrumbs.tsx +19 -0
  38. package/build/website/components/Callout.tsx +72 -0
  39. package/build/website/components/Cards.tsx +66 -0
  40. package/build/website/components/CodeBlock.tsx +118 -0
  41. package/build/website/components/CodeTabs.tsx +83 -0
  42. package/build/website/components/Columns.tsx +17 -0
  43. package/build/website/components/Content.tsx +7 -0
  44. package/build/website/components/ContentTab.tsx +7 -0
  45. package/build/website/components/ContentTabs.tsx +82 -0
  46. package/build/website/components/DefaultCatchBoundary.tsx +54 -0
  47. package/build/website/components/DefinitionList.tsx +21 -0
  48. package/build/website/components/Details.tsx +30 -0
  49. package/build/website/components/FileTree.tsx +52 -0
  50. package/build/website/components/Footer.tsx +9 -0
  51. package/build/website/components/FootnoteRef.tsx +52 -0
  52. package/build/website/components/Heading.tsx +20 -0
  53. package/build/website/components/InlineBadge.tsx +9 -0
  54. package/build/website/components/InlineIcon.tsx +21 -0
  55. package/build/website/components/InlineToc.tsx +31 -0
  56. package/build/website/components/Layout.tsx +220 -0
  57. package/build/website/components/LinkButton.tsx +31 -0
  58. package/build/website/components/Mermaid.tsx +37 -0
  59. package/build/website/components/NotFound.tsx +27 -0
  60. package/build/website/components/PackageTabs.tsx +74 -0
  61. package/build/website/components/PageToolbar.tsx +65 -0
  62. package/build/website/components/PrevNext.tsx +52 -0
  63. package/build/website/components/SearchDialog.tsx +245 -0
  64. package/build/website/components/Section.tsx +24 -0
  65. package/build/website/components/SectionHeader.tsx +35 -0
  66. package/build/website/components/SiteTitle.tsx +16 -0
  67. package/build/website/components/SortableTable.tsx +81 -0
  68. package/build/website/components/SoundCloud.tsx +16 -0
  69. package/build/website/components/Theme.tsx +40 -0
  70. package/build/website/components/Toc.tsx +99 -0
  71. package/build/website/components/YouTube.tsx +16 -0
  72. package/build/website/components/ZoomImage.tsx +30 -0
  73. package/build/website/content-collections.ts +181 -0
  74. package/build/website/elements/accordion.tsx +78 -0
  75. package/build/website/elements/alert-dialog.tsx +185 -0
  76. package/build/website/elements/alert.tsx +74 -0
  77. package/build/website/elements/aspect-ratio.tsx +22 -0
  78. package/build/website/elements/avatar.tsx +107 -0
  79. package/build/website/elements/badge.tsx +51 -0
  80. package/build/website/elements/breadcrumb.tsx +120 -0
  81. package/build/website/elements/button-group.tsx +87 -0
  82. package/build/website/elements/button.tsx +57 -0
  83. package/build/website/elements/calendar.tsx +229 -0
  84. package/build/website/elements/card.tsx +102 -0
  85. package/build/website/elements/carousel.tsx +240 -0
  86. package/build/website/elements/chart.tsx +372 -0
  87. package/build/website/elements/checkbox.tsx +26 -0
  88. package/build/website/elements/collapsible.tsx +19 -0
  89. package/build/website/elements/command.tsx +210 -0
  90. package/build/website/elements/context-menu.tsx +270 -0
  91. package/build/website/elements/dialog.tsx +152 -0
  92. package/build/website/elements/direction.tsx +4 -0
  93. package/build/website/elements/drawer.tsx +127 -0
  94. package/build/website/elements/dropdown-menu.tsx +260 -0
  95. package/build/website/elements/empty.tsx +104 -0
  96. package/build/website/elements/field.tsx +235 -0
  97. package/build/website/elements/hover-card.tsx +49 -0
  98. package/build/website/elements/input-group.tsx +171 -0
  99. package/build/website/elements/input-otp.tsx +84 -0
  100. package/build/website/elements/input.tsx +18 -0
  101. package/build/website/elements/item.tsx +199 -0
  102. package/build/website/elements/kbd.tsx +26 -0
  103. package/build/website/elements/label.tsx +17 -0
  104. package/build/website/elements/menubar.tsx +282 -0
  105. package/build/website/elements/native-select.tsx +56 -0
  106. package/build/website/elements/navigation-menu.tsx +171 -0
  107. package/build/website/elements/pagination.tsx +133 -0
  108. package/build/website/elements/popover.tsx +88 -0
  109. package/build/website/elements/progress.tsx +81 -0
  110. package/build/website/elements/radio-group.tsx +36 -0
  111. package/build/website/elements/resizable.tsx +48 -0
  112. package/build/website/elements/scroll-area.tsx +51 -0
  113. package/build/website/elements/select.tsx +200 -0
  114. package/build/website/elements/separator.tsx +22 -0
  115. package/build/website/elements/sheet.tsx +130 -0
  116. package/build/website/elements/sidebar.tsx +717 -0
  117. package/build/website/elements/skeleton.tsx +13 -0
  118. package/build/website/elements/slider.tsx +57 -0
  119. package/build/website/elements/sonner.tsx +40 -0
  120. package/build/website/elements/spinner.tsx +16 -0
  121. package/build/website/elements/switch.tsx +30 -0
  122. package/build/website/elements/table.tsx +112 -0
  123. package/build/website/elements/tabs.tsx +79 -0
  124. package/build/website/elements/textarea.tsx +16 -0
  125. package/build/website/elements/toggle-group.tsx +87 -0
  126. package/build/website/elements/toggle.tsx +43 -0
  127. package/build/website/elements/tooltip.tsx +67 -0
  128. package/build/website/env.d.ts +10 -0
  129. package/build/website/globals.d.ts +8 -0
  130. package/build/website/helpers/article-icon.ts +59 -0
  131. package/build/website/helpers/articles.ts +24 -0
  132. package/build/website/helpers/resolve-includes.ts +131 -0
  133. package/build/website/helpers/resolve-includes.unit.ts +126 -0
  134. package/build/website/helpers/search-text.ts +15 -0
  135. package/build/website/helpers/style.ts +6 -0
  136. package/build/website/helpers/theme.ts +27 -0
  137. package/build/website/helpers/toc-context.ts +10 -0
  138. package/build/website/helpers/toc.ts +51 -0
  139. package/build/website/helpers/toc.unit.ts +74 -0
  140. package/build/website/hooks/in-view.ts +30 -0
  141. package/build/website/hooks/media.ts +19 -0
  142. package/build/website/models/article.ts +40 -0
  143. package/build/website/plugins/rehype-inline-code.ts +107 -0
  144. package/build/website/plugins/remark-abbr.ts +46 -0
  145. package/build/website/plugins/remark-abbr.unit.ts +99 -0
  146. package/build/website/plugins/remark-ansi.ts +247 -0
  147. package/build/website/plugins/remark-ansi.unit.ts +197 -0
  148. package/build/website/plugins/remark-badge.ts +50 -0
  149. package/build/website/plugins/remark-badge.unit.ts +96 -0
  150. package/build/website/plugins/remark-button.ts +60 -0
  151. package/build/website/plugins/remark-button.unit.ts +147 -0
  152. package/build/website/plugins/remark-callout.ts +57 -0
  153. package/build/website/plugins/remark-card.ts +80 -0
  154. package/build/website/plugins/remark-code-tabs.ts +87 -0
  155. package/build/website/plugins/remark-columns.ts +66 -0
  156. package/build/website/plugins/remark-custom-heading-id.ts +70 -0
  157. package/build/website/plugins/remark-custom-heading-id.unit.ts +131 -0
  158. package/build/website/plugins/remark-deflist.ts +36 -0
  159. package/build/website/plugins/remark-details.ts +49 -0
  160. package/build/website/plugins/remark-filetree.ts +89 -0
  161. package/build/website/plugins/remark-filetree.unit.ts +137 -0
  162. package/build/website/plugins/remark-github-callout.ts +71 -0
  163. package/build/website/plugins/remark-icon.ts +42 -0
  164. package/build/website/plugins/remark-image.ts +25 -0
  165. package/build/website/plugins/remark-media.ts +62 -0
  166. package/build/website/plugins/remark-media.unit.ts +74 -0
  167. package/build/website/plugins/remark-mermaid.ts +23 -0
  168. package/build/website/plugins/remark-npm.ts +52 -0
  169. package/build/website/plugins/remark-steps.ts +36 -0
  170. package/build/website/plugins/remark-steps.unit.ts +97 -0
  171. package/build/website/plugins/remark-tab.ts +100 -0
  172. package/build/website/plugins/remark-tab.unit.ts +181 -0
  173. package/build/website/plugins/remark-toc.ts +39 -0
  174. package/build/website/plugins/remark-toc.unit.ts +82 -0
  175. package/build/website/plugins/shiki-icon.ts +82 -0
  176. package/build/website/plugins/shiki-line-highlight.ts +174 -0
  177. package/build/website/public/logo.svg +72 -0
  178. package/build/website/routeTree.gen.ts +86 -0
  179. package/build/website/router.tsx +34 -0
  180. package/build/website/routes/$.tsx +149 -0
  181. package/build/website/routes/__root.tsx +84 -0
  182. package/build/website/routes/index.tsx +13 -0
  183. package/build/website/server.ts +7 -0
  184. package/build/website/styles/general.css +347 -0
  185. package/build/website/vite.config.ts +36 -0
  186. package/entrypoints/main.ts +12 -0
  187. package/package.json +120 -0
@@ -0,0 +1,66 @@
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
+ /** Grid container for card components */
18
+ export function Cards(props: { children: React.ReactNode }) {
19
+ return (
20
+ <div className="not-prose grid grid-cols-1 gap-4 my-4 sm:grid-cols-2">
21
+ {props.children}
22
+ </div>
23
+ )
24
+ }
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,118 @@
1
+ import { Check, ChevronDown, Clipboard, WrapText } from "lucide-react"
2
+ import { useRef, useState } from "react"
3
+
4
+ export function CodeBlock(props: React.ComponentProps<"pre">) {
5
+ const [copied, setCopied] = useState(false)
6
+ const [wrap, setWrap] = useState(false)
7
+ const [expanded, setExpanded] = useState(false)
8
+ const preRef = useRef<HTMLPreElement>(null)
9
+ const extra = props as Record<string, unknown>
10
+ const meta = extra["data-meta"] as string | undefined
11
+ const title = meta?.match(/title="([^"]+)"/)?.[1]
12
+ const maxLines = meta?.match(/maxLines=(\d+)/)?.[1]
13
+ const icon = extra.icon as string | undefined
14
+ const style = props.style as Record<string, string> | undefined
15
+ const collapsedHeight = maxLines
16
+ ? `${Number(maxLines) * 1.5 + 1.75}rem`
17
+ : undefined
18
+
19
+ const handleCopy = () => {
20
+ const text = preRef.current?.textContent ?? ""
21
+ navigator.clipboard.writeText(text)
22
+ setCopied(true)
23
+ setTimeout(() => setCopied(false), 2000)
24
+ }
25
+
26
+ const handleWrapToggle = () => {
27
+ setWrap(prev => !prev)
28
+ }
29
+
30
+ return (
31
+ <figure
32
+ className="shiki not-prose my-4 relative rounded-xl border shadow-sm overflow-hidden text-sm bg-sidebar dark:bg-sidebar"
33
+ style={{ ...style, backgroundColor: undefined }}
34
+ >
35
+ {title ? (
36
+ <div className="flex items-center gap-2 h-9.5 border-b px-4 text-muted-foreground">
37
+ {icon && (
38
+ <span
39
+ className="size-3.5 [&>svg]:size-full"
40
+ dangerouslySetInnerHTML={{ __html: icon }}
41
+ />
42
+ )}
43
+ <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>
64
+ </div>
65
+ ) : (
66
+ <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>
87
+ </div>
88
+ )}
89
+ <div
90
+ className="text-[0.8125rem] py-3.5 overflow-auto transition-[max-height] duration-300"
91
+ style={{
92
+ maxHeight: collapsedHeight && !expanded ? collapsedHeight : "600px",
93
+ }}
94
+ >
95
+ <pre
96
+ {...props}
97
+ ref={preRef}
98
+ style={undefined}
99
+ className={
100
+ wrap
101
+ ? "min-w-full whitespace-pre-wrap break-words [&>code]:flex [&>code]:flex-col"
102
+ : "min-w-full w-max [&>code]:flex [&>code]:flex-col"
103
+ }
104
+ />
105
+ </div>
106
+ {collapsedHeight && !expanded && (
107
+ <button
108
+ type="button"
109
+ onClick={() => setExpanded(true)}
110
+ className="flex items-center justify-center gap-1 w-full py-1.5 text-xs text-muted-foreground hover:text-foreground border-t cursor-pointer transition-colors"
111
+ >
112
+ <ChevronDown className="size-3.5" />
113
+ Expand
114
+ </button>
115
+ )}
116
+ </figure>
117
+ )
118
+ }
@@ -0,0 +1,83 @@
1
+ import { Children, useEffect, useState } from "react"
2
+
3
+ /** Tabbed code block component for grouping multiple code blocks with optional sync */
4
+ export function CodeTabs(props: {
5
+ tabs: string
6
+ sync?: string
7
+ children: React.ReactNode
8
+ }) {
9
+ const names = JSON.parse(props.tabs) as string[]
10
+ const children = Children.toArray(props.children)
11
+
12
+ const getInitialIndex = () => {
13
+ if (!props.sync) return 0
14
+ const saved =
15
+ typeof window !== "undefined"
16
+ ? localStorage.getItem("livemark-tab-" + props.sync)
17
+ : null
18
+ if (!saved) return 0
19
+ const idx = names.indexOf(saved)
20
+ return idx >= 0 ? idx : 0
21
+ }
22
+
23
+ const [active, setActive] = useState(getInitialIndex)
24
+
25
+ const handleSelect = (i: number) => {
26
+ setActive(i)
27
+ if (props.sync) {
28
+ localStorage.setItem("livemark-tab-" + props.sync, names[i]!)
29
+ window.dispatchEvent(
30
+ new CustomEvent("livemark-tab-sync", {
31
+ detail: { sync: props.sync, title: names[i] },
32
+ }),
33
+ )
34
+ }
35
+ }
36
+
37
+ useEffect(() => {
38
+ if (!props.sync) return
39
+ const handler = (e: Event) => {
40
+ const { sync, title } = (
41
+ e as CustomEvent<{ sync: string; title: string }>
42
+ ).detail
43
+ if (sync !== props.sync) return
44
+ const idx = names.indexOf(title)
45
+ if (idx >= 0) setActive(idx)
46
+ }
47
+ window.addEventListener("livemark-tab-sync", handler)
48
+ return () => window.removeEventListener("livemark-tab-sync", handler)
49
+ }, [props.sync, names])
50
+
51
+ return (
52
+ <div className="not-prose my-4 rounded-xl border shadow-sm overflow-hidden text-sm bg-sidebar dark:bg-sidebar">
53
+ <div className="flex border-b">
54
+ {names.map((name, i) => (
55
+ <button
56
+ key={name}
57
+ type="button"
58
+ onClick={() => handleSelect(i)}
59
+ className={`px-4 py-2 text-xs font-medium transition-colors cursor-pointer ${
60
+ i === active
61
+ ? "text-foreground border-b-2 border-primary -mb-px"
62
+ : "text-muted-foreground hover:text-foreground"
63
+ }`}
64
+ >
65
+ {name}
66
+ </button>
67
+ ))}
68
+ </div>
69
+ {children.map((child, i) => (
70
+ <div
71
+ key={names[i]}
72
+ className={
73
+ i === active
74
+ ? "[&>figure]:my-0 [&>figure]:rounded-none [&>figure]:border-0 [&>figure]:shadow-none"
75
+ : "hidden"
76
+ }
77
+ >
78
+ {child}
79
+ </div>
80
+ ))}
81
+ </div>
82
+ )
83
+ }
@@ -0,0 +1,17 @@
1
+ /** Grid layout for arranging content side by side */
2
+ export function Columns(props: { cols?: string; children: React.ReactNode }) {
3
+ const cols = props.cols ? parseInt(props.cols, 10) : undefined
4
+ const style = cols
5
+ ? { gridTemplateColumns: `repeat(${cols}, 1fr)` }
6
+ : undefined
7
+ return (
8
+ <div className="grid gap-6 my-4 sm:grid-cols-2" style={style}>
9
+ {props.children}
10
+ </div>
11
+ )
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
+ }
@@ -0,0 +1,7 @@
1
+ export function Content(props: { children: React.ReactNode }) {
2
+ return (
3
+ <div className="w-full max-w-7xl mx-auto px-4 md:px-10">
4
+ {props.children}
5
+ </div>
6
+ )
7
+ }
@@ -0,0 +1,7 @@
1
+ /** Simple wrapper that provides a title prop for ContentTabs to read */
2
+ export function ContentTab(props: {
3
+ title: string
4
+ children: React.ReactNode
5
+ }) {
6
+ return <div>{props.children}</div>
7
+ }
@@ -0,0 +1,82 @@
1
+ import { Children, isValidElement, useEffect, useState } from "react"
2
+
3
+ /** Tabbed container component for grouping content tabs with optional sync across groups */
4
+ export function ContentTabs(props: {
5
+ sync?: string
6
+ children: React.ReactNode
7
+ }) {
8
+ const children = Children.toArray(props.children)
9
+ const titles = children.map(child =>
10
+ isValidElement<{ title: string }>(child) ? child.props.title : "",
11
+ )
12
+
13
+ const getInitialIndex = () => {
14
+ if (!props.sync) return 0
15
+ const saved =
16
+ typeof window !== "undefined"
17
+ ? localStorage.getItem("livemark-tab-" + props.sync)
18
+ : null
19
+ if (!saved) return 0
20
+ const idx = titles.indexOf(saved)
21
+ return idx >= 0 ? idx : 0
22
+ }
23
+
24
+ const [active, setActive] = useState(getInitialIndex)
25
+
26
+ const handleSelect = (i: number) => {
27
+ setActive(i)
28
+ if (props.sync) {
29
+ localStorage.setItem("livemark-tab-" + props.sync, titles[i]!)
30
+ window.dispatchEvent(
31
+ new CustomEvent("livemark-tab-sync", {
32
+ detail: { sync: props.sync, title: titles[i] },
33
+ }),
34
+ )
35
+ }
36
+ }
37
+
38
+ useEffect(() => {
39
+ if (!props.sync) return
40
+ const handler = (e: Event) => {
41
+ const { sync, title } = (
42
+ e as CustomEvent<{ sync: string; title: string }>
43
+ ).detail
44
+ if (sync !== props.sync) return
45
+ const idx = titles.indexOf(title)
46
+ if (idx >= 0) setActive(idx)
47
+ }
48
+ window.addEventListener("livemark-tab-sync", handler)
49
+ return () => window.removeEventListener("livemark-tab-sync", handler)
50
+ }, [props.sync, titles])
51
+
52
+ return (
53
+ <div className="not-prose my-4 rounded-xl border bg-card">
54
+ <div className="flex border-b">
55
+ {titles.map((title, i) => (
56
+ <button
57
+ key={title}
58
+ type="button"
59
+ onClick={() => handleSelect(i)}
60
+ className={`px-4 py-2 text-sm font-medium transition-colors cursor-pointer ${
61
+ i === active
62
+ ? "text-foreground border-b-2 border-primary -mb-px"
63
+ : "text-muted-foreground hover:text-foreground"
64
+ }`}
65
+ >
66
+ {title}
67
+ </button>
68
+ ))}
69
+ </div>
70
+ {children.map((child, i) => (
71
+ <div
72
+ key={titles[i]}
73
+ className={
74
+ i === active ? "p-4 prose dark:prose-invert max-w-none" : "hidden"
75
+ }
76
+ >
77
+ {child}
78
+ </div>
79
+ ))}
80
+ </div>
81
+ )
82
+ }
@@ -0,0 +1,54 @@
1
+ import type { ErrorComponentProps } from "@tanstack/react-router"
2
+ import {
3
+ ErrorComponent,
4
+ Link,
5
+ rootRouteId,
6
+ useMatch,
7
+ useRouter,
8
+ } from "@tanstack/react-router"
9
+
10
+ export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
11
+ const router = useRouter()
12
+ const isRoot = useMatch({
13
+ strict: false,
14
+ select: state => state.id === rootRouteId,
15
+ })
16
+
17
+ console.error(error)
18
+
19
+ return (
20
+ <div className="min-h-[60vh] flex flex-col items-center justify-center gap-6 px-4">
21
+ <ErrorComponent error={error} />
22
+ <div className="flex gap-3 items-center flex-wrap">
23
+ <button
24
+ type="button"
25
+ onClick={() => {
26
+ router.invalidate()
27
+ }}
28
+ className="bg-primary text-primary-foreground px-4 py-2 text-sm font-medium hover:bg-primary/80 transition-colors"
29
+ >
30
+ Try Again
31
+ </button>
32
+ {isRoot ? (
33
+ <Link
34
+ to="/"
35
+ className="border border-border px-4 py-2 text-sm font-medium hover:bg-card transition-colors"
36
+ >
37
+ Home
38
+ </Link>
39
+ ) : (
40
+ <Link
41
+ to="/"
42
+ className="border border-border px-4 py-2 text-sm font-medium hover:bg-card transition-colors"
43
+ onClick={(e: any) => {
44
+ e.preventDefault()
45
+ window.history.back()
46
+ }}
47
+ >
48
+ Go Back
49
+ </Link>
50
+ )}
51
+ </div>
52
+ </div>
53
+ )
54
+ }
@@ -0,0 +1,21 @@
1
+ import type { ComponentProps } from "react"
2
+
3
+ /** Styled wrapper for definition lists produced by remark-definition-list */
4
+ export function DefinitionList(props: ComponentProps<"dl">) {
5
+ return <dl {...props} className="not-prose space-y-4" />
6
+ }
7
+
8
+ /** Styled definition term rendered as bold text */
9
+ export function DefinitionTerm(props: ComponentProps<"dt">) {
10
+ return <dt {...props} className="text-foreground font-semibold" />
11
+ }
12
+
13
+ /** Styled definition detail with a left border accent */
14
+ export function DefinitionDetail(props: ComponentProps<"dd">) {
15
+ return (
16
+ <dd
17
+ {...props}
18
+ className="ml-0 pl-4 border-l-2 border-border text-muted-foreground"
19
+ />
20
+ )
21
+ }
@@ -0,0 +1,30 @@
1
+ import { useState } from "react"
2
+ import { ChevronRight } from "lucide-react"
3
+ import {
4
+ Collapsible,
5
+ CollapsibleTrigger,
6
+ CollapsibleContent,
7
+ } from "../elements/collapsible.tsx"
8
+
9
+ /** Collapsible content section with a summary label */
10
+ export function Details(props: { summary: string; children: React.ReactNode }) {
11
+ const [open, setOpen] = useState(false)
12
+
13
+ return (
14
+ <Collapsible
15
+ open={open}
16
+ onOpenChange={setOpen}
17
+ className="not-prose my-4 rounded-xl border border-border bg-card px-4 py-3"
18
+ >
19
+ <CollapsibleTrigger className="flex w-full cursor-pointer items-center gap-2 text-sm font-semibold select-none">
20
+ <ChevronRight
21
+ className={`size-4 shrink-0 transition-transform ${open ? "rotate-90" : ""}`}
22
+ />
23
+ {props.summary}
24
+ </CollapsibleTrigger>
25
+ <CollapsibleContent className="mt-3 text-sm [&>p]:my-1 [&>p:first-child]:mt-0 [&>p:last-child]:mb-0">
26
+ {props.children}
27
+ </CollapsibleContent>
28
+ </Collapsible>
29
+ )
30
+ }
@@ -0,0 +1,52 @@
1
+ import { File, Folder } from "lucide-react"
2
+
3
+ interface TreeItem {
4
+ name: string
5
+ isDir: boolean
6
+ children?: TreeItem[]
7
+ }
8
+
9
+ /** Renders a file/directory tree from a JSON-serialized tree structure */
10
+ export function FileTree(props: { tree: string }) {
11
+ const items: TreeItem[] = JSON.parse(props.tree)
12
+
13
+ return (
14
+ <div className="not-prose my-4 rounded-xl border border-border bg-card p-4 font-mono text-sm">
15
+ <TreeItems items={items} depth={0} />
16
+ </div>
17
+ )
18
+ }
19
+
20
+ function TreeItems(props: { items: TreeItem[]; depth: number }) {
21
+ return (
22
+ <ul className={props.depth > 0 ? "ml-4 border-l border-border pl-2" : ""}>
23
+ {props.items.map((item, index) => (
24
+ <TreeNode
25
+ key={`${item.name}-${index}`}
26
+ item={item}
27
+ depth={props.depth}
28
+ />
29
+ ))}
30
+ </ul>
31
+ )
32
+ }
33
+
34
+ function TreeNode(props: { item: TreeItem; depth: number }) {
35
+ const { item, depth } = props
36
+
37
+ return (
38
+ <li className="py-0.5">
39
+ <span className="inline-flex items-center gap-2">
40
+ {item.isDir ? (
41
+ <Folder className="size-4 shrink-0 text-primary" />
42
+ ) : (
43
+ <File className="size-4 shrink-0 text-muted-foreground" />
44
+ )}
45
+ <span className={item.isDir ? "font-semibold" : ""}>{item.name}</span>
46
+ </span>
47
+ {item.children && item.children.length > 0 && (
48
+ <TreeItems items={item.children} depth={depth + 1} />
49
+ )}
50
+ </li>
51
+ )
52
+ }
@@ -0,0 +1,9 @@
1
+ export function Footer() {
2
+ 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>
8
+ )
9
+ }
@@ -0,0 +1,52 @@
1
+ import { type ComponentProps, useEffect, useState } from "react"
2
+ import {
3
+ HoverCard,
4
+ HoverCardContent,
5
+ HoverCardTrigger,
6
+ } from "../elements/hover-card.tsx"
7
+
8
+ const FOOTNOTE_PREFIX = "#user-content-fn-"
9
+
10
+ /** Wraps footnote reference links in a HoverCard that previews footnote content inline */
11
+ export function FootnoteRef(props: ComponentProps<"a">) {
12
+ const href = props.href ?? ""
13
+ if (!href.startsWith(FOOTNOTE_PREFIX)) {
14
+ return <a {...props} />
15
+ }
16
+
17
+ return <FootnoteHoverCard {...props} href={href} />
18
+ }
19
+
20
+ function FootnoteHoverCard(props: ComponentProps<"a"> & { href: string }) {
21
+ const [footnoteHtml, setFootnoteHtml] = useState("")
22
+
23
+ useEffect(() => {
24
+ const targetId = props.href.slice(1)
25
+ const el = document.getElementById(targetId)
26
+ if (!el) return
27
+
28
+ const wrapper = document.createElement("div")
29
+ wrapper.innerHTML = el.innerHTML
30
+ for (const backref of wrapper.querySelectorAll("[data-footnote-backref]")) {
31
+ backref.remove()
32
+ }
33
+ setFootnoteHtml(wrapper.innerHTML)
34
+ }, [props.href])
35
+
36
+ return (
37
+ <HoverCard>
38
+ <HoverCardTrigger render={<a {...props} />} />
39
+ <HoverCardContent
40
+ className="prose dark:prose-invert text-sm w-80 [&>p]:my-1 [&>p:first-child]:mt-0 [&>p:last-child]:mb-0"
41
+ side="top"
42
+ sideOffset={8}
43
+ >
44
+ {footnoteHtml ? (
45
+ <div dangerouslySetInnerHTML={{ __html: footnoteHtml }} />
46
+ ) : (
47
+ <p className="text-muted-foreground">Footnote not found</p>
48
+ )}
49
+ </HoverCardContent>
50
+ </HoverCard>
51
+ )
52
+ }
@@ -0,0 +1,20 @@
1
+ /** Heading component that wraps content in an anchor link to itself */
2
+ function createHeading(Tag: "h1" | "h2" | "h3" | "h4" | "h5" | "h6") {
3
+ return function Heading(props: React.HTMLAttributes<HTMLHeadingElement>) {
4
+ if (!props.id) return <Tag {...props} />
5
+ return (
6
+ <Tag {...props}>
7
+ <a href={`#${props.id}`}>{props.children}</a>
8
+ </Tag>
9
+ )
10
+ }
11
+ }
12
+
13
+ export const headingComponents = {
14
+ h1: createHeading("h1"),
15
+ h2: createHeading("h2"),
16
+ h3: createHeading("h3"),
17
+ h4: createHeading("h4"),
18
+ h5: createHeading("h5"),
19
+ h6: createHeading("h6"),
20
+ }
@@ -0,0 +1,9 @@
1
+ import { Badge } from "../elements/badge.tsx"
2
+
3
+ /** Inline badge rendered from :badge text directives */
4
+ export function InlineBadge(props: {
5
+ label: string
6
+ variant?: "default" | "secondary" | "destructive" | "outline"
7
+ }) {
8
+ return <Badge variant={props.variant}>{props.label}</Badge>
9
+ }
@@ -0,0 +1,21 @@
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
+ }
9
+
10
+ /** Renders a Lucide icon inline by name */
11
+ 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
+ const className = props.className
17
+ ? `inline-block size-4 align-text-bottom ${props.className}`
18
+ : "inline-block size-4 align-text-bottom"
19
+
20
+ return <Icon className={className} />
21
+ }