@brett_lamy/docstream 0.3.7 → 0.4.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.3.7",
3
+ "version": "0.4.0",
4
4
  "description": "GitBook-aware readonly markdown and AI stream renderer.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,7 +1,10 @@
1
- import { useEffect, useState, type CSSProperties } from "react"
1
+ import { useEffect, useId, useMemo, useState, type CSSProperties } from "react"
2
+ import { HighlightedCode } from "../docs/HighlightedCode"
2
3
  import {
3
4
  createAlmostNodeWorkspace,
4
5
  createReactDemoFiles,
6
+ normalizeAlmostNodeFiles,
7
+ normalizeAlmostNodePath,
5
8
  type AlmostNodeFiles,
6
9
  type AlmostNodeWorkspace,
7
10
  type AlmostNodeWorkspaceOptions,
@@ -16,6 +19,12 @@ export interface ReactDemoProps {
16
19
  port?: number
17
20
  /** Height of the embedded preview. */
18
21
  height?: number | string
22
+ /** Source file displayed below the preview. Defaults to the entry file. */
23
+ codeFile?: string
24
+ /** Number of source lines visible while the code preview is collapsed. Defaults to 3. */
25
+ collapsedCodeLines?: number
26
+ /** Maximum visible source lines before expanded code scrolls. Defaults to 30. */
27
+ expandedCodeLines?: number
19
28
  /** Label shown above the preview. */
20
29
  title?: string
21
30
  /** Start the runtime on mount (default true). */
@@ -43,12 +52,33 @@ function frameHeight(height: number | string): CSSProperties {
43
52
  return { height: typeof height === "number" ? `${height}px` : height }
44
53
  }
45
54
 
55
+ function positiveLineCount(value: number, fallback: number): number {
56
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback
57
+ }
58
+
59
+ function sourceLanguage(path: string): string | null {
60
+ const extension = path.split(".").pop()?.toLowerCase()
61
+ return extension && extension !== path ? extension : null
62
+ }
63
+
64
+ function sourceText(files: AlmostNodeFiles, requestedPath: string): string | null {
65
+ const normalizedPath = normalizeAlmostNodePath(requestedPath)
66
+ const file = normalizeAlmostNodeFiles(files).find(
67
+ (candidate) => normalizeAlmostNodePath(candidate.path) === normalizedPath,
68
+ )
69
+ if (!file) return null
70
+ return typeof file.content === "string" ? file.content : new TextDecoder().decode(file.content)
71
+ }
72
+
46
73
  /** Render a multi-file React/JSX/TSX project through almost-node's Vite server. */
47
74
  export function ReactDemo({
48
75
  files,
49
76
  entry = "/src/main.jsx",
50
77
  port = 4173,
51
78
  height = 360,
79
+ codeFile,
80
+ collapsedCodeLines = 3,
81
+ expandedCodeLines = 30,
52
82
  title = "Live React preview",
53
83
  autoStart = true,
54
84
  sandbox = "allow-scripts allow-same-origin allow-forms allow-modals",
@@ -61,6 +91,18 @@ export function ReactDemo({
61
91
  const [state, setState] = useState<PreviewState>(autoStart ? "starting" : "idle")
62
92
  const [url, setUrl] = useState<string | null>(null)
63
93
  const [error, setError] = useState<string | null>(null)
94
+ const [codeExpanded, setCodeExpanded] = useState(false)
95
+ const codePanelId = useId()
96
+ const displayedCodeFile = codeFile ?? entry
97
+ const code = useMemo(
98
+ () => sourceText(files, displayedCodeFile),
99
+ [displayedCodeFile, files],
100
+ )
101
+ const collapsedLines = positiveLineCount(collapsedCodeLines, 3)
102
+ const expandedLines = Math.max(
103
+ collapsedLines,
104
+ positiveLineCount(expandedCodeLines, 30),
105
+ )
64
106
 
65
107
  useEffect(() => {
66
108
  if (!run) {
@@ -158,6 +200,32 @@ export function ReactDemo({
158
200
  {running ? "Starting almost-node…" : "Run the demo to open its virtual filesystem."}
159
201
  </div>
160
202
  )}
203
+ {code !== null ? (
204
+ <div className="docs-react-demo-code">
205
+ <pre
206
+ id={codePanelId}
207
+ className={codeExpanded ? "docs-react-demo-code-body docs-react-demo-code-body-expanded" : "docs-react-demo-code-body"}
208
+ style={{
209
+ "--docs-react-demo-code-lines": codeExpanded ? expandedLines : collapsedLines,
210
+ } as CSSProperties}
211
+ >
212
+ <HighlightedCode
213
+ code={code}
214
+ language={sourceLanguage(displayedCodeFile)}
215
+ lineNumbers
216
+ />
217
+ </pre>
218
+ <button
219
+ type="button"
220
+ className="docs-react-demo-code-toggle"
221
+ aria-controls={codePanelId}
222
+ aria-expanded={codeExpanded}
223
+ onClick={() => setCodeExpanded((value) => !value)}
224
+ >
225
+ {codeExpanded ? "Hide Code" : "View Code"}
226
+ </button>
227
+ </div>
228
+ ) : null}
161
229
  </section>
162
230
  )
163
231
  }
package/src/styles.css CHANGED
@@ -775,27 +775,36 @@
775
775
  .hljs-comment,
776
776
  .hljs-quote,
777
777
  .hljs-meta {
778
- color: var(--gb-muted-foreground);
778
+ color: var(--gb-muted-foreground, #71717a);
779
779
  }
780
780
 
781
781
  .hljs-operator,
782
782
  .hljs-punctuation {
783
- color: var(--gb-code-fg);
783
+ color: var(--gb-code-fg, #18181b);
784
784
  }
785
785
 
786
786
  .hljs-deletion {
787
- color: var(--gb-danger);
787
+ color: var(--gb-danger, #ef4444);
788
788
  }
789
789
 
790
790
  .hljs-addition {
791
- color: var(--gb-success);
791
+ color: var(--gb-success, #22c55e);
792
792
  }
793
793
 
794
794
  .docs-react-demo {
795
+ --docs-react-demo-panel: var(--gb-panel, var(--card, #ffffff));
796
+ --docs-react-demo-foreground: var(--gb-panel-foreground, var(--card-foreground, #18181b));
797
+ --docs-react-demo-muted-foreground: var(--gb-muted-foreground, var(--muted-foreground, #71717a));
798
+ --docs-react-demo-border: var(--gb-border, var(--border, #e4e4e7));
799
+ --docs-react-demo-accent: var(--gb-accent, var(--accent, #f4f4f5));
800
+ --docs-react-demo-primary: var(--gb-primary, var(--primary, #4f46e5));
801
+ --docs-react-demo-code-bg: var(--gb-code-bg, rgba(128, 128, 128, 0.13));
802
+ --docs-react-demo-success: var(--gb-success, #22c55e);
803
+ --docs-react-demo-danger: var(--gb-danger, #ef4444);
795
804
  overflow: hidden;
796
- border: 1px solid var(--gb-border);
805
+ border: 1px solid var(--docs-react-demo-border);
797
806
  border-radius: 12px;
798
- background: var(--gb-panel);
807
+ background: var(--docs-react-demo-panel);
799
808
  }
800
809
 
801
810
  .docs-react-demo-header {
@@ -804,33 +813,33 @@
804
813
  gap: 10px;
805
814
  min-height: 42px;
806
815
  padding: 8px 12px;
807
- border-bottom: 1px solid var(--gb-border);
808
- color: var(--gb-panel-foreground);
816
+ border-bottom: 1px solid var(--docs-react-demo-border);
817
+ color: var(--docs-react-demo-foreground);
809
818
  font-size: 13px;
810
819
  font-weight: 550;
811
820
  }
812
821
 
813
822
  .docs-react-demo-status {
814
823
  margin-left: auto;
815
- color: var(--gb-muted-foreground);
824
+ color: var(--docs-react-demo-muted-foreground);
816
825
  font-size: 11px;
817
826
  text-transform: uppercase;
818
827
  letter-spacing: 0.08em;
819
828
  }
820
829
 
821
830
  .docs-react-demo-status-ready {
822
- color: var(--gb-success);
831
+ color: var(--docs-react-demo-success);
823
832
  }
824
833
 
825
834
  .docs-react-demo-status-error {
826
- color: var(--gb-danger);
835
+ color: var(--docs-react-demo-danger);
827
836
  }
828
837
 
829
838
  .docs-react-demo-run {
830
- border: 1px solid var(--gb-border);
839
+ border: 1px solid var(--docs-react-demo-border);
831
840
  border-radius: 6px;
832
841
  padding: 4px 9px;
833
- color: var(--gb-panel-foreground);
842
+ color: var(--docs-react-demo-foreground);
834
843
  font-size: 12px;
835
844
  cursor: pointer;
836
845
  }
@@ -860,7 +869,87 @@
860
869
  min-height: 100px;
861
870
  overflow: auto;
862
871
  padding: 14px;
863
- color: var(--gb-danger);
864
- background: var(--gb-code-bg);
872
+ color: var(--docs-react-demo-danger);
873
+ background: var(--docs-react-demo-code-bg);
865
874
  white-space: pre-wrap;
866
875
  }
876
+
877
+ .docs-react-demo-code {
878
+ position: relative;
879
+ border-top: 1px solid var(--docs-react-demo-border);
880
+ background: var(--docs-react-demo-code-bg);
881
+ }
882
+
883
+ .docs-react-demo-code-body {
884
+ --docs-react-demo-code-line-height: 1.6rem;
885
+ max-height: calc(var(--docs-react-demo-code-lines) * var(--docs-react-demo-code-line-height));
886
+ margin: 0;
887
+ overflow: hidden;
888
+ padding: 1.15rem 1rem 2.4rem;
889
+ font-size: 0.875rem;
890
+ line-height: var(--docs-react-demo-code-line-height);
891
+ tab-size: 2;
892
+ }
893
+
894
+ .docs-react-demo-code-body::after {
895
+ position: absolute;
896
+ right: 0;
897
+ bottom: 0;
898
+ left: 0;
899
+ height: 4.5rem;
900
+ background: linear-gradient(to bottom, transparent, var(--docs-react-demo-panel));
901
+ content: "";
902
+ pointer-events: none;
903
+ }
904
+
905
+ .docs-react-demo-code-body-expanded {
906
+ overflow: auto;
907
+ padding-bottom: 3.25rem;
908
+ }
909
+
910
+ .docs-react-demo-code-body-expanded::after {
911
+ display: none;
912
+ }
913
+
914
+ .docs-react-demo-code-body > code {
915
+ display: block;
916
+ min-width: max-content;
917
+ padding: 0;
918
+ background: transparent;
919
+ }
920
+
921
+ .docs-react-demo-code-body:not(.docs-react-demo-code-body-expanded) > code {
922
+ max-height: calc(var(--docs-react-demo-code-lines) * var(--docs-react-demo-code-line-height));
923
+ overflow: hidden;
924
+ }
925
+
926
+ .docs-react-demo-code-body .docs-code-line {
927
+ min-height: var(--docs-react-demo-code-line-height);
928
+ }
929
+
930
+ .docs-react-demo-code-toggle {
931
+ position: absolute;
932
+ bottom: 0.85rem;
933
+ left: 50%;
934
+ z-index: 1;
935
+ transform: translateX(-50%);
936
+ border: 1px solid var(--docs-react-demo-border);
937
+ border-radius: 0.65rem;
938
+ padding: 0.45rem 0.85rem;
939
+ color: var(--docs-react-demo-foreground);
940
+ background: var(--docs-react-demo-panel);
941
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
942
+ font: inherit;
943
+ font-size: 0.8125rem;
944
+ font-weight: 600;
945
+ cursor: pointer;
946
+ }
947
+
948
+ .docs-react-demo-code-toggle:hover {
949
+ background: var(--docs-react-demo-accent);
950
+ }
951
+
952
+ .docs-react-demo-code-toggle:focus-visible {
953
+ outline: 2px solid var(--docs-react-demo-primary);
954
+ outline-offset: 2px;
955
+ }