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
@@ -1,86 +1,66 @@
1
- import { createHash } from "node:crypto"
2
- import { cpSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs"
3
- import { dirname, join } from "node:path"
1
+ import { cpSync, existsSync, mkdirSync, readlinkSync, rmSync, symlinkSync } from "node:fs"
2
+ import { basename, dirname, join, relative, resolve } from "node:path"
3
+ import { resolveDependencyDir } from "../../helpers/dependency-dir.ts"
4
4
  import { ESCAPABLE_FILES, ESCAPABLE_FOLDERS } from "../../settings.ts"
5
5
 
6
- // This file ships at <livemark-pkg>/source/actions/target/build.ts AND
7
- // gets copied into <livemark-pkg>/targets/<hash>/actions/target/build.ts
8
- // as part of every per-consumer mount. content-collections loads the
9
- // COPIED version, so `import.meta.dirname` can land inside `targets/`.
10
- // Walk up to the first directory holding livemark's `package.json` so
11
- // SOURCE_DIR / TARGETS_ROOT always anchor at the package, never at a
12
- // per-consumer dir.
13
- const LIVEMARK_PKG = findLivemarkPkg(import.meta.dirname)
14
- const SOURCE_DIR = join(LIVEMARK_PKG, "source")
15
- const TARGETS_ROOT = join(LIVEMARK_PKG, "targets")
16
-
17
- function findLivemarkPkg(start: string) {
18
- let cur = start
19
- while (cur !== dirname(cur)) {
20
- if (existsSync(join(cur, "package.json"))) return cur
21
- cur = dirname(cur)
22
- }
23
- throw new Error(`Could not locate livemark package root from ${start}`)
24
- }
25
-
26
6
  /**
27
- * Synthesize a fully self-contained per-consumer Vite mount at
28
- * `<livemark-pkg>/targets/<hash>/`.
29
- *
30
- * The mount is built by copying livemark's `source/` wholesale, then
31
- * overlaying any matching files from the consumer's `.livemark/`. Every
32
- * file lives at its target path as a real file — no symlinks — so
33
- * Vite's relative-import and bare-specifier resolution all anchor
34
- * inside the target dir, where node_modules walks reach livemark's pnpm
35
- * subtree natively.
36
- *
37
- * Behavior:
38
- * - First run (target dir missing): full source copy + overlay.
39
- * - Subsequent runs: skip the source copy. Source is immutable in
40
- * installed mode; in livemark's own dev the watcher keeps the target
41
- * in sync. Only the `.livemark/` overlay is re-applied each time —
42
- * cheap, and may have changed between runs.
43
- * - With `--clear` in argv: wipe the entire target dir (including
44
- * downstream caches like `.vite/`, `.content-collections/`,
45
- * `cache/`, `routeTree.gen.ts`) and rebuild from scratch. Use this
46
- * to recover from a stale or corrupted cache.
47
- *
48
- * Returns the absolute path to the target dir.
7
+ * Synthesize the project's Vite mount at `.livemark/.target/`: livemark's `source/` with
8
+ * the project's `.livemark/` escapes copied over it, plus a `node_modules` link to
9
+ * livemark's dependencies. Escapable folders are rebuilt on every run, so a removed escape
10
+ * never keeps rendering. Returns the target dir.
49
11
  */
50
- export function buildTarget(configPath: string) {
51
- const targetDir = targetDirFor(configPath)
52
- const overridesRoot = join(dirname(configPath), ".livemark")
53
- const clear = process.argv.includes("--clear")
54
-
55
- if (clear) rmSync(targetDir, { recursive: true, force: true })
56
-
57
- if (!existsSync(targetDir)) {
58
- mkdirSync(targetDir, { recursive: true })
59
- cpSync(SOURCE_DIR, targetDir, { recursive: true })
60
- }
12
+ export function buildTarget(props: { configPath: string; sourceDir: string }) {
13
+ const targetDir = targetDirFor(props.configPath)
14
+ const overridesRoot = dirname(targetDir)
15
+ mkdirSync(targetDir, { recursive: true })
61
16
 
17
+ cpSync(props.sourceDir, targetDir, {
18
+ recursive: true,
19
+ filter: src => isPackageFile(src) && !isEscapableFolder(src, props.sourceDir),
20
+ })
62
21
  for (const folder of ESCAPABLE_FOLDERS) {
63
- const overlay = join(overridesRoot, folder)
64
- if (!existsSync(overlay)) continue
65
22
  const dest = join(targetDir, folder)
66
- if (!existsSync(dest)) mkdirSync(dest, { recursive: true })
67
- for (const f of readdirSync(overlay)) {
68
- cpSync(join(overlay, f), join(dest, f), { recursive: true, force: true })
69
- }
23
+ rmSync(dest, { recursive: true, force: true })
24
+ const source = join(props.sourceDir, folder)
25
+ if (existsSync(source))
26
+ cpSync(source, dest, { recursive: true, filter: isPackageFile })
27
+ const overlay = join(overridesRoot, folder)
28
+ if (existsSync(overlay)) cpSync(overlay, dest, { recursive: true })
70
29
  }
71
30
  for (const file of ESCAPABLE_FILES) {
72
31
  const overlay = join(overridesRoot, file)
73
- if (!existsSync(overlay)) continue
74
- cpSync(overlay, join(targetDir, file), { force: true })
32
+ if (existsSync(overlay)) cpSync(overlay, join(targetDir, file))
75
33
  }
76
34
 
35
+ linkDependencies(targetDir, resolveDependencyDir(props.sourceDir))
77
36
  return targetDir
78
37
  }
79
38
 
80
- /** Compute the target dir for a given consumer config path without
81
- * rebuilding it. Used by code that needs to write into the target
82
- * (e.g. cache writers) outside of `buildTarget`'s setup phase. */
39
+ /** Locate the project's target dir without building it. */
83
40
  export function targetDirFor(configPath: string) {
84
- const hash = createHash("sha256").update(configPath).digest("hex").slice(0, 16)
85
- return join(TARGETS_ROOT, hash)
41
+ return join(dirname(configPath), ".livemark", ".target")
42
+ }
43
+
44
+ function isPackageFile(src: string) {
45
+ return !/\.unit\.tsx?$/.test(src) && basename(src) !== "fixtures"
46
+ }
47
+
48
+ function isEscapableFolder(src: string, sourceDir: string) {
49
+ return ESCAPABLE_FOLDERS.includes(relative(sourceDir, src))
50
+ }
51
+
52
+ function linkDependencies(targetDir: string, dependencyDir: string) {
53
+ const link = join(targetDir, "node_modules")
54
+ if (readLink(link) === resolve(dependencyDir)) return
55
+ // Not recursive: a stale link must go without touching the store it points into.
56
+ rmSync(link, { force: true })
57
+ symlinkSync(dependencyDir, link, "junction")
58
+ }
59
+
60
+ function readLink(path: string) {
61
+ try {
62
+ return resolve(dirname(path), readlinkSync(path))
63
+ } catch {
64
+ return undefined
65
+ }
86
66
  }
@@ -0,0 +1,95 @@
1
+ import { existsSync, readFileSync, readlinkSync, realpathSync } from "node:fs"
2
+ import { mkdir, rm, symlink, writeFile } from "node:fs/promises"
3
+ import { dirname, join } from "node:path"
4
+ import { temporaryDirectoryTask } from "tempy"
5
+ import { describe, expect, it } from "vite-plus/test"
6
+ import { buildTarget, targetDirFor } from "./build.ts"
7
+
8
+ describe("buildTarget", () => {
9
+ it("copies the source, overlays escapes and skips unit tests", () =>
10
+ temporaryDirectoryTask(async root => {
11
+ const { configPath, sourceDir } = await createFixture(root)
12
+ await put(join(root, "project/.livemark/components/Footer.tsx"), "escaped")
13
+ await put(join(root, "project/.livemark/head.ts"), "escaped head")
14
+
15
+ const targetDir = buildTarget({ configPath, sourceDir })
16
+
17
+ expect(targetDir).toBe(join(root, "project/.livemark/.target"))
18
+ expect(read(targetDir, "components/Footer.tsx")).toBe("escaped")
19
+ expect(read(targetDir, "components/Header.tsx")).toBe("default")
20
+ expect(read(targetDir, "head.ts")).toBe("escaped head")
21
+ expect(read(targetDir, "helpers/util.ts")).toBe("util")
22
+ expect(existsSync(join(targetDir, "helpers/util.unit.ts"))).toBe(false)
23
+ }))
24
+
25
+ it("drops escapes removed since the last run", () =>
26
+ temporaryDirectoryTask(async root => {
27
+ const { configPath, sourceDir } = await createFixture(root)
28
+ const overrides = join(root, "project/.livemark")
29
+ await put(join(overrides, "components/Footer.tsx"), "escaped")
30
+ await put(join(overrides, "routes/about.tsx"), "about")
31
+ await put(join(overrides, "head.ts"), "escaped head")
32
+ buildTarget({ configPath, sourceDir })
33
+
34
+ await rm(join(overrides, "components"), { recursive: true })
35
+ await rm(join(overrides, "routes"), { recursive: true })
36
+ await rm(join(overrides, "head.ts"))
37
+ const targetDir = buildTarget({ configPath, sourceDir })
38
+
39
+ expect(read(targetDir, "components/Footer.tsx")).toBe("default")
40
+ expect(existsSync(join(targetDir, "routes/about.tsx"))).toBe(false)
41
+ expect(read(targetDir, "head.ts")).toBe("head")
42
+ }))
43
+
44
+ it("keeps generated entries between runs", () =>
45
+ temporaryDirectoryTask(async root => {
46
+ const { configPath, sourceDir } = await createFixture(root)
47
+ const targetDir = buildTarget({ configPath, sourceDir })
48
+ await put(join(targetDir, "cache/changelog/entry.md"), "cached")
49
+
50
+ buildTarget({ configPath, sourceDir })
51
+
52
+ expect(read(targetDir, "cache/changelog/entry.md")).toBe("cached")
53
+ }))
54
+
55
+ it("links node_modules to livemark's dependencies and repairs a stale link", () =>
56
+ temporaryDirectoryTask(async root => {
57
+ const { configPath, sourceDir } = await createFixture(root)
58
+ const link = join(targetDirFor(configPath), "node_modules")
59
+ const dependencyDir = realpathSync(join(root, "livemark/node_modules"))
60
+
61
+ buildTarget({ configPath, sourceDir })
62
+ expect(readlinkSync(link)).toBe(dependencyDir)
63
+
64
+ await rm(link)
65
+ await mkdir(join(root, "elsewhere"))
66
+ await symlink(join(root, "elsewhere"), link, "junction")
67
+ buildTarget({ configPath, sourceDir })
68
+
69
+ expect(readlinkSync(link)).toBe(dependencyDir)
70
+ expect(existsSync(join(root, "elsewhere"))).toBe(true)
71
+ }))
72
+ })
73
+
74
+ async function createFixture(root: string) {
75
+ const sourceDir = join(root, "livemark/source")
76
+ await put(join(sourceDir, "components/Footer.tsx"), "default")
77
+ await put(join(sourceDir, "components/Header.tsx"), "default")
78
+ await put(join(sourceDir, "routes/index.tsx"), "index")
79
+ await put(join(sourceDir, "helpers/util.ts"), "util")
80
+ await put(join(sourceDir, "helpers/util.unit.ts"), "test")
81
+ await put(join(sourceDir, "head.ts"), "head")
82
+ await mkdir(join(root, "livemark/node_modules"))
83
+ const configPath = join(root, "project/livemark.config.ts")
84
+ await put(configPath, "export default {}")
85
+ return { configPath, sourceDir }
86
+ }
87
+
88
+ async function put(path: string, content: string) {
89
+ await mkdir(dirname(path), { recursive: true })
90
+ await writeFile(path, content)
91
+ }
92
+
93
+ function read(dir: string, path: string) {
94
+ return readFileSync(join(dir, path), "utf-8")
95
+ }
@@ -0,0 +1,14 @@
1
+ import { rmSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import { targetDirFor } from "./build.ts"
4
+
5
+ /**
6
+ * Wipe the project's target dir so the next run rebuilds it from scratch.
7
+ */
8
+ export function clearTarget(configPath: string) {
9
+ const targetDir = targetDirFor(configPath)
10
+ // Unlink the dependency link first so the recursive remove cannot reach the store.
11
+ rmSync(join(targetDir, "node_modules"), { force: true })
12
+ rmSync(targetDir, { recursive: true, force: true })
13
+ return targetDir
14
+ }
@@ -0,0 +1,25 @@
1
+ import { existsSync } from "node:fs"
2
+ import { mkdir, symlink, writeFile } 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 { targetDirFor } from "./build.ts"
7
+ import { clearTarget } from "./clear.ts"
8
+
9
+ describe("clearTarget", () => {
10
+ it("removes the target dir without following its dependency link", () =>
11
+ temporaryDirectoryTask(async root => {
12
+ const configPath = join(root, "livemark.config.ts")
13
+ const targetDir = targetDirFor(configPath)
14
+ await mkdir(join(targetDir, "cache"), { recursive: true })
15
+ await writeFile(join(targetDir, "cache", "entry.md"), "cached")
16
+ await mkdir(join(root, "deps"))
17
+ await writeFile(join(root, "deps", "kept.js"), "")
18
+ await symlink(join(root, "deps"), join(targetDir, "node_modules"), "junction")
19
+
20
+ expect(clearTarget(configPath)).toBe(targetDir)
21
+ expect(existsSync(targetDir)).toBe(false)
22
+ expect(existsSync(join(root, "deps", "kept.js"))).toBe(true)
23
+ expect(() => clearTarget(configPath)).not.toThrow()
24
+ }))
25
+ })
@@ -9,7 +9,6 @@ const configFile = join(import.meta.dirname, "..", "..", "source", "vite.config.
9
9
  */
10
10
  export const build = new Command("build")
11
11
  .description("Build the site for production")
12
- .option("--clear", "Wipe and fully rebuild the runtime target dir")
13
12
  .action(async () => {
14
13
  const builder = await createBuilder({ configFile })
15
14
  await builder.buildApp()
@@ -0,0 +1,15 @@
1
+ import { Command } from "commander"
2
+ import pc from "picocolors"
3
+ import { loadConfig } from "../actions/config/load.ts"
4
+ import { clearTarget } from "../actions/target/clear.ts"
5
+
6
+ /**
7
+ * Wipe the generated `.livemark/.target/` mount.
8
+ */
9
+ export const clear = new Command("clear")
10
+ .description("Wipe the generated .livemark/.target/ mount")
11
+ .action(async () => {
12
+ const config = await loadConfig()
13
+ const targetDir = clearTarget(config.configPath)
14
+ console.log(`Cleared ${pc.dim(targetDir)}`)
15
+ })
@@ -1,79 +1,62 @@
1
- import {
2
- existsSync,
3
- mkdirSync,
4
- readdirSync,
5
- readFileSync,
6
- statSync,
7
- writeFileSync,
8
- } from "node:fs"
9
- import { dirname, join } from "node:path"
1
+ import { join } from "node:path"
2
+ import { autocompleteMultiselect, cancel, isCancel } from "@clack/prompts"
10
3
  import { Command } from "commander"
11
4
  import pc from "picocolors"
12
- import { ESCAPABLE_FILES, ESCAPABLE_FOLDERS } from "../settings.ts"
5
+ import { copyEscape } from "../actions/escape/copy.ts"
6
+ import { listEscapes } from "../actions/escape/list.ts"
13
7
 
14
- const defaultsRoot = join(import.meta.dirname, "..", "..", "source")
8
+ const sourceRoot = join(import.meta.dirname, "..", "..", "source")
15
9
 
16
10
  /**
17
- * List or copy escapable livemark files into `.livemark/`.
11
+ * Pick, list or copy escapable livemark files into `.livemark/`.
18
12
  *
19
- * Without an argument, prints every file you can shadow. With a path, copies
20
- * the default from the livemark package into `.livemark/<path>`; fails if the
13
+ * Without an argument, opens a searchable picker in a terminal and prints every
14
+ * file you can shadow otherwise (or with `--list`). With paths, copies each
15
+ * default from the livemark package into `.livemark/<path>`; fails if a
21
16
  * destination already exists.
22
17
  */
23
18
  export const escape = new Command("escape")
24
- .description("List or copy escapable livemark files into .livemark/")
19
+ .description("Pick, list or copy escapable livemark files into .livemark/")
25
20
  .argument(
26
- "[path]",
27
- "path relative to .livemark/ (e.g. components/Footer.tsx, client.tsx)",
21
+ "[paths...]",
22
+ "paths relative to .livemark/ (e.g. components/Footer.tsx, client.tsx)",
28
23
  )
29
- .action((relPath?: string) => {
30
- if (!relPath) {
31
- for (const file of listEscapable()) console.log(file)
24
+ .option("-l, --list", "print every escapable file instead of opening the picker")
25
+ .action(async (paths: string[], options: { list?: boolean }) => {
26
+ const root = process.cwd()
27
+ if (paths.length) return copyPaths(root, paths)
28
+
29
+ const escapes = listEscapes({ root, sourceRoot })
30
+ if (options.list || !process.stdin.isTTY || !process.stdout.isTTY) {
31
+ for (const escape of escapes) console.log(escape.path)
32
32
  console.log(pc.dim("\nRun 'livemark escape <path>' to copy a file into .livemark/"))
33
33
  return
34
34
  }
35
35
 
36
- const isTopLevelFile = !relPath.includes("/") && ESCAPABLE_FILES.includes(relPath)
37
- const [first] = relPath.split("/")
38
- const isFolderPath =
39
- !!first && ESCAPABLE_FOLDERS.includes(first) && !relPath.includes("..")
40
- if (!isTopLevelFile && !isFolderPath) {
41
- console.error(
42
- pc.red(
43
- `Error: '${relPath}' is not escapable (folders: ${ESCAPABLE_FOLDERS.join(", ")}; files: ${ESCAPABLE_FILES.join(", ")})`,
44
- ),
45
- )
46
- process.exit(1)
47
- }
48
-
49
- const src = join(defaultsRoot, relPath)
50
- if (!existsSync(src) || !statSync(src).isFile()) {
51
- console.error(pc.red(`Error: '${relPath}' is not a livemark file`))
52
- process.exit(1)
53
- }
54
-
55
- const dest = join(process.cwd(), ".livemark", relPath)
56
- if (existsSync(dest)) {
57
- console.error(pc.red(`Error: .livemark/${relPath} already exists`))
58
- process.exit(1)
59
- }
60
-
61
- mkdirSync(dirname(dest), { recursive: true })
62
- writeFileSync(dest, readFileSync(src))
63
- console.log(`Copied ${pc.dim(relPath)} → ${pc.green(`.livemark/${relPath}`)}`)
36
+ const selected = await autocompleteMultiselect({
37
+ message: "Select files to escape into .livemark/",
38
+ placeholder: "Type to search...",
39
+ options: escapes.map(escape => ({
40
+ value: escape.path,
41
+ label: escape.path,
42
+ hint: escape.escaped ? "already escaped" : undefined,
43
+ disabled: escape.escaped,
44
+ })),
45
+ required: false,
46
+ })
47
+ if (isCancel(selected)) return cancel("Nothing copied")
48
+ copyPaths(root, selected)
64
49
  })
65
50
 
66
- function listEscapable() {
67
- const files: string[] = []
68
- for (const folder of ESCAPABLE_FOLDERS) {
69
- const dir = join(defaultsRoot, folder)
70
- if (!existsSync(dir)) continue
71
- for (const entry of readdirSync(dir)) {
72
- if (statSync(join(dir, entry)).isFile()) files.push(`${folder}/${entry}`)
51
+ function copyPaths(root: string, paths: string[]) {
52
+ for (const path of paths) {
53
+ try {
54
+ copyEscape({ root, sourceRoot, path })
55
+ console.log(`Copied ${pc.dim(path)} → ${pc.green(`.livemark/${path}`)}`)
56
+ } catch (error) {
57
+ const message = error instanceof Error ? error.message : String(error)
58
+ console.error(pc.red(`Error: ${message}`))
59
+ process.exitCode = 1
73
60
  }
74
61
  }
75
- for (const file of ESCAPABLE_FILES) {
76
- if (existsSync(join(defaultsRoot, file))) files.push(file)
77
- }
78
- return files.sort()
79
62
  }
@@ -9,7 +9,6 @@ const configFile = join(import.meta.dirname, "..", "..", "source", "vite.config.
9
9
  */
10
10
  export const preview = new Command("preview")
11
11
  .description("Preview the production build locally")
12
- .option("--clear", "Wipe and fully rebuild the runtime target dir")
13
12
  .action(async () => {
14
13
  const server = await vitePreview({
15
14
  configFile,
@@ -9,7 +9,6 @@ const configFile = join(import.meta.dirname, "..", "..", "source", "vite.config.
9
9
  */
10
10
  export const start = new Command("start")
11
11
  .description("Start a live server")
12
- .option("--clear", "Wipe and fully rebuild the runtime target dir")
13
12
  .action(async () => {
14
13
  const server = await createServer({
15
14
  configFile,
@@ -1,7 +1,7 @@
1
1
  /** Renders pre-formatted HTML produced from ANSI escape sequences */
2
2
  export function AnsiCode(props: { html: string }) {
3
3
  return (
4
- <figure className="shiki not-prose my-4 relative rounded-xl border shadow-sm overflow-hidden text-sm bg-sidebar dark:bg-sidebar">
4
+ <figure className="shiki not-prose my-4 relative rounded-card border shadow-sm overflow-hidden text-sm bg-sidebar dark:bg-sidebar">
5
5
  <div className="text-[0.8125rem] py-3.5 overflow-auto">
6
6
  <pre className="min-w-full w-max">
7
7
  <code
@@ -1,6 +1,8 @@
1
+ import { useRef } from "react"
1
2
  import { MDXContent } from "@content-collections/mdx/react"
2
3
  import { Calendar, History, User } from "lucide-react"
3
4
  import type { ArticleView } from "../models/article.ts"
5
+ import { useSearchHighlight } from "../helpers/search-highlight.ts"
4
6
  import { TocContext } from "../helpers/toc-context.ts"
5
7
  import { Abbr } from "./Abbr.tsx"
6
8
  import { AnsiCode } from "./AnsiCode.tsx"
@@ -42,8 +44,10 @@ import { ZoomImage } from "./ZoomImage.tsx"
42
44
  import { Separator } from "../elements/separator.tsx"
43
45
 
44
46
  /** Full-page article layout: MDX content, table of contents, toolbar, prev/next, footer */
45
- export function Article(props: { article: ArticleView }) {
47
+ export function Article(props: { article: ArticleView; highlight?: string }) {
46
48
  const { article } = props
49
+ const prose = useRef<HTMLDivElement>(null)
50
+ useSearchHighlight(prose, article.path, props.highlight)
47
51
  const lastUpdated = formatDate(article.lastUpdated)
48
52
  const publishedDate = formatDate(article.date)
49
53
  const authors = article.author
@@ -56,7 +60,7 @@ export function Article(props: { article: ArticleView }) {
56
60
  return (
57
61
  <TocContext.Provider value={article.tocItems}>
58
62
  {article.toc !== false && (
59
- <MobileToc items={article.tocItems}>
63
+ <MobileToc items={article.tocItems} title={article.title}>
60
64
  <PageToolbar
61
65
  file={article.file}
62
66
  sourceUrl={article.sourceUrl}
@@ -67,7 +71,7 @@ export function Article(props: { article: ArticleView }) {
67
71
  )}
68
72
  <div className="flex flex-1 gap-10 p-4 pt-8 md:p-10">
69
73
  <div className="flex-1 min-w-0 mx-auto max-w-3xl">
70
- <div className="prose dark:prose-invert max-w-none">
74
+ <div ref={prose} className="prose dark:prose-invert max-w-none">
71
75
  <h1 id="top">
72
76
  <a href="#top">{article.title}</a>
73
77
  </h1>
@@ -97,7 +101,7 @@ export function Article(props: { article: ArticleView }) {
97
101
  <img
98
102
  src={article.image}
99
103
  alt=""
100
- className="!mt-0 !mb-6 w-full rounded-xl border shadow-sm"
104
+ className="!mt-0 !mb-6 w-full rounded-card border shadow-sm"
101
105
  />
102
106
  )}
103
107
  <MDXContent
@@ -1,4 +1,14 @@
1
- /** Optional header banner. Empty by default; shadow via `.livemark/components/Banner.tsx` to render content. */
1
+ import { bannerHtml } from "livemark:virtual"
2
+
3
+ /** Announcement strip above the header, rendered from the `banner` config option. */
2
4
  export function Banner() {
3
- return null
5
+ if (!bannerHtml) return null
6
+ return (
7
+ <div className="bg-banner text-banner-foreground">
8
+ <div
9
+ className="min-w-0 px-4 py-2 text-center font-mono text-xs tracking-wide text-balance sm:px-6 [&_a]:underline [&_a]:underline-offset-4 [&_a:hover]:no-underline [&_code]:font-semibold"
10
+ dangerouslySetInnerHTML={{ __html: bannerHtml }}
11
+ />
12
+ </div>
13
+ )
4
14
  }
@@ -40,7 +40,7 @@ export function BlogIndex(props: { sectionPrefix: string }) {
40
40
 
41
41
  return (
42
42
  <TocContext.Provider value={tocItems}>
43
- <MobileToc items={tocItems}>
43
+ <MobileToc items={tocItems} title={section?.title ?? "Blog"}>
44
44
  <PageToolbar content={content} />
45
45
  </MobileToc>
46
46
  <div className="flex flex-1 gap-10 p-4 pt-8 md:p-10">
@@ -25,20 +25,18 @@ const configs: Record<CalloutType, CalloutConfig> = {
25
25
  tip: {
26
26
  icon: Lightbulb,
27
27
  label: "Tip",
28
- classes:
29
- "border-green-500/30 bg-green-50/50 text-green-900 dark:bg-green-950/30 dark:text-green-200",
28
+ classes: "border-positive/30 bg-positive-tint/50 text-positive-tint-foreground",
30
29
  },
31
30
  warning: {
32
31
  icon: AlertTriangle,
33
32
  label: "Warning",
34
- classes:
35
- "border-yellow-500/30 bg-yellow-50/50 text-yellow-900 dark:bg-yellow-950/30 dark:text-yellow-200",
33
+ classes: "border-warning/30 bg-warning-tint/50 text-warning-tint-foreground",
36
34
  },
37
35
  danger: {
38
36
  icon: Flame,
39
37
  label: "Danger",
40
38
  classes:
41
- "border-red-500/30 bg-red-50/50 text-red-900 dark:bg-red-950/30 dark:text-red-200",
39
+ "border-destructive/30 bg-destructive-tint/50 text-destructive-tint-foreground",
42
40
  },
43
41
  }
44
42
 
@@ -54,7 +52,7 @@ export function Callout(props: {
54
52
  const title = props.title ?? config.label
55
53
 
56
54
  return (
57
- <aside className={cn("not-prose my-4 rounded-xl border px-4 py-3", config.classes)}>
55
+ <aside className={cn("not-prose my-4 rounded-card border px-4 py-3", config.classes)}>
58
56
  <div className="flex items-center gap-2 font-semibold text-sm mb-2">
59
57
  <Icon className="size-4 shrink-0" />
60
58
  <span>{title}</span>
@@ -37,7 +37,7 @@ export function ChangelogIndex(props: { sectionPrefix: string }) {
37
37
 
38
38
  return (
39
39
  <TocContext.Provider value={tocItems}>
40
- <MobileToc items={tocItems}>
40
+ <MobileToc items={tocItems} title={section?.title ?? "Changelog"}>
41
41
  <PageToolbar content={content} />
42
42
  </MobileToc>
43
43
  <div className="flex flex-1 gap-10 p-4 pt-8 md:p-10">
@@ -0,0 +1,69 @@
1
+ import { CheckIcon } from "lucide-react"
2
+ import type { MouseEvent, ReactNode } from "react"
3
+ import { cn } from "cn"
4
+
5
+ export type Choice = {
6
+ id: string
7
+ label: ReactNode
8
+ icon?: ReactNode
9
+ href?: string
10
+ /** Indent level below the top one, for a nested choice like a sub-heading. */
11
+ depth?: number
12
+ }
13
+
14
+ /** The rows of a sheet that picks one of a list, the selected one tinted and ticked. */
15
+ export function ChoiceList(props: {
16
+ choices: Choice[]
17
+ selectedId?: string
18
+ onSelect?: (choice: Choice, event: MouseEvent<HTMLElement>) => void
19
+ }) {
20
+ return (
21
+ <ul className="-mx-2 flex flex-col gap-0.5">
22
+ {props.choices.map(choice => {
23
+ const selected = choice.id === props.selectedId
24
+ const className = cn(
25
+ "hover:bg-muted active:bg-muted flex h-10 w-full items-center gap-2.5 rounded-control px-2 text-left text-base outline-hidden focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-[18px] [&_svg]:shrink-0",
26
+ selected && "bg-sidebar-accent text-sidebar-accent-foreground font-medium",
27
+ )
28
+ const content = (
29
+ <>
30
+ {choice.icon}
31
+ <span
32
+ className={cn(
33
+ "flex-1 truncate",
34
+ choice.depth === 1 && "ps-4",
35
+ (choice.depth ?? 0) >= 2 && "ps-8",
36
+ )}
37
+ >
38
+ {choice.label}
39
+ </span>
40
+ {selected && <CheckIcon className="text-primary" />}
41
+ </>
42
+ )
43
+ return (
44
+ <li key={choice.id}>
45
+ {choice.href ? (
46
+ <a
47
+ href={choice.href}
48
+ aria-current={selected || undefined}
49
+ className={className}
50
+ onClick={event => props.onSelect?.(choice, event)}
51
+ >
52
+ {content}
53
+ </a>
54
+ ) : (
55
+ <button
56
+ type="button"
57
+ aria-pressed={selected}
58
+ className={className}
59
+ onClick={event => props.onSelect?.(choice, event)}
60
+ >
61
+ {content}
62
+ </button>
63
+ )}
64
+ </li>
65
+ )
66
+ })}
67
+ </ul>
68
+ )
69
+ }