livemark 0.2.0 → 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 (346) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +5 -1
  3. package/build/actions/changelog/build.d.ts +14 -0
  4. package/build/actions/changelog/build.js +158 -0
  5. package/build/actions/changelog/build.unit.js +66 -0
  6. package/build/actions/config/define.d.ts +6 -0
  7. package/build/actions/config/define.js +8 -0
  8. package/build/actions/config/define.unit.js +29 -0
  9. package/build/actions/config/load.d.ts +5 -0
  10. package/build/actions/config/load.js +25 -0
  11. package/build/actions/config/load.unit.js +23 -0
  12. package/build/commands/build.d.ts +5 -0
  13. package/build/commands/build.js +14 -0
  14. package/build/commands/escape.d.ts +9 -0
  15. package/build/commands/escape.js +56 -0
  16. package/build/commands/preview.d.ts +5 -0
  17. package/build/commands/preview.js +18 -0
  18. package/build/commands/start.d.ts +5 -0
  19. package/build/commands/start.js +19 -0
  20. package/build/docs/markdown/includes/example.d.ts +58 -0
  21. package/build/docs/markdown/includes/example.js +5 -0
  22. package/build/entrypoints/main.d.ts +2 -0
  23. package/build/entrypoints/main.js +12 -0
  24. package/build/helpers/program.d.ts +2 -0
  25. package/build/helpers/program.js +5 -0
  26. package/build/index.d.ts +1 -4
  27. package/build/index.js +2 -5
  28. package/build/livemark.config.d.ts +58 -0
  29. package/build/livemark.config.js +25 -0
  30. package/build/models/config.d.ts +340 -0
  31. package/build/models/config.js +34 -0
  32. package/build/models/link.d.ts +13 -0
  33. package/build/models/link.js +9 -0
  34. package/build/models/patch.d.ts +21 -0
  35. package/build/models/patch.js +7 -0
  36. package/build/models/section.d.ts +75 -0
  37. package/build/models/section.js +26 -0
  38. package/build/models/theme.d.ts +72 -0
  39. package/build/models/theme.js +73 -0
  40. package/build/program.d.ts +2 -0
  41. package/build/program.js +16 -0
  42. package/build/vite.config.d.ts +2 -0
  43. package/build/vite.config.js +48 -0
  44. package/build/website/assets/logo.svg +117 -0
  45. package/build/website/client.tsx +10 -0
  46. package/build/website/components/Abbr.tsx +17 -0
  47. package/build/website/components/AnsiCode.tsx +15 -0
  48. package/build/website/components/Article.tsx +172 -0
  49. package/build/website/components/BackToTop.tsx +32 -0
  50. package/build/website/components/Banner.tsx +32 -0
  51. package/build/website/components/BlogIndex.tsx +121 -0
  52. package/build/website/components/BlogSidebar.tsx +162 -0
  53. package/build/website/components/Breadcrumbs.tsx +22 -0
  54. package/build/website/components/Callout.tsx +72 -0
  55. package/build/website/components/Card.tsx +48 -0
  56. package/build/website/components/Cards.tsx +8 -0
  57. package/build/website/components/ChangelogIndex.tsx +111 -0
  58. package/build/website/components/ChangelogSidebar.tsx +120 -0
  59. package/build/website/components/CodeBlock.tsx +135 -0
  60. package/build/website/components/CodeTabs.tsx +83 -0
  61. package/build/website/components/Column.tsx +4 -0
  62. package/build/website/components/Columns.tsx +12 -0
  63. package/build/website/components/Content.tsx +7 -0
  64. package/build/website/components/ContentTab.tsx +7 -0
  65. package/build/website/components/ContentTabs.tsx +82 -0
  66. package/build/website/components/DefaultCatchBoundary.tsx +54 -0
  67. package/build/website/components/DefinitionList.tsx +21 -0
  68. package/build/website/components/Details.tsx +30 -0
  69. package/build/website/components/FileTree.tsx +52 -0
  70. package/build/website/components/Footer.tsx +16 -0
  71. package/build/website/components/FootnoteRef.tsx +52 -0
  72. package/build/website/components/Heading.tsx +20 -0
  73. package/build/website/components/InlineBadge.tsx +9 -0
  74. package/build/website/components/InlineIcon.tsx +10 -0
  75. package/build/website/components/InlineToc.tsx +31 -0
  76. package/build/website/components/Layout.tsx +190 -0
  77. package/build/website/components/LinkButton.tsx +31 -0
  78. package/build/website/components/Mermaid.tsx +18 -0
  79. package/build/website/components/MermaidRenderer.tsx +37 -0
  80. package/build/website/components/MiniSidebar.tsx +49 -0
  81. package/build/website/components/NotFound.tsx +27 -0
  82. package/build/website/components/PackageTabs.tsx +74 -0
  83. package/build/website/components/PageToolbar.tsx +75 -0
  84. package/build/website/components/PrevNext.tsx +60 -0
  85. package/build/website/components/Search.tsx +28 -0
  86. package/build/website/components/SearchDialog.tsx +245 -0
  87. package/build/website/components/Section.tsx +24 -0
  88. package/build/website/components/SectionHeader.tsx +35 -0
  89. package/build/website/components/Sidebar.tsx +160 -0
  90. package/build/website/components/SidebarLinks.tsx +70 -0
  91. package/build/website/components/SidebarSections.tsx +71 -0
  92. package/build/website/components/SiteTitle.tsx +21 -0
  93. package/build/website/components/SortableTable.tsx +81 -0
  94. package/build/website/components/SoundCloud.tsx +16 -0
  95. package/build/website/components/TagIndex.tsx +120 -0
  96. package/build/website/components/Theme.tsx +39 -0
  97. package/build/website/components/Toc.tsx +226 -0
  98. package/build/website/components/YouTube.tsx +16 -0
  99. package/build/website/components/ZoomImage.tsx +25 -0
  100. package/build/website/content/article.ts +78 -0
  101. package/build/website/content/tag.ts +29 -0
  102. package/build/website/content-collections.ts +208 -0
  103. package/build/website/elements/accordion.tsx +78 -0
  104. package/build/website/elements/alert-dialog.tsx +185 -0
  105. package/build/website/elements/alert.tsx +74 -0
  106. package/build/website/elements/aspect-ratio.tsx +22 -0
  107. package/build/website/elements/avatar.tsx +107 -0
  108. package/build/website/elements/badge.tsx +51 -0
  109. package/build/website/elements/breadcrumb.tsx +120 -0
  110. package/build/website/elements/button-group.tsx +87 -0
  111. package/build/website/elements/button.tsx +57 -0
  112. package/build/website/elements/calendar.tsx +229 -0
  113. package/build/website/elements/card.tsx +102 -0
  114. package/build/website/elements/carousel.tsx +240 -0
  115. package/build/website/elements/chart.tsx +372 -0
  116. package/build/website/elements/checkbox.tsx +26 -0
  117. package/build/website/elements/collapsible.tsx +19 -0
  118. package/build/website/elements/command.tsx +210 -0
  119. package/build/website/elements/context-menu.tsx +270 -0
  120. package/build/website/elements/dialog.tsx +152 -0
  121. package/build/website/elements/direction.tsx +4 -0
  122. package/build/website/elements/drawer.tsx +127 -0
  123. package/build/website/elements/dropdown-menu.tsx +260 -0
  124. package/build/website/elements/empty.tsx +104 -0
  125. package/build/website/elements/field.tsx +235 -0
  126. package/build/website/elements/hover-card.tsx +49 -0
  127. package/build/website/elements/input-group.tsx +171 -0
  128. package/build/website/elements/input-otp.tsx +84 -0
  129. package/build/website/elements/input.tsx +18 -0
  130. package/build/website/elements/item.tsx +199 -0
  131. package/build/website/elements/kbd.tsx +26 -0
  132. package/build/website/elements/label.tsx +17 -0
  133. package/build/website/elements/menubar.tsx +282 -0
  134. package/build/website/elements/native-select.tsx +56 -0
  135. package/build/website/elements/navigation-menu.tsx +171 -0
  136. package/build/website/elements/pagination.tsx +133 -0
  137. package/build/website/elements/popover.tsx +88 -0
  138. package/build/website/elements/progress.tsx +81 -0
  139. package/build/website/elements/radio-group.tsx +36 -0
  140. package/build/website/elements/resizable.tsx +48 -0
  141. package/build/website/elements/scroll-area.tsx +51 -0
  142. package/build/website/elements/select.tsx +200 -0
  143. package/build/website/elements/separator.tsx +22 -0
  144. package/build/website/elements/sheet.tsx +130 -0
  145. package/build/website/elements/sidebar.tsx +717 -0
  146. package/build/website/elements/skeleton.tsx +13 -0
  147. package/build/website/elements/slider.tsx +57 -0
  148. package/build/website/elements/sonner.tsx +40 -0
  149. package/build/website/elements/spinner.tsx +16 -0
  150. package/build/website/elements/switch.tsx +30 -0
  151. package/build/website/elements/table.tsx +112 -0
  152. package/build/website/elements/tabs.tsx +79 -0
  153. package/build/website/elements/textarea.tsx +16 -0
  154. package/build/website/elements/toggle-group.tsx +87 -0
  155. package/build/website/elements/toggle.tsx +43 -0
  156. package/build/website/elements/tooltip.tsx +67 -0
  157. package/build/website/env.d.ts +14 -0
  158. package/build/website/helpers/article-icon.ts +61 -0
  159. package/build/website/helpers/article.ts +90 -0
  160. package/build/website/helpers/changelog-source.ts +16 -0
  161. package/build/website/helpers/changelog-source.unit.ts +38 -0
  162. package/build/website/helpers/dynamic-icon-resolver.tsx +19 -0
  163. package/build/website/helpers/dynamic-icon.tsx +12 -0
  164. package/build/website/helpers/favicon.ts +7 -0
  165. package/build/website/helpers/model.ts +4 -0
  166. package/build/website/helpers/resolve-asset-path.ts +9 -0
  167. package/build/website/helpers/resolve-includes.ts +131 -0
  168. package/build/website/helpers/resolve-includes.unit.ts +126 -0
  169. package/build/website/helpers/search-text.ts +15 -0
  170. package/build/website/helpers/section.ts +50 -0
  171. package/build/website/helpers/section.unit.ts +71 -0
  172. package/build/website/helpers/style.ts +6 -0
  173. package/build/website/helpers/theme.ts +27 -0
  174. package/build/website/helpers/toc-context.ts +10 -0
  175. package/build/website/helpers/toc.ts +46 -0
  176. package/build/website/helpers/toc.unit.ts +74 -0
  177. package/build/website/hooks/in-view.ts +30 -0
  178. package/build/website/hooks/media.ts +19 -0
  179. package/build/website/models/article.ts +55 -0
  180. package/build/website/models/toc.ts +5 -0
  181. package/build/website/plugins/rehype-inline-code.ts +107 -0
  182. package/build/website/plugins/rehype-task-list-a11y.ts +33 -0
  183. package/build/website/plugins/remark-abbr.ts +46 -0
  184. package/build/website/plugins/remark-abbr.unit.ts +99 -0
  185. package/build/website/plugins/remark-ansi.ts +248 -0
  186. package/build/website/plugins/remark-ansi.unit.ts +197 -0
  187. package/build/website/plugins/remark-badge.ts +50 -0
  188. package/build/website/plugins/remark-badge.unit.ts +96 -0
  189. package/build/website/plugins/remark-button.ts +60 -0
  190. package/build/website/plugins/remark-button.unit.ts +147 -0
  191. package/build/website/plugins/remark-callout.ts +57 -0
  192. package/build/website/plugins/remark-card.ts +80 -0
  193. package/build/website/plugins/remark-code-tabs.ts +87 -0
  194. package/build/website/plugins/remark-columns.ts +66 -0
  195. package/build/website/plugins/remark-custom-heading-id.ts +70 -0
  196. package/build/website/plugins/remark-custom-heading-id.unit.ts +131 -0
  197. package/build/website/plugins/remark-deflist.ts +36 -0
  198. package/build/website/plugins/remark-details.ts +49 -0
  199. package/build/website/plugins/remark-filetree.ts +89 -0
  200. package/build/website/plugins/remark-filetree.unit.ts +137 -0
  201. package/build/website/plugins/remark-github-callout.ts +71 -0
  202. package/build/website/plugins/remark-icon.ts +42 -0
  203. package/build/website/plugins/remark-image.ts +34 -0
  204. package/build/website/plugins/remark-media.ts +62 -0
  205. package/build/website/plugins/remark-media.unit.ts +74 -0
  206. package/build/website/plugins/remark-mermaid.ts +23 -0
  207. package/build/website/plugins/remark-npm.ts +52 -0
  208. package/build/website/plugins/remark-steps.ts +36 -0
  209. package/build/website/plugins/remark-steps.unit.ts +97 -0
  210. package/build/website/plugins/remark-tab.ts +100 -0
  211. package/build/website/plugins/remark-tab.unit.ts +181 -0
  212. package/build/website/plugins/remark-toc.ts +39 -0
  213. package/build/website/plugins/remark-toc.unit.ts +82 -0
  214. package/build/website/plugins/shiki-icon.ts +82 -0
  215. package/build/website/plugins/shiki-line-highlight.ts +174 -0
  216. package/build/website/plugins/vite-livemark.ts +91 -0
  217. package/build/website/routeTree.gen.ts +86 -0
  218. package/build/website/router.tsx +34 -0
  219. package/build/website/routes/$.tsx +92 -0
  220. package/build/website/routes/__root.tsx +97 -0
  221. package/build/website/server.ts +7 -0
  222. package/build/website/styles/general.css +163 -0
  223. package/build/website/styles/markdown.css +214 -0
  224. package/build/website/vite.config.ts +71 -0
  225. package/entrypoints/main.ts +12 -0
  226. package/package.json +120 -12
  227. package/build/descriptor/index.d.ts +0 -3
  228. package/build/descriptor/index.js +0 -4
  229. package/build/descriptor/load.d.ts +0 -8
  230. package/build/descriptor/load.js +0 -40
  231. package/build/descriptor/node.d.ts +0 -125
  232. package/build/descriptor/node.js +0 -9
  233. package/build/descriptor/save.d.ts +0 -8
  234. package/build/descriptor/save.js +0 -17
  235. package/build/descriptor/save.spec.js +0 -80
  236. package/build/descriptor/validate.d.ts +0 -9
  237. package/build/descriptor/validate.js +0 -14
  238. package/build/descriptor/validate.spec.js +0 -124
  239. package/build/dialect/Dialect.d.ts +0 -88
  240. package/build/dialect/Dialect.js +0 -2
  241. package/build/dialect/index.d.ts +0 -3
  242. package/build/dialect/index.js +0 -3
  243. package/build/dialect/load.d.ts +0 -4
  244. package/build/dialect/load.js +0 -7
  245. package/build/dialect/save.d.ts +0 -9
  246. package/build/dialect/save.js +0 -13
  247. package/build/dialect/validate.d.ts +0 -1
  248. package/build/dialect/validate.js +0 -4
  249. package/build/metadata/index.d.ts +0 -3
  250. package/build/metadata/index.js +0 -4
  251. package/build/metadata/load.d.ts +0 -10
  252. package/build/metadata/load.js +0 -55
  253. package/build/metadata/node.d.ts +0 -125
  254. package/build/metadata/node.js +0 -9
  255. package/build/metadata/save.d.ts +0 -8
  256. package/build/metadata/save.js +0 -17
  257. package/build/metadata/save.spec.js +0 -80
  258. package/build/metadata/validate.d.ts +0 -9
  259. package/build/metadata/validate.js +0 -14
  260. package/build/metadata/validate.spec.d.ts +0 -1
  261. package/build/metadata/validate.spec.js +0 -124
  262. package/build/package/Contributor.d.ts +0 -21
  263. package/build/package/Contributor.js +0 -2
  264. package/build/package/Package.d.ts +0 -65
  265. package/build/package/Package.js +0 -2
  266. package/build/package/index.d.ts +0 -4
  267. package/build/package/index.js +0 -3
  268. package/build/package/load.d.ts +0 -4
  269. package/build/package/load.js +0 -7
  270. package/build/package/save.d.ts +0 -9
  271. package/build/package/save.js +0 -13
  272. package/build/package/validate.d.ts +0 -1
  273. package/build/package/validate.js +0 -4
  274. package/build/resource/License.d.ts +0 -18
  275. package/build/resource/License.js +0 -2
  276. package/build/resource/Resource.d.ts +0 -67
  277. package/build/resource/Resource.js +0 -2
  278. package/build/resource/Source.d.ts +0 -17
  279. package/build/resource/Source.js +0 -2
  280. package/build/resource/index.d.ts +0 -5
  281. package/build/resource/index.js +0 -2
  282. package/build/resource/load.d.ts +0 -1
  283. package/build/resource/load.js +0 -2
  284. package/build/resource/save.d.ts +0 -9
  285. package/build/resource/save.js +0 -13
  286. package/build/resource/types/index.d.ts +0 -1
  287. package/build/resource/types/index.js +0 -2
  288. package/build/resource/types/table.d.ts +0 -29
  289. package/build/resource/types/table.js +0 -2
  290. package/build/resource/validate.d.ts +0 -1
  291. package/build/resource/validate.js +0 -4
  292. package/build/schema/Field.d.ts +0 -5
  293. package/build/schema/Field.js +0 -2
  294. package/build/schema/Schema.d.ts +0 -55
  295. package/build/schema/Schema.js +0 -2
  296. package/build/schema/fields/any.d.ts +0 -20
  297. package/build/schema/fields/any.js +0 -2
  298. package/build/schema/fields/array.d.ts +0 -32
  299. package/build/schema/fields/array.js +0 -2
  300. package/build/schema/fields/base.d.ts +0 -42
  301. package/build/schema/fields/base.js +0 -2
  302. package/build/schema/fields/boolean.d.ts +0 -28
  303. package/build/schema/fields/boolean.js +0 -2
  304. package/build/schema/fields/date.d.ts +0 -35
  305. package/build/schema/fields/date.js +0 -2
  306. package/build/schema/fields/datetime.d.ts +0 -35
  307. package/build/schema/fields/datetime.js +0 -2
  308. package/build/schema/fields/duration.d.ts +0 -28
  309. package/build/schema/fields/duration.js +0 -2
  310. package/build/schema/fields/geojson.d.ts +0 -26
  311. package/build/schema/fields/geojson.js +0 -2
  312. package/build/schema/fields/geopoint.d.ts +0 -27
  313. package/build/schema/fields/geopoint.js +0 -2
  314. package/build/schema/fields/index.d.ts +0 -16
  315. package/build/schema/fields/index.js +0 -17
  316. package/build/schema/fields/integer.d.ts +0 -37
  317. package/build/schema/fields/integer.js +0 -2
  318. package/build/schema/fields/list.d.ts +0 -36
  319. package/build/schema/fields/list.js +0 -2
  320. package/build/schema/fields/number.d.ts +0 -40
  321. package/build/schema/fields/number.js +0 -2
  322. package/build/schema/fields/object.d.ts +0 -32
  323. package/build/schema/fields/object.js +0 -2
  324. package/build/schema/fields/string.d.ts +0 -40
  325. package/build/schema/fields/string.js +0 -2
  326. package/build/schema/fields/time.d.ts +0 -35
  327. package/build/schema/fields/time.js +0 -2
  328. package/build/schema/fields/year.d.ts +0 -28
  329. package/build/schema/fields/year.js +0 -2
  330. package/build/schema/fields/yearmonth.d.ts +0 -28
  331. package/build/schema/fields/yearmonth.js +0 -2
  332. package/build/schema/index.d.ts +0 -4
  333. package/build/schema/index.js +0 -2
  334. package/build/schema/load.d.ts +0 -1
  335. package/build/schema/load.js +0 -2
  336. package/build/schema/save.d.ts +0 -9
  337. package/build/schema/save.js +0 -13
  338. package/build/schema/validate.d.ts +0 -1
  339. package/build/schema/validate.js +0 -4
  340. package/build/tsconfig.tsbuildinfo +0 -1
  341. package/index.ts +0 -0
  342. package/tsconfig.json +0 -10
  343. package/typedoc.json +0 -4
  344. /package/build/{descriptor/save.spec.d.ts → actions/changelog/build.unit.d.ts} +0 -0
  345. /package/build/{descriptor/validate.spec.d.ts → actions/config/define.unit.d.ts} +0 -0
  346. /package/build/{metadata/save.spec.d.ts → actions/config/load.unit.d.ts} +0 -0
@@ -0,0 +1,75 @@
1
+ import { useState, useCallback } from "react"
2
+ import { Pencil, Copy, Check, ExternalLink } from "lucide-react"
3
+
4
+ /** Toolbar with action buttons displayed for the article content */
5
+ export function PageToolbar(props: { file?: 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
+ {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
+ )}
48
+ <button type="button" onClick={handleCopy} className={buttonClass}>
49
+ {copied ? (
50
+ <Check className="size-3.5" />
51
+ ) : (
52
+ <Copy className="size-3.5" />
53
+ )}
54
+ Copy Markdown
55
+ </button>
56
+ <button
57
+ type="button"
58
+ onClick={() => handleOpenIn("https://chatgpt.com/")}
59
+ className={buttonClass}
60
+ >
61
+ <ExternalLink className="size-3.5" />
62
+ Open in ChatGPT
63
+ </button>
64
+ <button
65
+ type="button"
66
+ onClick={() => handleOpenIn("https://claude.ai/")}
67
+ className={buttonClass}
68
+ >
69
+ <ExternalLink className="size-3.5" />
70
+ Open in Claude
71
+ </button>
72
+ </div>
73
+ </>
74
+ )
75
+ }
@@ -0,0 +1,60 @@
1
+ import { Link } from "@tanstack/react-router"
2
+ import { ChevronLeft, ChevronRight } from "lucide-react"
3
+ import {
4
+ sortedArticles,
5
+ flatArticles,
6
+ currentSection,
7
+ sectionFlatArticles,
8
+ } from "../content/article.ts"
9
+
10
+ /** Previous/next article navigation shown at the bottom of each article page */
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)
17
+ if (index < 0) return null
18
+
19
+ const prevPath = index > 0 ? flat[index - 1] : undefined
20
+ const nextPath = index < flat.length - 1 ? flat[index + 1] : undefined
21
+ const prev = prevPath
22
+ ? sortedArticles.find(a => a.path === prevPath)
23
+ : undefined
24
+ const next = nextPath
25
+ ? sortedArticles.find(a => a.path === nextPath)
26
+ : undefined
27
+
28
+ if (!prev && !next) return null
29
+
30
+ return (
31
+ <nav className="not-prose mt-10 flex gap-4">
32
+ {prev && (
33
+ <Link
34
+ to="/$"
35
+ params={{ _splat: prev.path.replace(/^\/|\/$/g, "") }}
36
+ className="flex-1 rounded-xl border shadow-sm p-4 hover:bg-muted/50 transition-colors"
37
+ >
38
+ <span className="flex items-center gap-1 text-sm text-muted-foreground">
39
+ <ChevronLeft className="size-4" />
40
+ Previous
41
+ </span>
42
+ <span className="mt-1 block font-medium">{prev.title}</span>
43
+ </Link>
44
+ )}
45
+ {next && (
46
+ <Link
47
+ to="/$"
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"
50
+ >
51
+ <span className="flex items-center justify-end gap-1 text-sm text-muted-foreground">
52
+ Next
53
+ <ChevronRight className="size-4" />
54
+ </span>
55
+ <span className="mt-1 block font-medium">{next.title}</span>
56
+ </Link>
57
+ )}
58
+ </nav>
59
+ )
60
+ }
@@ -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
+ }
@@ -0,0 +1,245 @@
1
+ import { useState, useRef, useCallback, useMemo } from "react"
2
+ import { useNavigate } from "@tanstack/react-router"
3
+ import { FileText } from "lucide-react"
4
+ import { create, insertMultiple, search } from "@orama/orama"
5
+ import { sortedArticles } from "../content/article.ts"
6
+ import { articleIcons } from "../helpers/article-icon.ts"
7
+ import {
8
+ CommandDialog,
9
+ CommandEmpty,
10
+ CommandGroup,
11
+ CommandInput,
12
+ CommandItem,
13
+ CommandList,
14
+ } from "../elements/command.tsx"
15
+
16
+ interface SearchResult {
17
+ title: string
18
+ path: string
19
+ icon: string
20
+ snippet: string
21
+ group: string
22
+ }
23
+
24
+ /** Full-text search dialog powered by Orama */
25
+ export function SearchDialog(props: {
26
+ open: boolean
27
+ onOpenChange: (open: boolean) => void
28
+ }) {
29
+ const [results, setResults] = useState<SearchResult[]>([])
30
+ const [query, setQuery] = useState("")
31
+ const dbRef = useRef<Awaited<ReturnType<typeof create>> | null>(null)
32
+ const navigate = useNavigate()
33
+
34
+ const ensureIndex = useCallback(async () => {
35
+ if (dbRef.current) return dbRef.current
36
+ const db = await create({
37
+ schema: {
38
+ title: "string",
39
+ description: "string",
40
+ searchText: "string",
41
+ },
42
+ })
43
+ await insertMultiple(
44
+ db,
45
+ sortedArticles.map(a => ({
46
+ title: a.title,
47
+ description: a.description ?? "",
48
+ searchText: a.searchText,
49
+ })),
50
+ )
51
+ dbRef.current = db
52
+ return db
53
+ }, [])
54
+
55
+ const toResult = useCallback(
56
+ (article: (typeof sortedArticles)[number], term: string): SearchResult => ({
57
+ title: article.title,
58
+ path: article.path,
59
+ icon: article.icon,
60
+ snippet: term
61
+ ? extractSnippet(article.searchText, term)
62
+ : (article.description ?? ""),
63
+ group: groupFor(article.path),
64
+ }),
65
+ [],
66
+ )
67
+
68
+ const handleSearch = useCallback(
69
+ async (term: string) => {
70
+ setQuery(term)
71
+ if (!term.trim()) {
72
+ setResults([])
73
+ return
74
+ }
75
+ const db = await ensureIndex()
76
+ const hits = await search(db, {
77
+ term,
78
+ properties: ["title", "description", "searchText"],
79
+ tolerance: 1,
80
+ boost: { title: 3, description: 2 },
81
+ })
82
+ setResults(
83
+ hits.hits.map(hit => {
84
+ const article = sortedArticles.find(
85
+ a => a.title === hit.document.title,
86
+ )
87
+ if (!article)
88
+ return toResult(
89
+ {
90
+ ...sortedArticles[0],
91
+ title: hit.document.title,
92
+ path: "",
93
+ searchText: hit.document.description,
94
+ description: hit.document.description,
95
+ icon: "",
96
+ },
97
+ term,
98
+ )
99
+ return toResult(article, term)
100
+ }),
101
+ )
102
+ },
103
+ [ensureIndex, toResult],
104
+ )
105
+
106
+ const handleSelect = useCallback(
107
+ (path: string) => {
108
+ props.onOpenChange(false)
109
+ setQuery("")
110
+ setResults([])
111
+ navigate({
112
+ to: "/$",
113
+ params: { _splat: path.replace(/^\/|\/$/g, "") },
114
+ })
115
+ },
116
+ [navigate, props.onOpenChange],
117
+ )
118
+
119
+ const grouped = useMemo(() => {
120
+ const groups = new Map<string, SearchResult[]>()
121
+ for (const r of results) {
122
+ const list = groups.get(r.group) ?? []
123
+ list.push(r)
124
+ groups.set(r.group, list)
125
+ }
126
+ return groups
127
+ }, [results])
128
+
129
+ return (
130
+ <CommandDialog
131
+ title="Search"
132
+ description="Search documentation"
133
+ open={props.open}
134
+ onOpenChange={props.onOpenChange}
135
+ shouldFilter={false}
136
+ loop
137
+ >
138
+ <CommandInput
139
+ placeholder="Search docs..."
140
+ value={query}
141
+ onValueChange={handleSearch}
142
+ onClear={() => handleSearch("")}
143
+ />
144
+ <CommandList className="max-h-80">
145
+ <CommandEmpty>
146
+ {query.trim()
147
+ ? "No results found."
148
+ : "Type to search across all documentation."}
149
+ </CommandEmpty>
150
+ {[...grouped.entries()].map(([group, items]) => (
151
+ <CommandGroup key={group} heading={group}>
152
+ {items.map(result => {
153
+ const Icon = articleIcons[result.icon] ?? FileText
154
+ return (
155
+ <CommandItem
156
+ key={result.path}
157
+ value={result.path}
158
+ onSelect={() => handleSelect(result.path)}
159
+ className="py-2.5"
160
+ >
161
+ <Icon className="size-5 shrink-0 mt-0.5 self-start opacity-60" />
162
+ <div className="flex flex-col gap-0.5 min-w-0">
163
+ <span className="text-base font-medium">
164
+ {result.title}
165
+ </span>
166
+ {result.snippet && (
167
+ <span className="text-sm text-muted-foreground line-clamp-1">
168
+ <HighlightedText text={result.snippet} term={query} />
169
+ </span>
170
+ )}
171
+ </div>
172
+ </CommandItem>
173
+ )
174
+ })}
175
+ </CommandGroup>
176
+ ))}
177
+ </CommandList>
178
+ <div className="flex items-center gap-4 border-t mt-2 px-4 py-2.5 text-sm text-muted-foreground">
179
+ <span className="flex items-center gap-1.5">
180
+ <kbd className="rounded border border-border bg-muted px-1.5 py-0.5 text-xs font-mono">
181
+ ↑↓
182
+ </kbd>
183
+ navigate
184
+ </span>
185
+ <span className="flex items-center gap-1.5">
186
+ <kbd className="rounded border border-border bg-muted px-1.5 py-0.5 text-xs font-mono">
187
+ ↵
188
+ </kbd>
189
+ select
190
+ </span>
191
+ <span className="flex items-center gap-1.5">
192
+ <kbd className="rounded border border-border bg-muted px-1.5 py-0.5 text-xs font-mono">
193
+ esc
194
+ </kbd>
195
+ close
196
+ </span>
197
+ </div>
198
+ </CommandDialog>
199
+ )
200
+ }
201
+
202
+ function groupFor(pathname: string) {
203
+ const segments = pathname.replace(/^\/|\/$/g, "").split("/")
204
+ if (segments.length <= 2) return "Pages"
205
+ return segments
206
+ .slice(1, -1)
207
+ .map(s =>
208
+ s
209
+ .split("-")
210
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1))
211
+ .join(" "),
212
+ )
213
+ .join(" › ")
214
+ }
215
+
216
+ function extractSnippet(text: string, term: string) {
217
+ const lower = text.toLowerCase()
218
+ const idx = lower.indexOf(term.toLowerCase())
219
+ if (idx === -1) return text.slice(0, 120)
220
+ const start = Math.max(0, idx - 40)
221
+ const end = Math.min(text.length, idx + term.length + 80)
222
+ const prefix = start > 0 ? "…" : ""
223
+ const suffix = end < text.length ? "…" : ""
224
+ return `${prefix}${text.slice(start, end)}${suffix}`
225
+ }
226
+
227
+ function HighlightedText(props: { text: string; term: string }) {
228
+ if (!props.term.trim()) return <>{props.text}</>
229
+ const escaped = props.term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
230
+ const regex = new RegExp(`(${escaped})`, "gi")
231
+ const parts = props.text.split(regex)
232
+ return (
233
+ <>
234
+ {parts.map((part, i) =>
235
+ regex.test(part) ? (
236
+ <span key={i} className="text-foreground font-medium">
237
+ {part}
238
+ </span>
239
+ ) : (
240
+ part
241
+ ),
242
+ )}
243
+ </>
244
+ )
245
+ }
@@ -0,0 +1,24 @@
1
+ import { cn } from "../helpers/style.ts"
2
+ import { useInView } from "../hooks/in-view.ts"
3
+
4
+ export function Section(props: {
5
+ children: React.ReactNode
6
+ className?: string
7
+ id?: string
8
+ }) {
9
+ const { ref, isVisible } = useInView()
10
+
11
+ return (
12
+ <section
13
+ ref={ref}
14
+ id={props.id}
15
+ className={cn(
16
+ "py-16 md:py-24 transition-all duration-700",
17
+ isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8",
18
+ props.className,
19
+ )}
20
+ >
21
+ {props.children}
22
+ </section>
23
+ )
24
+ }
@@ -0,0 +1,35 @@
1
+ import { cn } from "../helpers/style.ts"
2
+
3
+ export function SectionHeader(props: {
4
+ label?: string
5
+ title: string
6
+ subtitle?: string
7
+ className?: string
8
+ align?: "left" | "center"
9
+ }) {
10
+ const align = props.align ?? "center"
11
+
12
+ return (
13
+ <div
14
+ className={cn(
15
+ align === "center" && "text-center",
16
+ "mb-12",
17
+ props.className,
18
+ )}
19
+ >
20
+ {props.label && (
21
+ <p className="text-primary text-xs uppercase tracking-[0.2em] font-medium mb-3">
22
+ {props.label}
23
+ </p>
24
+ )}
25
+ <h2 className="font-serif text-3xl md:text-4xl lg:text-5xl font-semibold italic mb-4">
26
+ {props.title}
27
+ </h2>
28
+ {props.subtitle && (
29
+ <p className="text-muted-foreground text-lg max-w-2xl mx-auto">
30
+ {props.subtitle}
31
+ </p>
32
+ )}
33
+ </div>
34
+ )
35
+ }
@@ -0,0 +1,160 @@
1
+ import { Link, useLocation } from "@tanstack/react-router"
2
+ import { useHotkey } from "@tanstack/react-hotkeys"
3
+ import { ChevronRight, FileText } from "lucide-react"
4
+ import {
5
+ articleGroups,
6
+ currentSection,
7
+ sectionArticleGroups,
8
+ } from "../content/article.ts"
9
+ import { articleIcons } from "../helpers/article-icon.ts"
10
+ import type { ArticleNode } from "../models/article.ts"
11
+ import {
12
+ Collapsible,
13
+ CollapsibleContent,
14
+ CollapsibleTrigger,
15
+ } from "../elements/collapsible.tsx"
16
+ import {
17
+ Sidebar as SidebarRoot,
18
+ SidebarContent,
19
+ SidebarFooter,
20
+ SidebarGroup,
21
+ SidebarGroupContent,
22
+ SidebarGroupLabel,
23
+ SidebarHeader,
24
+ SidebarMenu,
25
+ SidebarMenuButton,
26
+ SidebarMenuItem,
27
+ SidebarMenuSub,
28
+ SidebarMenuSubButton,
29
+ SidebarMenuSubItem,
30
+ SidebarRail,
31
+ useSidebar,
32
+ } from "../elements/sidebar.tsx"
33
+ import { Search } from "./Search.tsx"
34
+ import { SidebarLinks } from "./SidebarLinks.tsx"
35
+ import { SidebarSections } from "./SidebarSections.tsx"
36
+ import { SiteTitle } from "./SiteTitle.tsx"
37
+ import { Theme } from "./Theme.tsx"
38
+
39
+ /** Application sidebar with article navigation, search, and theme toggle */
40
+ export function Sidebar() {
41
+ const pathname = useLocation({ select: l => l.pathname })
42
+ const { toggleSidebar } = useSidebar()
43
+
44
+ useHotkey("S", toggleSidebar)
45
+
46
+ const configSections = import.meta.env.CONFIG.sections
47
+ const section = currentSection(`/${pathname.replace(/^\/|\/$/g, "")}/`)
48
+ const groups = configSections?.length
49
+ ? (sectionArticleGroups.get(section?.prefix ?? "__default__") ??
50
+ articleGroups)
51
+ : articleGroups
52
+
53
+ return (
54
+ <SidebarRoot>
55
+ <SidebarHeader>
56
+ <SidebarMenu>
57
+ <SidebarMenuItem>
58
+ <SidebarMenuButton size="lg" render={<Link to="/" />}>
59
+ <SiteTitle />
60
+ </SidebarMenuButton>
61
+ </SidebarMenuItem>
62
+ </SidebarMenu>
63
+ </SidebarHeader>
64
+ <SidebarContent>
65
+ <SidebarSections />
66
+ {groups.map((group, index) => (
67
+ <SidebarGroup key={group.name ?? `__unnamed-${index}`}>
68
+ {group.name && (
69
+ <SidebarGroupLabel className="uppercase font-mono text-xs tracking-widest">
70
+ {group.name}
71
+ </SidebarGroupLabel>
72
+ )}
73
+ <SidebarGroupContent>
74
+ <SidebarMenu>
75
+ {group.nodes.map(node => (
76
+ <NavNode key={node.path} node={node} currentPath={pathname} />
77
+ ))}
78
+ </SidebarMenu>
79
+ </SidebarGroupContent>
80
+ </SidebarGroup>
81
+ ))}
82
+ <SidebarLinks />
83
+ </SidebarContent>
84
+ <SidebarFooter className="p-4 space-y-2">
85
+ <Search />
86
+ <Theme />
87
+ </SidebarFooter>
88
+ <SidebarRail />
89
+ </SidebarRoot>
90
+ )
91
+ }
92
+
93
+ function splatFor(pathname: string) {
94
+ return { _splat: pathname.replace(/^\/|\/$/g, "") }
95
+ }
96
+
97
+ function isActive(articlePathname: string, currentPath: string) {
98
+ const normalized = `/${currentPath.replace(/^\/|\/$/g, "")}/`
99
+ return articlePathname === normalized
100
+ }
101
+
102
+ function NavNode(props: { node: ArticleNode; currentPath: string }) {
103
+ const { node, currentPath } = props
104
+ const Icon = articleIcons[node.icon] ?? FileText
105
+ const active = isActive(node.path, currentPath)
106
+
107
+ if (node.children.length === 0) {
108
+ return (
109
+ <SidebarMenuItem>
110
+ <SidebarMenuButton
111
+ isActive={active}
112
+ className={active ? "" : "opacity-75"}
113
+ render={<Link to="/$" params={splatFor(node.path)} />}
114
+ >
115
+ <Icon className="size-4" />
116
+ <span>{node.label ?? node.title}</span>
117
+ </SidebarMenuButton>
118
+ </SidebarMenuItem>
119
+ )
120
+ }
121
+
122
+ return (
123
+ <SidebarMenuItem>
124
+ <Collapsible defaultOpen className="group/collapsible">
125
+ <SidebarMenuButton
126
+ isActive={active}
127
+ className={active ? "" : "opacity-75"}
128
+ render={<Link to="/$" params={splatFor(node.path)} />}
129
+ >
130
+ <Icon className="size-4" />
131
+ <span>{node.label ?? node.title}</span>
132
+ </SidebarMenuButton>
133
+ <CollapsibleTrigger
134
+ aria-label={`Toggle ${node.label ?? node.title} submenu`}
135
+ className="absolute right-1 top-1.5 p-1 rounded-md hover:bg-sidebar-accent"
136
+ >
137
+ <ChevronRight className="size-3.5 transition-transform group-data-[open]/collapsible:rotate-90" />
138
+ </CollapsibleTrigger>
139
+ <CollapsibleContent>
140
+ <SidebarMenuSub>
141
+ {node.children.map(child => {
142
+ const childIsActive = isActive(child.path, currentPath)
143
+ return (
144
+ <SidebarMenuSubItem key={child.path}>
145
+ <SidebarMenuSubButton
146
+ isActive={childIsActive}
147
+ className={childIsActive ? "" : "opacity-75"}
148
+ render={<Link to="/$" params={splatFor(child.path)} />}
149
+ >
150
+ <span>{child.label ?? child.title}</span>
151
+ </SidebarMenuSubButton>
152
+ </SidebarMenuSubItem>
153
+ )
154
+ })}
155
+ </SidebarMenuSub>
156
+ </CollapsibleContent>
157
+ </Collapsible>
158
+ </SidebarMenuItem>
159
+ )
160
+ }