@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brett_lamy/docstream-editor",
3
- "version": "0.5.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": "^0.5.5",
36
+ "@brett_lamy/docstream": "0.5.6",
37
37
  "lowlight": "^3.3.0",
38
38
  "lucide-react": "^1.17.0"
39
39
  },
@@ -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
- <EditorContent editor={editor} className="gb-content" />
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> | null>(null)
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
- <button
74
- type="button"
75
- className="gb-react-demo-preview-button"
76
- disabled={!selectedFile}
77
- onClick={() => {
78
- setPreviewFiles({ ...draftFiles })
79
- setPreviewRevision((value) => value + 1)
80
- }}
81
- >
82
- <Play className="size-3.5" /> Run demo
83
- </button>
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}
@@ -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
- <FileCode2 className="size-4 shrink-0" />
439
- {editable ? (
440
- <>
441
- <input className="gb-inline-input gb-source-ref-mount" value={node.attrs.mount} placeholder="mount" onChange={(event) => updateAttributes({ mount: event.target.value })} />
442
- <input className="gb-inline-input gb-source-ref-path" value={node.attrs.path} placeholder="src/Button.tsx" onChange={(event) => updateAttributes({ path: event.target.value })} />
443
- <input className="gb-inline-input gb-source-ref-export" value={node.attrs.exportName} placeholder="Button" onChange={(event) => updateAttributes({ exportName: event.target.value })} />
444
- <select className="gb-source-ref-kind" value={node.attrs.kind} onChange={(event) => updateAttributes({ kind: event.target.value })}>
445
- <option value="component">component</option>
446
- <option value="story">story</option>
447
- </select>
448
- </>
449
- ) : (
450
- <span>{node.attrs.title || `${node.attrs.path}#${node.attrs.exportName}`}</span>
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%;