From 72f9986220ce874bf91d82e92d557db741db6bd8 Mon Sep 17 00:00:00 2001 From: Lawrence Lane Date: Wed, 24 Jun 2026 10:27:09 -0400 Subject: [PATCH 1/2] Migrate Sphinx docs to Fern MDX for docs.nvidia.com/nemo/run. Adds docs/fern with converted guides, accordion FAQs, NVIDIA theme config, and GitHub Actions for check, preview, and publish workflows. Co-authored-by: Cursor --- .github/workflows/fern-docs-ci.yml | 47 +++ .github/workflows/fern-docs-preview-build.yml | 64 +++ .../workflows/fern-docs-preview-comment.yml | 142 +++++++ .github/workflows/publish-fern-docs.yml | 91 ++++ docs/fern/components/Authors.tsx | 56 +++ docs/fern/components/BadgeLinks.tsx | 37 ++ docs/fern/components/CustomCard.tsx | 34 ++ docs/fern/components/MetricsTable.tsx | 106 +++++ docs/fern/components/NotebookViewer.tsx | 399 ++++++++++++++++++ docs/fern/components/Tag.tsx | 63 +++ docs/fern/components/TrajectoryViewer.tsx | 144 +++++++ docs/fern/docs.yml | 40 ++ docs/fern/fern.config.json | 4 + docs/fern/versions/nightly.yml | 20 + docs/fern/versions/nightly/pages/faqs.mdx | 136 ++++++ .../versions/nightly/pages/guides/cli.mdx | 398 +++++++++++++++++ .../nightly/pages/guides/configuration.mdx | 197 +++++++++ .../nightly/pages/guides/execution.mdx | 308 ++++++++++++++ .../versions/nightly/pages/guides/index.mdx | 9 + .../nightly/pages/guides/management.mdx | 141 +++++++ .../versions/nightly/pages/guides/ray.mdx | 262 ++++++++++++ .../nightly/pages/guides/why-use-nemo-run.mdx | 72 ++++ docs/fern/versions/nightly/pages/index.mdx | 58 +++ 23 files changed, 2828 insertions(+) create mode 100644 .github/workflows/fern-docs-ci.yml create mode 100644 .github/workflows/fern-docs-preview-build.yml create mode 100644 .github/workflows/fern-docs-preview-comment.yml create mode 100644 .github/workflows/publish-fern-docs.yml create mode 100644 docs/fern/components/Authors.tsx create mode 100644 docs/fern/components/BadgeLinks.tsx create mode 100644 docs/fern/components/CustomCard.tsx create mode 100644 docs/fern/components/MetricsTable.tsx create mode 100644 docs/fern/components/NotebookViewer.tsx create mode 100644 docs/fern/components/Tag.tsx create mode 100644 docs/fern/components/TrajectoryViewer.tsx create mode 100644 docs/fern/docs.yml create mode 100644 docs/fern/fern.config.json create mode 100644 docs/fern/versions/nightly.yml create mode 100644 docs/fern/versions/nightly/pages/faqs.mdx create mode 100644 docs/fern/versions/nightly/pages/guides/cli.mdx create mode 100644 docs/fern/versions/nightly/pages/guides/configuration.mdx create mode 100644 docs/fern/versions/nightly/pages/guides/execution.mdx create mode 100644 docs/fern/versions/nightly/pages/guides/index.mdx create mode 100644 docs/fern/versions/nightly/pages/guides/management.mdx create mode 100644 docs/fern/versions/nightly/pages/guides/ray.mdx create mode 100644 docs/fern/versions/nightly/pages/guides/why-use-nemo-run.mdx create mode 100644 docs/fern/versions/nightly/pages/index.mdx diff --git a/.github/workflows/fern-docs-ci.yml b/.github/workflows/fern-docs-ci.yml new file mode 100644 index 00000000..9fe4c13e --- /dev/null +++ b/.github/workflows/fern-docs-ci.yml @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Validates Fern docs configuration on pull requests. + +name: Fern docs (check) + +on: + pull_request: + paths: + - 'docs/fern/**' + - 'docs/**' + - '.github/workflows/fern-docs-ci.yml' + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '20' + + - name: Install Fern CLI + run: npm install -g fern-api@$(jq -r .version docs/fern/fern.config.json) + + - name: Validate Fern configuration + working-directory: ./docs/fern + run: fern check diff --git a/.github/workflows/fern-docs-preview-build.yml b/.github/workflows/fern-docs-preview-build.yml new file mode 100644 index 00000000..415ee3aa --- /dev/null +++ b/.github/workflows/fern-docs-preview-build.yml @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Workflow 1 of 2 for Fern doc previews. +# +# Collects the fern/ sources and PR metadata from the (possibly untrusted) PR +# branch and uploads them as an artifact. No secrets are used here, so this is +# safe to run on fork PRs via the regular pull_request trigger. +# +# The companion workflow (fern-docs-preview-comment.yml) picks up the artifact, +# builds the preview with DOCS_FERN_TOKEN, and posts the PR comment. + +name: "Preview Fern Docs: Build" + +on: + pull_request: + paths: + - 'docs/fern/**' + - 'docs/**' + - '.github/workflows/fern-docs-preview-build.yml' + +permissions: + contents: read + +jobs: + collect: + runs-on: ubuntu-latest + steps: + - name: Checkout PR + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + + - name: Save PR metadata + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + HEAD_REF: ${{ github.head_ref }} + BASE_REF: ${{ github.base_ref }} + run: | + mkdir -p preview-metadata + echo "$PR_NUMBER" > preview-metadata/pr_number + echo "$HEAD_REF" > preview-metadata/head_ref + git diff --name-only "origin/${BASE_REF}...HEAD" -- '*.mdx' > preview-metadata/changed_mdx_files 2>/dev/null || true + + - name: Upload fern sources and metadata + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: fern-preview + path: | + docs/fern/ + preview-metadata/ + retention-days: 1 diff --git a/.github/workflows/fern-docs-preview-comment.yml b/.github/workflows/fern-docs-preview-comment.yml new file mode 100644 index 00000000..e57155db --- /dev/null +++ b/.github/workflows/fern-docs-preview-comment.yml @@ -0,0 +1,142 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Workflow 2 of 2 for Fern doc previews. +# +# Triggered by workflow_run after "Preview Fern Docs: Build" completes. +# Downloads the fern/ artifact, builds a preview with DOCS_FERN_TOKEN, and +# posts a stable :herb: comment on the PR. This workflow never checks out the +# PR branch directly, keeping secrets isolated from untrusted code. +# +# Required configuration: +# - Organization secret: DOCS_FERN_TOKEN (from `fern token` for the nvidia Fern org) + +name: "Preview Fern Docs: Comment" + +on: + workflow_run: + workflows: ["Preview Fern Docs: Build"] + types: [completed] + +permissions: + pull-requests: write + actions: read + +jobs: + preview: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + steps: + - name: Download fern sources and metadata + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: fern-preview + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Read PR metadata + id: metadata + run: | + echo "pr_number=$(cat preview-metadata/pr_number)" >> "$GITHUB_OUTPUT" + echo "head_ref=$(cat preview-metadata/head_ref)" >> "$GITHUB_OUTPUT" + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '20' + + - name: Install Fern CLI + run: npm install -g fern-api@$(jq -r .version docs/fern/fern.config.json) + + - name: Generate preview URL + id: generate-docs + env: + FERN_TOKEN: ${{ secrets.DOCS_FERN_TOKEN }} + HEAD_REF: ${{ steps.metadata.outputs.head_ref }} + working-directory: ./docs/fern + run: | + OUTPUT=$(fern generate --docs --preview --id "$HEAD_REF" 2>&1) + echo "$OUTPUT" + URL=$(echo "$OUTPUT" | grep -oP 'Published docs to \K.*(?= \()') + if [ -z "$URL" ]; then + echo "::error::Failed to generate preview URL. See fern output above." + exit 1 + fi + echo "preview_url=$URL" >> "$GITHUB_OUTPUT" + + - name: Build page links for changed MDX files + id: page-links + env: + FERN_TOKEN: ${{ secrets.DOCS_FERN_TOKEN }} + PREVIEW_URL: ${{ steps.generate-docs.outputs.preview_url }} + run: | + CHANGED_FILES="" + if [ -f preview-metadata/changed_mdx_files ]; then + CHANGED_FILES=$(cat preview-metadata/changed_mdx_files) + fi + + if [ -z "$CHANGED_FILES" ] || [ -z "$PREVIEW_URL" ]; then + echo "page_links=" >> "$GITHUB_OUTPUT"; exit 0 + fi + + BASE_URL=$(echo "$PREVIEW_URL" | grep -oP 'https?://[^/]+') + FILES_PARAM=$(echo "$CHANGED_FILES" | tr '\n' ',' | sed 's/,$//' \ + | python3 -c "import sys, urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip(), safe=',/'))") + RESPONSE=$(curl -sf -H "FERN_TOKEN: $FERN_TOKEN" "${PREVIEW_URL}/api/fern-docs/get-slug-for-file?files=${FILES_PARAM}" 2>/dev/null) || { + echo "page_links=" >> "$GITHUB_OUTPUT"; exit 0 + } + + PAGE_LINKS=$(echo "$RESPONSE" | jq -r --arg url "$BASE_URL" \ + '.mappings[] | select(.slug != null) | "- [\(.slug)](\($url)/\(.slug))"') + + if [ -n "$PAGE_LINKS" ]; then + { echo "page_links<> "$GITHUB_OUTPUT" + else + echo "page_links=" >> "$GITHUB_OUTPUT" + fi + + - name: Post or update PR comment + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ steps.metadata.outputs.pr_number }} + PREVIEW_URL: ${{ steps.generate-docs.outputs.preview_url }} + PAGE_LINKS: ${{ steps.page-links.outputs.page_links }} + run: | + # Build comment body + BODY=":herb: **Preview your docs:** <${PREVIEW_URL}>" + if [ -n "${PAGE_LINKS}" ]; then + BODY="${BODY} + + Here are the markdown pages you've updated: + ${PAGE_LINKS}" + fi + + # Hidden marker for upsert + MARKER="" + BODY="${BODY} + + ${MARKER}" + + # Find existing comment with marker + COMMENT_ID=$(gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \ + --jq ".[] | select(.body | contains(\"${MARKER}\")) | .id" | tr -d '\r' | head -1) + + if [ -n "$COMMENT_ID" ]; then + gh api "repos/${{ github.repository }}/issues/comments/${COMMENT_ID}" \ + -X PATCH -f body="$BODY" + else + gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \ + -f body="$BODY" + fi diff --git a/.github/workflows/publish-fern-docs.yml b/.github/workflows/publish-fern-docs.yml new file mode 100644 index 00000000..7bc6a561 --- /dev/null +++ b/.github/workflows/publish-fern-docs.yml @@ -0,0 +1,91 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Publishes the Fern documentation site when a docs tag is pushed or manually triggered. +# +# To publish: git tag docs/v1.2.0 && git push origin docs/v1.2.0 +# Or use the "Run workflow" button in the Actions tab. +# +# Required configuration: +# - Organization secret: DOCS_FERN_TOKEN (from `fern token` for the nvidia Fern org) + +name: Publish Fern Docs + +on: + push: + tags: + - 'docs/v*' + workflow_dispatch: {} + +permissions: + contents: read + +concurrency: + group: fern-publish + cancel-in-progress: true + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-tags: true + + - name: Checkout frozen version content + run: | + set -eo pipefail + for version_file in docs/fern/versions/v*.yml; do + [ -e "$version_file" ] || continue + version=$(basename "$version_file" .yml) + if git rev-parse "$version" >/dev/null 2>&1; then + mkdir -p "docs/fern/versions/${version}-content" + git archive "$version" -- docs/ | tar -x --strip-components=1 -C "docs/fern/versions/${version}-content" + find "docs/fern/versions/${version}-content" -name '*.md' -exec sed -i 's/]*[^/]\)>//g' {} + + echo "Extracted docs from $version" + else + echo "::warning::Tag $version not found — skipping content checkout" + fi + done + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '20' + + - name: Install Fern CLI + run: npm install -g fern-api@$(jq -r .version docs/fern/fern.config.json) + + - name: Stamp version in docs.yml + run: | + VERSION=$(curl -sf https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r .tag_name) + if [ -n "$VERSION" ] && [ "$VERSION" != "null" ]; then + sed -i 's/display-name: "Latest"/display-name: "Latest · '"${VERSION}"'"/' docs/fern/docs.yml + fi + echo "--- docs.yml versions after stamp ---" + grep "display-name:" docs/fern/docs.yml + + - name: Publish Docs + env: + FERN_TOKEN: ${{ secrets.DOCS_FERN_TOKEN }} + working-directory: ./docs/fern + run: | + set -o pipefail + fern generate --docs 2>&1 | tee /tmp/fern-output.log + URL=$(grep -oP 'Published docs to \K.*(?= \()' /tmp/fern-output.log || true) + if [ -n "$URL" ]; then + echo "### Published: [$URL]($URL)" >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/docs/fern/components/Authors.tsx b/docs/fern/components/Authors.tsx new file mode 100644 index 00000000..0c711979 --- /dev/null +++ b/docs/fern/components/Authors.tsx @@ -0,0 +1,56 @@ +/** + * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Authors - Renders author byline with avatars for dev notes / blog posts. + * + * Uses authors data from components/devnotes/authors-data.ts (synced with .authors.yml). + * NOTE: Fern's custom component pipeline uses the automatic JSX runtime. + * + * Usage in MDX (authors from frontmatter): + * --- + * authors: + * - jdoe + * - asmith + * --- + * + * import { Authors } from "@/components/Authors"; + * + */ + +import { authors } from "./devnotes/authors-data"; + +export interface AuthorsProps { + /** Author IDs from .authors.yml. From frontmatter: ids={authors} */ + ids?: string[]; +} + +export const Authors = ({ ids }: AuthorsProps) => { + const validAuthors = (ids ?? []) + .map((id) => authors[id]) + .filter(Boolean); + + if (validAuthors.length === 0) return null; + + return ( +
+ {validAuthors.map((author, i) => ( +
+ +
+ {author.name} + {author.description} +
+
+ ))} +
+ ); +}; diff --git a/docs/fern/components/BadgeLinks.tsx b/docs/fern/components/BadgeLinks.tsx new file mode 100644 index 00000000..c4a5949c --- /dev/null +++ b/docs/fern/components/BadgeLinks.tsx @@ -0,0 +1,37 @@ +/** + * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Badge links for GitHub, License, PyPI, etc. + * Uses a custom wrapper to avoid Fern's external-link icon stacking under badges. + * + * `badges` is required — there is intentionally no default. A previous + * version shipped placeholder URLs that could land in production for + * sites that rendered the component without props. See README-BadgeLinks.md. + */ +export type BadgeItem = { + href: string; + src: string; + alt: string; +}; + +export interface BadgeLinksProps { + badges: BadgeItem[]; +} + +export function BadgeLinks({ badges }: BadgeLinksProps) { + return ( +
+ {badges.map((b) => ( + + {b.alt} + + ))} +
+ ); +} diff --git a/docs/fern/components/CustomCard.tsx b/docs/fern/components/CustomCard.tsx new file mode 100644 index 00000000..f120898f --- /dev/null +++ b/docs/fern/components/CustomCard.tsx @@ -0,0 +1,34 @@ +/** + * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * CustomCard - Simple card with title, text, link, and optional sparkle. + * + * Alternative to Fern's built-in when you need custom styling + * (e.g. devnotes/blog landing pages). + * NOTE: Fern's custom component pipeline uses the automatic JSX runtime. + * + * Usage in MDX: + * import { CustomCard } from "@/components/CustomCard"; + * + */ + +export interface CustomCardProps { + title: string; + text: string; + link: string; + sparkle?: boolean; +} + +export const CustomCard = ({ title, text, link, sparkle = false }: CustomCardProps) => { + return ( + +

+ {title} {sparkle && "✨"} +

+

{text}

+
+ ); +}; diff --git a/docs/fern/components/MetricsTable.tsx b/docs/fern/components/MetricsTable.tsx new file mode 100644 index 00000000..f95dc186 --- /dev/null +++ b/docs/fern/components/MetricsTable.tsx @@ -0,0 +1,106 @@ +/** + * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * MetricsTable - Styled comparison table for benchmark results. + * + * Optional: highlights best values per column (bold). + * NOTE: Fern's custom component pipeline uses the automatic JSX runtime. + * Do NOT import React -- the `react` module is not resolvable in Fern's build. + * + * Usage in MDX: + * import { MetricsTable } from "@/components/MetricsTable"; + * + * + */ + +export interface MetricsTableProps { + headers: string[]; + rows: (string | number)[][]; + /** Column indices where lower is better (for highlighting) */ + lowerIsBetter?: number[]; + /** Column indices where higher is better (default for non-lowerIsBetter) */ + higherIsBetter?: number[]; +} + +function findBestIndices( + rows: (string | number)[][], + colIndex: number, + lowerIsBetter: boolean +): Set { + const values = rows.map((r) => { + const v = r[colIndex]; + if (typeof v === "number") return v; + const parsed = parseFloat(String(v)); + return isNaN(parsed) ? (lowerIsBetter ? Infinity : -Infinity) : parsed; + }); + const best = lowerIsBetter ? Math.min(...values) : Math.max(...values); + const bestIndices = new Set(); + values.forEach((v, i) => { + if (v === best) bestIndices.add(i); + }); + return bestIndices; +} + +export const MetricsTable = ({ + headers, + rows, + lowerIsBetter = [], + higherIsBetter = [], +}: MetricsTableProps) => { + const lowerSet = new Set(lowerIsBetter); + const bestByCol: Record> = {}; + + for (let c = 0; c < headers.length; c++) { + if (lowerSet.has(c)) { + bestByCol[c] = findBestIndices(rows, c, true); + } else if (higherIsBetter.includes(c)) { + bestByCol[c] = findBestIndices(rows, c, false); + } else { + const numLike = rows.every((r) => { + const v = r[c]; + return typeof v === "number" || !isNaN(parseFloat(String(v))); + }); + if (numLike) { + bestByCol[c] = findBestIndices(rows, c, false); + } + } + } + + return ( +
+ + + + {headers.map((h, i) => ( + + ))} + + + + {rows.map((row, rowIdx) => ( + + {row.map((cell, colIdx) => { + const isBest = bestByCol[colIdx]?.has(rowIdx); + return ( + + ); + })} + + ))} + +
{h}
+ {cell} +
+
+ ); +}; diff --git a/docs/fern/components/NotebookViewer.tsx b/docs/fern/components/NotebookViewer.tsx new file mode 100644 index 00000000..e3842891 --- /dev/null +++ b/docs/fern/components/NotebookViewer.tsx @@ -0,0 +1,399 @@ +/** + * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { ReactNode } from "react"; + +/** + * NotebookViewer - Renders Jupyter notebook content in Fern docs. + * + * Uses Fern's code block structure (fern-code, fern-code-block, etc.) so input + * and output cells match the default Fern code block styling. + * + * Accepts notebook cells (markdown + code) and optionally a Colab URL. + * Designed to work with notebooks converted via `scripts/converters/ipynb_to_fern_json.py` + * (NeMo Data Designer–compatible pipeline; sources may be plain `.ipynb` or Jupytext). + * + * NOTE: Fern's custom component pipeline uses the automatic JSX runtime. + * Only type-only imports from "react" are used (erased at compile time). + * + * Usage in MDX: + * import { NotebookViewer } from "@/components/NotebookViewer"; + * import notebook from "@/components/notebooks/1-the-basics"; + * + * + */ + +export interface CellOutput { + type: "text" | "image"; + data: string; + format?: "plain" | "html"; +} + +export interface NotebookCell { + type: "markdown" | "code"; + source: string; + /** Pre-rendered syntax-highlighted HTML (from Pygments). When present, used instead of escaped source. */ + source_html?: string; + language?: string; + outputs?: CellOutput[]; +} + +export interface NotebookData { + cells: NotebookCell[]; +} + +export interface NotebookViewerProps { + /** Notebook data with cells array. If import fails, this may be undefined. */ + notebook?: NotebookData | null; + /** Optional Colab URL for "Run in Colab" badge */ + colabUrl?: string; + /** Show code cell outputs (default: true) */ + showOutputs?: boolean; +} + +function NotebookViewerError({ message, detail }: { message: string; detail?: string }) { + return ( +
+ NotebookViewer error: {message} + {detail && ( +
+          {detail}
+        
+ )} +
+ ); +} + +function escapeHtml(text: string): string { + if (typeof text !== "string") return ""; + return text + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +// Sprint 4.2: markdown is rendered server-side in ipynb_to_fern_json.py +// (markdown-it-py) and emitted as cell.source_html. The component renders +// that HTML directly. The hand-rolled JS parser previously here mishandled +// blockquotes, fenced code, tables, and nested lists; it has been removed. + +function handleCopy(content: string, button: HTMLButtonElement) { + navigator.clipboard.writeText(content).catch(() => {}); + const originalHtml = button.innerHTML; + const originalLabel = button.getAttribute("aria-label") ?? "Copy code"; + button.innerHTML = "Copied!"; + button.setAttribute("aria-label", "Copied to clipboard"); + setTimeout(() => { + button.innerHTML = originalHtml; + button.setAttribute("aria-label", originalLabel); + }, 1500); +} + +const FLAG_ICON = ( + + + + +); + +const SCROLL_AREA_STYLE = `[data-radix-scroll-area-viewport]{scrollbar-width:none;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;}[data-radix-scroll-area-viewport]::-webkit-scrollbar{display:none}`; + +const BUTTON_BASE_CLASS = + "focus-visible:ring-(color:--accent) rounded-2 inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-colors hover:transition-none focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-(color:--grayscale-a11) hover:bg-(color:--accent-a3) hover:text-(color:--accent-11) pointer-coarse:size-9 size-7"; + +/** Fern code block structure – matches Fern docs (header with language + buttons, pre with scroll area). */ +function FernCodeBlock({ + title, + children, + className = "", + asPre = true, + copyContent, + showLineNumbers = false, + codeHtml, +}: { + title: string; + children: ReactNode; + className?: string; + /** Use div instead of pre for content (needed when children include block elements like img/div). */ + asPre?: boolean; + /** Raw text to copy when copy button is clicked. When provided, shows a copy button. */ + copyContent?: string; + /** Show line numbers in a table layout (matches Fern's code block structure). */ + showLineNumbers?: boolean; + /** Pre-rendered HTML for each line when showLineNumbers is true. Lines are split by newline. */ + codeHtml?: string; +}) { + const headerLabel = title === "Output" ? "Output" : title.charAt(0).toUpperCase() + title.slice(1); + const wrapperClasses = + "fern-code fern-code-block bg-card-background border-card-border rounded-3 shadow-card-grayscale relative mb-6 mt-4 flex w-full min-w-0 max-w-full flex-col border first:mt-0"; + const preStyle = { + backgroundColor: "rgb(255, 255, 255)", + ["--shiki-dark-bg" as string]: "#212121", + color: "rgb(36, 41, 46)", + ["--shiki-dark" as string]: "#EEFFFF", + }; + + const scrollAreaContent = () => { + if (codeHtml == null) return null; + const lines = codeHtml.split("\n"); + return ( +
+