@brett_lamy/docstream 0.5.0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brett_lamy/docstream",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "GitBook-aware readonly markdown and AI stream renderer.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -30,6 +30,10 @@
30
30
  "types": "./src/streamdown.tsx",
31
31
  "import": "./src/streamdown.tsx"
32
32
  },
33
+ "./reference": {
34
+ "types": "./src/docs/reference.tsx",
35
+ "import": "./src/docs/reference.tsx"
36
+ },
33
37
  "./assets": {
34
38
  "types": "./src/assets.ts",
35
39
  "import": "./src/assets.ts"
@@ -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,
@@ -2,6 +2,11 @@ import { useMemo } from "react"
2
2
  import { DocsRenderer } from "./docs/DocsRenderer"
3
3
  import { parseMarkdown, trimPartialInlineToken } from "./gitbook/parse"
4
4
 
5
+ // Re-exported so markdown-only consumers of ./streamdown can wire chip
6
+ // click handling without importing the heavy root entry.
7
+ export { CitationSources, DocstreamRefContext, InlineReference } from "./docs/reference"
8
+ export type { CitationPreview, CitationPreviewResolver, DocstreamRefContextValue } from "./docs/reference"
9
+
5
10
  export interface GitbookStreamdownProps {
6
11
  children?: string
7
12
  className?: 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: super;
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;