Skip to content
Merged
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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
matched here. Site chrome (footer badge, back-to-top) keeps its own rules
([#167](https://github.com/QuantEcon/quantecon-theme.mystmd/pull/167)).

### Removed
- The **Private JupyterHub** launch option. The toolbar's Launch control is now a
direct link to Google Colab instead of a popover with a radio group and a
hand-typed service URL. The demand signal this was gated on came back empty:
the book-theme only renders its hub button when `jupyterhub_url` is set in
`_config.yml`, and an org-wide search finds that key in no lecture repo at all
— every flagship series configures `colab_url` alone — so the button has never
appeared on a live QuantEcon site. The MyST version was also not the same
feature: it had no config key, rendered unconditionally, and asked the reader
to type a hub URL into a box whose value was `React.useState('')` and so was
discarded when the popover closed. Verified against a live build, it also
hardcoded a `/jupyter/hub/` path segment the Sphinx theme leaves to the
configured URL, so it only ever addressed hubs at `<host>/jupyter/hub/` and
could not reach a stock JupyterHub, let alone a local Jupyter server. Removing
it drops `buildJupyterHubUrl`, the Radix popover and radio group, and the
`launch-open.png` visual baseline; `launch-colab` now asserts the anchor's
`href`, which also pins that the control is a link rather than a chooser.
Re-adding hub, Binder and local-server targets is tracked as future work
([#87](https://github.com/QuantEcon/quantecon-theme.mystmd/issues/87)).

### Fixed
- Every toolbar control was nesting a second `<button>` inside its real button
or link, because the Radix tooltip trigger renders its own element by
Expand Down
21 changes: 12 additions & 9 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Derived from `quantecon-book-theme` v0.20.3 (see its `README.md`, `docs/user/*`,
| RTL support (`dir="rtl"`) | ✅ | ❌ | **5** |
| Collapsible stderr warnings in notebook cells | ✅ | ❓ verify | **6** |
| Full OpenGraph / Twitter card meta tags | ✅ | ⚠️ partial | **6** |
| **Already at parity:** dark mode, font scaling, fullscreen, search, "On this page" TOC + back-to-top, contents sidebar, downloads (PDF/notebook), Colab + private JupyterHub launch, edit-on-GitHub, author header, content-driven footer, responsive/mobile | ✅ | ✅ | — |
| **Already at parity:** dark mode, font scaling, fullscreen, search, "On this page" TOC + back-to-top, contents sidebar, downloads (PDF/notebook), Colab launch, edit-on-GitHub, author header, content-driven footer, responsive/mobile | ✅ | ✅ | — |

**Shipped state (2026-08-20).** Phase 0 completed across
[v2.1.0](https://github.com/QuantEcon/quantecon-theme.mystmd/releases/tag/v2.1.0)–[v2.2.0](https://github.com/QuantEcon/quantecon-theme.mystmd/releases/tag/v2.2.0); Phase 1 and the Thebe half of Phase 2
Expand Down Expand Up @@ -339,27 +339,30 @@ stripping) + `docs/user/launch.md`.
**GPU access** for the lectures that need it. #26 stays open as a demand-driven
future request (an implementation existed in PR #86 and was stripped before
merge — recoverable from that history if demand appears). The Private
JupyterHub option remains for now; its possible removal (collapsing the
launcher to a direct Colab button) is tracked in #87.
JupyterHub option was **removed in #87** (2026-08-26), collapsing the
launcher to a direct Colab link — see the note below.
- [x] Generalise the hardcoded `.notebooks` suffix and `main` branch into config under
`site.options` in `myst.yml` (MyST's analog of the book-theme `html_theme_options`),
so non-default branches and naming work. New optional keys (defaults reproduce the
historical behaviour, so existing lectures are unchanged): `launch_repo_suffix`
(default `.notebooks`), `launch_branch` (default `main`), plus `launch_repo_url`
to point at an arbitrary notebook repo (book-theme `nb_repository_url` parity).
URL logic extracted to pure, unit-tested builders in `launchUrls.ts`; applies to
**both** the Colab and the Private-JupyterHub launch URLs. Done in
[#97](https://github.com/QuantEcon/quantecon-theme.mystmd/pull/97).
- [x] Fix Colab/JupyterHub path handling for **nested** lecture dirs: strip the source
the Colab launch URL (and, until #87 removed it, the private-JupyterHub
URL). Done in [#97](https://github.com/QuantEcon/quantecon-theme.mystmd/pull/97).
- [x] Fix Colab path handling for **nested** lecture dirs: strip the source
extension robustly (the old `page.location.split('.')[0]` truncated paths/dirs
containing a dot), strip `launch_source_path` (book-theme `path_to_docs`) and
prepend `launch_notebooks_path` (book-theme `nb_path_to_notebooks`). Covered by
`tests/unit/launch-urls.test.mjs` (same PR,
[#97](https://github.com/QuantEcon/quantecon-theme.mystmd/pull/97)).

**Note:** removing the Private JupyterHub option (collapse to a single Colab button, #87)
is deliberately kept out of this work — it's gated on a maintainer demand signal — so the
generalisation above covers both Colab and the hub. See #87.
**Note:** the Private JupyterHub option was **removed (#87, 2026-08-26)** and the launcher
is now a direct Colab link. The demand signal it was gated on came back empty: no lecture
repo has ever set the book-theme's `jupyterhub_url`, so that button never rendered on a live
site, while the MyST version shipped a hand-typed URL box that hardcoded the `/jupyter/hub/`
path and so only worked for one hub layout. Re-adding hub, Binder and local-server targets
properly is tracked as future work — see the enhancement issues linked from #87.

**Effort:** M. **Risk:** low–medium. **Deps:** Phase 0.

Expand Down
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ QuantEcon lectures and books, distributed as a zip attached to each
[GitHub Release](https://github.com/QuantEcon/quantecon-theme.mystmd/releases).

- Responsive and mobile ready
- Notebook launch (Google Colab / private JupyterHub) with configurable repo, branch and paths
- Notebook launch (Google Colab) with configurable repo, branch and paths
- In-page live compute via Thebe (JupyterLite / Pyodide by default)
- Git history in page headers ("Last changed" plus an inline changelog)
- Page Footer based on MyST Content
Expand Down Expand Up @@ -54,12 +54,17 @@ downloads:

### Launch Notebooks

The launch notebooks capability has been developed to mirror capabilities in the previous QuantEcon theme. By default it assumes the `.notebooks` suffix convention for repository naming when launching both Google Colab and Private Jupyter Hub sessions.

Colab is the primary launch target (it provides GPU access for the lectures that
need it). BinderHub is deliberately not offered — it proved flaky in practice;
see issue [#26](https://github.com/QuantEcon/quantecon-theme.mystmd/issues/26),
kept open as a demand-driven future request.
The launch notebooks capability has been developed to mirror capabilities in the previous QuantEcon theme. By default it assumes the `.notebooks` suffix convention for repository naming when locating the notebook to launch.

The **Launch** control in the toolbar is a direct link to Google Colab, the
single launch target — it provides GPU access for the lectures that need it.
Two other targets are deliberately not offered today, each tracked as a
demand-driven future request: BinderHub, which proved flaky in practice
([#26](https://github.com/QuantEcon/quantecon-theme.mystmd/issues/26)), and a
private JupyterHub, whose launcher entry was removed in
[#87](https://github.com/QuantEcon/quantecon-theme.mystmd/issues/87) because no
lecture repo had ever configured one. For running cells without leaving the
page, see [Live compute](#live-compute-thebe--jupyterlite) below.

The repo/branch/path conventions are configurable under `site.options` in
`myst.yml` (MyST's theme-options section). All keys are optional and the
Expand Down
140 changes: 27 additions & 113 deletions app/components/toolbar/LaunchButton.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
import { useProjectManifest, useSiteManifest } from '@myst-theme/providers';
import * as Popover from '@radix-ui/react-popover';
import * as RadioGroup from '@radix-ui/react-radio-group';
import { CirclePlay } from 'lucide-react';
import type { SiteManifest } from 'myst-config';
import React from 'react';
import { usePage } from '../PageProvider';
import type { TemplateOptions } from '~/types';
import { buildColabUrl, buildJupyterHubUrl, type LaunchConfig } from './launchUrls';
import { buildColabUrl, type LaunchConfig } from './launchUrls';
import { Tooltip } from './Tooltip';

function LaunchPanel() {
export function LaunchButton({ size, showLabel }: { size: number; showLabel?: boolean }) {
const project = useProjectManifest();
const page = usePage();
const launchOptions: TemplateOptions =
(useSiteManifest() as SiteManifest & TemplateOptions)?.options ?? {};
const [service, setService] = React.useState<string | undefined>('colab');
const [privateServiceUrl, setPrivateServiceUrl] = React.useState<string>('');

const hasGitHub = !!project?.github;
// Source org/repo from `project.github`, minus the `.myst` suffix if present.
const orgRepo = project?.github
? new URL(project.github).pathname.slice(1).replace(/\.myst$/, '')
Expand All @@ -32,113 +26,33 @@ function LaunchPanel() {
launch_source_path,
} = launchOptions;

const handleSelect = React.useCallback((value: string) => setService(value), []);
const handleLaunch = React.useCallback(() => {
if (!orgRepo || !location) return;
const config: LaunchConfig = {
repoUrl: launch_repo_url,
repoSuffix: launch_repo_suffix,
branch: launch_branch,
notebooksPath: launch_notebooks_path,
sourcePath: launch_source_path,
};
let url: string | undefined;
if (service === 'colab') {
url = buildColabUrl(orgRepo, location, config);
} else if (privateServiceUrl) {
url = buildJupyterHubUrl(privateServiceUrl, orgRepo, location, config);
}
if (url) window.open(url, '_blank');
}, [
service,
privateServiceUrl,
orgRepo,
location,
launch_repo_url,
launch_repo_suffix,
launch_branch,
launch_notebooks_path,
launch_source_path,
]);
// Without a source repo or a page path there is no notebook to open, so the
// control is not rendered at all rather than shown as an inert affordance.
if (!orgRepo || !location) return null;

return (
<div className="p-3 space-y-3">
<div className="text-lg">Notebook Launcher</div>
<p className="text-sm text-opacity-80">
Choose public or private cloud service for "Launch" button.
</p>
<p className="text-sm border-b-[1px] border-b-qetoolbar-border">Select a server</p>
<RadioGroup.Root
className="flex flex-col space-y-3 text-sm"
defaultValue="colab"
onValueChange={handleSelect}
disabled={!hasGitHub}
>
<RadioGroup.Item
value="colab"
id="launch-colab"
autoFocus
className={`p-2 border-2 gap-2
border-qetoolbar-border data-[state="checked"]:border-qeborder-blue data-[state="checked"]:bg-qeborder-blue/20
flex justify-items-center items-center`}
>
Google Colab
</RadioGroup.Item>
<RadioGroup.Item
value="private"
id="launch-private"
className={`p-2 border-2
border-qetoolbar-border data-[state="checked"]:border-qeborder-blue data-[state="checked"]:bg-qeborder-blue/20
flex justify-items-center items-center gap-2`}
>
<div>Private</div>
<input
type="text"
className="w-full p-1 border dark:bg-qetoolbar-dark rounded-xs border-qetoolbar-border text-qetext-light dark:text-qetext-dark"
placeholder="Service URL/Endpoint"
value={privateServiceUrl}
onChange={(e) => setPrivateServiceUrl(e.target.value)}
/>
</RadioGroup.Item>
</RadioGroup.Root>
<button
disabled={!service || (service === 'private' && !privateServiceUrl)}
className="w-full p-1 font-light text-white bg-qeborder-blue/80 hover:bg-qeborder-blue/90 active:bg-qeborder-blue/100 dark:disabled:bg-qetoolbar-dark/90 disabled:bg-qetoolbar-border"
onClick={handleLaunch}
>
Launch Notebook
</button>
</div>
);
}
const config: LaunchConfig = {
repoUrl: launch_repo_url,
repoSuffix: launch_repo_suffix,
branch: launch_branch,
notebooksPath: launch_notebooks_path,
sourcePath: launch_source_path,
};

export function LaunchButton({ size, showLabel }: { size: number; showLabel?: boolean }) {
// The tooltip merges onto the anchor (`asChild`) rather than rendering its
// own trigger inside it -- see Tooltip.tsx and the GitHubButton for the
// same shape. The anchor is the one interactive element.
return (
<Popover.Root>
<Tooltip label="Launch Notebook" asChild>
<Popover.Trigger aria-label="Launch notebook" className="flex items-center cursor-pointer">
<CirclePlay className="opacity-90 hover:scale-110" width={size} height={size} />
{showLabel && <span className="ml-2">Launch</span>}
</Popover.Trigger>
</Tooltip>
<Popover.Portal>
<Popover.Content
sideOffset={3}
className={`
z-10 w-[360px] rounded bg-white dark:bg-qepage-dark p-5
text-qetext-light dark:text-qetext-dark
will-change-[transform,opacity]
shadow-md
dark:shadow-sm
dark:shadow-white/20
ring-0
has(:focus-visible):ring-1
`}
>
<Popover.Arrow className="shadow-md stroke-2 fill-white dark:fill-qepage-dark" />
<LaunchPanel />
</Popover.Content>
</Popover.Portal>
</Popover.Root>
<Tooltip label="Launch notebook in Google Colab" asChild>
<a
href={buildColabUrl(orgRepo, location, config)}
target="_blank"
rel="noopener noreferrer"
aria-label="Launch notebook"
className="flex items-center cursor-pointer"
>
<CirclePlay className="opacity-90 hover:scale-110" width={size} height={size} />
{showLabel && <span className="ml-2">Launch</span>}
</a>
</Tooltip>
);
}
17 changes: 0 additions & 17 deletions app/components/toolbar/launchUrls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,20 +76,3 @@ export function buildColabUrl(
return `${COLAB_BASE_URL}${orgRepo}/blob/${branch}/${relPath}`;
}

/** Private JupyterHub (nbgitpuller `git-pull`) launch URL for the given page. */
export function buildJupyterHubUrl(
hubBaseUrl: string,
sourceOrgRepo: string,
location: string,
config: LaunchConfig = {},
): string {
const orgRepo = notebookOrgRepo(sourceOrgRepo, config);
const repoName = orgRepo.split('/')[1] ?? '';
const branch = config.branch ?? DEFAULT_BRANCH;
const relPath = notebookRelPath(location, config);
// Drop a trailing slash on the user-provided hub URL to avoid `//jupyter`.
// Query values are left unencoded to match the book-theme `launch.py` /
// nbgitpuller format that the production QuantEcon hubs already accept.
const base = hubBaseUrl.replace(/\/+$/, '');
return `${base}/jupyter/hub/user-redirect/git-pull?repo=https://github.com/${orgRepo}&branch=${branch}&urlpath=tree/${repoName}/${relPath}`;
}
2 changes: 1 addition & 1 deletion app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface TemplateOptions {
git_metadata?: GitMetadata;

// Notebook launcher configuration (set under `site.options` in myst.yml).
// Generalises the previously hardcoded Colab/JupyterHub launch URLs so
// Generalises the previously hardcoded Colab launch URLs so
// non-default branches, repo naming and nested lecture dirs work. All keys
// are optional; the defaults reproduce the historical behaviour
// (`<github>.notebooks`, branch `main`, and no source/notebooks path
Expand Down
34 changes: 0 additions & 34 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@
"@myst-theme/styles": "^1.3.0",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-radio-group": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-tooltip": "^1.1.8",
"@radix-ui/react-visually-hidden": "^1.2.4",
Expand Down
Loading
Loading