livemark 0.2.0 → 0.7.1

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 (348) 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/package.json +120 -12
  226. package/skills/authoring/SKILL.md +97 -0
  227. package/skills/configuration/SKILL.md +120 -0
  228. package/skills/customization/SKILL.md +118 -0
  229. package/build/descriptor/index.d.ts +0 -3
  230. package/build/descriptor/index.js +0 -4
  231. package/build/descriptor/load.d.ts +0 -8
  232. package/build/descriptor/load.js +0 -40
  233. package/build/descriptor/node.d.ts +0 -125
  234. package/build/descriptor/node.js +0 -9
  235. package/build/descriptor/save.d.ts +0 -8
  236. package/build/descriptor/save.js +0 -17
  237. package/build/descriptor/save.spec.js +0 -80
  238. package/build/descriptor/validate.d.ts +0 -9
  239. package/build/descriptor/validate.js +0 -14
  240. package/build/descriptor/validate.spec.js +0 -124
  241. package/build/dialect/Dialect.d.ts +0 -88
  242. package/build/dialect/Dialect.js +0 -2
  243. package/build/dialect/index.d.ts +0 -3
  244. package/build/dialect/index.js +0 -3
  245. package/build/dialect/load.d.ts +0 -4
  246. package/build/dialect/load.js +0 -7
  247. package/build/dialect/save.d.ts +0 -9
  248. package/build/dialect/save.js +0 -13
  249. package/build/dialect/validate.d.ts +0 -1
  250. package/build/dialect/validate.js +0 -4
  251. package/build/metadata/index.d.ts +0 -3
  252. package/build/metadata/index.js +0 -4
  253. package/build/metadata/load.d.ts +0 -10
  254. package/build/metadata/load.js +0 -55
  255. package/build/metadata/node.d.ts +0 -125
  256. package/build/metadata/node.js +0 -9
  257. package/build/metadata/save.d.ts +0 -8
  258. package/build/metadata/save.js +0 -17
  259. package/build/metadata/save.spec.js +0 -80
  260. package/build/metadata/validate.d.ts +0 -9
  261. package/build/metadata/validate.js +0 -14
  262. package/build/metadata/validate.spec.d.ts +0 -1
  263. package/build/metadata/validate.spec.js +0 -124
  264. package/build/package/Contributor.d.ts +0 -21
  265. package/build/package/Contributor.js +0 -2
  266. package/build/package/Package.d.ts +0 -65
  267. package/build/package/Package.js +0 -2
  268. package/build/package/index.d.ts +0 -4
  269. package/build/package/index.js +0 -3
  270. package/build/package/load.d.ts +0 -4
  271. package/build/package/load.js +0 -7
  272. package/build/package/save.d.ts +0 -9
  273. package/build/package/save.js +0 -13
  274. package/build/package/validate.d.ts +0 -1
  275. package/build/package/validate.js +0 -4
  276. package/build/resource/License.d.ts +0 -18
  277. package/build/resource/License.js +0 -2
  278. package/build/resource/Resource.d.ts +0 -67
  279. package/build/resource/Resource.js +0 -2
  280. package/build/resource/Source.d.ts +0 -17
  281. package/build/resource/Source.js +0 -2
  282. package/build/resource/index.d.ts +0 -5
  283. package/build/resource/index.js +0 -2
  284. package/build/resource/load.d.ts +0 -1
  285. package/build/resource/load.js +0 -2
  286. package/build/resource/save.d.ts +0 -9
  287. package/build/resource/save.js +0 -13
  288. package/build/resource/types/index.d.ts +0 -1
  289. package/build/resource/types/index.js +0 -2
  290. package/build/resource/types/table.d.ts +0 -29
  291. package/build/resource/types/table.js +0 -2
  292. package/build/resource/validate.d.ts +0 -1
  293. package/build/resource/validate.js +0 -4
  294. package/build/schema/Field.d.ts +0 -5
  295. package/build/schema/Field.js +0 -2
  296. package/build/schema/Schema.d.ts +0 -55
  297. package/build/schema/Schema.js +0 -2
  298. package/build/schema/fields/any.d.ts +0 -20
  299. package/build/schema/fields/any.js +0 -2
  300. package/build/schema/fields/array.d.ts +0 -32
  301. package/build/schema/fields/array.js +0 -2
  302. package/build/schema/fields/base.d.ts +0 -42
  303. package/build/schema/fields/base.js +0 -2
  304. package/build/schema/fields/boolean.d.ts +0 -28
  305. package/build/schema/fields/boolean.js +0 -2
  306. package/build/schema/fields/date.d.ts +0 -35
  307. package/build/schema/fields/date.js +0 -2
  308. package/build/schema/fields/datetime.d.ts +0 -35
  309. package/build/schema/fields/datetime.js +0 -2
  310. package/build/schema/fields/duration.d.ts +0 -28
  311. package/build/schema/fields/duration.js +0 -2
  312. package/build/schema/fields/geojson.d.ts +0 -26
  313. package/build/schema/fields/geojson.js +0 -2
  314. package/build/schema/fields/geopoint.d.ts +0 -27
  315. package/build/schema/fields/geopoint.js +0 -2
  316. package/build/schema/fields/index.d.ts +0 -16
  317. package/build/schema/fields/index.js +0 -17
  318. package/build/schema/fields/integer.d.ts +0 -37
  319. package/build/schema/fields/integer.js +0 -2
  320. package/build/schema/fields/list.d.ts +0 -36
  321. package/build/schema/fields/list.js +0 -2
  322. package/build/schema/fields/number.d.ts +0 -40
  323. package/build/schema/fields/number.js +0 -2
  324. package/build/schema/fields/object.d.ts +0 -32
  325. package/build/schema/fields/object.js +0 -2
  326. package/build/schema/fields/string.d.ts +0 -40
  327. package/build/schema/fields/string.js +0 -2
  328. package/build/schema/fields/time.d.ts +0 -35
  329. package/build/schema/fields/time.js +0 -2
  330. package/build/schema/fields/year.d.ts +0 -28
  331. package/build/schema/fields/year.js +0 -2
  332. package/build/schema/fields/yearmonth.d.ts +0 -28
  333. package/build/schema/fields/yearmonth.js +0 -2
  334. package/build/schema/index.d.ts +0 -4
  335. package/build/schema/index.js +0 -2
  336. package/build/schema/load.d.ts +0 -1
  337. package/build/schema/load.js +0 -2
  338. package/build/schema/save.d.ts +0 -9
  339. package/build/schema/save.js +0 -13
  340. package/build/schema/validate.d.ts +0 -1
  341. package/build/schema/validate.js +0 -4
  342. package/build/tsconfig.tsbuildinfo +0 -1
  343. package/index.ts +0 -0
  344. package/tsconfig.json +0 -10
  345. package/typedoc.json +0 -4
  346. /package/build/{descriptor/save.spec.d.ts → actions/changelog/build.unit.d.ts} +0 -0
  347. /package/build/{descriptor/validate.spec.d.ts → actions/config/define.unit.d.ts} +0 -0
  348. /package/build/{metadata/save.spec.d.ts → actions/config/load.unit.d.ts} +0 -0
@@ -0,0 +1,70 @@
1
+ import { useLocation } from "@tanstack/react-router"
2
+ import { ExternalLink } from "lucide-react"
3
+ import { currentSection } from "../content/article.ts"
4
+ import { DynamicIcon } from "../helpers/dynamic-icon.tsx"
5
+ import {
6
+ SidebarGroup,
7
+ SidebarGroupContent,
8
+ SidebarGroupLabel,
9
+ SidebarMenu,
10
+ SidebarMenuButton,
11
+ SidebarMenuItem,
12
+ } from "../elements/sidebar.tsx"
13
+
14
+ /** Shared Links group. Renders sidebar-positioned links always, and
15
+ * header-positioned links only on mobile (mobile collapses all chrome
16
+ * links into the sidebar since the header strip is hidden below `md`). */
17
+ export function SidebarLinks() {
18
+ const pathname = useLocation({ select: l => l.pathname })
19
+ const section = currentSection(`/${pathname.replace(/^\/|\/$/g, "")}/`)
20
+ const links =
21
+ import.meta.env.CONFIG.links?.filter(
22
+ link => !link.prefix || link.prefix === section?.prefix,
23
+ ) ?? []
24
+
25
+ if (!links.length) return null
26
+ const hasDesktopItems = links.some(l => l.position === "sidebar")
27
+
28
+ return (
29
+ <SidebarGroup className={hasDesktopItems ? undefined : "md:hidden"}>
30
+ <SidebarGroupLabel className="uppercase font-mono text-xs tracking-widest">
31
+ Links
32
+ </SidebarGroupLabel>
33
+ <SidebarGroupContent>
34
+ <SidebarMenu>
35
+ {links.map(link => {
36
+ const mobileOnly = link.position !== "sidebar"
37
+ return (
38
+ <SidebarMenuItem
39
+ key={link.url}
40
+ className={mobileOnly ? "md:hidden" : undefined}
41
+ >
42
+ <SidebarMenuButton
43
+ className="text-muted-foreground"
44
+ render={
45
+ <a
46
+ href={link.url}
47
+ target="_blank"
48
+ rel="noopener noreferrer"
49
+ />
50
+ }
51
+ >
52
+ {link.icon && (
53
+ <DynamicIcon name={link.icon} className="size-4" />
54
+ )}
55
+ <span>
56
+ {link.title}{" "}
57
+ <ExternalLink
58
+ className="inline -mt-0.5 opacity-75"
59
+ style={{ width: 12, height: 12 }}
60
+ />
61
+ </span>
62
+ </SidebarMenuButton>
63
+ </SidebarMenuItem>
64
+ )
65
+ })}
66
+ </SidebarMenu>
67
+ </SidebarGroupContent>
68
+ </SidebarGroup>
69
+ )
70
+ }
@@ -0,0 +1,71 @@
1
+ import { Link, useLocation, useMatch } from "@tanstack/react-router"
2
+ import { currentSection, sectionFirstArticle } from "../content/article.ts"
3
+ import { DynamicIcon } from "../helpers/dynamic-icon.tsx"
4
+ import { sectionIcon } from "../helpers/section.ts"
5
+ import {
6
+ SidebarGroup,
7
+ SidebarGroupContent,
8
+ SidebarGroupLabel,
9
+ SidebarMenu,
10
+ SidebarMenuButton,
11
+ SidebarMenuItem,
12
+ } from "../elements/sidebar.tsx"
13
+
14
+ function splatFor(pathname: string) {
15
+ return { _splat: pathname.replace(/^\/|\/$/g, "") }
16
+ }
17
+
18
+ /** Shared Sections group. Renders sidebar-positioned sections always, and
19
+ * header-positioned sections only on mobile (mobile collapses all sections
20
+ * into the sidebar since the header nav strip is hidden below `md`). */
21
+ export function SidebarSections() {
22
+ const pathname = useLocation({ select: l => l.pathname })
23
+ const articleRoute = useMatch({ from: "/$", shouldThrow: false })
24
+ const section = articleRoute
25
+ ? currentSection(`/${pathname.replace(/^\/|\/$/g, "")}/`)
26
+ : undefined
27
+ const sections = import.meta.env.CONFIG.sections ?? []
28
+
29
+ if (!sections.length) return null
30
+ const hasDesktopItems = sections.some(s => s.position === "sidebar")
31
+
32
+ return (
33
+ <SidebarGroup className={hasDesktopItems ? undefined : "md:hidden"}>
34
+ <SidebarGroupLabel className="uppercase font-mono text-xs tracking-widest">
35
+ Sections
36
+ </SidebarGroupLabel>
37
+ <SidebarGroupContent>
38
+ <SidebarMenu>
39
+ {sections.map(s => {
40
+ const target =
41
+ s.type === "blog" || s.type === "changelog"
42
+ ? s.prefix
43
+ : (sectionFirstArticle.get(s.prefix) ?? s.prefix)
44
+ const active = section?.prefix === s.prefix
45
+ const mobileOnly = s.position !== "sidebar"
46
+ return (
47
+ <SidebarMenuItem
48
+ key={s.prefix}
49
+ className={mobileOnly ? "md:hidden" : undefined}
50
+ >
51
+ <SidebarMenuButton
52
+ isActive={active}
53
+ className={active ? "" : "opacity-75"}
54
+ render={
55
+ <Link
56
+ to={target === "/" ? "/" : "/$"}
57
+ params={target && target !== "/" ? splatFor(target) : {}}
58
+ />
59
+ }
60
+ >
61
+ <DynamicIcon name={sectionIcon(s)} className="size-4" />
62
+ <span>{s.title}</span>
63
+ </SidebarMenuButton>
64
+ </SidebarMenuItem>
65
+ )
66
+ })}
67
+ </SidebarMenu>
68
+ </SidebarGroupContent>
69
+ </SidebarGroup>
70
+ )
71
+ }
@@ -0,0 +1,21 @@
1
+ import defaultLogo from "../assets/logo.svg"
2
+
3
+ /** Renders the site logo, title, and description in the sidebar header */
4
+ export function SiteTitle() {
5
+ const logo = import.meta.env.CONFIG.logo ?? defaultLogo
6
+ return (
7
+ <div className="flex items-end gap-2.5 text-sm">
8
+ <img
9
+ src={logo}
10
+ alt={import.meta.env.CONFIG.title}
11
+ className="size-6.5 mb-1.25"
12
+ />
13
+ <div className="flex flex-col gap-0.5 leading-none">
14
+ <span className="font-semibold">{import.meta.env.CONFIG.title}</span>
15
+ <span className="text-xs opacity-80">
16
+ {import.meta.env.CONFIG.description}
17
+ </span>
18
+ </div>
19
+ </div>
20
+ )
21
+ }
@@ -0,0 +1,81 @@
1
+ import { useEffect, useRef, type ComponentProps } from "react"
2
+ import {
3
+ Table,
4
+ TableHeader,
5
+ TableBody,
6
+ TableRow,
7
+ TableHead,
8
+ TableCell,
9
+ } from "../elements/table.tsx"
10
+
11
+ /** Wraps markdown tables with click-to-sort column headers */
12
+ export function SortableTable(props: ComponentProps<"table">) {
13
+ const ref = useRef<HTMLDivElement>(null)
14
+
15
+ useEffect(() => {
16
+ const container = ref.current
17
+ if (!container) return
18
+
19
+ let activeCol = -1
20
+ let ascending = true
21
+
22
+ const headers = container.querySelectorAll<HTMLElement>("th")
23
+ const cleanups: (() => void)[] = []
24
+
25
+ headers.forEach((th, colIndex) => {
26
+ th.style.cursor = "pointer"
27
+ th.classList.add("select-none")
28
+
29
+ const handler = () => {
30
+ if (activeCol === colIndex) {
31
+ ascending = !ascending
32
+ } else {
33
+ activeCol = colIndex
34
+ ascending = true
35
+ }
36
+
37
+ const tbody = container.querySelector("tbody")
38
+ if (!tbody) return
39
+
40
+ const rows = Array.from(tbody.querySelectorAll("tr"))
41
+ rows.sort((a, b) => {
42
+ const aVal = a.children[colIndex]?.textContent?.trim() ?? ""
43
+ const bVal = b.children[colIndex]?.textContent?.trim() ?? ""
44
+ const aNum = Number(aVal)
45
+ const bNum = Number(bVal)
46
+ const cmp =
47
+ aVal !== "" && bVal !== "" && !isNaN(aNum) && !isNaN(bNum)
48
+ ? aNum - bNum
49
+ : aVal.localeCompare(bVal)
50
+ return ascending ? cmp : -cmp
51
+ })
52
+ for (const row of rows) tbody.appendChild(row)
53
+
54
+ headers.forEach((h, i) => {
55
+ const indicator = h.querySelector("[data-sort]")
56
+ if (indicator) indicator.remove()
57
+ if (i === colIndex) {
58
+ const span = document.createElement("span")
59
+ span.dataset.sort = ""
60
+ span.className = "ml-1 text-muted-foreground"
61
+ span.textContent = ascending ? "↑" : "↓"
62
+ h.appendChild(span)
63
+ }
64
+ })
65
+ }
66
+
67
+ th.addEventListener("click", handler)
68
+ cleanups.push(() => th.removeEventListener("click", handler))
69
+ })
70
+
71
+ return () => cleanups.forEach(fn => fn())
72
+ }, [])
73
+
74
+ return (
75
+ <div ref={ref} className="not-prose">
76
+ <Table {...props} />
77
+ </div>
78
+ )
79
+ }
80
+
81
+ export { TableHeader, TableBody, TableRow, TableHead, TableCell }
@@ -0,0 +1,16 @@
1
+ /** Renders a SoundCloud iframe embed */
2
+ export function SoundCloud(props: { url: string }) {
3
+ const src = `https://w.soundcloud.com/player/?url=${encodeURIComponent(props.url)}&color=%23ff5500&auto_play=false&hide_related=true&show_comments=false&show_user=true&show_reposts=false&show_teaser=false`
4
+
5
+ return (
6
+ <div className="not-prose my-4 overflow-hidden rounded-lg border">
7
+ <iframe
8
+ className="w-full"
9
+ height={166}
10
+ src={src}
11
+ title="SoundCloud audio"
12
+ allow="autoplay"
13
+ />
14
+ </div>
15
+ )
16
+ }
@@ -0,0 +1,120 @@
1
+ import { Link } from "@tanstack/react-router"
2
+ import { History } from "lucide-react"
3
+ import { sortedArticles } from "../content/article.ts"
4
+ import { sectionTags } from "../content/tag.ts"
5
+ import { Separator } from "../elements/separator.tsx"
6
+ import { TocContext } from "../helpers/toc-context.ts"
7
+ import { Footer } from "./Footer.tsx"
8
+ import { PageToolbar } from "./PageToolbar.tsx"
9
+ import { MobileToc, Toc } from "./Toc.tsx"
10
+
11
+ /** Blog tag page listing posts filtered by a specific tag */
12
+ export function TagIndex(props: { sectionPrefix: string; tag: string }) {
13
+ const tagMap = sectionTags.get(props.sectionPrefix)
14
+ const paths = tagMap?.get(props.tag) ?? []
15
+ const posts = paths
16
+ .map(p => sortedArticles.find(a => a.path === p))
17
+ .filter(a => a !== undefined)
18
+
19
+ const tocItems = posts.map(post => ({
20
+ url: `#${slugOf(post.path)}`,
21
+ title: post.title,
22
+ depth: 2,
23
+ }))
24
+
25
+ const title = `Posts tagged '${props.tag}'`
26
+
27
+ const content = [
28
+ `# ${title}`,
29
+ ...posts.map(post => {
30
+ const parts = [`## ${post.title}`]
31
+ if (post.date) parts.push(formatDate(post.date))
32
+ if (post.author) {
33
+ parts.push(
34
+ Array.isArray(post.author) ? post.author.join(", ") : post.author,
35
+ )
36
+ }
37
+ if (post.description) parts.push("", post.description)
38
+ return parts.join("\n")
39
+ }),
40
+ ].join("\n\n")
41
+
42
+ return (
43
+ <TocContext.Provider value={tocItems}>
44
+ <MobileToc items={tocItems}>
45
+ <PageToolbar content={content} />
46
+ </MobileToc>
47
+ <div className="flex flex-1 gap-10 p-4 pt-8 md:p-10">
48
+ <div className="flex-1 min-w-0 mx-auto max-w-3xl">
49
+ <div className="prose dark:prose-invert max-w-none mb-8">
50
+ <h1 id="top">
51
+ <a href="#top">Posts tagged &lsquo;{props.tag}&rsquo;</a>
52
+ </h1>
53
+ {posts[0]?.date && (
54
+ <p className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-muted-foreground !-mt-4 !mb-0">
55
+ <span className="flex items-center gap-1.5">
56
+ <History className="size-3.5" />
57
+ Updated {formatDate(posts[0].date)}
58
+ </span>
59
+ </p>
60
+ )}
61
+ </div>
62
+ <div className="space-y-6">
63
+ {posts.map((post, i) => (
64
+ <article key={post.path} id={slugOf(post.path)}>
65
+ {i > 0 && <Separator className="mb-6" />}
66
+ <h2 className="text-2xl">
67
+ <Link
68
+ to="/$"
69
+ params={{ _splat: post.path.replace(/^\/|\/$/g, "") }}
70
+ className="font-medium hover:text-primary transition-colors"
71
+ >
72
+ {post.title}
73
+ </Link>
74
+ </h2>
75
+ <p className="text-sm text-muted-foreground mt-1 flex gap-3">
76
+ {post.date && <span>{formatDate(post.date)}</span>}
77
+ {post.author && (
78
+ <span>
79
+ {Array.isArray(post.author)
80
+ ? post.author.join(", ")
81
+ : post.author}
82
+ </span>
83
+ )}
84
+ </p>
85
+ {post.description && (
86
+ <p className="text-muted-foreground mt-2">
87
+ {post.description}
88
+ </p>
89
+ )}
90
+ </article>
91
+ ))}
92
+ </div>
93
+ <div className="mt-10">
94
+ <Footer />
95
+ </div>
96
+ </div>
97
+ <Toc items={tocItems}>
98
+ <PageToolbar content={content} />
99
+ </Toc>
100
+ </div>
101
+ </TocContext.Provider>
102
+ )
103
+ }
104
+
105
+ function slugOf(path: string) {
106
+ return (
107
+ path
108
+ .replace(/^\/|\/$/g, "")
109
+ .split("/")
110
+ .pop() ?? ""
111
+ )
112
+ }
113
+
114
+ function formatDate(iso: string) {
115
+ return new Date(iso).toLocaleDateString("en-US", {
116
+ year: "numeric",
117
+ month: "short",
118
+ day: "numeric",
119
+ })
120
+ }
@@ -0,0 +1,39 @@
1
+ import { Moon, Sun } from "lucide-react"
2
+ import { useHotkey } from "@tanstack/react-hotkeys"
3
+ import { Button } from "../elements/button.tsx"
4
+ import { getCurrentTheme, setTheme } from "../helpers/theme.ts"
5
+
6
+ export function Theme() {
7
+ const handleToggle = () => {
8
+ const oldTheme = getCurrentTheme()
9
+ const newTheme = oldTheme === "light" ? "dark" : "light"
10
+ setTheme(newTheme)
11
+ }
12
+
13
+ useHotkey("T", handleToggle)
14
+
15
+ return (
16
+ <Button
17
+ variant="ghost"
18
+ size="sm"
19
+ onClick={handleToggle}
20
+ title="Change Theme"
21
+ className="w-full h-auto rounded-xl cursor-pointer text-xs font-normal justify-start text-muted-foreground bg-sidebar-accent/70 border border-border shadow-xs hover:!bg-background px-3 py-2"
22
+ >
23
+ <div className="flex flex-1 gap-2 items-center dark:hidden">
24
+ <Sun className="size-4" />
25
+ <span className="flex-1 text-left">Light Theme</span>
26
+ <kbd className="rounded border border-border bg-muted px-1.5 py-0.5 text-[10px] font-mono">
27
+ T
28
+ </kbd>
29
+ </div>
30
+ <div className="flex-1 gap-2 items-center hidden dark:flex">
31
+ <Moon className="size-4" />
32
+ <span className="flex-1 text-left">Dark Theme</span>
33
+ <kbd className="rounded border border-border bg-muted px-1.5 py-0.5 text-[10px] font-mono">
34
+ T
35
+ </kbd>
36
+ </div>
37
+ </Button>
38
+ )
39
+ }
@@ -0,0 +1,226 @@
1
+ import { ChevronDown } from "lucide-react"
2
+ import { useEffect, useRef, useState, type ReactNode } from "react"
3
+ import type { TocItem } from "../models/toc.ts"
4
+
5
+ export function Toc(props: { items: TocItem[]; children?: ReactNode }) {
6
+ const activeId = useActiveHeading(props.items)
7
+
8
+ if (props.items.length === 0 && !props.children) return null
9
+
10
+ return (
11
+ <aside className="hidden xl:block w-56 shrink-0">
12
+ <div className="sticky top-20 max-h-[calc(100vh-5rem)] overflow-auto [scrollbar-width:none]">
13
+ {props.children}
14
+ {props.items.length > 0 && (
15
+ <nav>
16
+ <p className="text-sm font-medium mb-3">Contents</p>
17
+ <ul className="text-sm border-l border-border">
18
+ {props.items.map(item => (
19
+ <li key={item.url}>
20
+ <a
21
+ href={item.url}
22
+ data-active={activeId === item.url.slice(1) || undefined}
23
+ className={`-ms-px block py-1.5 border-l border-transparent text-muted-foreground transition-colors hover:text-foreground data-[active]:border-primary data-[active]:text-primary data-[active]:font-medium ${item.depth <= 2 ? "ps-3" : item.depth === 3 ? "ps-6" : "ps-8"}`}
24
+ >
25
+ {item.title}
26
+ </a>
27
+ </li>
28
+ ))}
29
+ </ul>
30
+ </nav>
31
+ )}
32
+ </div>
33
+ </aside>
34
+ )
35
+ }
36
+
37
+ /** Mobile-only sticky secondary bar showing the active heading and an
38
+ * overlay dropdown with the Contents + page Actions. */
39
+ export function MobileToc(props: { items: TocItem[]; children?: ReactNode }) {
40
+ const activeId = useActiveHeading(props.items)
41
+ const [open, setOpen] = useState(false)
42
+ const wrapperRef = useRef<HTMLDivElement>(null)
43
+
44
+ useEffect(() => {
45
+ if (!open) return
46
+ const onPointerDown = (event: MouseEvent) => {
47
+ if (!wrapperRef.current?.contains(event.target as Node)) setOpen(false)
48
+ }
49
+ const onKey = (event: KeyboardEvent) => {
50
+ if (event.key === "Escape") setOpen(false)
51
+ }
52
+ document.addEventListener("mousedown", onPointerDown)
53
+ document.addEventListener("keydown", onKey)
54
+ return () => {
55
+ document.removeEventListener("mousedown", onPointerDown)
56
+ document.removeEventListener("keydown", onKey)
57
+ }
58
+ }, [open])
59
+
60
+ if (props.items.length === 0 && !props.children) return null
61
+
62
+ const active = props.items.find(item => item.url.slice(1) === activeId)
63
+ const title = active?.title ?? props.items[0]?.title ?? "Contents"
64
+
65
+ return (
66
+ <div
67
+ ref={wrapperRef}
68
+ className="xl:hidden sticky top-16 z-20 border-b bg-background"
69
+ >
70
+ <button
71
+ type="button"
72
+ onClick={() => setOpen(v => !v)}
73
+ aria-expanded={open}
74
+ className="flex w-full items-center gap-3 px-4 py-3 text-sm text-muted-foreground"
75
+ >
76
+ <ScrollProgressRing />
77
+ <span className="truncate flex-1 text-left">{title}</span>
78
+ <ChevronDown
79
+ className={`size-4 shrink-0 transition-transform ${open ? "rotate-180" : ""}`}
80
+ />
81
+ </button>
82
+ {open && (
83
+ <div className="absolute top-full left-0 right-0 mt-px max-h-[calc(100vh-8rem)] overflow-auto border-b bg-background shadow-md animate-in slide-in-from-top-2 fade-in-0 duration-200">
84
+ <div className="space-y-4 px-4 pt-4 pb-4">
85
+ {props.children}
86
+ {props.items.length > 0 && (
87
+ <nav>
88
+ <p className="mb-2 text-sm font-medium">Contents</p>
89
+ <ul className="border-l border-border text-sm">
90
+ {props.items.map(item => (
91
+ <li key={item.url}>
92
+ <a
93
+ href={item.url}
94
+ data-active={
95
+ activeId === item.url.slice(1) || undefined
96
+ }
97
+ onClick={() => setOpen(false)}
98
+ className={`-ms-px block border-l border-transparent py-1.5 text-muted-foreground transition-colors hover:text-foreground data-[active]:border-primary data-[active]:font-medium data-[active]:text-primary ${item.depth <= 2 ? "ps-3" : item.depth === 3 ? "ps-6" : "ps-8"}`}
99
+ >
100
+ {item.title}
101
+ </a>
102
+ </li>
103
+ ))}
104
+ </ul>
105
+ </nav>
106
+ )}
107
+ </div>
108
+ </div>
109
+ )}
110
+ </div>
111
+ )
112
+ }
113
+
114
+ function ScrollProgressRing() {
115
+ const progress = useScrollProgress()
116
+ const radius = 6
117
+ const circumference = 2 * Math.PI * radius
118
+ const offset = circumference * (1 - progress)
119
+ return (
120
+ <svg viewBox="0 0 16 16" className="size-4 shrink-0 -rotate-90" aria-hidden>
121
+ <circle
122
+ cx="8"
123
+ cy="8"
124
+ r={radius}
125
+ className="stroke-muted-foreground/30"
126
+ strokeWidth="2"
127
+ fill="none"
128
+ />
129
+ <circle
130
+ cx="8"
131
+ cy="8"
132
+ r={radius}
133
+ className="stroke-primary transition-[stroke-dashoffset] duration-150"
134
+ strokeWidth="2"
135
+ fill="none"
136
+ strokeLinecap="round"
137
+ strokeDasharray={circumference}
138
+ strokeDashoffset={offset}
139
+ />
140
+ </svg>
141
+ )
142
+ }
143
+
144
+ function useScrollProgress() {
145
+ const [progress, setProgress] = useState(0)
146
+ useEffect(() => {
147
+ const update = () => {
148
+ const max = document.documentElement.scrollHeight - window.innerHeight
149
+ const next = max > 0 ? window.scrollY / max : 0
150
+ setProgress(Math.min(1, Math.max(0, next)))
151
+ }
152
+ update()
153
+ window.addEventListener("scroll", update, { passive: true })
154
+ window.addEventListener("resize", update)
155
+ return () => {
156
+ window.removeEventListener("scroll", update)
157
+ window.removeEventListener("resize", update)
158
+ }
159
+ }, [])
160
+ return progress
161
+ }
162
+
163
+ function useActiveHeading(items: TocItem[]) {
164
+ const [activeId, setActiveId] = useState<string>("")
165
+ const observer = useRef<IntersectionObserver | null>(null)
166
+
167
+ useEffect(() => {
168
+ const ids = items.map(item => item.url.slice(1))
169
+ const elements = ids
170
+ .map(id => document.getElementById(id))
171
+ .filter(Boolean) as HTMLElement[]
172
+
173
+ if (elements.length === 0) return
174
+
175
+ const visibleSet = new Map<string, number>()
176
+
177
+ observer.current = new IntersectionObserver(
178
+ entries => {
179
+ for (const entry of entries) {
180
+ if (entry.isIntersecting) {
181
+ visibleSet.set(entry.target.id, entry.time)
182
+ } else {
183
+ visibleSet.delete(entry.target.id)
184
+ }
185
+ }
186
+
187
+ if (visibleSet.size > 0) {
188
+ let latest = ""
189
+ let latestTime = 0
190
+ for (const [id, time] of visibleSet) {
191
+ if (time > latestTime) {
192
+ latest = id
193
+ latestTime = time
194
+ }
195
+ }
196
+ setActiveId(latest)
197
+ } else {
198
+ const viewTop = window.scrollY
199
+ let closest = ""
200
+ let closestDist = Number.POSITIVE_INFINITY
201
+ for (const el of elements) {
202
+ const dist = Math.abs(
203
+ viewTop - el.getBoundingClientRect().top - window.scrollY,
204
+ )
205
+ if (dist < closestDist) {
206
+ closest = el.id
207
+ closestDist = dist
208
+ }
209
+ }
210
+ setActiveId(closest)
211
+ }
212
+ },
213
+ { threshold: 0.98 },
214
+ )
215
+
216
+ for (const el of elements) {
217
+ observer.current.observe(el)
218
+ }
219
+
220
+ return () => {
221
+ observer.current?.disconnect()
222
+ }
223
+ }, [items])
224
+
225
+ return activeId
226
+ }
@@ -0,0 +1,16 @@
1
+ /** Renders a responsive YouTube iframe embed */
2
+ export function YouTube(props: { id: string }) {
3
+ return (
4
+ <div className="not-prose my-4 overflow-hidden rounded-lg border">
5
+ <div className="aspect-video">
6
+ <iframe
7
+ className="h-full w-full"
8
+ src={`https://www.youtube.com/embed/${props.id}`}
9
+ title="YouTube video"
10
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
11
+ allowFullScreen
12
+ />
13
+ </div>
14
+ </div>
15
+ )
16
+ }
@@ -0,0 +1,25 @@
1
+ import { useEffect, useState, type ComponentProps } from "react"
2
+ import Zoom from "react-medium-image-zoom"
3
+ import "react-medium-image-zoom/dist/styles.css"
4
+
5
+ /** Image component with click-to-zoom and light/dark theme variant support */
6
+ export function ZoomImage(props: ComponentProps<"img">) {
7
+ const [mounted, setMounted] = useState(false)
8
+ useEffect(() => setMounted(true), [])
9
+
10
+ const src = props.src ?? ""
11
+ let imgProps = props
12
+ if (src.endsWith("#light")) {
13
+ imgProps = { ...props, src: src.slice(0, -6), className: "dark:hidden" }
14
+ } else if (src.endsWith("#dark")) {
15
+ imgProps = {
16
+ ...props,
17
+ src: src.slice(0, -5),
18
+ className: "hidden dark:block",
19
+ }
20
+ }
21
+
22
+ const img = <img {...imgProps} alt={imgProps.alt ?? ""} />
23
+ if (!mounted) return img
24
+ return <Zoom>{img}</Zoom>
25
+ }