livemark 0.25.0 → 0.26.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 (397) hide show
  1. package/README.md +3 -2
  2. package/build/actions/config/define.d.ts +6 -0
  3. package/build/actions/config/define.unit.js +8 -1
  4. package/build/actions/escape/copy.d.ts +11 -0
  5. package/build/actions/escape/copy.js +29 -0
  6. package/build/actions/escape/copy.unit.d.ts +1 -0
  7. package/build/actions/escape/copy.unit.js +37 -0
  8. package/build/actions/escape/list.d.ts +12 -0
  9. package/build/actions/escape/list.js +27 -0
  10. package/build/actions/escape/list.unit.d.ts +1 -0
  11. package/build/actions/escape/list.unit.js +36 -0
  12. package/build/actions/target/build.d.ts +9 -26
  13. package/build/actions/target/build.js +50 -71
  14. package/build/actions/target/build.unit.d.ts +1 -0
  15. package/build/actions/target/build.unit.js +76 -0
  16. package/build/actions/target/clear.d.ts +4 -0
  17. package/build/actions/target/clear.js +14 -0
  18. package/build/actions/target/clear.unit.d.ts +1 -0
  19. package/build/actions/target/clear.unit.js +23 -0
  20. package/build/commands/build.js +1 -2
  21. package/build/commands/clear.d.ts +5 -0
  22. package/build/commands/clear.js +15 -0
  23. package/build/commands/escape.d.ts +4 -3
  24. package/build/commands/escape.js +45 -48
  25. package/build/commands/preview.js +1 -2
  26. package/build/commands/start.js +1 -2
  27. package/build/components/Abbr.d.ts +1 -1
  28. package/build/components/Abbr.js +2 -4
  29. package/build/components/AnsiCode.d.ts +1 -1
  30. package/build/components/AnsiCode.js +2 -2
  31. package/build/components/Article.d.ts +2 -1
  32. package/build/components/Article.js +7 -14
  33. package/build/components/BackToTop.d.ts +1 -1
  34. package/build/components/Banner.d.ts +2 -2
  35. package/build/components/Banner.js +7 -3
  36. package/build/components/BlogIndex.d.ts +1 -1
  37. package/build/components/BlogIndex.js +2 -11
  38. package/build/components/Breadcrumbs.d.ts +1 -1
  39. package/build/components/Breadcrumbs.js +2 -3
  40. package/build/components/Callout.d.ts +1 -1
  41. package/build/components/Callout.js +5 -9
  42. package/build/components/Card.d.ts +1 -1
  43. package/build/components/Card.js +2 -3
  44. package/build/components/Cards.d.ts +1 -1
  45. package/build/components/ChangelogIndex.d.ts +1 -1
  46. package/build/components/ChangelogIndex.js +2 -11
  47. package/build/components/ChoiceList.d.ts +15 -0
  48. package/build/components/ChoiceList.js +13 -0
  49. package/build/components/CodeBlock.d.ts +1 -1
  50. package/build/components/CodeBlock.js +3 -16
  51. package/build/components/CodeTabs.d.ts +1 -1
  52. package/build/components/CodeTabs.js +2 -3
  53. package/build/components/Column.d.ts +1 -1
  54. package/build/components/Columns.d.ts +1 -1
  55. package/build/components/Content.d.ts +1 -1
  56. package/build/components/ContentTab.d.ts +1 -1
  57. package/build/components/ContentTabs.d.ts +1 -1
  58. package/build/components/ContentTabs.js +2 -3
  59. package/build/components/DefaultCatchBoundary.d.ts +1 -1
  60. package/build/components/DefaultCatchBoundary.js +5 -7
  61. package/build/components/DefinitionList.d.ts +3 -3
  62. package/build/components/Details.d.ts +1 -1
  63. package/build/components/Details.js +2 -5
  64. package/build/components/FileTree.d.ts +1 -1
  65. package/build/components/FileTree.js +3 -5
  66. package/build/components/Footer.d.ts +1 -1
  67. package/build/components/Footer.js +2 -5
  68. package/build/components/FootnoteRef.d.ts +1 -1
  69. package/build/components/FootnoteRef.js +2 -4
  70. package/build/components/Header.d.ts +6 -0
  71. package/build/components/Header.js +26 -0
  72. package/build/components/HeaderMenu.d.ts +2 -0
  73. package/build/components/HeaderMenu.js +27 -0
  74. package/build/components/HeaderNav.d.ts +6 -0
  75. package/build/components/HeaderNav.js +74 -0
  76. package/build/components/HeaderSearch.d.ts +4 -0
  77. package/build/components/HeaderSearch.js +33 -0
  78. package/build/components/Heading.d.ts +6 -6
  79. package/build/components/InlineBadge.d.ts +2 -2
  80. package/build/components/InlineBadge.js +1 -1
  81. package/build/components/InlineIcon.d.ts +1 -1
  82. package/build/components/InlineToc.d.ts +1 -1
  83. package/build/components/InlineToc.js +2 -4
  84. package/build/components/Layout.d.ts +2 -1
  85. package/build/components/Layout.js +5 -58
  86. package/build/components/LinkButton.d.ts +2 -2
  87. package/build/components/LinkButton.js +1 -1
  88. package/build/components/MenuSheet.d.ts +22 -0
  89. package/build/components/MenuSheet.js +27 -0
  90. package/build/components/Mermaid.d.ts +1 -1
  91. package/build/components/MermaidRenderer.d.ts +1 -1
  92. package/build/components/MermaidRenderer.js +20 -8
  93. package/build/components/NotFound.d.ts +1 -1
  94. package/build/components/NotFound.js +3 -6
  95. package/build/components/PackageTabs.d.ts +1 -1
  96. package/build/components/PackageTabs.js +3 -7
  97. package/build/components/PageToolbar.d.ts +1 -1
  98. package/build/components/PageToolbar.js +2 -10
  99. package/build/components/PrevNext.d.ts +1 -1
  100. package/build/components/PrevNext.js +2 -10
  101. package/build/components/Root.d.ts +4 -0
  102. package/build/components/Root.js +8 -0
  103. package/build/components/Search.d.ts +5 -2
  104. package/build/components/Search.js +11 -9
  105. package/build/components/SearchDialog.d.ts +1 -1
  106. package/build/components/SearchDialog.js +10 -133
  107. package/build/components/Section.d.ts +1 -1
  108. package/build/components/SectionHeader.d.ts +1 -1
  109. package/build/components/SheetPanel.d.ts +12 -0
  110. package/build/components/SheetPanel.js +19 -0
  111. package/build/components/Sidebar.d.ts +1 -1
  112. package/build/components/Sidebar.js +12 -14
  113. package/build/components/SidebarArticles.d.ts +4 -1
  114. package/build/components/SidebarArticles.js +5 -9
  115. package/build/components/SidebarLinks.d.ts +4 -1
  116. package/build/components/SidebarLinks.js +6 -9
  117. package/build/components/SidebarPosts.d.ts +4 -1
  118. package/build/components/SidebarPosts.js +7 -23
  119. package/build/components/SidebarReleases.d.ts +4 -1
  120. package/build/components/SidebarReleases.js +5 -15
  121. package/build/components/SidebarSectionPicker.d.ts +8 -0
  122. package/build/components/SidebarSectionPicker.js +31 -0
  123. package/build/components/SidebarSections.d.ts +1 -1
  124. package/build/components/SidebarSections.js +5 -9
  125. package/build/components/SiteLink.d.ts +1 -1
  126. package/build/components/SiteTitle.d.ts +1 -1
  127. package/build/components/SiteTitle.js +2 -6
  128. package/build/components/SortableTable.d.ts +1 -1
  129. package/build/components/SoundCloud.d.ts +1 -1
  130. package/build/components/SoundCloud.js +2 -2
  131. package/build/components/TagIndex.d.ts +1 -1
  132. package/build/components/TagIndex.js +2 -11
  133. package/build/components/Theme.d.ts +2 -1
  134. package/build/components/Theme.js +35 -15
  135. package/build/components/Toc.d.ts +5 -4
  136. package/build/components/Toc.js +35 -35
  137. package/build/components/YouTube.d.ts +1 -1
  138. package/build/components/YouTube.js +2 -2
  139. package/build/components/ZoomImage.d.ts +1 -1
  140. package/build/content-collections.js +5 -1
  141. package/build/elements/accordion.d.ts +4 -4
  142. package/build/elements/accordion.js +2 -3
  143. package/build/elements/alert-dialog.d.ts +12 -12
  144. package/build/elements/alert-dialog.js +2 -4
  145. package/build/elements/alert.d.ts +4 -4
  146. package/build/elements/alert.js +2 -2
  147. package/build/elements/aspect-ratio.d.ts +1 -1
  148. package/build/elements/avatar.d.ts +6 -6
  149. package/build/elements/badge.d.ts +1 -1
  150. package/build/elements/badge.js +3 -1
  151. package/build/elements/breadcrumb.d.ts +6 -6
  152. package/build/elements/breadcrumb.js +2 -4
  153. package/build/elements/button-group.d.ts +2 -2
  154. package/build/elements/button-group.js +5 -5
  155. package/build/elements/button.d.ts +2 -2
  156. package/build/elements/button.js +9 -7
  157. package/build/elements/calendar.d.ts +2 -2
  158. package/build/elements/calendar.js +2 -2
  159. package/build/elements/card.d.ts +7 -7
  160. package/build/elements/card.js +4 -4
  161. package/build/elements/carousel.d.ts +5 -5
  162. package/build/elements/carousel.js +3 -7
  163. package/build/elements/chart.d.ts +6 -6
  164. package/build/elements/chart.js +5 -11
  165. package/build/elements/checkbox.d.ts +1 -1
  166. package/build/elements/collapsible.d.ts +3 -3
  167. package/build/elements/command.d.ts +9 -9
  168. package/build/elements/command.js +5 -11
  169. package/build/elements/context-menu.d.ts +15 -15
  170. package/build/elements/context-menu.js +6 -9
  171. package/build/elements/dialog.d.ts +10 -10
  172. package/build/elements/dialog.js +2 -6
  173. package/build/elements/drawer.d.ts +14 -12
  174. package/build/elements/drawer.js +14 -15
  175. package/build/elements/dropdown-menu.d.ts +15 -15
  176. package/build/elements/dropdown-menu.js +7 -10
  177. package/build/elements/empty.d.ts +6 -6
  178. package/build/elements/empty.js +2 -2
  179. package/build/elements/field.d.ts +10 -10
  180. package/build/elements/field.js +3 -4
  181. package/build/elements/hover-card.d.ts +3 -3
  182. package/build/elements/hover-card.js +2 -2
  183. package/build/elements/input-group.d.ts +7 -7
  184. package/build/elements/input-group.js +5 -5
  185. package/build/elements/input-otp.d.ts +4 -4
  186. package/build/elements/input.d.ts +1 -1
  187. package/build/elements/input.js +2 -2
  188. package/build/elements/item.d.ts +9 -9
  189. package/build/elements/item.js +2 -2
  190. package/build/elements/kbd.d.ts +2 -2
  191. package/build/elements/label.d.ts +1 -1
  192. package/build/elements/menubar.d.ts +16 -16
  193. package/build/elements/menubar.js +9 -11
  194. package/build/elements/native-select.d.ts +3 -3
  195. package/build/elements/native-select.js +2 -4
  196. package/build/elements/navigation-menu.d.ts +8 -8
  197. package/build/elements/navigation-menu.js +3 -5
  198. package/build/elements/pagination.d.ts +7 -7
  199. package/build/elements/pagination.js +4 -10
  200. package/build/elements/popover.d.ts +6 -6
  201. package/build/elements/popover.js +2 -2
  202. package/build/elements/progress.d.ts +5 -5
  203. package/build/elements/progress.js +2 -3
  204. package/build/elements/radio-group.d.ts +2 -2
  205. package/build/elements/resizable.d.ts +3 -3
  206. package/build/elements/scroll-area.d.ts +2 -2
  207. package/build/elements/scroll-area.js +2 -4
  208. package/build/elements/select.d.ts +9 -9
  209. package/build/elements/select.js +4 -9
  210. package/build/elements/separator.d.ts +1 -1
  211. package/build/elements/sheet.d.ts +8 -8
  212. package/build/elements/sheet.js +2 -6
  213. package/build/elements/sidebar.d.ts +19 -19
  214. package/build/elements/sidebar.js +15 -37
  215. package/build/elements/skeleton.d.ts +1 -1
  216. package/build/elements/slider.d.ts +2 -1
  217. package/build/elements/slider.js +2 -3
  218. package/build/elements/sonner.d.ts +1 -1
  219. package/build/elements/spinner.d.ts +1 -1
  220. package/build/elements/switch.d.ts +1 -1
  221. package/build/elements/table.d.ts +8 -8
  222. package/build/elements/tabs.d.ts +4 -4
  223. package/build/elements/tabs.js +3 -3
  224. package/build/elements/textarea.d.ts +1 -1
  225. package/build/elements/textarea.js +2 -2
  226. package/build/elements/toggle-group.d.ts +3 -2
  227. package/build/elements/toggle-group.js +3 -3
  228. package/build/elements/toggle.d.ts +1 -1
  229. package/build/elements/toggle.js +2 -2
  230. package/build/elements/tooltip.d.ts +4 -4
  231. package/build/elements/tooltip.js +2 -3
  232. package/build/head.d.ts +4 -0
  233. package/build/head.js +6 -0
  234. package/build/helpers/article-search.d.ts +19 -0
  235. package/build/helpers/article-search.js +134 -0
  236. package/build/helpers/dependency-dir.d.ts +5 -0
  237. package/build/helpers/dependency-dir.js +20 -0
  238. package/build/helpers/dependency-dir.unit.d.ts +1 -0
  239. package/build/helpers/dependency-dir.unit.js +25 -0
  240. package/build/helpers/dynamic-icon-resolver.d.ts +1 -1
  241. package/build/helpers/dynamic-icon.d.ts +1 -1
  242. package/build/helpers/head.d.ts +53 -0
  243. package/build/helpers/head.js +69 -0
  244. package/build/helpers/head.unit.d.ts +1 -0
  245. package/build/helpers/head.unit.js +108 -0
  246. package/build/helpers/nav-items.d.ts +11 -0
  247. package/build/helpers/nav-items.js +65 -0
  248. package/build/helpers/render-inline-markdown.d.ts +1 -0
  249. package/build/helpers/render-inline-markdown.js +34 -0
  250. package/build/helpers/render-inline-markdown.unit.d.ts +1 -0
  251. package/build/helpers/render-inline-markdown.unit.js +20 -0
  252. package/build/helpers/search-highlight.d.ts +8 -0
  253. package/build/helpers/search-highlight.js +59 -0
  254. package/build/helpers/search-highlight.unit.d.ts +1 -0
  255. package/build/helpers/search-highlight.unit.js +36 -0
  256. package/build/helpers/target-export.d.ts +9 -0
  257. package/build/helpers/target-export.js +24 -0
  258. package/build/helpers/target-export.unit.d.ts +1 -0
  259. package/build/helpers/target-export.unit.js +25 -0
  260. package/build/helpers/theme.d.ts +6 -2
  261. package/build/helpers/theme.js +40 -7
  262. package/build/helpers/theme.unit.d.ts +1 -0
  263. package/build/helpers/theme.unit.js +30 -0
  264. package/build/hooks/hydrated.d.ts +2 -0
  265. package/build/hooks/hydrated.js +15 -0
  266. package/build/index.d.ts +1 -0
  267. package/build/index.js +1 -1
  268. package/build/models/config.d.ts +32 -6
  269. package/build/models/config.js +10 -1
  270. package/build/models/section.d.ts +4 -2
  271. package/build/models/section.js +1 -1
  272. package/build/plugins/vite-livemark.d.ts +4 -5
  273. package/build/plugins/vite-livemark.js +56 -90
  274. package/build/program.js +4 -2
  275. package/build/routes/$.d.ts +4 -1
  276. package/build/routes/$.js +20 -9
  277. package/build/routes/__root.js +29 -63
  278. package/build/settings.js +2 -1
  279. package/package.json +68 -66
  280. package/{.agents/skills → skills}/livemark-authoring/SKILL.md +2 -2
  281. package/{.agents/skills → skills}/livemark-configuration/SKILL.md +12 -5
  282. package/skills/livemark-customization/SKILL.md +145 -0
  283. package/source/actions/config/define.unit.ts +9 -0
  284. package/source/actions/escape/copy.ts +35 -0
  285. package/source/actions/escape/copy.unit.ts +51 -0
  286. package/source/actions/escape/list.ts +30 -0
  287. package/source/actions/escape/list.unit.ts +42 -0
  288. package/source/actions/target/build.ts +49 -69
  289. package/source/actions/target/build.unit.ts +95 -0
  290. package/source/actions/target/clear.ts +14 -0
  291. package/source/actions/target/clear.unit.ts +25 -0
  292. package/source/commands/build.ts +0 -1
  293. package/source/commands/clear.ts +15 -0
  294. package/source/commands/escape.ts +42 -59
  295. package/source/commands/preview.ts +0 -1
  296. package/source/commands/start.ts +0 -1
  297. package/source/components/AnsiCode.tsx +1 -1
  298. package/source/components/Article.tsx +8 -4
  299. package/source/components/Banner.tsx +12 -2
  300. package/source/components/BlogIndex.tsx +1 -1
  301. package/source/components/Callout.tsx +4 -6
  302. package/source/components/ChangelogIndex.tsx +1 -1
  303. package/source/components/ChoiceList.tsx +69 -0
  304. package/source/components/CodeBlock.tsx +6 -6
  305. package/source/components/CodeTabs.tsx +1 -1
  306. package/source/components/ContentTabs.tsx +1 -1
  307. package/source/components/DefaultCatchBoundary.tsx +4 -6
  308. package/source/components/Details.tsx +1 -1
  309. package/source/components/FileTree.tsx +1 -1
  310. package/source/components/Header.tsx +52 -0
  311. package/source/components/HeaderMenu.tsx +41 -0
  312. package/source/components/HeaderNav.tsx +145 -0
  313. package/source/components/HeaderSearch.tsx +77 -0
  314. package/source/components/InlineBadge.tsx +1 -1
  315. package/source/components/InlineToc.tsx +1 -1
  316. package/source/components/Layout.tsx +16 -160
  317. package/source/components/LinkButton.tsx +9 -1
  318. package/source/components/MenuSheet.tsx +92 -0
  319. package/source/components/MermaidRenderer.tsx +19 -8
  320. package/source/components/NotFound.tsx +3 -5
  321. package/source/components/PackageTabs.tsx +2 -2
  322. package/source/components/PageToolbar.tsx +11 -3
  323. package/source/components/PrevNext.tsx +2 -2
  324. package/source/components/Root.tsx +13 -0
  325. package/source/components/Search.tsx +28 -8
  326. package/source/components/SearchDialog.tsx +15 -189
  327. package/source/components/SheetPanel.tsx +69 -0
  328. package/source/components/Sidebar.tsx +27 -14
  329. package/source/components/SidebarArticles.tsx +4 -3
  330. package/source/components/SidebarLinks.tsx +3 -2
  331. package/source/components/SidebarPosts.tsx +3 -2
  332. package/source/components/SidebarReleases.tsx +3 -2
  333. package/source/components/SidebarSectionPicker.tsx +70 -0
  334. package/source/components/SiteTitle.tsx +5 -5
  335. package/source/components/SoundCloud.tsx +1 -1
  336. package/source/components/TagIndex.tsx +1 -1
  337. package/source/components/Theme.tsx +55 -29
  338. package/source/components/Toc.tsx +76 -55
  339. package/source/components/YouTube.tsx +1 -1
  340. package/source/content-collections.ts +4 -0
  341. package/source/elements/alert-dialog.tsx +1 -1
  342. package/source/elements/alert.tsx +1 -1
  343. package/source/elements/badge.tsx +4 -0
  344. package/source/elements/button-group.tsx +4 -4
  345. package/source/elements/button.tsx +10 -6
  346. package/source/elements/calendar.tsx +1 -1
  347. package/source/elements/card.tsx +3 -3
  348. package/source/elements/command.tsx +7 -4
  349. package/source/elements/context-menu.tsx +5 -5
  350. package/source/elements/dialog.tsx +1 -1
  351. package/source/elements/drawer.tsx +59 -43
  352. package/source/elements/dropdown-menu.tsx +6 -6
  353. package/source/elements/empty.tsx +1 -1
  354. package/source/elements/field.tsx +1 -1
  355. package/source/elements/hover-card.tsx +1 -1
  356. package/source/elements/input-group.tsx +4 -4
  357. package/source/elements/input.tsx +1 -1
  358. package/source/elements/item.tsx +1 -1
  359. package/source/elements/menubar.tsx +8 -8
  360. package/source/elements/native-select.tsx +1 -1
  361. package/source/elements/popover.tsx +1 -1
  362. package/source/elements/select.tsx +3 -3
  363. package/source/elements/sidebar.tsx +8 -21
  364. package/source/elements/tabs.tsx +2 -2
  365. package/source/elements/textarea.tsx +1 -1
  366. package/source/elements/toggle-group.tsx +2 -2
  367. package/source/elements/toggle.tsx +1 -1
  368. package/source/env.d.ts +1 -0
  369. package/source/head.ts +7 -0
  370. package/source/helpers/article-search.tsx +192 -0
  371. package/source/helpers/dependency-dir.ts +20 -0
  372. package/source/helpers/dependency-dir.unit.ts +39 -0
  373. package/source/helpers/head.ts +94 -0
  374. package/source/helpers/head.unit.ts +116 -0
  375. package/source/helpers/nav-items.tsx +102 -0
  376. package/source/helpers/render-inline-markdown.ts +44 -0
  377. package/source/helpers/render-inline-markdown.unit.ts +30 -0
  378. package/source/helpers/search-highlight.ts +73 -0
  379. package/source/helpers/search-highlight.unit.ts +42 -0
  380. package/source/helpers/target-export.ts +28 -0
  381. package/source/helpers/target-export.unit.ts +37 -0
  382. package/source/helpers/theme.ts +40 -6
  383. package/source/helpers/theme.unit.ts +32 -0
  384. package/source/hooks/hydrated.ts +18 -0
  385. package/source/index.ts +1 -0
  386. package/source/models/config.ts +13 -0
  387. package/source/models/section.ts +3 -0
  388. package/source/plugins/vite-livemark.ts +54 -95
  389. package/source/program.ts +2 -0
  390. package/source/routes/$.tsx +27 -9
  391. package/source/routes/__root.tsx +51 -71
  392. package/source/settings.ts +1 -0
  393. package/source/styles/general.css +59 -11
  394. package/source/styles/markdown.css +13 -7
  395. package/source/styles/shape.css +14 -0
  396. package/.agents/skills/change-review/SKILL.md +0 -47
  397. package/.agents/skills/livemark-customization/SKILL.md +0 -118
@@ -0,0 +1,102 @@
1
+ import { config } from "livemark:virtual"
2
+ import { Link, useLocation } from "@tanstack/react-router"
3
+ import {
4
+ sectionFirstArticle,
5
+ sectionFlatArticles,
6
+ sortedArticles,
7
+ } from "../content/article.ts"
8
+ import { DynamicIcon } from "./dynamic-icon.tsx"
9
+ import { prefixUrl } from "./prefix-url.ts"
10
+ import { customSectionActive } from "./section.ts"
11
+
12
+ export type NavItem = {
13
+ key: string
14
+ title: string
15
+ icon?: string
16
+ suffix?: string
17
+ active: boolean
18
+ href?: string
19
+ target?: string
20
+ }
21
+
22
+ export function useNavItems(activePrefix?: string) {
23
+ const pathname = useLocation({ select: l => l.pathname })
24
+ const sections = config.sections ?? []
25
+ const items: NavItem[] = sections
26
+ .filter(section => section.position === "header")
27
+ .map(section => {
28
+ if (section.type === "custom") {
29
+ return {
30
+ key: section.url,
31
+ title: section.title,
32
+ icon: section.icon,
33
+ active: customSectionActive(section.url, pathname),
34
+ href: section.url,
35
+ }
36
+ }
37
+ const suffix =
38
+ section.type === "changelog" && section.version
39
+ ? sortedArticles.find(
40
+ a => a.path === sectionFlatArticles.get(section.prefix)?.[0],
41
+ )?.title
42
+ : undefined
43
+ return {
44
+ key: section.prefix,
45
+ title: section.title,
46
+ icon: section.icon,
47
+ suffix,
48
+ active: activePrefix === section.prefix,
49
+ target:
50
+ section.type === "blog" || section.type === "changelog"
51
+ ? section.prefix
52
+ : sectionFirstArticle.get(section.prefix),
53
+ }
54
+ })
55
+ if (!sections.length) {
56
+ items.push({ key: "/", title: "Docs", active: true, target: "/" })
57
+ }
58
+ for (const link of config.links ?? []) {
59
+ if (link.position !== "header") continue
60
+ if (link.prefix && link.prefix !== activePrefix) continue
61
+ items.push({
62
+ key: link.url,
63
+ title: link.title,
64
+ icon: link.icon,
65
+ active: false,
66
+ href: link.url,
67
+ })
68
+ }
69
+ return items
70
+ }
71
+
72
+ export function renderNavAnchor(item: NavItem, className?: string) {
73
+ const children = (
74
+ <>
75
+ {item.icon && <DynamicIcon name={item.icon} className="size-3.5" />}
76
+ <span>
77
+ {item.title}
78
+ {item.suffix && <span className="opacity-80"> ({item.suffix})</span>}
79
+ </span>
80
+ </>
81
+ )
82
+ if (item.href) {
83
+ return (
84
+ <a href={prefixUrl(item.href, config.base)} className={className}>
85
+ {children}
86
+ </a>
87
+ )
88
+ }
89
+ return (
90
+ <Link
91
+ to={item.target === "/" ? "/" : "/$"}
92
+ params={item.target && item.target !== "/" ? splatFor(item.target) : {}}
93
+ className={className}
94
+ >
95
+ {children}
96
+ </Link>
97
+ )
98
+ }
99
+
100
+ function splatFor(pathname: string) {
101
+ return { _splat: pathname.replace(/^\/|\/$/g, "") }
102
+ }
@@ -0,0 +1,44 @@
1
+ import rehypeStringify from "rehype-stringify"
2
+ import remarkGfm from "remark-gfm"
3
+ import remarkParse from "remark-parse"
4
+ import remarkRehype from "remark-rehype"
5
+ import { unified } from "unified"
6
+ import { visit } from "unist-util-visit"
7
+ import { prefixUrl } from "./prefix-url.ts"
8
+
9
+ interface HastNode {
10
+ type: string
11
+ tagName?: string
12
+ properties?: Record<string, unknown>
13
+ children?: HastNode[]
14
+ }
15
+
16
+ export function renderInlineMarkdown(markdown: string, base: string | undefined) {
17
+ const html = unified()
18
+ .use(remarkParse)
19
+ .use(remarkGfm)
20
+ .use(remarkRehype)
21
+ .use(() => (tree: HastNode) => rewriteLinks(tree, base))
22
+ .use(() => unwrapParagraphs)
23
+ .use(rehypeStringify)
24
+ .processSync(markdown)
25
+ return String(html).trim()
26
+ }
27
+
28
+ function rewriteLinks(tree: HastNode, base: string | undefined) {
29
+ visit(tree, "element", (node: HastNode) => {
30
+ if (node.tagName !== "a" || typeof node.properties?.href !== "string") return
31
+ const href = node.properties.href
32
+ node.properties.href = prefixUrl(href, base)
33
+ if (/^https?:\/\//.test(href)) {
34
+ node.properties.target = "_blank"
35
+ node.properties.rel = ["noopener", "noreferrer"]
36
+ }
37
+ })
38
+ }
39
+
40
+ function unwrapParagraphs(tree: HastNode) {
41
+ tree.children = (tree.children ?? []).flatMap(child =>
42
+ child.type === "element" && child.tagName === "p" ? (child.children ?? []) : [child],
43
+ )
44
+ }
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it } from "vite-plus/test"
2
+ import { renderInlineMarkdown } from "./render-inline-markdown.ts"
3
+
4
+ describe("renderInlineMarkdown", () => {
5
+ it("should render plain text without a paragraph", () => {
6
+ expect(renderInlineMarkdown("Hello world", undefined)).toBe("Hello world")
7
+ })
8
+
9
+ it("should render bold, emphasis and code", () => {
10
+ expect(renderInlineMarkdown("**a** _b_ `c`", undefined)).toBe(
11
+ "<strong>a</strong> <em>b</em> <code>c</code>",
12
+ )
13
+ })
14
+
15
+ it("should open absolute links in a new tab", () => {
16
+ expect(renderInlineMarkdown("[x](https://example.com)", undefined)).toBe(
17
+ '<a href="https://example.com" target="_blank" rel="noopener noreferrer">x</a>',
18
+ )
19
+ })
20
+
21
+ it("should prefix root-relative links with the base", () => {
22
+ expect(renderInlineMarkdown("[x](/blog/)", "/repo")).toBe(
23
+ '<a href="/repo/blog/">x</a>',
24
+ )
25
+ })
26
+
27
+ it("should drop raw HTML tags", () => {
28
+ expect(renderInlineMarkdown("a <b onclick=x>c</b>", undefined)).toBe("a c")
29
+ })
30
+ })
@@ -0,0 +1,73 @@
1
+ import { useEffect, type RefObject } from "react"
2
+
3
+ const HIGHLIGHT_NAME = "search-match"
4
+
5
+ /** Build a case-insensitive pattern matching each word of a search query */
6
+ export function buildHighlightPattern(query: string) {
7
+ const words = [...new Set(query.toLowerCase().split(/\s+/))]
8
+ .filter(word => word.length >= 2)
9
+ .sort((a, b) => b.length - a.length)
10
+ .map(word => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
11
+ if (!words.length) return undefined
12
+ return new RegExp(words.join("|"), "gi")
13
+ }
14
+
15
+ /** Find the `[start, end]` offsets of every pattern match in a text */
16
+ export function findTextMatches(text: string, pattern: RegExp) {
17
+ return [...text.matchAll(pattern)].map(
18
+ match => [match.index, match.index + match[0].length] as const,
19
+ )
20
+ }
21
+
22
+ /** Highlight search query matches under `root` and scroll to the first visible one;
23
+ * `page` re-runs it when the same query lands on another page */
24
+ export function useSearchHighlight(
25
+ root: RefObject<HTMLElement | null>,
26
+ page: string,
27
+ query?: string,
28
+ ) {
29
+ useEffect(() => {
30
+ const pattern = query ? buildHighlightPattern(query) : undefined
31
+ if (!pattern || !root.current || typeof CSS === "undefined" || !CSS.highlights) return
32
+
33
+ const ranges = collectMatchRanges(root.current, pattern)
34
+ CSS.highlights.set(HIGHLIGHT_NAME, new Highlight(...ranges))
35
+
36
+ // An explicit anchor in the URL wins over the first match. The frame delay lets
37
+ // the router's scroll restoration, which resets to the top, run first.
38
+ const frame = window.location.hash
39
+ ? undefined
40
+ : requestAnimationFrame(() => scrollToFirstVisible(ranges))
41
+
42
+ return () => {
43
+ if (frame !== undefined) cancelAnimationFrame(frame)
44
+ CSS.highlights.delete(HIGHLIGHT_NAME)
45
+ }
46
+ }, [root, page, query])
47
+ }
48
+
49
+ function collectMatchRanges(root: HTMLElement, pattern: RegExp) {
50
+ const ranges: Range[] = []
51
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
52
+ acceptNode: node =>
53
+ node.parentElement?.closest("pre, script, style, svg, textarea")
54
+ ? NodeFilter.FILTER_REJECT
55
+ : NodeFilter.FILTER_ACCEPT,
56
+ })
57
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
58
+ for (const [start, end] of findTextMatches(node.textContent ?? "", pattern)) {
59
+ const range = new Range()
60
+ range.setStart(node, start)
61
+ range.setEnd(node, end)
62
+ ranges.push(range)
63
+ }
64
+ }
65
+ return ranges
66
+ }
67
+
68
+ function scrollToFirstVisible(ranges: Range[]) {
69
+ const first = ranges.find(range =>
70
+ range.startContainer.parentElement?.checkVisibility(),
71
+ )
72
+ first?.startContainer.parentElement?.scrollIntoView({ block: "center" })
73
+ }
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from "vite-plus/test"
2
+ import { buildHighlightPattern, findTextMatches } from "./search-highlight.ts"
3
+
4
+ describe("buildHighlightPattern", () => {
5
+ it("should return undefined for an empty query", () => {
6
+ expect(buildHighlightPattern(" ")).toBeUndefined()
7
+ })
8
+
9
+ it("should drop single-character words", () => {
10
+ expect(buildHighlightPattern("a b")).toBeUndefined()
11
+ expect(buildHighlightPattern("a config")?.source).toBe("config")
12
+ })
13
+
14
+ it("should put longer words first and dedupe case-insensitively", () => {
15
+ expect(buildHighlightPattern("Side sidebar side")?.source).toBe("sidebar|side")
16
+ })
17
+
18
+ it("should escape regex syntax", () => {
19
+ expect(buildHighlightPattern("c++ (x)")?.source).toBe("c\\+\\+|\\(x\\)")
20
+ })
21
+ })
22
+
23
+ describe("findTextMatches", () => {
24
+ it("should find every word case-insensitively", () => {
25
+ const pattern = buildHighlightPattern("search config")
26
+ expect(pattern && findTextMatches("Search the Config, search!", pattern)).toEqual([
27
+ [0, 6],
28
+ [11, 17],
29
+ [19, 25],
30
+ ])
31
+ })
32
+
33
+ it("should prefer the longest word at a shared position", () => {
34
+ const pattern = buildHighlightPattern("side sidebar")
35
+ expect(pattern && findTextMatches("the sidebar", pattern)).toEqual([[4, 11]])
36
+ })
37
+
38
+ it("should return no matches when nothing matches", () => {
39
+ const pattern = buildHighlightPattern("orama")
40
+ expect(pattern && findTextMatches("nothing here", pattern)).toEqual([])
41
+ })
42
+ })
@@ -0,0 +1,28 @@
1
+ import { join } from "node:path"
2
+
3
+ /**
4
+ * Map a `livemark/<subpath>` import onto the target copy its `exports` entry names, so
5
+ * escapes importing Livemark modules reach other escapes, not the package originals.
6
+ */
7
+ export function resolveTargetExport(props: {
8
+ id: string
9
+ targetDir: string
10
+ exports: Record<string, string>
11
+ }) {
12
+ const queryIndex = props.id.indexOf("?")
13
+ const specifier = queryIndex === -1 ? props.id : props.id.slice(0, queryIndex)
14
+ const query = queryIndex === -1 ? "" : props.id.slice(queryIndex)
15
+ if (!specifier.startsWith("livemark/")) return undefined
16
+
17
+ const subpath = `./${specifier.slice("livemark/".length)}`
18
+ for (const [key, value] of Object.entries(props.exports)) {
19
+ const [prefix, suffix] = key.split("*")
20
+ if (suffix === undefined || !prefix || !value.startsWith("./source/")) continue
21
+ if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix)) continue
22
+ const name = subpath.slice(prefix.length, subpath.length - suffix.length)
23
+ return (
24
+ join(props.targetDir, value.slice("./source/".length).replace("*", name)) + query
25
+ )
26
+ }
27
+ return undefined
28
+ }
@@ -0,0 +1,37 @@
1
+ import { join } from "node:path"
2
+ import { describe, expect, it } from "vite-plus/test"
3
+ import packageJson from "../../package.json" with { type: "json" }
4
+ import { resolveTargetExport } from "./target-export.ts"
5
+
6
+ const targetDir = "/project/.livemark/.target"
7
+
8
+ function resolveId(id: string) {
9
+ return resolveTargetExport({ id, targetDir, exports: packageJson.exports })
10
+ }
11
+
12
+ describe("resolveTargetExport", () => {
13
+ it("maps each source subpath export into the target", () => {
14
+ expect(resolveId("livemark/components/Footer")).toBe(
15
+ join(targetDir, "components/Footer.tsx"),
16
+ )
17
+ expect(resolveId("livemark/elements/button")).toBe(
18
+ join(targetDir, "elements/button.tsx"),
19
+ )
20
+ expect(resolveId("livemark/hooks/in-view")).toBe(join(targetDir, "hooks/in-view.ts"))
21
+ expect(resolveId("livemark/styles/general")).toBe(
22
+ join(targetDir, "styles/general.css"),
23
+ )
24
+ })
25
+
26
+ it("keeps the query", () => {
27
+ expect(resolveId("livemark/styles/general?url")).toBe(
28
+ `${join(targetDir, "styles/general.css")}?url`,
29
+ )
30
+ })
31
+
32
+ it("leaves other specifiers alone", () => {
33
+ expect(resolveId("livemark")).toBeUndefined()
34
+ expect(resolveId("livemark/unknown/thing")).toBeUndefined()
35
+ expect(resolveId("react")).toBeUndefined()
36
+ })
37
+ })
@@ -1,10 +1,12 @@
1
1
  const STORAGE_KEY = "livemark-theme"
2
+ const DARK_QUERY = "(prefers-color-scheme: dark)"
2
3
 
3
4
  export type Theme = "light" | "dark"
5
+ export type ThemePref = Theme | "system"
4
6
 
5
- export function detectClientTheme(): Theme {
6
- const stored = localStorage.getItem(STORAGE_KEY) as Theme | null
7
- return stored || "light"
7
+ export function resolveTheme(pref: ThemePref, prefersDark: boolean): Theme {
8
+ if (pref === "system") return prefersDark ? "dark" : "light"
9
+ return pref
8
10
  }
9
11
 
10
12
  export function activateTheme(theme: Theme) {
@@ -21,7 +23,39 @@ export function getCurrentTheme(): Theme {
21
23
  : "light"
22
24
  }
23
25
 
24
- export function setTheme(theme: Theme) {
25
- activateTheme(theme)
26
- localStorage.setItem(STORAGE_KEY, theme)
26
+ // Mirrors `buildThemeScript`: anything but an explicit light or dark follows the OS.
27
+ export function getThemePref(): ThemePref {
28
+ let stored: string | null = null
29
+ try {
30
+ stored = localStorage.getItem(STORAGE_KEY)
31
+ } catch {}
32
+ return stored === "light" || stored === "dark" ? stored : "system"
33
+ }
34
+
35
+ export function setTheme(pref: ThemePref) {
36
+ applyThemePref(pref)
37
+ try {
38
+ localStorage.setItem(STORAGE_KEY, pref)
39
+ } catch {}
40
+ }
41
+
42
+ // Returns the OS listener's cleanup, so an effect StrictMode runs twice keeps one.
43
+ export function startTheme() {
44
+ const query = matchMedia(DARK_QUERY)
45
+ const handleChange = () => {
46
+ if (getThemePref() === "system") applyThemePref("system")
47
+ }
48
+ applyThemePref(getThemePref())
49
+ query.addEventListener("change", handleChange)
50
+ return () => query.removeEventListener("change", handleChange)
51
+ }
52
+
53
+ // The pre-paint twin of `startTheme`, inlined in <head> so a dark reader never sees
54
+ // the light palette flash before hydration.
55
+ export function buildThemeScript() {
56
+ return `try{var t=localStorage.getItem(${JSON.stringify(STORAGE_KEY)})}catch(e){}if(t==="dark"||(t!=="light"&&matchMedia(${JSON.stringify(DARK_QUERY)}).matches))document.documentElement.classList.add("dark");`
57
+ }
58
+
59
+ function applyThemePref(pref: ThemePref) {
60
+ activateTheme(resolveTheme(pref, matchMedia(DARK_QUERY).matches))
27
61
  }
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it, vi } from "vite-plus/test"
2
+ import { buildThemeScript, resolveTheme } from "./theme.ts"
3
+
4
+ function runThemeScript(stored: string | null, prefersDark: boolean) {
5
+ const classes = new Set<string>()
6
+ vi.stubGlobal("localStorage", { getItem: () => stored })
7
+ vi.stubGlobal("matchMedia", () => ({ matches: prefersDark }))
8
+ vi.stubGlobal("document", {
9
+ documentElement: { classList: { add: (name: string) => classes.add(name) } },
10
+ })
11
+ new Function(buildThemeScript())()
12
+ vi.unstubAllGlobals()
13
+ return classes.has("dark") ? "dark" : "light"
14
+ }
15
+
16
+ describe("buildThemeScript", () => {
17
+ it.each([
18
+ ["light", false],
19
+ ["light", true],
20
+ ["dark", false],
21
+ ["dark", true],
22
+ ["system", false],
23
+ ["system", true],
24
+ ] as const)("paints what resolveTheme(%s, prefersDark %s) does", (pref, dark) => {
25
+ expect(runThemeScript(pref, dark)).toBe(resolveTheme(pref, dark))
26
+ })
27
+
28
+ it.each([null, "", "auto"])("follows the OS for a stored %j", stored => {
29
+ expect(runThemeScript(stored, true)).toBe("dark")
30
+ expect(runThemeScript(stored, false)).toBe("light")
31
+ })
32
+ })
@@ -0,0 +1,18 @@
1
+ import { useSyncExternalStore } from "react"
2
+
3
+ /** False during SSR and the hydrating render, true from the first client render on. */
4
+ export function useHydrated() {
5
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
6
+ }
7
+
8
+ function subscribe() {
9
+ return () => {}
10
+ }
11
+
12
+ function getSnapshot() {
13
+ return true
14
+ }
15
+
16
+ function getServerSnapshot() {
17
+ return false
18
+ }
package/source/index.ts CHANGED
@@ -1 +1,2 @@
1
1
  export { defineConfig } from "./actions/config/define.ts"
2
+ export type { RootHead } from "./helpers/head.ts"
@@ -22,6 +22,15 @@ export type ViteOverride =
22
22
  * resolves it to a string before any runtime sees it. */
23
23
  export type Base = string | ((command: "build" | "serve") => string | undefined)
24
24
 
25
+ /** Extra site-wide `<head>` tags, appended after Livemark's defaults. A meta tag
26
+ * sharing a default's `name` or `property` replaces it. */
27
+ export type Head = z.infer<typeof Head>
28
+ export const Head = z.object({
29
+ meta: z.array(z.record(z.string(), z.string())).optional(),
30
+ links: z.array(z.record(z.string(), z.string())).optional(),
31
+ scripts: z.array(z.record(z.string(), z.string())).optional(),
32
+ })
33
+
25
34
  /**
26
35
  * User-provided configuration for Livemark.
27
36
  *
@@ -46,6 +55,7 @@ export const UserConfig = z.object({
46
55
  logo: z.string().optional(),
47
56
  title: z.string().default("Livemark"),
48
57
  description: z.string().default("Markdown site generator"),
58
+ banner: z.string().optional(),
49
59
  include: z.union([z.string(), z.array(z.string())]).default("**/*.md"),
50
60
  exclude: z.union([z.string(), z.array(z.string())]).optional(),
51
61
  codeThemeLight: CodeThemeLight.default("catppuccin-latte"),
@@ -53,6 +63,7 @@ export const UserConfig = z.object({
53
63
  links: z.array(Link).optional(),
54
64
  sections: z.array(Section).optional(),
55
65
  patches: z.array(Patch).optional(),
66
+ head: Head.optional(),
56
67
  // Stored as `unknown` in the schema (zod can't type a Vite UserConfig
57
68
  // or a function returning one meaningfully); the public TypeScript
58
69
  // surface above lifts it back to ViteOverride for editor support.
@@ -69,8 +80,10 @@ export const WebsiteConfig = UserConfig.pick({
69
80
  site: true,
70
81
  favicon: true,
71
82
  logo: true,
83
+ banner: true,
72
84
  links: true,
73
85
  sections: true,
86
+ head: true,
74
87
  }).extend({
75
88
  base: z.string().optional(),
76
89
  })
@@ -62,3 +62,6 @@ export const Section = z.preprocess(
62
62
  CustomSection,
63
63
  ]),
64
64
  )
65
+
66
+ /** A section that owns a URL prefix — every type except `custom`. */
67
+ export type RoutedSection = Exclude<Section, CustomSection>