@brett_lamy/docstream 0.4.0 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brett_lamy/docstream",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "GitBook-aware readonly markdown and AI stream renderer.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -65,14 +65,14 @@
65
65
  "./styles.css": "./src/styles.css"
66
66
  },
67
67
  "dependencies": {
68
+ "@brett_lamy/viz-engine": ">=0.2.0",
68
69
  "hast-util-to-html": "^9.0.5",
69
70
  "lowlight": "^3.3.0",
70
71
  "lucide-react": "^1.17.0",
71
72
  "mermaid": "^11.15.0",
72
73
  "rrweb": "2.0.0-alpha.20",
73
74
  "streamdown": "^2.5.0",
74
- "yaml": "^2.9.0",
75
- "@brett_lamy/viz-engine": "^0.2.0"
75
+ "yaml": "^2.9.0"
76
76
  },
77
77
  "peerDependencies": {
78
78
  "@agent-wasm/core": ">=0.4.0",
@@ -16,6 +16,7 @@ import { VideoEmbed } from "../video"
16
16
  import { OpenApiOperation } from "../openapi/OpenApiOperation"
17
17
  import { Mermaid } from "./Mermaid"
18
18
  import { HighlightedCode } from "./HighlightedCode"
19
+ import { CitationSources, InlineReference } from "./reference"
19
20
 
20
21
  export interface LivePreviewProps {
21
22
  files: Record<string, string>
@@ -31,6 +32,7 @@ function InlineText({ nodes }: { nodes: Inline[] }) {
31
32
  return (
32
33
  <>
33
34
  {nodes.map((n, i) => {
35
+ if (n.type === "reference") return <InlineReference key={i} node={n} />
34
36
  if (n.type === "image") {
35
37
  const img = (
36
38
  <img
@@ -359,9 +361,11 @@ export function MarkdownContent({
359
361
  sourceRenderer,
360
362
  className,
361
363
  }: { markdown: string; className?: string } & Renderers) {
364
+ const doc = parseMarkdown(markdown)
362
365
  return (
363
366
  <div data-docstream="" className={className}>
364
- <Blocks blocks={parseMarkdown(markdown).children} liveRenderer={liveRenderer} sourceRenderer={sourceRenderer} />
367
+ <Blocks blocks={doc.children} liveRenderer={liveRenderer} sourceRenderer={sourceRenderer} />
368
+ <CitationSources citations={doc.citations} />
365
369
  </div>
366
370
  )
367
371
  }
@@ -370,6 +374,7 @@ export function DocsRenderer({ doc, liveRenderer, sourceRenderer }: { doc: Docum
370
374
  return (
371
375
  <article className="docs-article">
372
376
  <Blocks blocks={doc.children} liveRenderer={liveRenderer} sourceRenderer={sourceRenderer} />
377
+ <CitationSources citations={doc.citations} />
373
378
  </article>
374
379
  )
375
380
  }
@@ -0,0 +1,95 @@
1
+ import { createContext, useContext, useState, type ReactNode } from "react"
2
+ import { AtSign, Globe, Hash } from "lucide-react"
3
+
4
+ import type { CitationDef, ReferenceNode } from "../gitbook/ast"
5
+
6
+ export interface DocstreamRefContextValue {
7
+ /** Called when a mention/tag chip (or a citation, if it has no url) is clicked. */
8
+ onReferenceClick?: (ref: ReferenceNode) => void
9
+ /** Full override for chip rendering; return null to fall back to the default. */
10
+ renderReference?: (ref: ReferenceNode) => ReactNode | null
11
+ }
12
+
13
+ export const DocstreamRefContext = createContext<DocstreamRefContextValue>({})
14
+
15
+ export function InlineReference({ node }: { node: ReferenceNode }) {
16
+ const { onReferenceClick, renderReference } = useContext(DocstreamRefContext)
17
+ const custom = renderReference?.(node)
18
+ if (custom != null) return <>{custom}</>
19
+
20
+ if (node.kind === "citation") {
21
+ // Citations default to linking their source; hosts can still observe clicks.
22
+ 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
+ )
35
+ }
36
+ // Definition missing (not yet streamed, or standalone parseInline call).
37
+ return (
38
+ <span className="docs-cite" onClick={() => onReferenceClick?.(node)}>
39
+ {node.id}
40
+ </span>
41
+ )
42
+ }
43
+
44
+ const Icon = node.kind === "mention" ? AtSign : Hash
45
+ return (
46
+ <button
47
+ type="button"
48
+ className={`docs-ref docs-ref-${node.kind}`}
49
+ onClick={() => onReferenceClick?.(node)}
50
+ >
51
+ <Icon className="docs-ref-icon" />
52
+ {node.id}
53
+ </button>
54
+ )
55
+ }
56
+
57
+ function hostnameOf(url: string): string | null {
58
+ try {
59
+ return new URL(url).hostname.replace(/^www\./, "")
60
+ } catch {
61
+ return null
62
+ }
63
+ }
64
+
65
+ function SourceChip({ def }: { def: CitationDef }) {
66
+ const [faviconFailed, setFaviconFailed] = useState(false)
67
+ const host = hostnameOf(def.url)
68
+ return (
69
+ <a className="docs-source-chip" href={def.url} target="_blank" rel="noreferrer" title={def.label ?? def.url}>
70
+ {host && !faviconFailed ? (
71
+ <img
72
+ className="docs-source-favicon"
73
+ src={`https://www.google.com/s2/favicons?sz=32&domain=${host}`}
74
+ alt=""
75
+ onError={() => setFaviconFailed(true)}
76
+ />
77
+ ) : (
78
+ <Globe className="docs-source-favicon" />
79
+ )}
80
+ {host ?? def.url}
81
+ </a>
82
+ )
83
+ }
84
+
85
+ /** Row of source-domain pills for a document's citation definitions. */
86
+ export function CitationSources({ citations }: { citations?: CitationDef[] }) {
87
+ if (!citations?.length) return null
88
+ return (
89
+ <div className="docs-sources">
90
+ {citations.map((def) => (
91
+ <SourceChip key={def.id} def={def} />
92
+ ))}
93
+ </div>
94
+ )
95
+ }
@@ -23,7 +23,29 @@ export interface InlineImageNode {
23
23
  link?: string
24
24
  }
25
25
 
26
- export type Inline = TextNode | InlineImageNode
26
+ export type ReferenceKind = "mention" | "tag" | "citation"
27
+
28
+ /**
29
+ * Inline reference chip: `@mention`, `#tag`, or footnote-style citation `[^id]`.
30
+ * `id` carries no sigil. Citations resolve `url`/`label` from their
31
+ * `[^id]: url "Label"` definition at parse time when one exists.
32
+ */
33
+ export interface ReferenceNode {
34
+ type: "reference"
35
+ kind: ReferenceKind
36
+ id: string
37
+ url?: string
38
+ label?: string
39
+ }
40
+
41
+ /** A footnote citation definition (`[^id]: url "Label"`), collected doc-level. */
42
+ export interface CitationDef {
43
+ id: string
44
+ url: string
45
+ label?: string
46
+ }
47
+
48
+ export type Inline = TextNode | InlineImageNode | ReferenceNode
27
49
 
28
50
  export interface ParagraphNode {
29
51
  type: "paragraph"
@@ -214,6 +236,8 @@ export type Block =
214
236
  export interface DocumentNode {
215
237
  type: "doc"
216
238
  children: Block[]
239
+ /** Footnote citation definitions, in definition order. */
240
+ citations?: CitationDef[]
217
241
  }
218
242
 
219
243
  export const text = (t: string, marks: Partial<Omit<TextNode, "type" | "text">> = {}): TextNode => ({
@@ -1,6 +1,6 @@
1
1
  // Pure GitBook markdown engine (no React, no mermaid/streamdown). Safe to import
2
2
  // in non-DOM environments such as a server-side AI agent or a Cloudflare Worker.
3
3
  export type * from "./ast"
4
- export { parseMarkdown, parseBlocks } from "./parse"
4
+ export { parseMarkdown, parseBlocks, trimPartialInlineToken } from "./parse"
5
5
  export { serializeBlocks, serializeMarkdown } from "./serialize"
6
- export { parseInline, plainText, refDefinitions, serializeInline } from "./inline"
6
+ export { footnoteDefinitions, parseInline, plainText, refDefinitions, serializeInline, serializeReference } from "./inline"
@@ -1,4 +1,4 @@
1
- import type { Inline, InlineImageNode, TextNode } from "./ast"
1
+ import type { Inline, InlineImageNode, ReferenceNode, TextNode } from "./ast"
2
2
 
3
3
  type Marks = Partial<Omit<TextNode, "type" | "text">>
4
4
 
@@ -6,6 +6,17 @@ type Marks = Partial<Omit<TextNode, "type" | "text">>
6
6
  // and consumed here for [text][ref] / [text][] forms.
7
7
  export const refDefinitions = new Map<string, string>()
8
8
 
9
+ // Footnote citation definitions ([^id]: url "Label"), populated by
10
+ // parseMarkdown and consumed here to resolve [^id] markers.
11
+ export const footnoteDefinitions = new Map<string, { url: string; label?: string }>()
12
+
13
+ // @mention / #tag body: letter/underscore start, then word chars, dots, dashes.
14
+ const REFERENCE_BODY_RE = /^[@#]([A-Za-z_][\w.-]*)/
15
+ // Chips are only recognized at start-of-input or after whitespace/open brackets,
16
+ // so brett@replay.io and C# stay plain text.
17
+ const isReferenceBoundary = (prev: string | undefined) =>
18
+ prev === undefined || /[\s([{]/.test(prev)
19
+
9
20
  function imgAttrs(attrStr: string): Omit<InlineImageNode, "type" | "link"> {
10
21
  const attr = (name: string) => attrStr.match(new RegExp(`${name}="([^"]*)"`, "i"))?.[1]
11
22
  const out: Omit<InlineImageNode, "type" | "link"> = { src: attr("src") ?? "" }
@@ -110,6 +121,30 @@ export function parseInline(src: string, marks: Marks = {}): Inline[] {
110
121
  }
111
122
  if (matched) continue
112
123
 
124
+ // [^id] — footnote citation marker (must precede [text](url) / [text][ref])
125
+ const cite = rest.match(/^\[\^([^\]\s]+)\]/)
126
+ if (cite) {
127
+ flush()
128
+ const def = footnoteDefinitions.get(cite[1])
129
+ out.push({ type: "reference", kind: "citation", id: cite[1], ...def })
130
+ i += cite[0].length
131
+ continue
132
+ }
133
+
134
+ // @mention / #tag chips, only at a word boundary
135
+ if ((rest[0] === "@" || rest[0] === "#") && isReferenceBoundary(src[i - 1])) {
136
+ const m = rest.match(REFERENCE_BODY_RE)
137
+ if (m) {
138
+ const id = m[1].replace(/[.-]+$/, "")
139
+ if (id) {
140
+ flush()
141
+ out.push({ type: "reference", kind: rest[0] === "@" ? "mention" : "tag", id })
142
+ i += 1 + id.length
143
+ continue
144
+ }
145
+ }
146
+ }
147
+
113
148
  // [text](url)
114
149
  if (rest[0] === "[") {
115
150
  const m = rest.match(/^\[([^\]]*)\]\(([^)\s]+)\)/)
@@ -162,7 +197,18 @@ export function parseInline(src: string, marks: Marks = {}): Inline[] {
162
197
  return out
163
198
  }
164
199
 
165
- const escapeText = (t: string) => t.replace(/([*_~`[\]\\])/g, "\\$1")
200
+ const escapeText = (t: string) =>
201
+ t
202
+ .replace(/([*_~`[\]\\])/g, "\\$1")
203
+ // Escape @/# only at a chip boundary so emails/C# survive untouched.
204
+ // Note: node-local — a boundary formed across adjacent inline nodes
205
+ // (previous node ending in whitespace) is not caught; rare, accepted.
206
+ .replace(/(^|[\s([{])([@#])(?=[A-Za-z_])/g, "$1\\$2")
207
+
208
+ export function serializeReference(n: ReferenceNode): string {
209
+ if (n.kind === "citation") return `[^${n.id}]`
210
+ return `${n.kind === "mention" ? "@" : "#"}${n.id}`
211
+ }
166
212
 
167
213
  function serializeImage(n: InlineImageNode): string {
168
214
  const attrs = [
@@ -182,6 +228,7 @@ export function serializeInline(nodes: Inline[]): string {
182
228
  return nodes
183
229
  .map((n) => {
184
230
  if (n.type === "image") return serializeImage(n)
231
+ if (n.type === "reference") return serializeReference(n)
185
232
  // bare autolink: text identical to the URL, no other marks
186
233
  if (n.link && n.text === n.link && !n.bold && !n.italic && !n.strike && !n.code) {
187
234
  return n.link
@@ -198,5 +245,14 @@ export function serializeInline(nodes: Inline[]): string {
198
245
  }
199
246
 
200
247
  export function plainText(nodes: Inline[]): string {
201
- return nodes.map((n) => (n.type === "text" ? n.text : (n.alt ?? ""))).join("")
248
+ return nodes
249
+ .map((n) => {
250
+ if (n.type === "text") return n.text
251
+ if (n.type === "reference") {
252
+ // Citation superscripts are visual noise in plain text (titles etc.).
253
+ return n.kind === "citation" ? "" : serializeReference(n)
254
+ }
255
+ return n.alt ?? ""
256
+ })
257
+ .join("")
202
258
  }
@@ -9,7 +9,7 @@ import type {
9
9
  TabNode,
10
10
  UpdateNode,
11
11
  } from "./ast"
12
- import { parseInline, plainText, refDefinitions } from "./inline"
12
+ import { footnoteDefinitions, parseInline, plainText, refDefinitions } from "./inline"
13
13
 
14
14
  // Minimal HTML-inline → markdown-inline bridge for HTML table cells.
15
15
  function htmlToInlineMd(html: string): string {
@@ -119,13 +119,46 @@ const HINT_STYLES: HintStyle[] = ["info", "success", "warning", "danger"]
119
119
  export function parseMarkdown(src: string): DocumentNode {
120
120
  const lines = src.split(/\r?\n/)
121
121
  refDefinitions.clear()
122
+ footnoteDefinitions.clear()
122
123
  const content: string[] = []
124
+ let inFence = false
123
125
  for (const line of lines) {
124
- const def = line.match(/^\[([^\]]+)\]:\s*(\S+)\s*$/)
125
- if (def) refDefinitions.set(def[1].toLowerCase(), def[2])
126
- else content.push(line)
126
+ // Definition lines inside code fences are content, not definitions.
127
+ if (/^\s*(?:```|~~~)/.test(line)) inFence = !inFence
128
+ if (!inFence) {
129
+ // Footnote citation definitions first: [^id]: url "Optional Label"
130
+ const foot = line.match(/^\[\^([^\]\s]+)\]:\s*(\S+)(?:\s+"([^"]*)")?\s*$/)
131
+ if (foot) {
132
+ footnoteDefinitions.set(foot[1], { url: foot[2], ...(foot[3] ? { label: foot[3] } : {}) })
133
+ continue
134
+ }
135
+ const def = line.match(/^\[([^\]]+)\]:\s*(\S+)\s*$/)
136
+ // Keys starting with ^ are reserved for footnotes and must never
137
+ // shadow reference-style link definitions.
138
+ if (def && !def[1].startsWith("^")) {
139
+ refDefinitions.set(def[1].toLowerCase(), def[2])
140
+ continue
141
+ }
142
+ }
143
+ content.push(line)
144
+ }
145
+ const doc: DocumentNode = { type: "doc", children: parseBlocks(content) }
146
+ if (footnoteDefinitions.size) {
147
+ doc.citations = [...footnoteDefinitions.entries()].map(([id, d]) => ({ id, ...d }))
127
148
  }
128
- return { type: "doc", children: parseBlocks(content) }
149
+ return doc
150
+ }
151
+
152
+ // Strips an in-progress inline token (`[^1`, `@bre`, a lone `#`, a trailing
153
+ // `\`) from the end of a partially streamed document so chips don't flash as
154
+ // plain text mid-stream. No-op inside an unterminated code fence.
155
+ export function trimPartialInlineToken(md: string): string {
156
+ const fences = md.match(/^\s*(?:```|~~~)/gm)
157
+ if (fences && fences.length % 2 === 1) return md
158
+ return md
159
+ .replace(/\[\^[^\]]*$/, "")
160
+ .replace(/(^|[\s([{])[@#][\w.-]*$/, "$1")
161
+ .replace(/\\$/, "")
129
162
  }
130
163
 
131
164
  export function parseBlocks(lines: string[]): Block[] {
@@ -1,8 +1,15 @@
1
1
  import type { Block, DocumentNode, Inline, ListItemNode } from "./ast"
2
- import { serializeInline } from "./inline"
2
+ import { serializeInline, serializeReference } from "./inline"
3
3
 
4
4
  export function serializeMarkdown(doc: DocumentNode): string {
5
- return serializeBlocks(doc.children).trimEnd() + "\n"
5
+ let out = serializeBlocks(doc.children).trimEnd()
6
+ if (doc.citations?.length) {
7
+ const defs = doc.citations
8
+ .map((c) => `[^${c.id}]: ${c.url}${c.label ? ` "${c.label}"` : ""}`)
9
+ .join("\n")
10
+ out = out ? `${out}\n\n${defs}` : defs
11
+ }
12
+ return out + "\n"
6
13
  }
7
14
 
8
15
  export function serializeBlocks(blocks: Block[]): string {
@@ -156,6 +163,9 @@ function inlineToHtml(nodes: Inline[]): string {
156
163
  const img = `<img src="${n.src}"${n.alt ? ` alt="${n.alt}"` : ""}>`
157
164
  return n.link ? `<a href="${n.link}">${img}</a>` : img
158
165
  }
166
+ // HTML card tables round-trip through htmlToInlineMd, which strips
167
+ // tags — plain markers are the only representation that survives.
168
+ if (n.type === "reference") return serializeReference(n)
159
169
  let s = n.text
160
170
  if (n.code) s = `<code>${s}</code>`
161
171
  if (n.bold) s = `<strong>${s}</strong>`
package/src/index.ts CHANGED
@@ -2,6 +2,8 @@ export { PlaygroundStreamdown as GitbookStreamdown } from "./playground/Playgrou
2
2
  export type { PlaygroundStreamdownProps as GitbookStreamdownProps } from "./playground/PlaygroundStreamdown"
3
3
  export { DocsRenderer, MarkdownContent } from "./docs/DocsRenderer"
4
4
  export type { LivePreviewProps, LivePreviewRenderer, SourceReferenceRenderer } from "./docs/DocsRenderer"
5
+ export { CitationSources, DocstreamRefContext, InlineReference } from "./docs/reference"
6
+ export type { DocstreamRefContextValue } from "./docs/reference"
5
7
  export { ReplayEmbed, ReplayPreview } from "./replay"
6
8
  export type {
7
9
  ReplayEventsSource,
@@ -42,9 +44,16 @@ export type {
42
44
  SourceProvenance,
43
45
  SourceReferenceClient,
44
46
  } from "./source"
45
- export { parseMarkdown, parseBlocks } from "./gitbook/parse"
47
+ export { parseMarkdown, parseBlocks, trimPartialInlineToken } from "./gitbook/parse"
46
48
  export { serializeBlocks, serializeMarkdown } from "./gitbook/serialize"
47
- export { parseInline, plainText, refDefinitions, serializeInline } from "./gitbook/inline"
49
+ export {
50
+ footnoteDefinitions,
51
+ parseInline,
52
+ plainText,
53
+ refDefinitions,
54
+ serializeInline,
55
+ serializeReference,
56
+ } from "./gitbook/inline"
48
57
  export { Streamdown } from "streamdown"
49
58
  export type { StreamdownProps } from "streamdown"
50
59
  export type * from "./gitbook/ast"
@@ -1,7 +1,7 @@
1
1
  import { useMemo } from "react"
2
2
 
3
3
  import { DocsRenderer, type LivePreviewRenderer } from "../docs/DocsRenderer"
4
- import { parseMarkdown } from "../gitbook/parse"
4
+ import { parseMarkdown, trimPartialInlineToken } from "../gitbook/parse"
5
5
  import { ReactCodePreview } from "./ReactCodePreview"
6
6
 
7
7
  export interface PlaygroundStreamdownProps {
@@ -25,7 +25,10 @@ export function PlaygroundStreamdown({
25
25
  markdown,
26
26
  }: PlaygroundStreamdownProps) {
27
27
  const content = markdown ?? children ?? ""
28
- const doc = useMemo(() => parseMarkdown(content), [content])
28
+ const doc = useMemo(
29
+ () => parseMarkdown(isStreaming ? trimPartialInlineToken(content) : content),
30
+ [content, isStreaming]
31
+ )
29
32
  const liveRenderer: LivePreviewRenderer = ({ files, entry, title }) => (
30
33
  <ReactCodePreview files={files} entry={entry} title={title} />
31
34
  )
@@ -1,6 +1,6 @@
1
1
  import { useMemo } from "react"
2
2
  import { DocsRenderer } from "./docs/DocsRenderer"
3
- import { parseMarkdown } from "./gitbook/parse"
3
+ import { parseMarkdown, trimPartialInlineToken } from "./gitbook/parse"
4
4
 
5
5
  export interface GitbookStreamdownProps {
6
6
  children?: string
@@ -18,7 +18,10 @@ export function GitbookStreamdown({
18
18
  markdown,
19
19
  }: GitbookStreamdownProps) {
20
20
  const content = markdown ?? children ?? ""
21
- const gitbookDoc = useMemo(() => parseMarkdown(content), [content])
21
+ const gitbookDoc = useMemo(
22
+ () => parseMarkdown(isStreaming ? trimPartialInlineToken(content) : content),
23
+ [content, isStreaming]
24
+ )
22
25
 
23
26
  return (
24
27
  <div
package/src/styles.css CHANGED
@@ -953,3 +953,94 @@
953
953
  outline: 2px solid var(--docs-react-demo-primary);
954
954
  outline-offset: 2px;
955
955
  }
956
+
957
+ /* ---- Inline reference chips (@mention, #tag) and citations ---- */
958
+
959
+ .docs-ref {
960
+ display: inline-flex;
961
+ align-items: center;
962
+ gap: 4px;
963
+ padding: 1px 8px;
964
+ border-radius: 999px;
965
+ font-family: inherit;
966
+ font-size: 0.85em;
967
+ font-weight: 500;
968
+ line-height: 1.5;
969
+ vertical-align: baseline;
970
+ cursor: pointer;
971
+ }
972
+
973
+ .docs-ref-icon {
974
+ width: 12px;
975
+ height: 12px;
976
+ }
977
+
978
+ .docs-ref-mention {
979
+ border: 1px solid rgba(59, 130, 246, 0.45);
980
+ background: rgba(59, 130, 246, 0.1);
981
+ color: var(--gb-info);
982
+ }
983
+
984
+ .docs-ref-mention:hover {
985
+ background: rgba(59, 130, 246, 0.18);
986
+ }
987
+
988
+ .docs-ref-tag {
989
+ border: 1px solid var(--gb-border);
990
+ background: var(--gb-muted);
991
+ color: var(--gb-muted-foreground);
992
+ }
993
+
994
+ .docs-ref-tag:hover {
995
+ background: var(--gb-accent);
996
+ color: var(--gb-accent-foreground);
997
+ }
998
+
999
+ .docs-cite {
1000
+ display: inline-block;
1001
+ padding: 1px 5px;
1002
+ border-radius: 5px;
1003
+ background: rgba(59, 130, 246, 0.14);
1004
+ color: var(--gb-info) !important;
1005
+ font-size: 10px;
1006
+ font-weight: 700;
1007
+ line-height: 1.4;
1008
+ vertical-align: super;
1009
+ text-decoration: none !important;
1010
+ cursor: pointer;
1011
+ }
1012
+
1013
+ .docs-cite:hover {
1014
+ background: rgba(59, 130, 246, 0.22);
1015
+ }
1016
+
1017
+ .docs-sources {
1018
+ display: flex;
1019
+ flex-wrap: wrap;
1020
+ gap: 8px;
1021
+ margin-top: 16px;
1022
+ }
1023
+
1024
+ .docs-source-chip {
1025
+ display: inline-flex;
1026
+ align-items: center;
1027
+ gap: 6px;
1028
+ padding: 4px 12px;
1029
+ border: 1px solid var(--gb-border);
1030
+ border-radius: 999px;
1031
+ font-size: 12.5px;
1032
+ font-weight: 500;
1033
+ color: var(--gb-muted-foreground) !important;
1034
+ text-decoration: none !important;
1035
+ }
1036
+
1037
+ .docs-source-chip:hover {
1038
+ background: var(--gb-accent);
1039
+ color: var(--gb-accent-foreground) !important;
1040
+ }
1041
+
1042
+ .docs-source-favicon {
1043
+ width: 14px;
1044
+ height: 14px;
1045
+ border-radius: 4px;
1046
+ }