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.
- package/README.md +3 -2
- package/build/actions/config/define.d.ts +6 -0
- package/build/actions/config/define.unit.js +8 -1
- package/build/actions/escape/copy.d.ts +11 -0
- package/build/actions/escape/copy.js +29 -0
- package/build/actions/escape/copy.unit.d.ts +1 -0
- package/build/actions/escape/copy.unit.js +37 -0
- package/build/actions/escape/list.d.ts +12 -0
- package/build/actions/escape/list.js +27 -0
- package/build/actions/escape/list.unit.d.ts +1 -0
- package/build/actions/escape/list.unit.js +36 -0
- package/build/actions/target/build.d.ts +9 -26
- package/build/actions/target/build.js +50 -71
- package/build/actions/target/build.unit.d.ts +1 -0
- package/build/actions/target/build.unit.js +76 -0
- package/build/actions/target/clear.d.ts +4 -0
- package/build/actions/target/clear.js +14 -0
- package/build/actions/target/clear.unit.d.ts +1 -0
- package/build/actions/target/clear.unit.js +23 -0
- package/build/commands/build.js +1 -2
- package/build/commands/clear.d.ts +5 -0
- package/build/commands/clear.js +15 -0
- package/build/commands/escape.d.ts +4 -3
- package/build/commands/escape.js +45 -48
- package/build/commands/preview.js +1 -2
- package/build/commands/start.js +1 -2
- package/build/components/Abbr.d.ts +1 -1
- package/build/components/Abbr.js +2 -4
- package/build/components/AnsiCode.d.ts +1 -1
- package/build/components/AnsiCode.js +2 -2
- package/build/components/Article.d.ts +2 -1
- package/build/components/Article.js +7 -14
- package/build/components/BackToTop.d.ts +1 -1
- package/build/components/Banner.d.ts +2 -2
- package/build/components/Banner.js +7 -3
- package/build/components/BlogIndex.d.ts +1 -1
- package/build/components/BlogIndex.js +2 -11
- package/build/components/Breadcrumbs.d.ts +1 -1
- package/build/components/Breadcrumbs.js +2 -3
- package/build/components/Callout.d.ts +1 -1
- package/build/components/Callout.js +5 -9
- package/build/components/Card.d.ts +1 -1
- package/build/components/Card.js +2 -3
- package/build/components/Cards.d.ts +1 -1
- package/build/components/ChangelogIndex.d.ts +1 -1
- package/build/components/ChangelogIndex.js +2 -11
- package/build/components/ChoiceList.d.ts +15 -0
- package/build/components/ChoiceList.js +13 -0
- package/build/components/CodeBlock.d.ts +1 -1
- package/build/components/CodeBlock.js +3 -16
- package/build/components/CodeTabs.d.ts +1 -1
- package/build/components/CodeTabs.js +2 -3
- package/build/components/Column.d.ts +1 -1
- package/build/components/Columns.d.ts +1 -1
- package/build/components/Content.d.ts +1 -1
- package/build/components/ContentTab.d.ts +1 -1
- package/build/components/ContentTabs.d.ts +1 -1
- package/build/components/ContentTabs.js +2 -3
- package/build/components/DefaultCatchBoundary.d.ts +1 -1
- package/build/components/DefaultCatchBoundary.js +5 -7
- package/build/components/DefinitionList.d.ts +3 -3
- package/build/components/Details.d.ts +1 -1
- package/build/components/Details.js +2 -5
- package/build/components/FileTree.d.ts +1 -1
- package/build/components/FileTree.js +3 -5
- package/build/components/Footer.d.ts +1 -1
- package/build/components/Footer.js +2 -5
- package/build/components/FootnoteRef.d.ts +1 -1
- package/build/components/FootnoteRef.js +2 -4
- package/build/components/Header.d.ts +6 -0
- package/build/components/Header.js +26 -0
- package/build/components/HeaderMenu.d.ts +2 -0
- package/build/components/HeaderMenu.js +27 -0
- package/build/components/HeaderNav.d.ts +6 -0
- package/build/components/HeaderNav.js +74 -0
- package/build/components/HeaderSearch.d.ts +4 -0
- package/build/components/HeaderSearch.js +33 -0
- package/build/components/Heading.d.ts +6 -6
- package/build/components/InlineBadge.d.ts +2 -2
- package/build/components/InlineBadge.js +1 -1
- package/build/components/InlineIcon.d.ts +1 -1
- package/build/components/InlineToc.d.ts +1 -1
- package/build/components/InlineToc.js +2 -4
- package/build/components/Layout.d.ts +2 -1
- package/build/components/Layout.js +5 -58
- package/build/components/LinkButton.d.ts +2 -2
- package/build/components/LinkButton.js +1 -1
- package/build/components/MenuSheet.d.ts +22 -0
- package/build/components/MenuSheet.js +27 -0
- package/build/components/Mermaid.d.ts +1 -1
- package/build/components/MermaidRenderer.d.ts +1 -1
- package/build/components/MermaidRenderer.js +20 -8
- package/build/components/NotFound.d.ts +1 -1
- package/build/components/NotFound.js +3 -6
- package/build/components/PackageTabs.d.ts +1 -1
- package/build/components/PackageTabs.js +3 -7
- package/build/components/PageToolbar.d.ts +1 -1
- package/build/components/PageToolbar.js +2 -10
- package/build/components/PrevNext.d.ts +1 -1
- package/build/components/PrevNext.js +2 -10
- package/build/components/Root.d.ts +4 -0
- package/build/components/Root.js +8 -0
- package/build/components/Search.d.ts +5 -2
- package/build/components/Search.js +11 -9
- package/build/components/SearchDialog.d.ts +1 -1
- package/build/components/SearchDialog.js +10 -133
- package/build/components/Section.d.ts +1 -1
- package/build/components/SectionHeader.d.ts +1 -1
- package/build/components/SheetPanel.d.ts +12 -0
- package/build/components/SheetPanel.js +19 -0
- package/build/components/Sidebar.d.ts +1 -1
- package/build/components/Sidebar.js +12 -14
- package/build/components/SidebarArticles.d.ts +4 -1
- package/build/components/SidebarArticles.js +5 -9
- package/build/components/SidebarLinks.d.ts +4 -1
- package/build/components/SidebarLinks.js +6 -9
- package/build/components/SidebarPosts.d.ts +4 -1
- package/build/components/SidebarPosts.js +7 -23
- package/build/components/SidebarReleases.d.ts +4 -1
- package/build/components/SidebarReleases.js +5 -15
- package/build/components/SidebarSectionPicker.d.ts +8 -0
- package/build/components/SidebarSectionPicker.js +31 -0
- package/build/components/SidebarSections.d.ts +1 -1
- package/build/components/SidebarSections.js +5 -9
- package/build/components/SiteLink.d.ts +1 -1
- package/build/components/SiteTitle.d.ts +1 -1
- package/build/components/SiteTitle.js +2 -6
- package/build/components/SortableTable.d.ts +1 -1
- package/build/components/SoundCloud.d.ts +1 -1
- package/build/components/SoundCloud.js +2 -2
- package/build/components/TagIndex.d.ts +1 -1
- package/build/components/TagIndex.js +2 -11
- package/build/components/Theme.d.ts +2 -1
- package/build/components/Theme.js +35 -15
- package/build/components/Toc.d.ts +5 -4
- package/build/components/Toc.js +35 -35
- package/build/components/YouTube.d.ts +1 -1
- package/build/components/YouTube.js +2 -2
- package/build/components/ZoomImage.d.ts +1 -1
- package/build/content-collections.js +5 -1
- package/build/elements/accordion.d.ts +4 -4
- package/build/elements/accordion.js +2 -3
- package/build/elements/alert-dialog.d.ts +12 -12
- package/build/elements/alert-dialog.js +2 -4
- package/build/elements/alert.d.ts +4 -4
- package/build/elements/alert.js +2 -2
- package/build/elements/aspect-ratio.d.ts +1 -1
- package/build/elements/avatar.d.ts +6 -6
- package/build/elements/badge.d.ts +1 -1
- package/build/elements/badge.js +3 -1
- package/build/elements/breadcrumb.d.ts +6 -6
- package/build/elements/breadcrumb.js +2 -4
- package/build/elements/button-group.d.ts +2 -2
- package/build/elements/button-group.js +5 -5
- package/build/elements/button.d.ts +2 -2
- package/build/elements/button.js +9 -7
- package/build/elements/calendar.d.ts +2 -2
- package/build/elements/calendar.js +2 -2
- package/build/elements/card.d.ts +7 -7
- package/build/elements/card.js +4 -4
- package/build/elements/carousel.d.ts +5 -5
- package/build/elements/carousel.js +3 -7
- package/build/elements/chart.d.ts +6 -6
- package/build/elements/chart.js +5 -11
- package/build/elements/checkbox.d.ts +1 -1
- package/build/elements/collapsible.d.ts +3 -3
- package/build/elements/command.d.ts +9 -9
- package/build/elements/command.js +5 -11
- package/build/elements/context-menu.d.ts +15 -15
- package/build/elements/context-menu.js +6 -9
- package/build/elements/dialog.d.ts +10 -10
- package/build/elements/dialog.js +2 -6
- package/build/elements/drawer.d.ts +14 -12
- package/build/elements/drawer.js +14 -15
- package/build/elements/dropdown-menu.d.ts +15 -15
- package/build/elements/dropdown-menu.js +7 -10
- package/build/elements/empty.d.ts +6 -6
- package/build/elements/empty.js +2 -2
- package/build/elements/field.d.ts +10 -10
- package/build/elements/field.js +3 -4
- package/build/elements/hover-card.d.ts +3 -3
- package/build/elements/hover-card.js +2 -2
- package/build/elements/input-group.d.ts +7 -7
- package/build/elements/input-group.js +5 -5
- package/build/elements/input-otp.d.ts +4 -4
- package/build/elements/input.d.ts +1 -1
- package/build/elements/input.js +2 -2
- package/build/elements/item.d.ts +9 -9
- package/build/elements/item.js +2 -2
- package/build/elements/kbd.d.ts +2 -2
- package/build/elements/label.d.ts +1 -1
- package/build/elements/menubar.d.ts +16 -16
- package/build/elements/menubar.js +9 -11
- package/build/elements/native-select.d.ts +3 -3
- package/build/elements/native-select.js +2 -4
- package/build/elements/navigation-menu.d.ts +8 -8
- package/build/elements/navigation-menu.js +3 -5
- package/build/elements/pagination.d.ts +7 -7
- package/build/elements/pagination.js +4 -10
- package/build/elements/popover.d.ts +6 -6
- package/build/elements/popover.js +2 -2
- package/build/elements/progress.d.ts +5 -5
- package/build/elements/progress.js +2 -3
- package/build/elements/radio-group.d.ts +2 -2
- package/build/elements/resizable.d.ts +3 -3
- package/build/elements/scroll-area.d.ts +2 -2
- package/build/elements/scroll-area.js +2 -4
- package/build/elements/select.d.ts +9 -9
- package/build/elements/select.js +4 -9
- package/build/elements/separator.d.ts +1 -1
- package/build/elements/sheet.d.ts +8 -8
- package/build/elements/sheet.js +2 -6
- package/build/elements/sidebar.d.ts +19 -19
- package/build/elements/sidebar.js +15 -37
- package/build/elements/skeleton.d.ts +1 -1
- package/build/elements/slider.d.ts +2 -1
- package/build/elements/slider.js +2 -3
- package/build/elements/sonner.d.ts +1 -1
- package/build/elements/spinner.d.ts +1 -1
- package/build/elements/switch.d.ts +1 -1
- package/build/elements/table.d.ts +8 -8
- package/build/elements/tabs.d.ts +4 -4
- package/build/elements/tabs.js +3 -3
- package/build/elements/textarea.d.ts +1 -1
- package/build/elements/textarea.js +2 -2
- package/build/elements/toggle-group.d.ts +3 -2
- package/build/elements/toggle-group.js +3 -3
- package/build/elements/toggle.d.ts +1 -1
- package/build/elements/toggle.js +2 -2
- package/build/elements/tooltip.d.ts +4 -4
- package/build/elements/tooltip.js +2 -3
- package/build/head.d.ts +4 -0
- package/build/head.js +6 -0
- package/build/helpers/article-search.d.ts +19 -0
- package/build/helpers/article-search.js +134 -0
- package/build/helpers/dependency-dir.d.ts +5 -0
- package/build/helpers/dependency-dir.js +20 -0
- package/build/helpers/dependency-dir.unit.d.ts +1 -0
- package/build/helpers/dependency-dir.unit.js +25 -0
- package/build/helpers/dynamic-icon-resolver.d.ts +1 -1
- package/build/helpers/dynamic-icon.d.ts +1 -1
- package/build/helpers/head.d.ts +53 -0
- package/build/helpers/head.js +69 -0
- package/build/helpers/head.unit.d.ts +1 -0
- package/build/helpers/head.unit.js +108 -0
- package/build/helpers/nav-items.d.ts +11 -0
- package/build/helpers/nav-items.js +65 -0
- package/build/helpers/render-inline-markdown.d.ts +1 -0
- package/build/helpers/render-inline-markdown.js +34 -0
- package/build/helpers/render-inline-markdown.unit.d.ts +1 -0
- package/build/helpers/render-inline-markdown.unit.js +20 -0
- package/build/helpers/search-highlight.d.ts +8 -0
- package/build/helpers/search-highlight.js +59 -0
- package/build/helpers/search-highlight.unit.d.ts +1 -0
- package/build/helpers/search-highlight.unit.js +36 -0
- package/build/helpers/target-export.d.ts +9 -0
- package/build/helpers/target-export.js +24 -0
- package/build/helpers/target-export.unit.d.ts +1 -0
- package/build/helpers/target-export.unit.js +25 -0
- package/build/helpers/theme.d.ts +6 -2
- package/build/helpers/theme.js +40 -7
- package/build/helpers/theme.unit.d.ts +1 -0
- package/build/helpers/theme.unit.js +30 -0
- package/build/hooks/hydrated.d.ts +2 -0
- package/build/hooks/hydrated.js +15 -0
- package/build/index.d.ts +1 -0
- package/build/index.js +1 -1
- package/build/models/config.d.ts +32 -6
- package/build/models/config.js +10 -1
- package/build/models/section.d.ts +4 -2
- package/build/models/section.js +1 -1
- package/build/plugins/vite-livemark.d.ts +4 -5
- package/build/plugins/vite-livemark.js +56 -90
- package/build/program.js +4 -2
- package/build/routes/$.d.ts +4 -1
- package/build/routes/$.js +20 -9
- package/build/routes/__root.js +29 -63
- package/build/settings.js +2 -1
- package/package.json +68 -66
- package/{.agents/skills → skills}/livemark-authoring/SKILL.md +2 -2
- package/{.agents/skills → skills}/livemark-configuration/SKILL.md +12 -5
- package/skills/livemark-customization/SKILL.md +145 -0
- package/source/actions/config/define.unit.ts +9 -0
- package/source/actions/escape/copy.ts +35 -0
- package/source/actions/escape/copy.unit.ts +51 -0
- package/source/actions/escape/list.ts +30 -0
- package/source/actions/escape/list.unit.ts +42 -0
- package/source/actions/target/build.ts +49 -69
- package/source/actions/target/build.unit.ts +95 -0
- package/source/actions/target/clear.ts +14 -0
- package/source/actions/target/clear.unit.ts +25 -0
- package/source/commands/build.ts +0 -1
- package/source/commands/clear.ts +15 -0
- package/source/commands/escape.ts +42 -59
- package/source/commands/preview.ts +0 -1
- package/source/commands/start.ts +0 -1
- package/source/components/AnsiCode.tsx +1 -1
- package/source/components/Article.tsx +8 -4
- package/source/components/Banner.tsx +12 -2
- package/source/components/BlogIndex.tsx +1 -1
- package/source/components/Callout.tsx +4 -6
- package/source/components/ChangelogIndex.tsx +1 -1
- package/source/components/ChoiceList.tsx +69 -0
- package/source/components/CodeBlock.tsx +6 -6
- package/source/components/CodeTabs.tsx +1 -1
- package/source/components/ContentTabs.tsx +1 -1
- package/source/components/DefaultCatchBoundary.tsx +4 -6
- package/source/components/Details.tsx +1 -1
- package/source/components/FileTree.tsx +1 -1
- package/source/components/Header.tsx +52 -0
- package/source/components/HeaderMenu.tsx +41 -0
- package/source/components/HeaderNav.tsx +145 -0
- package/source/components/HeaderSearch.tsx +77 -0
- package/source/components/InlineBadge.tsx +1 -1
- package/source/components/InlineToc.tsx +1 -1
- package/source/components/Layout.tsx +16 -160
- package/source/components/LinkButton.tsx +9 -1
- package/source/components/MenuSheet.tsx +92 -0
- package/source/components/MermaidRenderer.tsx +19 -8
- package/source/components/NotFound.tsx +3 -5
- package/source/components/PackageTabs.tsx +2 -2
- package/source/components/PageToolbar.tsx +11 -3
- package/source/components/PrevNext.tsx +2 -2
- package/source/components/Root.tsx +13 -0
- package/source/components/Search.tsx +28 -8
- package/source/components/SearchDialog.tsx +15 -189
- package/source/components/SheetPanel.tsx +69 -0
- package/source/components/Sidebar.tsx +27 -14
- package/source/components/SidebarArticles.tsx +4 -3
- package/source/components/SidebarLinks.tsx +3 -2
- package/source/components/SidebarPosts.tsx +3 -2
- package/source/components/SidebarReleases.tsx +3 -2
- package/source/components/SidebarSectionPicker.tsx +70 -0
- package/source/components/SiteTitle.tsx +5 -5
- package/source/components/SoundCloud.tsx +1 -1
- package/source/components/TagIndex.tsx +1 -1
- package/source/components/Theme.tsx +55 -29
- package/source/components/Toc.tsx +76 -55
- package/source/components/YouTube.tsx +1 -1
- package/source/content-collections.ts +4 -0
- package/source/elements/alert-dialog.tsx +1 -1
- package/source/elements/alert.tsx +1 -1
- package/source/elements/badge.tsx +4 -0
- package/source/elements/button-group.tsx +4 -4
- package/source/elements/button.tsx +10 -6
- package/source/elements/calendar.tsx +1 -1
- package/source/elements/card.tsx +3 -3
- package/source/elements/command.tsx +7 -4
- package/source/elements/context-menu.tsx +5 -5
- package/source/elements/dialog.tsx +1 -1
- package/source/elements/drawer.tsx +59 -43
- package/source/elements/dropdown-menu.tsx +6 -6
- package/source/elements/empty.tsx +1 -1
- package/source/elements/field.tsx +1 -1
- package/source/elements/hover-card.tsx +1 -1
- package/source/elements/input-group.tsx +4 -4
- package/source/elements/input.tsx +1 -1
- package/source/elements/item.tsx +1 -1
- package/source/elements/menubar.tsx +8 -8
- package/source/elements/native-select.tsx +1 -1
- package/source/elements/popover.tsx +1 -1
- package/source/elements/select.tsx +3 -3
- package/source/elements/sidebar.tsx +8 -21
- package/source/elements/tabs.tsx +2 -2
- package/source/elements/textarea.tsx +1 -1
- package/source/elements/toggle-group.tsx +2 -2
- package/source/elements/toggle.tsx +1 -1
- package/source/env.d.ts +1 -0
- package/source/head.ts +7 -0
- package/source/helpers/article-search.tsx +192 -0
- package/source/helpers/dependency-dir.ts +20 -0
- package/source/helpers/dependency-dir.unit.ts +39 -0
- package/source/helpers/head.ts +94 -0
- package/source/helpers/head.unit.ts +116 -0
- package/source/helpers/nav-items.tsx +102 -0
- package/source/helpers/render-inline-markdown.ts +44 -0
- package/source/helpers/render-inline-markdown.unit.ts +30 -0
- package/source/helpers/search-highlight.ts +73 -0
- package/source/helpers/search-highlight.unit.ts +42 -0
- package/source/helpers/target-export.ts +28 -0
- package/source/helpers/target-export.unit.ts +37 -0
- package/source/helpers/theme.ts +40 -6
- package/source/helpers/theme.unit.ts +32 -0
- package/source/hooks/hydrated.ts +18 -0
- package/source/index.ts +1 -0
- package/source/models/config.ts +13 -0
- package/source/models/section.ts +3 -0
- package/source/plugins/vite-livemark.ts +54 -95
- package/source/program.ts +2 -0
- package/source/routes/$.tsx +27 -9
- package/source/routes/__root.tsx +51 -71
- package/source/settings.ts +1 -0
- package/source/styles/general.css +59 -11
- package/source/styles/markdown.css +13 -7
- package/source/styles/shape.css +14 -0
- package/.agents/skills/change-review/SKILL.md +0 -47
- 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
|
-
"
|
|
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
|
-
? "
|
|
227
|
-
: "
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
},
|
package/source/elements/tabs.tsx
CHANGED
|
@@ -21,7 +21,7 @@ function Tabs({
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const tabsListVariants = cva(
|
|
24
|
-
"rounded-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
package/source/head.ts
ADDED
|
@@ -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
|
+
})
|