@livemark/markdown 0.0.0-stage → 0.44.2

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 (184) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +5 -2
  3. package/build/actions/markdown/compile.d.ts +27 -0
  4. package/build/actions/markdown/compile.js +48 -0
  5. package/build/actions/processor/create.d.ts +17 -0
  6. package/build/actions/processor/create.js +212 -0
  7. package/build/components/Abbr.d.ts +5 -0
  8. package/build/components/Abbr.js +7 -0
  9. package/build/components/AnsiCode.d.ts +4 -0
  10. package/build/components/AnsiCode.js +7 -0
  11. package/build/components/Audio.d.ts +4 -0
  12. package/build/components/Audio.js +9 -0
  13. package/build/components/Badge.d.ts +5 -0
  14. package/build/components/Badge.js +7 -0
  15. package/build/components/Button.d.ts +7 -0
  16. package/build/components/Button.js +11 -0
  17. package/build/components/Callout.d.ts +8 -0
  18. package/build/components/Callout.js +41 -0
  19. package/build/components/Card.d.ts +7 -0
  20. package/build/components/Card.js +16 -0
  21. package/build/components/Cards.d.ts +4 -0
  22. package/build/components/Cards.js +6 -0
  23. package/build/components/CodeBlock.d.ts +1 -0
  24. package/build/components/CodeBlock.js +50 -0
  25. package/build/components/CodeTabs.d.ts +6 -0
  26. package/build/components/CodeTabs.js +48 -0
  27. package/build/components/Column.d.ts +4 -0
  28. package/build/components/Column.js +6 -0
  29. package/build/components/Columns.d.ts +5 -0
  30. package/build/components/Columns.js +8 -0
  31. package/build/components/DefinitionList.d.ts +7 -0
  32. package/build/components/DefinitionList.js +14 -0
  33. package/build/components/Details.d.ts +5 -0
  34. package/build/components/Details.js +10 -0
  35. package/build/components/Filetree.d.ts +4 -0
  36. package/build/components/Filetree.js +22 -0
  37. package/build/components/FootnoteRef.d.ts +4 -0
  38. package/build/components/FootnoteRef.js +33 -0
  39. package/build/components/Heading.d.ts +8 -0
  40. package/build/components/Heading.js +18 -0
  41. package/build/components/Icon.d.ts +5 -0
  42. package/build/components/Icon.js +10 -0
  43. package/build/components/Markdown.d.ts +13 -0
  44. package/build/components/Markdown.js +75 -0
  45. package/build/components/Mermaid.d.ts +4 -0
  46. package/build/components/Mermaid.js +10 -0
  47. package/build/components/MermaidRenderer.d.ts +4 -0
  48. package/build/components/MermaidRenderer.js +44 -0
  49. package/build/components/PackageTabs.d.ts +9 -0
  50. package/build/components/PackageTabs.js +26 -0
  51. package/build/components/SortableTable.d.ts +5 -0
  52. package/build/components/SortableTable.js +63 -0
  53. package/build/components/Tab.d.ts +5 -0
  54. package/build/components/Tab.js +6 -0
  55. package/build/components/Tabs.d.ts +5 -0
  56. package/build/components/Tabs.js +46 -0
  57. package/build/components/Toc.d.ts +4 -0
  58. package/build/components/Toc.js +14 -0
  59. package/build/components/Video.d.ts +4 -0
  60. package/build/components/Video.js +13 -0
  61. package/build/components/ZoomImage.d.ts +4 -0
  62. package/build/components/ZoomImage.js +26 -0
  63. package/build/elements/badge.d.ts +7 -0
  64. package/build/elements/badge.js +36 -0
  65. package/build/elements/button.d.ts +8 -0
  66. package/build/elements/button.js +37 -0
  67. package/build/elements/card.d.ts +11 -0
  68. package/build/elements/card.js +25 -0
  69. package/build/elements/collapsible.d.ts +5 -0
  70. package/build/elements/collapsible.js +13 -0
  71. package/build/elements/hover-card.d.ts +5 -0
  72. package/build/elements/hover-card.js +14 -0
  73. package/build/elements/separator.d.ts +3 -0
  74. package/build/elements/separator.js +8 -0
  75. package/build/elements/table.d.ts +9 -0
  76. package/build/elements/table.js +28 -0
  77. package/build/elements/tooltip.d.ts +6 -0
  78. package/build/elements/tooltip.js +17 -0
  79. package/build/helpers/ansi.d.ts +3 -0
  80. package/build/helpers/ansi.js +208 -0
  81. package/build/helpers/article-link.d.ts +7 -0
  82. package/build/helpers/article-link.js +8 -0
  83. package/build/helpers/code-icon.d.ts +2 -0
  84. package/build/helpers/code-icon.js +57 -0
  85. package/build/helpers/content-warnings.d.ts +8 -0
  86. package/build/helpers/content-warnings.js +47 -0
  87. package/build/helpers/dynamic-icon-resolver.d.ts +5 -0
  88. package/build/helpers/dynamic-icon-resolver.js +11 -0
  89. package/build/helpers/dynamic-icon.d.ts +5 -0
  90. package/build/helpers/dynamic-icon.js +14 -0
  91. package/build/helpers/frontmatter.d.ts +5 -0
  92. package/build/helpers/frontmatter.js +16 -0
  93. package/build/helpers/icon-names.d.ts +11 -0
  94. package/build/helpers/icon-names.js +35 -0
  95. package/build/helpers/message-format.d.ts +2 -0
  96. package/build/helpers/message-format.js +7 -0
  97. package/build/helpers/node-text.d.ts +9 -0
  98. package/build/helpers/node-text.js +9 -0
  99. package/build/helpers/site-messages.d.ts +86 -0
  100. package/build/helpers/site-messages.js +7 -0
  101. package/build/helpers/toc-context.d.ts +5 -0
  102. package/build/helpers/toc-context.js +8 -0
  103. package/build/helpers/toc-outline.d.ts +3 -0
  104. package/build/helpers/toc-outline.js +16 -0
  105. package/build/helpers/youtube-id.d.ts +2 -0
  106. package/build/helpers/youtube-id.js +25 -0
  107. package/build/hooks/icons.d.ts +13 -0
  108. package/build/hooks/icons.js +12 -0
  109. package/build/hooks/messages.d.ts +95 -0
  110. package/build/hooks/messages.js +17 -0
  111. package/build/models/message.d.ts +177 -0
  112. package/build/models/message.js +88 -0
  113. package/build/models/plugins.d.ts +10 -0
  114. package/build/models/plugins.js +6 -0
  115. package/build/models/theme.d.ts +76 -0
  116. package/build/models/theme.js +71 -0
  117. package/build/models/toc.d.ts +5 -0
  118. package/build/models/toc.js +2 -0
  119. package/build/plugins/twoslash.d.ts +13 -0
  120. package/build/plugins/twoslash.js +146 -0
  121. package/package.json +63 -3
  122. package/source/actions/markdown/compile.ts +82 -0
  123. package/source/actions/processor/create.ts +235 -0
  124. package/source/components/Abbr.tsx +13 -0
  125. package/source/components/AnsiCode.tsx +17 -0
  126. package/source/components/Audio.tsx +18 -0
  127. package/source/components/Badge.tsx +9 -0
  128. package/source/components/Button.tsx +33 -0
  129. package/source/components/Callout.tsx +68 -0
  130. package/source/components/Card.tsx +48 -0
  131. package/source/components/Cards.tsx +8 -0
  132. package/source/components/CodeBlock.tsx +144 -0
  133. package/source/components/CodeTabs.tsx +81 -0
  134. package/source/components/Column.tsx +4 -0
  135. package/source/components/Columns.tsx +10 -0
  136. package/source/components/DefinitionList.tsx +18 -0
  137. package/source/components/Details.tsx +30 -0
  138. package/source/components/Filetree.tsx +63 -0
  139. package/source/components/FootnoteRef.tsx +52 -0
  140. package/source/components/Heading.tsx +20 -0
  141. package/source/components/Icon.tsx +10 -0
  142. package/source/components/Markdown.tsx +89 -0
  143. package/source/components/Mermaid.tsx +20 -0
  144. package/source/components/MermaidRenderer.tsx +48 -0
  145. package/source/components/PackageTabs.tsx +72 -0
  146. package/source/components/SortableTable.tsx +81 -0
  147. package/source/components/Tab.tsx +4 -0
  148. package/source/components/Tabs.tsx +75 -0
  149. package/source/components/Toc.tsx +30 -0
  150. package/source/components/Video.tsx +28 -0
  151. package/source/components/ZoomImage.tsx +25 -0
  152. package/source/elements/badge.tsx +53 -0
  153. package/source/elements/button.tsx +61 -0
  154. package/source/elements/card.tsx +102 -0
  155. package/source/elements/collapsible.tsx +15 -0
  156. package/source/elements/hover-card.tsx +47 -0
  157. package/source/elements/separator.tsx +22 -0
  158. package/source/elements/table.tsx +103 -0
  159. package/source/elements/tooltip.tsx +60 -0
  160. package/source/helpers/ansi.ts +210 -0
  161. package/source/helpers/article-link.ts +11 -0
  162. package/source/helpers/code-icon.ts +59 -0
  163. package/source/helpers/content-warnings.ts +56 -0
  164. package/source/helpers/dynamic-icon-resolver.tsx +9 -0
  165. package/source/helpers/dynamic-icon.tsx +19 -0
  166. package/source/helpers/frontmatter.ts +16 -0
  167. package/source/helpers/icon-names.ts +37 -0
  168. package/source/helpers/message-format.ts +5 -0
  169. package/source/helpers/node-text.ts +14 -0
  170. package/source/helpers/site-messages.ts +8 -0
  171. package/source/helpers/toc-context.ts +10 -0
  172. package/source/helpers/toc-outline.ts +23 -0
  173. package/source/helpers/youtube-id.ts +23 -0
  174. package/source/hooks/icons.ts +18 -0
  175. package/source/hooks/messages.ts +28 -0
  176. package/source/models/message.ts +96 -0
  177. package/source/models/plugins.ts +11 -0
  178. package/source/models/theme.ts +78 -0
  179. package/source/models/toc.ts +5 -0
  180. package/source/plugins/twoslash.ts +163 -0
  181. package/source/standalone.css +7 -0
  182. package/source/styles/general.css +214 -0
  183. package/source/styles/markdown.css +231 -0
  184. package/source/styles/shape.css +14 -0
@@ -0,0 +1,10 @@
1
+ /** Grid layout for arranging content side by side */
2
+ export function Columns(props: { cols?: string; children: React.ReactNode }) {
3
+ const cols = props.cols ? parseInt(props.cols, 10) : undefined
4
+ const style = cols ? { gridTemplateColumns: `repeat(${cols}, 1fr)` } : undefined
5
+ return (
6
+ <div className="grid gap-6 my-4 sm:grid-cols-2" style={style}>
7
+ {props.children}
8
+ </div>
9
+ )
10
+ }
@@ -0,0 +1,18 @@
1
+ import type { ComponentProps } from "react"
2
+
3
+ /** Styled wrapper for definition lists produced by remark-definition-list */
4
+ export function DefinitionList(props: ComponentProps<"dl">) {
5
+ return <dl {...props} className="not-prose space-y-4" />
6
+ }
7
+
8
+ /** Styled definition term rendered as bold text */
9
+ export function DefinitionTerm(props: ComponentProps<"dt">) {
10
+ return <dt {...props} className="text-foreground font-semibold" />
11
+ }
12
+
13
+ /** Styled definition detail with a left border accent */
14
+ export function DefinitionDetail(props: ComponentProps<"dd">) {
15
+ return (
16
+ <dd {...props} className="ml-0 pl-4 border-l-2 border-border text-muted-foreground" />
17
+ )
18
+ }
@@ -0,0 +1,30 @@
1
+ import { useState } from "react"
2
+ import { ChevronRight } from "lucide-react"
3
+ import {
4
+ Collapsible,
5
+ CollapsibleTrigger,
6
+ CollapsibleContent,
7
+ } from "../elements/collapsible.tsx"
8
+
9
+ /** Collapsible content section with a summary label */
10
+ export function Details(props: { summary: string; children: React.ReactNode }) {
11
+ const [open, setOpen] = useState(false)
12
+
13
+ return (
14
+ <Collapsible
15
+ open={open}
16
+ onOpenChange={setOpen}
17
+ className="not-prose my-4 rounded-card border border-border bg-card px-4 py-3"
18
+ >
19
+ <CollapsibleTrigger className="flex w-full cursor-pointer items-center gap-2 text-sm font-semibold select-none">
20
+ <ChevronRight
21
+ className={`size-4 shrink-0 transition-transform ${open ? "rotate-90" : ""}`}
22
+ />
23
+ {props.summary}
24
+ </CollapsibleTrigger>
25
+ <CollapsibleContent className="mt-3 text-sm [&>p]:my-1 [&>p:first-child]:mt-0 [&>p:last-child]:mb-0">
26
+ {props.children}
27
+ </CollapsibleContent>
28
+ </Collapsible>
29
+ )
30
+ }
@@ -0,0 +1,63 @@
1
+ import { File, Folder } from "lucide-react"
2
+
3
+ interface TreeItem {
4
+ name: string
5
+ type: "directory" | "file" | "placeholder"
6
+ comment?: string
7
+ highlighted?: boolean
8
+ children?: TreeItem[]
9
+ }
10
+
11
+ /** Renders a file/directory tree from a JSON-serialized tree structure */
12
+ export function Filetree(props: { tree: string }) {
13
+ const items: TreeItem[] = JSON.parse(props.tree)
14
+
15
+ return (
16
+ <div className="not-prose my-4 rounded-card border border-border bg-card p-4 font-mono text-sm">
17
+ <TreeItems items={items} depth={0} />
18
+ </div>
19
+ )
20
+ }
21
+
22
+ function TreeItems(props: { items: TreeItem[]; depth: number }) {
23
+ return (
24
+ <ul className={props.depth > 0 ? "ml-4 border-l border-border pl-2" : ""}>
25
+ {props.items.map((item, index) => (
26
+ <TreeNode key={`${item.name}-${index}`} item={item} depth={props.depth} />
27
+ ))}
28
+ </ul>
29
+ )
30
+ }
31
+
32
+ function TreeNode(props: { item: TreeItem; depth: number }) {
33
+ const { item, depth } = props
34
+ const isDir = item.type === "directory"
35
+
36
+ return (
37
+ <li className="py-0.5">
38
+ <span className="inline-flex items-center gap-2">
39
+ {isDir && <Folder className="size-4 shrink-0 text-primary" />}
40
+ {item.type === "file" && (
41
+ <File className="size-4 shrink-0 text-muted-foreground" />
42
+ )}
43
+ <span
44
+ className={[
45
+ isDir && "font-semibold",
46
+ item.type === "placeholder" && "text-muted-foreground",
47
+ item.highlighted && "rounded bg-primary/10 px-1 text-primary",
48
+ ]
49
+ .filter(Boolean)
50
+ .join(" ")}
51
+ >
52
+ {item.name}
53
+ </span>
54
+ {item.comment && (
55
+ <span className="font-sans text-xs text-muted-foreground">{item.comment}</span>
56
+ )}
57
+ </span>
58
+ {item.children && item.children.length > 0 && (
59
+ <TreeItems items={item.children} depth={depth + 1} />
60
+ )}
61
+ </li>
62
+ )
63
+ }
@@ -0,0 +1,52 @@
1
+ import { type ComponentProps, useEffect, useState } from "react"
2
+ import { HoverCard, HoverCardContent, HoverCardTrigger } from "../elements/hover-card.tsx"
3
+ import { useArticleHref } from "../helpers/article-link.ts"
4
+ import { useMessages } from "../hooks/messages.ts"
5
+
6
+ const FOOTNOTE_PREFIX = "#user-content-fn-"
7
+
8
+ /** Wraps footnote reference links in a HoverCard that previews footnote content inline,
9
+ * and points relative links to Markdown files at their articles */
10
+ export function FootnoteRef(props: ComponentProps<"a">) {
11
+ const href = useArticleHref(props.href) ?? ""
12
+ if (!href.startsWith(FOOTNOTE_PREFIX)) {
13
+ return <a {...props} href={props.href && href} />
14
+ }
15
+
16
+ return <FootnoteHoverCard {...props} href={href} />
17
+ }
18
+
19
+ function FootnoteHoverCard(props: ComponentProps<"a"> & { href: string }) {
20
+ const [footnoteHtml, setFootnoteHtml] = useState("")
21
+ const t = useMessages()
22
+
23
+ useEffect(() => {
24
+ const targetId = props.href.slice(1)
25
+ const el = document.getElementById(targetId)
26
+ if (!el) return
27
+
28
+ const wrapper = document.createElement("div")
29
+ wrapper.innerHTML = el.innerHTML
30
+ for (const backref of wrapper.querySelectorAll("[data-footnote-backref]")) {
31
+ backref.remove()
32
+ }
33
+ setFootnoteHtml(wrapper.innerHTML)
34
+ }, [props.href])
35
+
36
+ return (
37
+ <HoverCard>
38
+ <HoverCardTrigger render={<a {...props} />} />
39
+ <HoverCardContent
40
+ className="prose dark:prose-invert text-sm w-80 [&>p]:my-1 [&>p:first-child]:mt-0 [&>p:last-child]:mb-0"
41
+ side="top"
42
+ sideOffset={8}
43
+ >
44
+ {footnoteHtml ? (
45
+ <div dangerouslySetInnerHTML={{ __html: footnoteHtml }} />
46
+ ) : (
47
+ <p className="text-muted-foreground">{t("footnote.missing")}</p>
48
+ )}
49
+ </HoverCardContent>
50
+ </HoverCard>
51
+ )
52
+ }
@@ -0,0 +1,20 @@
1
+ /** Heading component that wraps content in an anchor link to itself */
2
+ function createHeading(Tag: "h1" | "h2" | "h3" | "h4" | "h5" | "h6") {
3
+ return function Heading(props: React.HTMLAttributes<HTMLHeadingElement>) {
4
+ if (!props.id) return <Tag {...props} />
5
+ return (
6
+ <Tag {...props}>
7
+ <a href={`#${props.id}`}>{props.children}</a>
8
+ </Tag>
9
+ )
10
+ }
11
+ }
12
+
13
+ export const headingComponents = {
14
+ h1: createHeading("h1"),
15
+ h2: createHeading("h2"),
16
+ h3: createHeading("h3"),
17
+ h4: createHeading("h4"),
18
+ h5: createHeading("h5"),
19
+ h6: createHeading("h6"),
20
+ }
@@ -0,0 +1,10 @@
1
+ import { DynamicIcon } from "../helpers/dynamic-icon.tsx"
2
+
3
+ /** Renders a Lucide icon inline by name */
4
+ export function Icon(props: { name: string; className?: string }) {
5
+ const className = props.className
6
+ ? `inline-block size-4 align-text-bottom ${props.className}`
7
+ : "inline-block size-4 align-text-bottom"
8
+
9
+ return <DynamicIcon name={props.name} className={className} />
10
+ }
@@ -0,0 +1,89 @@
1
+ import type { ComponentType } from "react"
2
+ import { TocContext } from "../helpers/toc-context.ts"
3
+ import type { TocItem } from "../models/toc.ts"
4
+ import { Abbr } from "./Abbr.tsx"
5
+ import { AnsiCode } from "./AnsiCode.tsx"
6
+ import { Audio } from "./Audio.tsx"
7
+ import { Badge } from "./Badge.tsx"
8
+ import { Button } from "./Button.tsx"
9
+ import { Callout } from "./Callout.tsx"
10
+ import { Card } from "./Card.tsx"
11
+ import { Cards } from "./Cards.tsx"
12
+ import { CodeBlock } from "./CodeBlock.tsx"
13
+ import { CodeTabs } from "./CodeTabs.tsx"
14
+ import { Column } from "./Column.tsx"
15
+ import { Columns } from "./Columns.tsx"
16
+ import { DefinitionDetail, DefinitionList, DefinitionTerm } from "./DefinitionList.tsx"
17
+ import { Details } from "./Details.tsx"
18
+ import { Filetree } from "./Filetree.tsx"
19
+ import { FootnoteRef } from "./FootnoteRef.tsx"
20
+ import { headingComponents } from "./Heading.tsx"
21
+ import { Icon } from "./Icon.tsx"
22
+ import { Mermaid } from "./Mermaid.tsx"
23
+ import { PackageTabs } from "./PackageTabs.tsx"
24
+ import {
25
+ SortableTable,
26
+ TableBody,
27
+ TableCell,
28
+ TableHead,
29
+ TableHeader,
30
+ TableRow,
31
+ } from "./SortableTable.tsx"
32
+ import { Tab } from "./Tab.tsx"
33
+ import { Tabs } from "./Tabs.tsx"
34
+ import { Toc } from "./Toc.tsx"
35
+ import { Video } from "./Video.tsx"
36
+ import { ZoomImage } from "./ZoomImage.tsx"
37
+ import { Separator } from "../elements/separator.tsx"
38
+
39
+ type MarkdownComponents = Record<string, ComponentType<never>>
40
+
41
+ /** Renders compiled Markdown with Livemark's components for its HTML elements,
42
+ * directives and code blocks; `components` adds or replaces any of them. */
43
+ export function Markdown(props: {
44
+ component: ComponentType<{ components?: object }>
45
+ toc?: TocItem[]
46
+ components?: MarkdownComponents
47
+ }) {
48
+ const Content = props.component
49
+ const content = <Content components={{ ...COMPONENTS, ...props.components }} />
50
+ if (!props.toc) return content
51
+ return <TocContext.Provider value={props.toc}>{content}</TocContext.Provider>
52
+ }
53
+
54
+ const COMPONENTS = {
55
+ hr: Separator,
56
+ img: ZoomImage,
57
+ pre: CodeBlock,
58
+ table: SortableTable,
59
+ thead: TableHeader,
60
+ tbody: TableBody,
61
+ tr: TableRow,
62
+ th: TableHead,
63
+ td: TableCell,
64
+ a: FootnoteRef,
65
+ Abbr,
66
+ AnsiCode,
67
+ Audio,
68
+ Badge,
69
+ Button,
70
+ Callout,
71
+ Card,
72
+ Cards,
73
+ Column,
74
+ Columns,
75
+ CodeTabs,
76
+ DefinitionDetail,
77
+ DefinitionList,
78
+ DefinitionTerm,
79
+ Details,
80
+ Filetree,
81
+ Icon,
82
+ Mermaid,
83
+ PackageTabs,
84
+ Tab,
85
+ Tabs,
86
+ Toc,
87
+ Video,
88
+ ...headingComponents,
89
+ }
@@ -0,0 +1,20 @@
1
+ import { Suspense, lazy } from "react"
2
+ import { useMessages } from "../hooks/messages.ts"
3
+
4
+ const MermaidRenderer = lazy(() => import("./MermaidRenderer.tsx"))
5
+
6
+ /** Lazily loads and renders a Mermaid diagram */
7
+ export function Mermaid(props: { children: string }) {
8
+ const t = useMessages()
9
+ return (
10
+ <Suspense
11
+ fallback={
12
+ <div className="not-prose mermaid flex justify-center">
13
+ {t("media.loadingDiagram")}
14
+ </div>
15
+ }
16
+ >
17
+ <MermaidRenderer>{props.children}</MermaidRenderer>
18
+ </Suspense>
19
+ )
20
+ }
@@ -0,0 +1,48 @@
1
+ import { useEffect, useRef } from "react"
2
+ import mermaid from "mermaid"
3
+
4
+ mermaid.initialize({ startOnLoad: false })
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
+
10
+ /** Renders a Mermaid diagram from a chart definition string */
11
+ export default function MermaidRenderer(props: { children: string }) {
12
+ const ref = useRef<HTMLDivElement>(null)
13
+
14
+ useEffect(() => {
15
+ const el = ref.current
16
+ if (!el) return
17
+
18
+ const render = () => {
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
+ })
34
+ }
35
+
36
+ render()
37
+
38
+ const observer = new MutationObserver(render)
39
+ observer.observe(document.documentElement, {
40
+ attributes: true,
41
+ attributeFilter: ["class"],
42
+ })
43
+
44
+ return () => observer.disconnect()
45
+ }, [props.children])
46
+
47
+ return <div ref={ref} className="not-prose mermaid flex justify-center" />
48
+ }
@@ -0,0 +1,72 @@
1
+ import { Check, Clipboard } from "lucide-react"
2
+ import { useState } from "react"
3
+ import { useMessages } from "../hooks/messages.ts"
4
+
5
+ type Manager = "npm" | "pnpm" | "yarn" | "bun"
6
+
7
+ const managers: Manager[] = ["npm", "pnpm", "yarn", "bun"]
8
+
9
+ interface PackageTabsProps {
10
+ npm: string
11
+ pnpm: string
12
+ yarn: string
13
+ bun: string
14
+ }
15
+
16
+ /** Tabbed code block showing a command for each package manager */
17
+ export function PackageTabs(props: PackageTabsProps) {
18
+ const [active, setActive] = useState<Manager>("npm")
19
+ const [copied, setCopied] = useState(false)
20
+ const t = useMessages()
21
+
22
+ const commands: Record<Manager, string> = {
23
+ npm: props.npm,
24
+ pnpm: props.pnpm,
25
+ yarn: props.yarn,
26
+ bun: props.bun,
27
+ }
28
+
29
+ const handleCopy = () => {
30
+ navigator.clipboard.writeText(commands[active])
31
+ setCopied(true)
32
+ setTimeout(() => setCopied(false), 2000)
33
+ }
34
+
35
+ return (
36
+ <figure className="not-prose my-4 relative rounded-card border shadow-sm overflow-hidden text-sm bg-sidebar dark:bg-sidebar">
37
+ <div className="flex items-center border-b">
38
+ <div className="flex flex-1 items-center">
39
+ {managers.map(pm => (
40
+ <button
41
+ key={pm}
42
+ type="button"
43
+ onClick={() => setActive(pm)}
44
+ className={`px-3 py-2 text-xs font-medium transition-colors ${
45
+ pm === active
46
+ ? "text-foreground border-b-2 border-foreground"
47
+ : "text-muted-foreground hover:text-foreground"
48
+ }`}
49
+ >
50
+ {pm}
51
+ </button>
52
+ ))}
53
+ </div>
54
+ <button
55
+ type="button"
56
+ onClick={handleCopy}
57
+ className="me-2 p-1 rounded-control text-muted-foreground hover:text-foreground transition-colors"
58
+ aria-label={t("code.copy")}
59
+ >
60
+ {copied ? <Check className="size-3.5" /> : <Clipboard className="size-3.5" />}
61
+ </button>
62
+ </div>
63
+ <div className="text-[0.8125rem] py-3.5 overflow-auto max-h-[600px]">
64
+ <pre className="min-w-full w-max">
65
+ <code className="flex flex-col">
66
+ <span className="px-4">{commands[active]}</span>
67
+ </code>
68
+ </pre>
69
+ </div>
70
+ </figure>
71
+ )
72
+ }
@@ -0,0 +1,81 @@
1
+ import { useEffect, useRef, type ComponentProps } from "react"
2
+ import {
3
+ Table,
4
+ TableHeader,
5
+ TableBody,
6
+ TableRow,
7
+ TableHead,
8
+ TableCell,
9
+ } from "../elements/table.tsx"
10
+
11
+ /** Wraps markdown tables with click-to-sort column headers */
12
+ export function SortableTable(props: ComponentProps<"table">) {
13
+ const ref = useRef<HTMLDivElement>(null)
14
+
15
+ useEffect(() => {
16
+ const container = ref.current
17
+ if (!container) return
18
+
19
+ let activeCol = -1
20
+ let ascending = true
21
+
22
+ const headers = container.querySelectorAll<HTMLElement>("th")
23
+ const cleanups: (() => void)[] = []
24
+
25
+ headers.forEach((th, colIndex) => {
26
+ th.style.cursor = "pointer"
27
+ th.classList.add("select-none")
28
+
29
+ const handler = () => {
30
+ if (activeCol === colIndex) {
31
+ ascending = !ascending
32
+ } else {
33
+ activeCol = colIndex
34
+ ascending = true
35
+ }
36
+
37
+ const tbody = container.querySelector("tbody")
38
+ if (!tbody) return
39
+
40
+ const rows = Array.from(tbody.querySelectorAll("tr"))
41
+ rows.sort((a, b) => {
42
+ const aVal = a.children[colIndex]?.textContent?.trim() ?? ""
43
+ const bVal = b.children[colIndex]?.textContent?.trim() ?? ""
44
+ const aNum = Number(aVal)
45
+ const bNum = Number(bVal)
46
+ const cmp =
47
+ aVal !== "" && bVal !== "" && !isNaN(aNum) && !isNaN(bNum)
48
+ ? aNum - bNum
49
+ : aVal.localeCompare(bVal)
50
+ return ascending ? cmp : -cmp
51
+ })
52
+ for (const row of rows) tbody.appendChild(row)
53
+
54
+ headers.forEach((h, i) => {
55
+ const indicator = h.querySelector("[data-sort]")
56
+ if (indicator) indicator.remove()
57
+ if (i === colIndex) {
58
+ const span = document.createElement("span")
59
+ span.dataset.sort = ""
60
+ span.className = "ml-1 text-muted-foreground"
61
+ span.textContent = ascending ? "↑" : "↓"
62
+ h.appendChild(span)
63
+ }
64
+ })
65
+ }
66
+
67
+ th.addEventListener("click", handler)
68
+ cleanups.push(() => th.removeEventListener("click", handler))
69
+ })
70
+
71
+ return () => cleanups.forEach(fn => fn())
72
+ }, [])
73
+
74
+ return (
75
+ <div ref={ref} className="not-prose">
76
+ <Table {...props} />
77
+ </div>
78
+ )
79
+ }
80
+
81
+ export { TableHeader, TableBody, TableRow, TableHead, TableCell }
@@ -0,0 +1,4 @@
1
+ /** Simple wrapper that provides a title prop for Tabs to read */
2
+ export function Tab(props: { title: string; children: React.ReactNode }) {
3
+ return <div>{props.children}</div>
4
+ }
@@ -0,0 +1,75 @@
1
+ import { Children, isValidElement, useEffect, useState } from "react"
2
+
3
+ /** Tabbed container component for grouping content tabs with optional sync across groups */
4
+ export function Tabs(props: { sync?: string; children: React.ReactNode }) {
5
+ const children = Children.toArray(props.children)
6
+ const titles = children.map(child =>
7
+ isValidElement<{ title: string }>(child) ? child.props.title : "",
8
+ )
9
+
10
+ const getInitialIndex = () => {
11
+ if (!props.sync) return 0
12
+ const saved =
13
+ typeof window !== "undefined"
14
+ ? localStorage.getItem("livemark-tab-" + props.sync)
15
+ : null
16
+ if (!saved) return 0
17
+ const idx = titles.indexOf(saved)
18
+ return idx >= 0 ? idx : 0
19
+ }
20
+
21
+ const [active, setActive] = useState(getInitialIndex)
22
+
23
+ const handleSelect = (i: number) => {
24
+ setActive(i)
25
+ if (props.sync) {
26
+ localStorage.setItem("livemark-tab-" + props.sync, titles[i]!)
27
+ window.dispatchEvent(
28
+ new CustomEvent("livemark-tab-sync", {
29
+ detail: { sync: props.sync, title: titles[i] },
30
+ }),
31
+ )
32
+ }
33
+ }
34
+
35
+ useEffect(() => {
36
+ if (!props.sync) return
37
+ const handler = (e: Event) => {
38
+ const { sync, title } = (e as CustomEvent<{ sync: string; title: string }>).detail
39
+ if (sync !== props.sync) return
40
+ const idx = titles.indexOf(title)
41
+ if (idx >= 0) setActive(idx)
42
+ }
43
+ window.addEventListener("livemark-tab-sync", handler)
44
+ return () => window.removeEventListener("livemark-tab-sync", handler)
45
+ }, [props.sync, titles])
46
+
47
+ return (
48
+ <div className="not-prose my-4 rounded-card border bg-card">
49
+ <div className="flex border-b">
50
+ {titles.map((title, i) => (
51
+ <button
52
+ key={title}
53
+ type="button"
54
+ onClick={() => handleSelect(i)}
55
+ className={`px-4 py-2 text-sm font-medium transition-colors cursor-pointer ${
56
+ i === active
57
+ ? "text-foreground border-b-2 border-primary -mb-px"
58
+ : "text-muted-foreground hover:text-foreground"
59
+ }`}
60
+ >
61
+ {title}
62
+ </button>
63
+ ))}
64
+ </div>
65
+ {children.map((child, i) => (
66
+ <div
67
+ key={titles[i]}
68
+ className={i === active ? "p-4 prose dark:prose-invert max-w-none" : "hidden"}
69
+ >
70
+ {child}
71
+ </div>
72
+ ))}
73
+ </div>
74
+ )
75
+ }
@@ -0,0 +1,30 @@
1
+ import { useTocItems } from "../helpers/toc-context.ts"
2
+ import { useMessages } from "../hooks/messages.ts"
3
+
4
+ /** Renders the page's table of contents inline within the content */
5
+ export function Toc(props: { maxLevel?: string }) {
6
+ const allItems = useTocItems()
7
+ const t = useMessages()
8
+ const maxDepth = props.maxLevel ? Number(props.maxLevel) : 4
9
+ const items = allItems.filter(item => item.depth <= maxDepth)
10
+
11
+ if (items.length === 0) return null
12
+
13
+ return (
14
+ <nav className="not-prose my-8 rounded-card border border-border bg-card p-6">
15
+ <p className="text-base font-semibold mb-4">{t("toc.inline")}</p>
16
+ <ul className="text-base space-y-2.5">
17
+ {items.map(item => (
18
+ <li key={item.url} style={{ paddingLeft: `${(item.depth - 2) * 1.25}rem` }}>
19
+ <a
20
+ href={item.url}
21
+ className="text-muted-foreground hover:text-primary transition-colors"
22
+ >
23
+ {item.title}
24
+ </a>
25
+ </li>
26
+ ))}
27
+ </ul>
28
+ </nav>
29
+ )
30
+ }
@@ -0,0 +1,28 @@
1
+ import { parseYoutubeId } from "../helpers/youtube-id.ts"
2
+ import { useMessages } from "../hooks/messages.ts"
3
+ /** Renders a responsive YouTube iframe embed */
4
+ export function Video(props: { url: string }) {
5
+ const t = useMessages()
6
+ const id = parseYoutubeId(props.url)
7
+ if (!id) {
8
+ return (
9
+ <p>
10
+ <a href={props.url}>{props.url}</a>
11
+ </p>
12
+ )
13
+ }
14
+
15
+ return (
16
+ <div className="not-prose my-4 overflow-hidden rounded-card border">
17
+ <div className="aspect-video">
18
+ <iframe
19
+ className="h-full w-full"
20
+ src={`https://www.youtube.com/embed/${id}`}
21
+ title={t("media.youtube")}
22
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
23
+ allowFullScreen
24
+ />
25
+ </div>
26
+ </div>
27
+ )
28
+ }