@brett_lamy/docstream-editor 0.5.5 → 0.5.7
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 +2 -2
- package/src/editor/Editor.tsx +29 -2
- package/src/editor/ReactDemoEditor.tsx +39 -23
- package/src/editor/SourceFileEditor.tsx +20 -5
- package/src/editor/nodes.tsx +80 -16
- package/src/editor/runtime.tsx +21 -0
- package/src/styles.css +63 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brett_lamy/docstream-editor",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"description": "TipTap editor for Docstream GitBook-style markdown documents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"./styles.css": "./src/styles.css"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@brett_lamy/docstream": "
|
|
36
|
+
"@brett_lamy/docstream": "0.5.6",
|
|
37
37
|
"lowlight": "^3.3.0",
|
|
38
38
|
"lucide-react": "^1.17.0"
|
|
39
39
|
},
|
package/src/editor/Editor.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useRef } from "react"
|
|
1
|
+
import { useEffect, useMemo, useRef } from "react"
|
|
2
2
|
import { EditorContent, useEditor, type Editor as TiptapEditor } from "@tiptap/react"
|
|
3
3
|
import type { AnyExtension } from "@tiptap/core"
|
|
4
4
|
import {
|
|
@@ -11,8 +11,10 @@ import {
|
|
|
11
11
|
} from "lucide-react"
|
|
12
12
|
|
|
13
13
|
import { parseMarkdown, serializeMarkdown, type CitationDef } from "@brett_lamy/docstream/gitbook"
|
|
14
|
+
import type { SourceFileSnapshot, SourceReferenceClient } from "@brett_lamy/docstream/source"
|
|
14
15
|
import { astToTiptap, tiptapToAst, type PMNode } from "./convert"
|
|
15
16
|
import { createGitbookExtensions } from "./extensions"
|
|
17
|
+
import { EditorRuntimeProvider } from "./runtime"
|
|
16
18
|
import type { SlashItem } from "./slash-menu"
|
|
17
19
|
|
|
18
20
|
export interface GitbookEditorProps {
|
|
@@ -47,6 +49,16 @@ export interface GitbookEditorProps {
|
|
|
47
49
|
onPaste?: (event: ClipboardEvent) => boolean
|
|
48
50
|
/** Receive the underlying TipTap editor instance (and null on teardown). */
|
|
49
51
|
onEditorReady?: (editor: TiptapEditor | null) => void
|
|
52
|
+
/** Client used to edit and preview `{% source-ref %}` files. */
|
|
53
|
+
sourceClient?: SourceReferenceClient
|
|
54
|
+
/** Show the rendered export below a file-backed source editor (default true). */
|
|
55
|
+
sourcePreview?: boolean
|
|
56
|
+
/** Autosave file-backed source edits. `true` defaults to 500ms; a number sets the delay. */
|
|
57
|
+
sourceAutoSave?: boolean | number
|
|
58
|
+
/** Called after a file-backed source edit is written. */
|
|
59
|
+
onSourceSaved?: (snapshot: SourceFileSnapshot) => void
|
|
60
|
+
/** Called when reading, writing, or previewing a file-backed source fails. */
|
|
61
|
+
onSourceError?: (error: Error) => void
|
|
50
62
|
}
|
|
51
63
|
|
|
52
64
|
function ToolbarButton({
|
|
@@ -124,6 +136,11 @@ export function GitbookEditor({
|
|
|
124
136
|
onKeyDown,
|
|
125
137
|
onPaste,
|
|
126
138
|
onEditorReady,
|
|
139
|
+
sourceClient,
|
|
140
|
+
sourcePreview = true,
|
|
141
|
+
sourceAutoSave = true,
|
|
142
|
+
onSourceSaved,
|
|
143
|
+
onSourceError,
|
|
127
144
|
}: GitbookEditorProps) {
|
|
128
145
|
// Tracks the markdown the editor itself produced, so external updates
|
|
129
146
|
// (file switches) reset content but our own onChange echoes don't.
|
|
@@ -179,12 +196,22 @@ export function GitbookEditor({
|
|
|
179
196
|
editor.commands.setContent(astToTiptap(doc), { emitUpdate: false })
|
|
180
197
|
}, [editor, controlled, markdown])
|
|
181
198
|
|
|
199
|
+
const runtime = useMemo(() => ({
|
|
200
|
+
...(sourceClient ? { sourceClient } : {}),
|
|
201
|
+
sourcePreview,
|
|
202
|
+
sourceAutoSave,
|
|
203
|
+
...(onSourceSaved ? { onSourceSaved } : {}),
|
|
204
|
+
...(onSourceError ? { onSourceError } : {}),
|
|
205
|
+
}), [onSourceError, onSourceSaved, sourceAutoSave, sourceClient, sourcePreview])
|
|
206
|
+
|
|
182
207
|
if (!editor) return null
|
|
183
208
|
|
|
184
209
|
return (
|
|
185
210
|
<div className={className ? `gb ${className}` : "gb"}>
|
|
186
211
|
{toolbar && <Toolbar editor={editor} />}
|
|
187
|
-
<
|
|
212
|
+
<EditorRuntimeProvider value={runtime}>
|
|
213
|
+
<EditorContent editor={editor} className="gb-content" />
|
|
214
|
+
</EditorRuntimeProvider>
|
|
188
215
|
</div>
|
|
189
216
|
)
|
|
190
217
|
}
|
|
@@ -13,6 +13,10 @@ export interface ReactDemoEditorProps {
|
|
|
13
13
|
className?: string
|
|
14
14
|
/** Called whenever a file is edited. */
|
|
15
15
|
onChange?: (files: Record<string, string>) => void
|
|
16
|
+
/** Refresh the AlmostNode preview as files are edited (default true). */
|
|
17
|
+
autoPreview?: boolean
|
|
18
|
+
/** Delay before a live preview receives edited files (default 350ms). */
|
|
19
|
+
previewDebounceMs?: number
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
function nextFile(files: Readonly<Record<string, string>>): string {
|
|
@@ -26,17 +30,29 @@ export function ReactDemoEditor({
|
|
|
26
30
|
height = 320,
|
|
27
31
|
className,
|
|
28
32
|
onChange,
|
|
33
|
+
autoPreview = true,
|
|
34
|
+
previewDebounceMs = 350,
|
|
29
35
|
}: ReactDemoEditorProps) {
|
|
30
36
|
const [draftFiles, setDraftFiles] = useState<Record<string, string>>(() => ({ ...files }))
|
|
31
37
|
const [selectedFile, setSelectedFile] = useState(() => nextFile(files))
|
|
32
|
-
const [previewFiles, setPreviewFiles] = useState<Record<string, string
|
|
38
|
+
const [previewFiles, setPreviewFiles] = useState<Record<string, string>>(() => ({ ...files }))
|
|
33
39
|
const [previewRevision, setPreviewRevision] = useState(0)
|
|
34
40
|
|
|
35
41
|
useEffect(() => {
|
|
36
42
|
setDraftFiles({ ...files })
|
|
43
|
+
setPreviewFiles({ ...files })
|
|
37
44
|
setSelectedFile((current) => (current && files[current] !== undefined ? current : nextFile(files)))
|
|
38
45
|
}, [files])
|
|
39
46
|
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!autoPreview) return
|
|
49
|
+
const timer = window.setTimeout(
|
|
50
|
+
() => setPreviewFiles({ ...draftFiles }),
|
|
51
|
+
Math.max(0, previewDebounceMs),
|
|
52
|
+
)
|
|
53
|
+
return () => window.clearTimeout(timer)
|
|
54
|
+
}, [autoPreview, draftFiles, previewDebounceMs])
|
|
55
|
+
|
|
40
56
|
const updateFile = (content: string) => {
|
|
41
57
|
if (!selectedFile) return
|
|
42
58
|
const next = { ...draftFiles, [selectedFile]: content }
|
|
@@ -48,6 +64,13 @@ export function ReactDemoEditor({
|
|
|
48
64
|
|
|
49
65
|
return (
|
|
50
66
|
<section className={wrapperClass} data-docstream-react-demo-editor="">
|
|
67
|
+
<ReactCodePreview
|
|
68
|
+
key={previewRevision}
|
|
69
|
+
files={previewFiles}
|
|
70
|
+
entry={entry}
|
|
71
|
+
height={height}
|
|
72
|
+
title="React demo preview"
|
|
73
|
+
/>
|
|
51
74
|
<div className="gb-react-demo-editor-source">
|
|
52
75
|
<aside className="gb-react-demo-files" aria-label="Demo files">
|
|
53
76
|
<div className="gb-react-demo-files-header">
|
|
@@ -70,17 +93,21 @@ export function ReactDemoEditor({
|
|
|
70
93
|
<div className="gb-react-demo-editor-pane">
|
|
71
94
|
<div className="gb-react-demo-editor-toolbar">
|
|
72
95
|
<code>{selectedFile || "No file selected"}</code>
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
96
|
+
{autoPreview ? (
|
|
97
|
+
<span className="gb-react-demo-live-status">Live preview</span>
|
|
98
|
+
) : (
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
className="gb-react-demo-preview-button"
|
|
102
|
+
disabled={!selectedFile}
|
|
103
|
+
onClick={() => {
|
|
104
|
+
setPreviewFiles({ ...draftFiles })
|
|
105
|
+
setPreviewRevision((value) => value + 1)
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
<Play className="size-3.5" /> Run demo
|
|
109
|
+
</button>
|
|
110
|
+
)}
|
|
84
111
|
</div>
|
|
85
112
|
<textarea
|
|
86
113
|
className="gb-react-demo-textarea"
|
|
@@ -92,17 +119,6 @@ export function ReactDemoEditor({
|
|
|
92
119
|
/>
|
|
93
120
|
</div>
|
|
94
121
|
</div>
|
|
95
|
-
{previewFiles ? (
|
|
96
|
-
<ReactCodePreview
|
|
97
|
-
key={previewRevision}
|
|
98
|
-
files={previewFiles}
|
|
99
|
-
entry={entry}
|
|
100
|
-
height={height}
|
|
101
|
-
title="React demo preview"
|
|
102
|
-
/>
|
|
103
|
-
) : (
|
|
104
|
-
<div className="gb-react-demo-editor-empty">Edit a file, then run the demo to preview the complete project.</div>
|
|
105
|
-
)}
|
|
106
122
|
</section>
|
|
107
123
|
)
|
|
108
124
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useState } from "react"
|
|
1
|
+
import { useCallback, useEffect, useState } from "react"
|
|
2
2
|
import { FileCode2, Save } from "lucide-react"
|
|
3
3
|
import { SourcePreview, type SourceFileSnapshot, type SourceReferenceClient } from "@brett_lamy/docstream/source"
|
|
4
4
|
import type { SourceRefNode } from "@brett_lamy/docstream/gitbook"
|
|
@@ -8,6 +8,8 @@ export interface SourceFileEditorProps {
|
|
|
8
8
|
client: SourceReferenceClient
|
|
9
9
|
className?: string
|
|
10
10
|
preview?: boolean
|
|
11
|
+
/** Automatically write edits after a pause. `true` defaults to 500ms. */
|
|
12
|
+
autoSave?: boolean | number
|
|
11
13
|
onSaved?: (snapshot: SourceFileSnapshot) => void
|
|
12
14
|
onError?: (error: Error) => void
|
|
13
15
|
}
|
|
@@ -17,7 +19,7 @@ function asError(cause: unknown): Error {
|
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
/** Edit the real file behind a Markdown source reference and preview its export. */
|
|
20
|
-
export function SourceFileEditor({ reference, client, className, preview = true, onSaved, onError }: SourceFileEditorProps) {
|
|
22
|
+
export function SourceFileEditor({ reference, client, className, preview = true, autoSave = false, onSaved, onError }: SourceFileEditorProps) {
|
|
21
23
|
const [content, setContent] = useState("")
|
|
22
24
|
const [savedContent, setSavedContent] = useState("")
|
|
23
25
|
const [revision, setRevision] = useState(0)
|
|
@@ -48,7 +50,7 @@ export function SourceFileEditor({ reference, client, className, preview = true,
|
|
|
48
50
|
}
|
|
49
51
|
}, [client, onError, reference])
|
|
50
52
|
|
|
51
|
-
const save = async () => {
|
|
53
|
+
const save = useCallback(async () => {
|
|
52
54
|
setStatus("saving")
|
|
53
55
|
setError(null)
|
|
54
56
|
try {
|
|
@@ -63,7 +65,19 @@ export function SourceFileEditor({ reference, client, className, preview = true,
|
|
|
63
65
|
setStatus("error")
|
|
64
66
|
onError?.(next)
|
|
65
67
|
}
|
|
66
|
-
}
|
|
68
|
+
}, [client, content, onError, onSaved, reference])
|
|
69
|
+
|
|
70
|
+
const autoSaveDelay = autoSave === true
|
|
71
|
+
? 500
|
|
72
|
+
: typeof autoSave === "number" && Number.isFinite(autoSave)
|
|
73
|
+
? Math.max(0, autoSave)
|
|
74
|
+
: null
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (autoSaveDelay === null || status === "loading" || status === "saving" || content === savedContent) return
|
|
78
|
+
const timer = window.setTimeout(() => void save(), autoSaveDelay)
|
|
79
|
+
return () => window.clearTimeout(timer)
|
|
80
|
+
}, [autoSaveDelay, content, save, savedContent, status])
|
|
67
81
|
|
|
68
82
|
const wrapper = className ? `gb-source-file-editor ${className}` : "gb-source-file-editor"
|
|
69
83
|
return (
|
|
@@ -71,8 +85,9 @@ export function SourceFileEditor({ reference, client, className, preview = true,
|
|
|
71
85
|
<header className="gb-source-file-editor-header">
|
|
72
86
|
<FileCode2 className="size-4" />
|
|
73
87
|
<code>{reference.mount}:{reference.path}#{reference.exportName}</code>
|
|
88
|
+
{autoSaveDelay !== null ? <span className="gb-source-file-editor-status">{status === "saving" ? "Saving…" : content === savedContent ? "Saved" : "Unsaved"}</span> : null}
|
|
74
89
|
<button type="button" disabled={status === "loading" || status === "saving" || content === savedContent} onClick={() => void save()}>
|
|
75
|
-
<Save className="size-3.5" /> {status === "saving" ? "Saving…" : "Save file"}
|
|
90
|
+
<Save className="size-3.5" /> {status === "saving" ? "Saving…" : autoSaveDelay === null ? "Save file" : "Save now"}
|
|
76
91
|
</button>
|
|
77
92
|
</header>
|
|
78
93
|
{error ? <pre className="gb-source-file-editor-error">{error}</pre> : null}
|
package/src/editor/nodes.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Node, mergeAttributes } from "@tiptap/core"
|
|
2
2
|
import { CodeBlockLowlight } from "@tiptap/extension-code-block-lowlight"
|
|
3
3
|
import { common, createLowlight } from "lowlight"
|
|
4
|
+
import { useEffect, useMemo, useState } from "react"
|
|
4
5
|
import {
|
|
5
6
|
NodeViewContent,
|
|
6
7
|
NodeViewWrapper,
|
|
@@ -21,11 +22,24 @@ import {
|
|
|
21
22
|
XCircle,
|
|
22
23
|
} from "lucide-react"
|
|
23
24
|
|
|
25
|
+
import { DocsRenderer } from "@brett_lamy/docstream"
|
|
24
26
|
import { resolveAsset } from "@brett_lamy/docstream/assets"
|
|
25
27
|
import { OpenApiOperation } from "@brett_lamy/docstream/openapi"
|
|
26
|
-
import type { HintStyle } from "@brett_lamy/docstream/gitbook"
|
|
28
|
+
import type { DocumentNode, HintStyle, SourceRefNode } from "@brett_lamy/docstream/gitbook"
|
|
29
|
+
import { ReactCodePreview } from "@brett_lamy/docstream/playground"
|
|
27
30
|
import { ReplayPreview, isReplayQaUrl } from "@brett_lamy/docstream/replay"
|
|
28
31
|
import { VideoEmbed } from "@brett_lamy/docstream/video"
|
|
32
|
+
import { SourceFileEditor } from "./SourceFileEditor"
|
|
33
|
+
import { useEditorRuntime } from "./runtime"
|
|
34
|
+
|
|
35
|
+
function useDebouncedValue<T>(value: T, delay = 350): T {
|
|
36
|
+
const [debounced, setDebounced] = useState(value)
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const timer = window.setTimeout(() => setDebounced(value), delay)
|
|
39
|
+
return () => window.clearTimeout(timer)
|
|
40
|
+
}, [delay, value])
|
|
41
|
+
return debounced
|
|
42
|
+
}
|
|
29
43
|
|
|
30
44
|
// ---------- Hint ----------
|
|
31
45
|
|
|
@@ -433,22 +447,44 @@ export const GbContentRef = Node.create({
|
|
|
433
447
|
|
|
434
448
|
function SourceRefView({ node, updateAttributes, editor }: NodeViewProps) {
|
|
435
449
|
const editable = editor.isEditable
|
|
450
|
+
const { sourceClient, sourcePreview = true, sourceAutoSave = true, onSourceSaved, onSourceError } = useEditorRuntime()
|
|
451
|
+
const reference = useMemo<SourceRefNode>(() => ({
|
|
452
|
+
type: "source-ref",
|
|
453
|
+
mount: String(node.attrs.mount || "source"),
|
|
454
|
+
path: String(node.attrs.path || ""),
|
|
455
|
+
exportName: String(node.attrs.exportName || "default"),
|
|
456
|
+
kind: node.attrs.kind === "story" ? "story" : "component",
|
|
457
|
+
title: String(node.attrs.title || ""),
|
|
458
|
+
}), [node.attrs.exportName, node.attrs.kind, node.attrs.mount, node.attrs.path, node.attrs.title])
|
|
436
459
|
return (
|
|
437
|
-
<NodeViewWrapper className="gb-source-ref" contentEditable={false}>
|
|
438
|
-
<
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
<
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
460
|
+
<NodeViewWrapper className={sourceClient && reference.path ? "gb-source-ref-block gb-source-ref-block-editing" : "gb-source-ref-block"} contentEditable={false}>
|
|
461
|
+
<div className="gb-source-ref">
|
|
462
|
+
<FileCode2 className="size-4 shrink-0" />
|
|
463
|
+
{editable ? (
|
|
464
|
+
<>
|
|
465
|
+
<input className="gb-inline-input gb-source-ref-mount" value={node.attrs.mount} placeholder="mount" onChange={(event) => updateAttributes({ mount: event.target.value })} />
|
|
466
|
+
<input className="gb-inline-input gb-source-ref-path" value={node.attrs.path} placeholder="src/Button.tsx" onChange={(event) => updateAttributes({ path: event.target.value })} />
|
|
467
|
+
<input className="gb-inline-input gb-source-ref-export" value={node.attrs.exportName} placeholder="Button" onChange={(event) => updateAttributes({ exportName: event.target.value })} />
|
|
468
|
+
<select className="gb-source-ref-kind" value={node.attrs.kind} onChange={(event) => updateAttributes({ kind: event.target.value })}>
|
|
469
|
+
<option value="component">component</option>
|
|
470
|
+
<option value="story">story</option>
|
|
471
|
+
</select>
|
|
472
|
+
</>
|
|
473
|
+
) : (
|
|
474
|
+
<span>{node.attrs.title || `${node.attrs.path}#${node.attrs.exportName}`}</span>
|
|
475
|
+
)}
|
|
476
|
+
</div>
|
|
477
|
+
{sourceClient && reference.path ? (
|
|
478
|
+
<SourceFileEditor
|
|
479
|
+
reference={reference}
|
|
480
|
+
client={sourceClient}
|
|
481
|
+
preview={sourcePreview}
|
|
482
|
+
autoSave={editable ? sourceAutoSave : false}
|
|
483
|
+
onSaved={onSourceSaved}
|
|
484
|
+
onError={onSourceError}
|
|
485
|
+
className="gb-source-ref-editor"
|
|
486
|
+
/>
|
|
487
|
+
) : null}
|
|
452
488
|
</NodeViewWrapper>
|
|
453
489
|
)
|
|
454
490
|
}
|
|
@@ -597,8 +633,36 @@ export const GbMath = Node.create({
|
|
|
597
633
|
// ---------- Code block with GitBook title/lineNumbers ----------
|
|
598
634
|
|
|
599
635
|
function CodeView({ node, updateAttributes, editor }: NodeViewProps) {
|
|
636
|
+
const code = node.textContent
|
|
637
|
+
const previewCode = useDebouncedValue(code)
|
|
638
|
+
const language = String(node.attrs.language ?? "").toLowerCase()
|
|
639
|
+
const mermaid = language === "mermaid"
|
|
640
|
+
const almostNode = !!node.attrs.live && (language === "jsx" || language === "tsx" || language === "react")
|
|
641
|
+
const entry = String(node.attrs.entry || (language === "tsx" ? "/src/main.tsx" : "/src/main.jsx"))
|
|
642
|
+
const files = useMemo(() => ({ [entry]: previewCode }), [entry, previewCode])
|
|
643
|
+
const mermaidDocument = useMemo<DocumentNode>(() => ({
|
|
644
|
+
type: "doc",
|
|
645
|
+
children: [{ type: "code", language: "mermaid", code: previewCode, title: null, lineNumbers: true }],
|
|
646
|
+
}), [previewCode])
|
|
647
|
+
|
|
600
648
|
return (
|
|
601
649
|
<NodeViewWrapper className="gb-code">
|
|
650
|
+
{mermaid ? (
|
|
651
|
+
<div className="gb-code-preview gb-code-mermaid-preview" contentEditable={false}>
|
|
652
|
+
<DocsRenderer doc={mermaidDocument} />
|
|
653
|
+
</div>
|
|
654
|
+
) : null}
|
|
655
|
+
{almostNode ? (
|
|
656
|
+
<div className="gb-code-preview gb-code-almost-preview" contentEditable={false}>
|
|
657
|
+
<ReactCodePreview
|
|
658
|
+
files={files}
|
|
659
|
+
entry={entry}
|
|
660
|
+
codeFile={entry}
|
|
661
|
+
height={300}
|
|
662
|
+
title={node.attrs.title || "Live preview"}
|
|
663
|
+
/>
|
|
664
|
+
</div>
|
|
665
|
+
) : null}
|
|
602
666
|
<div className="gb-code-header" contentEditable={false}>
|
|
603
667
|
{editor.isEditable ? (
|
|
604
668
|
<>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createContext, useContext, type ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
import type { SourceFileSnapshot, SourceReferenceClient } from "@brett_lamy/docstream/source"
|
|
4
|
+
|
|
5
|
+
export interface EditorRuntimeOptions {
|
|
6
|
+
sourceClient?: SourceReferenceClient
|
|
7
|
+
sourcePreview?: boolean
|
|
8
|
+
sourceAutoSave?: boolean | number
|
|
9
|
+
onSourceSaved?: (snapshot: SourceFileSnapshot) => void
|
|
10
|
+
onSourceError?: (error: Error) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const EditorRuntimeContext = createContext<EditorRuntimeOptions>({})
|
|
14
|
+
|
|
15
|
+
export function EditorRuntimeProvider({ value, children }: { value: EditorRuntimeOptions; children: ReactNode }) {
|
|
16
|
+
return <EditorRuntimeContext.Provider value={value}>{children}</EditorRuntimeContext.Provider>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useEditorRuntime(): EditorRuntimeOptions {
|
|
20
|
+
return useContext(EditorRuntimeContext)
|
|
21
|
+
}
|
package/src/styles.css
CHANGED
|
@@ -62,6 +62,23 @@
|
|
|
62
62
|
background: var(--gb-muted);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
.gb-source-ref-block {
|
|
66
|
+
margin: 0.85em 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.gb-source-ref-block > .gb-source-ref {
|
|
70
|
+
margin: 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.gb-source-ref-block > .gb-source-ref-editor {
|
|
74
|
+
margin-top: -1px;
|
|
75
|
+
border-radius: 0 0 var(--gb-radius) var(--gb-radius);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.gb-source-ref-block-editing > .gb-source-ref {
|
|
79
|
+
border-radius: var(--gb-radius) var(--gb-radius) 0 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
65
82
|
.gb-source-ref-path {
|
|
66
83
|
flex: 1;
|
|
67
84
|
}
|
|
@@ -89,6 +106,16 @@
|
|
|
89
106
|
gap: 0.35rem;
|
|
90
107
|
}
|
|
91
108
|
|
|
109
|
+
.gb-source-file-editor-status {
|
|
110
|
+
margin-left: auto;
|
|
111
|
+
color: var(--gb-muted-foreground);
|
|
112
|
+
font-size: 11px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.gb-source-file-editor-status + button {
|
|
116
|
+
margin-left: 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
92
119
|
.gb-source-file-editor > textarea {
|
|
93
120
|
box-sizing: border-box;
|
|
94
121
|
width: 100%;
|
|
@@ -502,6 +529,24 @@
|
|
|
502
529
|
margin: 0.85em 0;
|
|
503
530
|
overflow: hidden;
|
|
504
531
|
}
|
|
532
|
+
.gb-code-preview {
|
|
533
|
+
min-height: 120px;
|
|
534
|
+
border-bottom: 1px solid var(--gb-border);
|
|
535
|
+
background: var(--gb-panel);
|
|
536
|
+
}
|
|
537
|
+
.gb-code-preview .docs-article {
|
|
538
|
+
margin: 0;
|
|
539
|
+
padding: 14px;
|
|
540
|
+
}
|
|
541
|
+
.gb-code-mermaid-preview .docs-mermaid-code,
|
|
542
|
+
.gb-code-almost-preview .docs-react-demo-code {
|
|
543
|
+
display: none;
|
|
544
|
+
}
|
|
545
|
+
.gb-code-almost-preview .docs-react-demo {
|
|
546
|
+
margin: 0;
|
|
547
|
+
border: 0;
|
|
548
|
+
border-radius: 0;
|
|
549
|
+
}
|
|
505
550
|
.gb-code-header {
|
|
506
551
|
display: flex;
|
|
507
552
|
align-items: center;
|
|
@@ -526,7 +571,7 @@
|
|
|
526
571
|
color: var(--gb-muted-foreground);
|
|
527
572
|
font-size: 11px;
|
|
528
573
|
}
|
|
529
|
-
.gb-code pre {
|
|
574
|
+
.gb-code > pre {
|
|
530
575
|
margin: 0;
|
|
531
576
|
padding: 13px 15px;
|
|
532
577
|
overflow-x: auto;
|
|
@@ -536,8 +581,8 @@
|
|
|
536
581
|
background: var(--gb-code-bg);
|
|
537
582
|
color: var(--gb-code-fg);
|
|
538
583
|
}
|
|
539
|
-
.gb-code pre code { background: none; padding: 0; font-size: inherit; color: inherit; }
|
|
540
|
-
.gb-code pre code .hljs-comment { font-style: italic; }
|
|
584
|
+
.gb-code > pre code { background: none; padding: 0; font-size: inherit; color: inherit; }
|
|
585
|
+
.gb-code > pre code .hljs-comment { font-style: italic; }
|
|
541
586
|
.gb-code .hljs-keyword,
|
|
542
587
|
.gb-code .hljs-selector-tag,
|
|
543
588
|
.gb-code .hljs-built_in,
|
|
@@ -646,6 +691,15 @@
|
|
|
646
691
|
border-radius: 12px;
|
|
647
692
|
background: var(--gb-panel, var(--card, #ffffff));
|
|
648
693
|
}
|
|
694
|
+
.gb-react-demo-editor > .docs-react-demo {
|
|
695
|
+
margin: 0;
|
|
696
|
+
border: 0;
|
|
697
|
+
border-radius: 0;
|
|
698
|
+
border-bottom: 1px solid var(--gb-border, var(--border, #e4e4e7));
|
|
699
|
+
}
|
|
700
|
+
.gb-react-demo-editor > .docs-react-demo .docs-react-demo-code {
|
|
701
|
+
display: none;
|
|
702
|
+
}
|
|
649
703
|
.gb-react-demo-editor-source {
|
|
650
704
|
display: grid;
|
|
651
705
|
grid-template-columns: minmax(150px, 0.28fr) minmax(0, 1fr);
|
|
@@ -708,6 +762,12 @@
|
|
|
708
762
|
font-size: 11px;
|
|
709
763
|
}
|
|
710
764
|
.gb-react-demo-preview-button:disabled { opacity: 0.5; }
|
|
765
|
+
.gb-react-demo-live-status {
|
|
766
|
+
margin-left: auto;
|
|
767
|
+
color: var(--gb-success, #22c55e);
|
|
768
|
+
font-size: 11px;
|
|
769
|
+
font-weight: 600;
|
|
770
|
+
}
|
|
711
771
|
.gb-react-demo-textarea {
|
|
712
772
|
flex: 1;
|
|
713
773
|
width: 100%;
|