Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ remix.config.js
/test-results
/tests/visual/fixture/myst.yml
/tests/visual/fixture/_build
/tests/visual/fixture-no-thebe/myst.yml
/tests/visual/fixture-no-thebe/_build
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- In-page live compute via Thebe (Phase 2 of [`PLAN.md`](./PLAN.md), closes #88):
setting the standard MyST `project.thebe` config surfaces a **Power** toggle in
the header toolbar next to Launch on notebook pages (desktop); clicking it boots
the kernel, after which **Run / Restart / Clear** take its place and cells
execute live. The control is the `@myst-theme/jupyter` notebook toolbar,
portaled into the header (`ComputeToolbarSlot.tsx` → `#qe-compute-slot`) so it
stays inside the Thebe providers while living in the always-visible header —
no provider-tree lift. The QuantEcon default is **JupyterLite**
(`thebe: { lite: true }`): Python runs in the browser via Pyodide, with no
server or Binder to host; `binder:`/`server:` backends remain available through
the same config. Pyodide package caveat (numba/JAX unavailable) documented in
the README. Covered by a presence test scoped to the header slot and a
disabled-path test against a second no-thebe fixture served on its own port,
proving the toggle is gated on the project opting in ([#98](https://github.com/QuantEcon/quantecon-theme.mystmd/pull/98)).

## [2.2.0] - 2026-07-16

> Headline: fancy ordered lists — `(a)` / `(i)` / `B)` markers from the QuantEcon
Expand Down
25 changes: 20 additions & 5 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Derived from `quantecon-book-theme` v0.20.3 (see its `README.md`, `docs/user/*`,
| # | Feature | Book-theme | MyST theme | Phase |
| --- | --- | :---: | :---: | :---: |
| Git history in lecture headers (last-modified + changelog dropdown) | ✅ | ❌ | **1** |
| Launch parity — BinderHub + Thebe (live compute) in addition to Colab / private hub | ✅ | ⚠️ partial | **2** |
| Launch parity — Thebe (live compute) in addition to Colab / private hub (BinderHub dropped, #26) | ✅ | | **2** |
| Configurable code highlighting (custom QE tokens vs Pygments styles) | ✅ | ❌ | **3** |
| Text colour schemes (`seoul256` / `gruvbox` / `none` + custom) | ✅ | ❌ | **3** |
| Language switcher (multilingual) + `hreflang` SEO tags | ✅ | ❌ | **4** |
Expand Down Expand Up @@ -268,10 +268,25 @@ in-page **Thebe** live compute.
Colab URL construction, `notebook_interface`, `nb_path_to_notebooks`, `path_to_docs`
stripping) + `docs/user/launch.md`.

- [ ] **Thebe:** the bundle already ships Thebe assets and `@myst-theme/jupyter` is wired
in (`PageContent.tsx` uses `ExecuteScopeProvider`, `NotebookToolbar`). Confirm/enable
`myst.yml` `project.jupyter`/`thebe` config path and surface a "live compute" toggle
consistent with the toolbar.
- [x] **Thebe:** in-page live compute enabled via `project.thebe` config in `myst.yml`.
The infra was already wired (`Page.tsx` `ComputeOptionsProvider` + `ThebeLoaderAndServer`;
`PageContent.tsx` `ExecuteScopeProvider` + `NotebookToolbar`); enablement is config-derived
(`compute.enabled = !!thebeFrontmatterToOptions(project.thebe)`, no runtime setter), so
setting `project.thebe` surfaces the `@myst-theme/jupyter` NotebookToolbar (the **Power**
toggle, then Run/Restart/Clear once a kernel connects) on notebook pages — **portaled into
the QuantEcon header toolbar** next to Launch (`ComputeToolbarSlot.tsx` renders it via
`createPortal` into `#qe-compute-slot` in `Toolbar.tsx`; `app.css` neutralises the default
floating pill and matches the 20px header icons), so it sits in the fixed header always
visible while scrolling. The portal keeps the component inside the Thebe providers (React
context flows through the React tree, not the DOM) even though the header is mounted outside
them — avoiding a provider-tree lift. That header toggle **is** the "live compute"
control. The QuantEcon default is **JupyterLite** (`thebe: { lite: true }`): Python
in the browser via Pyodide, no server/Binder (avoids the flaky Binder, see #26). Verified
end-to-end (Power → Pyodide kernel boots in-browser). Pyodide caveat documented in the README
(numba/JAX unavailable). Fixture sets `thebe.lite`; `tests/visual/theme.spec.ts` asserts the
toggle renders; `notebook.png`/`launch-open.png` snapshots refreshed. A second fixture
(`tests/visual/fixture-no-thebe/`, served on a second port) asserts the toggle is **absent**
on a notebook page when a project doesn't set `project.thebe` (the disabled path).
- [x] ~~**BinderHub:** add a Binder option to the `LaunchButton` radio group~~
**Decided against (2026-06-12):** BinderHub proved flaky in practice, and Colab
is the launch target QuantEcon standardises on — primarily because it provides
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,37 @@ defaults reproduce the behaviour above, so existing projects need no changes:
| `launch_notebooks_path` | _(none)_ | Sub-directory within the notebook repo where the notebooks live |
| `launch_source_path` | _(none)_ | Path prefix stripped from the page location (e.g. a `lectures/` source dir) |

### Live compute (Thebe / JupyterLite)

In addition to launching a notebook elsewhere (Colab/Hub), the theme can run
notebook cells **in place** via [Thebe](https://thebe.readthedocs.io). This is
opt-in per project through the standard MyST `thebe` config, set under
`project.thebe` in `myst.yml` (the theme reads it from the project manifest, so
it is project-level — not per-page frontmatter). The QuantEcon default is
**JupyterLite** — Python runs entirely in the browser via Pyodide, with no
server or Binder to host:

```yaml
# myst.yml
project:
thebe:
lite: true
```

With this set, a **Power** toggle appears in the header toolbar (next to the
Launch button) on notebook pages. Clicking it boots the in-browser kernel, after
which **Run / Restart / Clear** take its place; cells then execute live. The
toggle lives in the fixed header so it stays visible while scrolling, and only
appears on notebook pages (desktop). It is the `@myst-theme/jupyter` notebook
toolbar, relocated into the header via a portal.

**Caveat:** Pyodide runs pure-Python plus packages compiled for it (numpy,
scipy, pandas, matplotlib, sympy). Packages that aren't available for Pyodide
(e.g. numba, JAX) won't import, so live compute suits lectures whose runtime
stack is Pyodide-compatible. Other backends are available through the same
`thebe` config (`binder:` for BinderHub, `server:` for a hosted Jupyter
server) if a project needs a full environment.

## Usage with MyST

Point your project's `site.template` at a **pinned release** zip:
Expand Down
25 changes: 25 additions & 0 deletions app/components/ComputeToolbarSlot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { NotebookToolbar } from '@myst-theme/jupyter';

/**
* Renders the live-compute NotebookToolbar (a Power toggle, then
* Run/Restart/Clear once a kernel connects) into the fixed QuantEcon header
* toolbar via a portal, rather than in the article body — so it is always
* visible while scrolling and sits alongside the Launch button.
*
* The portal keeps this component a React child of `PageContent` (inside the
* Thebe `ThebeLoaderAndServer` / `ExecuteScopeProvider`), so its hooks and the
* kernel connection keep working even though its DOM lands in the header, which
* is mounted outside those providers. The target `#qe-compute-slot` lives in
* `Toolbar`. Client-only: portals don't render during SSR, and live compute is
* a client-side feature anyway.
*/
export function ComputeToolbarSlot() {
const [slot, setSlot] = useState<HTMLElement | null>(null);
useEffect(() => {
setSlot(document.getElementById('qe-compute-slot'));
}, []);
if (!slot) return null;
return createPortal(<NotebookToolbar />, slot);
}
6 changes: 4 additions & 2 deletions app/components/PageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ import { SourceFileKind } from 'myst-spec-ext';
import {
ExecuteScopeProvider,
BusyScopeProvider,
NotebookToolbar,
ConnectionStatusTray,
ErrorTray,
useComputeOptions,
} from '@myst-theme/jupyter';
import { ProjectFrontmatter } from './ProjectFrontmatter.js';
import { BackToTop, Outline } from './Outline.js';
import { SiteFooter } from './SiteFooter.js';
import { ComputeToolbarSlot } from './ComputeToolbarSlot.js';

export const PageContent = React.memo(function ({ article }: { article: PageLoader }) {
const config = useSiteManifest();
Expand Down Expand Up @@ -59,9 +59,11 @@ export const PageContent = React.memo(function ({ article }: { article: PageLoad
containerClassName="hidden lg:col-margin"
pageEnumerator={article.frontmatter.enumerator}
/>
{/* Live-compute toggle: portaled into the header toolbar (next to
Launch) rather than rendered here in the article body. */}
{compute?.enabled &&
compute.features.notebookCompute &&
article.kind === SourceFileKind.Notebook && <NotebookToolbar showLaunch />}
article.kind === SourceFileKind.Notebook && <ComputeToolbarSlot />}
{compute?.enabled && article.kind === SourceFileKind.Article && (
<ErrorTray pageSlug={article.slug} />
)}
Expand Down
3 changes: 3 additions & 0 deletions app/components/toolbar/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ export function Toolbar() {
<li className="hidden md:block">
<DownloadsButton size={iconSize} />
</li>
{/* Portal target for the live-compute toggle (see ComputeToolbarSlot).
`empty:hidden` keeps it from adding a gap on non-notebook pages. */}
<li id="qe-compute-slot" className="hidden md:flex items-center empty:hidden" />
<li className="hidden md:block">
<LaunchButton size={iconSize} />
</li>
Expand Down
35 changes: 29 additions & 6 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ import { defineConfig, devices } from "@playwright/test";
*/
const PORT = process.env.PORT || "3111";
const baseURL = `http://localhost:${PORT}`;
// Second fixture project WITHOUT `project.thebe`, used to assert the
// live-compute toggle is absent when a project hasn't opted into Thebe.
const NO_THEBE_PORT = process.env.NO_THEBE_PORT || "3112";
const noThebeURL = `http://localhost:${NO_THEBE_PORT}`;
// The two fixtures must not share a port: with reuseExistingServer, the second
// webServer entry would silently "reuse" the first (thebe-enabled) server and
// the absent-toggle test would assert against the wrong fixture.
if (NO_THEBE_PORT === PORT) {
throw new Error(`NO_THEBE_PORT (${NO_THEBE_PORT}) must differ from PORT (${PORT})`);
}
// Write the resolved value back so test workers read one source of truth
// (tests/visual/theme.spec.ts builds the no-thebe URL from this).
process.env.NO_THEBE_PORT = NO_THEBE_PORT;

export default defineConfig({
testDir: "./tests/visual",
Expand Down Expand Up @@ -54,10 +67,20 @@ export default defineConfig({
use: { ...devices["Desktop Safari"] },
},
],
webServer: {
command: "bash tests/visual/serve.sh",
url: baseURL,
reuseExistingServer: !process.env.CI,
timeout: 300 * 1000,
},
webServer: [
{
command: "bash tests/visual/serve.sh",
url: baseURL,
reuseExistingServer: !process.env.CI,
timeout: 300 * 1000,
},
{
// Thebe-disabled fixture (no `project.thebe`) on a second port, for the
// `live-compute-toggle-absent-without-thebe` test in theme.spec.ts.
command: `FIXTURE_DIR=fixture-no-thebe PORT=${NO_THEBE_PORT} bash tests/visual/serve.sh`,
url: noThebeURL,
reuseExistingServer: !process.env.CI,
timeout: 300 * 1000,
},
],
});
15 changes: 15 additions & 0 deletions styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@
@apply hidden xl:block;
}

/* Live-compute toggle in the header: the @myst-theme/jupyter NotebookToolbar
is portaled into the top toolbar (#qe-compute-slot, see ComputeToolbarSlot).
Neutralise its default right-aligned, sticky, full-width floating pill so it
sits inline among the header icons. */
#qe-compute-slot .myst-jp-nb-toolbar {
@apply static top-auto z-auto w-auto p-0 justify-start pointer-events-auto;
}
#qe-compute-slot .myst-jp-nb-toolbar-inner {
@apply m-0 p-0 border-0 shadow-none bg-transparent backdrop-blur-none;
}
/* Match the surrounding 20px header icons (the toolbar ships 24px icons). */
#qe-compute-slot .myst-jp-nb-toolbar svg {
@apply w-5 h-5;
}

html {
scroll-behavior: smooth;
}
Expand Down
Binary file modified tests/visual/__snapshots__/desktop-chrome-darwin/launch-open.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/desktop-chrome-darwin/notebook.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/desktop-chrome-linux/launch-open.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/desktop-chrome-linux/notebook.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions tests/visual/fixture-no-thebe/intro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# No-Thebe fixture

This project does **not** set `project.thebe`, so live compute is off and the
header must show no live-compute (Power) toggle. Used by the
`live-compute-toggle-absent-without-thebe` visual test.
17 changes: 17 additions & 0 deletions tests/visual/fixture-no-thebe/myst.yml.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Template for the "Thebe disabled" visual-regression fixture.
# Deliberately sets NO `project.thebe`, so the live-compute toggle must not
# appear in the header — see the `live-compute-toggle-absent-without-thebe`
# test in `tests/visual/theme.spec.ts`. `serve.sh` copies this to `myst.yml`,
# substituting __THEME__ with the theme under test (FIXTURE_DIR=fixture-no-thebe).
version: 1
project:
title: QuantEcon Theme — No-Thebe Fixture
github: https://github.com/QuantEcon/quantecon-theme.mystmd
toc:
- file: intro.md
- file: notebook.ipynb
site:
title: QE Theme No-Thebe Fixture
template: __THEME__
options:
logo_text: QE No-Thebe Fixture
86 changes: 86 additions & 0 deletions tests/visual/fixture-no-thebe/notebook.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Notebook outputs\n",
"\n",
"This page exercises **notebook cell output rendering** — the area changed by the\n",
"`@myst-theme` v1.0.0 output-node AST change. Outputs are baked in (no execution)."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"hello from a stream output\n"
]
}
],
"source": [
"print(\"hello from a stream output\")"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
" value\n",
"count 3.0\n",
"mean 2.0\n",
"std 1.0"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"summary # a plain-text execute_result"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"ename": "ValueError",
"evalue": "a deliberate error to render the traceback",
"output_type": "error",
"traceback": [
"Traceback (most recent call last):",
"ValueError: a deliberate error to render the traceback"
]
}
],
"source": [
"raise ValueError(\"a deliberate error to render the traceback\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.11"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
4 changes: 4 additions & 0 deletions tests/visual/fixture/myst.yml.in
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ project:
github: https://github.com/QuantEcon/quantecon-theme-src
plugins:
- fancy-lists.mjs
# In-browser live compute (Pyodide). Surfaces the @myst-theme/jupyter
# NotebookToolbar (Power/Run/Restart/Clear) on notebook pages.
thebe:
lite: true
toc:
- file: intro.md
- file: features.md
Expand Down
5 changes: 4 additions & 1 deletion tests/visual/serve.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
set -euo pipefail

here="$(cd "$(dirname "$0")" && pwd)"
cd "$here/fixture"
# FIXTURE_DIR selects which fixture project to serve (default the main one);
# `fixture-no-thebe` is the Thebe-disabled variant.
cd "$here/${FIXTURE_DIR:-fixture}"

: "${THEME_TEMPLATE:?set THEME_TEMPLATE to a local theme build dir or a zip URL}"

Expand All @@ -21,6 +23,7 @@ cd "$here/fixture"
esc=$(printf '%s' "$THEME_TEMPLATE" | sed 's/[\\&|]/\\&/g')
sed "s|__THEME__|${esc}|" myst.yml.in > myst.yml

echo "[serve] fixture: ${FIXTURE_DIR:-fixture}"
echo "[serve] template: $THEME_TEMPLATE"
echo "[serve] port: ${PORT:-3111}"
exec myst start --port "${PORT:-3111}"
Loading
Loading