@brett_lamy/docstream 0.5.1 → 0.5.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.
- package/package.json +1 -1
- package/src/docs/reference.tsx +85 -14
- package/src/index.ts +1 -1
- package/src/streamdown.tsx +1 -1
- package/src/styles.css +122 -1
package/package.json
CHANGED
package/src/docs/reference.tsx
CHANGED
|
@@ -1,17 +1,99 @@
|
|
|
1
|
-
import { createContext, useContext, useState, type ReactNode } from "react"
|
|
2
|
-
import { AtSign, Globe, Hash } from "lucide-react"
|
|
1
|
+
import { createContext, useContext, useId, useState, type ReactNode } from "react"
|
|
2
|
+
import { ArrowUpRight, AtSign, Globe, Hash } from "lucide-react"
|
|
3
3
|
|
|
4
4
|
import type { CitationDef, ReferenceNode } from "../gitbook/ast"
|
|
5
5
|
|
|
6
|
+
export interface CitationPreview {
|
|
7
|
+
title?: string
|
|
8
|
+
description?: string
|
|
9
|
+
image?: string
|
|
10
|
+
siteName?: string
|
|
11
|
+
favicon?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type CitationPreviewResolver = (citation: ReferenceNode) => CitationPreview | null | Promise<CitationPreview | null>
|
|
15
|
+
|
|
6
16
|
export interface DocstreamRefContextValue {
|
|
7
17
|
/** Called when a mention/tag chip (or a citation, if it has no url) is clicked. */
|
|
8
18
|
onReferenceClick?: (ref: ReferenceNode) => void
|
|
9
19
|
/** Full override for chip rendering; return null to fall back to the default. */
|
|
10
20
|
renderReference?: (ref: ReferenceNode) => ReactNode | null
|
|
21
|
+
/** Resolves Open Graph-style metadata when a linked citation is hovered or focused. */
|
|
22
|
+
resolveCitationPreview?: CitationPreviewResolver
|
|
11
23
|
}
|
|
12
24
|
|
|
13
25
|
export const DocstreamRefContext = createContext<DocstreamRefContextValue>({})
|
|
14
26
|
|
|
27
|
+
const previewCaches = new WeakMap<CitationPreviewResolver, Map<string, Promise<CitationPreview | null>>>()
|
|
28
|
+
|
|
29
|
+
function resolvePreview(resolver: CitationPreviewResolver, citation: ReferenceNode) {
|
|
30
|
+
let cache = previewCaches.get(resolver)
|
|
31
|
+
if (!cache) {
|
|
32
|
+
cache = new Map()
|
|
33
|
+
previewCaches.set(resolver, cache)
|
|
34
|
+
}
|
|
35
|
+
const key = citation.url ?? citation.id
|
|
36
|
+
let request = cache.get(key)
|
|
37
|
+
if (!request) {
|
|
38
|
+
request = Promise.resolve(resolver(citation)).catch(() => null)
|
|
39
|
+
cache.set(key, request)
|
|
40
|
+
}
|
|
41
|
+
return request
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function CitationHoverCard({ node }: { node: ReferenceNode }) {
|
|
45
|
+
const { onReferenceClick, resolveCitationPreview } = useContext(DocstreamRefContext)
|
|
46
|
+
const [preview, setPreview] = useState<CitationPreview | null>(null)
|
|
47
|
+
const [imageFailed, setImageFailed] = useState(false)
|
|
48
|
+
const [requested, setRequested] = useState(false)
|
|
49
|
+
const cardId = useId()
|
|
50
|
+
const host = node.url ? hostnameOf(node.url) : null
|
|
51
|
+
|
|
52
|
+
const requestPreview = () => {
|
|
53
|
+
if (!resolveCitationPreview || requested) return
|
|
54
|
+
setRequested(true)
|
|
55
|
+
void resolvePreview(resolveCitationPreview, node).then((value) => setPreview(value))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const title = preview?.title ?? node.label ?? host ?? node.url
|
|
59
|
+
const siteName = preview?.siteName ?? host
|
|
60
|
+
const favicon = preview?.favicon ?? (host ? `https://www.google.com/s2/favicons?sz=32&domain=${host}` : undefined)
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<span className="docs-cite-wrap" onMouseEnter={requestPreview} onFocus={requestPreview}>
|
|
64
|
+
<a
|
|
65
|
+
className="docs-cite"
|
|
66
|
+
href={node.url}
|
|
67
|
+
target="_blank"
|
|
68
|
+
rel="noreferrer"
|
|
69
|
+
aria-describedby={cardId}
|
|
70
|
+
onClick={() => onReferenceClick?.(node)}
|
|
71
|
+
>
|
|
72
|
+
{node.id}
|
|
73
|
+
</a>
|
|
74
|
+
<span className="docs-cite-card" id={cardId} role="tooltip">
|
|
75
|
+
{preview?.image && !imageFailed ? (
|
|
76
|
+
<span className="docs-cite-card-image-wrap">
|
|
77
|
+
<img className="docs-cite-card-image" src={preview.image} alt="" onError={() => setImageFailed(true)} />
|
|
78
|
+
</span>
|
|
79
|
+
) : null}
|
|
80
|
+
<span className="docs-cite-card-body">
|
|
81
|
+
<span className="docs-cite-card-source">
|
|
82
|
+
{favicon ? <img className="docs-cite-card-favicon" src={favicon} alt="" /> : <Globe className="docs-cite-card-favicon" />}
|
|
83
|
+
<span>{siteName}</span>
|
|
84
|
+
</span>
|
|
85
|
+
<span className="docs-cite-card-title">{title}</span>
|
|
86
|
+
{preview?.description ? <span className="docs-cite-card-description">{preview.description}</span> : null}
|
|
87
|
+
<span className="docs-cite-card-url">
|
|
88
|
+
{node.url}
|
|
89
|
+
<ArrowUpRight aria-hidden="true" />
|
|
90
|
+
</span>
|
|
91
|
+
</span>
|
|
92
|
+
</span>
|
|
93
|
+
</span>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
15
97
|
export function InlineReference({ node }: { node: ReferenceNode }) {
|
|
16
98
|
const { onReferenceClick, renderReference } = useContext(DocstreamRefContext)
|
|
17
99
|
const custom = renderReference?.(node)
|
|
@@ -20,18 +102,7 @@ export function InlineReference({ node }: { node: ReferenceNode }) {
|
|
|
20
102
|
if (node.kind === "citation") {
|
|
21
103
|
// Citations default to linking their source; hosts can still observe clicks.
|
|
22
104
|
if (node.url) {
|
|
23
|
-
return
|
|
24
|
-
<a
|
|
25
|
-
className="docs-cite"
|
|
26
|
-
href={node.url}
|
|
27
|
-
target="_blank"
|
|
28
|
-
rel="noreferrer"
|
|
29
|
-
title={node.label ?? node.url}
|
|
30
|
-
onClick={() => onReferenceClick?.(node)}
|
|
31
|
-
>
|
|
32
|
-
{node.id}
|
|
33
|
-
</a>
|
|
34
|
-
)
|
|
105
|
+
return <CitationHoverCard node={node} />
|
|
35
106
|
}
|
|
36
107
|
// Definition missing (not yet streamed, or standalone parseInline call).
|
|
37
108
|
return (
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type { PlaygroundStreamdownProps as GitbookStreamdownProps } from "./play
|
|
|
3
3
|
export { DocsRenderer, MarkdownContent } from "./docs/DocsRenderer"
|
|
4
4
|
export type { LivePreviewProps, LivePreviewRenderer, SourceReferenceRenderer } from "./docs/DocsRenderer"
|
|
5
5
|
export { CitationSources, DocstreamRefContext, InlineReference } from "./docs/reference"
|
|
6
|
-
export type { DocstreamRefContextValue } from "./docs/reference"
|
|
6
|
+
export type { CitationPreview, CitationPreviewResolver, DocstreamRefContextValue } from "./docs/reference"
|
|
7
7
|
export { ReplayEmbed, ReplayPreview } from "./replay"
|
|
8
8
|
export type {
|
|
9
9
|
ReplayEventsSource,
|
package/src/streamdown.tsx
CHANGED
|
@@ -5,7 +5,7 @@ import { parseMarkdown, trimPartialInlineToken } from "./gitbook/parse"
|
|
|
5
5
|
// Re-exported so markdown-only consumers of ./streamdown can wire chip
|
|
6
6
|
// click handling without importing the heavy root entry.
|
|
7
7
|
export { CitationSources, DocstreamRefContext, InlineReference } from "./docs/reference"
|
|
8
|
-
export type { DocstreamRefContextValue } from "./docs/reference"
|
|
8
|
+
export type { CitationPreview, CitationPreviewResolver, DocstreamRefContextValue } from "./docs/reference"
|
|
9
9
|
|
|
10
10
|
export interface GitbookStreamdownProps {
|
|
11
11
|
children?: string
|
package/src/styles.css
CHANGED
|
@@ -996,6 +996,12 @@
|
|
|
996
996
|
color: var(--gb-accent-foreground);
|
|
997
997
|
}
|
|
998
998
|
|
|
999
|
+
.docs-cite-wrap {
|
|
1000
|
+
position: relative;
|
|
1001
|
+
display: inline-block;
|
|
1002
|
+
vertical-align: super;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
999
1005
|
.docs-cite {
|
|
1000
1006
|
display: inline-block;
|
|
1001
1007
|
padding: 1px 5px;
|
|
@@ -1005,7 +1011,7 @@
|
|
|
1005
1011
|
font-size: 10px;
|
|
1006
1012
|
font-weight: 700;
|
|
1007
1013
|
line-height: 1.4;
|
|
1008
|
-
vertical-align:
|
|
1014
|
+
vertical-align: baseline;
|
|
1009
1015
|
text-decoration: none !important;
|
|
1010
1016
|
cursor: pointer;
|
|
1011
1017
|
}
|
|
@@ -1014,6 +1020,121 @@
|
|
|
1014
1020
|
background: rgba(59, 130, 246, 0.22);
|
|
1015
1021
|
}
|
|
1016
1022
|
|
|
1023
|
+
.docs-cite:focus-visible {
|
|
1024
|
+
outline: 2px solid var(--gb-info);
|
|
1025
|
+
outline-offset: 2px;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
.docs-cite-card {
|
|
1029
|
+
position: absolute;
|
|
1030
|
+
top: calc(100% + 10px);
|
|
1031
|
+
left: 50%;
|
|
1032
|
+
z-index: 50;
|
|
1033
|
+
display: block;
|
|
1034
|
+
width: min(320px, calc(100vw - 32px));
|
|
1035
|
+
overflow: hidden;
|
|
1036
|
+
border: 1px solid var(--gb-border);
|
|
1037
|
+
border-radius: 12px;
|
|
1038
|
+
color: var(--gb-panel-foreground);
|
|
1039
|
+
background: var(--gb-panel);
|
|
1040
|
+
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.16), 0 2px 8px rgba(15, 23, 42, 0.08);
|
|
1041
|
+
opacity: 0;
|
|
1042
|
+
visibility: hidden;
|
|
1043
|
+
transform: translate(-50%, -4px);
|
|
1044
|
+
transition: opacity 120ms ease, transform 120ms ease, visibility 120ms;
|
|
1045
|
+
pointer-events: none;
|
|
1046
|
+
text-align: left;
|
|
1047
|
+
white-space: normal;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
.docs-cite-wrap:hover .docs-cite-card,
|
|
1051
|
+
.docs-cite-wrap:focus-within .docs-cite-card {
|
|
1052
|
+
opacity: 1;
|
|
1053
|
+
visibility: visible;
|
|
1054
|
+
transform: translate(-50%, 0);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
.docs-cite-card-image-wrap {
|
|
1058
|
+
display: block;
|
|
1059
|
+
aspect-ratio: 1.91 / 1;
|
|
1060
|
+
overflow: hidden;
|
|
1061
|
+
border-bottom: 1px solid var(--gb-border);
|
|
1062
|
+
background: var(--gb-muted);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
.docs-cite-card-image {
|
|
1066
|
+
width: 100%;
|
|
1067
|
+
height: 100%;
|
|
1068
|
+
object-fit: cover;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
.docs-cite-card-body {
|
|
1072
|
+
display: flex;
|
|
1073
|
+
flex-direction: column;
|
|
1074
|
+
gap: 6px;
|
|
1075
|
+
padding: 12px 14px 13px;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
.docs-cite-card-source,
|
|
1079
|
+
.docs-cite-card-url {
|
|
1080
|
+
display: flex;
|
|
1081
|
+
align-items: center;
|
|
1082
|
+
gap: 6px;
|
|
1083
|
+
min-width: 0;
|
|
1084
|
+
color: var(--gb-muted-foreground);
|
|
1085
|
+
font-size: 11px;
|
|
1086
|
+
font-weight: 500;
|
|
1087
|
+
line-height: 1.35;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
.docs-cite-card-favicon {
|
|
1091
|
+
width: 14px;
|
|
1092
|
+
height: 14px;
|
|
1093
|
+
flex: 0 0 auto;
|
|
1094
|
+
border-radius: 3px;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
.docs-cite-card-title {
|
|
1098
|
+
display: -webkit-box;
|
|
1099
|
+
overflow: hidden;
|
|
1100
|
+
color: var(--gb-panel-foreground);
|
|
1101
|
+
font-size: 14px;
|
|
1102
|
+
font-weight: 650;
|
|
1103
|
+
line-height: 1.35;
|
|
1104
|
+
-webkit-box-orient: vertical;
|
|
1105
|
+
-webkit-line-clamp: 2;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
.docs-cite-card-description {
|
|
1109
|
+
display: -webkit-box;
|
|
1110
|
+
overflow: hidden;
|
|
1111
|
+
color: var(--gb-muted-foreground);
|
|
1112
|
+
font-size: 12px;
|
|
1113
|
+
font-weight: 400;
|
|
1114
|
+
line-height: 1.45;
|
|
1115
|
+
-webkit-box-orient: vertical;
|
|
1116
|
+
-webkit-line-clamp: 3;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
.docs-cite-card-url {
|
|
1120
|
+
margin-top: 2px;
|
|
1121
|
+
overflow: hidden;
|
|
1122
|
+
text-overflow: ellipsis;
|
|
1123
|
+
white-space: nowrap;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
.docs-cite-card-url svg {
|
|
1127
|
+
width: 12px;
|
|
1128
|
+
height: 12px;
|
|
1129
|
+
flex: 0 0 auto;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1133
|
+
.docs-cite-card {
|
|
1134
|
+
transition: none;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1017
1138
|
.docs-sources {
|
|
1018
1139
|
display: flex;
|
|
1019
1140
|
flex-wrap: wrap;
|