livemark 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (397) hide show
  1. package/README.md +3 -2
  2. package/build/actions/config/define.d.ts +6 -0
  3. package/build/actions/config/define.unit.js +8 -1
  4. package/build/actions/escape/copy.d.ts +11 -0
  5. package/build/actions/escape/copy.js +29 -0
  6. package/build/actions/escape/copy.unit.d.ts +1 -0
  7. package/build/actions/escape/copy.unit.js +37 -0
  8. package/build/actions/escape/list.d.ts +12 -0
  9. package/build/actions/escape/list.js +27 -0
  10. package/build/actions/escape/list.unit.d.ts +1 -0
  11. package/build/actions/escape/list.unit.js +36 -0
  12. package/build/actions/target/build.d.ts +9 -26
  13. package/build/actions/target/build.js +50 -71
  14. package/build/actions/target/build.unit.d.ts +1 -0
  15. package/build/actions/target/build.unit.js +76 -0
  16. package/build/actions/target/clear.d.ts +4 -0
  17. package/build/actions/target/clear.js +14 -0
  18. package/build/actions/target/clear.unit.d.ts +1 -0
  19. package/build/actions/target/clear.unit.js +23 -0
  20. package/build/commands/build.js +1 -2
  21. package/build/commands/clear.d.ts +5 -0
  22. package/build/commands/clear.js +15 -0
  23. package/build/commands/escape.d.ts +4 -3
  24. package/build/commands/escape.js +45 -48
  25. package/build/commands/preview.js +1 -2
  26. package/build/commands/start.js +1 -2
  27. package/build/components/Abbr.d.ts +1 -1
  28. package/build/components/Abbr.js +2 -4
  29. package/build/components/AnsiCode.d.ts +1 -1
  30. package/build/components/AnsiCode.js +2 -2
  31. package/build/components/Article.d.ts +2 -1
  32. package/build/components/Article.js +7 -14
  33. package/build/components/BackToTop.d.ts +1 -1
  34. package/build/components/Banner.d.ts +2 -2
  35. package/build/components/Banner.js +7 -3
  36. package/build/components/BlogIndex.d.ts +1 -1
  37. package/build/components/BlogIndex.js +2 -11
  38. package/build/components/Breadcrumbs.d.ts +1 -1
  39. package/build/components/Breadcrumbs.js +2 -3
  40. package/build/components/Callout.d.ts +1 -1
  41. package/build/components/Callout.js +5 -9
  42. package/build/components/Card.d.ts +1 -1
  43. package/build/components/Card.js +2 -3
  44. package/build/components/Cards.d.ts +1 -1
  45. package/build/components/ChangelogIndex.d.ts +1 -1
  46. package/build/components/ChangelogIndex.js +2 -11
  47. package/build/components/ChoiceList.d.ts +15 -0
  48. package/build/components/ChoiceList.js +13 -0
  49. package/build/components/CodeBlock.d.ts +1 -1
  50. package/build/components/CodeBlock.js +3 -16
  51. package/build/components/CodeTabs.d.ts +1 -1
  52. package/build/components/CodeTabs.js +2 -3
  53. package/build/components/Column.d.ts +1 -1
  54. package/build/components/Columns.d.ts +1 -1
  55. package/build/components/Content.d.ts +1 -1
  56. package/build/components/ContentTab.d.ts +1 -1
  57. package/build/components/ContentTabs.d.ts +1 -1
  58. package/build/components/ContentTabs.js +2 -3
  59. package/build/components/DefaultCatchBoundary.d.ts +1 -1
  60. package/build/components/DefaultCatchBoundary.js +5 -7
  61. package/build/components/DefinitionList.d.ts +3 -3
  62. package/build/components/Details.d.ts +1 -1
  63. package/build/components/Details.js +2 -5
  64. package/build/components/FileTree.d.ts +1 -1
  65. package/build/components/FileTree.js +3 -5
  66. package/build/components/Footer.d.ts +1 -1
  67. package/build/components/Footer.js +2 -5
  68. package/build/components/FootnoteRef.d.ts +1 -1
  69. package/build/components/FootnoteRef.js +2 -4
  70. package/build/components/Header.d.ts +6 -0
  71. package/build/components/Header.js +26 -0
  72. package/build/components/HeaderMenu.d.ts +2 -0
  73. package/build/components/HeaderMenu.js +27 -0
  74. package/build/components/HeaderNav.d.ts +6 -0
  75. package/build/components/HeaderNav.js +74 -0
  76. package/build/components/HeaderSearch.d.ts +4 -0
  77. package/build/components/HeaderSearch.js +33 -0
  78. package/build/components/Heading.d.ts +6 -6
  79. package/build/components/InlineBadge.d.ts +2 -2
  80. package/build/components/InlineBadge.js +1 -1
  81. package/build/components/InlineIcon.d.ts +1 -1
  82. package/build/components/InlineToc.d.ts +1 -1
  83. package/build/components/InlineToc.js +2 -4
  84. package/build/components/Layout.d.ts +2 -1
  85. package/build/components/Layout.js +5 -58
  86. package/build/components/LinkButton.d.ts +2 -2
  87. package/build/components/LinkButton.js +1 -1
  88. package/build/components/MenuSheet.d.ts +22 -0
  89. package/build/components/MenuSheet.js +27 -0
  90. package/build/components/Mermaid.d.ts +1 -1
  91. package/build/components/MermaidRenderer.d.ts +1 -1
  92. package/build/components/MermaidRenderer.js +20 -8
  93. package/build/components/NotFound.d.ts +1 -1
  94. package/build/components/NotFound.js +3 -6
  95. package/build/components/PackageTabs.d.ts +1 -1
  96. package/build/components/PackageTabs.js +3 -7
  97. package/build/components/PageToolbar.d.ts +1 -1
  98. package/build/components/PageToolbar.js +2 -10
  99. package/build/components/PrevNext.d.ts +1 -1
  100. package/build/components/PrevNext.js +2 -10
  101. package/build/components/Root.d.ts +4 -0
  102. package/build/components/Root.js +8 -0
  103. package/build/components/Search.d.ts +5 -2
  104. package/build/components/Search.js +11 -9
  105. package/build/components/SearchDialog.d.ts +1 -1
  106. package/build/components/SearchDialog.js +10 -133
  107. package/build/components/Section.d.ts +1 -1
  108. package/build/components/SectionHeader.d.ts +1 -1
  109. package/build/components/SheetPanel.d.ts +12 -0
  110. package/build/components/SheetPanel.js +19 -0
  111. package/build/components/Sidebar.d.ts +1 -1
  112. package/build/components/Sidebar.js +12 -14
  113. package/build/components/SidebarArticles.d.ts +4 -1
  114. package/build/components/SidebarArticles.js +5 -9
  115. package/build/components/SidebarLinks.d.ts +4 -1
  116. package/build/components/SidebarLinks.js +6 -9
  117. package/build/components/SidebarPosts.d.ts +4 -1
  118. package/build/components/SidebarPosts.js +7 -23
  119. package/build/components/SidebarReleases.d.ts +4 -1
  120. package/build/components/SidebarReleases.js +5 -15
  121. package/build/components/SidebarSectionPicker.d.ts +8 -0
  122. package/build/components/SidebarSectionPicker.js +31 -0
  123. package/build/components/SidebarSections.d.ts +1 -1
  124. package/build/components/SidebarSections.js +5 -9
  125. package/build/components/SiteLink.d.ts +1 -1
  126. package/build/components/SiteTitle.d.ts +1 -1
  127. package/build/components/SiteTitle.js +2 -6
  128. package/build/components/SortableTable.d.ts +1 -1
  129. package/build/components/SoundCloud.d.ts +1 -1
  130. package/build/components/SoundCloud.js +2 -2
  131. package/build/components/TagIndex.d.ts +1 -1
  132. package/build/components/TagIndex.js +2 -11
  133. package/build/components/Theme.d.ts +2 -1
  134. package/build/components/Theme.js +35 -15
  135. package/build/components/Toc.d.ts +5 -4
  136. package/build/components/Toc.js +35 -35
  137. package/build/components/YouTube.d.ts +1 -1
  138. package/build/components/YouTube.js +2 -2
  139. package/build/components/ZoomImage.d.ts +1 -1
  140. package/build/content-collections.js +5 -1
  141. package/build/elements/accordion.d.ts +4 -4
  142. package/build/elements/accordion.js +2 -3
  143. package/build/elements/alert-dialog.d.ts +12 -12
  144. package/build/elements/alert-dialog.js +2 -4
  145. package/build/elements/alert.d.ts +4 -4
  146. package/build/elements/alert.js +2 -2
  147. package/build/elements/aspect-ratio.d.ts +1 -1
  148. package/build/elements/avatar.d.ts +6 -6
  149. package/build/elements/badge.d.ts +1 -1
  150. package/build/elements/badge.js +3 -1
  151. package/build/elements/breadcrumb.d.ts +6 -6
  152. package/build/elements/breadcrumb.js +2 -4
  153. package/build/elements/button-group.d.ts +2 -2
  154. package/build/elements/button-group.js +5 -5
  155. package/build/elements/button.d.ts +2 -2
  156. package/build/elements/button.js +9 -7
  157. package/build/elements/calendar.d.ts +2 -2
  158. package/build/elements/calendar.js +2 -2
  159. package/build/elements/card.d.ts +7 -7
  160. package/build/elements/card.js +4 -4
  161. package/build/elements/carousel.d.ts +5 -5
  162. package/build/elements/carousel.js +3 -7
  163. package/build/elements/chart.d.ts +6 -6
  164. package/build/elements/chart.js +5 -11
  165. package/build/elements/checkbox.d.ts +1 -1
  166. package/build/elements/collapsible.d.ts +3 -3
  167. package/build/elements/command.d.ts +9 -9
  168. package/build/elements/command.js +5 -11
  169. package/build/elements/context-menu.d.ts +15 -15
  170. package/build/elements/context-menu.js +6 -9
  171. package/build/elements/dialog.d.ts +10 -10
  172. package/build/elements/dialog.js +2 -6
  173. package/build/elements/drawer.d.ts +14 -12
  174. package/build/elements/drawer.js +14 -15
  175. package/build/elements/dropdown-menu.d.ts +15 -15
  176. package/build/elements/dropdown-menu.js +7 -10
  177. package/build/elements/empty.d.ts +6 -6
  178. package/build/elements/empty.js +2 -2
  179. package/build/elements/field.d.ts +10 -10
  180. package/build/elements/field.js +3 -4
  181. package/build/elements/hover-card.d.ts +3 -3
  182. package/build/elements/hover-card.js +2 -2
  183. package/build/elements/input-group.d.ts +7 -7
  184. package/build/elements/input-group.js +5 -5
  185. package/build/elements/input-otp.d.ts +4 -4
  186. package/build/elements/input.d.ts +1 -1
  187. package/build/elements/input.js +2 -2
  188. package/build/elements/item.d.ts +9 -9
  189. package/build/elements/item.js +2 -2
  190. package/build/elements/kbd.d.ts +2 -2
  191. package/build/elements/label.d.ts +1 -1
  192. package/build/elements/menubar.d.ts +16 -16
  193. package/build/elements/menubar.js +9 -11
  194. package/build/elements/native-select.d.ts +3 -3
  195. package/build/elements/native-select.js +2 -4
  196. package/build/elements/navigation-menu.d.ts +8 -8
  197. package/build/elements/navigation-menu.js +3 -5
  198. package/build/elements/pagination.d.ts +7 -7
  199. package/build/elements/pagination.js +4 -10
  200. package/build/elements/popover.d.ts +6 -6
  201. package/build/elements/popover.js +2 -2
  202. package/build/elements/progress.d.ts +5 -5
  203. package/build/elements/progress.js +2 -3
  204. package/build/elements/radio-group.d.ts +2 -2
  205. package/build/elements/resizable.d.ts +3 -3
  206. package/build/elements/scroll-area.d.ts +2 -2
  207. package/build/elements/scroll-area.js +2 -4
  208. package/build/elements/select.d.ts +9 -9
  209. package/build/elements/select.js +4 -9
  210. package/build/elements/separator.d.ts +1 -1
  211. package/build/elements/sheet.d.ts +8 -8
  212. package/build/elements/sheet.js +2 -6
  213. package/build/elements/sidebar.d.ts +19 -19
  214. package/build/elements/sidebar.js +15 -37
  215. package/build/elements/skeleton.d.ts +1 -1
  216. package/build/elements/slider.d.ts +2 -1
  217. package/build/elements/slider.js +2 -3
  218. package/build/elements/sonner.d.ts +1 -1
  219. package/build/elements/spinner.d.ts +1 -1
  220. package/build/elements/switch.d.ts +1 -1
  221. package/build/elements/table.d.ts +8 -8
  222. package/build/elements/tabs.d.ts +4 -4
  223. package/build/elements/tabs.js +3 -3
  224. package/build/elements/textarea.d.ts +1 -1
  225. package/build/elements/textarea.js +2 -2
  226. package/build/elements/toggle-group.d.ts +3 -2
  227. package/build/elements/toggle-group.js +3 -3
  228. package/build/elements/toggle.d.ts +1 -1
  229. package/build/elements/toggle.js +2 -2
  230. package/build/elements/tooltip.d.ts +4 -4
  231. package/build/elements/tooltip.js +2 -3
  232. package/build/head.d.ts +4 -0
  233. package/build/head.js +6 -0
  234. package/build/helpers/article-search.d.ts +19 -0
  235. package/build/helpers/article-search.js +134 -0
  236. package/build/helpers/dependency-dir.d.ts +5 -0
  237. package/build/helpers/dependency-dir.js +20 -0
  238. package/build/helpers/dependency-dir.unit.d.ts +1 -0
  239. package/build/helpers/dependency-dir.unit.js +25 -0
  240. package/build/helpers/dynamic-icon-resolver.d.ts +1 -1
  241. package/build/helpers/dynamic-icon.d.ts +1 -1
  242. package/build/helpers/head.d.ts +53 -0
  243. package/build/helpers/head.js +69 -0
  244. package/build/helpers/head.unit.d.ts +1 -0
  245. package/build/helpers/head.unit.js +108 -0
  246. package/build/helpers/nav-items.d.ts +11 -0
  247. package/build/helpers/nav-items.js +65 -0
  248. package/build/helpers/render-inline-markdown.d.ts +1 -0
  249. package/build/helpers/render-inline-markdown.js +34 -0
  250. package/build/helpers/render-inline-markdown.unit.d.ts +1 -0
  251. package/build/helpers/render-inline-markdown.unit.js +20 -0
  252. package/build/helpers/search-highlight.d.ts +8 -0
  253. package/build/helpers/search-highlight.js +59 -0
  254. package/build/helpers/search-highlight.unit.d.ts +1 -0
  255. package/build/helpers/search-highlight.unit.js +36 -0
  256. package/build/helpers/target-export.d.ts +9 -0
  257. package/build/helpers/target-export.js +24 -0
  258. package/build/helpers/target-export.unit.d.ts +1 -0
  259. package/build/helpers/target-export.unit.js +25 -0
  260. package/build/helpers/theme.d.ts +6 -2
  261. package/build/helpers/theme.js +40 -7
  262. package/build/helpers/theme.unit.d.ts +1 -0
  263. package/build/helpers/theme.unit.js +30 -0
  264. package/build/hooks/hydrated.d.ts +2 -0
  265. package/build/hooks/hydrated.js +15 -0
  266. package/build/index.d.ts +1 -0
  267. package/build/index.js +1 -1
  268. package/build/models/config.d.ts +32 -6
  269. package/build/models/config.js +10 -1
  270. package/build/models/section.d.ts +4 -2
  271. package/build/models/section.js +1 -1
  272. package/build/plugins/vite-livemark.d.ts +4 -5
  273. package/build/plugins/vite-livemark.js +56 -90
  274. package/build/program.js +4 -2
  275. package/build/routes/$.d.ts +4 -1
  276. package/build/routes/$.js +20 -9
  277. package/build/routes/__root.js +29 -63
  278. package/build/settings.js +2 -1
  279. package/package.json +68 -66
  280. package/{.agents/skills → skills}/livemark-authoring/SKILL.md +2 -2
  281. package/{.agents/skills → skills}/livemark-configuration/SKILL.md +12 -5
  282. package/skills/livemark-customization/SKILL.md +145 -0
  283. package/source/actions/config/define.unit.ts +9 -0
  284. package/source/actions/escape/copy.ts +35 -0
  285. package/source/actions/escape/copy.unit.ts +51 -0
  286. package/source/actions/escape/list.ts +30 -0
  287. package/source/actions/escape/list.unit.ts +42 -0
  288. package/source/actions/target/build.ts +49 -69
  289. package/source/actions/target/build.unit.ts +95 -0
  290. package/source/actions/target/clear.ts +14 -0
  291. package/source/actions/target/clear.unit.ts +25 -0
  292. package/source/commands/build.ts +0 -1
  293. package/source/commands/clear.ts +15 -0
  294. package/source/commands/escape.ts +42 -59
  295. package/source/commands/preview.ts +0 -1
  296. package/source/commands/start.ts +0 -1
  297. package/source/components/AnsiCode.tsx +1 -1
  298. package/source/components/Article.tsx +8 -4
  299. package/source/components/Banner.tsx +12 -2
  300. package/source/components/BlogIndex.tsx +1 -1
  301. package/source/components/Callout.tsx +4 -6
  302. package/source/components/ChangelogIndex.tsx +1 -1
  303. package/source/components/ChoiceList.tsx +69 -0
  304. package/source/components/CodeBlock.tsx +6 -6
  305. package/source/components/CodeTabs.tsx +1 -1
  306. package/source/components/ContentTabs.tsx +1 -1
  307. package/source/components/DefaultCatchBoundary.tsx +4 -6
  308. package/source/components/Details.tsx +1 -1
  309. package/source/components/FileTree.tsx +1 -1
  310. package/source/components/Header.tsx +52 -0
  311. package/source/components/HeaderMenu.tsx +41 -0
  312. package/source/components/HeaderNav.tsx +145 -0
  313. package/source/components/HeaderSearch.tsx +77 -0
  314. package/source/components/InlineBadge.tsx +1 -1
  315. package/source/components/InlineToc.tsx +1 -1
  316. package/source/components/Layout.tsx +16 -160
  317. package/source/components/LinkButton.tsx +9 -1
  318. package/source/components/MenuSheet.tsx +92 -0
  319. package/source/components/MermaidRenderer.tsx +19 -8
  320. package/source/components/NotFound.tsx +3 -5
  321. package/source/components/PackageTabs.tsx +2 -2
  322. package/source/components/PageToolbar.tsx +11 -3
  323. package/source/components/PrevNext.tsx +2 -2
  324. package/source/components/Root.tsx +13 -0
  325. package/source/components/Search.tsx +28 -8
  326. package/source/components/SearchDialog.tsx +15 -189
  327. package/source/components/SheetPanel.tsx +69 -0
  328. package/source/components/Sidebar.tsx +27 -14
  329. package/source/components/SidebarArticles.tsx +4 -3
  330. package/source/components/SidebarLinks.tsx +3 -2
  331. package/source/components/SidebarPosts.tsx +3 -2
  332. package/source/components/SidebarReleases.tsx +3 -2
  333. package/source/components/SidebarSectionPicker.tsx +70 -0
  334. package/source/components/SiteTitle.tsx +5 -5
  335. package/source/components/SoundCloud.tsx +1 -1
  336. package/source/components/TagIndex.tsx +1 -1
  337. package/source/components/Theme.tsx +55 -29
  338. package/source/components/Toc.tsx +76 -55
  339. package/source/components/YouTube.tsx +1 -1
  340. package/source/content-collections.ts +4 -0
  341. package/source/elements/alert-dialog.tsx +1 -1
  342. package/source/elements/alert.tsx +1 -1
  343. package/source/elements/badge.tsx +4 -0
  344. package/source/elements/button-group.tsx +4 -4
  345. package/source/elements/button.tsx +10 -6
  346. package/source/elements/calendar.tsx +1 -1
  347. package/source/elements/card.tsx +3 -3
  348. package/source/elements/command.tsx +7 -4
  349. package/source/elements/context-menu.tsx +5 -5
  350. package/source/elements/dialog.tsx +1 -1
  351. package/source/elements/drawer.tsx +59 -43
  352. package/source/elements/dropdown-menu.tsx +6 -6
  353. package/source/elements/empty.tsx +1 -1
  354. package/source/elements/field.tsx +1 -1
  355. package/source/elements/hover-card.tsx +1 -1
  356. package/source/elements/input-group.tsx +4 -4
  357. package/source/elements/input.tsx +1 -1
  358. package/source/elements/item.tsx +1 -1
  359. package/source/elements/menubar.tsx +8 -8
  360. package/source/elements/native-select.tsx +1 -1
  361. package/source/elements/popover.tsx +1 -1
  362. package/source/elements/select.tsx +3 -3
  363. package/source/elements/sidebar.tsx +8 -21
  364. package/source/elements/tabs.tsx +2 -2
  365. package/source/elements/textarea.tsx +1 -1
  366. package/source/elements/toggle-group.tsx +2 -2
  367. package/source/elements/toggle.tsx +1 -1
  368. package/source/env.d.ts +1 -0
  369. package/source/head.ts +7 -0
  370. package/source/helpers/article-search.tsx +192 -0
  371. package/source/helpers/dependency-dir.ts +20 -0
  372. package/source/helpers/dependency-dir.unit.ts +39 -0
  373. package/source/helpers/head.ts +94 -0
  374. package/source/helpers/head.unit.ts +116 -0
  375. package/source/helpers/nav-items.tsx +102 -0
  376. package/source/helpers/render-inline-markdown.ts +44 -0
  377. package/source/helpers/render-inline-markdown.unit.ts +30 -0
  378. package/source/helpers/search-highlight.ts +73 -0
  379. package/source/helpers/search-highlight.unit.ts +42 -0
  380. package/source/helpers/target-export.ts +28 -0
  381. package/source/helpers/target-export.unit.ts +37 -0
  382. package/source/helpers/theme.ts +40 -6
  383. package/source/helpers/theme.unit.ts +32 -0
  384. package/source/hooks/hydrated.ts +18 -0
  385. package/source/index.ts +1 -0
  386. package/source/models/config.ts +13 -0
  387. package/source/models/section.ts +3 -0
  388. package/source/plugins/vite-livemark.ts +54 -95
  389. package/source/program.ts +2 -0
  390. package/source/routes/$.tsx +27 -9
  391. package/source/routes/__root.tsx +51 -71
  392. package/source/settings.ts +1 -0
  393. package/source/styles/general.css +59 -11
  394. package/source/styles/markdown.css +13 -7
  395. package/source/styles/shape.css +14 -0
  396. package/.agents/skills/change-review/SKILL.md +0 -47
  397. package/.agents/skills/livemark-customization/SKILL.md +0 -118
@@ -5,7 +5,15 @@ import { cn } from "cn"
5
5
  export function LinkButton(props: {
6
6
  href: string
7
7
  label: string
8
- variant?: "default" | "outline" | "secondary" | "ghost" | "destructive" | "link"
8
+ variant?:
9
+ | "default"
10
+ | "outline"
11
+ | "secondary"
12
+ | "ghost"
13
+ | "positive"
14
+ | "warning"
15
+ | "destructive"
16
+ | "link"
9
17
  size?: "default" | "sm" | "lg"
10
18
  }) {
11
19
  return (
@@ -0,0 +1,92 @@
1
+ import { CheckIcon } from "lucide-react"
2
+ import { cloneElement, useState, type ReactElement, type ReactNode } from "react"
3
+ import { cn } from "cn"
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuTrigger,
9
+ } from "../elements/dropdown-menu.tsx"
10
+ import { useIsMobile } from "../hooks/media.ts"
11
+ import { SheetPanel } from "./SheetPanel.tsx"
12
+
13
+ /** One row of a `MenuSheet` */
14
+ export type MenuSheetItem = {
15
+ id: string
16
+ label: ReactNode
17
+ icon?: ReactNode
18
+ active?: boolean
19
+ onSelect?: () => void
20
+ }
21
+
22
+ /** A menu that is an anchored dropdown from a laptop up and a bottom sheet on a
23
+ * phone */
24
+ export function MenuSheet(props: {
25
+ trigger: ReactElement<{ onClick?: () => void; children?: ReactNode }>
26
+ children?: ReactNode
27
+ title: ReactNode
28
+ items: MenuSheetItem[]
29
+ align?: "start" | "end"
30
+ onOpenChange?: (open: boolean) => void
31
+ }) {
32
+ const isMobile = useIsMobile()
33
+ const [open, setOpen] = useState(false)
34
+ const changeOpen = (next: boolean) => {
35
+ setOpen(next)
36
+ props.onOpenChange?.(next)
37
+ }
38
+
39
+ if (isMobile) {
40
+ return (
41
+ <>
42
+ {cloneElement(props.trigger, { onClick: () => changeOpen(true) }, props.children)}
43
+ <SheetPanel open={open} onOpenChange={changeOpen} title={props.title}>
44
+ <ul className="flex w-full min-w-0 flex-col gap-1">
45
+ {props.items.map(item => (
46
+ <li key={item.id}>
47
+ <button
48
+ type="button"
49
+ className={cn(ROW, item.active && ROW_ACTIVE)}
50
+ onClick={() => {
51
+ item.onSelect?.()
52
+ changeOpen(false)
53
+ }}
54
+ >
55
+ {item.icon}
56
+ <span className="flex-1 truncate">{item.label}</span>
57
+ {item.active && <CheckIcon className="size-4 shrink-0" />}
58
+ </button>
59
+ </li>
60
+ ))}
61
+ </ul>
62
+ </SheetPanel>
63
+ </>
64
+ )
65
+ }
66
+
67
+ return (
68
+ <DropdownMenu onOpenChange={props.onOpenChange}>
69
+ {/* Without a `render`, Base UI's trigger falls back to a bare <button> — which
70
+ SSR paints on a phone until `useIsMobile` flips, flashing a stray icon. */}
71
+ <DropdownMenuTrigger render={props.trigger}>{props.children}</DropdownMenuTrigger>
72
+ <DropdownMenuContent align={props.align ?? "end"}>
73
+ {props.items.map(item => (
74
+ <DropdownMenuItem
75
+ key={item.id}
76
+ onClick={item.onSelect}
77
+ className="cursor-pointer gap-2"
78
+ >
79
+ {item.icon}
80
+ <span className={item.active ? "font-medium" : undefined}>{item.label}</span>
81
+ </DropdownMenuItem>
82
+ ))}
83
+ </DropdownMenuContent>
84
+ </DropdownMenu>
85
+ )
86
+ }
87
+
88
+ const ROW =
89
+ "hover:bg-muted active:bg-muted flex h-10 w-full items-center gap-2.5 overflow-hidden rounded-md p-2 text-left text-base outline-hidden [&_svg]:size-[18px] [&_svg]:shrink-0"
90
+
91
+ const ROW_ACTIVE =
92
+ "bg-sidebar-accent text-sidebar-accent-foreground font-semibold [&_svg]:stroke-[2.5]"
@@ -3,6 +3,10 @@ import mermaid from "mermaid"
3
3
 
4
4
  mermaid.initialize({ startOnLoad: false })
5
5
 
6
+ // Module-wide so runs never overlap, even across effect re-runs: overlapping runs on
7
+ // one node reject with "reading 'firstChild'".
8
+ let renderQueue = Promise.resolve()
9
+
6
10
  /** Renders a Mermaid diagram from a chart definition string */
7
11
  export default function MermaidRenderer(props: { children: string }) {
8
12
  const ref = useRef<HTMLDivElement>(null)
@@ -12,14 +16,21 @@ export default function MermaidRenderer(props: { children: string }) {
12
16
  if (!el) return
13
17
 
14
18
  const render = () => {
15
- const isDark = document.documentElement.classList.contains("dark")
16
- mermaid.initialize({
17
- startOnLoad: false,
18
- theme: isDark ? "dark" : "default",
19
- })
20
- el.textContent = props.children
21
- el.removeAttribute("data-processed")
22
- mermaid.run({ nodes: [el] })
19
+ renderQueue = renderQueue
20
+ .then(() => {
21
+ if (!el.isConnected) return
22
+ const isDark = document.documentElement.classList.contains("dark")
23
+ mermaid.initialize({
24
+ startOnLoad: false,
25
+ theme: isDark ? "dark" : "default",
26
+ })
27
+ el.textContent = props.children
28
+ el.removeAttribute("data-processed")
29
+ return mermaid.run({ nodes: [el] })
30
+ })
31
+ .catch(error => {
32
+ if (el.isConnected) console.error(error)
33
+ })
23
34
  }
24
35
 
25
36
  render()
@@ -1,4 +1,5 @@
1
1
  import { Link } from "@tanstack/react-router"
2
+ import { buttonVariants } from "../elements/button.tsx"
2
3
 
3
4
  export function NotFound({ children }: { children?: any }) {
4
5
  return (
@@ -11,14 +12,11 @@ export function NotFound({ children }: { children?: any }) {
11
12
  <button
12
13
  type="button"
13
14
  onClick={() => window.history.back()}
14
- className="bg-primary text-primary-foreground px-4 py-2 text-sm font-medium hover:bg-primary/80 transition-colors"
15
+ className={buttonVariants({ size: "lg" })}
15
16
  >
16
17
  Go Back
17
18
  </button>
18
- <Link
19
- to="/"
20
- className="border border-border px-4 py-2 text-sm font-medium hover:bg-card transition-colors"
21
- >
19
+ <Link to="/" className={buttonVariants({ variant: "outline", size: "lg" })}>
22
20
  Home
23
21
  </Link>
24
22
  </div>
@@ -31,7 +31,7 @@ export function PackageTabs(props: PackageTabsProps) {
31
31
  }
32
32
 
33
33
  return (
34
- <figure className="not-prose my-4 relative rounded-xl border shadow-sm overflow-hidden text-sm bg-sidebar dark:bg-sidebar">
34
+ <figure className="not-prose my-4 relative rounded-card border shadow-sm overflow-hidden text-sm bg-sidebar dark:bg-sidebar">
35
35
  <div className="flex items-center border-b">
36
36
  <div className="flex flex-1 items-center">
37
37
  {managers.map(pm => (
@@ -52,7 +52,7 @@ export function PackageTabs(props: PackageTabsProps) {
52
52
  <button
53
53
  type="button"
54
54
  onClick={handleCopy}
55
- className="me-2 p-1 rounded-md text-muted-foreground hover:text-foreground transition-colors"
55
+ className="me-2 p-1 rounded-control text-muted-foreground hover:text-foreground transition-colors"
56
56
  aria-label="Copy code"
57
57
  >
58
58
  {copied ? <Check className="size-3.5" /> : <Clipboard className="size-3.5" />}
@@ -37,13 +37,13 @@ export function PageToolbar(props: {
37
37
 
38
38
  return (
39
39
  <>
40
- <div className="mb-6 flex flex-col gap-2 text-sm">
41
- <p className="text-sm font-medium mb-1">Actions</p>
40
+ <div data-slot="page-toolbar" className="flex flex-col gap-2 text-sm">
42
41
  {sourceHref ? (
43
42
  <a
44
43
  href={sourceHref}
45
44
  target="_blank"
46
45
  rel="noopener noreferrer"
46
+ data-slot="page-action"
47
47
  className={buttonClass}
48
48
  >
49
49
  <SourceIcon className="size-3.5" />
@@ -51,6 +51,7 @@ export function PageToolbar(props: {
51
51
  </a>
52
52
  ) : (
53
53
  <span
54
+ data-slot="page-action"
54
55
  className={`${buttonClass} opacity-50 cursor-not-allowed hover:text-muted-foreground`}
55
56
  aria-disabled="true"
56
57
  >
@@ -58,13 +59,19 @@ export function PageToolbar(props: {
58
59
  {sourceLabel}
59
60
  </span>
60
61
  )}
61
- <button type="button" onClick={handleCopy} className={buttonClass}>
62
+ <button
63
+ type="button"
64
+ onClick={handleCopy}
65
+ data-slot="page-action"
66
+ className={buttonClass}
67
+ >
62
68
  {copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
63
69
  Copy Markdown
64
70
  </button>
65
71
  <button
66
72
  type="button"
67
73
  onClick={() => openIn("https://chatgpt.com/", { hints: "search" })}
74
+ data-slot="page-action"
68
75
  className={buttonClass}
69
76
  >
70
77
  <ExternalLink className="size-3.5" />
@@ -73,6 +80,7 @@ export function PageToolbar(props: {
73
80
  <button
74
81
  type="button"
75
82
  onClick={() => openIn("https://claude.ai/new")}
83
+ data-slot="page-action"
76
84
  className={buttonClass}
77
85
  >
78
86
  <ExternalLink className="size-3.5" />
@@ -29,7 +29,7 @@ export function PrevNext(props: { path: string }) {
29
29
  <Link
30
30
  to="/$"
31
31
  params={{ _splat: prev.path.replace(/^\/|\/$/g, "") }}
32
- className="flex-1 rounded-xl border shadow-sm p-4 hover:bg-muted/50 transition-colors"
32
+ className="flex-1 rounded-card border shadow-sm p-4 hover:bg-muted/50 transition-colors"
33
33
  >
34
34
  <span className="flex items-center gap-1 text-sm text-muted-foreground">
35
35
  <ChevronLeft className="size-4" />
@@ -42,7 +42,7 @@ export function PrevNext(props: { path: string }) {
42
42
  <Link
43
43
  to="/$"
44
44
  params={{ _splat: next.path.replace(/^\/|\/$/g, "") }}
45
- className="flex-1 rounded-xl border shadow-sm p-4 hover:bg-muted/50 transition-colors text-right"
45
+ className="flex-1 rounded-card border shadow-sm p-4 hover:bg-muted/50 transition-colors text-right"
46
46
  >
47
47
  <span className="flex items-center justify-end gap-1 text-sm text-muted-foreground">
48
48
  Next
@@ -0,0 +1,13 @@
1
+ import { useHotkey } from "@tanstack/react-hotkeys"
2
+
3
+ /** Headless wrapper around every route; pages add their own chrome with `Layout` */
4
+ export function Root(props: { children?: React.ReactNode }) {
5
+ useHotkey("J", () =>
6
+ window.scrollBy({ top: window.innerHeight * 0.8, behavior: "smooth" }),
7
+ )
8
+ useHotkey("K", () =>
9
+ window.scrollBy({ top: -window.innerHeight * 0.8, behavior: "smooth" }),
10
+ )
11
+
12
+ return props.children
13
+ }
@@ -1,27 +1,47 @@
1
1
  import { useState } from "react"
2
2
  import { useHotkey } from "@tanstack/react-hotkeys"
3
3
  import { Search as SearchIcon } from "lucide-react"
4
+ import { cn } from "cn"
5
+ import { isMobileNow } from "../hooks/media.ts"
6
+ import { HeaderSearch } from "./HeaderSearch.tsx"
4
7
  import { SearchDialog } from "./SearchDialog.tsx"
5
8
 
6
- /** Search button with "/" keyboard shortcut and command palette dialog */
7
- export function Search() {
9
+ /** Search button with "S" keyboard shortcut: a command palette from `lg`, an in-header field below.
10
+ * `compact` keeps it an icon from `lg` too, when header sections need the room */
11
+ export function Search(props: { compact?: boolean }) {
8
12
  const [open, setOpen] = useState(false)
13
+ const [headerOpen, setHeaderOpen] = useState(false)
14
+ const openSearch = () => (isMobileNow() ? setHeaderOpen(true) : setOpen(true))
9
15
 
10
- useHotkey("/", () => setOpen(true))
16
+ useHotkey("S", openSearch)
11
17
 
12
18
  return (
13
19
  <>
14
20
  <button
15
21
  type="button"
16
- onClick={() => setOpen(true)}
17
- className="flex w-full items-center gap-2 rounded-xl border border-border bg-sidebar-accent/70 px-3 py-2 text-sm text-muted-foreground shadow-xs hover:bg-background transition-colors cursor-pointer"
22
+ onClick={openSearch}
23
+ aria-label="Search"
24
+ aria-keyshortcuts="S"
25
+ title="Search"
26
+ className={cn(
27
+ "flex h-9 items-center gap-2 rounded-control border border-border bg-muted/50 px-2.5 text-sm text-muted-foreground shadow-xs hover:bg-muted transition-colors cursor-pointer",
28
+ !props.compact && "lg:w-56",
29
+ )}
18
30
  >
19
31
  <SearchIcon className="size-4" />
20
- <span className="flex-1 text-left">Search...</span>
21
- <kbd className="rounded border border-border bg-muted px-1.5 py-0.5 text-[10px] font-mono">
22
- /
32
+ <span className={cn("hidden flex-1 text-left", !props.compact && "lg:inline")}>
33
+ Search...
34
+ </span>
35
+ <kbd
36
+ className={cn(
37
+ "hidden rounded border border-border bg-background px-1.5 py-0.5 text-[10px] font-mono",
38
+ !props.compact && "lg:inline",
39
+ )}
40
+ >
41
+ S
23
42
  </kbd>
24
43
  </button>
44
+ {headerOpen && <HeaderSearch onClose={() => setHeaderOpen(false)} />}
25
45
  <SearchDialog open={open} onOpenChange={setOpen} />
26
46
  </>
27
47
  )
@@ -1,127 +1,17 @@
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 "../helpers/article-icon.ts"
1
+ import { SearchResultGroups, useArticleSearch } from "../helpers/article-search.tsx"
6
2
  import {
7
3
  CommandDialog,
8
4
  CommandEmpty,
9
- CommandGroup,
10
5
  CommandInput,
11
- CommandItem,
12
6
  CommandList,
13
7
  } from "../elements/command.tsx"
14
8
 
15
- interface SearchResult {
16
- title: string
17
- path: string
18
- icon?: string
19
- snippet: string
20
- group: string
21
- }
22
-
23
9
  /** Full-text search dialog powered by Orama */
24
10
  export function SearchDialog(props: {
25
11
  open: boolean
26
12
  onOpenChange: (open: boolean) => void
27
13
  }) {
28
- const [results, setResults] = useState<SearchResult[]>([])
29
- const [query, setQuery] = useState("")
30
- const dbRef = useRef<Awaited<ReturnType<typeof create>> | null>(null)
31
- const navigate = useNavigate()
32
-
33
- const ensureIndex = useCallback(async () => {
34
- if (dbRef.current) return dbRef.current
35
- const db = await create({
36
- schema: {
37
- title: "string",
38
- description: "string",
39
- searchText: "string",
40
- },
41
- })
42
- await insertMultiple(
43
- db,
44
- sortedArticles.map(a => ({
45
- title: a.title,
46
- description: a.description ?? "",
47
- searchText: a.searchText,
48
- })),
49
- )
50
- dbRef.current = db
51
- return db
52
- }, [])
53
-
54
- const toResult = useCallback(
55
- (article: (typeof sortedArticles)[number], term: string): SearchResult => ({
56
- title: article.title,
57
- path: article.path,
58
- icon: article.icon,
59
- snippet: term
60
- ? extractSnippet(article.searchText, term)
61
- : (article.description ?? ""),
62
- group: groupFor(article.path),
63
- }),
64
- [],
65
- )
66
-
67
- const handleSearch = useCallback(
68
- async (term: string) => {
69
- setQuery(term)
70
- if (!term.trim()) {
71
- setResults([])
72
- return
73
- }
74
- const db = await ensureIndex()
75
- const hits = await search(db, {
76
- term,
77
- properties: ["title", "description", "searchText"],
78
- tolerance: 1,
79
- boost: { title: 3, description: 2 },
80
- })
81
- setResults(
82
- hits.hits.map(hit => {
83
- const article = sortedArticles.find(a => a.title === hit.document.title)
84
- if (!article)
85
- return toResult(
86
- {
87
- ...sortedArticles[0],
88
- title: hit.document.title,
89
- path: "",
90
- searchText: hit.document.description,
91
- description: hit.document.description,
92
- icon: "",
93
- },
94
- term,
95
- )
96
- return toResult(article, term)
97
- }),
98
- )
99
- },
100
- [ensureIndex, toResult],
101
- )
102
-
103
- const handleSelect = useCallback(
104
- (path: string) => {
105
- props.onOpenChange(false)
106
- setQuery("")
107
- setResults([])
108
- navigate({
109
- to: "/$",
110
- params: { _splat: path.replace(/^\/|\/$/g, "") },
111
- })
112
- },
113
- [navigate, props.onOpenChange],
114
- )
115
-
116
- const grouped = useMemo(() => {
117
- const groups = new Map<string, SearchResult[]>()
118
- for (const r of results) {
119
- const list = groups.get(r.group) ?? []
120
- list.push(r)
121
- groups.set(r.group, list)
122
- }
123
- return groups
124
- }, [results])
14
+ const search = useArticleSearch()
125
15
 
126
16
  return (
127
17
  <CommandDialog
@@ -134,43 +24,24 @@ export function SearchDialog(props: {
134
24
  >
135
25
  <CommandInput
136
26
  placeholder="Search docs..."
137
- value={query}
138
- onValueChange={handleSearch}
139
- onClear={() => handleSearch("")}
27
+ value={search.query}
28
+ onValueChange={search.searchArticles}
29
+ onClear={() => search.searchArticles("")}
140
30
  />
141
31
  <CommandList className="max-h-80">
142
32
  <CommandEmpty>
143
- {query.trim()
33
+ {search.query.trim()
144
34
  ? "No results found."
145
- : "Type to search across all documentation."}
35
+ : "Type to search across all contents."}
146
36
  </CommandEmpty>
147
- {[...grouped.entries()].map(([group, items]) => (
148
- <CommandGroup key={group} heading={group}>
149
- {items.map(result => {
150
- const Icon = resolveArticleIcon(result.icon)
151
- return (
152
- <CommandItem
153
- key={result.path}
154
- value={result.path}
155
- onSelect={() => handleSelect(result.path)}
156
- className="py-2.5"
157
- >
158
- {Icon && (
159
- <Icon className="size-5 shrink-0 mt-0.5 self-start opacity-60" />
160
- )}
161
- <div className="flex flex-col gap-0.5 min-w-0">
162
- <span className="text-base font-medium">{result.title}</span>
163
- {result.snippet && (
164
- <span className="text-sm text-muted-foreground line-clamp-1">
165
- <HighlightedText text={result.snippet} term={query} />
166
- </span>
167
- )}
168
- </div>
169
- </CommandItem>
170
- )
171
- })}
172
- </CommandGroup>
173
- ))}
37
+ <SearchResultGroups
38
+ query={search.query}
39
+ grouped={search.grouped}
40
+ onSelect={path => {
41
+ props.onOpenChange(false)
42
+ search.selectArticle(path)
43
+ }}
44
+ />
174
45
  </CommandList>
175
46
  <div className="flex items-center gap-4 border-t mt-2 px-4 py-2.5 text-sm text-muted-foreground">
176
47
  <span className="flex items-center gap-1.5">
@@ -195,48 +66,3 @@ export function SearchDialog(props: {
195
66
  </CommandDialog>
196
67
  )
197
68
  }
198
-
199
- function groupFor(pathname: string) {
200
- const segments = pathname.replace(/^\/|\/$/g, "").split("/")
201
- if (segments.length <= 2) return "Pages"
202
- return segments
203
- .slice(1, -1)
204
- .map(s =>
205
- s
206
- .split("-")
207
- .map(w => w.charAt(0).toUpperCase() + w.slice(1))
208
- .join(" "),
209
- )
210
- .join(" › ")
211
- }
212
-
213
- function extractSnippet(text: string, term: string) {
214
- const lower = text.toLowerCase()
215
- const idx = lower.indexOf(term.toLowerCase())
216
- if (idx === -1) return text.slice(0, 120)
217
- const start = Math.max(0, idx - 40)
218
- const end = Math.min(text.length, idx + term.length + 80)
219
- const prefix = start > 0 ? "…" : ""
220
- const suffix = end < text.length ? "…" : ""
221
- return `${prefix}${text.slice(start, end)}${suffix}`
222
- }
223
-
224
- function HighlightedText(props: { text: string; term: string }) {
225
- if (!props.term.trim()) return <>{props.text}</>
226
- const escaped = props.term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
227
- const regex = new RegExp(`(${escaped})`, "gi")
228
- const parts = props.text.split(regex)
229
- return (
230
- <>
231
- {parts.map((part, i) =>
232
- regex.test(part) ? (
233
- <span key={i} className="text-foreground font-medium">
234
- {part}
235
- </span>
236
- ) : (
237
- part
238
- ),
239
- )}
240
- </>
241
- )
242
- }
@@ -0,0 +1,69 @@
1
+ import { XIcon } from "lucide-react"
2
+ import { useRef, type ReactNode } from "react"
3
+ import { cn } from "cn"
4
+ import { Button } from "../elements/button.tsx"
5
+ import {
6
+ Drawer,
7
+ DrawerClose,
8
+ DrawerContent,
9
+ DrawerHeader,
10
+ DrawerTitle,
11
+ } from "../elements/drawer.tsx"
12
+
13
+ /** A phone's bottom sheet: title left, close right, a scrolling body and an
14
+ * optional pinned footer. */
15
+ export function SheetPanel(props: {
16
+ open: boolean
17
+ onOpenChange: (open: boolean) => void
18
+ onOpenChangeComplete?: (open: boolean) => void
19
+ title: ReactNode
20
+ footer?: ReactNode
21
+ bodyClassName?: string
22
+ children: ReactNode
23
+ }) {
24
+ const panelRef = useRef<HTMLDivElement>(null)
25
+
26
+ return (
27
+ <Drawer
28
+ open={props.open}
29
+ onOpenChange={open => props.onOpenChange(open)}
30
+ onOpenChangeComplete={props.onOpenChangeComplete}
31
+ >
32
+ <DrawerContent
33
+ // The body, never its first link: focusing that scrolls the list to it
34
+ // and paints a focus ring the tap never asked for.
35
+ initialFocus={panelRef}
36
+ showCloseButton={false}
37
+ className="max-h-[85dvh] gap-0"
38
+ >
39
+ <DrawerHeader className="shrink-0 flex-row items-center gap-2 pb-2">
40
+ <DrawerTitle className="line-clamp-1 text-lg font-semibold">
41
+ {props.title}
42
+ </DrawerTitle>
43
+ <DrawerClose
44
+ render={<Button variant="ghost" size="icon-sm" className="-mr-2 ml-auto" />}
45
+ >
46
+ <XIcon className="size-5" />
47
+ <span className="sr-only">Close</span>
48
+ </DrawerClose>
49
+ </DrawerHeader>
50
+ <div
51
+ ref={panelRef}
52
+ tabIndex={-1}
53
+ className={cn(
54
+ "min-h-0 flex-1 overflow-y-auto px-4 pt-2 text-base outline-none empty:hidden",
55
+ props.footer ? "pb-2" : "pb-[max(1.5rem,env(safe-area-inset-bottom))]",
56
+ props.bodyClassName,
57
+ )}
58
+ >
59
+ {props.children}
60
+ </div>
61
+ {props.footer && (
62
+ <div className="shrink-0 px-4 py-4 pb-[max(1rem,env(safe-area-inset-bottom))]">
63
+ {props.footer}
64
+ </div>
65
+ )}
66
+ </DrawerContent>
67
+ </Drawer>
68
+ )
69
+ }