Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash. | 1 | /** |
| 2 | * g1t's own blocks in the page editor, beside BlockNote's: callouts, | |
| 3 | * Mermaid diagrams, math, and cards for g1t things (an issue, a pull | |
| 4 | * request, a channel, a project, another page); and inline mentions of | |
| 5 | * people, agents and pages, dates, and citations of code (a file, folder | |
| 6 | * or pattern in a repository, and the symbol, endpoint or environment | |
| 7 | * variable there the page describes). The docs service reads the same | |
| 8 | * names and attributes when it writes Markdown (services/docs | |
| 9 | * src/markdown.ts), so keep the two in step. Browser-only: loaded with | |
| 10 | * the editor. | |
| 11 | */ | |
| 12 | import { BlockNoteSchema, createCodeBlockSpec, defaultBlockSpecs, defaultInlineContentSpecs } from "@blocknote/core"; | |
| 13 | import { createReactBlockSpec, createReactInlineContentSpec } from "@blocknote/react"; | |
| 14 | import { codeBlockOptions } from "@blocknote/code-block"; | |
| 15 | import { AlertTriangle, Braces, CheckCircle2, CircleDot, FileCode2, FileText, GitPullRequest, Hash, Info, KeyRound, OctagonAlert, Package, Route } from "lucide-react"; | |
| 16 | import { useEffect, useId, useState, type ReactNode } from "react"; | |
| 17 | ||
| 18 | import { citationHref } from "../../../lib/folios"; | |
| You choose how g1t looks: Auto follows your system, Light and Dark stay put, set from the account menu, the phone's You and help sheet, Settings → Account or the command palette; the page is drawn in your choice from the first byte, every colour has a light value that reads at 4.5:1 or better, code and diffs are highlighted for both, diagrams and the document editor follow along, the product tour and the g1t mark's avatar stay dark, and Dark is what you see until you choose; the workspaces guide says how. | 19 | import { useDrawnTheme } from "../../../lib/theme"; |
| Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash. | 20 | |
| 21 | /** What an embed shows, from the site (`-/artifacts/api?embed=`). */ | |
| 22 | export type EmbedCard = { kind: "issue" | "pull" | "channel" | "project" | "page" | "link"; title: string; subtitle: string | null; state: string | null; href: string }; | |
| 23 | ||
| 24 | const CALLOUTS = { | |
| 25 | info: { icon: <Info size={16} />, ring: "border-info/30 bg-info/8", text: "text-info" }, | |
| 26 | warning: { icon: <AlertTriangle size={16} />, ring: "border-warn/30 bg-warn/8", text: "text-warn" }, | |
| 27 | success: { icon: <CheckCircle2 size={16} />, ring: "border-success/30 bg-success/8", text: "text-success" }, | |
| 28 | danger: { icon: <OctagonAlert size={16} />, ring: "border-danger/30 bg-danger/8", text: "text-danger" }, | |
| 29 | } as const; | |
| 30 | ||
| 31 | export const Callout = createReactBlockSpec( | |
| 32 | { | |
| 33 | type: "callout", | |
| 34 | propSchema: { | |
| 35 | kind: { default: "info", values: ["info", "warning", "success", "danger"] }, | |
| 36 | backgroundColor: { default: "default" }, | |
| 37 | textColor: { default: "default" }, | |
| 38 | }, | |
| 39 | content: "inline", | |
| 40 | }, | |
| 41 | { | |
| 42 | render: ({ block, editor, contentRef }) => { | |
| 43 | const look = CALLOUTS[block.props.kind as keyof typeof CALLOUTS] ?? CALLOUTS.info; | |
| 44 | const next = { info: "warning", warning: "success", success: "danger", danger: "info" } as const; | |
| 45 | return ( | |
| 46 | <div className={`my-1 flex w-full gap-2.5 rounded-lg border px-3.5 py-2.5 ${look.ring}`}> | |
| 47 | <button | |
| 48 | type="button" | |
| 49 | contentEditable={false} | |
| 50 | aria-label="Change the callout's kind" | |
| 51 | disabled={!editor.isEditable} | |
| 52 | onClick={() => editor.updateBlock(block, { props: { kind: next[block.props.kind as keyof typeof next] ?? "info" } })} | |
| 53 | className={`mt-0.5 shrink-0 ${look.text}`} | |
| 54 | > | |
| 55 | {look.icon} | |
| 56 | </button> | |
| 57 | <div ref={contentRef} className="min-w-0 grow" /> | |
| 58 | </div> | |
| 59 | ); | |
| 60 | }, | |
| 61 | }, | |
| 62 | ); | |
| 63 | ||
| 64 | /** A diagram drawn from its Mermaid source, live as it is typed. */ | |
| 65 | function MermaidView({ code }: { code: string }) { | |
| 66 | const id = useId().replace(/[^a-zA-Z0-9]/g, ""); | |
| 67 | const [svg, setSvg] = useState<string | null>(null); | |
| 68 | const [error, setError] = useState<string | null>(null); | |
| You choose how g1t looks: Auto follows your system, Light and Dark stay put, set from the account menu, the phone's You and help sheet, Settings → Account or the command palette; the page is drawn in your choice from the first byte, every colour has a light value that reads at 4.5:1 or better, code and diffs are highlighted for both, diagrams and the document editor follow along, the product tour and the g1t mark's avatar stay dark, and Dark is what you see until you choose; the workspaces guide says how. | 69 | // Drawn again in the other theme's colours when the page switches. |
| 70 | const theme = useDrawnTheme(); | |
| Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash. | 71 | useEffect(() => { |
| 72 | let cancelled = false; | |
| 73 | const timer = setTimeout(async () => { | |
| 74 | try { | |
| 75 | const mermaid = (await import("mermaid")).default; | |
| You choose how g1t looks: Auto follows your system, Light and Dark stay put, set from the account menu, the phone's You and help sheet, Settings → Account or the command palette; the page is drawn in your choice from the first byte, every colour has a light value that reads at 4.5:1 or better, code and diffs are highlighted for both, diagrams and the document editor follow along, the product tour and the g1t mark's avatar stay dark, and Dark is what you see until you choose; the workspaces guide says how. | 76 | mermaid.initialize({ startOnLoad: false, theme: theme === "light" ? "default" : "dark", securityLevel: "strict", fontFamily: "inherit" }); |
| Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash. | 77 | const { svg } = await mermaid.render(`mermaid-${id}-${Date.now()}`, code || "flowchart LR\n A --> B"); |
| 78 | if (!cancelled) { | |
| 79 | setSvg(svg); | |
| 80 | setError(null); | |
| 81 | } | |
| 82 | } catch (e) { | |
| 83 | if (!cancelled) setError(e instanceof Error ? e.message.split("\n")[0]! : "This diagram has an error."); | |
| 84 | } | |
| 85 | }, 250); | |
| 86 | return () => { | |
| 87 | cancelled = true; | |
| 88 | clearTimeout(timer); | |
| 89 | }; | |
| You choose how g1t looks: Auto follows your system, Light and Dark stay put, set from the account menu, the phone's You and help sheet, Settings → Account or the command palette; the page is drawn in your choice from the first byte, every colour has a light value that reads at 4.5:1 or better, code and diffs are highlighted for both, diagrams and the document editor follow along, the product tour and the g1t mark's avatar stay dark, and Dark is what you see until you choose; the workspaces guide says how. | 90 | }, [code, id, theme]); |
| Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash. | 91 | if (error) return <p className="text-xs text-danger">{error}</p>; |
| 92 | if (!svg) return <p className="text-xs text-faint">Drawing…</p>; | |
| 93 | // Mermaid sanitizes what it draws (securityLevel "strict"). | |
| 94 | return <div className="flex justify-center overflow-x-auto [&_svg]:max-w-full" dangerouslySetInnerHTML={{ __html: svg }} />; | |
| 95 | } | |
| 96 | ||
| 97 | function SourceEditor({ value, onChange, placeholder, mono = true, editable }: { value: string; onChange: (v: string) => void; placeholder: string; mono?: boolean; editable: boolean }) { | |
| 98 | const [draft, setDraft] = useState(value); | |
| 99 | useEffect(() => setDraft(value), [value]); | |
| 100 | if (!editable) return null; | |
| 101 | return ( | |
| 102 | <textarea | |
| 103 | value={draft} | |
| 104 | placeholder={placeholder} | |
| 105 | spellCheck={false} | |
| 106 | rows={Math.min(14, Math.max(2, draft.split("\n").length))} | |
| 107 | onChange={(e) => setDraft(e.target.value)} | |
| 108 | onBlur={() => draft !== value && onChange(draft)} | |
| 109 | onKeyDown={(e) => e.stopPropagation()} | |
| You choose how g1t looks: Auto follows your system, Light and Dark stay put, set from the account menu, the phone's You and help sheet, Settings → Account or the command palette; the page is drawn in your choice from the first byte, every colour has a light value that reads at 4.5:1 or better, code and diffs are highlighted for both, diagrams and the document editor follow along, the product tour and the g1t mark's avatar stay dark, and Dark is what you see until you choose; the workspaces guide says how. | 110 | className={`w-full resize-y rounded-md border border-line bg-bg px-2.5 py-2 text-[0.8125rem] leading-relaxed text-fg outline-none focus:border-[var(--color-accent)] ${mono ? "font-mono" : ""}`} |
| Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash. | 111 | /> |
| 112 | ); | |
| 113 | } | |
| 114 | ||
| 115 | export const Mermaid = createReactBlockSpec( | |
| 116 | { type: "mermaid", propSchema: { code: { default: "flowchart LR\n A[Idea] --> B[Spec] --> C[Shipped]" } }, content: "none" }, | |
| 117 | { | |
| 118 | render: ({ block, editor }) => { | |
| 119 | const [editing, setEditing] = useState(false); | |
| 120 | return ( | |
| 121 | <div className="my-1 w-full rounded-lg border border-line bg-surface p-3" contentEditable={false}> | |
| 122 | <div className="mb-2 flex items-center justify-between text-[0.6875rem] font-medium tracking-wide text-faint uppercase"> | |
| 123 | <span>Diagram</span> | |
| 124 | {editor.isEditable && ( | |
| 125 | <button type="button" onClick={() => setEditing(!editing)} className="rounded px-1.5 py-0.5 normal-case hover:bg-raised hover:text-fg"> | |
| 126 | {editing ? "Done" : "Edit source"} | |
| 127 | </button> | |
| 128 | )} | |
| 129 | </div> | |
| 130 | {editing && <SourceEditor editable value={block.props.code} placeholder="flowchart LR A --> B" onChange={(code) => editor.updateBlock(block, { props: { code } })} />} | |
| 131 | <div className={editing ? "mt-3" : ""}> | |
| 132 | <MermaidView code={block.props.code} /> | |
| 133 | </div> | |
| 134 | </div> | |
| 135 | ); | |
| 136 | }, | |
| 137 | }, | |
| 138 | ); | |
| 139 | ||
| 140 | /** A formula, typeset with KaTeX. */ | |
| 141 | function MathView({ expression, inline = false }: { expression: string; inline?: boolean }) { | |
| 142 | const [html, setHtml] = useState<string | null>(null); | |
| 143 | useEffect(() => { | |
| 144 | let cancelled = false; | |
| 145 | void (async () => { | |
| 146 | const [{ default: katex }] = await Promise.all([import("katex"), import("katex/dist/katex.min.css")]); | |
| 147 | const out = katex.renderToString(expression || "\\;", { throwOnError: false, displayMode: !inline, output: "html", trust: false, strict: "ignore" }); | |
| 148 | if (!cancelled) setHtml(out); | |
| 149 | })(); | |
| 150 | return () => { | |
| 151 | cancelled = true; | |
| 152 | }; | |
| 153 | }, [expression, inline]); | |
| 154 | if (html === null) return <code className="text-xs text-faint">{expression}</code>; | |
| 155 | // KaTeX escapes what it is given and allows no commands that run anything (trust: false). | |
| 156 | return <div className="overflow-x-auto text-fg" dangerouslySetInnerHTML={{ __html: html }} />; | |
| 157 | } | |
| 158 | ||
| 159 | export const MathBlock = createReactBlockSpec( | |
| 160 | { type: "math", propSchema: { expression: { default: "e^{i\\pi} + 1 = 0" } }, content: "none" }, | |
| 161 | { | |
| 162 | render: ({ block, editor }) => { | |
| 163 | const [editing, setEditing] = useState(false); | |
| 164 | return ( | |
| 165 | <div className="group/math my-1 w-full rounded-lg px-3 py-2 hover:bg-surface" contentEditable={false}> | |
| 166 | {editing ? ( | |
| 167 | <SourceEditor editable value={block.props.expression} placeholder="\int_0^1 x^2\,dx" onChange={(expression) => editor.updateBlock(block, { props: { expression } })} /> | |
| 168 | ) : null} | |
| 169 | <button type="button" className="block w-full text-left" disabled={!editor.isEditable} onClick={() => setEditing(!editing)} aria-label={editing ? "Done editing the formula" : "Edit the formula"}> | |
| 170 | <MathView expression={block.props.expression} /> | |
| 171 | </button> | |
| 172 | </div> | |
| 173 | ); | |
| 174 | }, | |
| 175 | }, | |
| 176 | ); | |
| 177 | ||
| 178 | const EMBED_ICONS: Record<EmbedCard["kind"], ReactNode> = { | |
| 179 | issue: <CircleDot size={16} />, | |
| 180 | pull: <GitPullRequest size={16} />, | |
| 181 | channel: <Hash size={16} />, | |
| 182 | project: <Package size={16} />, | |
| 183 | page: <FileText size={16} />, | |
| 184 | link: <FileText size={16} />, | |
| 185 | }; | |
| 186 | ||
| 187 | /** The card for a g1t address, asked of the site as the reader (so it shows only what they may see). */ | |
| 188 | function EmbedView({ url, title }: { url: string; title: string }) { | |
| 189 | const [card, setCard] = useState<EmbedCard | null | "missing">(null); | |
| 190 | useEffect(() => { | |
| 191 | let cancelled = false; | |
| 192 | const workspace = window.location.pathname.split("/")[1] ?? ""; | |
| 193 | fetch(`/${workspace}/-/artifacts/api?embed=${encodeURIComponent(url)}`, { headers: { accept: "application/json" } }) | |
| 194 | .then((r) => r.json() as Promise<{ ok: boolean; value?: EmbedCard }>) | |
| 195 | .then((r) => !cancelled && setCard(r.ok && r.value ? r.value : "missing")) | |
| 196 | .catch(() => !cancelled && setCard("missing")); | |
| 197 | return () => { | |
| 198 | cancelled = true; | |
| 199 | }; | |
| 200 | }, [url]); | |
| 201 | const shown = card && card !== "missing" ? card : null; | |
| 202 | const state = shown?.state ?? null; | |
| 203 | const tone = state === "open" ? "text-success" : state === "merged" ? "text-merged" : state === "closed" ? "text-danger" : "text-[var(--g1t-muted)]"; | |
| 204 | return ( | |
| 205 | <a | |
| 206 | href={shown?.href ?? url} | |
| 207 | contentEditable={false} | |
| 208 | className="my-1 flex w-full items-center gap-3 rounded-lg border border-line bg-surface px-3.5 py-2.5 no-underline transition-colors hover:border-line-strong hover:bg-raised" | |
| 209 | > | |
| 210 | <span className={`shrink-0 ${tone}`}>{EMBED_ICONS[shown?.kind ?? "link"]}</span> | |
| 211 | <span className="min-w-0 grow"> | |
| 212 | <span className="block truncate text-sm font-medium text-fg">{shown?.title ?? (title || url)}</span> | |
| 213 | <span className="block truncate text-xs text-faint">{card === null ? "Loading…" : card === "missing" ? "You can't see this, or it's gone." : shown?.subtitle}</span> | |
| 214 | </span> | |
| 215 | {state && <span className={`shrink-0 rounded-full px-2 py-0.5 text-[0.6875rem] font-medium capitalize ring-1 ring-current/30 ${tone}`}>{state}</span>} | |
| 216 | </a> | |
| 217 | ); | |
| 218 | } | |
| 219 | ||
| 220 | export const Embed = createReactBlockSpec( | |
| 221 | { type: "embed", propSchema: { url: { default: "" }, title: { default: "" }, kind: { default: "link" } }, content: "none" }, | |
| 222 | { | |
| 223 | render: ({ block, editor }) => { | |
| 224 | const [draft, setDraft] = useState(""); | |
| 225 | if (!block.props.url) { | |
| 226 | if (!editor.isEditable) return <div contentEditable={false} />; | |
| 227 | return ( | |
| 228 | <form | |
| 229 | contentEditable={false} | |
| 230 | className="my-1 flex w-full gap-2 rounded-lg border border-dashed border-line p-2" | |
| 231 | onSubmit={(e) => { | |
| 232 | e.preventDefault(); | |
| 233 | const url = draft.trim(); | |
| 234 | if (url) editor.updateBlock(block, { props: { url } }); | |
| 235 | }} | |
| 236 | > | |
| 237 | <input | |
| 238 | autoFocus | |
| 239 | value={draft} | |
| 240 | onChange={(e) => setDraft(e.target.value)} | |
| 241 | onKeyDown={(e) => e.stopPropagation()} | |
| 242 | placeholder="Paste the address of an issue, pull request, channel, project or artifact" | |
| 243 | className="min-w-0 grow bg-transparent px-1.5 text-sm text-fg outline-none placeholder:text-faint" | |
| 244 | /> | |
| 245 | <button type="submit" className="rounded-md bg-raised px-2.5 py-1 text-xs font-medium text-fg hover:bg-line"> | |
| 246 | Embed | |
| 247 | </button> | |
| 248 | </form> | |
| 249 | ); | |
| 250 | } | |
| 251 | return <EmbedView url={block.props.url} title={block.props.title} />; | |
| 252 | }, | |
| 253 | }, | |
| 254 | ); | |
| 255 | ||
| 256 | /** A person, agent or page, inline. */ | |
| 257 | export const Mention = createReactInlineContentSpec( | |
| 258 | { | |
| 259 | type: "mention", | |
| 260 | propSchema: { kind: { default: "user" }, id: { default: "" }, name: { default: "" }, href: { default: "" } }, | |
| 261 | content: "none", | |
| 262 | }, | |
| 263 | { | |
| 264 | render: ({ inlineContent }) => { | |
| 265 | const p = inlineContent.props; | |
| 266 | if (p.kind === "page") { | |
| 267 | return ( | |
| You choose how g1t looks: Auto follows your system, Light and Dark stay put, set from the account menu, the phone's You and help sheet, Settings → Account or the command palette; the page is drawn in your choice from the first byte, every colour has a light value that reads at 4.5:1 or better, code and diffs are highlighted for both, diagrams and the document editor follow along, the product tour and the g1t mark's avatar stay dark, and Dark is what you see until you choose; the workspaces guide says how. | 268 | <a href={p.href || "#"} className="rounded px-0.5 font-medium text-[var(--color-accent)] no-underline hover:underline"> |
| Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash. | 269 | <FileText size={13} className="mr-0.5 inline -translate-y-px" aria-hidden="true" /> |
| 270 | {p.name || "Untitled"} | |
| 271 | </a> | |
| 272 | ); | |
| 273 | } | |
| You choose how g1t looks: Auto follows your system, Light and Dark stay put, set from the account menu, the phone's You and help sheet, Settings → Account or the command palette; the page is drawn in your choice from the first byte, every colour has a light value that reads at 4.5:1 or better, code and diffs are highlighted for both, diagrams and the document editor follow along, the product tour and the g1t mark's avatar stay dark, and Dark is what you see until you choose; the workspaces guide says how. | 274 | return <span className={`rounded px-1 py-px font-medium ${p.kind === "agent" ? "bg-[color-mix(in_srgb,var(--color-accent)_15%,transparent)] text-[var(--color-accent)]" : "bg-info/12 text-info"}`}>@{p.name}</span>; |
| Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash. | 275 | }, |
| 276 | }, | |
| 277 | ); | |
| 278 | ||
| 279 | /** A date, inline. */ | |
| 280 | export const DateChip = createReactInlineContentSpec( | |
| 281 | { type: "date", propSchema: { date: { default: "" } }, content: "none" }, | |
| 282 | { | |
| 283 | render: ({ inlineContent }) => { | |
| 284 | const at = new Date(`${inlineContent.props.date}T12:00:00`); | |
| 285 | const label = Number.isNaN(at.getTime()) ? inlineContent.props.date : at.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric", year: "numeric" }); | |
| 286 | return <span className="rounded bg-raised px-1 py-px text-[0.9em] text-fg-soft">{label}</span>; | |
| 287 | }, | |
| 288 | }, | |
| 289 | ); | |
| 290 | ||
| 291 | const CITATION_ICONS: Record<string, ReactNode> = { | |
| 292 | path: <FileCode2 size={12} aria-hidden="true" />, | |
| 293 | symbol: <Braces size={12} aria-hidden="true" />, | |
| 294 | endpoint: <Route size={12} aria-hidden="true" />, | |
| 295 | env: <KeyRound size={12} aria-hidden="true" />, | |
| 296 | }; | |
| 297 | ||
| 298 | /** | |
| 299 | * Code the page cites, inline: what it names (the path, or the symbol, | |
| 300 | * endpoint or variable there), linking to it in Code at the commit it was | |
| 301 | * cited at. When a merge changes it, the page is marked possibly out of | |
| 302 | * date (services/docs src/staleness.ts). | |
| 303 | */ | |
| 304 | export const Citation = createReactInlineContentSpec( | |
| 305 | { | |
| 306 | type: "citation", | |
| 307 | propSchema: { repo: { default: "" }, path: { default: "" }, kind: { default: "path" }, label: { default: "" }, ref: { default: "" } }, | |
| 308 | content: "none", | |
| 309 | }, | |
| 310 | { | |
| 311 | render: ({ inlineContent }) => { | |
| 312 | const p = inlineContent.props; | |
| 313 | const shown = p.kind !== "path" && p.label ? p.label : p.path; | |
| 314 | return ( | |
| 315 | <a | |
| 316 | href={citationHref({ repo: p.repo, path: p.path, ref: p.ref || null })} | |
| 317 | aria-label={`${shown} in ${p.repo}${p.kind !== "path" ? `, ${p.path}` : ""}`} | |
| 318 | className="inline-flex items-center gap-1 rounded border border-line bg-raised px-1 py-px align-baseline font-mono text-[0.85em] text-fg no-underline hover:border-line-strong" | |
| 319 | > | |
| 320 | <span className="text-faint">{CITATION_ICONS[p.kind] ?? CITATION_ICONS.path}</span> | |
| 321 | {shown} | |
| 322 | </a> | |
| 323 | ); | |
| 324 | }, | |
| 325 | }, | |
| 326 | ); | |
| 327 | ||
| 328 | export const schema = BlockNoteSchema.create({ | |
| 329 | blockSpecs: { | |
| 330 | ...defaultBlockSpecs, | |
| 331 | codeBlock: createCodeBlockSpec(codeBlockOptions), | |
| 332 | callout: Callout(), | |
| 333 | mermaid: Mermaid(), | |
| 334 | math: MathBlock(), | |
| 335 | embed: Embed(), | |
| 336 | }, | |
| 337 | inlineContentSpecs: { ...defaultInlineContentSpecs, mention: Mention, date: DateChip, citation: Citation }, | |
| 338 | }); | |
| 339 | ||
| 340 | export type DocSchema = typeof schema; | |
| 341 | export type DocEditorInstance = typeof schema.BlockNoteEditor; |