livemark 0.24.1 → 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
@@ -24,7 +24,6 @@ const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
24
24
  const SIDEBAR_WIDTH = "16rem"
25
25
  const SIDEBAR_WIDTH_MOBILE = "18rem"
26
26
  const SIDEBAR_WIDTH_ICON = "3rem"
27
- const SIDEBAR_KEYBOARD_SHORTCUT = "b"
28
27
 
29
28
  type SidebarContextProps = {
30
29
  state: "expanded" | "collapsed"
@@ -90,19 +89,6 @@ function SidebarProvider({
90
89
  return isMobileNow() ? setOpenMobile(open => !open) : setOpen(open => !open)
91
90
  }, [setOpen, setOpenMobile])
92
91
 
93
- // Adds a keyboard shortcut to toggle the sidebar.
94
- React.useEffect(() => {
95
- const handleKeyDown = (event: KeyboardEvent) => {
96
- if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
97
- event.preventDefault()
98
- toggleSidebar()
99
- }
100
- }
101
-
102
- window.addEventListener("keydown", handleKeyDown)
103
- return () => window.removeEventListener("keydown", handleKeyDown)
104
- }, [toggleSidebar])
105
-
106
92
  // We add a state so that we can do data-state="expanded" or "collapsed".
107
93
  // This makes it easier to style the sidebar with Tailwind classes.
108
94
  const state = open ? "expanded" : "collapsed"
@@ -221,10 +207,10 @@ function Sidebar({
221
207
  <div
222
208
  data-slot="sidebar-container"
223
209
  className={cn(
224
- "fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
210
+ "sticky top-0 z-10 hidden h-svh w-(--sidebar-width) transition-[margin,translate,width] duration-200 ease-linear md:flex",
225
211
  side === "left"
226
- ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
227
- : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
212
+ ? "group-data-[collapsible=offcanvas]:-ml-(--sidebar-width)"
213
+ : "group-data-[collapsible=offcanvas]:-ml-(--sidebar-width) group-data-[collapsible=offcanvas]:translate-x-full",
228
214
  // Adjust the padding for floating and inset variants.
229
215
  variant === "floating" || variant === "inset"
230
216
  ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
@@ -258,6 +244,7 @@ function SidebarTrigger({
258
244
  data-slot="sidebar-trigger"
259
245
  variant="ghost"
260
246
  size="icon-sm"
247
+ aria-keyshortcuts="L"
261
248
  className={cn(className)}
262
249
  onClick={event => {
263
250
  onClick?.(event)
@@ -415,7 +402,7 @@ function SidebarGroupAction({
415
402
  props: mergeProps<"button">(
416
403
  {
417
404
  className: cn(
418
- "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 w-5 rounded-md p-0 focus-visible:ring-2 [&>svg]:size-4 flex aspect-square items-center justify-center outline-hidden transition-transform [&>svg]:shrink-0 after:absolute after:-inset-2 md:after:hidden group-data-[collapsible=icon]:hidden",
405
+ "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 w-5 rounded-control p-0 focus-visible:ring-2 [&>svg]:size-4 flex aspect-square items-center justify-center outline-hidden transition-transform [&>svg]:shrink-0 after:absolute after:-inset-2 md:after:hidden group-data-[collapsible=icon]:hidden",
419
406
  className,
420
407
  ),
421
408
  },
@@ -463,7 +450,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
463
450
  }
464
451
 
465
452
  const sidebarMenuButtonVariants = cva(
466
- "ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-primary/2 dark:data-active:bg-primary/5 data-active:border data-active:border-primary/20 data-active:text-primary data-active:[&_svg]:stroke-[2.5] data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground gap-2 rounded-md p-2 text-left text-sm transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! focus-visible:ring-2 data-active:font-medium peer/menu-button flex w-full items-center overflow-hidden outline-hidden group/menu-button disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&_svg]:size-4 [&_svg]:shrink-0",
453
+ "ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-primary/2 dark:data-active:bg-primary/5 data-active:border data-active:border-primary/20 data-active:text-primary data-active:[&_svg]:stroke-[2.5] data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground gap-2 rounded-row px-3 py-2 text-left text-sm transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! focus-visible:ring-2 data-active:font-medium peer/menu-button flex w-full items-center overflow-hidden outline-hidden group/menu-button disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&_svg]:size-4 [&_svg]:shrink-0",
467
454
  {
468
455
  variants: {
469
456
  variant: {
@@ -552,7 +539,7 @@ function SidebarMenuAction({
552
539
  props: mergeProps<"button">(
553
540
  {
554
541
  className: cn(
555
- "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 aspect-square w-5 rounded-md p-0 peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 focus-visible:ring-2 [&>svg]:size-4 flex items-center justify-center outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 md:after:hidden [&>svg]:shrink-0",
542
+ "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 aspect-square w-5 rounded-control p-0 peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 focus-visible:ring-2 [&>svg]:size-4 flex items-center justify-center outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 md:after:hidden [&>svg]:shrink-0",
556
543
  showOnHover &&
557
544
  "peer-data-active/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-open:opacity-100 md:opacity-0",
558
545
  className,
@@ -658,7 +645,7 @@ function SidebarMenuSubButton({
658
645
  props: mergeProps<"a">(
659
646
  {
660
647
  className: cn(
661
- "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground data-active:bg-primary/2 dark:data-active:bg-primary/5 data-active:border data-active:border-primary/20 data-active:text-primary data-active:font-medium h-7 gap-2 rounded-md px-2 focus-visible:ring-2 data-[size=md]:text-sm data-[size=sm]:text-xs [&>svg]:size-4 flex min-w-0 -translate-x-px items-center overflow-hidden outline-hidden group-data-[collapsible=icon]:hidden disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:shrink-0",
648
+ "text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground data-active:bg-primary/2 dark:data-active:bg-primary/5 data-active:border data-active:border-primary/20 data-active:text-primary data-active:font-medium h-7 gap-2 rounded-row px-3 focus-visible:ring-2 data-[size=md]:text-sm data-[size=sm]:text-xs [&>svg]:size-4 flex min-w-0 -translate-x-px items-center overflow-hidden outline-hidden group-data-[collapsible=icon]:hidden disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:shrink-0",
662
649
  className,
663
650
  ),
664
651
  },
@@ -21,7 +21,7 @@ function Tabs({
21
21
  }
22
22
 
23
23
  const tabsListVariants = cva(
24
- "rounded-lg p-[3px] group-data-horizontal/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
24
+ "rounded-control p-[3px] group-data-horizontal/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
25
25
  {
26
26
  variants: {
27
27
  variant: {
@@ -55,7 +55,7 @@ function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
55
55
  <TabsPrimitive.Tab
56
56
  data-slot="tabs-trigger"
57
57
  className={cn(
58
- "gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg:not([class*='size-'])]:size-4 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
58
+ "gap-1.5 rounded-control border border-transparent px-2 py-1 text-sm font-medium group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg:not([class*='size-'])]:size-4 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
59
59
  "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
60
60
  "data-active:bg-background dark:data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 data-active:text-foreground",
61
61
  "after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
@@ -5,7 +5,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
5
5
  <textarea
6
6
  data-slot="textarea"
7
7
  className={cn(
8
- "border-input dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border bg-transparent px-2.5 py-2 text-base shadow-xs transition-[color,box-shadow] focus-visible:ring-[3px] aria-invalid:ring-[3px] md:text-sm placeholder:text-muted-foreground flex field-sizing-content min-h-16 w-full outline-none disabled:cursor-not-allowed disabled:opacity-50",
8
+ "border-input dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-textarea border bg-transparent px-2.5 py-2 text-base shadow-xs transition-[color,box-shadow] focus-visible:ring-[3px] aria-invalid:ring-[3px] md:text-sm placeholder:text-muted-foreground flex field-sizing-content min-h-16 w-full outline-none disabled:cursor-not-allowed disabled:opacity-50",
9
9
  className,
10
10
  )}
11
11
  {...props}
@@ -40,7 +40,7 @@ function ToggleGroup({
40
40
  data-orientation={orientation}
41
41
  style={{ "--gap": spacing } as React.CSSProperties}
42
42
  className={cn(
43
- "group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=0]:data-[variant=outline]:shadow-xs data-vertical:flex-col data-vertical:items-stretch",
43
+ "group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-control data-[spacing=0]:data-[variant=outline]:shadow-xs data-vertical:flex-col data-vertical:items-stretch",
44
44
  className,
45
45
  )}
46
46
  {...props}
@@ -68,7 +68,7 @@ function ToggleGroupItem({
68
68
  data-size={context.size || size}
69
69
  data-spacing={context.spacing}
70
70
  className={cn(
71
- "shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 group-data-[spacing=0]/toggle-group:shadow-none focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-md group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-md group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-md group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-md data-[state=on]:bg-muted group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t",
71
+ "shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 group-data-[spacing=0]/toggle-group:shadow-none focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-control group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-control group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-control group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-control data-[state=on]:bg-muted group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t",
72
72
  toggleVariants({
73
73
  variant: context.variant || variant,
74
74
  size: context.size || size,
@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"
4
4
  import { cn } from "cn"
5
5
 
6
6
  const toggleVariants = cva(
7
- "group/toggle inline-flex items-center justify-center gap-1 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
7
+ "group/toggle inline-flex items-center justify-center gap-1 rounded-control text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
8
8
  {
9
9
  variants: {
10
10
  variant: {
package/source/env.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  declare module "livemark:virtual" {
2
2
  export const config: import("./models/config.ts").WebsiteConfig
3
+ export const bannerHtml: string | undefined
3
4
  }
4
5
 
5
6
  declare module "content-collections" {
package/source/head.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { RootHead } from "./helpers/head.ts"
2
+
3
+ /** Final pass over the site-wide `<head>`; shadow with `.livemark/head.ts` to add,
4
+ * drop or replace tags. */
5
+ export function customizeHead(head: RootHead) {
6
+ return head
7
+ }
@@ -0,0 +1,192 @@
1
+ import { useState, useRef, useCallback, useMemo } from "react"
2
+ import { useNavigate } from "@tanstack/react-router"
3
+ import { create, insertMultiple, search } from "@orama/orama"
4
+ import { sortedArticles } from "../content/article.ts"
5
+ import { resolveArticleIcon } from "./article-icon.ts"
6
+ import { buildHighlightPattern, findTextMatches } from "./search-highlight.ts"
7
+ import { CommandGroup, CommandItem } from "../elements/command.tsx"
8
+
9
+ interface SearchResult {
10
+ title: string
11
+ path: string
12
+ icon?: string
13
+ snippet: string
14
+ group: string
15
+ }
16
+
17
+ export function useArticleSearch() {
18
+ const [results, setResults] = useState<SearchResult[]>([])
19
+ const [query, setQuery] = useState("")
20
+ const dbRef = useRef<Awaited<ReturnType<typeof create>> | null>(null)
21
+ const navigate = useNavigate()
22
+
23
+ const ensureIndex = useCallback(async () => {
24
+ if (dbRef.current) return dbRef.current
25
+ const db = await create({
26
+ schema: {
27
+ title: "string",
28
+ description: "string",
29
+ searchText: "string",
30
+ },
31
+ })
32
+ await insertMultiple(
33
+ db,
34
+ sortedArticles.map(a => ({
35
+ title: a.title,
36
+ description: a.description ?? "",
37
+ searchText: a.searchText,
38
+ })),
39
+ )
40
+ dbRef.current = db
41
+ return db
42
+ }, [])
43
+
44
+ const searchArticles = useCallback(
45
+ async (term: string) => {
46
+ setQuery(term)
47
+ if (!term.trim()) {
48
+ setResults([])
49
+ return
50
+ }
51
+ const db = await ensureIndex()
52
+ const hits = await search(db, {
53
+ term,
54
+ properties: ["title", "description", "searchText"],
55
+ tolerance: 1,
56
+ boost: { title: 3, description: 2 },
57
+ })
58
+ setResults(
59
+ hits.hits.map(hit => {
60
+ const article = sortedArticles.find(a => a.title === hit.document.title)
61
+ if (!article)
62
+ return toResult(
63
+ {
64
+ ...sortedArticles[0],
65
+ title: hit.document.title,
66
+ path: "",
67
+ searchText: hit.document.description,
68
+ description: hit.document.description,
69
+ icon: "",
70
+ },
71
+ term,
72
+ )
73
+ return toResult(article, term)
74
+ }),
75
+ )
76
+ },
77
+ [ensureIndex],
78
+ )
79
+
80
+ const selectArticle = useCallback(
81
+ (path: string) => {
82
+ setQuery("")
83
+ setResults([])
84
+ navigate({
85
+ to: "/$",
86
+ params: { _splat: path.replace(/^\/|\/$/g, "") },
87
+ search: { highlight: query.trim() || undefined },
88
+ })
89
+ },
90
+ [navigate, query],
91
+ )
92
+
93
+ const grouped = useMemo(() => {
94
+ const groups = new Map<string, SearchResult[]>()
95
+ for (const r of results) {
96
+ const list = groups.get(r.group) ?? []
97
+ list.push(r)
98
+ groups.set(r.group, list)
99
+ }
100
+ return groups
101
+ }, [results])
102
+
103
+ return { query, grouped, searchArticles, selectArticle }
104
+ }
105
+
106
+ export function SearchResultGroups(props: {
107
+ query: string
108
+ grouped: Map<string, SearchResult[]>
109
+ onSelect: (path: string) => void
110
+ }) {
111
+ return [...props.grouped.entries()].map(([group, items]) => (
112
+ <CommandGroup key={group} heading={group}>
113
+ {items.map(result => {
114
+ const Icon = resolveArticleIcon(result.icon)
115
+ return (
116
+ <CommandItem
117
+ key={result.path}
118
+ value={result.path}
119
+ onSelect={() => props.onSelect(result.path)}
120
+ className="py-2.5"
121
+ >
122
+ {Icon && <Icon className="size-5 shrink-0 mt-0.5 self-start opacity-60" />}
123
+ <div className="flex flex-col gap-0.5 min-w-0">
124
+ <span className="text-base font-medium">{result.title}</span>
125
+ {result.snippet && (
126
+ <span className="text-sm text-muted-foreground line-clamp-1">
127
+ <HighlightedText text={result.snippet} term={props.query} />
128
+ </span>
129
+ )}
130
+ </div>
131
+ </CommandItem>
132
+ )
133
+ })}
134
+ </CommandGroup>
135
+ ))
136
+ }
137
+
138
+ function toResult(article: (typeof sortedArticles)[number], term: string): SearchResult {
139
+ return {
140
+ title: article.title,
141
+ path: article.path,
142
+ icon: article.icon,
143
+ snippet: term
144
+ ? extractSnippet(article.searchText, term)
145
+ : (article.description ?? ""),
146
+ group: groupFor(article.path),
147
+ }
148
+ }
149
+
150
+ function groupFor(pathname: string) {
151
+ const segments = pathname.replace(/^\/|\/$/g, "").split("/")
152
+ if (segments.length <= 2) return "Pages"
153
+ return segments
154
+ .slice(1, -1)
155
+ .map(s =>
156
+ s
157
+ .split("-")
158
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1))
159
+ .join(" "),
160
+ )
161
+ .join(" › ")
162
+ }
163
+
164
+ function extractSnippet(text: string, term: string) {
165
+ const pattern = buildHighlightPattern(term)
166
+ const [match] = pattern ? findTextMatches(text, pattern) : []
167
+ if (!match) return text.slice(0, 120)
168
+ const [matchStart, matchEnd] = match
169
+ const start = Math.max(0, matchStart - 40)
170
+ const end = Math.min(text.length, matchEnd + 80)
171
+ const prefix = start > 0 ? "…" : ""
172
+ const suffix = end < text.length ? "…" : ""
173
+ return `${prefix}${text.slice(start, end)}${suffix}`
174
+ }
175
+
176
+ function HighlightedText(props: { text: string; term: string }) {
177
+ const pattern = buildHighlightPattern(props.term)
178
+ if (!pattern) return <>{props.text}</>
179
+ const parts: React.ReactNode[] = []
180
+ let cursor = 0
181
+ for (const [start, end] of findTextMatches(props.text, pattern)) {
182
+ parts.push(props.text.slice(cursor, start))
183
+ parts.push(
184
+ <span key={start} className="text-foreground font-medium">
185
+ {props.text.slice(start, end)}
186
+ </span>,
187
+ )
188
+ cursor = end
189
+ }
190
+ parts.push(props.text.slice(cursor))
191
+ return <>{parts}</>
192
+ }
@@ -0,0 +1,20 @@
1
+ import { existsSync, readdirSync, realpathSync } from "node:fs"
2
+ import { basename, dirname, join } from "node:path"
3
+
4
+ /**
5
+ * Locate the directory holding livemark's own dependencies, which the project's target
6
+ * links to as its `node_modules`.
7
+ */
8
+ export function resolveDependencyDir(sourceDir: string) {
9
+ const packageDir = realpathSync(dirname(sourceDir))
10
+ const parent = dirname(packageDir)
11
+ const nested = join(packageDir, "node_modules")
12
+ // pnpm gives an installed package a `.bin`-only `node_modules`; its deps sit beside it
13
+ // in the store. Only real packages there (a checkout, npm's conflict copies) count.
14
+ if (hasPackages(nested) || basename(parent) !== "node_modules") return nested
15
+ return parent
16
+ }
17
+
18
+ function hasPackages(dir: string) {
19
+ return existsSync(dir) && readdirSync(dir).some(name => !name.startsWith("."))
20
+ }
@@ -0,0 +1,39 @@
1
+ import { realpathSync } from "node:fs"
2
+ import { mkdir } from "node:fs/promises"
3
+ import { join } from "node:path"
4
+ import { temporaryDirectoryTask } from "tempy"
5
+ import { describe, expect, it } from "vite-plus/test"
6
+ import { resolveDependencyDir } from "./dependency-dir.ts"
7
+
8
+ describe("resolveDependencyDir", () => {
9
+ it("uses the store dir beside an isolated install", () =>
10
+ temporaryDirectoryTask(async root => {
11
+ const store = join(root, ".pnpm/livemark@1.0.0/node_modules")
12
+ await mkdir(join(store, "livemark/source"), { recursive: true })
13
+ await mkdir(join(store, "livemark/node_modules/.bin"), { recursive: true })
14
+
15
+ expect(resolveDependencyDir(join(store, "livemark/source"))).toBe(
16
+ realpathSync(store),
17
+ )
18
+ }))
19
+
20
+ it("uses the package's own node_modules when it has one", () =>
21
+ temporaryDirectoryTask(async root => {
22
+ const packageDir = join(root, "node_modules/livemark")
23
+ await mkdir(join(packageDir, "source"), { recursive: true })
24
+ await mkdir(join(packageDir, "node_modules/zod"), { recursive: true })
25
+
26
+ expect(resolveDependencyDir(join(packageDir, "source"))).toBe(
27
+ join(realpathSync(packageDir), "node_modules"),
28
+ )
29
+ }))
30
+
31
+ it("uses the checkout's node_modules outside an install", () =>
32
+ temporaryDirectoryTask(async root => {
33
+ await mkdir(join(root, "livemark/source"), { recursive: true })
34
+
35
+ expect(resolveDependencyDir(join(root, "livemark/source"))).toBe(
36
+ join(realpathSync(root), "livemark/node_modules"),
37
+ )
38
+ }))
39
+ })
@@ -0,0 +1,94 @@
1
+ import type * as React from "react"
2
+ import type { WebsiteConfig } from "../models/config.ts"
3
+ import { faviconType } from "./favicon.ts"
4
+ import { prefixUrl } from "./prefix-url.ts"
5
+ import { buildThemeScript } from "./theme.ts"
6
+
7
+ /** Site-wide `<head>` tags the root route renders on every page. */
8
+ export interface RootHead {
9
+ meta: Array<React.JSX.IntrinsicElements["meta"]>
10
+ links: Array<React.JSX.IntrinsicElements["link"]>
11
+ scripts: Array<React.JSX.IntrinsicElements["script"]>
12
+ }
13
+
14
+ export function buildRootHead(props: {
15
+ config: WebsiteConfig
16
+ defaultFavicon: string
17
+ stylesheets: string[]
18
+ }) {
19
+ const config = props.config
20
+ const favicon = prefixUrl(
21
+ config.favicon ?? config.logo ?? props.defaultFavicon,
22
+ config.base,
23
+ )
24
+ const feedLinks = config.site
25
+ ? (config.sections ?? [])
26
+ .filter(s => s.type === "blog")
27
+ .map(s => ({
28
+ rel: "alternate",
29
+ type: "application/rss+xml",
30
+ title: `${s.siteTitle ?? s.title} RSS Feed`,
31
+ href: prefixUrl(`${s.prefix}rss.xml`, config.base),
32
+ }))
33
+ : []
34
+ return {
35
+ meta: [
36
+ { charSet: "utf-8" },
37
+ { title: config.title },
38
+ { name: "description", content: config.description },
39
+ { property: "og:title", content: config.title },
40
+ { property: "og:description", content: config.description },
41
+ { property: "og:site_name", content: config.title },
42
+ { property: "og:type", content: "website" },
43
+ { name: "viewport", content: "width=device-width, initial-scale=1" },
44
+ ...(config.head?.meta ?? []),
45
+ ],
46
+ links: [
47
+ { rel: "icon", href: favicon, type: faviconType(favicon) },
48
+ { rel: "shortcut icon", href: favicon, type: faviconType(favicon) },
49
+ ...props.stylesheets.map(href => ({ rel: "stylesheet", href })),
50
+ ...feedLinks,
51
+ ...(config.head?.links ?? []),
52
+ ],
53
+ scripts: [{ children: buildThemeScript() }, ...(config.head?.scripts ?? [])],
54
+ }
55
+ }
56
+
57
+ export function buildPageHead(props: {
58
+ config: WebsiteConfig
59
+ path: string
60
+ title?: string
61
+ description?: string
62
+ image?: string
63
+ isArticle?: boolean
64
+ }) {
65
+ const site = props.config.site?.replace(/\/+$/, "")
66
+ const url = site ? buildAbsoluteUrl(site, props.config.base, props.path) : undefined
67
+ const image =
68
+ site && props.image
69
+ ? buildAbsoluteUrl(site, props.config.base, props.image)
70
+ : undefined
71
+ return {
72
+ meta: [
73
+ ...(props.title
74
+ ? [{ title: props.title }, { property: "og:title", content: props.title }]
75
+ : []),
76
+ ...(props.description
77
+ ? [
78
+ { name: "description", content: props.description },
79
+ { property: "og:description", content: props.description },
80
+ ]
81
+ : []),
82
+ ...(props.isArticle ? [{ property: "og:type", content: "article" }] : []),
83
+ ...(url ? [{ property: "og:url", content: url }] : []),
84
+ ...(image ? [{ property: "og:image", content: image }] : []),
85
+ ],
86
+ links: url ? [{ rel: "canonical", href: url }] : [],
87
+ }
88
+ }
89
+
90
+ function buildAbsoluteUrl(site: string, base: string | undefined, url: string) {
91
+ const prefixed = prefixUrl(url, base)
92
+ if (!prefixed.startsWith("/") || prefixed.startsWith("//")) return prefixed
93
+ return `${site}${prefixed}`
94
+ }
@@ -0,0 +1,116 @@
1
+ import { describe, expect, it } from "vite-plus/test"
2
+ import type { WebsiteConfig } from "../models/config.ts"
3
+ import { buildPageHead, buildRootHead } from "./head.ts"
4
+
5
+ const config: WebsiteConfig = { title: "Docs", description: "Blurb" }
6
+
7
+ describe("buildRootHead", () => {
8
+ it("builds the default site tags", () => {
9
+ const head = buildRootHead({
10
+ config,
11
+ defaultFavicon: "/logo.svg",
12
+ stylesheets: ["/a.css"],
13
+ })
14
+ expect(head.meta).toContainEqual({ property: "og:title", content: "Docs" })
15
+ expect(head.links).toContainEqual({
16
+ rel: "icon",
17
+ href: "/logo.svg",
18
+ type: "image/svg+xml",
19
+ })
20
+ expect(head.links).toContainEqual({ rel: "stylesheet", href: "/a.css" })
21
+ expect(head.scripts).toHaveLength(1)
22
+ })
23
+
24
+ it("appends config head tags after the defaults", () => {
25
+ const head = buildRootHead({
26
+ config: {
27
+ ...config,
28
+ head: {
29
+ meta: [{ property: "og:type", content: "book" }],
30
+ links: [{ rel: "me", href: "https://x.com/me" }],
31
+ scripts: [{ src: "https://cdn.example.com/a.js" }],
32
+ },
33
+ },
34
+ defaultFavicon: "/logo.svg",
35
+ stylesheets: [],
36
+ })
37
+ expect(head.meta.at(-1)).toEqual({ property: "og:type", content: "book" })
38
+ expect(head.links.at(-1)).toEqual({ rel: "me", href: "https://x.com/me" })
39
+ expect(head.scripts.at(-1)).toEqual({ src: "https://cdn.example.com/a.js" })
40
+ })
41
+
42
+ it("links blog feeds only when site is set", () => {
43
+ const sections: WebsiteConfig["sections"] = [
44
+ { type: "blog", title: "Blog", prefix: "/blog/", position: "header" },
45
+ ]
46
+ const withoutSite = buildRootHead({
47
+ config: { ...config, sections },
48
+ defaultFavicon: "/logo.svg",
49
+ stylesheets: [],
50
+ })
51
+ const withSite = buildRootHead({
52
+ config: { ...config, sections, site: "https://example.com", base: "/repo" },
53
+ defaultFavicon: "/logo.svg",
54
+ stylesheets: [],
55
+ })
56
+ expect(withoutSite.links.some(l => l.rel === "alternate")).toBe(false)
57
+ expect(withSite.links).toContainEqual({
58
+ rel: "alternate",
59
+ type: "application/rss+xml",
60
+ title: "Blog RSS Feed",
61
+ href: "/repo/blog/rss.xml",
62
+ })
63
+ })
64
+ })
65
+
66
+ describe("buildPageHead", () => {
67
+ it("emits title, description and article type", () => {
68
+ const head = buildPageHead({
69
+ config,
70
+ path: "/guide/",
71
+ title: "Guide",
72
+ description: "How to",
73
+ isArticle: true,
74
+ })
75
+ expect(head.meta).toEqual([
76
+ { title: "Guide" },
77
+ { property: "og:title", content: "Guide" },
78
+ { name: "description", content: "How to" },
79
+ { property: "og:description", content: "How to" },
80
+ { property: "og:type", content: "article" },
81
+ ])
82
+ expect(head.links).toEqual([])
83
+ })
84
+
85
+ it("adds absolute url, canonical and image when site is set", () => {
86
+ const head = buildPageHead({
87
+ config: { ...config, site: "https://example.com/", base: "/repo" },
88
+ path: "/guide/",
89
+ image: "/repo/assets/cover.png",
90
+ })
91
+ expect(head.meta).toEqual([
92
+ { property: "og:url", content: "https://example.com/repo/guide/" },
93
+ { property: "og:image", content: "https://example.com/repo/assets/cover.png" },
94
+ ])
95
+ expect(head.links).toEqual([
96
+ { rel: "canonical", href: "https://example.com/repo/guide/" },
97
+ ])
98
+ })
99
+
100
+ it("keeps an already absolute image", () => {
101
+ const head = buildPageHead({
102
+ config: { ...config, site: "https://example.com" },
103
+ path: "/",
104
+ image: "https://cdn.example.com/cover.png",
105
+ })
106
+ expect(head.meta).toContainEqual({
107
+ property: "og:image",
108
+ content: "https://cdn.example.com/cover.png",
109
+ })
110
+ })
111
+
112
+ it("skips og:image without site", () => {
113
+ const head = buildPageHead({ config, path: "/", image: "/cover.png" })
114
+ expect(head.meta).toEqual([])
115
+ })
116
+ })