@brett_lamy/docstream 1.19.1 → 1.20.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.
Files changed (62) hide show
  1. package/README.md +24 -0
  2. package/dist/demo/DemoViewer.d.ts.map +1 -1
  3. package/dist/demo/DemoViewer.js +20 -5
  4. package/dist/demo/DemoViewer.js.map +1 -1
  5. package/dist/demo/types.d.ts +2 -0
  6. package/dist/demo/types.d.ts.map +1 -1
  7. package/dist/doc-env.d.ts +60 -0
  8. package/dist/doc-env.d.ts.map +1 -0
  9. package/dist/doc-env.js +123 -0
  10. package/dist/doc-env.js.map +1 -0
  11. package/dist/docs/DocsRenderer.d.ts +14 -2
  12. package/dist/docs/DocsRenderer.d.ts.map +1 -1
  13. package/dist/docs/DocsRenderer.js +34 -12
  14. package/dist/docs/DocsRenderer.js.map +1 -1
  15. package/dist/index.d.ts +3 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +2 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/playground/InlineDemoPreview.d.ts +1 -1
  20. package/dist/playground/InlineDemoPreview.d.ts.map +1 -1
  21. package/dist/playground/InlineDemoPreview.js +5 -5
  22. package/dist/playground/InlineDemoPreview.js.map +1 -1
  23. package/dist/playground/PlaygroundStreamdown.d.ts.map +1 -1
  24. package/dist/playground/PlaygroundStreamdown.js +7 -1
  25. package/dist/playground/PlaygroundStreamdown.js.map +1 -1
  26. package/dist/playground/project.d.ts +9 -0
  27. package/dist/playground/project.d.ts.map +1 -1
  28. package/dist/playground/project.js +18 -0
  29. package/dist/playground/project.js.map +1 -1
  30. package/dist/site/DocsSite.d.ts +15 -0
  31. package/dist/site/DocsSite.d.ts.map +1 -1
  32. package/dist/site/DocsSite.js +51 -8
  33. package/dist/site/DocsSite.js.map +1 -1
  34. package/dist/site/index.d.ts +2 -2
  35. package/dist/site/index.d.ts.map +1 -1
  36. package/dist/site/index.js +1 -1
  37. package/dist/site/index.js.map +1 -1
  38. package/dist/site/site.css +67 -0
  39. package/dist/site/tree.d.ts +1 -1
  40. package/dist/site/tree.d.ts.map +1 -1
  41. package/dist/site/tree.js +3 -2
  42. package/dist/site/tree.js.map +1 -1
  43. package/dist/streamdown.d.ts +1 -1
  44. package/dist/streamdown.d.ts.map +1 -1
  45. package/dist/streamdown.js +2 -2
  46. package/dist/streamdown.js.map +1 -1
  47. package/dist/styles.css +89 -0
  48. package/package.json +1 -1
  49. package/src/demo/DemoViewer.tsx +23 -4
  50. package/src/demo/types.ts +2 -0
  51. package/src/doc-env.tsx +198 -0
  52. package/src/docs/DocsRenderer.tsx +48 -12
  53. package/src/index.ts +3 -1
  54. package/src/playground/InlineDemoPreview.tsx +5 -4
  55. package/src/playground/PlaygroundStreamdown.tsx +11 -2
  56. package/src/playground/project.ts +20 -0
  57. package/src/site/DocsSite.tsx +79 -5
  58. package/src/site/index.ts +2 -2
  59. package/src/site/site.css +67 -0
  60. package/src/site/tree.ts +3 -2
  61. package/src/streamdown.tsx +4 -0
  62. package/src/styles.css +89 -0
@@ -24,6 +24,7 @@ import {
24
24
  import { ArrowLeft, ArrowRight, ChevronRight, FileText, Hash, Menu, Moon, PencilLine, Search, Sun, X } from "lucide-react"
25
25
 
26
26
  import { AssetResolverContext, AssetResolverProvider, type AssetResolver } from "../asset-resolver"
27
+ import { envNamesFromProperties } from "../doc-env"
27
28
  import { MarkdownContent, type DocRenderOptions, type LivePreviewRenderer, type SourceReferenceRenderer } from "../docs/DocsRenderer"
28
29
  import { DocPageActions, type DocPageActionsOptions } from "../docs/PageActions"
29
30
  import { documentOutline, type OutlineEntry } from "../gitbook/outline"
@@ -40,6 +41,44 @@ export interface DocsSiteLink {
40
41
  icon?: ReactNode
41
42
  }
42
43
 
44
+ /** A section of the site in the header, like GitBook's spaces: the sidebar shows only its pages. */
45
+ export interface DocsSiteTab {
46
+ title: string
47
+ /** Slug prefix of its pages: `"blocks"` takes `blocks` and `blocks/…`. `""` takes every page no other tab claims. */
48
+ path: string
49
+ /** Where the tab goes. Default: the first of its pages in reading order. */
50
+ slug?: string
51
+ }
52
+
53
+ const trimPath = (p: string) => p.replace(/^\/+|\/+$/g, "")
54
+
55
+ /** The tab a page belongs to: the longest `path` that prefixes its slug, else the catch-all (`""`) tab. */
56
+ export function tabForSlug(tabs: readonly DocsSiteTab[], slug: string): DocsSiteTab | undefined {
57
+ let best: DocsSiteTab | undefined
58
+ for (const t of tabs) {
59
+ const p = trimPath(t.path)
60
+ if (p && (slug === p || slug.startsWith(`${p}/`)) && (!best || p.length > trimPath(best.path).length)) best = t
61
+ }
62
+ return best ?? tabs.find((t) => !trimPath(t.path))
63
+ }
64
+
65
+ /** The sidebar items whose pages belong to `tab` (groups and folders keep only those; empty ones go). */
66
+ function navForTab(items: DocsNavItem[], tabs: readonly DocsSiteTab[], tab: DocsSiteTab): DocsNavItem[] {
67
+ const out: DocsNavItem[] = []
68
+ for (const item of items) {
69
+ if (item.kind === "link") {
70
+ if (tab === tabs.find((t) => !trimPath(t.path))) out.push(item)
71
+ continue
72
+ }
73
+ const children = navForTab(item.children, tabs, tab)
74
+ if (item.kind === "page") {
75
+ if (tabForSlug(tabs, item.slug) === tab) out.push({ ...item, children })
76
+ else out.push(...children)
77
+ } else if (children.length) out.push({ ...item, children })
78
+ }
79
+ return out
80
+ }
81
+
43
82
  export interface DocsSiteProps extends Omit<DocRenderOptions, "pageActions"> {
44
83
  /** The files to publish: `{ path: content }` or `[{ path, content }]`. Only Markdown is kept. */
45
84
  files?: DocsFiles
@@ -52,6 +91,11 @@ export interface DocsSiteProps extends Omit<DocRenderOptions, "pageActions"> {
52
91
  logo?: ReactNode
53
92
  /** Header links (GitHub, npm, …). */
54
93
  links?: DocsSiteLink[]
94
+ /**
95
+ * Sections in the header (`[{ title: "Components", path: "" }, { title: "Blocks", path: "blocks" }]`): each owns the
96
+ * pages under its `path`, the sidebar and Previous / Next stay inside the current one, and search covers them all.
97
+ */
98
+ tabs?: DocsSiteTab[]
55
99
  /** `"hash"` (default): `#/guides/install`. `"history"`: real paths under `basePath`. `"memory"`: no URL. */
56
100
  routing?: DocsSiteRouting
57
101
  /** URL prefix for `routing="history"` (e.g. `"/docs"`). */
@@ -549,6 +593,7 @@ export function DocsSite(props: DocsSiteProps) {
549
593
  title,
550
594
  logo,
551
595
  links = [],
596
+ tabs,
552
597
  routing = "hash",
553
598
  basePath = "",
554
599
  initialPath,
@@ -573,6 +618,7 @@ export function DocsSite(props: DocsSiteProps) {
573
618
  demoVFS,
574
619
  onTaskStateChange,
575
620
  documentProperties,
621
+ envHost,
576
622
  className,
577
623
  style,
578
624
  } = props
@@ -699,9 +745,11 @@ export function DocsSite(props: DocsSiteProps) {
699
745
  go(a.dataset.dsSlug ?? "", a.dataset.dsHash ?? "")
700
746
  }
701
747
 
702
- const index = shown ? tree.order.indexOf(shown) : -1
703
- const prev = index > 0 ? tree.order[index - 1] : undefined
704
- const next = index >= 0 ? tree.order[index + 1] : undefined
748
+ const activeTab = tabs?.length ? tabForSlug(tabs, shown?.slug ?? location.slug) : undefined
749
+ const order = activeTab ? tree.order.filter((p) => tabForSlug(tabs!, p.slug) === activeTab) : tree.order
750
+ const index = shown ? order.indexOf(shown) : -1
751
+ const prev = index > 0 ? order[index - 1] : undefined
752
+ const next = index >= 0 ? order[index + 1] : undefined
705
753
 
706
754
  const actions = shown ? (typeof pageActions === "function" ? pageActions(shown) : pageActions) : false
707
755
  const pageMarkdown = shown ? `# ${shown.title}\n\n${shown.description ? `${shown.description}\n\n` : ""}${shown.body}` : ""
@@ -720,6 +768,8 @@ export function DocsSite(props: DocsSiteProps) {
720
768
  demoVFS={demoVFS}
721
769
  onTaskStateChange={onTaskStateChange}
722
770
  documentProperties={documentProperties}
771
+ envHost={envHost}
772
+ env={envNamesFromProperties(shown.frontmatter)}
723
773
  />
724
774
  </AssetResolverProvider>
725
775
  ) : null
@@ -744,7 +794,28 @@ export function DocsSite(props: DocsSiteProps) {
744
794
  {title ? <span className="ds-site-title">{title}</span> : null}
745
795
  </a>
746
796
  )
747
- const navTree = <NavList items={tree.nav} current={shown?.slug ?? location.slug} href={(s) => href(s)} go={(s) => go(s)} />
797
+ const navItems = activeTab ? navForTab(tree.nav, tabs!, activeTab) : tree.nav
798
+ const navTree = <NavList items={navItems} current={shown?.slug ?? location.slug} href={(s) => href(s)} go={(s) => go(s)} />
799
+ const tabTarget = (t: DocsSiteTab) => t.slug ?? tree.order.find((p) => tabForSlug(tabs!, p.slug) === t)?.slug ?? trimPath(t.path)
800
+ const tabBar = (where: "header" | "drawer") => tabs?.length ? (
801
+ <nav className={`ds-site-tabs ds-site-tabs-${where}`} aria-label="Sections">
802
+ {tabs.map((t) => (
803
+ <a
804
+ key={t.title}
805
+ className="ds-site-tab"
806
+ href={href(tabTarget(t))}
807
+ aria-current={t === activeTab ? "page" : undefined}
808
+ onClick={(e) => {
809
+ if (!plainClick(e)) return
810
+ e.preventDefault()
811
+ go(tabTarget(t))
812
+ }}
813
+ >
814
+ {t.title}
815
+ </a>
816
+ ))}
817
+ </nav>
818
+ ) : null
748
819
 
749
820
  return (
750
821
  <div
@@ -760,6 +831,7 @@ export function DocsSite(props: DocsSiteProps) {
760
831
  <Menu size={18} aria-hidden="true" />
761
832
  </button>
762
833
  {brand}
834
+ {tabBar("header")}
763
835
  <div className="ds-site-header-spacer" />
764
836
  {search ? (
765
837
  <button type="button" className="ds-site-search-btn" onClick={() => setSearchOpen(true)} aria-label="Search documentation" aria-keyshortcuts="Meta+K Control+K">
@@ -792,7 +864,8 @@ export function DocsSite(props: DocsSiteProps) {
792
864
  </nav>
793
865
 
794
866
  <div className="ds-site-scroll" ref={scrollerRef}>
795
- <div className="ds-site-columns">
867
+ {/* Frontmatter `layout: wide` widens the column (galleries, full-width demos) and drops the outline. */}
868
+ <div className="ds-site-columns" data-layout={shown?.frontmatter.layout === "wide" ? "wide" : undefined}>
796
869
  <main className="ds-site-main" ref={articleRef} onClick={onArticleClick}>
797
870
  {shown ? (
798
871
  <article className="ds-site-article">
@@ -937,6 +1010,7 @@ export function DocsSite(props: DocsSiteProps) {
937
1010
  <X size={18} aria-hidden="true" />
938
1011
  </button>
939
1012
  </div>
1013
+ {tabBar("drawer")}
940
1014
  <div className="ds-site-sidebar-scroll">{navTree}</div>
941
1015
  {sidebarFooter ? <div className="ds-site-sidebar-foot">{sidebarFooter}</div> : null}
942
1016
  </nav>
package/src/site/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // A GitBook-style docs site from a folder of Markdown files. Root-entry safe: no almost-node.
2
- export { DocsSite } from "./DocsSite"
3
- export type { DocsSiteLink, DocsSiteProps, DocsSiteRouting, DocsSiteTheme } from "./DocsSite"
2
+ export { DocsSite, tabForSlug } from "./DocsSite"
3
+ export type { DocsSiteLink, DocsSiteProps, DocsSiteRouting, DocsSiteTab, DocsSiteTheme } from "./DocsSite"
4
4
  export {
5
5
  SITE_FRONTMATTER_KEYS,
6
6
  buildDocsTree,
package/src/site/site.css CHANGED
@@ -206,6 +206,61 @@
206
206
  color: var(--ds-site-fg);
207
207
  }
208
208
 
209
+ /* Header tabs: the site's sections (`tabs`). In the drawer they sit above the navigation. */
210
+ .ds-site .ds-site-tabs {
211
+ display: flex;
212
+ align-items: center;
213
+ gap: 2px;
214
+ min-width: 0;
215
+ }
216
+
217
+ .ds-site .ds-site-tabs-header {
218
+ margin-left: 18px;
219
+ align-self: stretch;
220
+ }
221
+
222
+ .ds-site .ds-site-tab {
223
+ position: relative;
224
+ display: inline-flex;
225
+ align-items: center;
226
+ padding: 6px 10px;
227
+ border-radius: 8px;
228
+ color: var(--ds-site-muted);
229
+ font-size: 13.5px;
230
+ font-weight: 600;
231
+ text-decoration: none;
232
+ white-space: nowrap;
233
+ }
234
+
235
+ .ds-site .ds-site-tab:hover {
236
+ background: var(--ds-site-hover);
237
+ color: var(--ds-site-fg);
238
+ }
239
+
240
+ .ds-site .ds-site-tab[aria-current="page"] {
241
+ color: var(--ds-site-fg);
242
+ }
243
+
244
+ .ds-site .ds-site-tabs-header .ds-site-tab[aria-current="page"]::after {
245
+ content: "";
246
+ position: absolute;
247
+ right: 10px;
248
+ bottom: calc((var(--ds-site-header-h) - 31px) / -2);
249
+ left: 10px;
250
+ height: 2px;
251
+ border-radius: 2px;
252
+ background: var(--primary);
253
+ }
254
+
255
+ .ds-site .ds-site-tabs-drawer {
256
+ padding: 4px 12px 8px;
257
+ border-bottom: 1px solid var(--ds-site-border);
258
+ }
259
+
260
+ .ds-site .ds-site-tabs-drawer .ds-site-tab[aria-current="page"] {
261
+ background: var(--ds-site-hover);
262
+ }
263
+
209
264
  /* Body: sidebar | scroller (article + outline) */
210
265
  .ds-site .ds-site-body {
211
266
  display: flex;
@@ -381,6 +436,14 @@
381
436
  max-width: var(--ds-site-content-w);
382
437
  }
383
438
 
439
+ .ds-site .ds-site-columns[data-layout="wide"] .ds-site-main {
440
+ max-width: var(--ds-site-wide-w, 1240px);
441
+ }
442
+
443
+ .ds-site .ds-site-columns[data-layout="wide"] .ds-site-toc {
444
+ display: none;
445
+ }
446
+
384
447
  .ds-site .ds-site-article {
385
448
  padding: 36px 0 72px;
386
449
  }
@@ -773,6 +836,10 @@
773
836
  display: inline-grid;
774
837
  }
775
838
 
839
+ .ds-site .ds-site-tabs-header {
840
+ display: none;
841
+ }
842
+
776
843
  .ds-site .ds-site-header {
777
844
  padding: 0 12px;
778
845
  }
package/src/site/tree.ts CHANGED
@@ -10,7 +10,8 @@
10
10
  and titles. Frontmatter `order` overrides the prefix.
11
11
  - A page's title is, in order: its SUMMARY.md label, frontmatter `title`, its first
12
12
  `# heading`, then the file name (`quick-start.md` → "Quick start"). Frontmatter
13
- `description` is its lede and `hidden: true` keeps it out of the sidebar.
13
+ `description` is its lede, `hidden: true` keeps it out of the sidebar and `layout: wide`
14
+ gives it the full width.
14
15
  - Every other file in the docs root is an asset (`tree.assets`): DocsSite serves it to the
15
16
  pages, so `![logo](images/logo.svg)` or an OpenAPI block's `[api](specs/api.json)` loads
16
17
  the file from the VFS.
@@ -72,7 +73,7 @@ export interface BuildDocsTreeOptions {
72
73
  }
73
74
 
74
75
  /** Frontmatter keys that describe the page to the site rather than the reader. */
75
- export const SITE_FRONTMATTER_KEYS = ["title", "description", "order", "hidden", "icon"] as const
76
+ export const SITE_FRONTMATTER_KEYS = ["title", "description", "order", "hidden", "icon", "env", "layout"] as const
76
77
 
77
78
  const MARKDOWN = /\.(md|markdown|mdx)$/i
78
79
  const INDEX = /^(readme|index)$/i
@@ -37,6 +37,8 @@ export function GitbookStreamdown({
37
37
  pageActions,
38
38
  documentProperties,
39
39
  onTaskStateChange,
40
+ envHost,
41
+ env,
40
42
  }: GitbookStreamdownProps) {
41
43
  const content = markdown ?? children ?? ""
42
44
  const gitbookDoc = useMemo(
@@ -63,6 +65,8 @@ export function GitbookStreamdown({
63
65
  pageActions={isStreaming ? false : pageActions}
64
66
  documentProperties={documentProperties}
65
67
  onTaskStateChange={onTaskStateChange}
68
+ envHost={envHost}
69
+ env={env}
66
70
  />
67
71
  </div>
68
72
  )
package/src/styles.css CHANGED
@@ -3214,6 +3214,95 @@
3214
3214
  line-height: 1.5;
3215
3215
  }
3216
3216
 
3217
+ /* An example waiting for the page's env variables (frontmatter `env`). */
3218
+ .docs-demo-canvas-env {
3219
+ display: grid;
3220
+ place-items: center;
3221
+ padding: 20px 14px;
3222
+ }
3223
+ .docs-env-gate {
3224
+ display: grid;
3225
+ gap: 10px;
3226
+ width: min(100%, 420px);
3227
+ padding: 14px 16px;
3228
+ border: 1px solid var(--gb-border);
3229
+ border-radius: 10px;
3230
+ background: var(--gb-background);
3231
+ color: var(--gb-foreground);
3232
+ font-size: 13.5px;
3233
+ line-height: 1.45;
3234
+ }
3235
+ .docs-env-gate-head {
3236
+ display: flex;
3237
+ align-items: center;
3238
+ gap: 8px;
3239
+ font-weight: 600;
3240
+ }
3241
+ .docs-env-gate-icon {
3242
+ display: inline-grid;
3243
+ place-items: center;
3244
+ width: 26px;
3245
+ height: 26px;
3246
+ border-radius: 50%;
3247
+ background: color-mix(in oklab, var(--gb-primary) 14%, transparent);
3248
+ color: var(--gb-primary);
3249
+ flex: none;
3250
+ }
3251
+ .docs-env-gate-list {
3252
+ display: grid;
3253
+ gap: 4px;
3254
+ margin: 0;
3255
+ padding: 0;
3256
+ list-style: none;
3257
+ }
3258
+ .docs-env-gate-list li {
3259
+ display: flex;
3260
+ align-items: baseline;
3261
+ justify-content: space-between;
3262
+ gap: 12px;
3263
+ }
3264
+ .docs-env-gate-list span,
3265
+ .docs-env-gate-note {
3266
+ color: var(--gb-muted-foreground);
3267
+ font-size: 12.5px;
3268
+ }
3269
+ .docs-env-gate-form {
3270
+ display: grid;
3271
+ gap: 8px;
3272
+ }
3273
+ .docs-env-gate-form label {
3274
+ display: grid;
3275
+ gap: 4px;
3276
+ }
3277
+ .docs-env-gate-form input {
3278
+ font: inherit;
3279
+ padding: 6px 8px;
3280
+ border: 1px solid var(--gb-border);
3281
+ border-radius: 6px;
3282
+ background: transparent;
3283
+ color: inherit;
3284
+ }
3285
+ .docs-env-gate-button {
3286
+ justify-self: start;
3287
+ font: inherit;
3288
+ font-weight: 600;
3289
+ padding: 6px 14px;
3290
+ border: 0;
3291
+ border-radius: 999px;
3292
+ background: var(--gb-primary);
3293
+ color: var(--gb-primary-foreground, #fff);
3294
+ cursor: pointer;
3295
+ }
3296
+ .docs-env-gate-button:disabled {
3297
+ opacity: 0.6;
3298
+ cursor: default;
3299
+ }
3300
+ .docs-env-gate-error {
3301
+ margin: 0;
3302
+ color: var(--gb-destructive, #d93025);
3303
+ font-size: 12.5px;
3304
+ }
3305
+
3217
3306
  .docs-demo-loading {
3218
3307
  display: grid;
3219
3308
  place-items: center;