diff --git a/package-lock.json b/package-lock.json index 786beb72..a544a3c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8361,6 +8361,34 @@ } } }, + "node_modules/@radix-ui/react-form": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.0.tgz", + "integrity": "sha512-1/oVYPDjbFILOLIarcGcMKo+y6SbTVT/iUKVEw59CF4offwZgBgC3ZOeSBewjqU0vdA6FWTPWTN63obj55S/tQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-label": "2.1.0", + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-hover-card": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.1.tgz", @@ -8392,6 +8420,14 @@ } } }, + "node_modules/@radix-ui/react-icons": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", + "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==", + "peerDependencies": { + "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/@radix-ui/react-id": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", @@ -8410,27 +8446,49 @@ } } }, - "node_modules/@radix-ui/react-popover": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.1.tgz", - "integrity": "sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==", + "node_modules/@radix-ui/react-label": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.0.tgz", + "integrity": "sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==", "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.2.tgz", + "integrity": "sha512-u2HRUyWW+lOiA2g0Le0tMmT55FGOEWHwPFt1EPfbLly7uXQExFo5duNKqG2DzmFXIdqOeNd+TpE8baHWJCyP9w==", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.0", - "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.1", + "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.0", - "@radix-ui/react-portal": "1.1.1", - "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-portal": "1.1.2", + "@radix-ui/react-presence": "1.1.1", "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-slot": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.7" + "react-remove-scroll": "2.6.0" }, "peerDependencies": { "@types/react": "*", @@ -8447,22 +8505,30 @@ } } }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz", - "integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==", - "license": "MIT", + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz", + "integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==", "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.0", + "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0", - "@radix-ui/react-use-rect": "1.1.0", - "@radix-ui/react-use-size": "1.1.0", - "@radix-ui/rect": "1.1.0" + "@radix-ui/react-use-escape-keydown": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -8479,11 +8545,24 @@ } } }, - "node_modules/@radix-ui/react-portal": { + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-guards": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.1.tgz", - "integrity": "sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz", + "integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==", "dependencies": { "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-layout-effect": "1.1.0" @@ -8503,11 +8582,10 @@ } } }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz", - "integrity": "sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==", - "license": "MIT", + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-presence": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz", + "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==", "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" @@ -8527,13 +8605,46 @@ } } }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", - "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "node_modules/@radix-ui/react-popover/node_modules/react-remove-scroll": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz", + "integrity": "sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.6", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz", + "integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.1.0" + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -8550,21 +8661,14 @@ } } }, - "node_modules/@radix-ui/react-radio-group": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.2.0.tgz", - "integrity": "sha512-yv+oiLaicYMBpqgfpSPw6q+RyXlLdIpQWDHZbUKURxe+nEh53hFXPPlfhfQQtYkS5MMK/5IWIa76SksleQZSzw==", + "node_modules/@radix-ui/react-portal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.1.tgz", + "integrity": "sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==", + "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-presence": "1.1.0", "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-roving-focus": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-previous": "1.1.0", - "@radix-ui/react-use-size": "1.1.0" + "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -8581,31 +8685,50 @@ } } }, - "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-direction": { + "node_modules/@radix-ui/react-presence": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", - "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz", + "integrity": "sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, - "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-use-previous": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", - "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", + "node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@types/react-dom": { + "optional": true } } }, @@ -9216,6 +9339,86 @@ } } }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.1.tgz", + "integrity": "sha512-3GBUDmP2DvzmtYLMsHmpA1GtR46ZDZ+OreXM/N+kkQJOPIgytFWWTfDQmBQKBvaFS0Vno0FktdbVzN28KGrMdw==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-roving-focus": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-direction": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-presence": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz", + "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-toggle": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.0.tgz", @@ -41006,7 +41209,10 @@ "dependencies": { "@headlessui/react": "^1.7.15", "@heroicons/react": "^2.0.18", - "@radix-ui/react-popover": "^1.0.6", + "@radix-ui/react-form": "^0.1.0", + "@radix-ui/react-icons": "^1.3.2", + "@radix-ui/react-popover": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.1", "@scienceicons/react": "^0.0.6", "classnames": "^2.3.2", "myst-common": "*", @@ -41279,9 +41485,6 @@ "@myst-theme/search": "^0.13.4", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.3", - "@radix-ui/react-radio-group": "^1.2.0", - "@radix-ui/react-roving-focus": "^1.1.0", - "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-visually-hidden": "^1.1.0", "classnames": "^2.3.2", "lodash.throttle": "^4.1.1", diff --git a/packages/frontmatter/package.json b/packages/frontmatter/package.json index feff7f5f..e13e719c 100644 --- a/packages/frontmatter/package.json +++ b/packages/frontmatter/package.json @@ -22,7 +22,10 @@ "dependencies": { "@headlessui/react": "^1.7.15", "@heroicons/react": "^2.0.18", - "@radix-ui/react-popover": "^1.0.6", + "@radix-ui/react-form": "^0.1.0", + "@radix-ui/react-icons": "^1.3.2", + "@radix-ui/react-popover": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.1", "@scienceicons/react": "^0.0.6", "classnames": "^2.3.2", "myst-common": "*", diff --git a/packages/frontmatter/src/FrontmatterBlock.tsx b/packages/frontmatter/src/FrontmatterBlock.tsx index 72f3ff4f..02efd1cd 100644 --- a/packages/frontmatter/src/FrontmatterBlock.tsx +++ b/packages/frontmatter/src/FrontmatterBlock.tsx @@ -6,6 +6,8 @@ import { JupyterIcon, OpenAccessIcon, GithubIcon, TwitterIcon } from '@scienceic import { LicenseBadges } from './licenses.js'; import { DownloadsDropdown } from './downloads.js'; import { AuthorAndAffiliations, AuthorsList } from './Authors.js'; +import { LaunchButton } from './LaunchButton.js'; +import type { BinderLaunchProps, JupyterHubLaunchProps } from './LaunchButton.js'; function ExternalOrInternalLink({ to, @@ -187,6 +189,8 @@ export function Journal({ ); } +export type LaunchOptions = BinderLaunchProps | JupyterHubLaunchProps; + export function FrontmatterBlock({ frontmatter, kind = SourceFileKind.Article, @@ -194,6 +198,7 @@ export function FrontmatterBlock({ hideBadges, hideExports, className, + launchOptions, }: { frontmatter: Omit; kind?: SourceFileKind; @@ -201,6 +206,7 @@ export function FrontmatterBlock({ hideBadges?: boolean; hideExports?: boolean; className?: string; + launchOptions?: LaunchOptions; }) { if (!frontmatter) return null; const { @@ -226,6 +232,8 @@ export function FrontmatterBlock({ const hasHeaders = !!subject || !!venue || !!volume || !!issue; const hasDateOrDoi = !!doi || !!date; const showHeaderBlock = hasHeaders || (hasBadges && !hideBadges) || (hasExports && !hideExports); + const hideLaunch: boolean = false; + if (!title && !subtitle && !showHeaderBlock && !hasAuthors && !hasDateOrDoi) { // Nothing to show! return null; @@ -267,6 +275,7 @@ export function FrontmatterBlock({ )} {!hideExports && } + {!hideLaunch && launchOptions && } )} {title &&

{title}

} diff --git a/packages/frontmatter/src/LaunchButton.tsx b/packages/frontmatter/src/LaunchButton.tsx new file mode 100644 index 00000000..f7511a18 --- /dev/null +++ b/packages/frontmatter/src/LaunchButton.tsx @@ -0,0 +1,382 @@ +import React, { useRef, useCallback, useState } from 'react'; +import classNames from 'classnames'; + +import * as Popover from '@radix-ui/react-popover'; +import { RocketIcon, Cross2Icon, ClipboardCopyIcon, ExternalLinkIcon } from '@radix-ui/react-icons'; +import * as Tabs from '@radix-ui/react-tabs'; +import * as Form from '@radix-ui/react-form'; + +export type CommonLaunchProps = { + repo: string; + location: string; + ref?: string; + onLaunch?: () => void; +}; + +export type JupyterHubLaunchProps = CommonLaunchProps & { + jupyterhub?: string; +}; + +export type BinderLaunchProps = CommonLaunchProps & { + binder?: string; +}; + +const GITHUB_PATTERN = /https:\/\/github.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)/; + +type GitResource = { + // Provider + provider: 'github'; + // Per-provider info + org: string; + repo: string; +}; + +/** + * Parse a Git source URL into a Git "resource" consisting of a provider and provider info + * + * @param git - git URL + */ +function parseKnownGitProvider(git: string): GitResource | undefined { + let match; + if ((match = git.match(GITHUB_PATTERN))) { + return { + provider: 'github', + org: match[1], + repo: match[2], + }; + } + return undefined; +} + +/** + * Ensure URL of for http://foo.com/bar?baz + * has the form http://foo.com/bar/ + * + * @param url - URL to parse + */ +function ensureBasename(url: string): URL { + // Parse input URL (or fallback) + const parsedURL = new URL(url); + // Drop any fragments + let baseURL = `${parsedURL.origin}${parsedURL.pathname}`; + // Ensure a trailing fragment + if (!baseURL.endsWith('/')) { + baseURL = `${baseURL}/`; + } + return new URL(baseURL); +} + +/** + * Equivalent to Python's `urllib.parse.urlencode` function + * + * @param params - mapping from parameter name to string value + */ +function encodeURLParams(params: Record): string { + return Object.entries(params) + .filter(([key, value]) => value !== undefined) + .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value as string)}`) + .join('&'); +} + +type CopyButtonProps = { + defaultMessage: string; + alternateMessage?: string; + timeout?: number; + buildLink: () => string | undefined; + className?: string; +}; + +function CopyButton(props: CopyButtonProps) { + const { className, defaultMessage, alternateMessage, buildLink, timeout } = props; + const [message, setMessage] = useState(defaultMessage); + + const copyLink = useCallback(() => { + // Build the link for the clipboard + const link = props.buildLink(); + // In secure links, we can copy it! + if (window.isSecureContext) { + // Write to clipboard + window.navigator.clipboard.writeText(link ?? ''); + // Update UI + setMessage(alternateMessage ?? defaultMessage); + + // Set callback to restore message + setTimeout(() => { + setMessage(defaultMessage); + }, timeout ?? 1000); + } + }, [defaultMessage, alternateMessage, buildLink, timeout, setMessage]); + + return ( + + ); +} + +function BinderLaunchContent(props: BinderLaunchProps) { + const { onLaunch } = props; + const defaultBinderBaseURL = props.binder ?? 'https://mybinder.org'; + + // Determine Git ref + const refComponent = encodeURIComponent(props.ref ?? 'HEAD'); + + // Build binder URL path + const query = encodeURLParams({ urlpath: `/lab/tree/${props.location}` }); + + // Parse the repo, assume it is a validated GitHub URL + let gitComponent: string; + const resource = parseKnownGitProvider(props.repo); + switch (resource?.provider) { + case 'github': { + gitComponent = `gh/${resource.org}/${resource.repo}`; + break; + } + default: { + const escapedURL = encodeURIComponent(props.repo); + gitComponent = `git/${escapedURL}`; + } + } + + const formRef = useRef(null); + const buildLink = useCallback(() => { + const form = formRef.current; + if (!form) { + return; + } + + const data = Object.fromEntries(new FormData(form) as any); + const binderURL = ensureBasename(data.url || defaultBinderBaseURL); + binderURL.pathname = `${binderURL.pathname}v2/${gitComponent}/${refComponent}`; + binderURL.search = `?${query}`; + return binderURL.toString(); + }, [formRef, gitComponent, refComponent, query]); + + // FIXME: use ValidityState from radix-ui once passing-by-name is fixed + const urlRef = useRef(null); + const buildValidLink = useCallback(() => { + if (urlRef.current?.dataset.invalid === 'true') { + return; + } else { + return buildLink(); + } + }, [buildLink, urlRef]); + + const handleSubmit = useCallback( + (event: React.SyntheticEvent) => { + event.preventDefault(); + + const link = buildLink(); + + // Link should exist, but guard anyway + if (link) { + window?.open(link, '_blank')?.focus(); + } + onLaunch?.(); + }, + [defaultBinderBaseURL, buildLink, onLaunch], + ); + return ( + +

+ Launch on a BinderHub e.g. mybinder.org +

+ +
+ Binder URL + + Please provide a valid URL + +
+ + + +
+
+ + + + +
+
+ ); +} + +function JupyterHubLaunchContent(props: JupyterHubLaunchProps) { + const { onLaunch } = props; + const defaultHubBaseURL = props.jupyterhub ?? ''; + + const resource = parseKnownGitProvider(props.repo); + + let urlPath = 'lab/tree'; + switch (resource?.provider) { + case 'github': { + urlPath = `${urlPath}/${resource.repo}${props.location}`; + } + } + + // Encode query for nbgitpuller + const query = encodeURLParams({ + repo: props.repo, + urlpath: urlPath, + branch: props.ref, + }); + + const formRef = useRef(null); + const buildLink = useCallback(() => { + const form = formRef.current; + if (!form) { + return; + } + + const data = Object.fromEntries(new FormData(form) as any); + const rawHubBaseURL = data.url; + if (!rawHubBaseURL) { + return; + } + const hubURL = ensureBasename(rawHubBaseURL); + hubURL.pathname = `${hubURL.pathname}hub/user-redirect/git-pull`; + hubURL.search = `?${query}`; + return hubURL.toString(); + }, [formRef, query]); + + // FIXME: use ValidityState from radix-ui once passing-by-name is fixed + const urlRef = useRef(null); + const buildValidLink = useCallback(() => { + if (urlRef.current?.dataset.invalid === 'true') { + return; + } else { + return buildLink(); + } + }, [buildLink, urlRef]); + + const handleSubmit = useCallback( + (event: React.SyntheticEvent) => { + event.preventDefault(); + + const link = buildLink(); + + // Link should exist, but guard anyway + if (link) { + window?.open(link, '_blank')?.focus(); + } + onLaunch?.(); + }, + [defaultHubBaseURL, buildLink, onLaunch], + ); + + return ( + +

Launch on a JupyterHub

+ +
+ JupyterHub URL + + Please enter a URL + + + + Please provide a valid URL + +
+ + + +
+ +
+ + + + +
+
+ ); +} + +export function LaunchButton(props: BinderLaunchProps | JupyterHubLaunchProps) { + const closeRef = useRef(null); + const closePopover = useCallback(() => { + closeRef.current?.click?.(); + }, []); + return ( + + + + + + + + + + Binder + + + JupyterHub + + + + + + + + + + + + + + + + + ); +} diff --git a/packages/site/package.json b/packages/site/package.json index f214e43a..12685192 100644 --- a/packages/site/package.json +++ b/packages/site/package.json @@ -29,9 +29,6 @@ "@myst-theme/search": "^0.13.4", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.3", - "@radix-ui/react-radio-group": "^1.2.0", - "@radix-ui/react-roving-focus": "^1.1.0", - "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-visually-hidden": "^1.1.0", "classnames": "^2.3.2", "lodash.throttle": "^4.1.1", diff --git a/packages/site/src/pages/Article.tsx b/packages/site/src/pages/Article.tsx index af031946..29781db3 100644 --- a/packages/site/src/pages/Article.tsx +++ b/packages/site/src/pages/Article.tsx @@ -43,6 +43,19 @@ export const ArticlePage = React.memo(function ({ const tree = copyNode(article.mdast); const keywords = article.frontmatter?.keywords ?? []; const parts = extractKnownParts(tree, article.frontmatter?.parts); + const launchOptions = React.useMemo(() => { + if (!manifest) { + return undefined; + } + const repo = manifest.thebe?.binder?.repo ?? manifest.github; + if (!repo) { + return undefined; + } + const binder = manifest.thebe?.binder?.url; + const location = article.location; + const ref = manifest.thebe?.binder?.ref; + return { repo, binder, location, ref }; + }, [manifest, article.location]); return ( )} diff --git a/themes/article/app/components/Article.tsx b/themes/article/app/components/Article.tsx index b4c2eeb1..e649693c 100644 --- a/themes/article/app/components/Article.tsx +++ b/themes/article/app/components/Article.tsx @@ -9,9 +9,15 @@ import { extractKnownParts, Footnotes, } from '@myst-theme/site'; +import React from 'react'; import { ErrorTray, NotebookToolbar, useComputeOptions } from '@myst-theme/jupyter'; import { FrontmatterBlock } from '@myst-theme/frontmatter'; -import { ReferencesProvider, useThemeTop, useMediaQuery } from '@myst-theme/providers'; +import { + ReferencesProvider, + useThemeTop, + useMediaQuery, + useProjectManifest, +} from '@myst-theme/providers'; import type { GenericParent } from 'myst-common'; import { copyNode } from 'myst-common'; import { BusyScopeProvider, ConnectionStatusTray, ExecuteScopeProvider } from '@myst-theme/jupyter'; @@ -32,6 +38,7 @@ export function Article({ hideTitle?: boolean; outlineMaxDepth?: number; }) { + const manifest = useProjectManifest(); const keywords = article.frontmatter?.keywords ?? []; const tree = copyNode(article.mdast); const parts = extractKnownParts(tree, article.frontmatter?.parts); @@ -39,6 +46,19 @@ export function Article({ const compute = useComputeOptions(); const top = useThemeTop(); const isOutlineMargin = useMediaQuery('(min-width: 1024px)'); + const launchOptions = React.useMemo(() => { + if (!manifest) { + return undefined; + } + const repo = manifest.thebe?.binder?.repo ?? manifest.github; + if (!repo) { + return undefined; + } + const binder = manifest.thebe?.binder?.url; + const location = article.location; + const ref = manifest.thebe?.binder?.ref; + return { repo, binder, location, ref }; + }, [manifest, article.location]); return ( - {!hideTitle && } + {!hideTitle && ( + + )} {!hideOutline && (
{ + if (!manifest) { + return undefined; + } + const repo = manifest.thebe?.binder?.repo ?? manifest.github; + if (!repo) { + return undefined; + } + const binder = manifest.thebe?.binder?.url; + const location = article.location; + const ref = manifest.thebe?.binder?.ref; + return { repo, binder, location, ref }; + }, [manifest, article.location]); + return ( )} {!hide_outline && (